I am building a project where a user can type word in the input text and with the input value, canvas draws the particles onto the text. When the mouse hovers over the particles pushed back and comes back(core animation)
However, the performance is just terrible, it's way too slow and I have been looking up online and found stuff like frame rate, display ratio, getImageData, putImageData, new Uint32Array(), bitwise operator etc but after hours of trying different things I noticed I wasn't making any progress rather got stuck deeper
My codes are below, and if anyone can tell me where I should go about fixing it would be great.
in index.html
<canvas> </canvas>
<form>
<input class="text" type="text" value="touch me!" placeholder="type your message.."/>
<div class="input-bottom"></div>
</form>
in app.js - I didn't include any code on form submit since it works fine
let canvas = document.querySelector(".canvas")
let canvasContext2d = canvas.getContext("2d")
let canvasWidth = canvas.width = window.innerWidth
let canvasHeight = canvas.height = window.innerHeight
let form = document.querySelector('form')
let text = form.querySelector(".text")
let textMessage = text.value
let mouse = {x: undefined, y: undefined}
function Particle(x, y, r, accX, accY){
this.x = randomIntFromRange(r, canvasWidth-r)
this.y = randomIntFromRange(r, canvasHeight-r)
this.r = r
this.color = "black"
this.velocity = {
x: randomIntFromRange(-10, 10),
y: randomIntFromRange(-10, 10)
}
this.dest = {x : x, y : y}
this.accX = 5;
this.accY = 5;
this.accX = accX;
this.accY = accY;
this.friction = randomNumDecimal(0.94, 0.98)
this.draw = function(){
canvasContext2d.beginPath()
canvasContext2d.arc(this.x, this.y, this.r, 0, Math.PI * 2)
canvasContext2d.fillStyle = "rgb(250, 250, 247)"
canvasContext2d.fill()
canvasContext2d.closePath()
// mouse ball
canvasContext2d.beginPath()
canvasContext2d.arc(mouse.x, mouse.y, 50, 0, Math.PI * 2)
canvasContext2d.fill()
canvasContext2d.closePath()
}
this.update = function(){
this.draw()
if(this.x + this.r > canvasWidth || this.x - this.r < 0){
this.velocity.x = -this.velocity.x
}
if(this.y + this.r > canvasHeight || this.y - this.r < 0){
this.velocity.y = -this.velocity.y
}
this.accX = (this.dest.x - this.x) / 300;
this.accY = (this.dest.y - this.y) / 300;
this.velocity.x += this.accX;
this.velocity.y += this.accY;
this.velocity.x *= this.friction;
this.velocity.y *= this.friction;
this.x += this.velocity.x;
this.y += this.velocity.y;
if(dist(this.x, this.y, mouse.x, mouse.y) < 70){
this.accX = (this.x - mouse.x) / 30;
this.accY = (this.y - mouse.y) / 30;
this.velocity.x += this.accX;
this.velocity.y += this.accY;
}
}
}
let particles;
function init(){
particles = []
canvasContext2d.font = `bold ${canvasWidth/10}px sans-serif`;
canvasContext2d.textAlign = "center"
canvasContext2d.fillText(textMessage, canvasWidth/2, canvasHeight/2)
let imgData = canvasContext2d.getImageData(0, 0, canvasWidth, canvasHeight)
let data = imgData.data
for(let i = 0; i < canvasWidth; i += 4){
for(let j = 0; j < canvasHeight; j += 4){
if(data[((canvasWidth * j + i) * 4) + 3]){
let x = i + randomNumDecimal(0, 3)
let y = j + randomNumDecimal(0, 3)
let r = randomNumDecimal(1, 1.5)
let accX = randomNumDecimal(-3, 0.2)
let accY = randomNumDecimal(-3, 0.2)
particles.push(new Particle(x, y, r, accX, accY))
}
}
}
}
function animate(){
canvasContext2d.clearRect(0, 0, canvasWidth, canvasHeight)
for(let i = 0; i < particles.length; i++){
particles[i].update()
}
requestAnimationFrame(animate)
}
init()
animate()
First you can consider reducing how much total work is going on for a full screen's number of pixels, for instance:
transform: scale
to scale it back up if you must),fillRect
instead of drawing arcs. (At such a small size it won't make much of a difference visually, but these are generally less expensive to draw- you may want to test if it makes much of a difference),setTimeout
to wrap your requestAnimationFrame
and increasing the delay between frames (requestAnimationFrame is generally about 17ms))And some more small optimizations in the code:
store particles.length
in a variable after they are created so that you don't calculation particles.length on every for
loop iteration in your animate
function. But millions of calculations minus this 2048 isn't going to make much of a difference.
only set the context fillStyle
once. You never change this color, so why set it on every draw?
remove the closePath()
lines. They do nothing here.
draw the particles onto an offscreen "buffer" canvas, and draw that canvas to the onscreen one only after all the particles have been drawn to it. This can be done with a normal <canvas>
object, but depending on what browser you are working with, you can also look into OffscreenCanvas. Basic example would look something like this:
var numParticles;
// Inside init(), after creating all the Particle instances
numParticles = particles.length;
function animate(){
// Note: if offscreen canvas has background color drawn, this line is unnecessary
canvasContext2d.clearRect(0, 0, canvasWidth, canvasHeight)
for(let i = 0; i < numParticles; i++){
particles[i].update() // remove .draw() from .update() method
particles[i].drawToBuffer(); // some new method drawing to buffer canvas
}
drawBufferToScreen(); // some new method drawing image from buffer to onscreen canvas
requestAnimationFrame(animate)
}