canvas rendering not smooth - javascript

For some reason my rendering stutters sometimes. As you can see here(http://sirius-btx.com/test/).
Use your arrow keys to move.
The bottom canvas is prerendered only once, the prerender will then be drawn on the main upper canvas every frame.
Here is my code thats gets called every frame:
var tick = (function() {
var lastTimestamp = 0;
return function(timestamp) {
dt = (timestamp - lastTimestamp) / 1000;
lastTimestamp = timestamp;
ctx.fillStyle = "black";
ctx.fillRect(0, 0, self.resolution.width, self.resolution.height);
var cameraPosition = camera.position;
if(keys[37]) {
cameraPosition.x -= 10 * dt;
}
if(keys[38]) {
cameraPosition.y -= 10 * dt;
}
if(keys[39]) {
cameraPosition.x += 10 * dt;
}
if(keys[40]) {
cameraPosition.y += 10 * dt;
}
camera.position = cameraPosition;
var tileSet = self.resource.get("tiles");
ctx.save();
// mapCamera starts at the same position as camera.
// So (mapCamera.position - camera.position) is the offset we have to move.
ctx.translate((mapCamera.position.x - camera.position.x) * 16, (mapCamera.position.y - camera.position.y) * 16);
// mapCanvas is the prerendered canvas.
ctx.drawImage(mapCanvas, -16, -16, self.resolution.width + 32, self.resolution.height + 32);
ctx.restore();
requestAnimationFrame(tick);
};
})();
I've been trying to figure out why its not 100% smooth, but I can't find a solution.
Someone has an idea why it is happening?

3 advices here :
• Call early requestAnimationFrame : with my modest testing , it showed that things were a bit smoother when rAF is the first function called. Most probably because it needs behind-the-scene work to get to work. Try it !
• When using drawImage, coordinates will be rounded for you : you might want to do the rounding yourself, since it is 'floor' that is used, and that 'ceil' that gives smoother results.
• Most importantly, you are using the DOM (divs or like) to show the mouse position / fps. Don't do this. use fillText on your canvas (or on some other visible canvas), because you can't be sure you cause a reflow/repaint when changing those values.

Related

Javascript canvas animation not appearing

I'm trying to create a canvas animation with 2 objects: a circumference and a filled circle. My objective is to make it seem that the circumference represents the circles orbit. However when trying to animate there's no animation and only when I click to stop the page does the image appear with the circle in a random position in the orbit (this means that the moving part works).
Thank you for your time and here's the code:
function restartAnimate(){
runAnimation(0);
setTimeout(restartAnimate(),1000);
}
function runAnimation(i){
let animation = document.getElementById("Animation");
let anim = animation.getContext("2d");
anim.clearRect(0,0,300,150);
anim.save();
anim.strokeStyle = "#99ebff";
anim.lineWidth = 10;
anim.beginPath();
anim.arc(150, 75, 40, 0, 2 * Math.PI);
anim.stroke();
anim.restore();
anim.save()
anim.fillStyle = "#000000";
anim.translate(150,75);
anim.rotate(2 * Math.PI * i / 1000);
anim.translate(-150,-75);
anim.beginPath();
anim.arc(150 + 36.5, 75 ,13, 0, 2 * Math.PI);
anim.fill();
anim.restore();
i += 16;
if(i < 1000) setTimeout(runAnimation(i),16);
}
You should use requestAnimationFrame to animate so that the render results are displayed in sync with the display hardware refresh.
setTimeout is very inaccurate and your function will fall behind over time. If you use requestAnimationFrame you can use the first argument (time in ms) to keep precisely on time.
ctx.save, and ctx.restore can be very expensive calls and should be avoided if you can. As you are only restoring the transform you can set it manually as needed with ctx.setTransform()
There is no need to restart the animation, just let it cycle.
Example rewrites your code with above points in mind and some other changes. See code comments for more info.
// Define constants and query DOM outside animation functions
const canvas = document.getElementById("animCanvas");
const ctx = canvas.getContext("2d");
Math.PI2 = Math.PI * 2;
var startTime;
restartAnimate();
function restartAnimate() {
if (startTime === undefined) {
requestAnimationFrame(runAnimation);
} else {
startTime = 0; // next frame animation we have restarted
}
// setTimeout(restartAnimate(),1000); // No need to restart as angle is cyclic.
}
function runAnimation(time) {
if (!startTime) { startTime = time }
const currentTime = time - startTime;
ctx.setTransform(1,0,0,1,0,0); // resets transform, better than using save and restore
ctx.clearRect(0,0,ctx.canvas.width,ctx.canvas.height); // avoid magic numbers
//ctx.save(); // not needed
ctx.setTransform(1,0,0,1,150, 75); // last two values set the origin
// and is the point we rotate around
ctx.strokeStyle = "#99ebff";
ctx.lineWidth = 10;
ctx.beginPath();
ctx.arc(0, 0, 40, 0, Math.PI2); // rendering at the origin
ctx.stroke();
//ctx.restore(); // not needed
//ctx.save(); // not needed
ctx.fillStyle = "#000000";
//ctx.translate(150,75); // working from origin so don't need to translate
ctx.rotate(Math.PI2 * currentTime / 1000);
//ctx.translate(-150,-75); // working from origin so don't need to translate
ctx.beginPath();
ctx.arc(36.5, 0 ,13, 0, Math.PI2);
ctx.fill();
//ctx.restore(); not needed
requestAnimationFrame(runAnimation);
}
<canvas id="animCanvas"></canvas>

Issue with JS Mouse Wheel Event Delta

I am working on a project that uses mouse events to zoom and pan a canvas. They work fine independently, however, I would like the canvas to pan towards the cursor while zooming.
My objective: have the cursor location be where you are zooming towards. I have been playing around with this for some time and I was able to get it working.
I did notice something odd. If you reload the page with the cursor over the canvas panning while zooming did not work. What would happen instead, is, the canvas would zoom in but displacement.x and displacement.y would be unchanged.
I believe it is because of event.delta. It is as if event.delta = 0 upon reload only if the cursor location is over the canvas.
Here are my mouse events:
function panCanvasOnZoom(event) {
var sensitivity = 0.05 * event.delta;
var halfSize = { w: canvasSize.w / 2, h: canvasSize.h / 2 }
var centerOrientedMouse = { x: halfSize.w - pmouseX, y: halfSize.h - pmouseY }
var relativeMousePercentage = { x: pmouseX / canvasSize.w, y: pmouseY / canvasSize.h }
var panX = (halfSize.w - pmouseX) * sense * relativeMousePercentage.x;
var panY = (halfSize.h - pmouseY) * sense * relativeMousePercentage.y;
panX += panX * relativeMousePercentage.x;
panY += panY * relativeMousePercentage.y;
displacement.x += panX;
displacement.y += panY;
}
function mouseWheel(event) {
if (pmouseX < 0 || pmouseX > canvasSize.w ||
pmouseY < 0 || pmouseY > canvasSize.h)
return;
panCanvasOnZoom(event);
displacement.z += 0.05 * event.delta;
displacement.z = constrain(displacement.z, 0.05, 9.00);
return false;
}
I am calculating the sensitivity using the event.delta. When I noticed this issue, I began debugging and realized that if I replaced var sensitivity = 0.05 * event.delta with var sensitivity = 0.05 it would essentially replicate the cursor issue.
My question is, is there a way to circumvent event.delta = 0? I do not mean an if statement to check for the condition. I mean something along the lines of setting event.delta to something besides 0 upon initializing.
Also, if you see any improvements regarding the efficiency of panCanvasOnZoom() please post those.

Make clearRect() of canvas work faster

I am trying to design a traveling sine wave in JavaScript, but the design appears quite slow. The main bottleneck is the clearRect() for canvas clearing.
How can I solve this?
Also I am drawing the pixel by ctx.fillRect(x, y,1,1), but when I clear using clearRect(x, y,1,1), it leaves some footprints. Instead I have to do clearRect(x, y,5,5) to get proper clearing. What can be the work around?
/******************************/
var x = 0;
var sineval = [];
var offset = 0;
var animFlag;
function init() {
for(var i=0; i<=1000; ++i){
sineval[i] = Math.sin(i*Math.PI/180);
}
// Call the sineWave() function repeatedly every 1 microseconds
animFlag = setInterval(sineWave, 1);
//sineWave();
}
function sineWave()
{ //console.log('Drawing Sine');
var canvas = document.getElementById("canvas");
if (canvas.getContext) {
var ctx = canvas.getContext("2d");
}
for(x=0 ; x<1000 ;++x){
// Find the sine of the angle
//var i = x % 361;
var y = sineval[x+offset];
// If the sine value is positive, map it above y = 100 and change the colour to blue
if(y >= 0)
{
y = 100 - (y-0) * 70;
ctx.fillStyle = "green";
}
// If the sine value is negative, map it below y = 100 and change the colour to red
if( y < 0 )
{
y = 100 + (0-y) * 70;
ctx.fillStyle = "green";
}
// We will use the fillRect method to draw the actual wave. The length and breath of the
if(x == 0) ctx.clearRect(0,y-1,5,5);
else ctx.clearRect(x,y,5,5);
ctx.fillRect(x, y,1,1 /*Math.sin(x * Math.PI/180) * 5, Math.sin(x * Math.PI/180 * 5)*/);
}
offset = (offset > 360) ? 0 : ++offset ;
}
You need to refactor the code a bit:
Move all global variables such as canvas and context outside of the loop function
Inside the loop, clear full canvas at beginning, redraw sine
Use requestAnimationFrame instead of setInterval
Replace fillRect() with rect() and do a single fill() outside the inner for-loop
Using a timeout value of 1 ms will potentially result in blocking the browser, or at least slow it down noticeably. Considering that a monitor update only happens every 16.7ms this will of course be wasted cycles. If you want to reduce/increase the speed of the sine you can reduce/increase the incremental step instead.
In essence:
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
var sineval = [];
var offset = 0;
init();
function init() {
for (var i = 0; i <= 1000; ++i) {
sineval.push(Math.sin(i * Math.PI / 180));
}
// Call the sineWave() function
sineWave();
}
function sineWave() {
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
ctx.beginPath();
ctx.fillStyle = "green";
// draw positive part of sine wave here
for (var x = 0; x < 1000; x++) {
var y = sineval[x + offset];
if (y >= 0) {
y = 100 - (y - 0) * 70;
ctx.rect(x, y, 2, 2);
}
}
ctx.fill();
ctx.beginPath();
ctx.fillStyle = "red";
// draw negative part of sine wave here
for (var x = 0; x < 1000; x++) {
var y = sineval[x + offset];
if (y < 0) {
y = 100 - (y - 0) * 70;
ctx.rect(x, y, 2, 2);
}
}
ctx.fill();
offset = (offset > 360) ? 0 : ++offset;
requestAnimationFrame(sineWave);
}
<canvas id="canvas" width=800 height=500></canvas>
And of course, if you load the script in <head> you need to wrap it in a window.onload block so canvas element is available. Or simply place the script at the bottom of the page if you haven't already.
A few speedups and odd ends:
In init, set up the sine wave pixel values one time.
Use typed arrays for these since sticking with integers is faster than using floats if possible.
We will manipulate the pixel data directly instead of using fill and clear. To start this, in init we call ctx.getImageData one time. We also just one time max the alpha value of all the pixels since the default 0 value is transparent and we want full opacity at 255.
Use setInterval like before. We want to update the pixels at a steady rate.
Use 'adj' as knob to adjust how fast the sine wave moves on the screen. The actual value (a decimal) will depend on the drawing frame rate. We use Date.now() calls to keep track of milliseconds consumed across frames. So the adjustment on the millisecond is mod 360 to set the 'offset' variable. Thus offset value is not inc by 1 every frame but instead is decided based on the consumption of time. The adj value could later be connected to gui if want.
At end of work (in sineWave function), we call requestAnimationFrame simply to do the ctx.putImageData to the canvas,screen in sync to avoid tearing. Notice 'paintit' function is fast and simple. Notice also that we still require setInterval to keep steady pace.
In between setting the offset and calling requestAnimationFrame, we do two loops. The first efficiently blackens out the exact pixels we drew from the prior frame (sets to 0). The second loop draws the new sine wave. Top half of wave is green (set the G in pixel rgba to 255). Bottom half is red (set the R pixel rgba to 255).
Use the .data array to paint a pixel, and index it to the pixel using 4x + 4y*canvas.width. Add 1 more if want the green value instead of the red one. No need to touch the blue value (byte offset 2) nor the already set alpha (byte offset 3).
The >>>0 used in some places turns the affected value into an unsigned integer if it wasn't already. It can also be used instead of Math.ceil. .data is typed Array already I think.
This answer is rather late but it addresses some issues brought up in comments or otherwise not yet addressed. The question showed up during googling.
Code hasn't been profiled. It's possible some of the speedups didn't speed anything up; however, the cpu consumption of firefox was pretty light by the end of the adjustments. It's set to run at 40 fps. Make 'delay' smaller to speed it up and tax cpu more.
var sineval;
var offset = 0;
var animFlag;
var canvas;
var ctx;
var obj;
var milli;
var delay=25;
var adj=1/delay; // .04 or so for 25 delay
function init() {
canvas = document.getElementById("canvas");
ctx = canvas.getContext("2d");
obj=ctx.getImageData(0,0,canvas.width,canvas.height);
for (let i=0; i<obj.data.length; i+=4) {
obj.data[i+3]=255; //set all alpha to full one time only needed.
}
sineval=new Uint8Array(1400); //set up byte based table of final pixel sine values.. 1400 degrees total
for (let i=0; i<=1400; ++i) { //1400
sineval[i] = (100-70*Math.sin(i*Math.PI/180))>>>0;
}
animFlag = setInterval(sineWave, delay); //do processing once every 25 milli
milli=Date.now()>>>0; //start time in milli
}
function sineWave() {
let m=((Date.now()-milli)*adj)>>>0;
let oldoff = offset;
offset=(m % 360)>>>0; //offset,frequency tuned with adj param.
for(x=0 ; x<1000 ;++x) { //draw sine wave across canvas length of 1000
let y=sineval[x+oldoff];
obj.data [0+x*4+y*4*canvas.width]=0; //black the reds
obj.data [1+x*4+y*4*canvas.width]=0; //black the greens
}
for(x=0 ; x<1000 ;++x) { //draw sine wave across canvas length of 1000
let y=sineval[x+offset];
if (y<100) {
obj.data [1+x*4+y*4*canvas.width]=255; //rGba //green for top half
} else {
obj.data [0+x*4+y*4*canvas.width]=255; //Rgba //red for bottom half
}
}
requestAnimationFrame(paintit); //at end of processing try to paint next frame boundary
}
function paintit() {
ctx.putImageData(obj,0,0);
}
init();
<canvas id="canvas" height=300 width=1000></canvas>

Drawing image in a loop onto canvas - how could I optimize this code?

I am drawing a background image on a canvas element. I create a loop with requestAnimationFrame. In this loop, I draw an image onto the canvas with the appropriate coordinates.
The animation seems to be smooth, in Chrome 60 fps, but I have few glitches every now and then. It's worse in Firefox than in Chrome. It's better when I view it with a clean profile, without open tabs - but it's still not perfect.
Here is the full source: http://jsbin.com/vopiw/1/edit?html,output
This function gets called in every frame:
function draw(delta) {
totalSeconds += delta;
var vx = 100; // the background scrolls with a speed of 100 pixels/sec
var numImages = Math.ceil(canvas.width / img.width) + 1;
var xpos = totalSeconds * vx % img.width;
context.save();
context.translate(-xpos, 0);
for (var i = 0; i < numImages; i++) {
context.drawImage(img, i * img.width, 0);
}
context.restore();
}
Can you spot anything, which could be a real performance drawback?
What I have found so far:
memory consumption is growing slightly but constantly
BUT there is no garbage collection happening, which could be blamed for the glitches
Do you maybe have any clues?
Use the image as a background image on the element itself and use background position to scroll it.
Instead of the img onload just go directly into the code:
(function imageLoaded() {
canvas.style.backgroundImage = 'url(...)';
canvas.style.backgroundRepeat = 'repeat-x';
draw(0);
...
Then just update the draw() method with something like this:
// cache these
var iw = 400,
cw = canvas.width;
function draw(delta) {
totalSeconds += delta;
var vx = 100; // if always 100 just insert the value directly below
var numImages = ((cw / iw)|0) + 1; // use logic OR to remove fractions
var xpos = totalSeconds * vx % iw;
// update background position
canvas.style.backgroundPosition = (-xpos + iw) + 'px 0';
}
The second issue is the way you calculate the time delta. Using the low-resolution timer can add to the jerky-ness.
Try to use the built-in high-resolution timer instead. Luckily the rAF provides a high-res time stamp which you can use instead:
function loop(now) { // use argument from rAF (hi-res timestamp)
if (!looping) {
return;
}
requestAnimationFrame(loop);
var deltaSeconds = (now - lastFrameTime) * 0.001; //mul is faster than div
lastFrameTime = now;
draw(deltaSeconds);
}
Modified jsbin
This hands the drawing action to the browser but have in mind that the gain is not all that. The reason is that the drawImage() method is pretty fast in itself but you are saving a few steps in the JavaScript which is the real bottle-neck (canvas is very fast in itself despite the myth) and the repetition of these draw operations are left to internal compiled code in the browser.
Other factors that influence the smoothness is the hardware clock and hardware capability in general as well as other things going on in the browser.
I would also put that canvas element on an absolute or fixed position as the browser will give the element a separate bitmap (not related to canvas bitmap) for it which could improve the CSS background performance (not shown in the modified jsbin).

Simulate a physical 3d ball throw on a 2d js canvas from mouse click into the scene

I'd like to throw a ball (with an image) into a 2d scene and check it for a collision when it reached some distance. But I can't make it "fly" correctly. It seems like this has been asked like a million times, but with the more I find, the more confused I get..
Now I followed this answer but it seems, like the ball behaves very different than I expect. In fact, its moving to the top left of my canvas and becoming too little way too fast - ofcouse I could adjust this by setting vz to 0.01 or similar, but then I dont't see a ball at all...
This is my object (simplyfied) / Link to full source who is interested. Important parts are update() and render()
var ball = function(x,y) {
this.x = x;
this.y = y;
this.z = 0;
this.r = 0;
this.src = 'img/ball.png';
this.gravity = -0.097;
this.scaleX = 1;
this.scaleY = 1;
this.vx = 0;
this.vy = 3.0;
this.vz = 5.0;
this.isLoaded = false;
// update is called inside window.requestAnimationFrame game loop
this.update = function() {
if(this.isLoaded) {
// ball should fly 'into' the scene
this.x += this.vx;
this.y += this.vy;
this.z += this.vz;
// do more stuff like removing it when hit the ground or check for collision
//this.r += ?
this.vz += this.gravity;
}
};
// render is called inside window.requestAnimationFrame game loop after this.update()
this.render = function() {
if(this.isLoaded) {
var x = this.x / this.z;
var y = this.y / this.z;
this.scaleX = this.scaleX / this.z;
this.scaleY = this.scaleY / this.z;
var width = this.img.width * this.scaleX;
var height = this.img.height * this.scaleY;
canvasContext.drawImage(this.img, x, y, width, height);
}
};
// load image
var self = this;
this.img = new Image();
this.img.onLoad = function() {
self.isLoaded = true;
// update offset to spawn the ball in the middle of the click
self.x = this.width/2;
self.y = this.height/2;
// set radius for collision detection because the ball is round
self.r = this.x;
};
this.img.src = this.src;
}
I'm also wondering, which parametes for velocity should be apropriate when rendering the canvas with ~ 60fps using requestAnimationFrame, to have a "natural" flying animation
I'd appreciate it very much, if anyone could point me to the right direction (also with pseudocode explaining the logic ofcourse).
Thanks
I think the best way is to simulate the situation first within metric system.
speed = 30; // 30 meters per second or 108 km/hour -- quite fast ...
angle = 30 * pi/180; // 30 degree angle, moved to radians.
speed_x = speed * cos(angle);
speed_y = speed * sin(angle); // now you have initial direction vector
x_coord = 0;
y_coord = 0; // assuming quadrant 1 of traditional cartesian coordinate system
time_step = 1.0/60.0; // every frame...
// at most 100 meters and while not below ground
while (y_coord > 0 && x_coord < 100) {
x_coord += speed_x * time_step;
y_coord += speed_y * time_step;
speed_y -= 9.81 * time_step; // in one second the speed has changed 9.81m/s
// Final stage: ball shape, mass and viscosity of air causes a counter force
// that is proportional to the speed of the object. This is a funny part:
// just multiply each speed component separately by a factor (< 1.0)
// (You can calculate the actual factor by noticing that there is a limit for speed
// speed == (speed - 9.81 * time_step)*0.99, called _terminal velocity_
// if you know or guesstimate that, you don't need to remember _rho_,
// projected Area or any other terms for the counter force.
speed_x *= 0.99; speed_y *=0.99;
}
Now you'll have a time / position series, which start at 0,0 (you can calculate this with Excel or OpenOffice Calc)
speed_x speed_y position_x position_y time
25,9807687475 14,9999885096 0 0 0
25,72096106 14,6881236245 0,4286826843 0,2448020604 1 / 60
25,4637514494 14,3793773883 0,8530785418 0,4844583502 2 / 60
25,2091139349 14,0737186144 1,2732304407 0,7190203271
...
5,9296028059 -9,0687933774 33,0844238036 0,0565651137 147 / 60
5,8703067779 -9,1399704437 33,1822622499 -0,0957677271 148 / 60
From that sheet one can first estimate the distance of ball hitting ground and time.
They are 33,08 meters and 2.45 seconds (or 148 frames). By continuing the simulation in excel, one also notices that the terminal velocity will be ~58 km/h, which is not much.
Deciding that terminal velocity of 60 m/s or 216 km/h is suitable, a correct decay factor would be 0,9972824054451614.
Now the only remaining task is to decide how long (in meters) the screen will be and multiply the pos_x, pos_y with correct scaling factor. If screen of 1024 pixels would be 32 meters, then each pixel would correspond to 3.125 centimeters. Depending on the application, one may wish to "improve" the reality and make the ball much larger.
EDIT: Another thing is how to project this on 3D. I suggest you make the path generated by the former algorithm (or excel) as a visible object (consisting of line segments), which you will able to rotate & translate.
The origin of the bad behaviour you're seeing is the projection that you use, centered on (0,0), and more generally too simple to look nice.
You need a more complete projection with center, scale, ...
i use that one for adding a little 3d :
projectOnScreen : function(wx,wy,wz) {
var screenX = ... real X size of your canvas here ... ;
var screenY = ... real Y size of your canvas here ... ;
var scale = ... the scale you use between world / screen coordinates ...;
var ZOffset=3000; // the bigger, the less z has effet
var k =ZOffset; // coeficient to have projected point = point for z=0
var zScale =2.0; // the bigger, the more a change in Z will have effect
var worldCenterX=screenX/(2*scale);
var worldCenterY=screenY/(2*scale);
var sizeAt = ig.system.scale*k/(ZOffset+zScale*wz);
return {
x: screenX/2 + sizeAt * (wx-worldCenterX) ,
y: screenY/2 + sizeAt * (wy-worldCenterY) ,
sizeAt : sizeAt
}
}
Obviously you can optimize depending on your game. For instance if resolution and scale don't change you can compute some parameters once, out of that function.
sizeAt is the zoom factor (canvas.scale) you will have to apply to your images.
Edit : for your update/render code, as pointed out in the post of Aki Suihkonen, you need to use a 'dt', the time in between two updates. so if you change later the frame per second (fps) OR if you have a temporary slowdown in the game, you can change the dt and everything still behaves the same.
Equation becomes x+=vx*dt / ... / vx+=gravity*dt;
you should have the speed, and gravity computed relative to screen height, to have same behaviour whatever the screen size.
i would also use a negative z to start with. to have a bigger ball first.
Also i would separate concerns :
- handle loading of the image separatly. Your game should start after all necessary assets are loaded. Some free and tiny frameworks can do a lot for you. just one example : crafty.js, but there are a lot of good ones.
- adjustment relative to the click position and the image size should be done in the render, and x,y are just the mouse coordinates.
var currWidth = this.width *scaleAt, currHeight= this.height*scaleAt;
canvasContext.drawImage(this.img, x-currWidth/2, y-currHeight/2, currWidth, currHeight);
Or you can have the canvas to do the scale. bonus is that you can easily rotate this way :
ctx.save();
ctx.translate(x,y);
ctx.scale(scaleAt, scaleAt); // or scaleAt * worldToScreenScale if you have
// a scaling factor
// ctx.rotate(someAngle); // if you want...
ctx.drawImage(this.img, x-this.width/2, x-this.height/2);
ctx.restore();

Categories

Resources