Canvas redrawing/erasing when window resizes - javascript

Im having a few issues resetting my canvas when the window re-sizes or after a given amount of time. I want to complete reset and have it fresh. The problem is if your wait a few minutes (Because the refresh runs) or you re-size your window, everything starts to go haywire. I believe its because its drawing the canvas over top of an existing one and that's whats bleeding through. Any ideas on how to resolve this?
// Constellations
function constellations() {
var pr = (function () {
var ctx = document.createElement("canvas").getContext("2d"),
dpr = window.devicePixelRatio || 1,
bsr = ctx.webkitBackingStorePixelRatio ||
ctx.mozBackingStorePixelRatio ||
ctx.msBackingStorePixelRatio ||
ctx.oBackingStorePixelRatio ||
ctx.backingStorePixelRatio || 1;
return dpr / bsr;
})();
function t() {
this.x = Math.random() * o.width, this.y = Math.random() * o.height, this.vx = -.5 + Math.random(), this.vy = -.5 + Math.random(), this.radius = Math.random()
}
function d() {
for (a.clearRect(0, 0, o.width, o.height), i = 0; i < r.nb; i++) r.array.push(new t), dot = r.array[i], dot.create();
dot.line(), dot.animate()
}
var o = document.querySelector("#constellations");
var ratio = pr;
o.width = $(window).width() * ratio;
o.height = $(window).height() * ratio;
o.style.width = $(window).width() + "px";
o.style.height = $(window).height() + "px";
o.style.display = "block";
var n = "#1A2732";
var linecolor = "#FF535A";
a = o.getContext("2d");
a.setTransform(ratio, 0, 0, ratio, 0, 0);
a.clearRect(0, 0, o.width, o.height);
a.fillStyle = n;
a.lineWidth = .1;
a.strokeStyle = linecolor;
var e = {
x: 30 * o.width / 100,
y: 30 * o.height / 100
},
r = {
nb: o.width / 10,
distance: 80,
d_radius: 150,
array: []
};
t.prototype = {
create: function() {
a.beginPath(), a.arc(this.x, this.y, this.radius, 0, 2 * Math.PI, !1), a.fill()
},
animate: function() {
for (i = 0; i < r.nb; i++) {
var t = r.array[i];
t.y < 0 || t.y > o.height ? (t.vx = t.vx, t.vy = -t.vy) : (t.x < 0 || t.x > o.width) && (t.vx = -t.vx, t.vy = t.vy), t.x += t.vx, t.y += t.vy
}
},
line: function() {
for (i = 0; i < r.nb; i++)
for (j = 0; j < r.nb; j++) i_dot = r.array[i], j_dot = r.array[j], i_dot.x - j_dot.x < r.distance && i_dot.y - j_dot.y < r.distance && i_dot.x - j_dot.x > -r.distance && i_dot.y - j_dot.y > -r.distance && i_dot.x - e.x < r.d_radius && i_dot.y - e.y < r.d_radius && i_dot.x - e.x > -r.d_radius && i_dot.y - e.y > -r.d_radius && (a.beginPath(), a.moveTo(i_dot.x, i_dot.y), a.lineTo(j_dot.x, j_dot.y), a.stroke(), a.closePath())
}
};
var refresh = setInterval(d, 1e3 / 30);
$(window).resize(function() {
window.clearInterval(refresh);
constellations();
});
}constellations();
<canvas id="constellations"></canvas>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
To see it working in action. Just re-size the divider and watch. It may even be a case of reseting the function. Im not sure what should be done to resolve this issue. Its a weird one.
https://jsfiddle.net/v4dqgazr/

The problem is, as the browser is being resized, $(window).resize() is triggered continuously as the resizing is in progress. You can use David Walsh's debounce method to solve this issue.
Here is an update demo with event logs
The debounce method looks like this
function debounce(func, wait, immediate) {
var timeout;
return function() {
var context = this, args = arguments;
var later = function() {
timeout = null;
if (!immediate) func.apply(context, args);
};
var callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) func.apply(context, args);
};
};

Just wanted to give some insight on what's happening:
Every instant that the window is being resized, constellations() is being called. Apparently different browsers handle this differently, Dhiraj's answer should help you.
I added code like this, which shows you that many instances are being created and are running:
function constellations(instanceID) {
...
function d() {
console.log("Draw called from instance id: " + instanceID;
...
}
$(window).resize(function() {
window.clearInterval(refresh);
constellations(Math.random());
});
You'll see a lot of log calls being made once you resize.

Related

Javascript setInterval randomly stops

I'm using setInterval to call a function that animates a fractal on a HTML5 canvas. There is also a slider to allow the user to change the quality of the fractal. Everything works fine until I start changing the slider. When I change it, the fractal animation becomes choppy, and eventually the "drawFractal" function stops being called.
Here is the slider HTML:
<input type="range" id="qualitySlider" min="1" max="10"></input>
Here is the javascript (it just generates a fractal):
var count = 0.5;
var slider = document.getElementById("qualitySlider");
var g = document.getElementById("drawingCanvas").getContext("2d");
function drawFractal() {
var cellSize = Math.ceil(slider.value);
//canvas is 700 by 400
g.fillStyle = "black";
g.clearRect(0, 0, 700, 400);
//Eveything from here to the end of this function generates the fractal
var imagC = Math.cos(count)*0.8;
var realC = Math.sin(count)*0.5;
for (x = 0; x < 700; x+=cellSize) {
for (y = 0; y < 400; y+=cellSize) {
var yCoord = (x / 700.0 - 0.5)*3;
var xCoord = (y / 400.0 - 0.5)*3;
var real = xCoord;
var imag = yCoord;
var broken = 0;
for (i = 0; i < 8; i++) {
var temp = real*real - imag*imag + realC;
imag = 2*imag*real + imagC;
real = temp;
if (real*real + imag*imag >= 4) {
broken = true;
break;
}
}
if (!broken) {
g.fillRect(x, y, cellSize, cellSize);
}
}
}
count = count + 0.04;
}
setInterval(drawFractal, 60);
I just need the "drawFractal" function to be called reliably every 60 milliseconds.
This is my improved code. I just used requestAnimationFrame to recursively call the "drawFractal" function. I also restricted the animation to 24 frames/sec with the setTimeout function.
var count = 0.5;
var qualitySlider = document.getElementById("qualitySlider");
var g = document.getElementById("drawingCanvas").getContext("2d");
function drawFractal() {
var cellSize = Math.ceil(qualitySlider.value);
//canvas is 700 by 400
g.fillStyle = "black";
g.clearRect(0, 0, 700, 400);
var imagC = Math.cos(count)*0.8;
var realC = Math.sin(count)*0.5;
for (x = 0; x < 700; x+=cellSize) {
for (y = 0; y < 400; y+=cellSize) {
var yCoord = (x / 700.0 - 0.5)*3;
var xCoord = (y / 400.0 - 0.5)*3;
var real = xCoord;
var imag = yCoord;
var broken = 0;
for (i = 0; i < 8; i++) {
var temp = real*real - imag*imag + realC;
imag = 2*imag*real + imagC;
real = temp;
if (real*real + imag*imag >= 4) {
broken = true;
break;
}
}
if (!broken) {
g.fillRect(x, y, cellSize, cellSize);
}
}
}
count = count + 0.04;
setTimeout(function() {
requestAnimationFrame(drawFractal);
}, 41);
}
drawFractal();
You are using setInterval() to call drawFractal every 60 ms, and then every time drawFractal is executed, you're calling setInterval() again, which is unnecessary. You now have two timers attempting to draw fractals every 60 ms... then you'll have 4, then 8, etc.
You need to either (1) call setInterval() once at the start of program execution and not call it again, or (2) switch to using setTimeout(), and call it at the end of each drawFractal().
I'd use the second option, just in case your fractal ever takes more than 60 ms to draw.

Adobe Animate CC not drawing anything on publish/test. Java-issue? HTML5-canvast document

When I test or publish my project, which is a jigsaw puzzle from this tutorial: https://www.youtube.com/watch?v=uCQuUZs3UGE
Nothing gets drawn except the grey background. No puzzle pices nor any puzzle shape is drawn at all. I also get a warning:
WARNINGS:
Frame numbers in EaselJS start at 0 instead of 1. For example, this affects gotoAndStop and gotoAndPlay calls. (17)
But that should just be a warning, not an error. So might there be an error in my javascript code? This is the code, its fairly simple:
//************
// Initialize;
var numPieces = 16;
for (var i = 0; i = < numPieces; i++)
{
var pieceName = "p" + (i + 1);
var piece = this[pieceName];
if (piece)
{
piece.name = pieceName;
piece.on("mousedown", function(evt)
{
this.scaleX = 1;
this.scaleY = 1;
this.shadow = null;
this.parent.addChild(this);
this.offset = (x:this.x - evt.stageX, y:this.y - evt.stageY);
});
piece.on("pressmove", function (evt)
{
this.x = evt.stageX + this.offset.x;
this.y = evt.stageY + this.offset.y;
});
piece.on("pressup", function(evt)
{
var target = this.parent["t"+this.name.substr(1)];
if (target && hitTestInRange(target, 30) )
{
this.x = target.x;
this.y = target.y;
}
});
}
}
function hitTestInRange( target, range )
{
if (target.x > stage.mouseX - range &&
target.x < stage.mouseX + range &&
target.y > stage.mouseY - range &&
target.y < stage.mouseY + range)
{
return true;
}
return false;
}
I also included a screenshot of the project. screenshot
Thanks in advance

Creating a class of Crafty JS entity (class of a class?)

I am trying to create a class which creates a Crafty entity with specific properties. So far, the functions within the class do not run because 'this' refers to the window object
$(document).ready(function () {
Crafty.init(window.innerWidth, window.innerHeight);
var player = new controller(37,38,39,40);
player.d.color("red").attr({
w: 50,
h: 50,
x: 0,
y: 0
});
// Jump Height = velocity ^ 2 / gravity * 2
// Terminal Velocity = push * (1 / viscosity)
var gravity = 1;
var viscosity = 0.5;
var frame = (1 / 20);
var distanceMultiplier = 10; //pixels per meter
var timeMultiplier = 20; //relative to actual time
var keystart = [];
var keyboard = [];
function controller (controls) {
this.d = Crafty.e();
this.d.addComponent("2D, Canvas, Color, Collision");
this.d.collision();
this.d.mass = 1;
this.d.a = {
extradistance : 0,
velocity : 0,
acceleration : 0,
force : 0,
resistance : 0
};
this.d.a.push = 0;
this.d.v = {
extradistance : 0,
velocity : 0,
acceleration : 0,
force : 0
};
this.d.jumping = true;
this.d.onHit("Collision", function () {
var a = this.d.hit("Collision");
if (a) {
for (var b in a) {
this.d.x = this.d.x - a[b].normal.x * a[b].overlap;
this.d.y = this.d.y - a[b].normal.y * a[b].overlap;
if (a[b].normal.y < -0.5) {
this.d.jumping = false;
}
if (Math.abs(a[b].normal.x) < 0.2) {
this.d.v.velocity = this.d.v.velocity * a[b].normal.y * 0.2;
}
if (Math.abs(a[b].normal.y) < 0.2) {
this.d.a.velocity = this.d.a.velocity * a[b].normal.x * 0.2;
}
}
return;
}
});
this.d.physics = function () {
if (keyboard[arguments[1]] && !this.jumping) {
this.v.velocity = 5;
this.jumping = true;
}
if (keyboard[arguments[1]] && this.jumping) {
var now = new Date();
if (now.getTime() - keystart[arguments[1]].getTime() < 500) {
this.v.velocity = 5;
}
}
if (keyboard[arguments[0]] && keyboard[arguments[2]]) {
this.a.velocity = 0;
} else {
if (keyboard[arguments[0]]) {
this.a.velocity = -3;
}
if (keyboard[arguments[2]]) {
this.a.velocity = 3;
}
}
if (keyboard[arguments[3]]) {
this.v.velocity = -5;
}
this.a.force = this.a.push - this.a.resistance;
this.a.acceleration = this.a.force / this.mass;
this.a.velocity = this.a.velocity + (this.a.acceleration * frame);
this.a.extradistance = (this.a.velocity * frame);
this.a.resistance = this.a.velocity * viscosity;
this.attr({
x: (this.x + (this.a.extradistance * distanceMultiplier))
});
this.v.force = gravity * this.mass;
this.v.acceleration = this.v.force / this.mass;
this.v.velocity = this.v.velocity - (this.v.acceleration * frame);
this.v.extradistance = (this.v.velocity * frame);
this.attr({
y: (this.y - (this.v.extradistance * distanceMultiplier))
});
setTimeout(this.physics, (frame * 1000) / timeMultiplier);
};
this.d.listen = function(){ document.body.addEventListener("keydown", function (code) {
var then = new Date();
if (!keyboard[code.keyCode] && !this.jumping && code.keyCode == arguments[1]) { //only if not yet pressed it will ignore everything until keyup
keyboard[code.keyCode] = true; //start movement
keystart[code.keyCode] = then; //set time
}
if (!keyboard[code.keyCode] && code.keyCode != arguments[1]) { //only if not yet pressed it will ignore everything until keyup
keyboard[code.keyCode] = true; //start movement
keystart[code.keyCode] = then; //set time
}
});
};
}
player.d.physics();
player.d.listen();
document.body.addEventListener("keyup", function (code) {
keyboard[code.keyCode] = false;
});
});
In trying to put the functions as prototypes of the class, I run into a problem.
Crafty.init(500,500);
function block () {
block.d = Crafty.e("2D, Color, Canvas");
block.d.color("red");
block.d.attr({x:0,y:0,h:50,w:50});
}
block.d.prototype.green = function() {
this.color("green");
}
var block1 = new block();
block1.d.color();
If an object is defined in the constructor, I cannot use it to add a prototype to.
Generally in Crafty, we favor composition. That is, you extend an entity by adding more components to it. You can have kind of a hierarchy by having one component automatically add others during init.
I haven't looked through all of your example code, because there's a lot! But consider the second block:
function block () {
block.d = Crafty.e("2D, Color, Canvas");
block.d.color("red");
block.d.attr({x:0,y:0,h:50,w:50});
}
block.d.prototype.green = function() {
this.color("green");
}
var block1 = new block();
block1.d.color();
You're trying to combine Crafty's way of doing things (an entity component system) with classes in a way that's not very idiomatic. Better to do this:
// Define a new component with Crafty.c(), rather than creating a class
Crafty.c("Block", {
// On init, add the correct components and setup the color and dimensions
init: function() {
this.requires("2D, Color, Canvas")
.color("red")
.attr({x:0,y:0,h:50,w:50});
},
// method for changing color
green: function() {
this.color("green");
}
});
// Create an entity with Crafty.e()
block1 = Crafty.e("Block");
// It's not easy being green!
block1.green();

Whack-a-Mole game flaws

I am working a building a simple Javascript Whack-a-mole game. I believe the issue is when by the time the "co-ordinates" for the mouse are read, the picture's X and Y values change.
When playing the game, you click on the picture, and the text that should come up when you successful click does not appear. This will not allow me to change to the picture to the "hit" picture, to let the players know they hit the object.
This is the code:
$(document).ready(function() {
document.body.onmousedown = function() {
return false;
} //so page is unselectable
//Canvas stuff
var canvas = $("#canvas")[0];
var ctx = canvas.getContext("2d");
var w = $("#canvas").width();
var h = $("#canvas").height();
var mx, my;
var player;
var mC;
var mR;
var smackSound = new Audio("audio/boing.wav");
var smackSound2 = new Audio("audio/boing2.wav");
var smackSound3 = new Audio("audio/boing3.wav");
var mel = new Image();
var melHit = new Image();
var melX;
var melY;
var melXref;
var melYref;
/////////////////////////////////
////////////////////////////////
//////// GAME INIT
/////// Runs this code right away, as soon as the page loads.
////// Use this code to get everything in order before the game starts
//////////////////////////////
/////////////////////////////
function init() {
//////////
///STATE VARIABLES
mel.src = "images/mel.jpg";
melHit.src = "images/melCrazy.jpg";
//////////////////////
///GAME ENGINE START
// This starts the game/program
// "paint is the piece of code that runs over and over again.
// "60" sets how fast things should go
if (typeof game_loop != "undefined") clearInterval(game_loop);
game_loop = setInterval(paint, 1000);
}
init();
function generate() {
var random;
random = Math.floor(Math.random() * 4);
while (random == 3) {
random = Math.floor(Math.random() * 4);
}
return random;
}
function posDisplay() {
ctx.fillStyle = "black"
ctx.fillText("Mouse Column: " + mC, 10, 10);
ctx.fillText("Mouse Row: " + mR, 10, 20);
}
///////////////////////////////////////////////////////
//////////////////////////////////////////////////////
//////// Main Game Engine
////////////////////////////////////////////////////
///////////////////////////////////////////////////
function paint() {
ctx.clearRect(0, 0, w, h);
melX = generate() * w / 3;
melY = generate() * h / 3;
//melXref = generate() / w / 3;
//melYref = generate() / h / 3;
//ctx.fillStyle = 'white';
posDisplay()
ctx.drawImage(mel, melX, melY, 200, 200);
if (melXref == mR && melYref == mC && clicker = true) {
ctx.fillStyle = "black";
ctx.fillText("It works!!!!!", 200, 200);
}
if (melX < w / 3 && clicker = true) { // First Column (Mel)
if (melY < h / 3) {
melXref = 1
melYref = 1
// clicker = true;
} else if (melY > h / 3 && melY < h / 1.5) {
melXref = 1
melYref = 2
// clicker = true;
} else if (melY > h / 1.5) {
melXref = 1
melYref = 3
// clicker = true;
}
} else if (melX > w / 3 && melX < w / 1.5 && clicker = true) { // Second Column (Mel)
if (melY < h / 3) {
melXref = 2
melYref = 1
// clicker = true;
} else if (melY > h / 3 && melY < h / 1.5) {
melXref = 2
melYref = 2
// clicker = true;
} else if (melY > h / 1.5) {
melXref = 2
melYref = 3
// clicker = true;
}
} else if (melX > w / 1.5 && clicker = true) { // Third Column (Mel)
if (melY < h / 3) {
melXref = 3
melYref = 1
// clicker = true;
} else if (melY > h / 3 && melY < h / 1.5) {
melXref = 3
melYref = 2
// clicker = true;
} else if (melY > h / 1.5) {
melXref = 3
melYref = 3
// clicker = true;
}
if (melXref == mR && melYref == mC) {
ctx.fillStyle = "black";
ctx.fillText("IT WORKS", 200, 200);
}
ctx.drawImage(mel, melX, melY, 200, 200);
if (melX == mx && melY == my) {
ctx.fillStyle = "black";
ctx.fillText("YESSSSSSSSS", 250, 250);
//ctx.drawImage(melHit,(generate()*200),(generate()*200),200,200);
}
} ////////////////////////////////////////////////////////////////////////////////END PAINT/ GAME ENGINE
}
////////////////////////////////////////////////////////
///////////////////////////////////////////////////////
///// MOUSE LISTENER
//////////////////////////////////////////////////////
/////////////////////////////////////////////////////
/////////////////
// Mouse Click
///////////////
canvas.addEventListener('click', function(evt) {
if (mx < w && my < h) {
clicker = true;
} else {
clicker = false;
}
if (clicker = true && mx < w && my < h) { // Randomizes the sound && only allows audio to be played if it is within the canvas
if (generate() == 1) {
smackSound.play();
} else if (generate() == 2) {
smackSound2.play();
} else if (generate() == 3) {
smackSound3.play();
}
}
}, false);
canvas.addEventListener('mouseout', function() {
pause = true;
}, false);
canvas.addEventListener('mouseover', function() {
pause = false;
}, false);
canvas.addEventListener('mousemove', function(evt) {
var mousePos = getMousePos(canvas, evt);
mx = mousePos.x;
my = mousePos.y;
if (mx < w / 3) { // First Column
if (my < h / 3) {
mC = 1
mR = 1
// clicker = true;
} else if (my > h / 3 && my < h / 1.5) {
mC = 1
mR = 2
//clicker = true;
} else if (my > h / 1.5) {
mC = 1
mR = 3
// clicker = true;
}
} else if (mx > w / 3 && mx < w / 1.5) { // Second Column
if (my < h / 3) {
mC = 2
mR = 1
// clicker = true;
} else if (my > h / 3 && my < h / 1.5) {
mC = 2
mR = 2
// clicker = true;
} else if (my > h / 1.5) {
mC = 2
mR = 3
// clicker = true;
}
} else if (mx > w / 1.5) { // Third Column
if (my < h / 3) {
mC = 3
mR = 1
// clicker = true;
} else if (my > h / 3 && my < h / 1.5) {
mC = 3
mR = 2
// clicker = true;
} else if (my > h / 1.5) {
mC = 3
mR = 3
// clicker = true;
}
}
}, false);
function getMousePos(canvas, evt) {
var rect = canvas.getBoundingClientRect();
return {
x: evt.clientX - rect.left,
y: evt.clientY - rect.top
};
}
///////////////////////////////////
//////////////////////////////////
//////// KEY BOARD INPUT
////////////////////////////////
window.addEventListener('keydown', function(evt) {
var key = evt.keyCode;
//p 80
//r 82
//1 49
//2 50
//3 51
}, false);
})
This is a bit of a shot in the dark (and its too much to put in a comment), but from what I can tell, while you are recording the mouse position in the mousemove event, you are not doing so in the click event. I believe its not so much that the picture's X and Y have changed, it is probably that the last recorded mouse's X and Y have changed due to the mousemove event firing after the click.
A general solution (because it can be solved many different ways) might be to record the mouse's row and column in the click event, and then suppress any mouse position updates in the mousemove event if your clicker variable is true. So maybe something like the following:
canvas.addEventListener('mousemove', function(evt) {
if(!clicker){
// happily record the mouse position, because user hasn't clicked anything
In the paint method, you can check if clicker is true and do the text thing using the last known mouse position previously recorded in the click event, and then reset it back to false when you're done processing. Your paint method is fairly large, and clicker seems to be used throughout so I won't post a full blown code block in the interests of brevity, but I think you get the idea.
ALSO: I notice in your if statements, you're checking the clicker variable using && clicker = true. Notice the single equal sign? That's extremely bad! Instead of checking its value, you're assigned its value. Remember single equals is for assignment, double equals is for equivalence.
You can solve that error a couple of ways; the simplest is to simply make sure you change it to == instead, and anywhere else you might have made that error. Or, since its a boolean, you can simple do && clicker or && !clicker, depending on the condition.
Alternatively, from the looks of your logic, since every if and if else is dependent on whether clicker is true, you can get rid of the redundant checking in each condition and simply wrap the entire thing in oneif`, like so:
if(clicker){
if (melXref == mR && melYref == mC) {
ctx.fillStyle = "black";
ctx.fillText("It works!!!!!", 200, 200);
}
if (melX < w / 3) { // First Column (Mel)
... other conditions
}

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