skip to Main Content

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 result I aimed for

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


  1. 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

    Login or Signup to reply.
  2. OK I did it.

    The truth is that your code isn’t that inefficient! It’s pretty good. There are just two problems:

    • you are redrawing every single pixel instead of reusing them. I corrected this by extracting the childrens array outside the createPixels function.
    • you are artificially making it seem like your site is lagging because your setInterval callback is so high. This was corrected by using requestAnimationFrame with no delay.

    Take a look at this: https://jsfiddle.net/b7pv152w/

    The code, for other users’ reference, can be found below:

    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)
    
    }
    
    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;
    
          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()
      }, 50);
    }
    
    let drawnPixels = false;
    let children = []
    
    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)`;
    
      if (!drawnPixels) {
        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);
            children.push(child)
          }
        }
        drawnPixels = true;
      }
    
      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 = "" //INSERT SOURCE HERE
    
    starTXT.onload = resizeCanvas;
    
    window.addEventListener('resize', resizeCanvas);
    <!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>
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search