20 Jul 2014

Introduction to HTML5 Canvas

The <canvas> element is part of HTML5 and a specified area, which can be used to create drawings with JavaScript. The element provides the possibility to draw lines, shapes, images and text. In addition it is also very easy to make animations. Big advantages of HTML5 canvas compared to Adobe flash are:

  • it runs natively in your browser ( more stable & less crashes )
  • modern mobile operating systems support it ( Android, iOS, etc )
  • more efficient ( less required memory & cpu power equals less energy consumption )
  • Adobe software especially flash is a security vulnerability
  • open standard

Support

MOZILLA FIREFOX INTERNET EXPLORER GOOGLE CHROME APPLE SAFARI OPERA
18.31% (>3.5) 16.39% (>8.0) 43.29% (>3.0) 9.18% (>3.0) 1.24% (>10.5)
Total: 88.41% StatCount 2013/2014

Implement the canvas element

The implementation of the canvas element is very simple. With the attributes “width” and “height” the size can be determined.

<canvas id="demo" width="400" height="400">No support</canvas>

HTML is only a markup language and therefore JavaScript is used to draw on the element. The following line shows the common way to fetch the <canvas> element with JavaScript.

var canvas = document.getElementById('demo'); 

With JavaScript it is possible to change the attributes ‘width’ and ‘height’ dynamically. For instance to fit the browser screen.

canvas.setAttribute('width', window.innerWidth + "px");
canvas.setAttribute('height', window.innerHeight + "px");

window.onresize = function(event) {
    canvas.setAttribute('width', window.innerWidth + "px");
    canvas.setAttribute('height', window.innerHeight + "px");
};

With Javascript the support of the element can be checked, which is a more elegant solution than the html one. Here is the code (true and false are returned):

function checkSupport(){
     return !!(document.createElement('canvas').getContext('2d'));
}

Now the canvas element is created, but to draw on it, the 2D- Context is needed, which can be fetched with the getContext(’2D’) instruction, as shown below:

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

Coordinate system

The coordinate system is a 2-dimensional grid, where the upper-left corner has the coordinate (0,0).

To translate the center of the coordinate system from the upper-left corner to a custom position (for instance the center of the canvas element) the ‘translate(x,y)’ instruction can be used.

var x = canvas.offsetWidth/2;    //center of the canvas element
var y = canvas.offsetHeight/2;
ctx.translate(x,y); 

Sometimes it is necessary to rotate the coordinate system. If you want to draw a lopsided rectangle, for example. The rotation can be done with the ‘rotate(angel)‘ instruction. The code for drawing a lopsided rectangle is shown below.

 ctx.rotate(Math.PI/4);    // 45° rotation
ctx.fillRect(0,0,50,50); 

The draw of the rectangle is a little fetch-ahead, the exact explanation follows in the next section.

Drawing shapes & lines

Simple line

Canvas contains no ‘one line’  instruction to draw a line, but you can implement the following function. With ‘beginPath()‘  a new Path is created, afterwards the ‘moveTo(x1,y1)‘ instruction sets the initial position to the desired point (x1,y1). Now the ‘lineTo(x2,y2)‘ draws a imaginary line from this initial point to the specfied end point (x2,y2). To visible this imaginary line the ‘stroke()‘ function can be used. If no special style is specified (shown below), a solid line will be drawn.

/**
 * Draw a line from (x1,y1) to (x2,y2).
 *
 * @param {Integer, Float} x1   x-coordinate of the intial point
 * @param {Integer, Float} y1   y-coordinate of the intial point
 * @param {Integer, Float} x2   x-coordinate of the endpoint
 * @param {Integer, Float} y2   y-coordinate of the endpoint
 * @param {Object} ctx          2D-Context of the canvas element
 */
function drawLine(x1, y1, x2, y2, ctx) {
    ctx.beginPath();
    ctx.moveTo(x1, y1);
    ctx.lineTo(x2, y2);
    ctx.stroke();
}

Rectangle

A filled rectangle can be drawn with the fillRect(x,y,w,h) instruction, a hollow rectangle with the strokeRect(x,y,w,h) instruction. The parameters of this two functions are:

  • x ….. x-coordinate of the upper-left corner of the rectangle
  • y ….. y-coordinate of the upper-left corner of the rectangle
  • w …. width
  • h …. height
ctx.fillRect(10, 10, 10, 10);
ctx.strokeRect(10, 10, 10, 10);

 Circle

Canvas also contains no ‘on line’ instruction for drawing a circle. At first a new path must be created .To draw a  imaginary circle the ‘arc(x,y,r,sa,ea,w)‘ instruction is used. A short explanation of the parameters:

  • x,y …… (x,y) coordinate of  the center
  • r ……… radius
  • sa,se … start angle / end angle
  • w …….. True = clockwise and False = anticlockwise

Now two methods can be used to visible this imaginary circle. The ‘stroke()‘ instruction draws a hollow circle with a solid outline. In contrast, the ‘fill()‘ instruction draws a filled circle.

/**
*
* @param {Integer, Float} x,y  (x,y) coordinate of the center
* @param {Integer, Float} r    radius
* @param {Integer, Float} sa   start angle in radiant
* @param {Integer, Float} ea   end angle in radiant
* @param {Boolean} w           Clockwise or anticlockwise
* @param {Object} ctx          2D-Context of the canvas element
*/
function drawCircle(x, y, r, sa, ea, w, ctx) {
     ctx.beginPath();
     ctx.arc(x, y, r, sa, ea, w);
     ctx.stroke(); // or ctx.fill()
}

drawCircle(100, 100, 50, 0, 2 * Math.PI, true, ctx); // circle 

Any shapes

If you want to draw any shape, for which no instruction exits, as mentioned above the “path” instructions can be used. With ‘beginPath()‘  a new Path is created, afterwards those instructions can follow:

  • moveTo(x,y)  moves the current initial point to the coordinate (x,y) without drawing an imaginary line.
  • lineTo(x,y)  draws an imaginary line from the current initial point to the coordinate (x,y)
  • arc(x,y,r,sa,ea,w)  mentioned above.
  • arcTo(x1,y1,x2,y2,r)  is shown here.

After the path is created it can be drawn. There are two different ways to do so. One is the stroke() instruction, where only the imaginary line is drawn. The second way is the fill() instruction, where the complete area, which is enclosed by the path, is filled.

An example (smiley):

ctx.beginPath();
ctx.arc(100, 100, 70, 0, Math.PI * 2, true);
ctx.stroke();
ctx.beginPath();
ctx.arc(75, 75, 10, 0, Math.PI * 2, true);
ctx.fill();
ctx.beginPath();
ctx.arc(125, 75, 10, 0, Math.PI * 2, true);
ctx.fill();
ctx.beginPath();
ctx.arc(100, 100, 50, 0, Math.PI, false);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(100, 120);
ctx.lineTo(85, 100);
ctx.lineTo(115, 100);
ctx.closePath();
ctx.fill(); 

Drawing Text

Drawing text on the canvas element is quite simple and can be done with the ‘fillText(text,x,y,mw)‘ or ‘strokeText(text,x,y)‘  instruction.

  • text …. the desired text
  • x,y ….. (x,y) position of the upper-left corner of the text
  • mv ….. optional parameter to limit the maximum width of the text
ctx.fillText("Lorem ipsum",10,10);
ctx.strokeText("dolor sit amet.",10,35); 

The default font-type is ‘normal‘, the default font-size 10px and the default font-family ‘sans-serif‘. This default values can be changed by overwriting the font variable as shown below.

ctx.font = "bold 22px sans-serif";

font = ” [ font-style ] [ font-variant ] [ font-weight ] [ font-size ] [ font-family ] “, where  every attribute is optional. If the attribute is not specified the last value for this attribute is taken.

font-style

font-variant

font-weight

font-size

font-family

  • normal
  • italic
  • oblique
  • normal
  • small-caps
  • normal
  • bold
  • bolder
  • lighter
  • 100-900
 Specifies the font size of the text in pixel.  Specifies the font family of the text.

Style

Colour

To draw shapes & lines we used either the fill or the stroke function. If you wand to apply colour to filled shapes & fonts, you can change the value of fillStyle and if you want to apply colour to outlines & hollow fonts, you can change the value of strokeStyle. There are various valid ways to specify the new colour.

  • name of the colour – for example ‘blue’
  • hex codefor example ‘#0000FF’
  • RGBfor example ‘rgb(0,0,255)’
  • RGBAfor example ‘rgb(0,0,255,0.5)’
    • A is a placeholder for alpha and specifies the transparency of the colour. The value must be between 0 ( highest transparency ) and 1 ( lowest transparency ).
ctx.fillStyle = 'rgba(255,0,0,0.6)';
ctx.strokeStyle = '#334433';

Lines

Canvas provides several parameters to change the style of a line. Here are the most common:

lineWidth lineCap lineJoin
 Specifies the new width in px. Specifies the appearance of the ends.

  • butt (default)
  • round
  • squared
 Specifies the appearance of the corners, where lines meet.

  • round
  • bevel
  • mitter

 Dashed line

One way to draw dashed lines is to program it on your own ( for instance with a for-loop ). The better way to use the ‘setDashedLine(..)‘ instruction.

  • setLineDash([x])
    • x …. the length of the dashes in px
  • setLineDash([x,y])
    • x …. the length of the dashes in px
    • y …. the space between the dashes in px
ctx.setLineDash([5,5]);
ctx.lineWidth = 5;

Drawing Images

The canvas element also contain the ability to draw images and to manipulate them. First of all the ‘drawImage(….)‘ instruction can be used to place images on the canvas element. The instruction  has a bunch of possible parameters.

  • img … specifies the image
  • Optional parameters for clipping
    • sx ….. the initial clipping position in x-direction
    • sy ….. the initial clipping position in y-direction
    • sw ….  width of the clipping area
    • sh ….. height of the clipping area
  • x,y ….. (x,y) coordinate of the upper-left corner of the image on the canvas element
  • w …….  desired width of the image (possibility to stretch or downsize the image) (optional)
  • h …….  desired height of the image (possibility to stretch or downsize the image) (optional)

As you can see the ‘drawImage(..)‘ instruction has the capability to draw only a special area of the image as well as stretch or downsize it. To specify the image source various ways exist. If the image is already present as <img> element, it can be fetched with JavaScript. But also an other <canvas> element or a <video> element ( current frame ) are valid image sources. Furthermore the image can be loaded explicit per JavaScript (no prespecified html element necessary) as shown below.

var img = new Image();
img.src = "image.jpg";
img.onload = function() {
    //Draw image
    ctx.drawImage(img, 5, 5);
    //Clip, scale & draw image
    ctx.drawImage(img, 84, 20, 100, 100, 20 + img.width, 5, 200, 200);
} 

Manipulate an Image

The ‘getImageData(x,y,w,h)‘ instruction returns  an array with the raw data (every pixel with the information rgba) of a special area on the canvas element, specified by the given parameters.

  • x,y …. (x,y) coordinate of the upper-left corner of the special area
  • w …… width of this area
  • h ……. height of this area

The information of the single pixels are stored sequentially in this array, starting from the pixel in the upper-left corner and ending at the pixel in the lower-right corner (line for line). With this raw data every kind of image manipulation, which is doable witch JavaScript, can be done. A simple manipulation for instance is the conversion of a colored picture into a grayscale picture. The code is shown below. Finally to draw the changes on the canvas element the ‘putImageData(data,x,y)‘ is needed.

var img = new Image();
img.src = "image.jpg";
img.onload = function() {
    ctxcreate.drawImage(img, 15, 15);
    var greyimg = ctxcreate.getImageData(15, 15, img.width, img.height);
    var gdata = greyimg.data;
    for (var i = 0; i < gdata.length; i = i + 4) {
        var grayscale = (gdata[i] + gdata[i + 1] + gdata[i + 2]) / 3; // Creates the average of rgb
        gdata[i] = grayscale; // Red share
        gdata[i + 1] = grayscale; // Green share
        gdata[i + 2] = grayscale; // Blue share
        gdata[i + 3] = 255; // Alpha value
    }
    ctxcreate.putImageData(greyimg, 15, 15);
} 

If the image is stored on another server than the script, the ‘getImageDate()‘ instruction can throw a security exception. Therefore the image should be placed ( if possible ) on the same server.

Create an Image

If you want to draw a graphic, where you have to do a lot of state changes (for instance color), the wrong approach can cost much performance. Even if every single pixel is drawn separately with different colors, the change of the color followed by a fill/stroke instruction is no good idea. To improve the performance an empty image can be created, where the graphic is pre-rendered. With the ‘putImageData(data,x,y)‘ instruction the pre-rendered image is drawn on the canvas element. Example (Noise):

var fimg = ctxcreate.createImageData(width, height);
for (var x = 0; x < width; x++) {
    for (var y = 0; y < width; y++) {
        var index = getPixelIndex(x, y, width);
        fimg.data[index] = Math.floor(Math.random() * 255);   //Red
        fimg.data[index + 1] = Math.floor(Math.random() * 255); //Green
        fimg.data[index + 2] = Math.floor(Math.random() * 255); //Blue
        fimg.data[index + 3] = 255; //Alpha
    }
}
ctxcreate.putImageData(fimg, 0, 0);

/*
  * Get the index of the pixel in the imagedata-array.
  *
  * @param {Integer} x x-position of the pixel
  * @param {Integer} y y-position of the pixel
  * @param {Integer} w width of the image
  * @returns {Number} index
  */
function getPixelIndex(x, y, w) {
    return 4 * (x + y * w);
}

Animations

Clear & Clip

To create animations the old frame ( or only special parts ) of the scene must be removed. This removal can be done with the ‘clearRect(x,y,w,h)‘ instruction.

  • x,y ….. (x,y) coordinate of the upper-left corner of the rectangle
  • w  …… width
  • h ……. height

It may happen, that you want to clear another form than a rectangle. For this case the ‘clip()‘ function can be used. At first a path, which builds the outline of the desired form, must be created. If the clip() instruction is executed, the following instructions will only affect this specific area. To guarantee that only the next instruction ‘clearRect()‘ is affected, the Save & Restore method can be used, which is explained in the next section.

ctx.fillRect(0, 0, 200, 200);
//Clear a circle
ctx.save();
ctx.beginPath();
ctx.arc(100, 100, 100, 0, Math.PI * 2, true);
ctx.clip();
ctx.clearRect(0, 0, 200, 200);
ctx.restore();

Save and Restore

The save() operation make it possible to save the current state of the canvas element. What does state mean ?

  • current transformation matrix (changed by translate, rotate, etc.)
  • current style (changed by fillStyle, strokeStyle, lineWidth, etc)
  • current clipping region

With the save() operation the current state is pushed on the top of a stack. With the restore() operation the state at the top of the stack is going to be restored and removed from the stack. This two operations are similar to  the usual push and pop operations of a stack, which operates on the principle ‘First In, First Out’.

For instance usable for the interface of an analog clock, where the strokes for the hours have another color and line width.

ctx.translate(width / 2, height / 2);
ctx.save();
ctx.strokeStyle = "rgb(183, 203, 227)";
ctx.lineWidth = 2;
for (var n = 0; n < 60; n++) {
    ctx.save();
    if (n % 5 === 0) {
        ctx.strokeStyle = "rgb(100, 100, 100)";
        ctx.lineWidth = 4;
    }
    ctx.beginPath();
    ctx.moveTo(0, 100);
    ctx.lineTo(0, 110);
    ctx.stroke();
    ctx.restore();
    ctx.rotate(Math.PI * 2 / 60);
}
ctx.restore();

Animate

Finally to make animations time functions are required, which draw the frames isochronous on the canvas element.

  • setInterval( function, delay )
    • Executes function every delay milliseconds
  • setTimeout( function, delay )
    • Executes function in delay milliseconds

To create interactive animations event listener, which handle the user input (for example keyboard or mouse), can be used to draw new frames as a function of user interaction.

Example analog clock:

function updateClock() {
    getTime();
    drawClock();
}

function getTime() {
    var date = new Date();
    h = date.getHours();
    min = date.getMinutes();
    sec = date.getSeconds();
    msec = date.getMilliseconds();
}

function drawClock() {
    ctx.save();
    ctx.translate(width / 2, height / 2);
    ctx.clearRect(-cradius / 2, -cradius / 2, cradius, cradius);
    drawClockCase();
    drawClockPointer();
    ctx.restore();
}

function drawClockCase() {
    ctx.save();
    ctx.fillStyle = "rgb(183, 203, 227)";
    for (var n = 0; n < 60; n++) {
        if (n % 5 === 0) {
            ctx.fillRect(-5, -(height / 2 - 20), 10, 50);
        } else {
            ctx.fillRect(-2, -(height / 2 - 20), 4, 20);
        }
        ctx.rotate(Math.PI * 2 / 60);
    }
    ctx.restore();
    ctx.save();
    ctx.strokeStyle = "rgb(111, 164, 122)";
    ctx.beginPath();
    ctx.arc(0, 0, height / 2 - 20, 0, Math.PI * 2, true);
    ctx.stroke();
    ctx.restore();
}

function drawClockPointer() {
    //Hours
    ctx.save();
    ctx.fillStyle = "rgb(93, 94, 96)";
    ctx.rotate(Math.PI * 2 / (43200 / (h * 60 * 60 + min * 60 + sec)));
    ctx.fillRect(-12, -height / 4, 24, height / 4);
    ctx.restore();

    //Minutes
    ctx.save();
    ctx.fillStyle = "rgb(93, 94, 96)";
    ctx.rotate(Math.PI * 2 / (3600000 / (min * 60000 + sec * 1000 + msec)));
    ctx.fillRect(-10, -3 * height / 8, 20, 3 * height / 8);
    ctx.restore();

    //Seconds
    ctx.save();
    ctx.fillStyle = "rgb(255, 154, 105)";
    ctx.rotate(Math.PI * 2 / (60000 / (sec * 1000 + msec)));
    ctx.fillRect(-8, -34 * height / 80, 16, 34 * height / 80 + 45);
    ctx.restore();

    // Middle Point
    ctx.save();
    ctx.fillStyle = "rgb(50,50,50)";
    ctx.beginPath();
    ctx.arc(0, 0, 20, 0, Math.PI * 2, true);
    ctx.fill();
    ctx.restore();
}

Download script   

Leave a Reply