Let's make a Halloween JavaScript card with parallax effect.

In this article, we are building a small animation using JavaScript. It is going to be a Halloween card with a parallax effect.

You can see the demo here

The graphic was created by combining several free pictures from the Internet. The finished project you can download at the end of this article.

First of all we need to create an empty HTML5 document and place canvas 640x640 px there, which is the size of our card.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Happy Halloween!</title>
</head>
<body>
    <canvas id="canvas" width="640" height="640"></canvas>
    <script>
        {
         }
    </script>
</body>
</html>

All code we put inside {} to not declare any global variables. This is creating a block scope.

Let's create our constants and variables.

const SCREEN_WIDTH = 640;
const SCREEN_HEIGHT = 640;
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const art = new Image();
art.src = "art.png";

Our view size is in SCREEN_WIDTH and SCREEN_HEIGHT. We also got our canvas and its context in ctx. And we start to load our graphic in art.

In order to create the parallax effect, we need to divide our scene on layers by distance from camera to the objects. Moving a layer with closer to the camera objects faster, and with farther objects slower, we create illusion of depth. But first, we need an object to contain our layers.

const layers = {
   lastTime:  0,
   data: [],
   addLayer: function(layer) { this.data.push(layer); },
   getLayer: function(index) { return this.data[index]}
};

In this object we will keep our layers and calculate movement in time using variable lastTime.

This is a method update for all calculations.

update: function(time) {
    if(this.lastTime == 0) {
        this.lastTime = time;
    }
    //calculations here

    this.lastTime = time;
}

Let's add the first layer.

layers.addLayer({sx:0,sy:0,w:640,h:640,dx:0,speed:0});

sx and sy mean position on the source wich is our art.png. w and h mean width and height. dx means displacement on X coordinate and speed is the velocity of the layer. Our first layer is a static layer with objects like star and the moon. So it's size the same as screen size 640x640 px and speed 0.

Now we create the main loop and run it when art.png is loaded.

function MainLoop(time) {
    layers.update(time);
       requestAnimationFrame(MainLoop);
}
art.onload = function() {
   requestAnimationFrame(MainLoop);
}

If we run our script now, nothing will happen. We need to create script to calculate the movement of the layers and method render to draw the layers.

update: function(time) {
    if(this.lastTime == 0) {
        this.lastTime = time;
    }
    this.data.forEach((layer) => {
        layer.dx += layer.speed * (time - this.lastTime);
        if(layer.dx > layer.w) {
           layer.dx -= layer.w;
        }
        return layer;
    });
    this.lastTime = time;
},
render: function(i) {
    const {sx,sy,w,h,dx} = this.getLayer(i);
    ctx.drawImage(art,sx+dx,sy,SCREEN_WIDTH,SCREEN_HEIGHT,
                          0,0,SCREEN_WIDTH,SCREEN_HEIGHT);
    if(dx > (w - SCREEN_WIDTH)) {
        ctx.drawImage(art, sx,sy,SCREEN_WIDTH,SCREEN_HEIGHT,
                            w - dx,0,SCREEN_WIDTH,SCREEN_HEIGHT);
    }
}

In update method we are using time and speed to calculate dx. In method render we draw the layers using dx. We draw one layer in render, it lets us to control order in the loop. We will need it.

Let's add render for our first layer to the loop.

layers.render(0);

If we run the script we will see the static picture with the moon and stars.

Let's add all layers.

layers.addLayer({sx:0,sy:0,w:640,h:640,dx:0,speed:0});
layers.addLayer({sx:640,sy:0,w:1280,h:640,dx:0,speed:0.03});
layers.addLayer({sx:0,sy:640,w:1280,h:640,dx:0,speed:0.1});
layers.addLayer({sx:0,sy:1280,w:1920,h:640,dx:0,speed:0.2});
layers.addLayer({sx:0,sy:1920,w:1920,h:640,dx:0,speed:0.33});

And we need to add them to the main loop

function MainLoop(time) {
    layers.update(time);
    layers.render(0);
    layers.render(1);
    layers.render(2);
    layers.render(3);
    layers.render(4);
    requestAnimationFrame(MainLoop);
}

If we run now the scene will move. Now we only need a hero.

Our hero is a zombie. The free animation was taken from the Internet. The animation has 10 frames of 300x320 sprites. The calculation and render for this are completely different. So we create a new object.

const zombie = {
                x: 150,
                y: 200,
                w: 300,
                h: 320,
                lastTime: 0,
                frame: 0,
                frameRate: 100,

                getSprite: function() {
                    if(this.frame > 4) {
                        return {sx:((this.frame -5)*300),sy:2880 };
                    } else {
                        return {sx:(this.frame*300),sy:2560};
                    }
                },

                update: function(time) {
                    if(this.lastTime == 0) {
                        this.lastTime = time;
                        return;
                    }

                    if((time - this.lastTime) > this.frameRate) {
                        this.frame++;
                        if(this.frame > 9) {
                            this.frame = 0;
                        }
                        this.lastTime = time;
                    }
                },

                render: function() {
                    const {sx, sy} = this.getSprite();
                    ctx.drawImage(art,sx,sy,this.w,this.h,
                            this.x,this.y,this.w,this.h);

                }
            };

We draw our zombie in one place all we change is a frame.

Let's put our zombie in main loop. Our last layer is a fence that is closer to the camera than our zombie, thus we put our zombie before the last layer in the render.

function MainLoop(time) {
                layers.update(time);
                zombie.update(time);

                layers.render(0);
                layers.render(1);
                layers.render(2);
                layers.render(3);
                zombie.render();
                layers.render(4);
                requestAnimationFrame(MainLoop);
            }

And it is all done. Demo is here

Full source code here:

Download as archive here: Download

Login to post a comment.