Showing posts with label webpack. Show all posts
Showing posts with label webpack. Show all posts

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!

Monday, January 24, 2022

Setting up a test bench for TypeScript compiled client code with Node.js (why are things like this?)

My first computer was an Apple ][c owned by my parents. In that environment, booting the machine with no disk in the drive would load a working development environment in the form of an Applesoft BASIC REPL.

Sometimes, when I spend an hour configuring a development environment for testing a web application, I miss those days.

I've uploaded a bare-bones testbench to GitHub for playing with TypeScript code compiled to run in a browser client. To do so, I set up the following dependencies:

  • typescript
  • webpack (collects JavaScript into one file)
  • ts-loader (lets webpack drive TypeScript compilation)
  • webpack-cli (command-line tools to drive webpack)
  • webpack-dev-server (server to watch system changes)

Once these were installed, I had to set up with a tsconfig.json file for TypeScript and a webpack.config.js file for webpack. It took a little while to pull all the pieces together, but the nice thing is that now they're set up and I don't have to do it again.

Why are things like this?

When I look back on how programming was when I started vs. now, I get nostalgic from time-to-time about how things were simpler. We just turned the computer on and coded it! What happened?

More flexibility means more complexity

The computer I coded on when I was six did exactly one thing at a time. Meanwhile, as I type this, I have my testbench open in another window, documentation for npm in a second window, my blog statistics open in a third window... Modern computers are simply more flexible, and there does exist a strong correlation between flexibility and complexity.

In a real sense, the art of software engineering isn't creating features, it's denying options. The most featureful computer is a machine exposing a REPL. You can do anything it can do! You just have to write the code! But we've built an entire global industry around paring down the set of possible things a computer could do to the subset people actually want to do (and in so doing, surfacing those features behind a few buttons at the cost of pushing every other feature of the machine deeper into a tree of options). An Apple ][c with no disk in it "knew" I wanted to program something in BASIC when I turned it on; a modern computer has no "idea" what the user wants.

Since building a tiny web server with TypeScript and Node.js is as likely to be what I want to do as checking my bank account, there is necessarily more complexity to make it happen.

Embedded development isn't like desktop development

Since I'm working on "just some browser stuff," it's easy to fall into the trap of thinking that what I'm doing is simple. Browsers are easy to code; pop open one text file and start writing.

That's true... If you want to code in HTML and JavaScript. But add TypeScript to the mix and, oops, I'm not doing "native" coding anymore. Now I need an infrastructure to deal with the impedance mismatch between the fact that the browser only understands JavaScript and HTML and the fact that the only context I'm willing to code in bare JavaScript anymore is if a child's life is on the line (and even then, it'd better be a child I like).

Looked at in this way, writing web comtent in TypeScript isn't even just compiled; it's more like embedded development: we are writing code in a language not directly supported by the embedding target (the browser) and we need to update the target's view of the code when it changes (recompile and put the data where the browser can see it). In that sense, writing web content with TypeScript looks a lot more like writing code against a microcontroller (albeit a fancy microcontroller run in a virtual machine on my desktop) than writing a BASIC program. Some complexity is to be expected. 100% of the dependencies I installed were to support automatic recompilation and hosting of changes to the code without having to stop and re-start a server.

The real problem is discoverability

At the end of the day, none of these problems would be problems if I already knew what I was doing. This testbench isn't the first one people have written (nor will it be the last). But currently, a search on GitHub for typescript node testbench only returns my code repository!

A view of the GitHub search interface, showing typescript-node-testbench as the only search result for "typescript node testbench"
This can't be true.

I can hope that someone sees this blog post and that the existence of my project somehow gets into StackOverflow or Google search results, but ultimately, this is the real complexity of modern programming: when you want to do something relatively simple, how do you get started? In the '80s, I could start by powering on the computer. How do I start now? typescript node getting started returns several more results, but none quite what I'd want (most of them add more complexity than I need; many are descriptions of how to solve the problem, not workable starter code).

This problem isn't trivial to solve; computing moves fast these days. Several of the technologies I'm using for this project didn't exist ten years ago. But putting starter frameworks in a place people can find them (and perhaps, even, creating a language where people can describe what they want) still feels like a problem in need of a better solution.

We've made incredibly flexible machines, and our ability to talk about them hasn't caught up to the inherent complexity of that flexibility.

Monday, December 27, 2021

Making a turn-based game: A game server on DreamHost

The next step in setting up our turn-based game is prepping up our server to do double-duty: it needs to both vend static HTML content and handle dynamic client-server traffic to run the game logic. To that end, we need to extend the initial configuration to support both.

Koa server

Boardgame.io integrates with the koa framework to serve HTML. It's a relatively straightforward web server that supports a static server (koa-static); the server is even instrumented with the npm debug library and can give debug logging output if the DEBUG=koa-static env var is set.

Armed with this knowledge, I can improve the server.js file to vend static content.

const frontendJsPath = path.resolve(__dirname, '..');
const frontendPublicPath = path.resolve(__dirname, '../../public');
server.app.use(async (ctx, next) => {
    console.log(`requesting ${ctx.request.path}`);
    await next();
});
server.app.use(serve((frontendJsPath)));
server.app.use(serve((frontendPublicPath)));

Now we're making progress, but the client code doesn't work because the browser can't import npm modules. It's time to bundle up the client-side code into something a regular JS browser can handle.

Webpack

Being already familiar with webpack, I chose to grab that as my weapon of choice for generating browser-compatible code. First step is to get it into the project:

npm install --save-dev webpack
npm install --save-dev ts-loader

I set up the webpack config based on the guide and changed my build rule to tsc && webpack. My output is now public/bundle.js instead of build.

At this point, I realized I'd need two different configs for client and server code. I can do this with two tsconfig.json files and the tsc -p build option to select which one to build. With a little reorganizing of my code (client-specific in "client", server-specific in "server", and common in "game"), I can now generate two build artifacts: a server that can run in Node, and a client that the server vends to a browser that connects to it.

After uploading all that content (the build artifacts and the node_modules directory) and giving it a run, I had a successful instance of the game running from my server.

The Swatch user interface, showing a color to name


Now to set up a lobby.

Supporting a lobby

Boardgame.io provides a rudimentary lobby server that makes it easy to create and connect to games (just replace the top-level game object in your code with a Lobby client). Not quite documented is that lobby also assumes that HTML components that are class hidden will be hidden from user view (display: none css rule works fine). At this point, I also discovered that the default config rules for caching on my Dreamhost server didn't work well; every piece of GET content was served with a Cache-Control: max-age=172800. I added some cache-breaking to the .htaccess file:

Header set Cache-Control "no-cache, no-store, must-revalidate"
Header set Pragma "no-cache"
Header set Expires 0

Hiding colors from end-users

One last bit of housekeeping at this stage: the color database is a JSON object that gets read into memory, but it should live only in the server memory; clients should not get a whole copy of it. To that end, I stored the colors in private/colors.json and used Node to fetch them (require('fs'), require('path'), dynamically load the file at boot time). I also had to get those files out of webpack's field of view so it didn't try (and fail) to bind fs and path into browser code. The following got added to webpack.config.js:

resolve: {
  fallback: {
    'fs': false,
    'path': false,
  }
}

So in the common code, I can check if fs.readFileSync is defined, and if it is not I know I'm in the browser and can avoid trying to load colors.

Give it a try!

The game is up and available for 2 or more players at http://swatch.fixermark.com; feel free to try it and let me know what you think!

Next post, I'll talk a bit about re-architecting the game to support different kinds of rounds.