How to lower mic input volume using navigator.mediaDevices.getUserMedia? - javascript

I'm creating audio record app using navigator.mediaDevices.getUserMedia() and it records every sound around me even very quiet and which is 10m away from me. I DO NOT play this sound, I only visualize it depending on volume, so I need only quite loud sounds or which are close to mic, cause there's too much interference.
Also if I enable playback to hear my mic input and start making quiet noise like tapping on the table, I can't here this sound in playback but I see it in visualizer and this is exactly what I don't want
Here's my code:
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
this.audioContext = new AudioContext();
this.sourceNode = this.audioContext.createMediaStreamSource(stream);
this.analyserNode = this.audioContext.createAnalyser();
this.sourceNode.connect(this.analyserNode);
const data = new Float32Array(this.analyserNode.fftSize);
this.analyserNode.getFloatTimeDomainData(data);
So how can I lower mic sensivity using Web Audio API or lower mic input volume or maybe transform data from analyser? I've read about AudioContext.createGain(), gain.volume, but it's used for output audio volume, not input one

I've read about AudioContext.createGain(), gain.volume, but it's used for output audio volume, not input one
No, it is used to control the volume of the audio that goes through it.
You have to see your audio context nodes as a chain, then you might understand that you can indeed use a GainNode to control the input volume of the next node to which it is connected.
Like e.g if we declare something like
gainNode.gain.volume = 0.5;
input.connect(gainNode);
gainNode.connect(analyserNode);
input.connect(audioContext.destination);
it can be seen as
Input [mic] ===> GainNode ===> AnalyserNode
100% || 50% 50%
||
===> AudioContext Output
100%
So your gainNode here did lower the volume of your AnalyserNode, but not the one of the context output.
But this is not really what you want.
Indeed, the AnalyserNode API has minDecibels and maxDecibels properties which will do exactly what you want (filter out out of db range sounds).
But these properties make sense only for frequency data (getXXXFrequencyData) since waveform doesn't take volume into account.
However, it is still possible to check if this frequency data is in our required bounds before deciding if we should draw our waveform or not.
polyfill();
(async() => {
const ctx = new AudioContext();
const input = await loadFileAsBufferNode(ctx);
const analyser = ctx.createAnalyser();
analyser.minDecibels = -90;
analyser.maxDecibels = -10;
analyser.fftSize = 512;
input.connect(analyser);
const gainNode = ctx.createGain();
input.connect(gainNode);
const bufferLength = analyser.frequencyBinCount;
const freqArray = new Uint8Array(bufferLength);
const waveArray = new Uint8Array(bufferLength);
const canvasCtx = canvas.getContext('2d');
const WIDTH = canvas.width;
const HEIGHT = canvas.height;
canvasCtx.lineWidth = 2;
draw();
// taken from https://developer.mozilla.org/en-US/docs/Web/API/AnalyserNode/maxDecibels#Example
function draw() {
requestAnimationFrame(draw);
canvasCtx.clearRect(0, 0, WIDTH, HEIGHT);
analyser.getByteFrequencyData(freqArray);
gainNode.gain.value = 1;
analyser.getByteTimeDomainData(waveArray);
var barWidth = (WIDTH / bufferLength) * 2.5;
var barHeight;
var x = 0;
for (var i = 0; i < bufferLength; i++) {
barHeight = freqArray[i];
canvasCtx.fillStyle = 'rgb(' + (barHeight + 100) + ',50,50)';
canvasCtx.fillRect(x, HEIGHT - barHeight / 2, barWidth, barHeight / 2);
x += barWidth + 1;
}
// here we check if the volume is in bounds
if (freqArray.some(isTooHigh) || !freqArray.some(hasValue)) {
canvasCtx.fillRect(0, HEIGHT / 2, WIDTH, 1);
gainNode.gain.value = 0;
return;
}
canvasCtx.beginPath();
var sliceWidth = WIDTH * 1.0 / bufferLength;
var x = 0;
for (var i = 0; i < bufferLength; i++) {
var v = waveArray[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();
};
function isTooHigh(val) {
return val === 255;
}
function hasValue(val) {
return val;
}
// DOM
maxDB.oninput = e => {
const max = +maxDB.value;
if (+minDB.value >= max) minDB.value = analyser.minDecibels = max - 1;
analyser.maxDecibels = max;
}
minDB.oninput = e => {
const min = +minDB.value;
if (+maxDB.value <= min) maxDB.value = analyser.maxDecibels = min + 1;
analyser.minDecibels = min;
}
out.onchange = e => {
if (out.checked)
gainNode.connect(ctx.destination);
else
gainNode.disconnect(ctx.destination);
};
})();
function loadFileAsBufferNode(ctx, url = 'https://dl.dropboxusercontent.com/s/8c9m92u1euqnkaz/GershwinWhiteman-RhapsodyInBluePart1.mp3') {
return fetch(url)
.then(r => r.arrayBuffer())
.then(buf => ctx.decodeAudioData(buf))
.then(bufferNode => {
const source = ctx.createBufferSource();
source.buffer = bufferNode;
source.repeat = true;
source.start(0);
return source;
});
};
/* for Safari */
function polyfill() {
window.AudioContext = window.AudioContext || window.webkitAudioContext;
try {
const prom = new AudioContext().decodeAudioData(new ArrayBuffer()).catch(e => {});
} catch (e) {
const prev = AudioContext.prototype.decodeAudioData;
Object.defineProperty(AudioContext.prototype, 'decodeAudioData', {
get: () => asPromise
});
function asPromise(audioBuffer, done, failed) {
return new Promise((res, rej) => {
prev.apply(this, [audioBuffer, onsuccess, onerror]);
function onsuccess(buf) {
if (typeof done === 'function') done(buf);
res(buf);
}
function onerror(err) {
if (typeof failed === 'function') failed(err);
rej(err);
}
});
}
}
}
<label>min<input type="range" id="minDB" min="-100" max="-1" value="-90"></label>
<label>max<input type="range" id="maxDB" min="-99" max="0" value="-10"></label>
<label>output audio<input type="checkbox" id="out"></label>
<canvas id="canvas"></canvas>

Related

Why is this Canvas component rendering twice and shifting abruptly at the end?

I've been trying to implement Roco C. Buljan's JS Spinning Wheel in React and I'm completely stuck at this particular point. The intended behavior is that the wheel spins once and after stopping displays the pointed sector.
Here's what my code looks like:
import React, { useEffect, useRef } from "react";
import "./SpinWheel.css";
function SpinWheel() {
const sectors = [
{ color: "#f82", label: "Stack" },
{ color: "#0bf", label: "10" },
{ color: "#fb0", label: "200" },
{ color: "#0fb", label: "50" },
{ color: "#b0f", label: "100" },
{ color: "#f0b", label: "5" },
];
// Generate random float in range min-max:
const rand = (m, M) => Math.random() * (M - m) + m;
const tot = sectors.length;
const wheel = useRef(null);
const spin = useRef(null);
useEffect(() => {
const ctx = wheel.current.getContext("2d");
const elSpin = spin.current;
spinWheel(ctx, elSpin);
}, []);
function spinWheel(ctx, elSpin) {
const dia = ctx.canvas.width;
const rad = dia / 2;
const PI = Math.PI;
const TAU = 2 * PI;
const arc = TAU / sectors.length;
const friction = 0.991; // 0.995=soft, 0.99=mid, 0.98=hard
const angVelMin = 0.002; // Below that number will be treated as a stop
let angVelMax = 0; // Random ang.vel. to acceletare to
let angVel = 0; // Current angular velocity
let ang = 0; // Angle rotation in radians
let isSpinning = false;
let isAccelerating = false;
//* Get index of current sector */
const getIndex = () => Math.floor(tot - (ang / TAU) * tot) % tot;
//* Draw sectors and prizes texts to canvas */
const drawSector = (sector, i) => {
const ang = arc * i;
ctx.save();
// COLOR
ctx.beginPath();
ctx.fillStyle = sector.color;
ctx.moveTo(rad, rad);
ctx.arc(rad, rad, rad, ang, ang + arc);
ctx.lineTo(rad, rad);
ctx.fill();
// TEXT
ctx.translate(rad, rad);
ctx.rotate(ang + arc / 2);
ctx.textAlign = "right";
ctx.fillStyle = "#fff";
ctx.font = "bold 30px sans-serif";
ctx.fillText(sector.label, rad - 10, 10);
//
ctx.restore();
};
//* CSS rotate CANVAS Element */
const rotate = () => {
const sector = sectors[getIndex()];
ctx.canvas.style.transform = `rotate(${ang - PI / 2}rad)`;
elSpin.textContent = !angVel ? sector.label : "SPIN";
elSpin.style.background = sector.color;
};
const frame = () => {
if (!isSpinning) return;
if (angVel >= angVelMax) isAccelerating = false;
// Accelerate
if (isAccelerating) {
angVel ||= angVelMin; // Initial velocity kick
angVel *= 1.06; // Accelerate
}
// Decelerate
else {
isAccelerating = false;
angVel *= friction; // Decelerate by friction
// SPIN END:
if (angVel < angVelMin) {
isSpinning = false;
angVel = 0;
}
}
ang += angVel; // Update angle
ang %= TAU; // Normalize angle
rotate(); // CSS rotate!
};
const engine = () => {
frame();
requestAnimationFrame(engine);
};
elSpin.addEventListener("click", () => {
if (isSpinning) return;
isSpinning = true;
isAccelerating = true;
angVelMax = rand(0.25, 0.4);
});
// INIT!
sectors.forEach(drawSector);
rotate(); // Initial rotation
engine(); // Start engine!
}
return (
<div id="wheelOfFortune">
<canvas id="wheel" ref={wheel} width="300" height="300"></canvas>
<div id="spin" ref={spin}>
SPIN asd asd asd as dasd as dasd asd asd as d
</div>
</div>
);
}
export default SpinWheel;
And here's the sandbox link
The issue is that if I console.log the useEffect block, I can see that it executes twice at the start which is not what I intend. I tried putting an empty array as the dependency array but it still doesn't work for me. The second issue is that the wheel supposedly stops and then spins a small amount yet again, resulting in two final sector outputs. Again, not what I intend. I can only guess that it is rendering more than once but I'm not sure. Any ideas what I'm doing wrong?
There are a couple things you need to do to get this working in a react context:
The spinWheel method should return start/stop/cleanup methods for controlling the wheel. The cleanup method should clean up any artifacts such as event handlers and whatnot.
const startSpin = () => {
if (isSpinning) return;
isSpinning = true;
isAccelerating = true;
angVelMax = rand(0.25, 0.4);
};
const stopSpin = () => {
isSpinning = false;
isAccelerating = false;
}
elSpin.addEventListener("click", startSpin);
const cleanup = () => {
elSpin.removeEventListener("click", startSpin);
};
...
return { startSpin, stopSpin, cleanup };
As mentioned in a comment, you need to make sure your effect has a "cleanup" function which stops the wheel (see this link). This is necessary so that the setup → cleanup → setup sequence works in development as described on that page.
useEffect(() => {
const ctx = wheel.current.getContext("2d");
const elSpin = spin.current;
const { startSpin, stopSpin, cleanup } = spinWheel(ctx, elSpin);
startSpin();
return () => {
stopSpin();
cleanup();
}
}, []);
Here's a link to a working sandbox: https://codesandbox.io/s/spin-wheel-forked-tb42zf?file=/src/components/SpinWheel/SpinWheel.js:546-826

audio element not valid

I'm creating a audio player with visualizer.
But currently when I press the input to start the audio player my debug console returns:
Uncaught (in promise) DOMException: Failed to load because no
supported source was found.
What I'm currently doing is setting the whole audio element up in JS / jQuery:
var bins = 512;
var backgroundColour = "#2C2E3B";
var barColour = "#EC1A55";
var floorLevel = 32;
var audioContext;
var audioBuffer;
var audioAnalyserNode;
var initialized = false;
var songText = "";
var textSize;
var freqLookup = [];
var canvasContext;
var isStream = true;
var canvasWidth;
var canvasHeight;
var src;
var audioElement;
var isPlaying = false;
var volume = 1;
function play() {
audioElement = document.createElement('audio');
// Opus support check stuff
var streamEndpoint = 'http://**.**.**.**:8003/stream';
var canPlayOpus = (typeof audioElement.canPlayType === "function" && audioElement.canPlayType('audio/ogg; codecs="opus"') !== "");
if(volume > 1) {
volume = volume / 100;
}
audioElement.src = streamEndpoint;
audioElement.crossOrigin = 'anonymous';
audioElement.volume = volume;
audioElement.play();
isPlaying = true;
setUpCanvas(audioElement);
}
function pause() {
audioElement.pause();
audioElement.currentTime = 0;
audioElement.src = '';
isPlaying = false;
}
function setUpCanvas(audioElement){
try {
initCanvas(document.getElementById("canvas"));
if(typeof audioContext === 'undefined') {
audioContext = new AudioContext();
}
if (audioElement) {
isStream = true;
setupAudioApi(true, audioElement);
}
} catch(e) {
console.log(e);
}
}
function setupAudioApi(isStream, audioElement) {
//var src;
if (isStream){
if(typeof src === 'undefined'){
src = audioContext.createMediaElementSource(audioElement);
audioContext.crossOrigin = "anonymous";
audioAnalyserNode = audioContext.createAnalyser();
audioAnalyserNode.fftSize = bins * 4;
src.connect(audioAnalyserNode);
audioAnalyserNode.connect(audioContext.destination);
}
}
if (!isStream) {
src.start();
}
initialized = true;
initFreqLookupTable();
}
function initCanvas(canvasElement) {
canvasContext = canvasElement.getContext('2d');
canvasElement.width = canvasElement.clientWidth;
canvasElement.height = canvasElement.clientHeight;
canvasWidth = canvasElement.width;
canvasHeight = canvasElement.height;
requestAnimationFrame(paint);
}
function getFreqPoint(start, stop, n, binCount) {
return start * Math.pow(stop / start, n / (binCount - 1));
}
function initFreqLookupTable() {
var lastPoint = 0;
var bins = audioAnalyserNode.frequencyBinCount;
for(var i = 0; i < bins / 2; i++) {
//Scale to perceived frequency distribution
var newFreq = getFreqPoint(20, 20000, i * 2, bins);
var point = Math.floor(bins * newFreq / 20000);
while (point <= lastPoint) {
point++;
}
lastPoint = point;
freqLookup.push(point);
}
}
//Render some fancy bars
function paint() {
requestAnimationFrame(paint);
if(!initialized) {
alert('Er is iets fout gegaan');
return false;
}
canvasContext.clearRect(0, 0, canvasWidth, canvasHeight);
canvasContext.fillStyle = backgroundColour;
canvasContext.fillRect(0, 0, canvasWidth, canvasHeight);
var bins = audioAnalyserNode.frequencyBinCount;
var data = new Uint8Array(bins);
audioAnalyserNode.getByteFrequencyData(data);
canvasContext.fillStyle = barColour;
for(var i = 0; i < bins; i++) {
var point = freqLookup[i];
//Pretty much any volume will push it over 128 so we set that as the bottom threshold
//I suspect I should be doing a logarithmic space for the volume as well
var height = Math.max(0, (data[point] - floorLevel));
//Scale to the height of the bar
//Since we change the base level in the previous operations, 256 should be changed to 160 (i think) if we want it to go all the way to the top
height = (height / (256 - floorLevel)) * canvasHeight * 0.8;
var width = Math.ceil(canvasWidth / ((bins / 2) - 1));
canvasContext.fillRect(i * width, canvasHeight - height, width, height);
}
}
The stream is in audio/mpeg format, it does load when I simply create an audio element in HTML with a src.
Can someone help me clarify and find the solution to the DOMException I'm getting. I have been searching other cases of this error but the fixes there didn't resolve the problem.
Try creating the audio tag like this:
var audio = new Audio('audio_file.mp3');
And try setting the type:
audio.type = "audio/mpeg";
I think that will fix your problem.
This creates an element, identical to the one you use in your code.
I suggest you put an extension on your stream.
I know this way works, and I don't know why the other way doesn't.

Why does this simple ScriptProcessorNode have so much latency?

I'm having an issue with ScriptProcessorNode and lag. Even when doing an incredibly simple process that just takes the sample and does x %= 0.35. Why does this simple ScriptProcessorNode (included below) have so much latency?
The MDN says nothing about latency: https://developer.mozilla.org/en-US/docs/Web/API/ScriptProcessorNode
Also, I know that ScriptProcessorNode is to be deprecated soon, but audioWorklets aren't yet implemented, so this is the best there is.
var baseFreq = 220;
var audioCtx = new(window.AudioContext || window.webkitAudioContext)();
var osc = audioCtx.createOscillator();
osc.type = 'sine';
osc.frequency.value = baseFreq;
osc.start();
var intervals = [1, 1.125, 1.25, 1.5, 1.6666666, 1.875, 2, 4]
var index = 0;
function newTone() {
index++;
index %= intervals.length;
osc.frequency.value = intervals[index] * baseFreq;
}
var modulus = audioCtx.createScriptProcessor();
modulus.onaudioprocess = function(audioProcessingEvent) {
var inputBuffer = audioProcessingEvent.inputBuffer;
var outputBuffer = audioProcessingEvent.outputBuffer;
for (var channel = 0; channel < outputBuffer.numberOfChannels; channel++) {
var inputData = inputBuffer.getChannelData(channel);
var outputData = outputBuffer.getChannelData(channel);
for (var sample = 0; sample < inputBuffer.length; sample++) {
outputData[sample] = inputData[sample] % moduAmount;
}
}
}
var moduLow = 0.35;
var moduHigh = 2;
var moduAmount = moduLow;
function turnModulusOnOff() {
if (moduAmount == moduLow)
moduAmount = moduHigh;
else
moduAmount = moduLow;
}
var gain = audioCtx.createGain();
gain.gain.value = 0.05;
gain.connect(audioCtx.destination);
modulus.connect(gain)
osc.connect(modulus);
osc.connect(gain);
document.body.addEventListener("keydown", newTone);
html,
body {
height: 100%;
width: 100%;
font-family: arial;
}
<button onclick="newTone()">Next Tone</button>
<button onclick="turnModulusOnOff()">Mute/Unmute Modulus</button>
<br>
To test, click anywhere and then press any key to change the tone. Notice when Modulus is muted how responsive the tone change is. Notice the lag between the sine frequency change and the modulus effect.
<br>
<b>For Extra Fun: Go crazy on your keyboard</b>

Issue with reduced quality on file after saving/converting from canvas

This is the code I am using. (code is at way bottom of this post but here is link to GitHubGist :: Noitidart / _ff-addon-snippet-browseForBadgeThenCreateSaveAnApply.js) It is copy pastatble to scratchpad (i tried fiddle but it needs privelage scope). When you run it will ask you to select a 16x16 image. Then it will take the firefox icon and put it on a canvas and then take the icon you browsed to and overlay it on the bottom right. Then it will convert it to .ico and save to your desktop as profilist16.ico and profilist32.ico. It will then change the icons of all your firefox windows.
After you do the above, please open a new firefox window and then in alt+tab you'll see the firefox logo of the badged icon is dirtier.
On the bottom you see the original canvas drawing (it looks blurry but i think thats my zoom level on firefox). The icon is crisp but if you notice the badged icon (on right) on the edges (especially top) you see dirt, like black jagged stuff which is not seen in the usual icon (at left)
var win = Services.wm.getMostRecentWindow(null);
var me = win;
//these should be global vars
var sizes = []; //os dependent
var img = {}; //holds Image for each size image
var osIconFileType = 'ico'; //os dependent
var cOS = 'Windows';
function badgeIt() {
var fp = Cc["#mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
fp.init(win, "Select Badge Image", Ci.nsIFilePicker.modeOpen);
var fpCallback = function(rv) {
if (rv == Ci.nsIFilePicker.returnOK || rv == Ci.nsIFilePicker.returnReplace) {
if (sizes.length == 0) {
//figure out what os this is and populate sizes withthe sizes needed for this os
sizes = [32, 16]; //note: ask on SO how to determine what sizes the os uses for its icons?
}
loadBadgeImage();
} else {
//user did not select an file to badge with
}
}
var ranOnce0 = false;
var checkAllDefaultImagesLoaded = function() {
for (var i=0; i<sizes.length; i++) {
//console.log('img.sizes[i].loaded for i = ' + sizes[i] + ' is == ' + uneval(img[sizes[i]]));
if (!img[sizes[i]] || !img[sizes[i]].loaded) {
console.log('returning false as sizes[i]', sizes[i], 'is not loaded yet')
return false; //return as not yet all are done
}
//me.alert('all img sizes loaded');
}
//ok all sizes loaded
if (ranOnce0) {
alert('already ranOnce0 so return false');
return false;
}
ranOnce0 = true;
return true;
}
var loadDefaultImages = function() {
for (var i=0; i<sizes.length; i++) {
img[sizes[i]] = {};
img[sizes[i]].Image = new Image();
img[sizes[i]].Image.onload = function(iBinded) {
console.log('i', iBinded);
//console.log('img', img);
console.log('sizes[i]', sizes[iBinded]);
console.log('img[sizes[iBinded]].loaded=', uneval(img[sizes[iBinded]]), 'will now set it to true')
img[sizes[iBinded]].loaded = true;
console.log('just loaded size of (sizes[iBinded]) = ' + sizes[iBinded]);
var allLoaded = checkAllDefaultImagesLoaded();
if (allLoaded == true) {
console.log('allLoaded == true so createAndSave')
createAndSaveIcons();
} else {
console.warn('allLoaded is false so dont create')
}
}.bind(null, i)
img[sizes[i]].Image.src = 'chrome://branding/content/icon' + sizes[i] + '.png';
}
}
var loadBadgeImage = function() {
console.log('loadBadgeImage')
img.badge = {};
img.badge.Image = new Image();
img.badge.Image.onload = function() {
console.log('bagde image loaded')
img.badge.loaded = true;
if (checkAllDefaultImagesLoaded()) {
console.log('all dfault images PRELOADED so continue to createAndSaveIcons')
createAndSaveIcons();
} else {
console.log('all default images not loaded so start loading them')
loadDefaultImages();
}
}
img.badge.Image.src = Services.io.newFileURI(fp.file).spec;
}
var badgedIconMade = {};
var ranOnce = false;
var checkAllBadgedIconsMade = function() {
for (var i=0; i<sizes.length; i++) {
if (!badgedIconMade[sizes[i]]) {
return; //not yt done making
}
}
if (ranOnce) {
alert('already ranOnce so return');
return;
}
ranOnce = true;
// all badged icons made
applyIcons();
}
var blobCallback = function(size) {
return function (b) {
var r = new FileReader();
r.onloadend = function () {
// r.result contains the ArrayBuffer.
//alert(r.result)
img[size].ArrayBuffer = r.result;
badgedIconMade[size] = true;
//checkAllBadgedIconsMade();
Cu.import('resource://gre/modules/osfile.jsm');
var writePath = OS.Path.join(OS.Constants.Path.desktopDir, 'profilist' + size + '.' + osIconFileType);
console.log('writePath', writePath)
var promise = OS.File.writeAtomic(writePath, new Uint8Array(r.result), {tmpPath:writePath + '.tmp'});
promise.then(
function() {
//win.alert('success')
checkAllBadgedIconsMade();
},
function() {
//win.alert('failure')
}
);
};
//var url = window.URL.createObjectURL(b)
//img[size].blobUrl = url;
//prompt('', url)
r.readAsArrayBuffer(b);
}
}
var createAndSaveIcons = function() {
console.log('createAndSave')
var canvas = document.createElementNS('http://www.w3.org/1999/xhtml', 'canvas');
var ctx = canvas.getContext('2d');
gBrowser.contentDocument.documentElement.appendChild(canvas);
var badgeDim = { //holds key which is size of default icon, and the value is the dimension to draw the badge for that default icon size //this is set by me the dev, maybe make preference for this for user
'16': 10,
'32': 16
};
for (var i=0; i<sizes.length; i++) {
canvas.width = sizes[i];
canvas.height = sizes[i];
ctx.clearRect(0, 0, sizes[i], sizes[i]);
ctx.drawImage(img[sizes[i]].Image, 0, 0);
if (sizes[i] in badgeDim) {
if (badgeDim[sizes[i]] != sizes[i]) { //before i had `img.badge.Image.width` in place of `sizes[i]`, but can just use sizes[i] because thats the dim of the default icon duh
ctx.drawImage(img.badge.Image, sizes[i]-badgeDim[sizes[i]], sizes[i]-badgeDim[sizes[i]], badgeDim[sizes[i]], badgeDim[sizes[i]]);
} else {
//the redim size is same as icon size anyways so just draw it
ctx.drawImage(img.badge.Image, sizes[i]-badgeDim[sizes[i]], sizes[i]-badgeDim[sizes[i]]);
}
} else {
//sizes[i] is not in badgeDim meaning i dont care what size the badge is on this size of icon
ctx.drawImage(img.badge.Image, sizes[i]-badgeDim[sizes[i]], sizes[i]-badgeDim[sizes[i]]);
}
//canvas.mozFetchAsStream(mfasCallback(sizes[i]), 'image/vnd.microsoft.icon')
canvas.toBlob(blobCallback(sizes[i]), "image/vnd.microsoft.icon", "-moz-parse-options:format=bmp;bpp=32");
}
}
var applyIcons = function() {
if (cOS == 'Windows') {
Cu.import('resource://gre/modules/ctypes.jsm');
var user32 = ctypes.open('user32.dll');
/* http://msdn.microsoft.com/en-us/library/windows/desktop/ms644950%28v=vs.85%29.aspx
* LRESULT WINAPI SendMessage(
* __in HWND hWnd,
* __in UINT Msg,
* __in WPARAM wParam,
* __in LPARAM lParam
* );
*/
var SendMessage = user32.declare('SendMessageW', ctypes.winapi_abi, ctypes.uintptr_t,
ctypes.voidptr_t,
ctypes.unsigned_int,
ctypes.int32_t,
ctypes.voidptr_t
);
/* http://msdn.microsoft.com/en-us/library/windows/desktop/ms648045%28v=vs.85%29.aspx
* HANDLE WINAPI LoadImage(
* __in_opt_ HINSTANCE hinst,
* __in_ LPCTSTR lpszName,
* __in_ UINT uType,
* __in_ int cxDesired,
* __in_ int cyDesired,
* __in_ UINT fuLoad
* );
*/
var LoadImage = user32.declare('LoadImageA', ctypes.winapi_abi, ctypes.voidptr_t,
ctypes.voidptr_t,
ctypes.char.ptr,
ctypes.unsigned_int,
ctypes.int,
ctypes.int,
ctypes.unsigned_int
);
var IMAGE_BITMAP = 0;
var IMAGE_ICON = 1;
var LR_LOADFROMFILE = 16;
var DOMWindows = Services.wm.getEnumerator(null);
while (DOMWindows.hasMoreElements()) {
var aDOMWindow = DOMWindows.getNext();
var baseWindow = aDOMWindow.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebNavigation)
.QueryInterface(Ci.nsIDocShellTreeItem)
.treeOwner
.QueryInterface(Ci.nsIInterfaceRequestor)
.nsIBaseWindow;
var nativeHandle = baseWindow.nativeHandle;
var targetWindow_handle = ctypes.voidptr_t(ctypes.UInt64(nativeHandle));
console.log('aappplying now')
var hIconBig = LoadImage(targetWindow_handle, OS.Path.join(OS.Constants.Path.desktopDir, 'profilist32.' + osIconFileType), IMAGE_ICON, 32, 32, LR_LOADFROMFILE); //MUST BE A FILEPATH TO A ICO!!!
var hIconSmall = LoadImage(targetWindow_handle, OS.Path.join(OS.Constants.Path.desktopDir, 'profilist16.' + osIconFileType), IMAGE_ICON, 16, 16, LR_LOADFROMFILE); //MUST BE A FILEPATH TO A ICO!!!
var successSmall = SendMessage(targetWindow_handle, 0x0080 /** WM_SETICON **/ , 0 /** ICON_SMALL **/ , hIconSmall); //if it was success it will return 0? im not sure. on first time running it, and it was succesful it returns 0 for some reason
var successBig = SendMessage(targetWindow_handle, 0x0080 /** WM_SETICON **/ , 1 /** ICON_BIG **/ , hIconBig); //if it was success it will return 0? im not sure. on first time running it, and it was succesful it returns 0 for some reason
}
user32.close();
}
}
fp.open(fpCallback);
}
badgeIt();
Alright. This is actually quite reproducible, but only when using BMP icons, but not PNG icons.
Seems the icon encoder that Firefox ships is pretty bad/buggy indeed (for RGBA stuff). Well, actually it is the BMP encoder that the ICO encoder uses...
So since Belgium/Algeria (the game, football, not American) was mostly boring just now, I wrote my own icon encoder, which isn't too hard actually.
So here is my complete example code incl. icon encoder (just setting the 32x32 icon), but which lacks deposing of icons. But as a bonus, it shows how to set the icon via the WNDCLASS.
Cu.import('resource://gre/modules/ctypes.jsm');
Cu.import('resource://gre/modules/osfile.jsm');
let IMAGE_BITMAP = 0;
let IMAGE_ICON = 1;
let WM_SETICON = 128;
let GCLP_HICON = -14;
let user32 = ctypes.open('user32.dll');
let SendMessage = user32.declare(
'SendMessageW',
ctypes.winapi_abi,
ctypes.intptr_t,
ctypes.voidptr_t, // HWND
ctypes.uint32_t, // MSG
ctypes.uintptr_t, // WPARAM
ctypes.intptr_t // LPARAM
);
let CreateIconFromResourceEx = user32.declare(
'CreateIconFromResourceEx',
ctypes.winapi_abi,
ctypes.voidptr_t,
ctypes.uint8_t.ptr, // icon
ctypes.uint32_t, // size
ctypes.int32_t, // icon
ctypes.uint32_t, // dwVersion
ctypes.int, // dx
ctypes.int, // dy
ctypes.uint32_t // flags
);
let SetClassLongPtr = user32.declare(
ctypes.intptr_t.size == 8 ? 'SetClassLongPtrW' : 'SetClassLongW',
ctypes.winapi_abi,
ctypes.uintptr_t,
ctypes.voidptr_t, // HWND
ctypes.int, // index
ctypes.uintptr_t // value
);
let gdi32 = ctypes.open('gdi32.dll');
let DeleteObject = gdi32.declare(
'DeleteObject',
ctypes.winapi_abi,
ctypes.int,
ctypes.voidptr_t // Object
);
let setPerWindow = false;
let badges = [
'chrome://browser/skin/places/starred48.png',
'chrome://browser/skin/places/downloads.png',
'chrome://browser/skin/places/tag.png',
'chrome://browser/skin/places/livemark-item.png',
'chrome://browser/skin/places/query.png',
'chrome://browser/skin/pluginInstall-64.png',
'chrome://browser/skin/pluginInstall-16.png',
];
function getRandomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
Task.spawn(function* setIcon() {
"use strict";
try {
let p = Promise.defer();
let img = new Image();
img.onload = () => p.resolve();
img.src = 'chrome://branding/content/icon32.png';
yield p.promise;
p = Promise.defer();
let badge = new Image();
badge.onload = () => p.resolve();
badge.src = badges[getRandomInt(0, badges.length - 1)];
console.log(badge.src);
yield p.promise;
let canvas = document.createElementNS(
'http://www.w3.org/1999/xhtml',
'canvas');
canvas.width = img.naturalWidth;
canvas.height = img.naturalHeight;
let ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
let onethird = canvas.width / 3;
ctx.drawImage(
badge,
onethird,
onethird,
canvas.width - onethird,
canvas.height - onethird);
// Our own little ico encoder
// http://msdn.microsoft.com/en-us/library/ms997538.aspx
// Note: We would have been able to skip ICONDIR/ICONDIRENTRY,
// if we were to use CreateIconFromResourceEx only instead of also
// writing the icon to a file.
let data = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
let XOR = data.length;
let AND = canvas.width * canvas.height / 8;
let size = 22 /* ICONDIR + ICONDIRENTRY */ + 40 /* BITMAPHEADER */ + XOR + AND;
let buffer = new ArrayBuffer(size);
// ICONDIR
let view = new DataView(buffer);
view.setUint16(2, 1, true); // type 1
view.setUint16(4, 1, true); // count;
// ICONDIRENTRY
view = new DataView(buffer, 6);
view.setUint8(0, canvas.width % 256);
view.setUint8(1, canvas.height % 256);
view.setUint16(4, 1, true); // Planes
view.setUint16(6, 32, true); // BPP
view.setUint32(8, 40 + XOR + AND, true); // data size
view.setUint32(12, 22, true); // data start
// BITMAPHEADER
view = new DataView(buffer, 22);
view.setUint32(0, 40, true); // BITMAPHEADER size
view.setInt32(4, canvas.width, true);
view.setInt32(8, canvas.height * 2, true);
view.setUint16(12, 1, true); // Planes
view.setUint16(14, 32, true); // BPP
view.setUint32(20, XOR + AND, true); // size of data
// Reorder RGBA -> BGRA
for (let i = 0; i < XOR; i += 4) {
let temp = data[i];
data[i] = data[i + 2];
data[i + 2] = temp;
}
let ico = new Uint8Array(buffer, 22 + 40);
let stride = canvas.width * 4;
// Write bottom to top
for (let i = 0; i < canvas.height; ++i) {
let su = data.subarray(XOR - i * stride, XOR - i * stride + stride);
ico.set(su, i * stride);
}
// Write the icon to inspect later. (We don't really need to write it at all)
let writePath = OS.Path.join(OS.Constants.Path.desktopDir, 'icon32.ico');
yield OS.File.writeAtomic(writePath, new Uint8Array(buffer), {
tmpPath: writePath + '.tmp'
});
// Cut off ICONDIR/ICONDIRENTRY for CreateIconFromResourceEx
buffer = buffer.slice(22);
let hicon = CreateIconFromResourceEx(
ctypes.uint8_t.ptr(buffer),
buffer.byteLength,
IMAGE_ICON,
0x30000,
0,
0,
0);
if (hicon.isNull()) {
throw new Error("Failed to load icon");
}
if (setPerWindow) {
let DOMWindows = Services.wm.getEnumerator(null);
while (DOMWindows.hasMoreElements()) {
let win = DOMWindows.getNext().QueryInterface(Ci.nsIInterfaceRequestor).
getInterface(Ci.nsIWebNavigation).
QueryInterface(Ci.nsIDocShellTreeItem).
treeOwner.QueryInterface(Ci.nsIInterfaceRequestor).
getInterface(Ci.nsIBaseWindow);
let handle = ctypes.voidptr_t(ctypes.UInt64(win.nativeHandle));
if (handle.isNull()) {
console.error("Failed to get window handle");
continue;
}
var lparam = ctypes.cast(hicon, ctypes.intptr_t);
var oldIcon = SendMessage(handle, WM_SETICON, 1, lparam);
if (ctypes.voidptr_t(oldIcon).isNull()) {
console.log("There was no old icon", oldIcon.toString());
}
else {
console.log("There was an old icon already", oldIcon.toString());
// In a perfect world, we should actually kill our old icons
// using DeleteObject...
}
}
}
else {
let win = Services.wm.getMostRecentWindow(null).
QueryInterface(Ci.nsIInterfaceRequestor).
getInterface(Ci.nsIWebNavigation).
QueryInterface(Ci.nsIDocShellTreeItem).
treeOwner.QueryInterface(Ci.nsIInterfaceRequestor).
getInterface(Ci.nsIBaseWindow);
let handle = ctypes.voidptr_t(ctypes.UInt64(win.nativeHandle));
if (handle.isNull()) {
throw new Error("Failed to get window handle");
}
let oldIcon = SetClassLongPtr(handle, GCLP_HICON, ctypes.cast(hicon, ctypes.uintptr_t));
if (ctypes.voidptr_t(oldIcon).isNull()) {
console.log("There was no old icon", oldIcon.toString());
}
else {
console.log("There was an old icon already", oldIcon.toString());
// In a perfect world, we should actually kill our old icons
// using DeleteObject...
}
}
console.log("done", badge.src);
}
catch (ex) {
console.error(ex);
}
});
PS: Here is a screenshot from the Task Switcher on XP:
Tested the code on Win7, the icon once applied is real real crappy. In image below, we see the icon in canvas is perfect. In the alt+tab menu the first icon is SUPER crappy, second icon is unbadged so perfect, and third and fifth icons are normal crappy.
Image is here: http://i.stack.imgur.com/47dIr.png
Edit:
Fixed the SUPER crappiness by changing this line for win7, it was 32, 32, i made it 256, 256, no clue why it fixed the SUPER crap:
var hIconBig = LoadImage(targetWindow_handle, OS.Path.join(OS.Constants.Path.desktopDir, 'profilist32.' + osIconFileType), IMAGE_ICON, 256, 256, LR_LOADFROMFILE);
before it was: var hIconBig = LoadImage(targetWindow_handle, OS.Path.join(OS.Constants.Path.desktopDir, 'profilist32.' + osIconFileType), IMAGE_ICON, 32, 32, LR_LOADFROMFILE);
However the usual crap, black rough edge remains.

Why does chrome struggle to display lots of images on a canvas when the other browsers don't?

We're working with the HTML5 canvas, displaying lots of images at one time.
This is working pretty well but recently we've had a problem with chrome.
When drawing images on to a canvas you seem to reach a certain point where the performance degrades very quickly.
It's not a slow effect, it seems that you go right from 60fps to 2-4fps.
Here's some reproduction code:
// Helpers
// https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Math/random
function getRandomInt(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; }
// http://www.paulirish.com/2011/requestanimationframe-for-smart-animating/
window.requestAnimFrame = (function () { return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || function (callback) { window.setTimeout(callback, 1000 / 60); }; })();
// https://github.com/mrdoob/stats.js
var Stats = function () { var e = Date.now(), t = e; var n = 0, r = Infinity, i = 0; var s = 0, o = Infinity, u = 0; var a = 0, f = 0; var l = document.createElement("div"); l.id = "stats"; l.addEventListener("mousedown", function (e) { e.preventDefault(); y(++f % 2) }, false); l.style.cssText = "width:80px;opacity:0.9;cursor:pointer"; var c = document.createElement("div"); c.id = "fps"; c.style.cssText = "padding:0 0 3px 3px;text-align:left;background-color:#002"; l.appendChild(c); var h = document.createElement("div"); h.id = "fpsText"; h.style.cssText = "color:#0ff;font-family:Helvetica,Arial,sans-serif;font-size:9px;font-weight:bold;line-height:15px"; h.innerHTML = "FPS"; c.appendChild(h); var p = document.createElement("div"); p.id = "fpsGraph"; p.style.cssText = "position:relative;width:74px;height:30px;background-color:#0ff"; c.appendChild(p); while (p.children.length < 74) { var d = document.createElement("span"); d.style.cssText = "width:1px;height:30px;float:left;background-color:#113"; p.appendChild(d) } var v = document.createElement("div"); v.id = "ms"; v.style.cssText = "padding:0 0 3px 3px;text-align:left;background-color:#020;display:none"; l.appendChild(v); var m = document.createElement("div"); m.id = "msText"; m.style.cssText = "color:#0f0;font-family:Helvetica,Arial,sans-serif;font-size:9px;font-weight:bold;line-height:15px"; m.innerHTML = "MS"; v.appendChild(m); var g = document.createElement("div"); g.id = "msGraph"; g.style.cssText = "position:relative;width:74px;height:30px;background-color:#0f0"; v.appendChild(g); while (g.children.length < 74) { var d = document.createElement("span"); d.style.cssText = "width:1px;height:30px;float:left;background-color:#131"; g.appendChild(d) } var y = function (e) { f = e; switch (f) { case 0: c.style.display = "block"; v.style.display = "none"; break; case 1: c.style.display = "none"; v.style.display = "block"; break } }; var b = function (e, t) { var n = e.appendChild(e.firstChild); n.style.height = t + "px" }; return { REVISION: 11, domElement: l, setMode: y, begin: function () { e = Date.now() }, end: function () { var f = Date.now(); n = f - e; r = Math.min(r, n); i = Math.max(i, n); m.textContent = n + " MS (" + r + "-" + i + ")"; b(g, Math.min(30, 30 - n / 200 * 30)); a++; if (f > t + 1e3) { s = Math.round(a * 1e3 / (f - t)); o = Math.min(o, s); u = Math.max(u, s); h.textContent = s + " FPS (" + o + "-" + u + ")"; b(p, Math.min(30, 30 - s / 100 * 30)); t = f; a = 0 } return f }, update: function () { e = this.end() } } }
// Firefox events suck
function getOffsetXY(eventArgs) { return { X: eventArgs.offsetX == undefined ? eventArgs.layerX : eventArgs.offsetX, Y: eventArgs.offsetY == undefined ? eventArgs.layerY : eventArgs.offsetY }; }
function getWheelDelta(eventArgs) { if (!eventArgs) eventArgs = event; var w = eventArgs.wheelDelta; var d = eventArgs.detail; if (d) { if (w) { return w / d / 40 * d > 0 ? 1 : -1; } else { return -d / 3; } } else { return w / 120; } }
// Reproduction Code
var stats = new Stats();
document.body.appendChild(stats.domElement);
var masterCanvas = document.getElementById('canvas');
var masterContext = masterCanvas.getContext('2d');
var viewOffsetX = 0;
var viewOffsetY = 0;
var viewScaleFactor = 1;
var viewMinScaleFactor = 0.1;
var viewMaxScaleFactor = 10;
var mouseWheelSensitivity = 10; //Fudge Factor
var isMouseDown = false;
var lastMouseCoords = null;
var imageDimensionPixelCount = 25;
var paddingPixelCount = 2;
var canvasDimensionImageCount = 50;
var totalImageCount = Math.pow(canvasDimensionImageCount, 2);
var images = null;
function init() {
images = createLocalImages(totalImageCount, imageDimensionPixelCount);
initInteraction();
renderLoop();
}
function initInteraction() {
var handleMouseDown = function (eventArgs) {
isMouseDown = true;
var offsetXY = getOffsetXY(eventArgs);
lastMouseCoords = [
offsetXY.X,
offsetXY.Y
];
};
var handleMouseUp = function (eventArgs) {
isMouseDown = false;
lastMouseCoords = null;
}
var handleMouseMove = function (eventArgs) {
if (isMouseDown) {
var offsetXY = getOffsetXY(eventArgs);
var panX = offsetXY.X - lastMouseCoords[0];
var panY = offsetXY.Y - lastMouseCoords[1];
pan(panX, panY);
lastMouseCoords = [
offsetXY.X,
offsetXY.Y
];
}
};
var handleMouseWheel = function (eventArgs) {
var mouseX = eventArgs.pageX - masterCanvas.offsetLeft;
var mouseY = eventArgs.pageY - masterCanvas.offsetTop;
var zoom = 1 + (getWheelDelta(eventArgs) / mouseWheelSensitivity);
zoomAboutPoint(mouseX, mouseY, zoom);
if (eventArgs.preventDefault !== undefined) {
eventArgs.preventDefault();
} else {
return false;
}
}
masterCanvas.addEventListener("mousedown", handleMouseDown, false);
masterCanvas.addEventListener("mouseup", handleMouseUp, false);
masterCanvas.addEventListener("mousemove", handleMouseMove, false);
masterCanvas.addEventListener("mousewheel", handleMouseWheel, false);
masterCanvas.addEventListener("DOMMouseScroll", handleMouseWheel, false);
}
function pan(panX, panY) {
masterContext.translate(panX / viewScaleFactor, panY / viewScaleFactor);
viewOffsetX -= panX / viewScaleFactor;
viewOffsetY -= panY / viewScaleFactor;
}
function zoomAboutPoint(zoomX, zoomY, zoomFactor) {
var newCanvasScale = viewScaleFactor * zoomFactor;
if (newCanvasScale < viewMinScaleFactor) {
zoomFactor = viewMinScaleFactor / viewScaleFactor;
} else if (newCanvasScale > viewMaxScaleFactor) {
zoomFactor = viewMaxScaleFactor / viewScaleFactor;
}
masterContext.translate(viewOffsetX, viewOffsetY);
masterContext.scale(zoomFactor, zoomFactor);
viewOffsetX = ((zoomX / viewScaleFactor) + viewOffsetX) - (zoomX / (viewScaleFactor * zoomFactor));
viewOffsetY = ((zoomY / viewScaleFactor) + viewOffsetY) - (zoomY / (viewScaleFactor * zoomFactor));
viewScaleFactor *= zoomFactor;
masterContext.translate(-viewOffsetX, -viewOffsetY);
}
function renderLoop() {
clearCanvas();
renderCanvas();
stats.update();
requestAnimFrame(renderLoop);
}
function clearCanvas() {
masterContext.clearRect(viewOffsetX, viewOffsetY, masterCanvas.width / viewScaleFactor, masterCanvas.height / viewScaleFactor);
}
function renderCanvas() {
for (var imageY = 0; imageY < canvasDimensionImageCount; imageY++) {
for (var imageX = 0; imageX < canvasDimensionImageCount; imageX++) {
var x = imageX * (imageDimensionPixelCount + paddingPixelCount);
var y = imageY * (imageDimensionPixelCount + paddingPixelCount);
var imageIndex = (imageY * canvasDimensionImageCount) + imageX;
var image = images[imageIndex];
masterContext.drawImage(image, x, y, imageDimensionPixelCount, imageDimensionPixelCount);
}
}
}
function createLocalImages(imageCount, imageDimension) {
var tempCanvas = document.createElement('canvas');
tempCanvas.width = imageDimension;
tempCanvas.height = imageDimension;
var tempContext = tempCanvas.getContext('2d');
var images = new Array();
for (var imageIndex = 0; imageIndex < imageCount; imageIndex++) {
tempContext.clearRect(0, 0, imageDimension, imageDimension);
tempContext.fillStyle = "rgb(" + getRandomInt(0, 255) + ", " + getRandomInt(0, 255) + ", " + getRandomInt(0, 255) + ")";
tempContext.fillRect(0, 0, imageDimension, imageDimension);
var image = new Image();
image.src = tempCanvas.toDataURL('image/png');
images.push(image);
}
return images;
}
// Get this party started
init();
And a jsfiddle link for your interactive pleasure:
http://jsfiddle.net/BtyL6/14/
This is drawing 50px x 50px images in a 50 x 50 (2500) grid on the canvas. I've also quickly tried with 25px x 25px and 50 x 50 (2500) images.
We have other local examples that deal with bigger images and larger numbers of images and the other browser start to struggle with these at higher values.
As a quick test I jacked up the code in the js fiddle to 100px x 100px and 100 x 100 (10000) images and that was still running at 16fps when fully zoomed out. (Note: I had to lower the viewMinScaleFactor to 0.01 to fit it all in when zoomed out.)
Chrome on the other hand seems to hit some kind of limit and the FPS drops from 60 to 2-4.
Here's some info about what we've tried and the results:
We've tried using setinterval rather than requestAnimationFrame.
If you load 10 images and draw them 250 times each rather than 2500 images drawn once each then the problem goes away. This seems to indicate that chrome is hitting some kind of limit/trigger as to how much data it's storing about the rendering.
We have culling (not rendering images outside of the visual range) in our more complex examples and while this helps it's not a solution as we need to be able to show all the images at once.
We have the images only being rendered if there have been changes in our local code, against this helps (when nothing changes, obviously) but it isn't a full solution because the canvas should be interactive.
In the example code we're creating the images using a canvas, but the code can also be run hitting a web service to provide the images and the same behaviour (slowness) will be seen.
We've found it very hard to even search for this issue, most results are from a couple of years ago and woefully out of date.
If any more information would be useful then please ask!
EDIT: Changed js fiddle URL to reflect the same code as in the question. The code itself didn't actually change, just the formatting. But I want to be consistent.
EDIT: Updated jsfiddle and and code with css to prevent selection and call requestAnim after the render loop is done.
In Canary this code freezes it on my computer. As to why this happens in Chrome the simple answer is that it uses a different implementation than f.ex. FF. In-depth detail I don't know, but there is obviously room for optimizing the implementation in this area.
I can give some tip however on how you can optimize the given code to make it run in Chrome as well :-)
There are several things here:
You are storing each block of colors as images. This seem to have a huge performance impact on Canary / Chrome.
You are calling requestAnimationFrame at the beginning of the loop
You are clearing and rendering even if there are no changes
Try to (addressing the points):
If you only need solid blocks of colors, draw them directly using fillRect() instead and keep the color indexes in an array (instead of images). Even if you draw them to an off-screen canvas you will only have to do one draw to main canvas instead of multiple image draw operations.
Move requestAnimationFrame to the end of the code block to avoid stacking.
Use dirty flag to prevent unnecessary rendering:
I modified the code a bit - I modified it to use solid colors to demonstrate where the performance impact is in Chrome / Canary.
I set a dirty flag in global scope as true (to render the initial scene) which is set to true each time the mouse move occur:
//global
var isDirty = true;
//mouse move handler
var handleMouseMove = function (eventArgs) {
// other code
isDirty = true;
// other code
};
//render loop
function renderLoop() {
if (isDirty) {
clearCanvas();
renderCanvas();
}
stats.update();
requestAnimFrame(renderLoop);
}
//in renderCanvas at the end:
function renderCanvas() {
// other code
isDirty = false;
}
You will of course need to check for caveats for the isDirty flag elsewhere and also introduce more criteria if it's cleared at the wrong moment. I would store the old position of the mouse and only (in the mouse move) if it changed set the dirty flag - I didn't modify this part though.
As you can see you will be able to run this in Chrome and in FF at a higher FPS.
I also assume (I didn't test) that you can optimize the clearCanvas() function by only drawing the padding/gaps instead of clearing the whole canvas. But that need to be tested.
Added a CSS-rule to prevent the canvas to be selected when using the mouse:
For further optimizing in cases such as this, which is event driven, you don't actually need an animation loop at all. You can just call the redraw when the coords or mouse-wheel changes.
Modification:
http://jsfiddle.net/BtyL6/10/
This was a legitimate bug in chrome.
https://code.google.com/p/chromium/issues/detail?id=247912
It has now been fixed and should be in a chrome mainline release soon.

Categories