Continuing the comic theme I’ve been building over the past few weeks and inspired by the news that Ghost In The Shell is finally in the process of becoming a live-action movie, I thought it might be interesting to take a panel from the manga series and animate it using web technologies: in this case, the <canvas> element.

Whitewashing Tech

Putting aside the horrible decision Dreamworks made in whitewashing the cast of the movie, which I am trying very hard not to be drawn into…

Rinko Kikuchi
Maggie Q
Tao Okamoto
Nope, can’t think of an Asian actress to play Major Motoko Kusanagi. Not one.

…let’s look at the markup. The <canvas> element takes the same dimensions as the image that will be used to fill its background:

<canvas id="gits" width="500" height="707"></canvas>

Rather than writing the panel into the element with JavaScript, the simplest way is to apply it as a background image:

canvas {
  box-sizing: border-box;
  background-image: url(gits.png);
  background-size: cover;
  border: 20px solid #111;
  display: block;
  margin: 0 auto;
  width: 30%;
  max-width: 500px;
}

We’ll be animating the snow effect against of this background.

Snow

Creating the snow is created via a particle system. “Particles” are tiny visual elements controlled en masse with a few rules. Snow, fire and smoke are often particle systems in computer animation. It’s important to understand that these systems don’t allow control over individual particles: we don’t “choose” a path for a snowflake. Rather, the motion of a snowflake is a random emergent property of an underlying physical system that is described with math.

Our script starts by identifying some of these properties as variables:

var canvas = document.getElementById("gits"),
ctx = canvas.getContext("2d"),
W = canvas.width,
H = canvas.height,
maxParticles = 25,
maxParticleSize = 12,
minParticleSize = 4,
maxMove = 1,
angle = 0,
particles = [];

particles is an array into which our particle elements are pushed. Each particle has a position in x y space together with a width and height (wh).

for (var i = 0; i < maxParticles; i++) {
    particles.push({
        x: Math.random() * W, 
        y: Math.random() * H, 
        wh: Math.random() * maxParticleSize + minParticleSize
    })
}

The randomness introduced into the calculations adds the first aspect of chaotic behaviour: the initial snowflakes will be randomly positioned within the <canvas> area and provided with a random size (between the limits imposed by maxParticleSize and minParticleSize).

Fuzzy Snow

I wanted to introduce a bokeh-like effect to the snowflakes, which created a challenge: how could I blur the particles while still keeping the code efficient? A few possibilities occurred to me:

  1. I could add a blur filter to the entire <canvas>: but that would blur the background image as well, while being computationally expensive and slow on mobile.
  2. I could create a shadow with a shadowBlur for each circle, although that would effectively double the draw for each element
  3. Alternatively, I could write a gaussian blur shader for the circles: but since I didn't need every particle to appear blurred, and the blur amount per particle did not need to change (the snow is always falling parallel to the viewer, and not moving closer or further away) that seemed overkill.

In the end, I opted for a slightly counter-intuitive approach: the particles themselves would be square, with a blurry circle drawn inside each using a radial gradient. This is contained inside a draw function:

function draw() {
    ctx.clearRect(0, 0, W, H);
    ctx.beginPath();
        for (var i = 0; i < maxParticles; i++) {
            var p = particles[i];
            ctx.moveTo(p.x, p.y);
            ctx.rect(p.x, p.y, p.wh, p.wh);
            var radgrad = ctx.createRadialGradient(p.x + p.wh/2,p.y + p.wh/2,0,p.x + p.wh/2,p.y + p.wh/2,p.wh/2);
            radgrad.addColorStop(0, 'rgba(255, 255, 255, 1)');
            radgrad.addColorStop(0.5, 'rgba(255, 255, 255, .8)');
            radgrad.addColorStop(1, 'rgba(255,255,255,0)');
            ctx.fillStyle = radgrad;
            ctx.fill();
        }
    update();
}

For small snowflakes, this makes little difference - they still look “solid” - but larger snowflakes that are closer to the user will appear to have a blurry edge.

The function starts with clearing the entirety of the canvas (which doesn’t affect the background image, since that’s in CSS). A slightly strange requirement is the beginPath method added immediately after, even though we are not actually drawing a path: without it, the canvas will not completely clear.

Finally, there’s a call to the update function, which moves each particle.

Snowfall

function update() {
    angle += 0.01;
    for (var i = 0; i < maxParticles; i++) {
        var p = particles[i];
        p.y += Math.cos(angle) + p.wh/4;
        p.x += Math.sin(angle) * 2;
        if (p.x > W + maxMove || p.x < -maxMove || p.y > H) {
            if (i%3 > 0) {
                particles[i] = {x: Math.random() * W, y: -(maxMove), wh: p.wh };
            } else {
                if (Math.sin(angle) > 0) {
                    particles[i] = {x: -maxMove, y: Math.random()*H, wh: p.wh };
                } else {
                    particles[i] = {x: W+maxMove, y: Math.random()*H, wh: p.wh };
    } } } }
requestAnimationFrame(draw);
}
draw();

While I have addressed most of the operators used in the script, this function has more math than I’ve covered in JavaScript so far, so I’ll leave in-depth discussion for future articles. However, this won’t be the last time I cover animating comics with canvas: there will be more articles to come.

I must acknowledge the help of this anonymous contribution at codeplayer, which helped inspire this code.

Enjoy this piece? I invite you to follow me at twitter.com/dudleystorey to learn more.
Check out the CodePen demo for this article at https://codepen.io/dudleystorey/pen/GZBGYQ