Gameplay in HTML5: Canvas

Homework #3 review

Instructor's solution

http://EpsilonDelta.us/UW_GameplayHTML5/Homework/Solutions/3

What is Canvas?

Creating a canvas

Directly in HTML

  • You can declare a canvas element right in your HTML:
    <canvas id="myCanvas"></canvas>
  • This will create a transparent 300x150-pixel canvas.
  • Anything between the <canvas> and </canvas> tags will be displayed only if the browser does not support canvas.
  • We can then obtain a JavaScript object referring to this element in the usual ways, e.g. var canvas = $("#myCanvas");
  • Generally, directly declaring canvas unnecessarily mixes intent with implementation.

In JavaScript

  • You can create a canvas element with JavaScript and append it into a DOM element:
    var canvas = document.createElement( "canvas" );
    element.appendChild( canvas );
                  
  • In the jQuery idiom this is:
    var canvas = $("<canvas />")[0];
    $(canvas).appendTo( element );
                  

Dimensions

  • By default, the canvas is 300x150 pixels.
  • You can resize it by setting the properties of the object:
    canvas.width = 800;
    canvas.height = 432;
                  
  • These properties are the dimensions of the surface into which we draw, but are not the dimensions of the element on the screen.
    Those dimensions are controlled via CSS.
    If these dimensions differ, the canvas's bitmap image will be stretched or shrunk to fit the CSS dimensions.
  • It is best to keep the two sets of dimensions in agreement. Suppose our containing element is declared in the HTML as
    <div id="game">
      <div id="gameBoard"></div>
    </div>
                  
    Then the dimensions might be set in CSS as
    #game
    {
        width: 40em;
        height: 24em;
    }
    
    #gameBoard
    {
        width: 100%;
        height: 90%;
    }
                  
    In our JavaScript (with jQuery), we create a canvas and set its dimensions to match those of the containing element like this:
    var boardDiv = $("#gameBoard"),
        canvas = $("<canvas />")[0];
    canvas.width = boardDiv.width();
    canvas.height = boardDiv.height();
    $(canvas).appendTo( boardDiv );
                  

Context

Most of the useful properties and methods we need are provided, not by the canvas object, but by a CanvasRenderingContext2D object, which is usually abbreviated to "context", "ctx", or "CRC".
We get this object for a given canvas simply by calling
var ctx = canvas.getContext( "2d" );      
          
(Again, we will not be covering 3D graphics, but for a 3D context one would call getContext( "webgl" );.)

Shapes and paths

Coordinates

  • By default, coordinates on the canvas are measured in pixels.
  • The X-axis has 0 on the left and increases to the right.
  • The Y-axis has 0 at the top and increases downward. This is the standard convention in 2D graphics, though it is opposite to the coordinate system used in most other contexts, including math texts and graphs in newspapers.

Rectangles

Drawing an outlined (stroked) rectangle

We specify a rectangle with four parameters: x (left), y (top), width, and height. Stroke draws the lines that form the rectangle.
ctx.strokeRect( 30, 20, 100, 50 );
            

Drawing a filled rectangle

In contrast to stroke, fill fills the area defined by the rectangle.
ctx.fillRect( 30, 20, 100, 50 );
            

Filling the canvas

Passing the appropriate values fills the whole canvas.
ctx.fillRect( 0, 0, canvas.width, canvas.height );
            

Clearing a rectangle

Clear fills the rectangle with transparent black.
ctx.fillRect( 0, 0, canvas.width, canvas.height );
ctx.clearRect( 30, 20, 100, 50 );
            

Paths

Paths

Shapes other than rectangles are drawn by creating paths. Paths are made out of line segments, arcs, and other curves.

Creating a simple path

This code creates a simple path, made of two connected line segments, but nothing actually gets drawn yet.
ctx.beginPath( );
ctx.moveTo( 100, 10 );
ctx.lineTo( 20, 80 );
ctx.lineTo( 180, 80 );
            

Drawing a path

We need to call stroke() to render the path.
ctx.beginPath( );
ctx.moveTo( 100, 10 );
ctx.lineTo( 20, 80 );
ctx.lineTo( 180, 80 );
ctx.stroke( );
            

Filling a path

We can also fill a path.
ctx.beginPath( );
ctx.moveTo( 100, 10 );
ctx.lineTo( 20, 80 );
ctx.lineTo( 180, 80 );
ctx.fill( );
            

Closing a path

Notice that fill() worked as if we had closed the path by drawing a line back to the starting point.
Of course we can do this manually:
ctx.beginPath( );
ctx.moveTo( 100, 10 );
ctx.lineTo( 20, 80 );
ctx.lineTo( 180, 80 );
ctx.lineTo( 100, 10 );
ctx.stroke( );
              
But an easier, and more error-proof, way is with closePath():
ctx.beginPath( );
ctx.moveTo( 100, 10 );
ctx.lineTo( 20, 80 );
ctx.lineTo( 180, 80 );
ctx.closePath( );
ctx.stroke( );
              

Setting color

Colors used for stroking and filling paths (and rectangles) are controlled by setting two properties of the context: strokeStyle and fillStyle.
There are various ways to set these styles, but the most common is to set the red, green, and blue values to numbers between 0 and 255, using a string containing a call to the rgb function.
ctx.fillStyle = "rgb( 0, 255, 0 )";
ctx.strokeStyle = "rgb( 192, 0, 0 )";
ctx.beginPath( );
ctx.moveTo( 100, 10 );
ctx.lineTo( 20, 80 );
ctx.lineTo( 180, 80 );
ctx.fill( );
ctx.stroke( );
            

Line width

We can also control the width of the stroke by setting the lineWidth property.
ctx.fillStyle = "rgb( 0, 255, 0 )";
ctx.strokeStyle = "rgb( 192, 0, 0 )";
ctx.lineWidth = 6.0;
ctx.beginPath( );
ctx.moveTo( 100, 10 );
ctx.lineTo( 20, 80 );
ctx.lineTo( 180, 80 );
ctx.fill( );
ctx.stroke( );
            

Line caps and joins

The lineCap and lineJoin properties give further control over the look of paths.
ctx.beginPath( );
ctx.moveTo( 10, 30 );
ctx.lineTo( 50, 130 );
ctx.lineTo( 90, 30 );
ctx.strokeStyle = "rgb( 0, 0, 128 )";
ctx.lineWidth = 12.0;
ctx.stroke( );
ctx.strokeStyle = "rgb( 255, 255, 0 )";
ctx.lineWidth = 1.0;
ctx.stroke( );

ctx.beginPath( );
ctx.moveTo( 70, 130 );
ctx.lineTo( 110, 30 );
ctx.lineTo( 150, 130 );
ctx.strokeStyle = "rgb( 0, 0, 128 )";
ctx.lineCap = "butt";
ctx.lineJoin = "miter";
ctx.lineWidth = 12.0;
ctx.stroke( );
ctx.strokeStyle = "rgb( 255, 255, 0 )";
ctx.lineWidth = 1.0;
ctx.stroke( );

ctx.beginPath( );
ctx.moveTo( 130, 30 );
ctx.lineTo( 170, 130 );
ctx.lineTo( 210, 30 );
ctx.lineCap = "square";
ctx.lineJoin = "bevel";
ctx.strokeStyle = "rgb( 0, 0, 128 )";
ctx.lineWidth = 12.0;
ctx.stroke( );
ctx.strokeStyle = "rgb( 255, 255, 0 )";
ctx.lineWidth = 1.0;
ctx.stroke( );

ctx.beginPath( );
ctx.moveTo( 190, 130 );
ctx.lineTo( 230, 30 );
ctx.lineTo( 270, 130 );
ctx.strokeStyle = "rgb( 0, 0, 128 )";
ctx.lineCap = "round";
ctx.lineJoin = "round";
ctx.lineWidth = 12.0;
ctx.stroke( );
ctx.strokeStyle = "rgb( 255, 255, 0 )";
ctx.lineWidth = 1.0;
ctx.stroke( );
            

Alpha

We can also control the opacity of the stroke or fill by setting the alpha value of the color. This is a value between 0.0 and 1.0, where 0 is completely transparent and 1 is completely opaque (the default).
This is done by using rgba instead of rgb.
ctx.fillStyle = "rgba( 0, 0, 192, 1.0 )"
ctx.fillRect( 0, 0, 90, 100 );
ctx.fillStyle = "rgba( 0, 255, 0, 0.3 )";
ctx.strokeStyle = "rgba( 192, 0, 0, 0.7 )";
ctx.lineWidth = 6.0;
ctx.beginPath( );
ctx.moveTo( 100, 10 );
ctx.lineTo( 20, 80 );
ctx.lineTo( 180, 80 );
ctx.fill( );
ctx.stroke( );
            

Arcs

An arc is a segment of a circle. It is created by specifying the center of the circle (x and y), its radius, and the start angle and end angle.
Angles are measured in radians clockwise from the positive X-axis. In other words angle 0 on the circle is directly to the right of the center; angle PI/2 is down, angle PI is to the left, and angle PI*3/2 is up. (This is consistent with the "Y-axis points down" coordinate system.)
ctx.strokeStyle = "rgb( 0, 0, 192 )";
ctx.lineWidth = 4.0;
ctx.beginPath( );
ctx.arc( 75, 75, 50, 0.25 * Math.PI, Math.PI );
ctx.stroke( );
ctx.beginPath( );
ctx.arc( 200, 75, 50, 1.75 * Math.PI, Math.PI );
ctx.stroke( );
            

Arc direction

By default arcs are draw clockwise, but if you add an argument of true, they are drawn counter-clockwise. Notice that the angles themselves are still the same.
ctx.strokeStyle = "rgb( 0, 0, 192 )";
ctx.lineWidth = 4.0;
ctx.beginPath( );
ctx.arc( 75, 75, 50, 0.25 * Math.PI, Math.PI, true );
ctx.stroke( );
ctx.beginPath( );
ctx.arc( 200, 75, 50, 1.75 * Math.PI, Math.PI, true );
ctx.stroke( );
            

Connecting arcs

I created separate arcs by calling beginPath() before each one, but like line segments, they can be used to build up a path. Line segments are automatically added to connect them.
ctx.fillStyle = "rgb( 255, 255, 0 )";
ctx.strokeStyle = "rgb( 0, 0, 192 )";
ctx.lineWidth = 4.0;
ctx.beginPath( );
ctx.arc( 75, 75, 50, 0.25 * Math.PI, Math.PI );
//No beginPath();
ctx.arc( 200, 75, 50, 1.75 * Math.PI, Math.PI );
ctx.fill( );
ctx.stroke( );
            

Other path elements

In addition to line segments and arcs, the Canvas API offers quadratic and cubic Bezier curves. The math behind these is more complicated, but they offer the ability to draw a variety of nice shapes.
You won't need them for most games, but you can read about them in Seidelin's and Geary's books.

Gradients and patterns

Fill and stroke styles do not need to be solid colors.

Linear gradients

A linear gradient blends colors along a line specified by the endpoints. Here our line runs from [ 100, 50 ] to [ 50, 100 ]. We set color stops at points on the line ranging from 0 for the beginning ([100, 50]) to 1 for the end ([50, 100]).
var gradient = ctx.createLinearGradient( 100, 50,  50, 100 );
gradient.addColorStop( 0, "rgb( 255, 0, 0 )" );
gradient.addColorStop( 0.7, "rgb( 0, 255, 0 )" );
gradient.addColorStop( 1, "rgb( 0, 0, 255 )" );
ctx.fillStyle = gradient;
ctx.fillRect( 10, 10, 130, 130 );
            

Radial gradients

Radial gradients are similar, but create a circular pattern.
var gradient = ctx.createRadialGradient( 40, 40, 20,  40, 40, 120 );
gradient.addColorStop( 0, "rgb( 255, 0, 0 )" );
gradient.addColorStop( 0.7, "rgb( 0, 255, 0 )" );
gradient.addColorStop( 1, "rgb( 0, 0, 255 )" );
ctx.fillStyle = gradient;
ctx.fillRect( 10, 10, 130, 130 );
            

Gradients for lighting effect

The previous examples used rather garish colors, but gradients are more often used to create more subtle lighting effects.
var gradient = ctx.createLinearGradient( 0, 0,  150, 150 );
ctx.fillStyle = gradient;
gradient.addColorStop( 0, "rgb( 128, 128, 255 )" );
gradient.addColorStop( 1, "rgb( 0, 0, 128 )" );
ctx.fillRect( 10, 10, 130, 130 );

gradient = ctx.createRadialGradient( 150, 0, 10,  150, 0, 220 );
ctx.fillStyle = gradient;
gradient.addColorStop( 0, "rgb( 128, 128, 255 )" );
gradient.addColorStop( 1, "rgb( 0, 0, 128 )" );
ctx.fillRect( 160, 10, 130, 130 );
            

Patterns

We can also use an image, such as this little Turkish flag, as a pattern for filling shapes. You specify whether to repeat the image horizontally, vertically, both, or neither. Both ("repeat") is usually best.
var img = $("#bayrak")[0];
var pattern = ctx.createPattern( img, "repeat" );
ctx.fillStyle = pattern;
ctx.fillRect( 10, 10, 280, 130 );
            

Clipping

By default we can draw anywhere on the canvas. However, we can set a clipping path which restricts the area affected by any drawing operations to the interior of that path.
This technique has many uses. One of the most obvious is to simulate a window or opening through which part of a scene is visible.
ctx.beginPath( );
ctx.moveTo( 10, 75 );
ctx.lineTo( 200, 10 );
ctx.lineTo( 200, 140 );
ctx.closePath( );
ctx.clip( );
ctx.fillStyle = "rgb( 0, 255, 0 )";
ctx.strokeStyle = "rgb( 0, 0, 192 )";
ctx.lineWidth = 5.0;
ctx.beginPath( );
ctx.arc( 60, 75, 50, 0, 2 * Math.PI );
ctx.fill( );
ctx.stroke( );
ctx.beginPath( );
ctx.arc( 190, 75, 50, 0, 2 * Math.PI );
ctx.fill( );
ctx.stroke( );
        

Saving and restoring state

The context state

  • The canvas context has a state, which includes the current values of many properties, including:
    • Stroke and fill styles and line properties
    • The clipping path
    • Text properties
    • Shadow properties
    • The transformation matrix
  • save() and restore() operate on the entire state.
  • States are saved by pushing them onto a stack and restored by popping them off in a last-in-first-out fashion.
  • This allows you to nest pairs of save/restore calls.

Example

ctx.lineWidth = 6.0;      
ctx.fillStyle = "rgb( 255, 255, 0 )";
ctx.strokeStyle = "rgb( 255, 0, 255 )";
ctx.save( );
ctx.fillStyle = "rgb( 0, 0, 255 )";
ctx.strokeStyle = "rgb( 255, 0, 0 )";
ctx.save( );
ctx.beginPath( );
ctx.moveTo( 10, 75 );
ctx.lineTo( 200, 10 );
ctx.lineTo( 200, 140 );
ctx.closePath( );
ctx.clip( );
ctx.fillRect( 30, 10, 50, 130 );
ctx.strokeRect( 30, 10, 50, 130 );
ctx.restore( );
ctx.fillRect( 130, 10, 50, 130 );
ctx.strokeRect( 130, 10, 50, 130 );
ctx.restore( );
ctx.fillRect( 230, 10, 50, 130 );
ctx.strokeRect( 230, 10, 50, 130 );
          

Text

Drawing text

ctx.font = "bold 40px Text";
ctx.fillText( "Merhaba!", 20, 50 );
ctx.strokeText( "Merhaba!", 20, 100 );
          

Placement

ctx.strokeStyle = "rgb( 255, 0, 0 )";
ctx.beginPath( );
ctx.moveTo( 0, 30 );
ctx.lineTo( 299, 30 );
ctx.moveTo( 0, 60 );
ctx.lineTo( 299, 60 );
ctx.moveTo( 0, 90 );
ctx.lineTo( 299, 90 );
ctx.moveTo( 0, 120 );
ctx.lineTo( 299, 120 );
ctx.moveTo( 150, 0 );
ctx.lineTo( 150, 149 );
ctx.stroke( );
ctx.strokeStyle = "rgb( 0, 0, 0 )";
ctx.font = "bold 15px Text";
ctx.strokeText( "default default", 150, 30 );
ctx.textAlign = "left";
ctx.textBaseline = "alphabetic";
ctx.strokeText( "left alphabetic", 150, 60 );
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.strokeText( "center middle", 150, 90 );
ctx.textAlign = "right";
ctx.textBaseline = "top";
ctx.strokeText( "right top", 150, 120 );
          

Shadows

ctx.fillStyle = "rgb( 255, 0, 255 )";
ctx.shadowColor = "rgb( 0, 0, 0 )";
ctx.shadowOffsetX = 10;
ctx.shadowOffsetY = 20;
ctx.shadowBlur = 0;
ctx.fillRect( 20, 20, 100, 100 );
ctx.shadowColor = "rgb( 0, 255, 0 )";
ctx.shadowOffsetX = -20;
ctx.shadowOffsetY = -10;
ctx.shadowBlur = 10;
ctx.fillRect( 180, 20, 100, 100 );
        

Transformations

Changing coordinates

  • A transformation changes the current coordinate system used for drawing.
  • Transformations can include and/or combine translation, rotation, scaling (uniform and nonuniform), or any other linear operation.
  • You will generally apply transformations within a save/restore block.

Translation

translate takes two parameters, x and y and adds the vector [ x, y ] to each point.
ctx.fillStyle = "rgb( 96, 96, 96 )";
ctx.lineWidth = 4.0;
ctx.fillRect( 110, 55, 80, 40 );
ctx.translate( -60, 30 );
ctx.strokeRect( 110, 55, 80, 40 );
          

Scaling

scale takes two parameters, sx and sy and multiplies the x-coordinates by sx and the y-coordinates by sy.
This is equivalent to scalar multiplication of vectors if sx = sy. That is called uniform scaling.
ctx.fillStyle = "rgb( 96, 96, 96 )";
ctx.lineWidth = 4.0;
ctx.fillRect( 110, 55, 80, 40 );
ctx.scale( 0.5, 2 );
ctx.strokeRect( 110, 55, 80, 40 );
          

Scaling about a point

Scaling changes both the size and position of objects. Often you want to keep some point, say the center of the object, fixed. In this case you must first translate the origin to that point, then scale, then translate back.
(We can't do much about the fact that non-uniform scaling scales the stroke non-uniformly.)
ctx.fillStyle = "rgb( 96, 96, 96 )";
ctx.lineWidth = 4.0;
ctx.fillRect( 110, 55, 80, 40 );
ctx.translate( 150, 75 );
ctx.scale( 0.5, 2 );
ctx.translate( -150, -75 );
ctx.strokeRect( 110, 55, 80, 40 );
          

Rotation

rotate takes one parameter, an angle, and rotates the coordinate system clockwise by this angle.
Note that rotation is about the origin ([ 0, 0 ]) which is the top-left corner of the canvas.
ctx.fillStyle = "rgb( 96, 96, 96 )";
ctx.lineWidth = 4.0;
ctx.fillRect( 110, 55, 80, 40 );
ctx.rotate( Math.PI / 6 );
ctx.strokeRect( 110, 55, 80, 40 );
          

Rotation about a point

Generally want to rotate about another point, say the center of the object you are drawing. In this case you must first translate the origin, then rotate, then translate back.
ctx.fillStyle = "rgb( 96, 96, 96 )";
ctx.lineWidth = 4.0;
ctx.fillRect( 110, 55, 80, 40 );
ctx.translate( 150, 75 );
ctx.rotate( Math.PI / 6 );
ctx.translate( -150, -75 );
ctx.strokeRect( 110, 55, 80, 40 );
          

Transformation matrix

  • All of the supported transformations are linear: they preserve the straightness of lines.
  • Any linear translation can be represented by a matrix, which is a two-dimensional array of numbers in the same sense that a point or vector is a one-dimensional array (e.g. [ x, y ]).
  • For example, this is the matrix that represents translate( x, y ):
    1 0 x
    0 1 y
    0 0 1
                  
  • There is a well-defined way that matrices can be multiplied by vectors, points, and other matrices to produce new vectors, points, or matrices. But we will not go into this mathematics in this class.
  • The canvas context maintains the current transformations as a matrix and even allows you to set the top two rows directly:
    setTransform( a, b, c, d, e, f ) sets the matrix to
    a c e
    b d f
    0 0 1
                  
  • Setting the transformation matrix does allow for some special transformations, such as shearing, which are used occasionally. translate, scale, and rotate will generally be plenty.

Images

Drawing an image

The simplest form of drawImage takes three parameters. The first specifies the image, which can be an HTML img element, or a canvas element (or the current frame from a video). The next parameters, x and y, specify the location of the top-left corner on the canvas. The whole image is rendered.
var img = $("#Tux")[0];
ctx.drawImage( img, 25, 10 );  
          

Scaling an image

We can also pass a width and height parameter. The whole image is rendered, but scaled to fill the specified rectangle on the canvas.
This will make our well-fed penguin even fatter.
var img = $("#Tux")[0];
ctx.drawImage( img, 25, 10, 200, 100 );
          

Drawing part of an image

Another form of drawImage takes nine parameters: the image object and two rectangles. The first rectangle is the portion of the source image to render and the second is the position and extent of the canvas destination.
This will both crop and scale our image:
var img = $("#Tux")[0];
ctx.drawImage( img, 40, 0, 64, 60,  125, 10, 128, 120 );
          

Accessing pixel data

Pixel data

  • A pixel (short for "picture element") represents a single dot on the screen, the "atom" of raster-based graphics.
  • On the Canvas, each pixel is four bytes, representing the red, green, blue, and alpha values of the color (and opacity) of the pixel.
  • The getImageData method gets the pixel data from a rectangle on the canvas.
  • The image data is a sequence of bytes, four for each pixel, arranged from left to right, top to bottom.
  • We can read from and write to this array
  • The putImageData method writes an image data array to the canvas at the specified location.
  • Most often we will write it back to the same location we read from, after making some modifications.

Example

We first draw some horizontal lines on the canvas. Then we read this image data as our source. We create a destination data buffer of the same size. For each pixel in the destination buffer we use one in the source buffer that is at the same X, but with its Y offset by a specific amount using the sine function. We write the destination buffer back to our canvas.
var bg = { r: 64, g: 64, b: 255, a: 255 },
    srcImgData, destImgData, imgWidth, imgHeight,
    waveTable, i, a, s,
    x, y, srcY, srcLoc, destLoc;
//Draw strips on the canvas
ctx.fillStyle = "rgb( " + bg.r + ", " + bg.g + ", " + bg.b + " )";
ctx.fillRect( 0, 0, canvas.width, canvas.height );
ctx.strokeStyle = "rgb( 0, 255, 0 )";
ctx.lineWidth = 10;
ctx.beginPath( );
for ( i = 0; i < canvas.height / 20; ++i )
{
    ctx.moveTo( 0, i * 20 );
    ctx.lineTo( 300, i * 20 );
}
ctx.stroke( );
//Read the image data and make a copy to write to
srcImgData = ctx.getImageData( 0, 0, canvas.width, canvas.height );
destImgData = ctx.createImageData( srcImgData );
imgWidth = srcImgData.width;
imgHeight = srcImgData.height;
//Make a table of wave offsets
waveTable = [];
for ( i = 0; i < imgWidth; ++i )
{
    a = (i/imgWidth) * 6 * Math.PI;
  s = Math.sin( a );
    waveTable.push( Math.round( s * 15 ) );
}
//Build the dest image data based on the source
for ( y = 0; y < imgHeight; ++y )
{
    for ( x = 0; x < imgWidth; ++x )
    {
        destLoc = (x  +  y * imgWidth) * 4;
        srcY = y - waveTable[ x ];
        if ( (srcY >= 0) && (srcY < imgHeight) )
        {
            srcLoc = (x  +  srcY * imgWidth) * 4;
            for ( i = 0; i < 4; ++i )
            {
                destImgData.data[ destLoc + i ] = srcImgData.data[ srcLoc + i ];
            }
        }
        else
        {
            destImgData.data[ destLoc ] = bg.r;
            destImgData.data[ destLoc + 1 ] = bg.g;
            destImgData.data[ destLoc + 2 ] = bg.b;
            destImgData.data[ destLoc + 3 ] = bg.a;
        }
    }
}
//Replace the canvas's image data with the new version
ctx.putImageData( destImgData, 0, 0 );
          

Background example

BeachSpin background

Homework #4

http://EpsilonDelta.us/UW_GameplayHTML5/Homework/Homework_4.html.