Canvas animation / reactjs / with requestAnimationFrame - javascript

I have multiple canvas animation on my app, everything is working smooth except this, but now I have a problem with animation Frame (my suspect).
When I load component(animation1) and then change to component(animation 2)and then get back to first component something weird is happened, animation is getting faster every time when I come back to that component and I don't know the reason.
Animation for both component is the same, moving objects up and down.
Strange thing is that on my console, moving speed is always the same even after 6-7 switches but objects are moving faster and faster every time...
Any idea what may be a problem?
Here is one animation, second is quite similar to this one:
import React, { Component } from 'react';
let loadBall = [];
let canvas;
let c;
let counterX = 40;
let counterY = 30;
let y = counterY ;
class Loading extends Component {
constructor(props){
super(props)
this.state = {
vy: 0,
time:this.props.time
}
this.loadingLoop = this.loadingLoop.bind(this);
}
componentDidMount(){
canvas = document.getElementById('ball');
canvas.height = 150;
canvas.width = window.innerHeight;
c = canvas.getContext('2d');
this.loadingInit()
this.loadingLoop()
window.addEventListener('resize', () => {
canvas.width = window.innerHeight;
this.loadingInit()
})
this.loadingInit();
}
loadingLoop(){
requestAnimationFrame(this.loadingLoop);
c.clearRect(0,0, canvas.width, canvas.height);
for (let i = 0; i < loadBall.length; i++) {
loadBall[i].update();
}
}
loadingInit(){
loadBall = [];
for (let i = 0; i < 3; i++) {
let radius = 30//Math.floor(Math.random() * 20) + 15;
let x = (canvas.width / 2) - (radius * 4) + counterX;
y = counterY;
let color = colors[i];
loadBall.push(new loadingBall(x,y, radius, color));
counterY += 30;
counterX += 70;
}
}
render() {
return (
<canvas id='ball' style={{position:'fixed', top: '50%', left: '50%',WebkitTransform:'translate(-50%, -50%)'}}></canvas>
);
}
}
function loadingBall(x,y,radius,color){
this.x = x;
this.y = y;
this.radius = radius;
this.color = color;
this.move = 2
this.update = () =>{
if (this.y + this.radius + this.move >= canvas.height - 3) {
this.move = -this.move
}
if (this.y - this.radius - this.move <= 3) {
this.move = 2;
}
this.y += this.move;
this.draw();
}
this.draw = () => {
c.beginPath();
c.arc(this.x, this.y, this.radius, 0, Math.PI * 2, false);
c.fillRect(this.x, this.y, this.radius, 5);
c.fillStyle = this.color;
c.fill();
c.strokeStyle = this.color;
c.stroke();
c.closePath();
}
}
export default Loading;
Any suggestion can be helpful!

This problem is likely caused by your render loop (ie this.loadingLoop) not being stopped after your component is unmounted. As a result, when the component is unmounted the render loop continues to run and, when the component is mounted again, a new render loop is started - this causing two render loops running in tandem, causing the percived "speed up" in animation.
One way to address this would be to add a flag to your component that controls the continuation of the render loop. This could be achieved by adding the following:
constructor(props){
super(props)
this.state = {
vy: 0,
time:this.props.time
}
this.loadingLoop = this.loadingLoop.bind(this);
// Add this flag to control if animation is allowed to continue
this.stopAnimation = false;
}
componentDidMount(){
// Add this to the start of componentDidMount to let animation
// start and continue while component is mounted
this.stopAnimation = false;
// ...
// your existing code
// ...
}
componentWillUnmount() {
// When the component unmounts, set flag to stop animation
this.stopAnimation = true;
}
loadingLoop(){
// If the stopAnimation flag is not truthy, stop future
// frames for this animation cycle
if( this.stopAnimation ) {
return
}
requestAnimationFrame(this.loadingLoop);
c.clearRect(0,0, canvas.width, canvas.height);
for (let i = 0; i < loadBall.length; i++) {
loadBall[i].update();
}
}

Related

Javascript Audio Visualization

I'm making a small recording feature using the user/browser microphone. When the microphone is getting sound an audio visualization is shown (like an equalizer). So fare so good.
But i really want to change the way the visualization looks to something like the image below. But i have never worked with this area before and don't know how to go about it.
I imagine something like this:
https://images.app.goo.gl/pfKgnGnQz3MJVkbW6
I have two questions:
Is it possible to get a result like the attached?
How do you get started on something like that? (or has anyone done something like this that can share examples?)
My current code for the equlizer visualization
audioContext = new AudioContext();
gumStream = stream;
input = audioContext.createMediaStreamSource(stream);
rec = new Recorder(input,{numChannels:1})
rec.record()
inputPoint = audioContext.createGain();
audioInput = input;
audioInput.connect(inputPoint);
analyserNode = audioContext.createAnalyser();
analyserNode.fftSize = 1024;
inputPoint.connect( analyserNode );
updateAnalysers();
function updateAnalysers(time) {
if (!analyserContext) {
var canvas = document.getElementById("analyser");
canvasWidth = canvas.width;
canvasHeight = canvas.height;
analyserContext = canvas.getContext('2d');
}
{
var SPACING = 5;
var BAR_WIDTH = 5;
var numBars = Math.round(canvasWidth / SPACING);
var freqByteData = new Uint8Array(analyserNode.frequencyBinCount);
analyserNode.getByteFrequencyData(freqByteData);
analyserContext.clearRect(0, 0, canvasWidth, canvasHeight);
analyserContext.fillStyle = '#D5E9EB';
analyserContext.lineCap = 'round';
var multiplier = analyserNode.frequencyBinCount / numBars;
// Draw rectangle for each frequency bin.
for (var i = 0; i < numBars; ++i) {
var magnitude = 0;
var offset = Math.floor( i * multiplier );
for (var j = 0; j< multiplier; j++)
magnitude += freqByteData[offset + j];
magnitude = magnitude / multiplier;
var magnitude2 = freqByteData[i * multiplier];
analyserContext.fillRect(i * SPACING, canvasHeight, BAR_WIDTH, -magnitude);
}
}
rafID = window.requestAnimationFrame( updateAnalysers );
}
Ans 1 :
Your image is broken so can't answer but as far as I know, you can visualize any waveform using audio data
How do you get started on something like that? (or has anyone done something like this that can share examples?)
Ans 2:
So I did use the customized waveform. I am sharing my code
import React, { Component } from "react";
import AudioVisualiser from "./AudioVisualiser";
class AudioAnalyser extends Component {
constructor(props) {
super(props);
this.state = { audioData: new Uint8Array(0) };
this.tick = this.tick.bind(this);
}
componentDidMount() {
this.audioContext = new (window.AudioContext ||
window.webkitAudioContext)();
this.analyser = this.audioContext.createAnalyser();
this.dataArray = new Uint8Array(this.analyser.frequencyBinCount);
this.source = this.audioContext.createMediaStreamSource(this.props.audio);
this.source.connect(this.analyser);
this.rafId = requestAnimationFrame(this.tick);
}
tick() {
this.analyser.getByteTimeDomainData(this.dataArray);
this.setState({ audioData: this.dataArray });
this.rafId = requestAnimationFrame(this.tick);
}
componentWillUnmount() {
cancelAnimationFrame(this.rafId);
// this.analyser.disconnect();
// this.source.disconnect();
}
render() {
return <AudioVisualiser audioData={this.state.audioData} />;
}
}
export default AudioAnalyser;
import React, { Component } from 'react';
class AudioVisualiser extends Component {
constructor(props) {
super(props);
this.canvas = React.createRef();
}
componentDidUpdate() {
this.draw();
}
draw() {
const { audioData } = this.props;
const canvas = this.canvas.current;
const height = canvas.height;
const width = canvas.width;
const context = canvas.getContext('2d');
let x = 0;
const sliceWidth = (width * 1.0) / audioData.length;
context.lineWidth = 2;
context.strokeStyle = '#000000';
context.clearRect(0, 0, width, height);
context.beginPath();
context.moveTo(0, height / 2);
for (const item of audioData) {
const y = (item / 255.0) * height;
context.lineTo(x, y);
x += sliceWidth;
}
context.lineTo(x, height / 2);
context.stroke();
}
render() {
return <canvas width="300" height="300" ref={this.canvas} />;
}
}
export default AudioVisualiser;

Optimise canvas drawing of a circle

I am new to HTML5 canvas and looking to make a few circles move in random directions for a fancy effect on my website.
I have noticed that when these circles move, the CPU usage is very high. When there is just a couple of circles moving it is often ok, but when there is around 5 or more it starts to be a problem.
Here is a screenshot of profiling this in Safari for a few seconds with 5 circles.
Here is the code I have so far for my Circle component:
export default function Circle({ color = null }) {
useEffect(() => {
if (!color) return
let requestId = null
let canvas = ref.current
let context = canvas.getContext("2d")
let ratio = getPixelRatio(context)
let canvasWidth = getComputedStyle(canvas).getPropertyValue("width").slice(0, -2)
let canvasHeight = getComputedStyle(canvas).getPropertyValue("height").slice(0, -2)
canvas.width = canvasWidth * ratio
canvas.height = canvasHeight * ratio
canvas.style.width = "100%"
canvas.style.height = "100%"
let y = random(0, canvas.height)
let x = random(0, canvas.width)
const height = random(100, canvas.height * 0.6)
let directionX = random(0, 1) === 0 ? "left" : "right"
let directionY = random(0, 1) === 0 ? "up" : "down"
const speedX = 0.1
const speedY = 0.1
context.fillStyle = color
const render = () => {
//draw circle
context.clearRect(0, 0, canvas.width, canvas.height)
context.beginPath()
context.arc(x, y, height, 0, 2 * Math.PI)
//prevent circle from going outside of boundary
if (x < 0) directionX = "right"
if (x > canvas.width) directionX = "left"
if (y < 0) directionY = "down"
if (y > canvas.height) directionY = "up"
//move circle
if (directionX === "left") x -= speedX
else x += speedX
if (directionY === "up") y -= speedY
else y += speedY
//apply color
context.fill()
//animate
requestId = requestAnimationFrame(render)
}
render()
return () => {
cancelAnimationFrame(requestId)
}
}, [color])
let ref = useRef()
return <canvas ref={ref} />
}
Is there a more performant way to draw and move circles using canvas?
When they do not move, the CPU usage starts off around ~3% then drops to less than 1%, and when I remove the circles from the DOM, the CPU usage is always less than 1%.
I understand it's often better to do these types of animations with CSS (as I believe it uses the GPU rather than the CPU), but I couldn't work out how to get it to work using the transition CSS property. I could only get the scale transformation to work.
My fancy effect only looks "cool" when there are many circles moving on the screen, hence looking for a more performant way to draw and move the circles.
Here is a sandbox for a demo: https://codesandbox.io/s/async-meadow-vx822 (view in chrome or safari for best results)
Here is a slightly different approach to combine circles and background to have only one canvas element to improve rendered dom.
This component uses the same colours and sizes with your randomization logic but stores all initial values in a circles array before rendering anything. render functions renders background colour and all circles together and calculates their move in each cycle.
export default function Circles() {
useEffect(() => {
const colorList = {
1: ["#247ba0", "#70c1b3", "#b2dbbf", "#f3ffbd", "#ff1654"],
2: ["#05668d", "#028090", "#00a896", "#02c39a", "#f0f3bd"]
};
const colors = colorList[random(1, Object.keys(colorList).length)];
const primary = colors[random(0, colors.length - 1)];
const circles = [];
let requestId = null;
let canvas = ref.current;
let context = canvas.getContext("2d");
let ratio = getPixelRatio(context);
let canvasWidth = getComputedStyle(canvas)
.getPropertyValue("width")
.slice(0, -2);
let canvasHeight = getComputedStyle(canvas)
.getPropertyValue("height")
.slice(0, -2);
canvas.width = canvasWidth * ratio;
canvas.height = canvasHeight * ratio;
canvas.style.width = "100%";
canvas.style.height = "100%";
[...colors, ...colors].forEach(color => {
let y = random(0, canvas.height);
let x = random(0, canvas.width);
const height = random(100, canvas.height * 0.6);
let directionX = random(0, 1) === 0 ? "left" : "right";
let directionY = random(0, 1) === 0 ? "up" : "down";
circles.push({
color: color,
y: y,
x: x,
height: height,
directionX: directionX,
directionY: directionY
});
});
const render = () => {
context.fillStyle = primary;
context.fillRect(0, 0, canvas.width, canvas.height);
circles.forEach(c => {
const speedX = 0.1;
const speedY = 0.1;
context.fillStyle = c.color;
context.beginPath();
context.arc(c.x, c.y, c.height, 0, 2 * Math.PI);
if (c.x < 0) c.directionX = "right";
if (c.x > canvas.width) c.directionX = "left";
if (c.y < 0) c.directionY = "down";
if (c.y > canvas.height) c.directionY = "up";
if (c.directionX === "left") c.x -= speedX;
else c.x += speedX;
if (c.directionY === "up") c.y -= speedY;
else c.y += speedY;
context.fill();
context.closePath();
});
requestId = requestAnimationFrame(render);
};
render();
return () => {
cancelAnimationFrame(requestId);
};
});
let ref = useRef();
return <canvas ref={ref} />;
}
You can simply replace all bunch of circle elements and background style with this one component in your app component.
export default function App() {
return (
<>
<div className="absolute inset-0 overflow-hidden">
<Circles />
</div>
<div className="backdrop-filter-blur-90 absolute inset-0 bg-gray-900-opacity-20" />
</>
);
}
I tried to assemble your code as possible, it seems you have buffer overflow (blue js heap), you need to investigate here, these are the root cause.
The initial approach is to create circle just once, then animate the child from parent, by this way you avoid intensive memory and CPU computing.
Add how many circles by clicking on the canvas, canvas credit goes to Martin
Update
Following for alexander discussion it is possible to use setTimeout, or Timeinterval (Solution 2)
Soltion #1
App.js
import React from 'react';
import { useCircle } from './useCircle';
import './App.css';
const useAnimationFrame = callback => {
// Use useRef for mutable variables that we want to persist
// without triggering a re-render on their change
const requestRef = React.useRef();
const previousTimeRef = React.useRef();
const animate = time => {
if (previousTimeRef.current != undefined) {
const deltaTime = time - previousTimeRef.current;
callback(deltaTime)
}
previousTimeRef.current = time;
requestRef.current = requestAnimationFrame(animate);
}
React.useEffect(() => {
requestRef.current = requestAnimationFrame(animate);
return () => cancelAnimationFrame(requestRef.current);
}, []); // Make sure the effect runs only once
}
function App() {
const [count, setCount] = React.useState(0)
const [coordinates, setCoordinates, canvasRef, canvasWidth, canvasHeight, counts] = useCircle();
const speedX = 1 // tunne performance by changing this
const speedY = 1 // tunne performance by changing this
const requestRef = React.useRef();
const previousTimeRef = React.useRef();
const handleCanvasClick = (event) => {
// on each click get current mouse location
const currentCoord = { x: event.clientX, y: event.clientY ,directionX:"right",directionY:"down"};
// add the newest mouse location to an array in state
setCoordinates([...coordinates, currentCoord]);
// query.push(currentCoord)
//query.push(currentCoord)
};
const move = () => {
let q = [...coordinates]
q.map(coordinate => { return { x: coordinate.x + 10, y: coordinate.y + 10 } })
setCoordinates(q)
}
const handleClearCanvas = (event) => {
setCoordinates([]);
};
const animate = time => {
//if (time % 2===0){
setCount(time)
if (previousTimeRef.current != undefined) {
const deltaTime = time - previousTimeRef.current;
setCoordinates(coordinates => coordinates.map((coordinate)=> {
let x=coordinate.x;
let y=coordinate.y;
let directionX=coordinate.directionX
let directionY=coordinate.directionY
if (x < 0) directionX = "right"
if (x > canvasWidth) directionX = "left"
if (y < 0) directionY = "down"
if (y > canvasHeight) directionY = "up"
if (directionX === "left") x -= speedX
else x += speedX
if (directionY === "up") y -= speedY
else y += speedY
return { x:x,y:y,directionX:directionX,directionY:directionX}
}))
// }
}
previousTimeRef.current = time;
requestRef.current = requestAnimationFrame(animate);
}
React.useEffect(() => {
requestRef.current = requestAnimationFrame(animate);
return () => cancelAnimationFrame(requestRef.current);
}, []); // Make sure the effect runs only once
return (
<main className="App-main" >
<div>{Math.round(count)}</div>
<canvas
className="App-canvas"
ref={canvasRef}
width={canvasWidth}
height={canvasHeight}
onClick={handleCanvasClick}
/>
<div className="button" >
<button onClick={handleClearCanvas} > CLEAR </button>
</div>
</main>
);
};
export default App;
userCircle.js
import React, { useState, useEffect, useRef } from 'react';
var circle = new Path2D();
circle.arc(100, 100, 50, 0, 2 * Math.PI);
const SCALE = 1;
const OFFSET = 80;
export const canvasWidth = window.innerWidth * .5;
export const canvasHeight = window.innerHeight * .5;
export const counts=0;
export function draw(ctx, location) {
console.log("attempting to draw")
ctx.fillStyle = 'red';
ctx.shadowColor = 'blue';
ctx.shadowBlur = 15;
ctx.save();
ctx.scale(SCALE, SCALE);
ctx.translate(location.x / SCALE - OFFSET, location.y / SCALE - OFFSET);
ctx.rotate(225 * Math.PI / 180);
ctx.fill(circle);
ctx.restore();
};
export function useCircle() {
const canvasRef = useRef(null);
const [coordinates, setCoordinates] = useState([]);
useEffect(() => {
const canvasObj = canvasRef.current;
const ctx = canvasObj.getContext('2d');
ctx.clearRect(0, 0, canvasWidth, canvasHeight);
coordinates.forEach((coordinate) => {
draw(ctx, coordinate)
}
);
});
return [coordinates, setCoordinates, canvasRef, canvasWidth, canvasHeight,counts];
}
Soltion #2 Using Interval
IntervalExample.js (app) 9 sample circle
import React, { useState, useEffect } from 'react';
import Circlo from './Circlo'
const IntervalExample = () => {
const [seconds, setSeconds] = useState(0);
const [circules, setCircules] = useState([]);
let arr =[
{x:19,y:15, r:3,directionX:'left',directionY:'down'},
{x:30,y:10,r:4,directionX:'left',directionY:'down'},
{x:35,y:20,r:5,directionX:'left',directionY:'down'},
{x:0,y:15, r:3,directionX:'left',directionY:'down'},
{x:10,y:30,r:4,directionX:'left',directionY:'down'},
{x:20,y:50,r:5,directionX:'left',directionY:'down'},
{x:70,y:70, r:3,directionX:'left',directionY:'down'},
{x:80,y:80,r:4,directionX:'left',directionY:'down'},
{x:10,y:20,r:5,directionX:'left',directionY:'down'},
]
const reno =(arr)=>{
const table = Array.isArray(arr) && arr.map(item => <Circlo x={item.x} y={item.y} r={item.r} />);
return(table)
}
const speedX = 0.1 // tunne performance by changing this
const speedY = o.1 // tunne performance by changing this
const move = (canvasHeight,canvasWidth) => {
let xarr= arr.map(((coordinate)=> {
let x=coordinate.x;
let y=coordinate.y;
let directionX=coordinate.directionX
let directionY=coordinate.directionY
let r=coordinate.r
if (x < 0) directionX = "right"
if (x > canvasWidth) directionX = "left"
if (y < 0) directionY = "down"
if (y > canvasHeight) directionY = "up"
if (directionX === "left") x -= speedX
else x += speedX
if (directionY === "up") y -= speedY
else y += speedY
return { x:x,y:y,directionX:directionX,directionY:directionY,r:r}
}))
return xarr;
}
useEffect(() => {
const interval = setInterval(() => {
arr =move(100,100)
setCircules( arr)
setSeconds(seconds => seconds + 1);
}, 10);
return () => clearInterval(interval);
}, []);
return (
<div className="App">
<p>
{seconds} seconds have elapsed since mounting.
</p>
<svg viewBox="0 0 100 100">
{ reno(circules)}
</svg>
</div>
);
};
export default IntervalExample;
Circlo.js
import React from 'react';
export default function Circlo(props) {
return (
<circle cx={props.x} cy={props.y} r={props.r} fill="red" />
)
}
First of all, nice effect!
Once said that, I read carefully your code and it seems fine. I'm afraid that the high CPU load is unavoidable with many canvas and transparencies...
To optimize your effect you could try two ways:
try to use only one canvas
try use only CSS, at the end you are using canvas only to draw a filled circle with color from a fixed set: you could use images with pre-drawn same circles and use more or less the same code to simply chage style properties of the images
Probably with a shader you'll be able to obtain the same effect with high CPU save, but unfortunately I'm not proficient on shaders so I can't give you any relevant hint.
Hope I given you some ideas.
Cool effect! I was really surprised that solution proposed by #Sam Erkiner did not perform that much better for me than your original. I would have expected single canvas to be way more efficient.
I decided to try this out with new animation API and pure DOM elements and see how well that works.
Here is my solution(Only changed Circle.js file):
import React, { useEffect, useRef, useMemo } from "react";
import { random } from "lodash";
const WIDTH = window.innerWidth;
const HEIGHT = window.innerHeight;
export default function Circle({ color = null }) {
let ref = useRef();
useEffect(() => {
let y = random(0, HEIGHT);
let x = random(0, WIDTH);
let directionX = random(0, 1) === 0 ? "left" : "right";
let directionY = random(0, 1) === 0 ? "up" : "down";
const speed = 0.5;
const render = () => {
if (x <= 0) directionX = "right";
if (x >= WIDTH) directionX = "left";
if (y <= 0) directionY = "down";
if (y >= HEIGHT) directionY = "up";
let targetX = directionX === 'right' ? WIDTH : 0;
let targetY = directionY === 'down' ? HEIGHT : 0;
const minSideDistance = Math.min(Math.abs(targetX - x), Math.abs(targetY - y));
const duration = minSideDistance / speed;
targetX = directionX === 'left' ? x - minSideDistance : x + minSideDistance;
targetY = directionY === 'up' ? y - minSideDistance : y + minSideDistance;
ref.current.animate([
{ transform: `translate(${x}px, ${y}px)` },
{ transform: `translate(${targetX}px, ${targetY}px)` }
], {
duration: duration,
});
setTimeout(() => {
x = targetX;
y = targetY;
ref.current.style.transform = `translate(${targetX}px, ${targetY}px)`;
}, duration - 10);
setTimeout(() => {
render();
}, duration);
};
render();
}, [color]);
const diameter = useMemo(() => random(0, 0.6 * Math.min(WIDTH, HEIGHT)), []);
return <div style={{
background: color,
position: 'absolute',
width: `${diameter}px`,
height: `${diameter}px`,
top: 0,
left: 0
}} ref={ref} />;
}
Here are performance stats from Safari on my 6 year old Macbook:
Maybe with some additional tweaks could be pushed into green zone?
Your original solution was at the start of red zone, single canvas solution was at the end of yellow zone on Energy impact chart.
I highly recommend reading the article Optimizing the Canvas on the Mozilla Developer's Network website. Specifically, without getting into actual coding, it is not advisable to perform expensive rendering operations repeatedly in the canvas. Alternatively, you can create a virtual canvas inside your circle class and perform the drawing on there when you initially create the circle, then scale your Circle canvas and blit it the main canvas, or blit it and then scale it on the canvas you are blitting to. You can use CanvasRenderingContext2d.getImageData and .putImageData to copy from one canvas to another. How you implement it is up to you, but the idea is not to draw primitives repeatedly when drawing it once and copying the pixel data is pretty fast by comparison.
Update
I tried messing around with your example but I don't have any experience with react so I'm not exactly sure what's going on. Anyway, I cooked up a pure Javascript example without using virtual canvasses, but rather drawing to a canvas, adding it to the document, and animating the canvas itself inside the constraints of the original canvas. This seems to work the fastest and smoothest (Press c to add circles and d to remove circles):
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Buffer Canvas</title>
<style>
body, html {
background-color: aquamarine;
padding: 0;
margin: 0;
}
canvas {
border: 1px solid black;
padding: 0;
margin: 0;
box-sizing: border-box;
}
</style>
<script>
function randInt(min, max) {
return min + Math.floor(Math.random() * max);
}
class Circle {
constructor(x, y, r) {
this._canvas = document.createElement('canvas');
this.x = x;
this.y = y;
this.r = r;
this._canvas.width = 2*this.r;
this._canvas.height = 2*this.r;
this._canvas.style.width = this._canvas.width+'px';
this._canvas.style.height = this._canvas.height+'px';
this._canvas.style.border = '0px';
this._ctx = this._canvas.getContext('2d');
this._ctx.beginPath();
this._ctx.ellipse(this.r, this.r, this.r, this.r, 0, 0, Math.PI*2);
this._ctx.fill();
document.querySelector('body').appendChild(this._canvas);
const direction = [-1, 1];
this.vx = 2*direction[randInt(0, 2)];
this.vy = 2*direction[randInt(0, 2)];
this._canvas.style.position = "absolute";
this._canvas.style.left = this.x + 'px';
this._canvas.style.top = this.y + 'px';
this._relativeElem = document.querySelector('body').getBoundingClientRect();
}
relativeTo(elem) {
this._relativeElem = elem;
}
getImageData() {
return this._ctx.getImageData(0, 0, this._canvas.width, this._canvas.height);
}
right() {
return this._relativeElem.left + this.x + this.r;
}
left() {
return this._relativeElem.left + this.x - this.r;
}
top() {
return this._relativeElem.top + this.y - this.r
}
bottom() {
return this._relativeElem.top + this.y + this.r;
}
moveX() {
this.x += this.vx;
this._canvas.style.left = this.x - this.r + 'px';
}
moveY() {
this.y += this.vy;
this._canvas.style.top = this.y - this.r + 'px';
}
move() {
this.moveX();
this.moveY();
}
reverseX() {
this.vx = -this.vx;
}
reverseY() {
this.vy = -this.vy;
}
}
let canvas, ctx, width, height, c, canvasRect;
window.onload = preload;
let circles = [];
function preload() {
canvas = document.createElement('canvas');
canvas.style.backgroundColor = "antiquewhite";
ctx = canvas.getContext('2d');
width = canvas.width = 800;
height = canvas.height = 600;
document.querySelector('body').appendChild(canvas);
canvasRect = canvas.getBoundingClientRect();
document.addEventListener('keypress', function(e) {
if (e.key === 'c') {
let radius = randInt(10, 50);
let c = new Circle(canvasRect.left + canvasRect.width / 2 - radius, canvasRect.top + canvasRect.height / 2 - radius, radius);
c.relativeTo(canvasRect);
circles.push(c);
} else if (e.key === 'd') {
let c = circles.pop();
c._canvas.parentNode.removeChild(c._canvas);
}
});
render();
}
function render() {
// Draw
ctx.clearRect(0, 0, canvas.width, canvas.height);
circles.forEach((c) => {
// Check position and change direction if we hit the edge
if (c.left() <= canvasRect.left || c.right() >= canvasRect.right) {
c.reverseX();
}
if (c.top() <= canvasRect.top || c.bottom() >= canvasRect.bottom) {
c.reverseY();
}
// Update position for next render
c.move();
});
requestAnimationFrame(render);
}
</script>
</head>
<body>
</body>
</html>

JS canvas animation, my effect is accelerating and accumulating, but the speed of the effect is in the console same?

I tried to create a canvas effect with fireworks, but the more you click, the faster it gets and it seems to accumulate on itself. When I listed the speed it was similar and did not correspond to what was happening there. I also tried to cancel the draw if it got out of the canvas but it didnĀ“t help.
Here is link https://dybcmwd8icxxdxiym4xkaw-on.drv.tw/canvasTest.html
var fireAr = [];
var expAr = [];
function Firework(x, y, maxY, maxX, cn, s, w, en) {
this.x = x;
this.y = y;
this.maxY = maxY;
this.maxX = maxX;
this.cn = cn;
this.s = s;
this.w = w;
this.en = en;
this.i = 0;
this.explosion = function() {
for (; this.i < this.en; this.i++) {
var ey = this.maxY;
var ex = this.maxX;
var ecn = Math.floor(Math.random() * color.length);
var esX = (Math.random() - 0.5) * 3;
var esY = (Math.random() - 0.5) * 3;
var ew = Math.random() * 10;
var t = true;
expAr.push(new Exp(ew, esX, esY, ex, ey, ecn, t));
}
for (var e = 0; e < expAr.length; e++) {
expAr[e].draw();
}
}
this.draw = function() {
if (this.y < this.maxY) {
this.explosion();
} else {
this.track();
this.y -= this.s;
}
}
}
function Exp(ew, esX, esY, ex, ey, ecn, t) {
this.ew = ew;
this.esX = esX;
this.esY = esY;
this.ex = ex;
this.ey = ey;
this.ecn = ecn;
this.t = t;
this.draw = function() {
if (this.t == true) {
c.beginPath();
c.shadowBlur = 20;
c.shadowColor = color[this.ecn];
c.rect(this.ex, this.ey, this.ew, this.ew);
c.fillStyle = color[this.ecn];
c.fill();
c.closePath();
this.ex += this.esX;
this.ey += this.esY;
}
}
}
window.addEventListener('click', function(event) {
var x = event.clientX;
var y = canvas.height;
mouse.clickX = event.clientX;
mouse.clickY = event.clientY;
var maxY = event.clientY;
var maxX = event.clientX;
var cn = Math.floor(Math.random() * color.length);
var s = Math.random() * 5 + 5;
var w = Math.random() * 20 + 2;
var en = Math.random() * 50 + 5;
fireAr.push(new Firework(x, y, maxY, maxX, cn, s, w, en));
});
function ani() {
requestAnimationFrame(ani);
c.clearRect(0, 0, canvas.width, canvas.height);
for (var i = 0; i < fireAr.length; i++) {
fireAr[i].draw();
}
}
ani();
I deleted some unnecessary parts in my opinion but if I'm wrong and I missed something I'll try to fix it
Here are a few simple ways you can improve performance:
Commenting out shadowBlur gives a noticeable boost. If you need shadows, see this answer which illustrates pre-rendering.
Try using fillRect and ctx.rotate() instead of drawing a path. Saving/rotating/restoring the canvas might be prohibitive, so you could use non-rotated rectangles.
Consider using a smaller canvas which is quicker to repaint than one that may fill the entire window.
Another issue is more subtle: Fireworks and Exps are being created (making objects is expensive!) and pushed onto arrays. But these arrays are never trimmed and objects are never reused after they've left the visible canvas. Eventually, the rendering loop gets bogged down by all of the computation for updating and rendering every object in the fireAr and expAr arrays.
A naive solution is to check for objects exiting the canvas and splice them from the expAr. Here's pseudocode:
for (let i = expAr.length - 1; i >= 0; i--) {
if (!inBounds(expAr[i], canvas)) {
expAr.splice(i, 1);
}
}
Iterate backwards since this mutates the array's length. inBounds is a function that checks an Exp object's x and y properties along with its size and the canvas width and height to determine if it has passed an edge. More pseudocode:
function inBounds(obj, canvas) {
return obj.x >= 0 && obj.x <= canvas.width &&
obj.y >= 0 && obj.y <= canvas.height;
}
This check isn't exactly correct since the rectangles are rotated. You could check each corner of the rectangle with a pointInRect function to ensure that at least one is inside the canvas.
Fireworks can be spliced out when they "explode".
splice is an expensive function that walks up to the entire array to shift items forward to fill in the vacated element. Splicing multiple items in a loop gives quadratic performance. This can be made linear by putting surviving fireworks in a new list and replacing the previous generation on each frame. Dead firework objects can be saved in a pool for reuse.
Beyond that, I strongly recommend using clear variable names.
this.cn = cn;
this.s = s;
this.w = w;
this.en = en;
this.i = 0;
These names have little or no meaning to an outside reader and are unlikely to mean much to you if you take a couple months away from the code. Use full words like "size", "width", etc.
Another side point is that it's a good idea to debounce your window resize listener.
Here's a quick proof of concept that illustrates the impact of shadowBlur and pruning dead elements.
const rnd = n => ~~(Math.random() * n);
const mouse = {pressed: false, x: 0, y: 0};
let fireworks = [];
let shouldSplice = false;
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
document.body.appendChild(canvas);
document.body.style.margin = 0;
canvas.style.background = "#111";
canvas.width = document.body.scrollWidth;
canvas.height = document.body.clientHeight;
ctx.shadowBlur = 0;
const fireworksAmt = document.querySelector("#fireworks-amt");
document.querySelector("input[type=range]").addEventListener("change", e => {
ctx.shadowBlur = e.target.value;
document.querySelector("#shadow-amt").textContent = ctx.shadowBlur;
});
document.querySelector("input[type=checkbox]").addEventListener("change", e => {
shouldSplice = !shouldSplice;
});
const createFireworks = (x, y) => {
const color = `hsl(${rnd(360)}, 100%, 60%)`;
return Array(rnd(20) + 1).fill().map(_ => ({
x: x,
y: y,
vx: Math.random() * 6 - 3,
vy: Math.random() * 6 - 3,
size: rnd(4) + 2,
color: color
}));
}
(function render() {
if (mouse.pressed) {
fireworks.push(...createFireworks(mouse.x, mouse.y));
}
ctx.clearRect(0, 0, canvas.width, canvas.height);
for (const e of fireworks) {
e.x += e.vx;
e.y += e.vy;
e.vy += 0.03;
ctx.beginPath();
ctx.fillStyle = ctx.shadowColor = e.color;
ctx.arc(e.x, e.y, e.size, 0, Math.PI * 2);
ctx.fill();
if (shouldSplice) {
e.size -= 0.03;
if (e.size < 1) {
e.dead = true;
}
}
}
fireworks = fireworks.filter(e => !e.dead);
fireworksAmt.textContent = "fireworks: " + fireworks.length;
requestAnimationFrame(render);
})();
let debounce;
addEventListener("resize", e => {
clearTimeout(debounce);
debounce = setTimeout(() => {
canvas.width = document.body.scrollWidth;
canvas.height = document.body.clientHeight;
}, 100);
});
canvas.addEventListener("mousedown", e => {
mouse.pressed = true;
});
canvas.addEventListener("mouseup", e => {
mouse.pressed = false;
});
canvas.addEventListener("mousemove", e => {
mouse.x = e.offsetX;
mouse.y = e.offsetY;
});
* {
font-family: monospace;
user-select: none;
}
div > span, body > div {padding: 0.5em;}
canvas {display: block;}
<div>
<div id="fireworks-amt">fireworks: 0</div>
<div>
<label>splice? </label>
<input type="checkbox">
</div>
<div>
<label>shadowBlur (<span id="shadow-amt">0</span>): </label>
<input type="range" value=0>
</div>
</div>

setInterval is slowing down over time [duplicate]

This question already has an answer here:
HTML5 Canvas slows down with each stroke and clear
(1 answer)
Closed 4 years ago.
I've just started learning javascript.
My problem is, that the website is slowing down after a few seconds. I'm using setinterval to "tick" the things on the screen and i feel like this might be the cause of the problem.
Here is my code:
var r = [];
var ctx;
function init() {
ctx = document.getElementById("canvas").getContext("2d");
for(var i = 0; i < 20; i++) {
var x = Math.floor(Math.random() * (ctx.canvas.width - 20)) + 10;
var y = Math.floor(Math.random() * (ctx.canvas.height - 20)) + 10;
r.push(new Rect(x,y, 10, 10, ctx));
}
window.setInterval(tick,10);
window.setInterval(draw,10);
}
function tick() {
for(var i = 0; i < r.length; i++) {
r[i].tick();
}
}
function draw() {
ctx.clearRect(0,0,ctx.canvas.width,ctx.canvas.height);
for(var i = 0; i < r.length; i++) {
r[i].draw();
}
ctx.lineWidth = 5;
ctx.rect(0,0,ctx.canvas.width,ctx.canvas.height);
ctx.stroke();
}
Here's another class:
class Rect {
constructor(x, y, width, height, ctx) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
this.cxt = ctx;
this.xVel = 2.5;
this.yVel = 2.5;
if (Math.random() < 0.5) {
this.xVel = -this.xVel;
}
if (Math.random() < 0.5) {
this.yVel = -this.yVel;
}
}
tick(){
this.x += this.xVel;
this.y += this.yVel;
if (this.x + this.width >= ctx.canvas.width | this.x <= 0){
this.xVel = -this.xVel;
}
if (this.y + this.height >= ctx.canvas.height | this.y <= 0){
this.yVel = -this.yVel;
}
}
draw() {
ctx.fillRect(this.x,this.y,this.width,this.height);
}
}
So what exactly is the cause of this issue and how can i fix it?
You can download the files here: https://drive.google.com/file/d/1pg4ASPvjbo2ua_7cCvQvzucLgbegtiw6/view?usp=sharing
This issue is in your draw function.
Canvas-es remember all the lines drawn, over time it slows down your animation.
The solution is to reset the lines drawn list by calling ctx.beginPath()
function draw() {
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
for (var i = 0; i < r.length; i++) {
r[i].draw();
}
ctx.beginPath()
ctx.lineWidth = 5;
ctx.rect(0, 0, ctx.canvas.width, ctx.canvas.height);
ctx.stroke();
}
First of all the screen only refreshes at a rate of 16 milliseconds (assuming 60 frames per second). So calling the two function at 10 milliseconds is a bit overkill. But in the modern browser, we now have a native support to do anything when the screen refreshes. Its called request animation frame: requestAnimationFrame(animationrDrawCallback).
You can read more about it here: https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame. Now back to your code, it can be refactored like this:
const r = [];
const ctx;
function init() {
ctx = document.getElementById("canvas").getContext("2d");
for(let i = 0; i < 20; i++) {
const x = Math.floor(Math.random() * (ctx.canvas.width - 20)) + 10;
const y = Math.floor(Math.random() * (ctx.canvas.height - 20)) + 10;
r.push(new Rect(x,y, 10, 10, ctx));
}
// start our animation
window.requestAnimationFrame(render);
}
function render() {
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
r.forEach((item) => {
item.trick();
item.draw();
})
ctx.beginPath();
ctx.lineWidth = 5;
ctx.rect(0,0,ctx.canvas.width,ctx.canvas.height);
ctx.stroke();
// this will be called at next screen refresh
window.requestAnimationFrame(render);
}
The BIGGEST BONUS of using requestAnimationFrame is that it will stop executing when the tab is no longer in focus. Big boost for smartphones. Hope this helps.

Creating trail over an already made canvas

So I am trying to implement a concept of shooting star over an already drawn canvas of slowly moving stars. But I haven't found a way to do so. I tried implementing an array to make it look so but the trail isn't as efficient.
This code is as follows:
var canvas = document.querySelector('canvas');
var c = canvas.getContext('2d');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
var mouse = {
x : innerWidth/2,
y : innerHeight/2
};
var colors = [
'#3399CC',
'#67B8DE',
'#91C9E8',
'#B4DCED',
'#E8F8FF'
];
addEventListener('resize', function () {
canvas.width = innerWidth;
canvas.height = innerHeight;
init();
});
var isClicked = false;
addEventListener('click', function () {
mouse.x = event.clientX;
mouse.y = event.clientY;
isClicked = true;
});
function randomIntFromRange (min, max) {
return Math.floor(Math.random() * (max - min + 1) + min);
}
function randomColor (colors) {
return colors[Math.floor(Math.random() * colors.length)];
}
function Stars (x, y, radius, dy, color) {
this.x = x;
this.y = y;
this.radius = radius;
this.dy = dy;
this.color = color;
this.draw = function () {
c.beginPath();
c.arc(this.x, this.y, this.radius, 0, 2 * Math.PI, false);
c.shadowColor = this.color;
c.shadowBlur = 15;
c.shadowOffsetX = 0;
c.shadowOffsetY = 0;
c.fillStyle = this.color;
c.fill();
c.closePath();
}
this.update = function () {
if (this.y < -10) {
this.y = canvas.height + 10;
this.x = randomIntFromRange(this.radius, canvas.width);
}
this.y -= this.dy;
this.draw();
}
}
function ShootingStar (x, y, radius) {
this.x = x;
this.y = y;
this.radius = radius;
this.draw = function () {
c.beginPath();
c.arc(this.x, this.y, this.radius, 0, 2 * Math.PI, false);
c.shadowColor = "red";
c.shadowBlur = 15;
c.shadowOffsetX = 0;
c.shadowOffsetY = 0;
c.fillStyle = "red";
c.fill();
c.closePath();
}
this.update = function () {
this.x += 10;
this.y += 10;
this.draw();
}
}
let stars = [];
let shooting_star = [];
function init () {
stars = [];
for (var i = 0; i < 300; i++) {
var stars_radius = randomIntFromRange(2, 3);
var stars_x = randomIntFromRange(stars_radius, canvas.width);
var stars_y = randomIntFromRange(stars_radius, canvas.height);
var stars_dy = Math.random() / 6;
var color = randomColor(colors);
stars.push(new Stars(stars_x, stars_y, stars_radius, stars_dy, color));
}
}
function Explode () {
shooting_star = [];
var shooting_star_radius = 3;
var shooting_star_x = mouse.x;
var shooting_star_y = mouse.y;
for (var i = 0; i < 50; i++) {
shooting_star.push(new ShootingStar(shooting_star_x, shooting_star_y, shooting_star_radius));
if (shooting_star_radius > 0.2) {
shooting_star_radius -= .2;
}
var initiator = randomIntFromRange(-1, 1);
console.log(initiator);
shooting_star_x -= 3;
shooting_star_y -= 3;
}
}
function animate () {
requestAnimationFrame(animate);
c.clearRect(0, 0, canvas.width, canvas.height);
for (var i = 0; i < stars.length; i++)
stars[i].update();
for (var i = 0; i < shooting_star.length; i++)
shooting_star[i].update();
if (isClicked == true) {
Explode();
isClicked = false;
}
}
init();
animate();
Here is the jsfiddle to it
https://jsfiddle.net/qjug4qdz/
I basically want the shooting star to come from a random location to the point where my mouse is clicked, but the trail is difficult to work with using an array.
Basic particles
For the particular effect you are looking for you can use a basic particle system.
As the shooting star moves you will drop a particle that starts with the star velocity and then slows and fades out.
The particle
You first start with the particle. I like to use Object.assign when creating objects but you can use any method you like, class, new, factory...
// This defines a particle and is copied to create new particles
const starDust = {
x : 0, // the current position
y : 0,
dx : 0, // delta x,y (velocity)
dy : 0,
drag : 0, // the rate that the particle slows down.
life : 0, // count down till particle is removed
age : 0, // the starting value of life
draw(){ // function to update and draw the particle
this.x += this.dx; // move it
this.y += this.dy;
this.dx *= this.drag; // slow it down
this.dy *= this.drag;
const unitLife = (this.life / this.age); // get the life left as a value
// from 0 to 1 where 0 is end
ctx.globalAlpha = unitLife; // set the alpha
ctx.beginPath();
ctx.arc(this.x,this.y,4,0,Math.PI); // draw the particle
this.life -= 1; // count down
return this.life > 0; // return true if still alive
}
Be memory aware.
A common mistake when creating particle systems is that people forget that creating and destroying objects will add a lot of work to javascripts memory management. The worst of which is GC (Garbage Collection). GC is a major source of lag and if you are wasteful with memory it will impact the quality of the animation. For simple particles it may not be noticeable, but you may want hundreds of complex particles spawning each frame. This is when GC realy hurts the animation.
Most Game engines reduce the GC impact by reusing objects rather than dereferencing and recreating. A common method is an object pool, where a second array holds objects that are no longer used. When a new object is needed then the pool is first checked, if there is an unused object, it is used, else a new object is created.
This way you never delete any particles, greatly reducing the GC workload, and preventing your animation from dropping frames (if you use a lot of particles)
Particle needs initializer
But you need to provide a way to re-initialize the object. Thus add the function init to the particle that will set it up to be used again
init(x,y,vx,vy){ // where x,y and velocity vx,vy of shooting star
this.x = x;
this.y = y;
this.dx = vx;
this.dy = vy;
// give a random age
this.age = (Math.random() * 100 + 60) | 0; // in frames and | 0 floors the value
this.life = this.age; // and set the life countdown
this.drag = Math.random() * 0.01 + 0.99; // the drag that slows the particle down
}
} // end of starDust object.
The arrays
To manage all the particles we create object that has arrays and methods for adding, creating and rendering the particles. In this case I will call it dust
const dust = {
particles : [], // array of active particles
pool : [], // array of unused particels
createParticle(particleDesc){ // creates a new particle from particleDesc
return Object.assign({},particleDesc);
},
add(x,y,vx,vy){ // where x,y and velocity vx,vy
var dust;
if(this.pool.length){ // are there any particles in the pool
dust = this.pool.pop(); // get one
}else{ // else there are no spare particles so create a new one
dust = this.createParticle(starDust);
}
dust.init(x,y,vx,vy); // init the particle
this.items.push(dust); // put it in the active particle array
return dust; // return it (sometimes you want to do something with it)
},
draw(){ // updates and draws all active particles
var i = 0;
while(i < this.items.length){ // iterate each particle in items
if(this.items[i].draw() === false){ // is it dead??
this.pool.push(this.items.splice(i,1)[0]); // if dead put in the pool for later
}else{ i ++ } // if not dead get index of next particle
}
}
}//end of dust object
Using the particle system
The simplest way to create a particle is to use a random number and set the chance of a particle being created every frame.
In your main loop
// assuming that the falling star is called star and has an x,y and dx,dy (delta)
if(star) { // only if there is a start to spawn from
// add a particle once every 10 frame (on average
if(Math.random() < 0.1) {
dust.add(star.x, star.y, star.dx, star.dy); // add some dust at the shooting starts position and speed
}
}
dust.draw(); // draw all particles
And that is it.

Categories

Resources