I am working on an audio visualizer for the web that also lets the user "tune" the raw audio signal visualizer to a frequency. This is a feature of many hardware oscilloscopes. Basically, when a user centers on 440Hz and I have a 440Hz sine wave, the wave should stay still on the canvas and not move left or right. My plan was to move the graph to the left according to the frequency (440Hz = 1/440s to the left per second because the wave should repeat every 1/440s), but this does not work as it seems.
I could not find the units used by the Audio Analyzer Node's time domain data. I guess that it's in milliseconds, but I am not certain.
"use strict";
// Oscillator instead of mic for debugging
const USE_OSCILLATOR = true;
// Compatibility
if (!window.AudioContext)
window.AudioContext = window.webkitAudioContext;
if (!navigator.getUserMedia)
navigator.getUserMedia =
navigator.mozGetUserMedia ||
navigator.webkitGetUserMedia ||
navigator.msGetUserMedia;
// Main
class App {
constructor(visualizerElement, optionsElement) {
this.visualizerElement = visualizerElement;
this.optionsElement = optionsElement;
// HTML elements
this.canvas = document.createElement("canvas");
// Context
this.context = new AudioContext({
// Low latency
latencyHint: "interactive",
});
this.canvasCtx = this.canvas.getContext("2d", {
// Low latency
desynchronized: true,
alpha: false,
});
// Audio nodes
this.audioAnalyser = this.context.createAnalyser();
this.audioBuffer = new Uint8Array(this.audioAnalyser.frequencyBinCount);
this.audioInputStream = null;
this.audioInputNode = null;
if (this.canvasCtx === null)
throw new Error("2D rendering Context not supported by browser.");
this.updateCanvasSize();
window.addEventListener("resize", () => this.updateCanvasSize());
this.drawVisualizer();
this.visualizerElement.appendChild(this.canvas);
if (USE_OSCILLATOR) {
let oscillator = this.context.createOscillator();
oscillator.type = "sine";
oscillator.frequency.setValueAtTime(440, this.context.currentTime);
oscillator.connect(this.audioAnalyser);
oscillator.start();
}
else {
navigator.getUserMedia({ audio: true }, (stream) => {
this.audioInputStream = stream;
this.audioInputNode = this.context.createMediaStreamSource(stream);
this.audioInputNode.channelCountMode = "explicit";
this.audioInputNode.channelCount = 1;
this.audioBuffer = new Uint8Array(this.audioAnalyser.frequencyBinCount);
this.audioInputNode.connect(this.audioAnalyser);
}, (err) => console.error(err));
}
}
updateCanvasSize() {
var _a;
this.canvas.width = window.innerWidth;
this.canvas.height = window.innerHeight;
(_a = this.canvasCtx) === null || _a === void 0 ? void 0 : _a.setTransform(1, 0, 0, -1, 0, this.canvas.height * 0.5);
}
drawVisualizer() {
if (this.canvasCtx === null)
return;
const ctx = this.canvasCtx;
ctx.fillStyle = "black";
ctx.fillRect(0, -0.5 * this.canvas.height, this.canvas.width, this.canvas.height);
// Draw FFT
this.audioAnalyser.getByteFrequencyData(this.audioBuffer);
const step = this.canvas.width / this.audioBuffer.length;
const scale = this.canvas.height / (2 * 255);
ctx.beginPath();
ctx.moveTo(-step, this.audioBuffer[0] * scale);
this.audioBuffer.forEach((sample, index) => {
ctx.lineTo(index * step, scale * sample);
});
ctx.strokeStyle = "white";
ctx.stroke();
// Get the highest dominant frequency
let highestFreqHalfHz = 0;
{
/**
* Highest frequency in 0.5Hz
*/
let highestFreq = NaN;
let highestFreqAmp = NaN;
let remSteps = NaN;
for (let i = this.audioBuffer.length - 1; i >= 0; i--) {
const sample = this.audioBuffer[i];
if (sample > 20 && (isNaN(highestFreqAmp) || sample > highestFreqAmp)) {
highestFreq = i;
highestFreqAmp = sample;
if (isNaN(remSteps))
remSteps = 500;
}
if (!isNaN(remSteps)) {
if (remSteps-- < 0)
break;
}
}
if (!isNaN(highestFreq)) {
ctx.beginPath();
ctx.moveTo(highestFreq * step, 0);
ctx.lineTo(highestFreq * step, scale * 255);
ctx.strokeStyle = "green";
ctx.stroke();
highestFreqHalfHz = highestFreq;
}
}
// Draw Audio
this.audioAnalyser.getByteTimeDomainData(this.audioBuffer);
{
const bufferSize = this.audioBuffer.length;
const offsetY = -this.canvas.height * 0.5;
// I don't know what I am doing here:
const offsetX = highestFreqHalfHz == 0
? 0
: bufferSize -
Math.round(((this.context.currentTime * 1000) % (1 / 440)) % bufferSize);
// Draw the audio graph with the given offset
ctx.beginPath();
ctx.moveTo(-step, this.audioBuffer[0] * scale + offsetY);
for (let i = 0; i < bufferSize; i++) {
const index = (offsetX + i) % bufferSize;
const sample = this.audioBuffer[index];
ctx.lineTo(i * step, scale * sample + offsetY);
}
ctx.strokeStyle = "white";
ctx.stroke();
}
}
}
window.addEventListener("load", () => {
const app = new App(document.getElementById("visualizer"), document.getElementById("options"));
requestAnimationFrame(draw);
function draw() {
requestAnimationFrame(draw);
app.drawVisualizer();
}
});
html {
background: black;
}
body {
width: 100vw;
height: 100vh;
margin: 0;
overflow: hidden;
}
#visualizer {
position: fixed;
inset: 0;
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Equalizer</title>
</head>
<body>
<div id="visualizer"></div>
<div id="options"></div>
</body>
</html>
The above snippet was generated from TypeScript. You can find the source here. If it worked as intended, the oscillating graph (bottom) would not be moving.
I was able to solve this problem thanks to Raymond Toy's comment and my maths teacher (thank you Mr. Klein). The solution was Math.round((this.context.currentTime % iv) * sampleRate) where iv is the interval of the frequency (1/Hz). The wave is not perfectly centered. The FFT approximation is not very accurate though. In the following example I forced the detected frequency to be the specified one.
"use strict";
// Oscillator instead of mic for debugging
const USE_OSCILLATOR = true;
const OSCILLATOR_HZ = 1000;
// Compatibility
if (!window.AudioContext)
window.AudioContext = window.webkitAudioContext;
if (!navigator.getUserMedia)
navigator.getUserMedia =
navigator.mozGetUserMedia ||
navigator.webkitGetUserMedia ||
navigator.msGetUserMedia;
// Main
class App {
constructor(visualizerElement, optionsElement) {
this.visualizerElement = visualizerElement;
this.optionsElement = optionsElement;
// HTML elements
this.canvas = document.createElement("canvas");
// Context
this.context = new AudioContext({
// Low latency
latencyHint: "interactive",
});
this.canvasCtx = this.canvas.getContext("2d", {
// Low latency
desynchronized: true,
alpha: false,
});
// Audio nodes
this.audioAnalyser = this.context.createAnalyser();
this.audioBuffer = new Uint8Array(0);
this.audioInputStream = null;
this.audioInputNode = null;
if (this.canvasCtx === null)
throw new Error("2D rendering Context not supported by browser.");
this.updateCanvasSize();
window.addEventListener("resize", () => this.updateCanvasSize());
this.drawVisualizer();
this.visualizerElement.appendChild(this.canvas);
this.audioAnalyser.fftSize = 2048;
this.audioAnalyser.maxDecibels = -10;
this.audioBuffer = new Uint8Array(this.audioAnalyser.frequencyBinCount * 2);
this.audioFilter = this.context.createBiquadFilter();
this.audioFilter.type = "bandpass";
this.audioFilter.frequency.value = 900;
this.audioFilter.Q.value = 20;
this.audioAmplifier = this.context.createGain();
this.audioAmplifier.gain.value = 5;
this.audioFilter.connect(this.audioAmplifier);
this.audioAmplifier.connect(this.audioAnalyser);
if (USE_OSCILLATOR) {
let oscillator = this.context.createOscillator();
oscillator.type = "sine";
oscillator.frequency.setValueAtTime(OSCILLATOR_HZ, this.context.currentTime);
oscillator.connect(this.audioFilter);
oscillator.start();
}
else {
navigator.getUserMedia({ audio: true }, (stream) => {
this.audioInputStream = stream;
this.audioInputNode = this.context.createMediaStreamSource(stream);
this.audioInputNode.channelCountMode = "explicit";
this.audioInputNode.channelCount = 1;
this.audioBuffer = new Uint8Array(this.audioAnalyser.frequencyBinCount);
this.audioInputNode.connect(this.audioFilter);
}, (err) => console.error(err));
}
}
updateCanvasSize() {
var _a;
this.canvas.width = window.innerWidth;
this.canvas.height = window.innerHeight;
(_a = this.canvasCtx) === null || _a === void 0 ? void 0 : _a.setTransform(1, 0, 0, -1, 0, this.canvas.height * 0.5);
}
drawVisualizer() {
if (this.canvasCtx === null)
return;
const ctx = this.canvasCtx;
ctx.globalAlpha = 0.5;
ctx.fillStyle = "black";
ctx.fillRect(0, -0.5 * this.canvas.height, this.canvas.width, this.canvas.height);
ctx.globalAlpha = 1;
// Draw FFT
this.audioAnalyser.getByteFrequencyData(this.audioBuffer);
const scale = this.canvas.height / (2 * 255);
const { frequencyBinCount } = this.audioAnalyser;
const { sampleRate } = this.context;
{
const step = this.canvas.width / frequencyBinCount;
ctx.beginPath();
ctx.moveTo(-step, this.audioBuffer[0] * scale);
for (let index = 0; index < frequencyBinCount; index++) {
ctx.lineTo(index * step, scale * this.audioBuffer[index]);
}
ctx.strokeStyle = "white";
ctx.stroke();
}
// Get the highest dominant frequency
const step = this.canvas.width / frequencyBinCount;
let highestFreqHz = 0;
{
/**
* Highest frequency index in the buffer
*/
let highestFreqIndex = NaN;
let highestFreqAmp = NaN;
let remSteps = NaN;
for (let i = frequencyBinCount - 1; i >= 0; i--) {
const sample = this.audioBuffer[i];
if (sample > 30) {
if (isNaN(highestFreqAmp)) {
highestFreqIndex = i;
highestFreqAmp = sample;
}
else {
if (sample > highestFreqAmp) {
highestFreqIndex = i;
highestFreqAmp = sample;
}
}
//if (isNaN(remSteps)) remSteps = 100;
}
if (!isNaN(remSteps)) {
if (remSteps-- < 0)
break;
}
}
if (!isNaN(highestFreqIndex)) {
// Force exact value: (not necessary)
highestFreqIndex =
(OSCILLATOR_HZ * (2 * frequencyBinCount)) / sampleRate;
ctx.beginPath();
ctx.moveTo(highestFreqIndex * step, 0);
ctx.lineTo(highestFreqIndex * step, scale * 255);
ctx.strokeStyle = "green";
ctx.stroke();
highestFreqHz =
(highestFreqIndex * sampleRate) / (2 * frequencyBinCount);
window.HZ = highestFreqHz;
}
}
// Draw Audio
this.audioAnalyser.getByteTimeDomainData(this.audioBuffer);
{
const iv = highestFreqHz == 0 ? 0 : 1 / highestFreqHz;
const bufferSize = this.audioBuffer.length;
const offsetY = -this.canvas.height / 2.4;
const startIndex = Math.round(iv * sampleRate);
const step = this.canvas.width / (this.audioBuffer.length - startIndex);
const scale = this.canvas.height / (3 * 255);
const offsetX = highestFreqHz == 0
? 0
: Math.round((this.context.currentTime % iv) * sampleRate) %
bufferSize;
// Draw the audio graph with the given offset
ctx.beginPath();
ctx.moveTo(-step, this.audioBuffer[startIndex - offsetX] * scale + offsetY);
for (let i = startIndex; i < bufferSize; i += 4) {
const index = (i - offsetX) % bufferSize;
const sample = this.audioBuffer[index];
ctx.lineTo((i - startIndex) * step, scale * sample + offsetY);
}
ctx.strokeStyle = "white";
ctx.stroke();
}
}
}
window.addEventListener("load", () => {
const app = new App(document.getElementById("visualizer"), document.getElementById("options"));
requestAnimationFrame(draw);
function draw() {
requestAnimationFrame(draw);
app.drawVisualizer();
}
});
html {
background: black;
}
body {
width: 100vw;
height: 100vh;
margin: 0;
overflow: hidden;
}
#visualizer {
position: fixed;
inset: 0;
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Equalizer</title>
</head>
<body>
<div id="visualizer"></div>
<div id="options"></div>
</body>
</html>
Related
I am trying to make a React app for recording voice samples and save them as wav files, I used recorder-js to make the record but I am still struggling to make the saving part as wav file, and I got the exporWAV is not a function everytime.
If you've been there please help
Sample code is here
//create
var record_recorder;
record_recorder = new RecorderV2(input,{sampleRate:44100, numChannels:2});
//start
record_recorder.record();
//stop
record_recorder.stop();
//export
record_recorder.exportWAV(function(blob) {
var url = URL.createObjectURL(blob);
var li = document.createElement('div');
var au = document.createElement('audio');
var hf = document.createElement('a');
var br = document.createElement('br');
record_result_blob = blob;
au.controls = true;
au.src = url;
hf.href = url;
hf.download = 'record.wav';
hf.innerHTML = hf.download;
li.appendChild(au);
li.appendChild(br);
li.appendChild(hf);
if (result)
result(li);
});
See the example website:
https://voice-recorder-online.com
code:
https://voice-recorder-online.com/js/index_editor.js
Here's How u can make waves depending on your voice frequency and record and save files in wav format.
(async() => {
let leftchannel = [];
let rightchannel = [];
let recorder = null;
let recording = false;
let recordingLength = 0;
let volume = null;
let audioInput = null;
let sampleRate = null;
let AudioContext = window.AudioContext || window.webkitAudioContext;
let context = null;
let analyser = null;
let canvas = document.querySelector("canvas");
let canvasCtx = canvas.getContext("2d");
let visualSelect = document.querySelector("#visSelect");
let micSelect = document.querySelector("#micSelect");
let stream = null;
let tested = false;
try {
window.stream = stream = await getStream();
console.log("Got stream");
} catch (err) {
alert("Issue getting mic", err);
}
const deviceInfos = await navigator.mediaDevices.enumerateDevices();
var mics = [];
for (let i = 0; i !== deviceInfos.length; ++i) {
let deviceInfo = deviceInfos[i];
if (deviceInfo.kind === "audioinput") {
mics.push(deviceInfo);
let label = deviceInfo.label || "Microphone " + mics.length;
console.log("Mic ", label + " " + deviceInfo.deviceId);
const option = document.createElement("option");
option.value = deviceInfo.deviceId;
option.text = label;
micSelect.appendChild(option);
}
}
function getStream(constraints) {
if (!constraints) {
constraints = {
audio: true,
video: false
};
}
return navigator.mediaDevices.getUserMedia(constraints);
}
setUpRecording();
function setUpRecording() {
context = new AudioContext();
sampleRate = context.sampleRate;
// creates a gain node
volume = context.createGain();
// creates an audio node from teh microphone incoming stream
audioInput = context.createMediaStreamSource(stream);
// Create analyser
analyser = context.createAnalyser();
// connect audio input to the analyser
audioInput.connect(analyser);
// connect analyser to the volume control
// analyser.connect(volume);
let bufferSize = 2048;
let recorder = context.createScriptProcessor(bufferSize, 2, 2);
// we connect the volume control to the processor
// volume.connect(recorder);
analyser.connect(recorder);
// finally connect the processor to the output
recorder.connect(context.destination);
recorder.onaudioprocess = function(e) {
// Check
if (!recording) return;
// Do something with the data, i.e Convert this to WAV
console.log("recording");
let left = e.inputBuffer.getChannelData(0);
let right = e.inputBuffer.getChannelData(1);
if (!tested) {
tested = true;
// if this reduces to 0 we are not getting any sound
if (!left.reduce((a, b) => a + b)) {
alert("There seems to be an issue with your Mic");
// clean up;
stop();
stream.getTracks().forEach(function(track) {
track.stop();
});
context.close();
}
}
// we clone the samples
leftchannel.push(new Float32Array(left));
rightchannel.push(new Float32Array(right));
recordingLength += bufferSize;
};
visualize();
}
function mergeBuffers(channelBuffer, recordingLength) {
let result = new Float32Array(recordingLength);
let offset = 0;
let lng = channelBuffer.length;
for (let i = 0; i < lng; i++) {
let buffer = channelBuffer[i];
result.set(buffer, offset);
offset += buffer.length;
}
return result;
}
function interleave(leftChannel, rightChannel) {
let length = leftChannel.length + rightChannel.length;
let result = new Float32Array(length);
let inputIndex = 0;
for (let index = 0; index < length;) {
result[index++] = leftChannel[inputIndex];
result[index++] = rightChannel[inputIndex];
inputIndex++;
}
return result;
}
function writeUTFBytes(view, offset, string) {
let lng = string.length;
for (let i = 0; i < lng; i++) {
view.setUint8(offset + i, string.charCodeAt(i));
}
}
function start() {
recording = true;
document.querySelector("#msg").style.visibility = "visible";
// reset the buffers for the new recording
leftchannel.length = rightchannel.length = 0;
recordingLength = 0;
console.log("context: ", !!context);
if (!context) setUpRecording();
}
function stop() {
console.log("Stop");
recording = false;
document.querySelector("#msg").style.visibility = "hidden";
// we flat the left and right channels down
let leftBuffer = mergeBuffers(leftchannel, recordingLength);
let rightBuffer = mergeBuffers(rightchannel, recordingLength);
// we interleave both channels together
let interleaved = interleave(leftBuffer, rightBuffer);
///////////// WAV Encode /////////////////
// from http://typedarray.org/from-microphone-to-wav-with-getusermedia-and-web-audio/
//
// we create our wav file
let buffer = new ArrayBuffer(44 + interleaved.length * 2);
let view = new DataView(buffer);
// RIFF chunk descriptor
writeUTFBytes(view, 0, "RIFF");
view.setUint32(4, 44 + interleaved.length * 2, true);
writeUTFBytes(view, 8, "WAVE");
// FMT sub-chunk
writeUTFBytes(view, 12, "fmt ");
view.setUint32(16, 16, true);
view.setUint16(20, 1, true);
// stereo (2 channels)
view.setUint16(22, 2, true);
view.setUint32(24, sampleRate, true);
view.setUint32(28, sampleRate * 4, true);
view.setUint16(32, 4, true);
view.setUint16(34, 16, true);
// data sub-chunk
writeUTFBytes(view, 36, "data");
view.setUint32(40, interleaved.length * 2, true);
// write the PCM samples
let lng = interleaved.length;
let index = 44;
let volume = 1;
for (let i = 0; i < lng; i++) {
view.setInt16(index, interleaved[i] * (0x7fff * volume), true);
index += 2;
}
// our final binary blob
const blob = new Blob([view], {
type: "audio/wav"
});
const audioUrl = URL.createObjectURL(blob);
console.log("BLOB ", blob);
console.log("URL ", audioUrl);
document.querySelector("#audio").setAttribute("src", audioUrl);
const link = document.querySelector("#download");
link.setAttribute("href", audioUrl);
link.download = "output.wav";
}
// Visualizer function from
// https://webaudiodemos.appspot.com/AudioRecorder/index.html
//
function visualize() {
WIDTH = canvas.width;
HEIGHT = canvas.height;
CENTERX = canvas.width / 2;
CENTERY = canvas.height / 2;
let visualSetting = visualSelect.value;
console.log(visualSetting);
if (!analyser) return;
if (visualSetting === "sinewave") {
analyser.fftSize = 2048;
var bufferLength = analyser.fftSize;
console.log(bufferLength);
var dataArray = new Uint8Array(bufferLength);
canvasCtx.clearRect(0, 0, WIDTH, HEIGHT);
var draw = function() {
drawVisual = requestAnimationFrame(draw);
analyser.getByteTimeDomainData(dataArray);
canvasCtx.fillStyle = "rgb(200, 200, 200)";
canvasCtx.fillRect(0, 0, WIDTH, HEIGHT);
canvasCtx.lineWidth = 2;
canvasCtx.strokeStyle = "rgb(0, 0, 0)";
canvasCtx.beginPath();
var sliceWidth = (WIDTH * 1.0) / bufferLength;
var x = 0;
for (var i = 0; i < bufferLength; i++) {
var v = dataArray[i] / 128.0;
var y = (v * HEIGHT) / 2;
if (i === 0) {
canvasCtx.moveTo(x, y);
} else {
canvasCtx.lineTo(x, y);
}
x += sliceWidth;
}
canvasCtx.lineTo(canvas.width, canvas.height / 2);
canvasCtx.stroke();
};
draw();
} else if (visualSetting == "frequencybars") {
analyser.fftSize = 64;
var bufferLengthAlt = analyser.frequencyBinCount;
console.log(bufferLengthAlt);
var dataArrayAlt = new Uint8Array(bufferLengthAlt);
canvasCtx.clearRect(0, 0, WIDTH, HEIGHT);
var drawAlt = function() {
drawVisual = requestAnimationFrame(drawAlt);
analyser.getByteFrequencyData(dataArrayAlt);
canvasCtx.fillStyle = "rgb(0, 0, 0)";
canvasCtx.fillRect(0, 0, WIDTH, HEIGHT);
var barWidth = WIDTH / bufferLengthAlt;
var barHeight;
var x = 0;
for (var i = 0; i < bufferLengthAlt; i++) {
barHeight = dataArrayAlt[i];
canvasCtx.fillStyle = "rgb(" + (barHeight + 100) + ",50,50)";
canvasCtx.fillRect(
x,
HEIGHT - barHeight / 2,
barWidth,
barHeight / 2
);
x += barWidth + 1;
}
};
drawAlt();
} else if (visualSetting == "circle") {
analyser.fftSize = 32;
let bufferLength = analyser.frequencyBinCount;
console.log(bufferLength);
let dataArray = new Uint8Array(bufferLength);
canvasCtx.clearRect(0, 0, WIDTH, HEIGHT);
let draw = () => {
drawVisual = requestAnimationFrame(draw);
analyser.getByteFrequencyData(dataArray);
canvasCtx.fillStyle = "rgb(0, 0, 0)";
canvasCtx.fillRect(0, 0, WIDTH, HEIGHT);
// let radius = dataArray.reduce((a,b) => a + b) / bufferLength;
let radius = dataArray[2] / 2;
if (radius < 20) radius = 20;
if (radius > 100) radius = 100;
// console.log('Radius ', radius)
canvasCtx.beginPath();
canvasCtx.arc(CENTERX, CENTERY, radius, 0, 2 * Math.PI, false);
// canvasCtx.fillStyle = 'rgb(50,50,' + (radius+100) +')';
// canvasCtx.fill();
canvasCtx.lineWidth = 6;
canvasCtx.strokeStyle = "rgb(50,50," + (radius + 100) + ")";
canvasCtx.stroke();
};
draw();
}
}
#msg {
visibility: hidden;
color: red;
font-weight: bold;
font-size: 22px;
font-family: Verdana;
}
button {
padding: 5px 10px;
border: 1px solid grey;
font-size: 18px;
background: white;
}
.audio-controls {
display: flex;
align-items: center;
padding-top: 20px;
justify-content: center;
}
.audio-controls button {
margin: 0px 5px;
}
canvas {
margin-top: 10px;
background-color: black;
}
select {
height: 25px;
margin: 0px 5px;
}
a {
margin-left: 20px;
}
.app {
text-align: center;
padding-top: 20px;
}
<div class="app">
<select name="" id="micSelect"></select>
<select id="visSelect">
<option value="frequencybars">Bar</option>
<option value="sinewave">Wave</option>
<option value="circle">Circle</option>
</select>
<a id="download">Download</a>
<div class="audio-controls">
<button id="record">Record</button>
<button id="stop">Stop</button>
<audio id="audio" controls></audio>
</div>
<div id="msg">Recording...</div>
<canvas width="500" height="300"></canvas>
<div>
Live Demo: https://codepen.io/furki911/pen/jOYpvMx
How can you detect the pitch of a live audio input in the browser?
The below code will get you 1,024 frequency values. However I don't know how to go from this to actual pitches (e.g. A#).
const audioContext = new window.AudioContext();
const analyser = audioContext.createAnalyser();
navigator.getUserMedia(
{ audio: true },
stream => {
audioContext.createMediaStreamSource(stream).connect(analyser);
const dataArray = new Uint8Array(analyser.frequencyBinCount);
analyser.getByteTimeDomainData(dataArray);
// Log the contents of the analyzer ever 500ms.
setInterval(() => {
console.log(dataArray.length);
}, 500);
},
err => console.log(err)
);
What you are currently accessing is the Time Domain Data, and can not be used to retrieve a note (which seems to be what you want).
What you'd want is the Frequency Domain, using AnalyserNode.get[XXX]FrequencyData, from which you could get which frequencies are louder or more quiet.
However, since most sound is made of harmonies, you can't retrieve what note was played from a microphone, add to this that we only have access to limited resolution, and not only will you be unable to retrieve a note from a microphone, but not even from a virtual oscillator either.
Below example has been made from this Q/A and examples from MDN;
const canvasCtx = canvas.getContext('2d');
const WIDTH = canvas.width = 500;
const HEIGHT = canvas.height = 150;
const audioCtx = new (window.AudioContext || window.webkitAudioContext);
const analyser = audioCtx.createAnalyser();
const nyquist = audioCtx.sampleRate / 2;
// highest precision
analyser.fftSize = 32768;
const bufferLength = analyser.frequencyBinCount;
const dataArray = new Uint8Array(bufferLength);
const osc = audioCtx.createOscillator();
osc.frequency.value = 400;
osc.connect(analyser);
osc.connect(audioCtx.destination);
range.oninput = e => {
osc.frequency.value = range.value;
};
if(!audioCtx.state || audioCtx.state === 'running') {
begin();
}
else {
log.textContent = 'click anywhere to begin';
onclick = e => {
onclick = null;
begin()
}
}
function begin() {
osc.start(0);
draw();
}
function draw() {
requestAnimationFrame(draw);
// get the Frequency Domain
analyser.getByteFrequencyData(dataArray);
canvasCtx.fillStyle = 'rgb(0, 0, 0)';
canvasCtx.fillRect(0, 0, WIDTH, HEIGHT);
const barWidth = (WIDTH / bufferLength) * 2.5;
let max_val = -Infinity;
let max_index = -1;
let x = 0;
for(let i = 0; i < bufferLength; i++) {
let barHeight = dataArray[i];
if(barHeight > max_val) {
max_val = barHeight;
max_index = i;
}
canvasCtx.fillStyle = 'rgb(' + (barHeight+100) + ',50,50)';
canvasCtx.fillRect(x,HEIGHT-barHeight/2,barWidth,barHeight/2);
x += barWidth;
}
log.textContent = `loudest freq: ${max_index * (nyquist / bufferLength)}
real value: ${range.value}`;
}
#log{display: inline-block; margin:0 12px}
#canvas{display: block;}
<input id="range" type="range" min="0" max="1000" value="400"><pre id="log"></pre>
<canvas id="canvas"></canvas>
This question already has an answer here:
CanvasContext2D drawImage() issue [onload and CORS]
(1 answer)
Closed 4 years ago.
I am trying to make a infinite scrolling game in javascript.
The following code works as expected (it doesn't work here because the images cannot be uploaded):
class Vec {
constructor(x = 0, y = 0) {
this.x = x;
this.y = y;
}
}
class Rect {
constructor(src) {
this.pos = new Vec;
this.vel = new Vec;
this.image = new Image();
this.image.src = src;
}
get left() {
return this.pos.x;
}
get right() {
return this.pos.x + this.image.width;
}
get top() {
return this.pos.y;
}
get bottom() {
return this.pos.y + this.image.height;
}
}
class Player extends Rect {
constructor() {
super("images/frog-still.png");
this.pos.x = 100;
this.pos.y = HEIGHT - GROUND_Y - this.image.height;
}
}
class Enemy extends Rect {
constructor() {
super("images/spike.png");
this.image.width = this.image.width / 4;
this.image.height = this.image.height / 4;
this.pos.x = WIDTH;
this.pos.y = HEIGHT - GROUND_Y - this.image.height;
}
}
// setup
const ctx = document.getElementById("canvas").getContext("2d");
let isOver = false;
const WIDTH = 600;
const HEIGHT = 400;
const GROUND_Y = 50;
const player = new Player;
let enemies = [];
const MAX_ENEMY = 3;
let isJumping = false;
const JUMP_STRENGTH = -450;
const GRAVITY = 25;
document.addEventListener("keydown", (e) => {
if (e.keyCode === 38 && isJumping === false) {
isJumping = true;
player.vel.y = JUMP_STRENGTH;
}
}, false);
let lastUpdate = 0;
let random = Math.floor((Math.random() * 2501) + 1000);
function update(dt, now) {
// update player
player.vel.y += GRAVITY;
player.pos.y += player.vel.y * dt;
if (player.bottom >= HEIGHT - GROUND_Y) {
isJumping = false;
player.pos.y = HEIGHT - GROUND_Y - player.image.height;
player.vel.y = 0;
}
// create enemy
if (now - lastUpdate > random) {
lastUpdate = now;
random = Math.floor((Math.random() * 2501) + 1000);
enemies.push(new Enemy);
}
for (let i = 0; i < enemies.length; i++) {
// update enemy
enemies[i].vel.x = -300 - (now / 500);
enemies[i].pos.x += enemies[i].vel.x * dt;
if (player.right > enemies[i].left && player.left < enemies[i].right && player.bottom > enemies[i].top)
isOver = true;
if (enemies[i].right < 0)
enemies.splice(i, 1);
}
}
function draw() {
// draw background
ctx.fillStyle = "#000";
ctx.fillRect(0, 0, WIDTH, HEIGHT);
// draw ground
ctx.fillStyle = "#0f0";
ctx.fillRect(0, HEIGHT - GROUND_Y, WIDTH, GROUND_Y);
// draw player
ctx.drawImage(player.image, player.pos.x, player.pos.y, player.image.width, player.image.height);
// draw enemy
ctx.fillStyle = "#f00";
for (let i = 0; i < enemies.length; i++) {
ctx.drawImage(enemies[i].image, enemies[i].pos.x, enemies[i].pos.y, enemies[i].image.width, enemies[i].image.height);
}
}
// game loop
const TIMESTEP = 1 / 60;
let accumulator = 0;
let lastRender = 0;
function loop(timestamp) {
accumulator += (timestamp - lastRender) / 1000;
lastRender = timestamp;
while (accumulator >= TIMESTEP) {
update(TIMESTEP, timestamp);
accumulator -= TIMESTEP;
}
draw();
if (!isOver)
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
<!DOCTYPE html>
<html>
<head>
<title>Jump_Over_It</title>
<link href="css/default.css" rel="stylesheet" />
</head>
<body>
<canvas id="canvas" width="600" height="400"></canvas>
<script src="js/main.js"></script>
</body>
</html>
However, when add
this.image.width = this.image.width / 2;
this.image.height = this.image.height / 2;
to the Player class (like I did with the Enemy class), the player disappears. If I do console.log(player.image), it says that the image width and the image height is equal to 0. Why is this happening?
How can I fix this problem?
I don't know why, but while the image is not rendered the width and height are 0. So, I created the method beforeRender() that is implemented by Player and Enemy. This method is invoked before the ctx.drawImage and update the graphics settings.
I know it is not the answer you are looking for, but it is my best.
class Vec {
constructor(x = 0, y = 0) {
this.x = x;
this.y = y;
}
}
class Rect {
constructor(src) {
this.pos = new Vec;
this.vel = new Vec;
this.image = new Image;
this.image.src = src;
this.rendered = false;
}
get left() {
return this.pos.x;
}
get right() {
return this.pos.x + this.image.width;
}
get top() {
return this.pos.y;
}
get bottom() {
return this.pos.y + this.image.height;
}
get render() {
if (!this.rendered) {
if (this.beforeRender()) {
this.rendered = true;
}
}
return this.image;
}
beforeRender() {}
}
class Player extends Rect {
constructor() {
super("https://cdn4.iconfinder.com/data/icons/spring-flat-colorful/2048/5683_-_Frog-256.png");
this.pos.x = 100;
this.pos.y = HEIGHT - GROUND_Y - this.image.height;
}
beforeRender() {
if (this.image.width && this.image.height) {
this.image.width /= 2;
this.image.height /= 2;
return true;
}
return false;
}
}
class Enemy extends Rect {
constructor() {
super("https://vignette.wikia.nocookie.net/scribblenauts/images/8/8d/Steel_Spike.png/revision/latest?cb=20130105173440");
this.image.width /= 4;
this.image.height /= 4;
this.pos.x = WIDTH;
this.pos.y = HEIGHT - GROUND_Y - this.image.height;
}
beforeRender() {
if (this.image.width && this.image.height) {
this.image.width /= 4;
this.image.height /= 4;
return true;
}
return false;
}
}
// setup
const ctx = document.getElementById("canvas").getContext("2d");
let isOver = false;
const WIDTH = 600;
const HEIGHT = 400;
const GROUND_Y = 50;
const player = new Player;
let enemies = [];
const MAX_ENEMY = 3;
let isJumping = false;
const JUMP_STRENGTH = -450;
const GRAVITY = 25;
document.addEventListener("keydown", (e) => {
if (e.keyCode === 38 && isJumping === false) {
isJumping = true;
player.vel.y = JUMP_STRENGTH;
}
}, false);
let lastUpdate = 0;
let random = Math.floor((Math.random() * 2501) + 1000);
function update(dt, now) {
// update player
player.vel.y += GRAVITY;
player.pos.y += player.vel.y * dt;
if (player.bottom >= HEIGHT - GROUND_Y) {
isJumping = false;
player.pos.y = HEIGHT - GROUND_Y - player.image.height;
player.vel.y = 0;
}
// create enemy
if (now - lastUpdate > random) {
lastUpdate = now;
random = Math.floor((Math.random() * 2501) + 1000);
enemies.push(new Enemy);
}
for (let i = 0; i < enemies.length; i++) {
// update enemy
enemies[i].vel.x = -300 - (now / 500);
enemies[i].pos.x += enemies[i].vel.x * dt;
if (player.right > enemies[i].left && player.left < enemies[i].right && player.bottom > enemies[i].top)
isOver = true;
if (enemies[i].right < 0)
enemies.splice(i, 1);
}
}
function draw() {
// draw background
ctx.fillStyle = "#000";
ctx.fillRect(0, 0, WIDTH, HEIGHT);
// draw ground
ctx.fillStyle = "#0f0";
ctx.fillRect(0, HEIGHT - GROUND_Y, WIDTH, GROUND_Y);
// draw player
ctx.drawImage(player.render, player.pos.x, player.pos.y, player.image.width, player.image.height);
// draw enemy
ctx.fillStyle = "#f00";
for (let i = 0; i < enemies.length; i++) {
ctx.drawImage(enemies[i].image, enemies[i].pos.x, enemies[i].pos.y, enemies[i].image.width, enemies[i].image.height);
}
}
// game loop
const TIMESTEP = 1 / 60;
let accumulator = 0;
let lastRender = 0;
requestAnimationFrame(loop);
function loop(timestamp) {
accumulator += (timestamp - lastRender) / 1000;
lastRender = timestamp;
while (accumulator >= TIMESTEP) {
update(TIMESTEP, timestamp);
accumulator -= TIMESTEP;
}
draw();
if (!isOver)
requestAnimationFrame(loop);
}
<!DOCTYPE html>
<html>
<head>
<title>Jump_Over_It</title>
<link href="css/default.css" rel="stylesheet" />
</head>
<body>
<canvas id="canvas" width="600" height="400"></canvas>
<script src="js/main.js"></script>
</body>
</html>
I have this working canvas javascript animation but i would like to use it multiple times, currently it's only possible to have one canvas element with the id "stars" and use that one. Could i perhaps add a class for them instead and get the elements class and loop or what would be my best solution for achieving this? I would like to make this work without repeating to much since i could end up using the animation on different pages.
// Settings
var particleCount = 40,
flareCount = 0,
motion = 0.05,
tilt = 0.05,
color = '#00FF7B',
particleSizeBase = 1,
particleSizeMultiplier = 0.5,
flareSizeBase = 100,
flareSizeMultiplier = 100,
lineWidth = 1,
linkChance = 75, // chance per frame of link, higher = smaller chance
linkLengthMin = 5, // min linked vertices
linkLengthMax = 7, // max linked vertices
linkOpacity = 0.25; // number between 0 & 1
linkFade = 90, // link fade-out frames
linkSpeed = 0, // distance a link travels in 1 frame
glareAngle = -60,
glareOpacityMultiplier = 0.4,
renderParticles = true,
renderParticleGlare = true,
renderFlares = false,
renderLinks = false,
renderMesh = false,
flicker = false,
flickerSmoothing = 15, // higher = smoother flicker
blurSize = 0,
orbitTilt = true,
randomMotion = true,
noiseLength = 1000,
noiseStrength = 3;
var canvas = document.getElementById('stars'),
context = canvas.getContext('2d'),
mouse = {
x: 0,
y: 0
},
m = {},
r = 0,
c = 1000, // multiplier for delaunay points, since floats too small can mess up the algorithm
n = 0,
nAngle = (Math.PI * 2) / noiseLength,
nRad = 100,
nScale = 0.5,
nPos = {
x: 0,
y: 0
},
points = [],
vertices = [],
triangles = [],
links = [],
particles = [],
flares = [];
function init() {
var i, j, k;
// requestAnimFrame polyfill
window.requestAnimFrame = (function() {
return window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
function(callback) {
window.setTimeout(callback, 1000 / 60);
};
})();
// Size canvas
resize();
mouse.x = canvas.clientWidth / 2;
mouse.y = canvas.clientHeight / 2;
// Create particle positions
for (i = 0; i < particleCount; i++) {
var p = new Particle();
particles.push(p);
points.push([p.x * c, p.y * c]);
}
vertices = Delaunay.triangulate(points);
var tri = [];
for (i = 0; i < vertices.length; i++) {
if (tri.length == 3) {
triangles.push(tri);
tri = [];
}
tri.push(vertices[i]);
}
// Tell all the particles who their neighbors are
for (i = 0; i < particles.length; i++) {
// Loop through all tirangles
for (j = 0; j < triangles.length; j++) {
// Check if this particle's index is in this triangle
k = triangles[j].indexOf(i);
// If it is, add its neighbors to the particles contacts list
if (k !== -1) {
triangles[j].forEach(function(value, index, array) {
if (value !== i && particles[i].neighbors.indexOf(value) == -1) {
particles[i].neighbors.push(value);
}
});
}
}
}
var fps = 15;
var now;
var then = Date.now();
var interval = 1000 / fps;
var delta;
// Animation loop
(function animloop() {
requestAnimFrame(animloop);
now = Date.now();
delta = now - then;
if (delta > interval) {
then = now - (delta % interval);
resize();
render();
}
})();
}
function render() {
if (randomMotion) {
n++;
if (n >= noiseLength) {
n = 0;
}
nPos = noisePoint(n);
}
if (renderParticles) {
// Render particles
for (var i = 0; i < particleCount; i++) {
particles[i].render();
}
}
}
function resize() {
canvas.width = window.innerWidth * (window.devicePixelRatio || 1);
canvas.height = canvas.width * (canvas.clientHeight / canvas.clientWidth);
}
// Particle class
var Particle = function() {
this.x = random(-0.1, 1.1, true);
this.y = random(-0.1, 1.1, true);
this.z = random(0, 4);
this.color = color;
this.opacity = random(0.1, 1, true);
this.flicker = 0;
this.neighbors = []; // placeholder for neighbors
};
Particle.prototype.render = function() {
var pos = position(this.x, this.y, this.z),
r = ((this.z * particleSizeMultiplier) + particleSizeBase) * (sizeRatio() / 1000),
o = this.opacity;
context.fillStyle = this.color;
context.globalAlpha = o;
context.beginPath();
context.fill();
context.closePath();
if (renderParticleGlare) {
context.globalAlpha = o * glareOpacityMultiplier;
context.ellipse(pos.x, pos.y, r * 100, r, (glareAngle - ((nPos.x - 0.5) * noiseStrength * motion)) * (Math.PI / 180), 0, 2 * Math.PI, false);
context.fill();
context.closePath();
}
context.globalAlpha = 1;
};
// Flare class
// Link class
var Link = function(startVertex, numPoints) {
this.length = numPoints;
this.verts = [startVertex];
this.stage = 0;
this.linked = [startVertex];
this.distances = [];
this.traveled = 0;
this.fade = 0;
this.finished = false;
};
// Utils
function noisePoint(i) {
var a = nAngle * i,
cosA = Math.cos(a),
sinA = Math.sin(a),
rad = nRad;
return {
x: rad * cosA,
y: rad * sinA
};
}
function position(x, y, z) {
return {
x: (x * canvas.width) + ((((canvas.width / 2) - mouse.x + ((nPos.x - 0.5) * noiseStrength)) * z) * motion),
y: (y * canvas.height) + ((((canvas.height / 2) - mouse.y + ((nPos.y - 0.5) * noiseStrength)) * z) * motion)
};
}
function sizeRatio() {
return canvas.width >= canvas.height ? canvas.width : canvas.height;
}
function random(min, max, float) {
return float ?
Math.random() * (max - min) + min :
Math.floor(Math.random() * (max - min + 1)) + min;
}
// init
if (canvas) init();
html,
body {
margin: 0;
padding: 0;
height: 100%;
}
body {
background: #000;
background-image: linear-gradient(-180deg, rgba(0, 0, 0, 0.00) 0%, #000000 100%);
}
#stars {
display: block;
position: relative;
width: 100%;
height: 100%;
z-index: 1;
position: absolute;
}
<script src="https://rawgit.com/ironwallaby/delaunay/master/delaunay.js"></script>
<script src="http://requirejs.org/docs/release/2.1.15/minified/require.js"></script>
<canvas id="stars" width="300" height="300"></canvas>
id="identifier" is unique
class="identifier" could be shared by a list of items
As you mention using class could be an option but you'll need to change your code to select all elements by that class before:
$(".identifier").each(function(a,b)
{
// Actions for each element
}
With javascript:
var elementList = document.getElementsByClassName("identifier");
var elementListSize=elementList.length;
for(var i=0;i<elementListSize;i++) {
// Actions for each element (elementList[i])
}
I'm trying to get this particle explosion working. It's working but it looks like some frames does not get rendered. If I click many times to call several explosions it starts to uhm.. "lag/stutter". Is there something I have forgotten to do? It may look like the browser hangs when I click many times. Is it too much to have 2 for loops inside each other?
Attached my code so you can see.
Just try to click many times, and you will see the problem visually.
// Request animation frame
var requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || window.msRequestAnimationFrame;
// Canvas
var c = document.getElementById('canvas');
var ctx = c.getContext('2d');
// Set full-screen
c.width = window.innerWidth;
c.height = window.innerHeight;
// Options
var background = '#333'; // Background color
var particlesPerExplosion = 20;
var particlesMinSpeed = 3;
var particlesMaxSpeed = 6;
var particlesMinSize = 1;
var particlesMaxSize = 3;
var explosions = [];
var fps = 60;
var now, delta;
var then = Date.now();
var interval = 1000 / fps;
// Optimization for mobile devices
if (/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)) {
fps = 29;
}
// Draw
function draw() {
// Loop
requestAnimationFrame(draw);
// Set NOW and DELTA
now = Date.now();
delta = now - then;
// New frame
if (delta > interval) {
// Update THEN
then = now - (delta % interval);
// Our animation
drawBackground();
drawExplosion();
}
}
// Draw explosion(s)
function drawExplosion() {
if (explosions.length == 0) {
return;
}
for (var i = 0; i < explosions.length; i++) {
var explosion = explosions[i];
var particles = explosion.particles;
if (particles.length == 0) {
explosions.splice(i, 1);
return;
}
for (var ii = 0; ii < particles.length; ii++) {
var particle = particles[ii];
// Check particle size
// If 0, remove
if (particle.size < 0) {
particles.splice(ii, 1);
return;
}
ctx.beginPath();
ctx.arc(particle.x, particle.y, particle.size, Math.PI * 2, 0, false);
ctx.closePath();
ctx.fillStyle = 'rgb(' + particle.r + ',' + particle.g + ',' + particle.b + ')';
ctx.fill();
// Update
particle.x += particle.xv;
particle.y += particle.yv;
particle.size -= .1;
}
}
}
// Draw the background
function drawBackground() {
ctx.fillStyle = background;
ctx.fillRect(0, 0, c.width, c.height);
}
// Clicked
function clicked(e) {
var xPos, yPos;
if (e.offsetX) {
xPos = e.offsetX;
yPos = e.offsetY;
} else if (e.layerX) {
xPos = e.layerX;
yPos = e.layerY;
}
explosions.push(new explosion(xPos, yPos));
}
// Explosion
function explosion(x, y) {
this.particles = [];
for (var i = 0; i < particlesPerExplosion; i++) {
this.particles.push(new particle(x, y));
}
}
// Particle
function particle(x, y) {
this.x = x;
this.y = y;
this.xv = randInt(particlesMinSpeed, particlesMaxSpeed, false);
this.yv = randInt(particlesMinSpeed, particlesMaxSpeed, false);
this.size = randInt(particlesMinSize, particlesMaxSize, true);
this.r = randInt(113, 222);
this.g = '00';
this.b = randInt(105, 255);
}
// Returns an random integer, positive or negative
// between the given value
function randInt(min, max, positive) {
if (positive == false) {
var num = Math.floor(Math.random() * max) - min;
num *= Math.floor(Math.random() * 2) == 1 ? 1 : -1;
} else {
var num = Math.floor(Math.random() * max) + min;
}
return num;
}
// On-click
$('canvas').on('click', function(e) {
clicked(e);
});
draw();
<!DOCTYPE html>
<html>
<head>
<style>
* {
margin: 0;
padding: 0;
overflow: hidden;
}
</style>
</head>
<body>
<canvas id="canvas"></canvas>
</body>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
</html>
You are returning from iterating over the particles if one is too small. This causes the other particles of that explosion to render only in the next frame.
I have a working version:
// Request animation frame
const requestAnimationFrame = window.requestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.msRequestAnimationFrame;
// Canvas
const c = document.getElementById('canvas');
const ctx = c.getContext('2d');
// Set full-screen
c.width = window.innerWidth;
c.height = window.innerHeight;
// Options
const background = '#333'; // Background color
const particlesPerExplosion = 20;
const particlesMinSpeed = 3;
const particlesMaxSpeed = 6;
const particlesMinSize = 1;
const particlesMaxSize = 3;
const explosions = [];
let fps = 60;
const interval = 1000 / fps;
let now, delta;
let then = Date.now();
// Optimization for mobile devices
if (/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)) {
fps = 29;
}
// Draw
function draw() {
// Loop
requestAnimationFrame(draw);
// Set NOW and DELTA
now = Date.now();
delta = now - then;
// New frame
if (delta > interval) {
// Update THEN
then = now - (delta % interval);
// Our animation
drawBackground();
drawExplosion();
}
}
// Draw explosion(s)
function drawExplosion() {
if (explosions.length === 0) {
return;
}
for (let i = 0; i < explosions.length; i++) {
const explosion = explosions[i];
const particles = explosion.particles;
if (particles.length === 0) {
explosions.splice(i, 1);
return;
}
const particlesAfterRemoval = particles.slice();
for (let ii = 0; ii < particles.length; ii++) {
const particle = particles[ii];
// Check particle size
// If 0, remove
if (particle.size <= 0) {
particlesAfterRemoval.splice(ii, 1);
continue;
}
ctx.beginPath();
ctx.arc(particle.x, particle.y, particle.size, Math.PI * 2, 0, false);
ctx.closePath();
ctx.fillStyle = 'rgb(' + particle.r + ',' + particle.g + ',' + particle.b + ')';
ctx.fill();
// Update
particle.x += particle.xv;
particle.y += particle.yv;
particle.size -= .1;
}
explosion.particles = particlesAfterRemoval;
}
}
// Draw the background
function drawBackground() {
ctx.fillStyle = background;
ctx.fillRect(0, 0, c.width, c.height);
}
// Clicked
function clicked(e) {
let xPos, yPos;
if (e.offsetX) {
xPos = e.offsetX;
yPos = e.offsetY;
} else if (e.layerX) {
xPos = e.layerX;
yPos = e.layerY;
}
explosions.push(
new explosion(xPos, yPos)
);
}
// Explosion
function explosion(x, y) {
this.particles = [];
for (let i = 0; i < particlesPerExplosion; i++) {
this.particles.push(
new particle(x, y)
);
}
}
// Particle
function particle(x, y) {
this.x = x;
this.y = y;
this.xv = randInt(particlesMinSpeed, particlesMaxSpeed, false);
this.yv = randInt(particlesMinSpeed, particlesMaxSpeed, false);
this.size = randInt(particlesMinSize, particlesMaxSize, true);
this.r = randInt(113, 222);
this.g = '00';
this.b = randInt(105, 255);
}
// Returns an random integer, positive or negative
// between the given value
function randInt(min, max, positive) {
let num;
if (positive === false) {
num = Math.floor(Math.random() * max) - min;
num *= Math.floor(Math.random() * 2) === 1 ? 1 : -1;
} else {
num = Math.floor(Math.random() * max) + min;
}
return num;
}
// On-click
$('canvas').on('click', function (e) {
clicked(e);
});
draw();
<!DOCTYPE html>
<html>
<head>
<style>* {margin:0;padding:0;overflow:hidden;}</style>
</head>
<body>
<canvas id="canvas"></canvas>
</body>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
</html>
Loops, break and continue.
The problem was caused when you checked for empty particle arrays and when you found a particle to remove.
The bugs
The following two statements and blocks caused the problem
if (particles.length == 0) {
explosions.splice(i, 1);
return;
}
and
if (particles.size < 0) {
explosions.splice(ii, 1);
return;
}
The returns stopped the rendering of particles, so you would sometimes return before drawing a single particle was rendered just because the first explosion was empty or first particle was too small.
Continue and break
You can use the continue token in javascript to skip the rest of a for, while, do loop
for(i = 0; i < 100; i++){
if(test(i)){
// need to skip this iteration
continue;
}
// more code
// more code
// continue skips all the code upto the closing }
} << continues to here and if i < 100 the loop continues on.
Or you can completely break out of the loop with break
for(i = 0; i < 100; i++){
if(test(i)){
// need to exit the for loop
break;
}
// more code
// more code
// break skips all the code to the first line after the closing }
}
<< breaks to here and if i remains the value it was when break was encountered
The fix
if (particles.length == 0) {
explosions.splice(i, 1);
continue;
}
and
if (particles.size < 0) {
explosions.splice(ii, 1);
continue;
}
Your example with the fix
Your code with the fix. Befor I found it I started changing stuff.
Minor stuff.
requestAnimationFrame passes a time in milliseconds so to an accuracy of micro seconds.
You were setting then incorrectly and would have been losing frames. I changed the timing to use the argument time and then is just set to the time when a frame is drawn.
There are some other issues, nothing major and more of a coding style thing. You should capitalise objects created with new
function Particle(...
not
function particle(...
and your random is a overly complex
function randInt(min, max = min - (min = 0)) {
return Math.floor(Math.random() * (max - min) + min);
}
or
function randInt(min,max){
max = max === undefined ? min - (min = 0) : max;
return Math.floor(Math.random() * (max - min) + min);
}
randInt(100); // int 0 - 100
randInt(10,20); // int 10-20
randInt(-100); // int -100 to 0
randInt(-10,20); // int -10 to 20
this.xv = randInt(-particlesMinSpeed, particlesMaxSpeed);
this.yv = randInt(-particlesMinSpeed, particlesMaxSpeed);
this.size = randInt(particlesMinSize, particlesMaxSize);
And if you are using the same name in variables a good sign to create an object
var particlesPerExplosion = 20;
var particlesMinSpeed = 3;
var particlesMaxSpeed = 6;
var particlesMinSize = 1;
var particlesMaxSize = 3;
Could be
const settings = {
particles : {
speed : {min : 3, max : 6 },
size : {min : 1 : max : 3 },
explosionCount : 20,
},
background : "#000",
}
Anyways your code.
var c = canvas;
var ctx = c.getContext('2d');
// Set full-screen
c.width = innerWidth;
c.height = innerHeight;
// Options
var background = '#333'; // Background color
var particlesPerExplosion = 20;
var particlesMinSpeed = 3;
var particlesMaxSpeed = 6;
var particlesMinSize = 1;
var particlesMaxSize = 3;
var explosions = [];
var fps = 60;
var now, delta;
var then = 0; // Zero start time
var interval = 1000 / fps;
// Optimization for mobile devices
if (/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)) {
fps = 29;
}
// Draw
// as time is passed you need to start with requestAnimationFrame
requestAnimationFrame(draw);
function draw(time) { //requestAnimationFrame frame passes the time
requestAnimationFrame(draw);
delta = time - then;
if (delta > interval) {
then = time
drawBackground();
drawExplosion();
}
}
// Draw explosion(s)
function drawExplosion() {
if (explosions.length == 0) {
return;
}
for (var i = 0; i < explosions.length; i++) {
var explosion = explosions[i];
var particles = explosion.particles;
if (particles.length == 0) {
explosions.splice(i, 1);
//return;
continue;
}
for (var ii = 0; ii < particles.length; ii++) {
var particle = particles[ii];
// Check particle size
// If 0, remove
if (particle.size < 0) {
particles.splice(ii, 1);
// return;
continue;
}
ctx.beginPath();
ctx.arc(particle.x, particle.y, particle.size, Math.PI * 2, 0, false);
ctx.closePath();
ctx.fillStyle = 'rgb(' + particle.r + ',' + particle.g + ',' + particle.b + ')';
ctx.fill();
// Update
particle.x += particle.xv;
particle.y += particle.yv;
particle.size -= .1;
}
}
}
// Draw the background
function drawBackground() {
ctx.fillStyle = background;
ctx.fillRect(0, 0, c.width, c.height);
}
// Clicked
function clicked(e) {
var xPos, yPos;
if (e.offsetX) {
xPos = e.offsetX;
yPos = e.offsetY;
} else if (e.layerX) {
xPos = e.layerX;
yPos = e.layerY;
}
explosions.push(new explosion(xPos, yPos));
}
// Explosion
function explosion(x, y) {
this.particles = [];
for (var i = 0; i < particlesPerExplosion; i++) {
this.particles.push(new particle(x, y));
}
}
// Particle
function particle(x, y) {
this.x = x;
this.y = y;
this.xv = randInt(particlesMinSpeed, particlesMaxSpeed, false);
this.yv = randInt(particlesMinSpeed, particlesMaxSpeed, false);
this.size = randInt(particlesMinSize, particlesMaxSize, true);
this.r = randInt(113, 222);
this.g = '00';
this.b = randInt(105, 255);
}
// Returns an random integer, positive or negative
// between the given value
function randInt(min, max, positive) {
if (positive == false) {
var num = Math.floor(Math.random() * max) - min;
num *= Math.floor(Math.random() * 2) == 1 ? 1 : -1;
} else {
var num = Math.floor(Math.random() * max) + min;
}
return num;
}
// On-click
$('canvas').on('click', function(e) {
clicked(e);
});
<!DOCTYPE html>
<html>
<head>
<style>
* {
margin: 0;
padding: 0;
overflow: hidden;
}
</style>
</head>
<body>
<canvas id="canvas"></canvas>
</body>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
</html>