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.
Related
I'm trying to design a small project using canvas and I want to design a sine wave in such a way that the wave is generated from the top right corner to the left bottom corner infinitely if possible.
Something like an inverse sine graph.
All I can do is make the sine wave go from left to right but making this work from top right to bottom left is very difficult.
This is my code at the moment
It's looking very sad...
const canvas = document.querySelector(".canvas");
const ctx = canvas.getContext("2d");
canvas.width = window.innerWidth - 5;
canvas.height = window.innerHeight - 5;
window.addEventListener("resize", () => {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
});
const wave = {
y: canvas.height / 2,
length: 0.02,
amplitude: 100,
frequency: 0.01,
yOffSet: canvas.height,
};
let increment = wave.frequency;
function animate() {
requestAnimationFrame(animate);
ctx.fillStyle = "rgb(0,0,0)";
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.beginPath();
ctx.moveTo(0, canvas.height);
for (let i = 0; i < canvas.width; i++) {
ctx.lineTo(Math.sin(i / wave.length + increment) * wave.amplitude + wave.yOffSet, 0);
}
ctx.stroke();
increment += wave.frequency;
}
animate();
body {
margin: 0;
padding: 0;
}
<div>
<canvas class="canvas"></canvas>
</div>
Desired Output
The problem is in this line:
ctx.lineTo(Math.sin(i / wave.length + increment) * wave.amplitude + wave.yOffSet, 0);
You are only moving in the x co-ordinate.
I have added the motion in the y co-ordinate and rewrote on three lines just for clarity.
x=i+wave.amplitude*Math.sin(i/wave.length);
y=canvas.height-(i-(wave. amplitude * Math.sin(i/wave.length)));
ctx.lineTo(x,y);
The result it produces is like what you describe, if I understood correctly. There are many more waves than you show in the drawing, but that can be cahnged by the wave.length parameter.
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'm trying to draw a little chart using some JavaScript and the canvas.
I'm doing a stuff like that :
RadarChart.prototype.fillRadar = function () {
var nbLabel = this.data.labels.length;
this.context.save();
this.context.lineWidth = 0.5;
this.context.lineJoin = "round";
this.context.translate(this.canvas.width / 2, this.canvas.height / 2);
this.context.strokeStyle = this.data.lineColor;
for (var i = 0; i < nbLabel; i++) {
this.context.moveTo(0, 0);
this.context.lineTo(0, -((this.canvas.height / 2) - 30))
this.context.stroke();
this.context.rotate((Math.PI / 180) * (360 / nbLabel));
}
this.context.restore();
}
the problem is that my lines are so pixelated and are not perfect. their width seems to change. It's like it's fading out over time...
Why is it doing that? How can I fix it?
Thanks for help.
Don't set the width / height of the canvas using css
use
canvas.width = 500;
canvas.height = 500;
make a function that find out how much 20% of the screen represent in pixel and then set the width/height in the code.
The problem seems to be related only with Internet Explorer.
When the example is run with jsfiddle in IE, it runs smooth.
If tried on my local machine and opened with IE, it stutters.
What could the problem be?
UPDATE: If setTimeout is removed, it does not stutter, but if I force it to 60 frames by adding the setTimeout, it does.
I need to force it to 60 frames/sec and at least run smooth.
The code in jsfiddle
// Check if canvas exists and get the context of the canvas
var canvas = document.getElementsByTagName('canvas')[0];
var ctx = canvas.getContext('2d');
// Add width and height of the canvas
var width = 640, height = 480;
// Make the canvas width and height as the variables
canvas.width = width, canvas.height = height;
rect = {
x: 0,
y: 0,
width: 100,
height: 100
}
ctx.fillStyle = 'red';
ctx.fillRect(rect.x,rect.y,rect.width,rect.height);
function animate(){
setTimeout(function(){
rect.x += 5;
ctx.fillStyle = 'red';
ctx.clearRect ( 0 , 0 , canvas.width, canvas.height );
ctx.fillRect(rect.x,rect.y,rect.width,rect.height);
if(rect.x <= 200){
requestID = requestAnimationFrame(animate);
}
}, 1000 / 60);
}
requestAnimationFrame(animate);
It makes no sense to combine requestAnimationFrame and setTimeout for painting. Maybe you want to update your animation at a stable rate of 60 fps, but that doesn't mean you need to repaint at exactly 60 fps.
Separate animation update from animation paint and your problem will be solved:
function paintAnimation() {
ctx.fillStyle = 'red';
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillRect(rect.x, rect.y, rect.width, rect.height);
requestAnimationFrame(paintAnimation);
}
requestAnimationFrame(paintAnimation);
function updateAnimation() {
rect.x += 5;
if (rect.x <= 200)
setTimeout(updateAnimation, 1000 / 60);
}
updateAnimation();
See JSFiddle
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;
}