Friday, 6 August 2021

Hexworld 2: Encoding

As mentioned in the first part, the map of Hexworld consists of a 864-by-360 staggered grid of hexagons. Each hexagon (hexel) holds an 8-bit index, so the whole thing fits into about 300KB of binary data. It can be stored as an 8-bit PNG weighing in at about 13½KB:

Greyscale optimisations and PNG Crush will reduce this to about 11½KB. However, reading the pixel index values back using JavaScript so that we can reconstruct the hexel data is problematic. One would think you could simply do the following:

  • Encode the greyscale PNG as a data URL,
  • Load it into an offscreen img element,
  • Draw it into an offscreen canvas element, 
  • Capture the pixel data via getImageData() element, then
  • Reconstruct the indices.

The two main stumbling blocks are:

  1. Loading images (even data URLs) is an asynchronous activity.
  2. Colour space management typically applied a "gamma" correction which means the greyscale RGB values are not one-to-one with the original indices.
The first problem can be solved with some truly horrific fudging:

      var done = false;
      img.onload = () => {
        done = true;
      img.src = dataURL;
      while (!done) {
        await new Promise(r => setTimeout(r, 100));

    The second issue cannot be solved without using a third-party JavaScript library (like pngjs) to decode the raw PNG data and extracting the indices directly.

    Another option is to encode the raw pixel data (as opposed to the PNG) into the JavaScript script itself. For example:

      function decode_hex(buffer) {
        const HEXWORLD = /* hexadecimal string */;
        buffer.set(HEXWORLD.match(/../g).map(x => parseInt(x, 16)));

    This function takes a 311,040-element Uint8Array (that's 864 times 360) as an argument and fills it with the indices. Unfortunately, the hexadecimal string is over 600,000 characters long!

    If we limit ourselves to ASCII JavaScript, can we do better?

    No comments:

    Post a Comment