I have a JavaScript animation that simulates a starfall effect using HTML canvas and "pixel" manipulation. While the animation works, I’m facing efficiency and speed issues, and I’m looking for ways to optimize the code. The animation involves creating stars that fall from the top of the screen and leaving a trail of pixels as they move. The thing is, the pixels are divs
, and it reaches thousands.
the current JS code:
const content = document.getElementById("bkg-content");
const background = document.getElementById("bkg");
const ctx = content.getContext("2d", { willReadFrequently: true });
let stars = [];
const maxStars = 20; // Maximum number of stars
let last_x = 0;
let next_spawn = 0;
function getX(size) {
let random = Math.random() * (window.innerWidth - size);
return last_x - size > random || random > last_x + size ? random : getX(size);
}
function createStar() {
let transparency = Math.random();
let size = transparency * 200 + 100;
let x = getX(size)
let fallingSpeed = Math.random() * 30 + 20;
next_spawn = size / fallingSpeed * 500;
last_x = x;
return {
x,
y: 0,
size,
transparency,
rotationAngle: 0,
rotationSpeed: Math.random() * 0.3 + 0.05,
rotationDirection: Math.random() < 0.5 ? 1 : -1,
fallingSpeed,
};
}
function spawnStars(count) {
for (let i = 0; i < count; i++) {
stars.push(createStar());
}
setTimeout(() => {
spawnStars(1)
}, next_spawn)
console.log(stars.length);
}
function draw() {
ctx.clearRect(0, 0, content.width, content.height);
for (let i = 0; i < stars.length; i++) {
let star = stars[i];
star.rotationAngle += star.rotationSpeed * star.rotationDirection;
star.y += star.fallingSpeed;
if (star.y > window.innerHeight + star.size / 2) {
// Remove stars that have completely passed the bottom of the screen
stars.splice(i, 1);
i--; // Adjust the loop variable since the array length has changed
} else {
ctx.save();
ctx.translate(star.x + star.size / 2, star.y);
ctx.rotate(star.rotationAngle);
// Adjust transparency based on star size
ctx.globalAlpha = star.transparency;
console.log(ctx.globalAlpha)
ctx.drawImage(starTXT, -star.size / 2, -star.size / 2, star.size, star.size);
// Reset global alpha for subsequent drawings
ctx.globalAlpha = 1;
ctx.restore();
}
}
}
function resizeCanvas() {
content.width = window.innerWidth;
content.height = window.innerHeight;
spawnStars(Math.min(maxStars, 1)); // Initially spawn one star
setInterval(() => {
createPixels()
draw()
}, 500);
}
function createPixels() {
const pixel = { size: 9, gap: 3 };
const numX = Math.floor(window.innerWidth / (pixel.size + pixel.gap));
const numY = Math.floor(window.innerHeight / (pixel.size + pixel.gap));
background.style.gridTemplateColumns = `repeat(${numX}, ${pixel.size}px)`;
background.innerHTML = ''; // clear existing pixels
for (let x = 0; x < numX; x++) {
for (let y = 0; y < numY; y++) {
const child = document.createElement("div");
child.classList.add("pixel");
background.appendChild(child);
}
}
const children = Array.from(background.getElementsByClassName("pixel"));
children.forEach((child) => {
const rect = child.getBoundingClientRect();
const { data } = ctx.getImageData(rect.left, rect.top, pixel.size, pixel.size);
const averageColor = getAverageColor(data);
const color = `rgb(${averageColor}, ${averageColor}, ${averageColor})`;
child.style.backgroundColor = color;
child.style.boxShadow = `0 0 10px ${color}`;
});
}
function getAverageColor(data) {
let sum = 0;
for (let i = 0; i < data.length; i += 4) {
sum += data[i];
}
const averageColor = sum / (data.length / 4);
return averageColor;
}
const starTXT = new Image(1600, 1600);
starTXT.src = "star.png";
starTXT.onload = resizeCanvas;
window.addEventListener('resize', resizeCanvas);
Another problem is resizing, so that it is optimal and fast, the code lags lots. Any tips? Any different approaches I could take?
The current implementation seems to have performance bottlenecks. I suspect the loop in the draw function and the frequent manipulation of the canvas might be causing slowdowns. How can I optimize the code to improve efficiency? The animation speed is crucial for a smooth user experience. Are there any specific techniques or best practices I should follow to enhance the speed of the starfall effect? I’m using a combination of canvas drawing for stars and pixel manipulation for the background. Is this the most optimal approach, or are there alternative methods that could provide better performance?
I’ve already tried some basic optimizations like using requestAnimationFrame instead of setInterval, but I’m looking for more insights and suggestions from the community. Any help in identifying and addressing the performance issues in the code would be greatly appreciated.
Sorry if my code is any messy or unreadable, I’ve tried a lot.
EDIT: the HTML file is underneath
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>...</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
height: 100vh;
background-color: black;
overflow: hidden;
/* Prevent scrolling */
}
#bkg-content {
width: 100%;
height: 100%;
position: absolute;
z-index: 1;
opacity: 0%;
}
#bkg {
width: 100%;
height: 100%;
display: grid;
gap: 3px;
position: absolute;
z-index: 2;
}
.pixel {
width: 9px;
height: 9px;
}
</style>
</head>
<body>
<canvas id="bkg-content"></canvas>
<div id="bkg"></div>
<script src="./script.js"></script>
</body>
</html>
2
Answers
The best way to get good animation speed is to do it via CSS Animation, rather than trying to do it with JS.
An introduction to CSS Animation is a bit beyond a SO answer, so here is an intro to the subject on MDN.
https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_animations/Using_CSS_animations
OK I did it.
The truth is that your code isn’t that inefficient! It’s pretty good. There are just two problems:
childrens
array outside thecreatePixels
function.setInterval
callback is so high. This was corrected by usingrequestAnimationFrame
with no delay.Take a look at this: https://jsfiddle.net/b7pv152w/
The code, for other users’ reference, can be found below: