1 - The Canvas

 ·   ·  Graphics Programming

There are multiple ways to render things to a webpage. We’re going to choose the <canvas> element as it provides the most control. Given the declarative nature of our approach though the drawing routines provided by the canvas API are not going to be used. Therefore we’ll create a proxy class to wrap the element and hide the dirty details:

class Canvas {
    #canvas = document.createElement('canvas')
    #ctx = this.#canvas.getContext('2d',{alpha: false})
    constructor({height, width}) {
      Object.assign(this.#canvas, {height, width})

    appendTo({element}) {

The private property #canvas holds the actual HTML element, and the appendTo method will be used to attach it the document. The constructor accepts a height and a width for defining the initial size. The #ctx property references the drawing api of the canvas. The canvas is transparent by default. To prevent any confusion about what is on the canvas and what is behind it we make it opaque with the {alpha: false} option.

We want to keep this class simple so we’ll limit the ability to draw on it by providing a single method called draw:

class Canvas {
    draw({imageData, top, left}) {
        this.#ctx.putImageData(imageData, top, left)

The drawing context requires an ImageData object and a position therefore our draw method must accept the same. The ImageData object contains a one-dimensional array that represents pixels in RGBA order with integer values between 0 and 255. That’s 24 bits for color plus 8 bits for the alpha channel. This color depth is referred to as True Color. The initial value of the array is zero filled which represents transparent black.


For example if the dimensions are 640x360 then the size of imageData is width x height x 4 bytes = 921,600 bytes. With different dimensions you can see that the memory requirements would change significantly.

A note on style: you’ll notice that parameter destructuring is used in the definition of draw. Named parameters are preferable to positional for a couple reasons:

  1. It makes it obvious what the actual parameters are without having to inspect the implementation. Ex: repo.find(12,140,53202) vs repo.find({age: 12, weight: 140, zip: 53202})
  2. Extensions can be made without impacting clients Ex: repo.find({name: 'bob', age: 12, weight: 140, zip: 53202})

This style of named parameters will be used in all future examples.


You can create, reply to, and manage comments on GitHub