Monday, January 31, 2022

Dynamic web page background with the Paint API

To spruce up the look of Swătch, I'm investigating animating a background for the game. This turns out to be a bit less trivial than I'd hoped; there are a couple of options, but none of them at this time are great. In this post, I'll describe some options I considered and which I settled on.

A pattern of regular hexagons; egshell fill with white borders
The bestagon

CSS Painting API

A relatively new proposal is the CSS Painting API. This API lets you "hook" the CSS rendering pipeline via code similar to a web-worker. When CSS demands an image to render, it will delegate to your painter with a 2D render context, height and width of the space to draw, and any additional CSS properties you declare that your painter receives; you then render into the provided context (using JavaScript code) and CSS will use the result as an image.

This API is powerful, but hooking into it required some adjustment. To build a web worker, I had to modify my webpack configuration to emit two output files; webpack supports this via modifying the entrypoint to use object syntax and then modifying the output rule to look like [name].js to template-in the object name from the entrypoints. But I then had to fight TypeScript because the Painting API isn't in the dom types yet; I fought with webpack trying to use .d.ts files (tricky; webpack hates this) but finally gave up and wrote the paint worker in plain ol' JavaScript.

Pros:

  • Anything you can do in canvas rendering (kinda) you can do to the background of your object
  • Uses regular CSS, so tools like animations work

Cons:

  • Fighting with TypeScript to use an API that isn't typed yet
  • Modify webpack to emit a web worker
  • (the big one) This API not supported by Firefox of Safari yet

Deciding I didn't want to limit my new feature to only Chrome users, I sought an alternative.

toDataURL and canvas

It's possible to have a bare canvas object emit its image buffer via a data URL. Once you've done that, you can set the background image of an element to that data URL, and you're all set. A bit inefficient, but it works. There's a great tutorial post here describing the process.

Armed with this approach, it was relatively straightforward to build a small hexagon background on a 100 x (height of the hexagon) rectangle.

Pros:

  • Supported in all modern browsers
  • Works with background-repeat CSS, so constructing and setting a small image automatically gives you tesselation

Cons:

  • Will this be fast enough to do simple animations? Not yet sure

Armed with this approach, I put together a simple demo on top of my testbench to prove out the idea.

index.ts:

const SEGMENT = 100 / 6;
const HEX_HEIGHT_WIDTH_RATIO = 1.1547005;

function go() {
    const canvas = document.createElement('canvas');
    canvas.width = 100;
    const HEIGHT = SEGMENT * 4 / HEX_HEIGHT_WIDTH_RATIO;
    canvas.height = HEIGHT;
    const ctx = canvas.getContext('2d');
    if (!ctx) {
        return;
    }

    ctx.fillStyle = '#EEEEDD';
    ctx.fillRect(0,0,100,HEIGHT);


    ctx.strokeStyle= "white";
    ctx.lineWidth = 3;
    ctx.beginPath();
    ctx.moveTo(0, HEIGHT / 2);
    ctx.lineTo(SEGMENT, HEIGHT / 2);
    ctx.lineTo(2 * SEGMENT, 0);
    ctx.lineTo(4 * SEGMENT, 0);
    ctx.lineTo(5 * SEGMENT, HEIGHT / 2);
    ctx.lineTo(100, HEIGHT / 2);
    ctx.moveTo(SEGMENT, HEIGHT / 2);
    ctx.lineTo(2 * SEGMENT, HEIGHT);
    ctx.lineTo(4 * SEGMENT, HEIGHT);
    ctx.lineTo(5 * SEGMENT, HEIGHT / 2);
    ctx.stroke();

    const body = document.getElementsByTagName('body').item(0);
    if (!body) {
        return;
    }
    body.style.background=`url(${canvas.toDataURL()})`;
}

go();

The result generated the pattern at the top of this post; I'm not at all unhappy with it!

No comments:

Post a Comment