After many attempts I managed to create a working custom progress bar, even if customizing it with the OnDraw command, it flickers (I still don't understand why, maybe it could be the continuous redraw with the update command).
I wanted to add a button that would stop the progress bar (then also the script), and maybe add a warning message, however I ran into a problem.
It seems that the buttons don't respond to clicks, either while the progress bar is advancing or on a normal palette.
On a dialog it works, on palette and window it doesn't.
I suspect it's because, to keep a palette open in photoshop, it has to be busy, otherwise it closes. This causes me to be unable to press the button.
I tried everything I could think of, but nothing worked.
There is a way to implement this?
Here is a sample of my code:
progressbar (100);
for ( var e = 0; e < 100; e++ ) {
progressbar.progress ();
}
progressbar.close ();
function progressbar (total) {
var win = new Window ("palette", undefined, undefined, {closeButton: false, borderless: true});
win.orientation = "row";
var barheight = 15;
var halfbarheight = barheight / 2;
var barwidth = 500;
var bar = win.add ("progressbar", [0, 0, barwidth, barheight], 0, total);
bar.onDraw = function () {
with (bar) {
graphics.drawOSControl ();
graphics.newPath ();
graphics.moveTo (halfbarheight, 0);
for (var a = 0; a < Math.PI; a += Math.PI / 100) {
graphics.lineTo ((-halfbarheight * Math.sin (a)) + halfbarheight, (-halfbarheight * Math.cos (a)) + halfbarheight);
}
graphics.lineTo (halfbarheight + (bar.value * ((barwidth - barheight) / total)), barheight);
for (var b = 0; b < Math.PI; b += Math.PI / 100) {
graphics.lineTo ((halfbarheight * Math.sin (b)) + halfbarheight + (bar.value * ((barwidth - barheight) / total)), (halfbarheight * Math.cos (b)) + halfbarheight);
}
graphics.lineTo (halfbarheight, 0);
graphics.closePath ();
graphics.fillPath (graphics.newBrush (graphics.BrushType.SOLID_COLOR, [0.25, 0.83, 0.1]));
}
}
var close = win.add ("iconbutton", [0, 0, 15, 15]);
close.onDraw = function (event) {
with (close) {
graphics.drawOSControl ();
graphics.ellipsePath (0, 0, size[0], size[1]);
graphics.fillPath (graphics.newBrush (graphics.BrushType.SOLID_COLOR, [0.6, 0, 0]));
if (event.mouseOver) {
graphics.fillPath (graphics.newBrush (graphics.BrushType.SOLID_COLOR, [0.85, 0, 0]));
}
}
}
close.onClick = function () {
progressbar.close ();
return;
}
progressbar.close = function () {
win.close ();
}
progressbar.progress = function () {
bar.value++;
$.sleep (100);
win.update ();
}
win.show ();
}
I'm not sure what you're trying to do. So, just in case, here is how I used to build a progressbar (with a text area and the button 'Cancel'):
var iterations = 10;
// show the progress bar
var bar = new Window('palette', 'Progress...');
var stat = bar.add('statictext', undefined, '');
stat.characters = 80;
var progressbar = bar.add('progressbar', [15,15,600,35], 0, iterations);
var bar_visible = true;
var button = bar.add('button', undefined, 'Cancel');
button.onClick = function() { bar.close(); bar_visible = false; }
bar.center();
bar.show();
var counter = 0;
for (var i=0; i<iterations; i++) {
if (bar_visible == false) break;
// update the progress bar
counter++;
progressbar.value = counter;
stat.text = 'Processed ' + counter + ' from ' + iterations;
bar.update();
alert(counter); // <------- do stuff
}
// close the progress bar
bar.close();
Note: this example works fine on Windows, but on macOS the alert window blocks the button 'Cancel'. I used the alert window just to show the concept. Otherwise the 'Cancel' button works fine on macOS: it closes the palette and breaks the loop.
Related
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.
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.
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();
Okay, I am building a game and after implementing a level selection I nearly got my project working. However, there is a error(probably easy to fix) that has caught my eye.
In my create function I create the variable "pages", but when I want to use it in an other function I get the ReferenceError: Uncaught ReferenceError: pages is not defined. I am fairly new to javascript and I also tried to pass the variable in the paramater, the error will go away, but the code won't work as I want to.
I define the pages in the create function and I want to use it in the arrowClicked function
UshanGame.Selection = function(game){};
var thumbRows = 2;
var thumbCols = 3;
var thumbWidth = 128;
var thumbHeight = 128;
var thumbSpacing = 3;
var levelThumbsGroup;
var currentPage = 0;
var leftArrow;
var rightArrow;
UshanGame.Selection.prototype = {
create: function(){
console.log("%c ✔✔ Level Selection Ready! ✔✔", "color:red;");
// array with finished levels and stars collected.
var starsArray = [1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2];
// how many pages are needed to show all levels?
var pages = starsArray.length /(thumbRows * thumbCols);
leftArrow = this.add.button(50, 420, "level_arrows", this.arrowClicked);
leftArrow.anchor.setTo(0.5);
leftArrow.frame = 0;
leftArrow.alpha = 0.3;
rightArrow = this.add.button(270, 420, "level_arrows", this.arrowClicked);
rightArrow.anchor.setTo(0.5);
rightArrow.frame = 1;
// creation of the thumbails group
levelThumbsGroup = this.add.group();
// determining level thumbnails width and height for each page
var levelLength = thumbWidth * thumbCols + thumbSpacing * (thumbCols - 1);
var levelHeight = thumbWidth * thumbRows + thumbSpacing * (thumbRows - 1);
// looping through each page
for(var l = 0; l < pages; l++){
// horizontal offset to have level thumbnails horizontally centered in the page
var offsetX = (this.width-levelLength)/ 2 + game.width * l;
// I am not interested in having level thumbnails vertically centered in the page, but
// if you are, simple replace my "20" with
// (game.height-levelHeight)/2
var offsetY = 20;
// looping through each level thumbnails
for(var i = 0; i < thumbRows; i ++){
for(var j = 0; j < thumbCols; j ++){
var levelNumber = i * thumbCols + j + l *(thumbRows * thumbCols);
var levelThumb = this.add.button(offsetX + j * (thumbWidth + thumbSpacing), offsetY + i * (thumbHeight + thumbSpacing), "levels", this.thumbClicked, this);
levelThumb.frame=starsArray[levelNumber];
levelThumb.levelNumber = levelNumber + 1;
levelThumbsGroup.add(levelThumb);
// if the level is playable, also write level number
if(starsArray[levelNumber] < 4){
var style = {
font: "18px Arial",
fill: "#ffffff"
};
var levelText = this.add.text(levelThumb.x + 5, levelThumb.y + 5, levelNumber + 1,style);
levelText.setShadow(2, 2, 'rgba(0,0,0,0.5)', 1);
levelThumbsGroup.add(levelText);
}
}
}
}
},
thumbClicked: function(button){
// the level is playable, then play the level!!
if(button.frame < 4){
alert("playing level " + button.levelNumber);
} else {
var buttonTween = this.add.tween(button)
buttonTween.to({
x: button.x + thumbWidth / 15
}, 20, Phaser.Easing.Cubic.None);
buttonTween.to({
x: button.x - thumbWidth / 15
}, 20, Phaser.Easing.Cubic.None);
buttonTween.to({
x: button.x + thumbWidth / 15
}, 20, Phaser.Easing.Cubic.None);
buttonTween.to({
x: button.x - thumbWidth / 15
}, 20, Phaser.Easing.Cubic.None);
buttonTween.to({
x: button.x
}, 20, Phaser.Easing.Cubic.None);
buttonTween.start();
}
},
arrowClicked: function(button){
if(button.frame == 1 && currentPage < pages - 1){
leftArrow.alpha = 1;
currentPage++;
if(currentPage == pages - 1){
button.alpha = 0.3;
}
var buttonsTween = this.add.tween(levelThumbsGroup);
buttonsTween.to({
x: currentPage * game.width * -1
}, 500, Phaser.Easing.Cubic.None);
buttonsTween.start();
}
if(button.frame==0 && currentPage>0){
rightArrow.alpha = 1;
currentPage--;
if(currentPage == 0){
button.alpha = 0.3;
}
var buttonsTween = game.add.tween(levelThumbsGroup);
buttonsTween.to({
x: currentPage * game.width * -1
}, 400, Phaser.Easing.Cubic.None);
buttonsTween.start();
}
}
};
You're getting the error because pages is defined within your create function but isn't defined anywhere else. pages doesn't exist in your arrowClicked function.
To fix this, make pages a global variable by adding it alongside your var declarations at the top:
...
var leftArrow;
var rightArrow;
var pages;
Then modify your create function to instead reference this variable (by dropping the var keyword):
// array with finished levels and stars collected.
var starsArray = [1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2];
// how many pages are needed to show all levels?
pages = starsArray.length /(thumbRows * thumbCols);
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.