user10946285
user10946285

Reputation:

How can you make a snowflake shape rotate to its center, with canvas and JavaScript?

I am trying to create a script that emulates snowing using a snowflake shape that rotates in its center as it falls.

The problem is that trying ctx.rotate(), rotates all the snowflakes in the center of the canvas and not to each individual flake to its center. The code.

    (function() {
        window.requestAnimationFrame = requestAnimationFrame;
    })();
    var flakes = [],
        c = document.getElementById('canvas'),
        ctx = c.getContext("2d"),
        flakeCount = 100
    c.width = window.innerWidth;
    c.height = window.innerHeight;
    init();

    function init() {
        for (var i = 0; i < flakeCount; i++) {
            var x = Math.floor(Math.random() * canvas.width),
                y = Math.floor(Math.random() * canvas.height),
                speed = (Math.random() * 1) + 0.5
            img = draw();
            flakes.push({
                img: img,
                velY: speed,
                velX: 0,
                x: x,
                y: y,
                color: getRandomColor()
            });
        }
        snow();
    }
    function snow() {
        ctx.clearRect(0, 0, canvas.width, canvas.height);
        for (var i = 0; i < flakeCount; i++) {
            var flake = flakes[i]
            flake.y += flake.velY;
            flake.x += flake.velX+getRand(0.1,0.3);
            if (flake.y >= canvas.height || flake.y <= 0) {
                reset(flake);
            }
            if (flake.x >= canvas.width || flake.x <= 0) {
                reset(flake);
            }
            ctx.filter = flake.color;
            ctx.drawImage(flake.img, flake.x, flake.y)
        }
        requestAnimationFrame(snow);
    };
    function reset(flake) {
        flake.x = Math.floor(Math.random() * canvas.width);
        flake.y = 0;
        flake.speed = (Math.random() * 1) + 0.5;
        flake.velY = flake.speed;
        flake.velX = 0;
        flake.color = getRandomColor()
    }
    function draw() {
        let c = document.createElement('canvas');
        var canvas = c;
        c.width = window.innerWidth;
        c.height = window.innerHeight;
        if (canvas.getContext) {
            var context = canvas.getContext('2d');
            var width = canvas.width;
            var height = canvas.height;
            context.lineWidth = 20;
            context.lineCap = 'round';
            context.fillStyle = "rgba(255, 255, 255, 0.0)";
            context.strokeStyle = "#FFFFFF";
            context.fillRect(0,0,width,height);
            context.scale(0.1, 0.1)
            context.translate(width/2,height/2);
            for(var count = 0; count < 6; count++) {
                context.save();     
                drawSegment(context, 100, 40);
                drawSegment(context, 100, 80);
                drawSegment(context, 100, 0);
                context.restore();          
                context.rotate(Math.PI/3);
            }
        }
        return canvas;
    }
    function drawSegment(context, segmentLength, branchLength) {
        context.beginPath();
        context.moveTo(0,0);
        context.lineTo(segmentLength,0);
        context.stroke();
        context.translate(segmentLength,0);
        if (branchLength > 0) {
            drawBranch(context, branchLength, 1);
            drawBranch(context, branchLength, -1);
        }
    }
    function drawBranch(context, branchLength, direction) {
        context.save();
        context.rotate(direction*Math.PI/3);
        context.moveTo(0,0);
        context.lineTo(branchLength,0);
        context.stroke();
        context.restore();
    }

In the following JSFiddle you can find the code https://jsfiddle.net/hkz3nyfL/7/

(You will notice that the shape appears broken on the top side, i dont know why it does that, it happens because the screen is too small)

Thanks in advance

Upvotes: 1

Views: 375

Answers (1)

A Haworth
A Haworth

Reputation: 36590

Running the given code my browser (Edge) complained occasionally about a violation - it seemed too much JS runtime was required between requestAnimationFrames - which I think means that it couldn't keep up with the normal 60fps.

The GPU usage ranged between 98 and 100%. The problem is that just too much work is being done on 100 canvases, drawing them onto the master canvas for each frame.

Moving to using img elements instead of canvases for each snowflake, but still drawing them using the canvas technique given in the question, and animating each using CSS animations, no violations were flagged and the GPU usage was around 30%.

Note: there was also a problem that the given code did not run properly on Safari. This was due to the use of canvas filter so redrawing the img with its new color after each animation cycle was substituted.

Now to get round to answering the question!

Pursuing the canvas rotation route is off because of the high processing usage so sticking with the img route, each snowflake can be rotated by adding rotate(30deg) etc to the keyframes. The GPU usage goes up, to somewhere between 40 and 50%, but there are no violations flagged. As an aside, the GPU usage was consistently higher on Firefox than on Edge on a Windows 10 laptop.

Here is a snippet using imgs instead of canvases. There is some tidying up needed on initial condition, and we note that the snowflakes are a fixed size. For a responsive solution they probably should be sized proportional to the viewport minimum dimension. Each snowflake has a fixed number of rotates and if this is required to change it would be better to calculate the keyframes in the JS for a more general solution.

function loaded() {
    const flakeCount = 100;
    const flakesEl = document.getElementById('flakes');
    
    const imgc = document.getElementById('canvas'); // work space on which to draw a snowflake of random color
    
    // FF seemed to require that this is done rather than just innerWidth which was smaller than 100vw 
    const width = Math.max(document.documentElement.clientWidth, window.innerWidth || 0);
    const height= Math.max(document.documentElement.clientHeight, window.innerHeight || 0);
    
    init();
    function init() {
        for (var i = 0; i < flakeCount; i++) {
            let flakeEl = document.createElement('img');
            flakeEl.addEventListener('webkitAnimationEnd', animationEnd);
            flakeEl.addEventListener('animationend', animationEnd);
            flakeEl.style.animationDuration = ((Math.random() * 1) + 0.5) * 10 + 's';
            flakeEl.style.left = Math.floor(Math.random() * width) + 'px';
            flakeEl.style.top = '-100px';
            flakeEl.classList.add('descend');
            flakeEl.classList.remove('descend1');
            draw(flakeEl);
            flakesEl.appendChild(flakeEl);
        }
    }    
    function animationEnd(event) {
      reset(event.target);
    }
    function reset(flakeEl) {
        flakeEl.style.top = '-100px';
        flakeEl.style.left = Math.floor(Math.random() * width) + 'px';
        flakeEl.style.animationDuration = ((Math.random() * 1) + 0.5) * 10 + 's';
        flakeEl.style.animationDelay = '0s';
        draw(flakeEl);
        flakeEl.classList.toggle('descend');
        flakeEl.classList.toggle('descend1');
    }
    function getRandomColor() {
        let color = `invert(${getRand(40,50)}%) sepia(${getRand(30,100)}%) hue-rotate(${getRand(-360,360)}deg) saturate(${getRand(10,50)})`;
        // Safari does not support canvas fill filter so we just use a normal rgb for this test
        return 'rgb('+getRand(0, 255)+', '+getRand(0, 255)+', '+getRand(0, 255)+')';
    }
    function getRand(min, max) {
        return Math.random() * (max - min) + min;
    }
    function draw(flakeEl) {
        imgc.width = 650; // enough spaace to get a whole snowflake in
        imgc.height = imgc.width;
            var imgctx = imgc.getContext('2d');
            var width = imgc.width;
            var height = imgc.height;
            imgctx.clearRect(0, 0, width, height);
            imgctx.lineWidth = 20;
            imgctx.lineCap = 'round';
            imgctx.fillStyle = "rgba(255, 255, 255, 0.0)";
            imgctx.strokeStyle = getRandomColor();
            imgctx.fillRect(0, 0, width, height);
            imgctx.translate(width/2,height/2);
            for(var count = 0; count < 6; count++) {
                imgctx.save();      
                drawSegment(imgctx, 100, 40);
                drawSegment(imgctx, 100, 80);
                drawSegment(imgctx, 100, 0);
                imgctx.restore();           
                imgctx.rotate(Math.PI/3);
            }
        flakeEl.src = imgc.toDataURL();
    }
    function drawSegment(context, segmentLength, branchLength) {
        context.beginPath();
        context.moveTo(0,0);
        context.lineTo(segmentLength,0);
        context.stroke();
        context.translate(segmentLength,0);
        if (branchLength > 0) {
            drawBranch(context, branchLength, 1);
            drawBranch(context, branchLength, -1);
        }
    }
    function drawBranch(context, branchLength, direction) {
        context.save();
        context.rotate(direction*Math.PI/3);
        context.moveTo(0,0);
        context.lineTo(branchLength,0);
        context.stroke();
        context.restore();
    }
}
window.onload = loaded;
*{
   margin: 0;
   padding: 0;
   box-sizing: border-box;
}
html, body{
  height: 100vh;
  width: 100vw;
  roverflow: hidden;
}
#flakes {
  position: relative;
  overflow: hidden;
  width: 100%;
  height: 100%;
}

#canvas {
  visibility: hidden;
  position: absolute;
  top: 0;
  left: 0;
}
img {
  position: absolute;
  animation-name: descend1;
  animation-iteration-count: 1;
  animation-timing-function: linear;
  width: 100px;
  height: 100px;
}

.descend {
  animation-name: descend;
}

.descend1 {
  animation-name: descend1;
  }

@keyframes descend {
  0% {
    transform: translateY(0) rotate(0deg);
  }
  16.66% {
    transform: translateY(calc(16.66vh + 16.66px)) rotate(30deg);
  }
  33.32% {
    transform: translateY(calc(33.32vh + 33.32px)) rotate(0deg);
  }
  49.98% {
    transform: translateY(calc(49.98vh + 49.98px)) rotate(-30deg);
  }
  66.64% {
    transform: translateY(calc(66.64vh + 66.64px)) rotate(0deg);
  }
  83.30% {
    transform: translateY(calc(83.30vh + 83.30px)) rotate(30deg);
  }
  100% {
    transform: translateY(calc(100vh + 100px)) rotate(0deg);
  }
}
@keyframes descend1 {
  0% {
    transform: translateY(0) rotate(0deg);
  }
  16.66% {
    transform: translateY(calc(16.66vh + 16.66px)) rotate(30deg);
  }
  33.32% {
    transform: translateY(calc(33.32vh + 33.32px)) rotate(0deg);
  }
  49.98% {
    transform: translateY(calc(49.98vh + 49.98px)) rotate(-30deg);
  }
  66.64% {
    transform: translateY(calc(66.64vh + 66.64px)) rotate(0deg);
  }
  83.30% {
    transform: translateY(calc(83.30vh + 83.30px)) rotate(30deg);
  }
  100% {
    transform: translateY(calc(100vh + 100px)) rotate(0deg);
  }
}
<div id="flakes"></div>
    <canvas id="canvas">
    </canvas>

Upvotes: 1

Related Questions