Canvas based Web Components: Lessons Learned

I recently have spent time learning html canvas and found it to be a perfect fit for web components. Finding the need to maintain simplicity while creating canvas animations I have learned the following lessons.

Setting the scene

The web component should be responsible for creating the canvas and sizing it to fill the needed space. This will often be the size of the web component itself but might also be the full window size in some scenarios such as for website intro animations.

The the web component should pass around configuration values, maintain environment information, and run the event loop.

Configurations passed through the attributes of the element should be scaled to a value of 1. For example providing a speed of 2 should double the speed. In this way client code does not need to know that the default speed might actually be 7, for example.

Here is an example of how you can scale a provided attribute from the value of 1:
this._defaultSpeed = 0.1;
this.speed = parseFloat(this.getAttribute("speed")) * this._defaultSpeed
                      || this._defaultSpeed;

Making stuff happen

Drawing to the canvas and updating the state of the objects in order to create animations should not be done in the component but in objects that the component creates
connectedCallback() {
    ...
  
    // Set the scene by creating objects
    this.objects.push(new ExampleLine({
        context: this.context,
        contextWidth: this.canvas.width,
        contextHeight: this.canvas.height,
        thickness: this.lineThickness,
        speed: this.speed
    }));

    ...
}
This ExampleLine class should be capable of updating and drawing itself on each animation loop.
class ExampleLine {
    draw() {
        // Draw this object to the canvas
        ...
    }

    update() {
        // Update internal state of this object.
        // Environment information may need passed in from the web component
        ...
    }
}

Knowing when to quit

The web component should know when to destroy objects that are no longer need as well as when to stop the animation loop.

If an animation never ends then the complexity of animation must be constant. If you add a new object to the scene every 100ms for example, then you need to destroy objects at the same rate. Otherwise the scene will grow to be too complex and will eventually use too much memory.

If an animation is only needed during certain user interactions such as a hover or a scroll, the web component needs to know this and only run the animation loop when it is needed. The 'objects' within the scene should not need to care about this information. They should simply update and draw themselves when their corresponding methods are called.

Relativity is crucial

All calculations should be done as a percentage of width and height and then only converted to absolute values when drawing to the canvas based upon the width and height of the canvas.
class ExampleLine {
    // Only convert from scaled positions to absolute positions
    // when they are needed to draw to the canvas.
    // This method converts the 0 to 1 scaled positions into
    // absolute sizes based upon the canvas's width and height.
    get pos() {
        return {
            p1: { x: this.contextWidth  * this.p1.x,
                    y: this.contextHeight * this.p1.y },
            p2: { x: this.contextWidth  * this.p2.x,
                    y: this.contextHeight * this.p2.y }
        }
    }
}

A full example

This codepen provides a full example of these methods of creating canvas based web components. It is overkill for this simple animation but provides an extensible pattern for creating more complex animations. The website canvas.alexlockhart.me provides an example of some of the web components I have created using these methods.

Comments

Popular Articles

The Vanilla Javascript Component Pattern

The Sunless Citadel: A D&D 5e Session Report

Getting Started with Harp