I hope you can help me out here as I have meet my nemesis... Math!
I am making a background animation that will run constantly and it works fine exept one litle thing, it speeds up by a tiny amount over a very long time.
I have extracted the code that is essential for the script.
The animation is simple, it creates a bunch of circles that it then move around in a circular motion for ever. faster/slower and bigger/smaller randomly on creation.
So if the animation runs for let's say 40min~ the circular animation will have increased it's speed A LOT!
The constructor is run about 100 times and the instances is put in a particles array, here i just set the default values when instantiating the circles
constructor(){
this.radius = 100;
this.xPos = Math.round(Math.random() * canvas.width);
this.yPos = Math.round(Math.random() * canvas.height);
this.counter = 0;
this.maxWidth = 14;
this.width = Math.floor(Math.random() * (this.maxWidth - 4) + 4); //4 -> 24
this.speed = Math.random() * 0.4; //0.0 -> 0.4
this.color = "#3cccd3";
this.alpha = this.getAlpha();
this.sign = Math.round(Math.random()) * 2 - 1;
}
The draw method is run in the animation loop, I have checked many examples on animating a circular motion and this seems to be the popular way to do it, but there animation dont speed up as mine do :)
draw() {
this.counter += this.sign * this.speed;
ctx.beginPath();
//define the circleparticle
ctx.arc(this.xPos + this.radius * Math.cos(this.counter / 100) ,
this.yPos + this.radius * Math.sin(this.counter / 100) ,
this.width,
0,
Math.PI * 2,
true);
ctx.globalAlpha = this.alpha;
ctx.fillStyle = this.color;
ctx.fill();
}
And this is just the loop
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height); //empty the canvas
particles.forEach((particle) => particle.draw()); //run the draw method
if(playParticles && !gameFocus) {
requestAnimationFrame(draw); //rerun
}
}
UPDATE HERE IS THE FIX
In the constructor
this.speed = 100 + Math.random() * 500;
In the draw method
this.timestamp = Math.floor(Date.now());
this.counter = this.sign * (this.timestamp / this.speed * Math.PI);
I can not find anything in your code to explain a continuous increase in rotation speed.
However, the speed of your animation is a product of how often the draw method is called. You increment the counter a fixed amount for each invocation, but there is no guarantee that it will be called at any given frequency.
requestAnimationFrame will call your draw function each time it is ready for a new rendering. The timing of it could vary depending on a number of things, such as the time you need for each draw, the total computational load of your webpage, the total CPU load of your device or its power settings.
Instead of making the counter be a product of the draw methods call count, consider making it a product of time. For this reason, the first argument to the callback of requestAnimationFrame is a timestamp which should be used as the current time in each part of that rendering.
Edit: example code
The code below is a simplified example, producing x and y which together describe a circular motion of one revolution per second.
requestAnimationFrame(
function(timestamp) {
var counter = timestamp / 1000 * Math.PI;
var x = Math.cos(counter);
var y = Math.sin(counter);
// do something with x and y
}
);
Related
This question already has an answer here:
HTML5 Canvas performance very poor using rect()
(1 answer)
Closed 2 years ago.
Okay, so I am really scratching my head of what is happening here.
I have the following code running in a loop:
const canvas = document.getElementById("canvas") as HTMLCanvasElement;
const ctx = canvas.getContext("2d") as CanvasRenderingContext2D;
let latestTime = performance.now();
function draw(time: number) {
const dt = time - latestTime;
const { width, height } = (document.getElementById(
"main"
) as HTMLElement).getBoundingClientRect();
if (canvas.width != width) canvas.width = width;
if (canvas.height != height) canvas.height = height;
ctx.clearRect(0, 0, width, height);
ctx.fillStyle = "#333333";
ctx.rect(0, 0, width, height);
ctx.fill();
/* When this section is commented out, the performance INCREASES...
const size = 20;
const radius = 100;
const period = 10;
const x =
width / 2 + radius * Math.sin((((2 * Math.PI) / period) * time) / 1000);
const y =
height / 2 + radius * Math.cos((((2 * Math.PI) / period) * time) / 1000);
ctx.fillStyle = "#9999ff";
ctx.beginPath();
ctx.ellipse(x, y, size, size, 0, 0, 2 * Math.PI);
ctx.fill();*/
ctx.font = "12px Montserrat";
ctx.fillStyle = "#ffffff";
ctx.fillText(`Frame rate: ${Math.round(1.0 / (dt / 1000))}`, 10, 20);
latestTime = time;
window.requestAnimationFrame(draw);
}
draw(performance.now());
It is a simple circle orbiting around the center when it is uncommented. However, I commented the circle part and only render the framerate.
When I run this and inspect in the Chrome Dev Tools the frame budget, I see this:
7ms is taken up by the system.
The framerate gradually goes down, but in steps. It is nicely 60. Then it goes to 30 all of a sudden after 30 seconds or so. Then it goes to 15. And so on.
When I uncomment the circle code the result in the inspector is this:
The system task only takes up 0.67ms and the framerate is constant.
I tried moving the section but it doesn't matter.
What is going on here? It doesn't make sense to me.
Calling ctx.fillText also does a ctx.beginPath thus resetting any paths you have created.
The function ctx.rect add to the current path. Without the beginPath you are adding a rect to the current path each frame. Thus over time you are rendering more and more rectangles and thus the slow down.
Use ctx.fillRect rather than ctx.rect or start a new path with ctx.beginPath before the call to ctx.rect
I've made a sine wave animation with javascript where the area below the sine wave is filled with a light blue color. But when I run the code my computer starts heating up and lags. This could also be beacause my computer is pretty worn out by now, but I really would like to know how to optimize this code or maybe recreate the effect with something else that isn't so performance intensive if possible.
The Sine wave animation:
https://jsfiddle.net/x2audoqk/13/
The code:
const canvas = document.querySelector("canvas")
const c = canvas.getContext("2d")
canvas.width = innerWidth
canvas.height = innerHeight
window.addEventListener("resize", function () {
canvas.width = innerWidth
canvas.height = innerHeight
wave.y = canvas.height / 1.5
wave.length = -4.5 / canvas.width
amplitude = canvas.width / 35
})
const wave = {
y: canvas.height / 1.5,
length: -4.5 / canvas.width,
amplitude: canvas.width / 25,
frequency: 0.0045
}
let increment = wave.frequency
function animate() {
requestAnimationFrame(animate)
// Deletes previous waves
c.clearRect(0, 0, canvas.width, canvas.height)
c.beginPath()
// Get all the points on the line so you can modify it with Sin
for (let i = 0; i <= canvas.width; i++) {
c.moveTo(i, wave.y + Math.sin(i * wave.length + increment) * wave.amplitude * Math.sin(increment))
c.lineTo(i, canvas.height)
}
// Fill the path
c.strokeStyle = 'rgba(1, 88, 206, .25)'
c.stroke()
increment += wave.frequency
c.closePath()
}
animate()
Any suggestions are welcome.
The heavy load is due to requestAnimationFrame which run over and over again. An approach is to limit the frame rate of the animation. Knowing that the human's eyes need at least 24 fps for a fluid image, you can pick a fps between 24-60 fps of your choice (limited by monitor refresh rate up to 60Hz depends on configuration but this is mostly the default).
Here is a guide how to control the fps
var fps = 30;
var now;
var then = Date.now();
var interval = 1000/fps;
var delta;
function animate() {
requestAnimationFrame(animate);
now = Date.now();
delta = now - then;
if (delta > interval) {
then = now - (delta % interval);
//your code drawing here
}
}
animate();
The the difference between 30 fps and 60 fps
Another technique to achieve the same effect with less workload is to use CSS animation (horizontal), with your background wave pre-draw as an image.
I want to make a simple page that grows circles from its center ad infinitum. I'm almost there, but I can't figure out how to repeatedly grow them (resetting the radius i to 0 at a certain interval and calling the function again). I assume it will require a closure and some recursion, but I can't figure it out.
// Initialize canvas
var canvas = document.createElement("canvas");
var ctx = canvas.getContext("2d");
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
document.getElementsByTagName('body')[0].appendChild(canvas);
// Grow a circle
var i = 0;
var draw = function() {
ctx.fillStyle = '#000';
ctx.beginPath();
ctx.arc(canvas.width / 2, canvas.height / 2, i, 0, 2 * Math.PI);
ctx.fill();
i += 4;
window.requestAnimationFrame(draw);
}
draw();
Two things I'd do...
First, modify your draw function so that if the circle gets to a certain size, the i variable is reset back to zero. That starts the circle over again.
Second, add a setInterval timer to call your draw function at some time interval. See http://www.w3schools.com/js/js_timing.asp for details.
This setup will cause draw() to be called regularly, and the reset of i to zero makes it repeat.
So this did indeed require a closure. We wrap the initial function in a closure, and call it's wrapper function, which reinitializes I every time when called. draw() grows a single circle, and drawIt()() starts a new circle.
var drawIt = function(color) {
var i = 0;
return function draw() {
ctx.fillStyle = color;
ctx.beginPath();
ctx.arc(canvas.width/2, canvas.height/2, i, 0, 2 * Math.PI);
ctx.fill();
i+=1*growthFactor;
// Growing circles until they are huge
if (i < canvas.width) {
window.requestAnimationFrame(draw);
if (i === spacing) {
circles++
drawIt(nextColor())();
}
}
}
};
drawIt(nextColor())();
})();
What do I need to do to this animation to make the text animate along with the background image?
Fiddle here
I have seen a couple of different examples online but either they don't have the text rotated like I do (which is causing the problem) or they don't explain the maths behind the solution.
This is quite good - http://tech.pro/tutorial/1008/creating-a-roulette-wheel-using-html5-canvas
but it doesn't offer a great insight into the use of any of the Math functions.
I obviously need to affect the line:
context.rotate(i * arc);
in the loop that writes the text but I am unsure of the maths involved.
var cvs = document.getElementById("cvs");
var context = cvs.getContext("2d");
var height = 400,
width = 400,
spinning = false,
angle = 0,
awards = [100,200,300,400,500,600,700,800,900,1000,1100,1200],
segmentCount = 12,
angleAmount = 30,
arc = Math.PI / 6,
image = new Image();
image.src = 'http://placehold.it/400x400';
function draw() {
// clear
context.clearRect(0,0,width,height);
// rotate whole wheel here?
context.save();
context.translate(height/2, width/2);
context.rotate(angle * (Math.PI / 180));
context.drawImage(image,-width/2,-height/2);
context.restore();
// draw the prize amount text
for(var i = 0; i < segmentCount; i++){
context.save();
context.translate(height/2, width/2);
context.font = "bold 18px sans-serif";
context.textAlign = "end";
context.rotate(i * arc);
context.fillStyle = "#000000";
context.textBaseline = 'middle';
context.fillText(awards[i],145,0);
context.restore();
angle += angleAmount;
}
}
function update(){
draw();
angle += 5;
setTimeout(update,1000/30);
}
image.onload = update;
I think you got confused by the way you are using the 'angle' var : it is in fact the var that holds the current rotation, and you also use it in the for loop that draws the amounts (angle+=angleAmount)... just to increase it. Luckily enough you add 360° to it so it did not create a bug.
So first thing is to stop increasing this var in the text drawing for loop, second thing is to add the current rotation angle to each text draw (with a degree-> rad conversion) :
context.rotate(( i * angleAmount + angle ) * (Math.PI / 180));
http://jsfiddle.net/gamealchemist/fwter56k/4/
(or with a bit of optimisation : http://jsfiddle.net/gamealchemist/fwter56k/5/ )
I made a quick simple solution in JSFiddle, for better and faster explaining:
var Canvas = document.getElementById("canvas");
var ctx = Canvas.getContext("2d");
var startAngle = (2*Math.PI);
var endAngle = (Math.PI*1.5);
var currentAngle = 0;
var raf = window.mozRequestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.msRequestAnimationFrame ||
window.oRequestAnimationFrame;
function Update(){
//Clears
ctx.clearRect(0,0,Canvas.width,Canvas.height);
//Drawing
ctx.beginPath();
ctx.arc(40, 40, 30, startAngle + currentAngle, endAngle + currentAngle, false);
ctx.strokeStyle = "orange";
ctx.lineWidth = 11.0;
ctx.stroke();
currentAngle += 0.02;
document.getElementById("angle").innerHTML=currentAngle;
raf(Update);
}
raf(Update);
http://jsfiddle.net/YoungDeveloper/YVEhE/3/
As the browser chooses the fps, how would I rotate the ring independently from frame speed. Because for now, if speed is 30fps it will rotate slower, but if 60fps faster, because its rotate amount is added for each call.
As i understand from couple of thread it has something to do with getTime, i really tried but could not get it done, i would need to rotate it once in 10 seconds.
The other thing is, angle, it will increase more and more, and after long long time it will crash because variable max amount will be exceeded, so how do i make seamless rotate cap ?
Thank you for reading!
Simply use a time-diff approach locking the steps to the difference between old and new time:
DEMO
Start with getting current time:
var oldTime = getTime();
/// for convenience later
function getTime() {
return (new Date()).getTime();
}
Then in your loop:
function Update(){
var newTime = getTime(), /// get new time
diff = newTime - oldTime; /// calc diff between old and new time
oldTime = newTime; /// update old time
...
currentAngle += diff * 0.001; /// use diff to calc angle step
/// reset angle
currentAngle %= 2 * Math.PI;
raf(Update);
}
Using this approach will bind the animation to time instead of FPS.
Update For one minute I thought MODing the angle wouldn't work with floats, but you can (had to double check) so code updated.
Some math will let you draw your shape at a specified speed inside an animation loop.
Demo: http://jsfiddle.net/m1erickson/9Z8pG/
Declare a startTime.
var startTime=Date.now();
Declare the time-length of a 360 degree rotation (10seconds == 10000ms)
var cycleTime=1000*10; // 1000ms X 10 seconds
Inside each animation frame...
Use modulus math to divide the current time into 10000ms cycles.
var elapsed=Date.now()-startTime;
var elapsedCycle=elapsed%cycleTime;
In each animation frame, calculate the percent that the current time is through the current cycle.
var elapsedCyclePercent=elapsedCycle/cycleTime;
The current rotation angle is a full circle (Math.PI*2) X the percentage.
var radianRotation=Math.PI*2 * elapsedCyclePercent;
Redraw your object at the frame’s current rotation angle:
ctx.clearRect(0,0,canvas.width,canvas.height);
ctx.beginPath();
ctx.arc(40, 40, 30, -Math.PI/2, -Math.PI/2+radianRotation, false);
ctx.strokeStyle = "orange";
ctx.lineWidth = 11.0;
ctx.stroke();
To have consistent behaviour across devices, you need to handle time by yourself, and to update positions/rotations/... based on the good old formula : position = speed * time ;
The secondary benefit of this is that in case of a frame drop, the movement will still keep same speed, hence it will be less noticeable.
fiddle is here :
http://jsfiddle.net/gamealchemist/YVEhE/6/
The time is commonly measured in milliseconds in Javascript, so the speed will be in radians per milliseconds.
This formula might make things easier :
var turnsPerSecond = 3;
var speed = turnsPerSecond * 2 * Math.PI / 1000; // in radian per millisecond
Then to update your rotation, just compute time elapsed since last frame (dt) at the start of your update function and do :
currentAngle += speed * dt ;
instead of adding a constant.
To avoid the angle overflow, or the loss of precision (which will happen after quite some time...), use the % operator :
currentAngle = currentAngle % ( 2 * Math.PI) ;
function Update() {
var callTime = perfNow();
var dt = callTime - lastUpdateTime;
lastUpdateTime = callTime;
raf(Update);
//Clears
ctx.clearRect(0, 0, Canvas.width, Canvas.height);
//Drawing
ctx.beginPath();
ctx.arc(40, 40, 30, startAngle + currentAngle, endAngle + currentAngle, false);
ctx.strokeStyle = "orange";
ctx.lineWidth = 11.0;
ctx.stroke();
currentAngle += (speed * dt);
currentAngle = currentAngle % (2 * Math.PI);
angleDisplay.innerHTML = currentAngle;
}