I have serious performance problems using canvas clip() with chrome.
I have made a test case to illustrate.
Even in a simple case like this the red rectangle blinks as if it takes too much time to redraw, and a CPU profiling shows the clip() method takes about 10% of the CPU.
In my real program, it get to 16% and keep increasing each frame until the canvas almost freezes the browser..
Is there something wrong in my use of clip ?
Thank you for any suggestions,
Regards.
Cause
Insert a beginPath() as rect() adds to the path unlike fillRect()/strokeRect(). What happens here is that the rectangles are accumulating eventually slowing the clipping down over time.
This in combination with using setInterval, which is not able to synchronize with monitor/screen refreshes, worsens the problem.
To elaborate on the latter:
Using setInterval()/setTimeout() can cause tearing which happens when the draw is in the "middle" of its operation not fully completed and a screen refresh occur.
setInterval can only take integer values and you would need for 16.67 ms to synchronize frame-wise (#60Hz). Even if setInterval could take floats it would not synchronize the timing with the monitor timing as the timer mechanism isn't bound to monitor at all.
To solve this always use requestAnimationFrame to synchronize drawings with screen updates. This is directly linked to monitor refreshes and is a more low-level and efficient implementation than the other, and is made for this purpose, hence the name.
Solution embedding both fixes above
See modified bin here.
The code for future visitors:
function draw() {
context.fillStyle = '#000';
context.fillRect(0, 0, width, height);
context.save();
context.beginPath(); /// add a beginPath here
context.rect(0, 0, 100, 100);
context.clip();
context.fillStyle = '#ff0000';
context.fillRect(0, 0, 200, 200);
context.restore();
requestAnimationFrame(draw); /// use rAF here
}
canvas.width = width;
canvas.height = height;
canvas.style.width = width+'px';
canvas.style.height = height+'px';
requestAnimationFrame(draw); /// start loop
PS: If you need to stop the loop inject a condition to run rAF all inside the loop, ie:
if (isPlaying) requestAnimationFrame(draw);
There is BTW no need for closePath() as this will be done implicit for you as by the specs. You can add it if you want, but calling clip() (or fill() etc.) will do this for you (specs is addressing browser implementers):
Open subpaths must be implicitly closed when computing the clipping region
Related
I have to make an analog clock in canvas. Wrote some code and I need help with how to adjust speed of the hands (hours, mins and seconds). When I start the app hands are just spinning around really fast. I am ending up with hands line allover the clock. Is there any way to remove those lines?
I tried adjusting the rotation but that didn't help. I just started learning about canvas and am really not sure how to fix this problem.
window.onload = draw;
function draw() {
let myCanvas = document.getElementById("my-canvas");
if (myCanvas.getContext) {
let ctx = myCanvas.getContext('2d');
var img = new Image();
img.addEventListener('load', function() {
ctx.drawImage(img, 0, 0, 400, 400);
}, false);
img.src = 'image.png';
update(ctx);
} else {
alert("Canvas is not supported.");
}
}
let angle = 0;
function update(ctx) {
ctx.save();
ctx.translate(200, 200);
ctx.rotate((Math.PI / 180) * angle);
ctx.translate(-200, -200);
ctx.beginPath();
ctx.moveTo(200, 200);
ctx.lineTo(200, 150);
ctx.closePath();
ctx.stroke();
ctx.beginPath();
ctx.moveTo(200, 200);
ctx.lineTo(200, 100);
ctx.closePath();
ctx.stroke();
ctx.beginPath();
ctx.moveTo(200, 150);
ctx.lineTo(200, 30);
ctx.closePath();
ctx.stroke();
ctx.restore();
angle++;
requestAnimationFrame(function() {
update(ctx);
});
}
#my-canvas {
border: #cacaca 1px solid;
}
<canvas id="my-canvas" width="400" height="400">
Your web browser does not support canvas element.
</canvas>
I'm not going to write code for you, since this is homework, but I can walk you through some of the changes I think you need to make.
First off, instead of incrementing your "angle" variable for every iteration, I'd suggest using basic geometry or algebra to figure out where the hand needs to be based on what second, minute, or hour you are wanting to display on a 360 degree circle. You already have some of it, you just need to make it work for time, instead of an ever increasing variable. This will help you reuse the "update" method for each hand.
Next, don't use a recursive method. Use a timer. Since this is a clock, I'd suggest setInterval(), but there's some caveats to this, so look at the link below. Also, I would play around with the timer delay to match the smoothness you want. Do you want the hands to smoothly go around the clock face or do you want them to snap to the next number? You can do some of that in your angle calculations too, but I'd suggest starting at 250ms and see how that looks. If it's not smooth enough, reduce the delay.
How to create an accurate timer in javascript?
Instead of calling "update" directly in the "draw" method, you'll instead set up the timer/interval.
Having this as a recursive method prevents other events from happening in your code, so if you have to add other features, they won't work without some complicated logic. Setting up the timer/interval will allow your code to act in a more multi-threaded way, but not true parallel threads. This gets into somewhat advanced topics, so I'll just stop there.
Third, you will want to add parameters to the "update" method for which hand you are updating and the time. This is because you will be calling it from a new method you create that calls "update" for each hand. This new method is the one that is fed into setInterval. You can use these parameters to determine the length and/or width of your clock hands. Doing it this way avoids you having to create duplicate methods for each hand.
Fourth, now that you have the correct code in the question, you need to add back in the line that clears the canvas, as Mike 'Pomax' Kamermans mentions in the comments. This comes after the "cts.save()" line in your "update" method. Also, your clock is apparently a different size than the original code, so your have to clear a larger area.
ctx.clearRect(0, 0, 400, 400);
In JS, I have some performance-critical code that essentially looks like this:
context.clearRect(0, 0, context.canvas.width, context.canvas.height);
context.drawImage(otherImage);
I profile my code with Chrome and I find that the bottleneck is... clearRect.
Wait, what?
This is really stupid. I shouldn't even need to clearRect! I'm currently touching every pixel of context twice (once for clearRect, once for drawImage), which is a total waste. In theory, I should only need to do it once, to directly copy each pixel from otherImage to context.
How can I say something like "please, screw alpha blending and whatever, just replace the contents of context with whatever is in otherImage!"
You can ensure that no blending is done by calling context.globalCompositeOperation = 'source-over'; (default), which performs only alpha blending or context.globalCompositeOperation = 'copy'; which literally just copies the image on top of the existing content (see the examples here)
I'd like to use setInterval for actual steps in a program and requestAnimationFrame for rendering. I was under the impression this would be the perfect mix: rendering speed would not slow down actual progression, so dips in frame rate would only affect frame rate and still sync up. What I got, however, were drops in the setInterval function as well.
An example of what I mean, intentionally bogged down with shadows: http://jsfiddle.net/Ltdz168m/
Reduce or increase the number of refills and you'll see the difference
It would appear that requestAnimationFrame is actually not a solution to rendering lag slowing down JavaScript. How then would I update the logic side of things, which can be done at 60 fps, without hindrance from rendering? Are WebWorkers absolutely necessary?
The problem is that the browser is still only capable of doing one thing at a time. So, if it's rendering, it can't update the position.
When doing stuff to support variable framerates, you should always use Delta Timing. It works something like this:
requestAnimationFrame(function(e) {
document.getElementById('status').innerHTML = "Delta time: "+e;
// lag randomly
while(Math.random() > 0.0000001) {}
requestAnimationFrame(arguments.callee);
});
<div id="status"></div>
As you can see (hopefully), regardless of framerate, the delta time shown goes up consistently. This means you can do, for example, angleFromStart = e/1000*Math.PI*2; and your dot will orbit at precisely 60 RPM.
var angle=0,
radian=Math.PI/180;
var canvas=document.getElementById("canvas"),
context=canvas.getContext("2d");
context.shadowColor="black";
context.shadowBlur=100;
requestAnimationFrame(function draw(e) {
angle = e/1000*Math.PI*2;
var x=canvas.width/2+Math.cos(angle)*canvas.width/4,
y=canvas.height/2+Math.sin(angle)*canvas.height/4;
context.clearRect(0, 0, canvas.width, canvas.height);
context.beginPath();
context.arc(x, y, 5, 0, Math.PI*2);
context.closePath();
for(var i=0; i<255; i++) {
context.fill();
}
requestAnimationFrame(draw);
});
#canvas {
border: 1px solid black;
}
<canvas id="canvas"></canvas>
PS: I love the new Stack Snippet feature!
I am trying to align bars which are synced to music (therefore moving) to a circle on a canvas. I already have the sync to music and make a round circle with it ready.
Right now I am trying to rotate them so it looks good, however since this is my first attempt with canvas I am failing miserably..
Here is the code Gist.
If i run it with the c.rotate(bar[i].rot); it gets all scrambled...
Please can you help me out with this.
Thank you very much.
First thing :
- Clear the whole canvas on each frame. It's too complicated, and performance-wise not worth the trouble to erase only what's required.
Second :
The Canvas's RenderingContext2D, is a context, meaning it's a state machine that will modify its state each time you perform a change on it.
This change might be a transform : translate, scale, rotate.
Or affect the rendering : globalAlpha, globalCompositeOperation, shadows.
Or be a strokeStyle/fillStyle/font change.
Any time you change the context's state, it is a very good practice to save it before, and restore it after :
context.save();
context.translate(.., ..);
context.beginPath();
context.move();
context.restore();
this way you end up with a context just as 'clean' after the call than before, no question asked.
For your code, you were quite close, for instance this code is quite ok (using random values) :
function animate() {
requestAnimationFrame(animate);
// clear whole screen
context.clearRect(0,0,600,600);
context.fillStyle = '#000';
// rotating step
var angle = 2*Math.PI/cnt ;
// save context
context.save();
context.translate(300,300);
for (var i=0; i<cnt; i++) {
context.rotate(angle);
var val = values[i];
context.fillRect(-10, 100, 20, 80*val );
values[i]+=(Math.random()-0.5)/20;
}
// restore now we're done.
context.restore();
}
you can try it here :
http://jsbin.com/haxeqaza/1/edit?js,output
do not hesitate to comment // animate() and to launch animate2() instead, with 2 nice little trick.
:-)
All transformations add up, unless you use the contexts .save() and .restore() methods. They save the current transformation and restore it afterwards, so transformations in beteeen will not affect the next draw.
But since you want to rotate each bar by a fixed amount, I suggest you do exactly that and set the rotation for each bar so a fixed amount instead of increasing it and let the rotations add up.
I'm working on a javascript game that simulates gravitational forces. It uses the HTML5 canvas element to draw 2D ellipses for planets. I test my game in Google Chrome. Here's a link to the game: http://gravitygame.hostingsiteforfree.com/index.php?page=playHTML
Up until May 24th, it worked just fine. However, after Chrome upgraded from 26.0.1410.64 to 27.0.1453.94, the filled ellipses are sometimes not drawn. It doesn't happen every time I load my game, and I've never gotten it to break while running locally.
Here's a screenshot of the game working:
And here's a screenshot that shows it not filling the ellipses:
I can't tell what's happening. I'll include the portion of the loop that draws all of the planets. I've modified it for readability.
var i = bodies.length;
while(i--){
var I = bodies[i];
var planetRad = (I.width/2)*_scale;
if(_showTrails){
//draw the planet's trail
}
if(//the planet is completely off the screen){
//draw a red planet on the edge of the screen
ctx.beginPath();
ctx.arc(nX, nY, 2.5, 0, TWOPI);
ctx.fillStyle = offScreenColor;
ctx.fill();
ctx.strokeStyle = offScreenOutline;
ctx.stroke();
}
else{
//draw planet
ctx.beginPath();
ctx.arc(nX, nY, (I.width/2)*_scale, 0, TWOPI);
ctx.closePath();
ctx.fillStyle = I.bodyColor;
ctx.fill();
}
if(_showMotionVector){
//draw a line from the center of a planet showing the direction and speed it's travelling
ctx.strokeStyle = motionColor;
ctx.beginPath();
ctx.moveTo(I.getScX(), I.getScY());
ctx.lineTo(I.motion.x * _scale * 12 + I.getScX(), I.motion.y * _scale * 12 + I.getScY());
ctx.stroke();
}
}
Why would it suddenly break on occasion?
I took a look at your online code and discovered you are using setInterval for the animation loop.
This is most likely the reason as if the code is not able to finish calling the calcs etc. you run the risk of stacking calls - for context that means you can have path's that reset each other.
Try first to replace setInterval with setTimeout. You will of course need to retrigger it again from within the code - better yet, put everything in a function with a setTimeout at the end of that function, ie.:
function animate() {
//... calcs and redraws which you have in setInterval
setTimeout(animate, 0);
}
animate();
I use 0 for timeout here for this test. setTimeout/setInterval won't sync to screen refresh rate in any case.
If that works then you know the reason. The next step would be to replace it with requestAnimationFrame, but let me know how it goes.
In an attempt to illustrate the problem we can look at this illustration:
Each block represent a function within the loop, and one loop is one color. Remember that setInterval calls at fixed intervals while setTimeout calls relative to when it's called. In this example the functions perform within the time budget so everything goes well.
In the next illustration:
the spending is outside the budget so setInterval is called again and queues up next call to the second loop before the first has finished. When the queue is processed between the calls you end up risking having two functions working on the context at the "same time" (or comes in a different order than you might expect).
Javascript is of course single-threaded so they do not execute at the same time, but one is held at wait - if the first block for next queue is called before the last block has time to be called then the first block will modify the context and perhaps even change the path before the last call of the previous call is invoked. Over time the lag-behind will increase and potentially (unless some extra available processing resources resolves the queue now and then - on a busy system this is less likely to happen) become worse and worse as more stacking occur.
Ie, in this case you could have lines added to the context with beginPath() before arc got filled.
(hope that made any sense...)
Using setTimeout will prevent this as it won't be executed before all calls in the animation loop has returned. The better option is to use requestAnimationFrame as this will call in sync with the screen-refresh rate, also when possible. It's more low-level and therefor also more efficient.
Another path (no pun intended) is to use Web-workers to do the calculations. This will be multi-threaded and can increase overall performance as a web-worker does not affect the UI thread.