Canvas – Transforms

The Canvas API has 3 basic transform functions. What’s a little non-intuitive about these functions is they act on the canvas itself, and only indirectly on the canvas contents.

In Canvas – The Beginning the coordinate system was introduced with 0,0 defaulting to the top left corner of the canvas; which was kind of weird because the 2D plots most are first introduced to depict 0,0 in the center of the graph.

Translate

The Translate function moves this 0,0 origin to an offset passed in the x,y argument. So to move the origin to the center of the canvas on the screen:

ctx.translate(canvas.width/2, canvas.height/2);

Transform functions effect what is drawn after the function, it doesn’t effect the contents that already on canvas. A rectangle drawn after the translate function call above:

ctx.fillRect(0,0,25,25);

Results in this

translate1

Rather than this (if drawn before the translate):

Note that the rectangle is drawn from its top left corner. So to center the rectangle in the canvas after the translate, its x and y need to be adjusted:

 var size = 50;
ctx.fillRect(-0.5*size,-0.5*size,size,size);

 

translate2

Rotate

Rotates a canvas around its current origin. That origin part is key to some nifty effects (and confusion sometimes.)

To rotate the rectangle drawn previously 45 degrees:

ctx.translate(canvas.width/2, canvas.height/2);
ctx.rotate(45*180/Math.PI);
var size = 50;
ctx.fillRect(-0.5*size,-0.5*size,size,size);

rotate

the rotate function takes radians as its argument and the formulas for switching between degrees and radians are:

radians = degrees * Math.PI / 180
degrees = radians * 180 / Math.PI

Note this will rotate everything that is drawn after rotate is called until the transformations are reset back to default either through:

ctx.save() and ctx.restore() which saves and restores the state of the canvas, including the state of the transformations, fills, line sizes, etc (but not the graphics on the canvas.)

or

ctx.setTransform(1,0,0,1,0,0);

which is faster, but just restores the state of the transformations back to their default settings.

Scale

ctx.scale(x,y) scales pixels (by default) x horizontally and y vertically where x and y are real numbers and 1.0 = current size and < 1.0 shrinks and > 1.0 expands. So 0.5 pixels would be half size and 2.0 would be twice as large.

Negative values can create some interesting effects:

// mirror horizontally
ctx.scale(-1, 1);

Trig and the Circle

Trigonometry is all about the triangle. What makes it useful for navigating points and angles around a circle is illustrated in this diagram.

trig

Trig functions, like sine and cosine are ratios of different sides of triangles at different angles (in radians) and this ratio can be used to draw an object at a specific point on the perimeter of a circle of any radius.

Another trig function, the tangent, or rather an inverse variant of it Math.arctan2(), when given the length of two sides returns the angle. Very handy for rotating one thing to face another or reading a value off interface elements like dials.

Using a couple of these transforms, and some trigonometry, the next experiment explores aesthetic effects of rotation on a simple construct. Try playing with the rotation speed and blur sliders to see a range of visuals that can be created from a few spokes of rectangles.

See the Pen Pinwheels by Kentskyo (@kentskyo) on CodePen.0

Canvas – Scaling

Adapting a canvas application to display on multiple devices with different resolutions and aspect ratios is a gnarly but necessary task. Before diving further into creating canvas art, this post designs a framework that handles scaling issues and can be found on codepen as a template.

Presenting canvas art on multiple devices usually involves one of two strategies:

  1. Scale the real dimensions of the canvas (may be hard, depending on the design.) The physical dimensions of the canvas change; the position and size of the canvas elements must adapt programmatically based on the dynamic resolution and aspect ratio of the device.
  2. Scale the Canvas element (challenging, depending on the range of devices.) This approach starts with a fixed canvas size; the position and size of canvas elements target this size and the canvas dimensions do no appear to change to the program, however, the canvas element itself is visually scaled used CSS to match the device resolution and aspect ratio.

Approach #1: Code for Resolution Independence

Generally this is much trickier to code for. Starting with a simple example:

var canvas = document.getElementById('canvas'); 
var ctx = canvas.getContext('2d'); 
var img = new Image();

img.src = '200ax200a.png'; 
img.onload = draw;

function draw(){

    ctx.fillStyle = "#48160D";
    ctx.fillRect(0,0,300,300);

    ctx.fillStyle = "#FEBF28";
    ctx.fillRect(0,0, 50,50); // top left corner
    ctx.fillRect(0,250, 50, 50); // bottom left
    ctx.fillRect(250,0, 50, 50); // top right
    ctx.fillRect(250, 250, 50, 50); // bottom left

    // put a 200x200 image in the middle
    ctx.drawImage(img, 50, 50);
}

on a device with screen dimensions of 1024 x 768 this code will result in this:

2015-09-26_192120

Note the page background color is a lighter red than the canvas background to demarcate what is happening. The canvas can be repositioned with CSS but here it defaults to the standard flow in HTML where elements are written from the top, left corner to the right until they wrap or extend off-screen.

To own the space on the page with a resolution independent approach every element drawn on the canvas needs to adapt:

function draw() {
    ctx.fillStyle = "#48160D";
    ctx.fillRect(0,0,canvas.width,canvas.height);

    ctx.fillStyle = "#FEBF28";
    var newRW = 0.166 * canvas.width;
    var newRH = 0.166 * canvas.height;
    var newIW = 0.666 * canvas.width;
    var newIH = 0.666 * canvas.height;

    ctx.fillRect(0,0, newRW,newRH); // top left corner
    ctx.fillRect(0,canvas.height-newRH,newRW,newRH); // bottom left
    ctx.fillRect(canvas.width-newRW,0, newRW,newRH); // top right
    ctx.fillRect(canvas.width-newRH, canvas.height-newRH,newRW,newRH); // bottom left

    // put a 200x200 image in the middle and scale up
    ctx.drawImage(img, canvas.width/2-newIW/2, canvas.height/2 - newIH/2, newIW, newIH);
}

Now on the 1024 x 768 layout it looks like this:

2015-09-26_195214

It’s elements are skewed because it ignores the aspect ratio of the original (300×300 or 1:1)  It just took whatever ratio was needed to fill the display area using the following for its adaption:

function resizeCanvas() {
    var viewport = { // Get the dimensions of the viewport
        width: window.innerWidth,
        height: window.innerHeight
    };
    // adjust canvas
    canvas.width = viewport.width; 
    canvas.height = viewport.height;
    draw();
}

For a best fit, preserving the original aspect ratio but filling as much of the viewport as possible a different approach is required:

function resizeCanvasAspect() {

    var viewport = { // Get the dimensions of the viewport
        width: window.innerWidth,
        height: window.innerHeight
    };

    if (canvas.height / canvas.width > viewport.height / viewport.width) {
        newHeight = viewport.height;
        newWidth = newHeight * canvas.width / canvas.height;
    } else {
        newWidth = viewport.width;
        newHeight = newWidth * canvas.height / canvas.width;
    }

    // adjust canvas
    canvas.width = newWidth;
    canvas.height = newHeight;
    draw();
}

Now it stays proportional and fills the max available space with those proportions:

2015-09-26_201556

adding a little tweak to center the canvas on the page:

// center
var newX = (viewport.width - newWidth) / 2;
var newY = (viewport.height - newHeight) / 2;

// Center by setting the padding of the game
document.getElementById("canvas").style.padding = newY + "px " + newX + "px";

2015-09-26_202339

Note the CSS background color now brackets each side of the centered canvas. This is called a “letter-box” Some ways of mitigating this padding are discussed a bit later.

Approach #2: Scale the Canvas Element

The second technique, scaling the canvas, uses CSS to resize the canvas element and doesn’t require adapting any of the canvas code or recalculating coordinates. As far as the code is concerned it’s still operating in a 300×300 pixel world:

var canvas = document.getElementById('canvas'); 
var ctx = canvas.getContext('2d'); 
var img = new Image();

img.src = '200ax200a.png'; 
img.onload = resizeCanvas();


function draw(){

    ctx.fillStyle = "#48160D";
    ctx.fillRect(0,0,300,300);

    ctx.fillStyle = "#FEBF28";
    ctx.fillRect(0,0, 50,50); // top left corner
    ctx.fillRect(0,250, 50, 50); // bottom left
    ctx.fillRect(250,0, 50, 50); // top right
    ctx.fillRect(250, 250, 50, 50); // bottom left

    // put a 200x200 image in the middle
    ctx.drawImage(img, 50, 50);
}


function resizeCanvas () {
    // Get the dimensions of the viewport
    var viewport = {
        width: window.innerWidth,
        height: window.innerHeight
    };

    // Determine canvas size
    if (canvas.height / canvas.width > viewport.height / viewport.width) {
        newHeight = viewport.height;
        newWidth = newHeight * canvas.width / canvas.height;
    } else {
        newWidth = viewport.width;
        newHeight = newWidth * canvas.height / canvas.width;
    }


    // Resize canvas
    document.getElementById("canvas").style.width = newWidth + "px";
    document.getElementById("canvas").style.height = newHeight + "px";

    // center
    newX = (viewport.width - newWidth) / 2;
    newY = (viewport.height - newHeight) / 2;

    // Center by setting the padding of the game
    document.getElementById("canvas").style.padding = newY + "px " + newX + "px";

    draw();
}

window.addEventListener("resize", resizeCanvas);

A CSS Transform could be use in place of modifying the style width and height above, and may prove faster as transforms are often optimized in the GPU of modern devices:

function resizeCanvas () {

    var scaleX = window.innerWidth / canvas.width; 
    var scaleY = window.innerHeight / canvas.height;

    var scaleToFit = Math.min(scaleX, scaleY);

    //var scaleToCover = Math.max(scaleX, scaleY); 
    document.getElementById("canvas").style.transformOrigin = "0 0"; //scale from top left 
    document.getElementById("canvas").style.transform = "scale(" + scaleToFit + ")";


    // Center by setting the padding of the game
    var newX = Math.floor(((window.innerWidth - scaleToFit * canvas.width) / 2) / scaleToFit);
    var newY = Math.floor(((window.innerHeight - scaleToFit * canvas.height) / 2) / scaleToFit);
    document.getElementById("canvas").style.padding = newY + "px " + newX + "px";

    draw();
}

Some notes on managing letter-boxing:

  • Use a background image on the page that blends with the canvas.
    https://css-tricks.com/perfect-full-page-background-image/
  • Extend the canvas a little more than needed, center it and keep all the critical operations with a safe-area
    http://www.williammalone.com/articles/html5-game-scaling/

Choosing a good aspect ratio helps mask the letter-box effect. Unfortunately there is no universal ratio that works best, it all depends on your target audience/devices. Popular ratios for HTML5 game development are:

  • 800×500 (1.6)
  • 960×540 (1.77..)
  • 960/640 (1.5)

Viewports

On desktop browsers it’s pretty simple, the size of the viewport is the size of the browser window. On mobiles it gets squirrelly.

A mobile device, with physical screen of 480 x 640 css pixels, for example, runs into a typical website, not optimized for mobile or responsive, which generally has widths between 800 and 1024. In order to try to display this site without zooming into a corner, the mobile will claim it has a default width of 980 (usually, though it varies by mobile device) This is the layout viewport, used to accommodate sites designed for desktops. A second viewport, the visual viewport is different and defines the area of the layout currently visible. This viewport zooms in and out on the content without affecting the layout viewport. A third viewport, the ideal viewport, is the dimensions that the mobile device reports as optimal for a display area that doesn’t need to zoom. This viewport is defined by the mobile’s browser (i.e. different vendor’s browsers on the same mobile may have different ideal viewports) and usually matches, more or less, the physical CSS pixels (not the device pixels) that the mobile is capable of.

When a meta tag in the HTML states:

<meta name="viewport" content="width=device-width">

it tells the mobile to use the ideal viewport size instead of the default layout size and basically means the site is mobile ready.

This will be used for the canvas html as well, but it needs some additional tweaks:

<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height, target-densitydpi=device-dpi" />

While heretical to responsive design, where disabling user zoom is regarded as evil, it is essential for our canvas art where a double touch can result in zooming parts of the canvas off the screen.

Those are the hacks to-date, with more experiments this article may undergo further revision.

There is also a set of strategies for optimizing for high pixel densities, where 1 CSS pixel may map to 2 or more actual device pixels. Topic for a future article though.

Some additional reading and sources: