For those of you who don’t know Dark, it’s and excellent and intriguing series from Netflix based in the town of Winden, Germany. The opening effect is rather simple but compelling. With this snippet I tried to recreate that look-and-feel with an image animated by the mouse movement.

Here’s a quick link to the demo.

The html it’s just a canvas and a fixed positioned title. The canvas is set to fill the window with this css:

* {

padding: 0;

margin: 0;

}

html,

body {

height: 100%;

}

canvas {

display:block;

}

Now we can jump to the script part. First of all, we have to make sure our canvas dimensions will fit the window, even if a resize occurs.

var canvas;

var ctx;

var img; function start() {

canvas = document.getElementById('dark');

ctx = canvas.getContext('2d');

load();

} function load() {

img = new Image();

img.addEventListener('load', init, false);

img.src = 'dark.jpg';

} function init() {

window.addEventListener('resize', resize, false);

resize();

} function resize() {

canvas.width = window.innerWidth;

canvas.height = window.innerHeight;

draw();

}

The function start is called at the body onload event and it simply grabs our references for the canvas and the context, then it begins loading the image (so we can assume that our assets are always ready). When the image is loaded, we add a listener on the resize event. When a resize occurs, we update the canvas dimension and redraw it.

The draw function is fairly simple:

function draw() {

var w = canvas.width / 3;

ctx.clearRect(0,0, canvas.width, canvas.height);

drawImage(false, 0, 0, w, canvas.height);

drawImage(true, w, 0, w, canvas.height);

drawImage(false, w*2, 0, w, canvas.height);

}

Here we are just splitting the canvas in 3 parts and we are using the drawImage function to draw our image in each of these.

All the math calculation happens in the drawImage function.

function drawImage(flip, x, y, w, h) {

ctx.save(); var hW = w / 2;

var hH = h / 2;

var sourceRect = getSourceRect(w, h);



ctx.translate(x, y);

ctx.translate(hW, hH);

ctx.scale(flip ? -1 : 1, 1); ctx.drawImage(

img,

sourceRect .x,

sourceRect .y,

sourceRect .w,

sourceRect .h,

-hW,

-hH,

w,

h,

); ctx.restore();

}

Before entering the details, is important to understand how the ctx.drawImage function works. Here’s the full explanation and sintax. In short, it can take a portion of an image given from source rectangle and draw it on the destination rectangle on canvas.

Our goal is to render our source image in a specific region of our canvas (identified from x, y, w, h ). But it also should be possible to flip it before drawing. To achieve the mirror effect, we have to scale our image with a negative value. However it’s not that straightforward because we have to apply our transformations to the context.

So, as first, we take a snapshot of our current transformation matrix with ctx.save() and we restore it at the end when we have finished with ctx.restore() .

The first translate ctx.translate(x, y) will move our image to the correct position. Before applying the scale, we need to move the pivot of our rotation to the center of our image with ctx.translate(hW, hH) . Then make the flip according to the parameter with ctx.scale(flip ? -1 : 1, 1) . To get back to our x,y position, we set the destination to -hW and -hH . The sourceRect thing is just an utility function for getting the biggest rectangle on the image coordinates that satisfy the destination aspect ratio, so that our image won’t appear stretched. Here it is:

function getSourceRect(srcW, srcH) {

var destW = img.naturalWidth;

var destH = img.naturalHeight; var scale = Math.min(destW / srcW, destH / srcH); var scaledSrcW = srcW * scale;

var scaledSrcH = srcH * scale; var startX = (destW - scaledSrcW) * 0.5;

var startY = (destH - scaledSrcH) * 0.5;

return {

x: startX,

y: startY,

w: scaledSrcW,

h: scaledSrcH,

};

}

By now we should have our three images drawn in the canvas, with the second one flipped. Now let’s see how we can animate those images when the mouse move.

We add two variables, mouseX that will hold our mouse x position and weight that will tell how many pixel we want our images to move.

On the init function we add a listener:

function init() {

window.addEventListener('resize', resize, false);

canvas.addEventListener('mousemove', move);

resize();

}

And at every mouse move event we call the move function that simply store the x position as a percentage and re-draw the canvas.

function move(e) {

mouseX = (e.clientX / canvas.width);

draw();

}

Now that we have our mouseX value which range varies from 0 to 1, we want to move the images according to that. The idea is to shift the source position that we pass to the drawImage .

ctx.drawImage(

img,

sourceRect.x - (mouseX * weight),

sourceRect.y,

sourceRect.w,

sourceRect.h,

-hW,

-hH,

w,

h,

);

In this way our images are moving with the mouse, but there’s a problem. Indeed, moving the source, is not sufficient because we are taken pixels outside our image that turn out to be undefined (that’s why we see white pixels). The simple fix here is to take a source rectangle smaller than the current so that we can safely add or subtract that (mouseX * weight) value.

var scaleFactor = weight / img.naturalWidth; var stepX = img.naturalWidth * scaleFactor;

var stepY = img.naturalHeight * scaleFactor; ctx.drawImage(

img,

sourceRect.x + stepX - (mouseX * weight),

sourceRect.y + stepY,

sourceRect.w - stepX,

sourceRect.h - stepY,

-hW,

-hH,

w,

h,

);

And we have done. The mouse animation is just an option, it can be animated with a continuous loop as here or triggered by an action, there are many possibilities!

Here’s the full code on Github

Here’s the link to the demo

Let’s connect on twitter!