HTML Canvas Christmas tutorial - part 3: MOVEMENT, BEZIER CURVE

Following part1 and part 2 of the tutorial you created a wonderful static Christmas card. But what's the point of having it on the computer, if nothing changes? That's why we're going to add some simple movement on our card. Before that, we'll create a more complicated object, using Bezier Curve.

You can see the code of the previous two tutorials here

What will you create?

  1. Let's follow the star! We'll create more complex shapes, combining triangles and Bezier curve to create a comet.

  2. More gifts! We'll create one more gift. That one will fall from the sky and land on the grass.

  3. Santa's coming to town! We're going to include an image of Santa Claus that is going to ride through the air forever.

Prerequisites

  • basic knowledge of HTML
  • basic knowledge of JavaScript
  • basic understanding of HTML Canvas

We covered basics of HTML Canvas in the first part of this tutorial

Bigger Christmas card

We used all the available space on our canvas. Let's make it a little wider, so we can put more awesome things on it.

You need to make HTML Canvas in your html file wider. Switch the width you currently have with 800.

HTML

<canvas id="canpic" width = 800; height = 450; >Your browser does not support the canvas element.</canvas>

If you refresh the page, you can see that nothing (visible) happened. Your canvas element is bigger, but the image you drew, stayed the same. Let's stretch the linear-gradient background we created in part 1 over the whole canvas.

Find that part of the code:

JavaScript

var backgroundLinear = ctx.createLinearGradient(0, 0, 0, 350);
backgroundLinear.addColorStop(0, "#151c2e");
backgroundLinear.addColorStop(0.9, "white");
backgroundLinear.addColorStop(1,"green");
ctx.fillStyle = backgroundLinear;
ctx.fillRect(0, 0, 500, 450);

and make the fillRect bigger, like this:

ctx.fillRect(0, 0, 800, 450);

That's the only number you have to change to make your background cover the whole canvas.

Comet

This comet will consist of 2 separate parts:

  • The star, drawn with 2 inverted equilateral triangles
  • Comet's tail, drawn with Bezier curve

Follow the star

We're going to create a star using 2 triangles because it is mathematically less demanding. You could also draw it with the lineTo method, like in this example.

comet.png

We are going to draw 2 overlapping yellow triangles (one facing down and one facing upward) with lineTo.

ctx.fillStyle = "yellow";

// triangle turned up
ctx.beginPath();
ctx.moveTo(600, 145);
ctx.lineTo(660, 50);
ctx.lineTo(720, 145);
ctx.closePath();
ctx.fill();

// triangle turned down
ctx.beginPath();
ctx.moveTo(600, 85);
ctx.lineTo(720, 85);
ctx.lineTo(660, 180);
ctx.closePath();
ctx.fill();

As you can see, here we didn't do anything else than we did in the previous tutorial. We selected fillStyle color and we begun path (ctx.beginPath()) in the middle of the canvas (moveTo(x, y)) After that, we draw lines to 2 more points (lineTo(x, y)) and finally we closed the path back to where it began (ctx.closePath()) and filled it with the selected color (ctx.fill()). You can see all the important points on the graph.

juststar.PNG

Bézier curve

A Bézier curve is a curve used in computer graphics and related fields. On canvas, it's defined with 4 points - a beginning (that is not defined in the function), an ending, and 2 control points that usually aren't located on the curve. You could imagine it as a loose string that is fixed at both ends. You're holding it somewhere in the middle with both hands and you move those hands. With movements of your hand, you're creating different curved shapes.

bezierexplanation.png

We create a Bézier curve with bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y) method that accepts 3 x 2 parameters.

  • x of first control point
  • y of first control point
  • x of second control point
  • y of second control point
  • x of end point
  • y of end point

As you saw before Bézier should be defined with 4 points - the beginning point is missing in the method. The method as a beginning point uses the latest point in the current path. That latest point can be changed with the moveTo() method.

We are going to draw 2 similar Bézier curves, one under the other. At the right, they will be connected with a straight, vertical line. At the left, they'll be connected with a line, broken somewhere in the middle.

cometTail.png

ctx.beginPath();
ctx.moveTo(500,80);

// first bezierCurve (top edge of comet's tail)
ctx.bezierCurveTo(550, 30, 600, 30, 650, 80);

// straight line from first bezier curve to the second one on the right side (in the star)
ctx.lineTo(650,130);

// second bezierCurve (bottom edge of comet's tail)
ctx.bezierCurveTo(600, 80, 550, 80, 500, 130);

// pointy part of the tail
ctx.lineTo(520, 90);

ctx.closePath();
ctx.fill();

Using the points specified on the graph, we moved our brush to the starting point of the comet's tail and drew the first Bézier curve with bezierCurveTo. With lineTo we moved a little lower and draw the second Bézier curve - this one from right to left as our 'brush' was on the right side of the canvas. With lineTo we created a little indentation at the end of the tail and with closePath closed the shape.

drawnComet.PNG

I created two small red circles with the same coordinates as the two checkpoints, so you can check for yourself that checkpoints are indeed not on the tail: beziercheckpoints.PNG

Object movement on canvas

Since HTML canvas works like a real-world canvas, you can't really move an object. You can erase said object on the previous location and draw it again on the new one. You can imagine object movement on canvas as a movement of a picture in a flipbook.

But when you move an object, you create a problem. Pixels of a drawn object replace everything that was there before. That means that once you create an object over the background, things that created the background disappear. If you then move that object, on the place that it was before, remains only an empty canvas. HTML Canvas has no memory of what was there before, so it can't put the background back once the object moves.

That is solvable in three ways:

  1. You can recreate the missing part of the canvas
  2. You can recreate the whole canvas content every time an object moves
  3. You can create more than one canvas and on one put the background content and moving objects on the other

Recreating the missing part is complicated, especially when said background changes rapidly throughout the picture. That's useful only if you have a very big canvas and the background is simple. If there's not a lot going on on the canvas (eg. just gradient in the background) and the canvas isn't very big, it might be a good idea to recreate the whole canvas each time the object moves.

Because we created a lot of content on our canvas, we're going to use the third option - we'll create another, overlapping canvas.

Put another canvas in your html like this:

HTML

<canvas id="canpic" width = 800; height = 450; >Your browser does not support the canvas element.</canvas>
<canvas id="canmovement" width = 800; height = 450; >Your browser does not support the canvas element.</canvas>

This new canvas has to have the same width and height as the first one and a unique id. By default, those two canvases will be put next to each other, so you need to change their position to absolute and put them on each other. Because that's the only CSS we'll need, we'll put it in the<head> of the HTML file.

HTML

<head>
    <title>Awesome canvas</title>
    <style>
        #canpic, #canmovement {
            position: absolute;
            left: 0;
            top: 0;
        }
    </style>
</head>

Gift falling from the sky!

We'll create a gift, similar to those we created in the part 2 that will move straight down. That means, only the y value will change, x will stay the same. Since we'll draw the falling gift on the new canvas we created, we'll also need to create a new 'brush' (context) for it.

After creating a 'brush', we'll create a function draw, that will draw a (for now) non-moving gift.

const canvas_moving = document.getElementById("canmovement");
const ctx_mov = canvas_moving.getContext("2d");

var gift_x = 550,
    gift_y = 200,
    gift_height = 100,
    gift_width = 150,
    mov_ribbon_width = 10;

function draw() {

    ctx_mov.fillStyle = 'red'
    ctx_mov.fillRect(gift_x, gift_y, gift_width, gift_height)
    ctx_mov.strokeRect(gift_x, gift_y, gift_width, gift_height)

    ctx_mov.fillStyle = 'blue'

    //vertical ribbon
    ctx_mov.fillRect(gift_x + gift_width/2 - mov_ribbon_width/2 , gift_y, mov_ribbon_width, gift_height);
    ctx_mov.strokeRect(gift_x + gift_width/2 - mov_ribbon_width/2 , gift_y, mov_ribbon_width, gift_height);

    //horizontal ribbon
    ctx_mov.fillRect(gift_x, gift_y + gift_height/2 - mov_ribbon_width/2, gift_width, mov_ribbon_width);
    ctx_mov.strokeRect(gift_x, gift_y + gift_height/2 - mov_ribbon_width/2, gift_width, mov_ribbon_width);
}

At the top, we created a new context for the second canvas. Outside the draw function, we set the variables that determine the size and the position of the gift. The current draw function should look completely familiar. It creates a big red square with a blue ribbon on it the same way the code in the for loop created other gifts.

We need to add the function that will provide the movement.

function movement() {
    requestAnimationFrame(movement);

    gift_y += 2;

    draw();
}

movement()

Function movement has 3 parts:

  1. requestAnimationFrame() requestAnimationFrame() is a better alternative to setInterval(), but its effects are similar. The method takes a callback as an argument and invokes it before the repaint, though updating an animation.

    Read more

  2. gift_y += 2; Each time the callback function invokes, our gift will be 2px lower.

  3. draw() Call to a function that draws our gift each time the movement function is invoked.

Don't forget to execute the function!

The effect we get is uncanny and definitely not what we had in mind.

giftdrag.PNG

We are repainting the gift over and over again, but we are not deleting the previous one. Add the black stroke around it and you get that weird dark red thing with a blue stripe in the middle ruining our beautiful canvas! And so we come to a reason why we create the second canvas. Each time, before drawing on it, we'll clear the whole canvas. We add that code at the beginning of the draw() function.

function draw() {

    ctx_mov.clearRect(0, 0, canvas_moving.width, canvas_moving.height)

    ctx_mov.fillStyle = 'red'
    ctx_mov.fillRect(gift_x, gift_y, gift_width, gift_height)
    ctx_mov.strokeRect(gift_x, gift_y, gift_width, gift_height)

    ctx_mov.fillStyle = 'blue'

    //vertical ribbon
    ctx_mov.fillRect(gift_x + gift_width/2 - mov_ribbon_width/2 , gift_y, mov_ribbon_width, gift_height);
    ctx_mov.strokeRect(gift_x + gift_width/2 - mov_ribbon_width/2 , gift_y, mov_ribbon_width, gift_height);

    //horizontal ribbon
    ctx_mov.fillRect(gift_x, gift_y + gift_height/2 - mov_ribbon_width/2, gift_width, mov_ribbon_width);
    ctx_mov.strokeRect(gift_x, gift_y + gift_height/2 - mov_ribbon_width/2, gift_width, mov_ribbon_width);
}

As you can see, the method clearRect() is the exact opposite of the fillRect. Its parameters are the same:

  • x coordinate of the left corner of the square
  • y coordinate of the left corner of the square
  • width of the square
  • height of the square

The only difference is, fillRect() creates a rectangle, and clearRect() deletes everything in the specified rectangle. That's how we create movement on canvas -> we redraw the gift with different y coordinate, each time the draw() function is invoked, but before that, we delete either everything or just part of what we draw before.

You don't necessarily need to clear the whole canvas to create the movement. You also could each time just call clearRect() with the same parameters as you used to previously create the gift, thus deleting only the part of the canvas where the gift had previously been. That might be useful if you'd create the tree on the same canvas as the gift and would want for the tree to stay the same and only delete the gift.

But that didn't fix all of our problems. The gift is now moving and doesn't live an elephant's trail behind it, but it starts in the middle of the screen and then disappears off it. Let's change that.

var gift_x = 550,
    gift_y = -200,
    gift_height = 100,
    gift_width = 150,
    mov_ribbon_width = 10;

function draw() {
    ...
}

function movement() {
    requestAnimationFrame(movement);

    // gift must stop when its bottom is on the same height as the tree (360 - gift_height)
    if(gift_y < (360 - gift_height)) {
        gift_y += 2;
    }

    draw();
}

movement()

As you can see, that was easy enough. We only changed 2 small parts of the code:

  1. Our variable gift_y is now set to -200 and that sets the start of our gift off the canvas.
  2. The step change of gift_y is now wrapped in an if. When incremented y reaches a certain value, it stops incrementing. And though the gift stops moving.

Refresh the page and you'll see the gift behave exactly how we wanted.

falling_gift.PNG

Santa on his sleigh

When you created the falling gift, you learned the basics about movement on the canvas. Now we are going to create a little more complicated, repetitive movement and learn how to use an image in canvas along the way.

Before you can put ("draw") an image on the canvas you need first to create the image and wait for it to load. You can do that by using the onload property of the image object.

The Image() constructor creates a new HTMLImageElement instance. It is functionally equivalent to document.createElement('img') (developer.mozilla.org/en-US/docs/Web/API/HT..)

Here is a very detailed explanation of how the image loading on canvas works.

You can get that image at pngall.com/sleigh-png/download/29075, name it santa_image.png, and put it in the same folder as your HTML and Javascript file.

santa_img = new Image();
santa_img.src = "./santa_image.png"
santa_img.onload = function() {ctx_mov.drawImage(santa_img, 10, 10)};

This doesn't work, does it?

Well, actually, it does. We put that image on the canvas with id 'canmovement'. That's the same canvas we created the falling gift on. And that canvas clears itself on repeat before the gift is drawn on it again. But the image is outside the function that covers that. So it is created in the start, but the canvas clears itself before we can see it. Let's comment out the falling gift part for a little while. But leave the context creation!

const canvas_moving = document.getElementById("canmovement");
const ctx_mov = canvas_moving.getContext("2d");

/*
var gift_x = 550,

...

movement()
*/

santa_img = new Image();
santa_img.src = "./santa_image.png"
santa_img.onload = function() {ctx_mov.drawImage(santa_img, 10, 10)};

You can see that now by some miracle of commenting god, it works!

santanotmoving.PNG

But that's not what we wanted, we need to move the santa_img part inside the draw function. But we can't simply move all of the code inside. We can't call the .onload() each time the canvas refreshes. We need first to load the image and then execute everything else.

Outside the function, we need to create a new Image and assign its source. Then we call the movement function when the image loads.

var santa_img = new Image(),
santa_img.onload = movement;
santa_img.src = "./santa_image.png"

The only thing missing now is moving the drawImage method to the bottom of the draw function.

Whole code now looks like this:

const canvas_moving = document.getElementById("canmovement");
const ctx_mov = canvas_moving.getContext("2d");

var gift_x = 550,
    gift_y = -200,
    gift_height = 100,
    gift_width = 150,
    mov_ribbon_width = 10,
    santa_img = new Image();

santa_img.src = "./santa_image.png"
santa_img.onload = movement;


function draw() {

    ctx_mov.clearRect(0, 0, canvas_moving.width, canvas_moving.height)

    ctx_mov.fillStyle = 'red'
    ctx_mov.fillRect(gift_x, gift_y, gift_width, gift_height)
    ctx_mov.strokeRect(gift_x, gift_y, gift_width, gift_height)

    ctx_mov.fillStyle = 'blue'

    //vertical ribbon
    ctx_mov.fillRect(gift_x + gift_width/2 - mov_ribbon_width/2 , gift_y, mov_ribbon_width, gift_height);
    ctx_mov.strokeRect(gift_x + gift_width/2 - mov_ribbon_width/2 , gift_y, mov_ribbon_width, gift_height);

    //horizontal ribbon
    ctx_mov.fillRect(gift_x, gift_y + gift_height/2 - mov_ribbon_width/2, gift_width, mov_ribbon_width);
    ctx_mov.strokeRect(gift_x, gift_y + gift_height/2 - mov_ribbon_width/2, gift_width, mov_ribbon_width);

    ctx_mov.drawImage(santa_img, 10, 10);
}

function movement() {
    requestAnimationFrame(movement);

    // gift must sto when bottom is on the same height as the tree (360 - gift_height)
    if(gift_y < (360 - gift_height)) {
        gift_y += 2;
    }

    draw();
}

Now both, the moving gift and the Santa are showing, but Santa's not moving yet. We need to add some kind of change to Santa's coordinates. Switch the x and y coordinates inside the drawImage method that are currently numbers for new variables, santa_x and santa_y.

    ctx_mov.drawImage(santa_img, santa_x, santa_y);

And don't forget to define them on the top, where all our variables are defined.

var gift_x = 550,
    gift_y = -200,
    gift_height = 100,
    gift_width = 150,
    fall_ribbon_width = 10,
    santa_img = new Image(),
    santa_x = 10,
    santa_y = 10;

Now, let's discuss what we are trying to achieve. I want the sleigh to start off the screen and slowly rising toward the right side. When the sleigh completely disappears from the screen, I want them to start rasing from the starting point and so on repeat. We need to change the starting santa_x and santa_y to off the screen and then increment both inside the movement. The sleigh will disappear from the canvas when santa_x is bigger than the canvas width. When that happens, we need to reset both values back to the starting ones.

var gift_x = 550,
    gift_y = -200,
    gift_height = 100,
    gift_width = 150,
    mov_ribbon_width = 10,
    santa_img = new Image(),
    santa_x = -300,
    santa_y = canvas.height;

santa_img.src = "./santa_image.png"
santa_img.onload = movement;


function draw() {

    ... Gift drawing ...

    ctx_mov.drawImage(santa_img, santa_x, santa_y);
}

function movement() {
    requestAnimationFrame(movement);

    // gift must sto when bottom is on the same height as the tree (360 - gift_height)
    if(gift_y < (360 - gift_height)) {
        gift_y += 2;
    }

    if(santa_x > canvas.width) {
        santa_x = -300
        santa_y = canvas.height
    } else {
        santa_x += 2;
        santa_y -= 1;
    }

    draw();
}

As you can see, we changed santa_yto canvas.height - that means that the Santa image is just under the canvas. Inside the ´movement´ function we have an if clause that checks if santa_x is bigger than canvases width. If it is, the images reset to the starting position. If not, Santa is moving to the right a little more than it raises to the top.

Here's the whole code that covers movement:


const canvas_moving = document.getElementById("canmovement");
const ctx_mov = canvas_moving.getContext("2d");

var gift_x = 550,
    gift_y = -200,
    gift_height = 100,
    gift_width = 150,
    mov_ribbon_width = 10,
    santa_img = new Image(),
    santa_x = -300,
    santa_y = canvas.height;

santa_img.src = "./santa_image.png"
santa_img.onload = movement;


function draw() {

    ctx_mov.clearRect(0, 0, canvas_moving.width, canvas_moving.height)

    ctx_mov.fillStyle = 'red'
    ctx_mov.fillRect(gift_x, gift_y, gift_width, gift_height)
    ctx_mov.strokeRect(gift_x, gift_y, gift_width, gift_height)

    ctx_mov.fillStyle = 'blue'

    //vertical ribbon
    ctx_mov.fillRect(gift_x + gift_width/2 - mov_ribbon_width/2 , gift_y, mov_ribbon_width, gift_height);
    ctx_mov.strokeRect(gift_x + gift_width/2 - mov_ribbon_width/2 , gift_y, mov_ribbon_width, gift_height);

    //horizontal ribbon
    ctx_mov.fillRect(gift_x, gift_y + gift_height/2 - mov_ribbon_width/2, gift_width, mov_ribbon_width);
    ctx_mov.strokeRect(gift_x, gift_y + gift_height/2 - mov_ribbon_width/2, gift_width, mov_ribbon_width);

    ctx_mov.drawImage(santa_img, santa_x, santa_y);
}

function movement() {
    requestAnimationFrame(movement);

    // gift must sto when bottom is on the same height as the tree (360 - gift_height)
    if(gift_y < (360 - gift_height)) {
        gift_y += 2;
    }

    if(santa_x > canvas.width) {
        santa_x = -300
        santa_y = canvas.height
    } else {
        santa_x += 2;
        santa_y -= 1;
    }

    draw();
}

finnished.PNG

And that's it!

Congratulations, you came to the end of the Christmas Canvas tutorial!

Check the code from all the parts of the tutorial

No Comments Yet