Saturday, 16 October 2021

Voronoi

I've just been baking. But what do you think of when you see the results?

  1. Biscuits
  2. Cookies
  3. Voronoi

Depending on your answer:

  1. You're probably British
  2. You're probably American
  3. You're definitely a geek

Thursday, 7 October 2021

Machin Postage Stamps 3

I've done some investigation into Machin stamp designs that take into account colour vision deficiency (CVD). In particular, I looked at CVD-safe colour schemes and visual representation of numerals for the denominations. See those three web pages for more details.

The numeral scheme I finally came up with (after a lot of experimentation) is as follows:

The symbols have the following properties:

  1. The number of small dots is the value of the digit.
  2. Each has a high-contrast, CVD-safe colour (from the 'Chilliant Pale/Deep 19' palette).
  3. The outlines are distinct.
  4. They all fit within a circle the size of the grey ring zero.

For dark backgrounds, simply switch the pale/deep pairings:

The names I've ascribed to each of the symbols are:

  • 0: grey ring
  • 1: slate circle
  • 2: brown oval
  • 3: purple triangle
  • 4: cyan cross
  • 5: green bowtie
  • 6: pink star
  • 7: blue heptagon
  • 8: orange square
  • 9: mint lozenge

Monday, 4 October 2021

Daltonism

Machin stamps are colour-coded according to their denomination. The coding scheme is somewhat ad hoc. But what if we wanted to be more systematic?

There are surprising few existing systems for encoding numbers as colours. One is the old system used for electronic components:

The digits zero to nine are represented by ten colours. My favourite mnemonic is:

Black Bananas Really Offend Your Girlfriend, But Violets Get Welcomed

In the past, colour-blind people were allegedly discouraged from becoming electricians because of the possibility of confusing earth (green), live (red) and neutral (black) in the pre-1977 UK domestic mains cabling colour scheme.

In an ideal world, the mapping of colours to numeric values would be fairly immune to any colour vision deficiency (CVD) experienced by the viewer. This led me to an investigation into colour-blindness: what used to be called "Daltonism" in the UK.

There are plenty of resources on the web explaining the various forms of colour blindness, but I wanted to be able to objectively assess how "good" a palette of colours was for:

  1. People with no colour deficiency (I'll call them "trichromats"),
  2. People with protanopia, deuteranopia or tritanopia ("dichromats"), and
  3. People with monochrome vision ("monochromats").

One metric of how well a palette of colours "fills" a colour space is to measure the minimum "distance" between any two entries. I chose the CIELAB colour space and the CIEDE2000 metric because they were close-at-hand as part of my previous Goldenrod project.

I defined lambda as the minimum distance (ΔE2000) between colours in the palette as seen by trichromats; also known as minD(normal). The greater this number, the less likely that two entries within the palette will be confused by trichromats.

I then used a corrected version of Color.Vision.Simulate from HCIRN to simulate the four colour deficiencies and produce four "confused" palettes from the original:

  1. protanope
  2. deuteranope
  3. tritanope
  4. achromatope

Each of the confused palettes will have a minimum distance between colours within them; call these minimum distances minD(protanope), etc.

I defined beta as the minimum of minD(protanope), minD(deuteranope) and minD(tritanope). The greater this number, the less likely that two entries within the original palette will be confused by dichromats. I appreciate that there are far fewer sufferers of tritanopia in the general population than of the other two forms of dichromatism, but I haven't scaled the three distance metrics accordingly; I'm running under the premise of "none left behind".

I defined alpha to be minD(achromatope). The greater this number, the less likely that two entries within the original palette will be confused by monochromats.

Finally, I defined omega to be the minimum distance between two adjacent CIELChab hues from the original palette, measured in degrees. This number measures hue separation perceived by trichromats.

In summary:

  • Lambda, λ, is "min ΔE2000" for the original, trichromatic palette. It measures how different the colours in the palette seem to a viewer with no colour vision deficiency. Larger values indicate greater variation.
  • Beta, β, is the least "min ΔE2000" for the dichromatic remappings of the palette. It measures how different the colours in the palette seem to a viewer with one of those colour vision deficiencies. Larger values indicate greater variation.
  • Alpha, α, is "min ΔE2000" for the achromatic remapping of the palette. It measures how different the colours in the palette seem to a viewer with achromatopsia, when rendered in greyscale or in some low-light conditions. Larger values indicate greater variation.
  • Omega, ω, is "min Δhab" for the original, trichromatic palette. It measures the perceived hue separation (measured in degrees) experienced by a viewer with no colour vision deficiency. Larger values indicate greater separation.

Unfortunately, as the number of entries in a palette increases, we expect the λ, β, α and ω scores to decrease, so we cannot easily compare palettes with differing numbers of entries. Perhaps the scores could be scaled depending on the palette size, but I haven't tried to work out the factor; I suspect it's not linear.

In general, for our purposes, a "good" palette is one with high λ, β and/or α scores. Maximising λ is appropriate for about 92% of the population; β for about 8%; and α for less than 0.01%.

The accompanying web page computes the scores for existing and novel palettes.

If we take "Chilliant Pale/Deep 19" from that page and re-order the palette into hue order, we get a colour-blind-friendly scheme with which to encode the digits zero to nine:

This could be the basis for a new colour-coding of stamp denominations...

Friday, 17 September 2021

Unicode Numeral Systems

As part of my investigations into Machin stamps, I started looking at numeral systems for labelling stamp denominations and also clock faces.

Unicode 13.0.0 contains a few General Categories used to classify code points according to numeric value ("Nd", "Nl" & "No") plus Derived Numeric Types ("Decimal", "Digit" & "Numeric"). Trawling through these and the Wikipedia pages on numeral systems, I came up with well over one hundred systems that can be used to represent numbers in Unicode using single glyphs:

Unicode Numerals web page

As it turns out, this experiment became more of an exercise in font management. I initially used Unifont for rendering, but this is quite ugly. The Code2000 fonts are no longer maintained. I ended up using Googles Noto fonts. However, there doesn't seem to be a definitive list of which glyphs exist in which font, so I had to create a tool to interrogate all the Noto web fonts I could find. Even an online list of those web fonts seems lacking, so here's one:

  • Noto Kufi Arabic
  • Noto Naskh Arabic
  • Noto Naskh Arabic UI
  • Noto Nastaliq Urdu
  • Noto Sans
  • Noto Sans JP
  • Noto Sans KR
  • Noto Sans SC
  • Noto Sans TC
  • Noto Sans Adlam
  • Noto Sans Adlam Unjoined
  • Noto Sans Anatolian Hieroglyphs
  • Noto Sans Arabic
  • Noto Sans Arabic UI
  • Noto Sans Armenian
  • Noto Sans Avestan
  • Noto Sans Balinese
  • Noto Sans Bamum
  • Noto Sans Batak
  • Noto Sans Bengali
  • Noto Sans Bengali UI
  • Noto Sans Bhaiksuki
  • Noto Sans Brahmi
  • Noto Sans Buginese
  • Noto Sans Buhid
  • Noto Sans Canadian Aboriginal
  • Noto Sans Carian
  • Noto Sans Chakma
  • Noto Sans Cham
  • Noto Sans Cherokee
  • Noto Sans Coptic
  • Noto Sans Cuneiform
  • Noto Sans Cypriot
  • Noto Sans Deseret
  • Noto Sans Devanagari
  • Noto Sans Devanagari UI
  • Noto Sans Display
  • Noto Sans Egyptian Hieroglyphs
  • Noto Sans Ethiopic
  • Noto Sans Georgian
  • Noto Sans Glagolitic
  • Noto Sans Gothic
  • Noto Sans Gujarati
  • Noto Sans Gujarati UI
  • Noto Sans Gurmukhi
  • Noto Sans Gurmukhi UI
  • Noto Sans Gunjala Gondi
  • Noto Sans Hanifi Rohingya
  • Noto Sans Hanunoo
  • Noto Sans Hebrew
  • Noto Sans Imperial Aramaic
  • Noto Sans Indic Siyaq Numbers
  • Noto Sans Inscriptional Pahlavi
  • Noto Sans Inscriptional Parthian
  • Noto Sans Javanese
  • Noto Sans Kaithi
  • Noto Sans Kannada
  • Noto Sans Kannada UI
  • Noto Sans Kayah Li
  • Noto Sans Kharoshthi
  • Noto Sans Khmer
  • Noto Sans Khmer UI
  • Noto Sans Khudawadi
  • Noto Sans Lao
  • Noto Sans Lao UI
  • Noto Sans Lepcha
  • Noto Sans Limbu
  • Noto Sans Linear B
  • Noto Sans Lisu
  • Noto Sans Lycian
  • Noto Sans Lydian
  • Noto Sans Malayalam
  • Noto Sans Malayalam UI
  • Noto Sans Mandaic
  • Noto Sans Masaram Gondi
  • Noto Sans Mayan Numerals
  • Noto Sans Medefaidrin
  • Noto Sans MeeteiMayek
  • Noto Sans Mende Kikakui
  • Noto Sans Meroitic
  • Noto Sans Modi
  • Noto Sans Mongolian
  • Noto Sans Mono
  • Noto Sans Mro
  • Noto Sans Myanmar
  • Noto Sans Myanmar UI
  • Noto Sans New Tai Lue
  • Noto Sans Newa
  • Noto Sans Ogham
  • Noto Sans Ol Chiki
  • Noto Sans Old Italic
  • Noto Sans Old Persian
  • Noto Sans Old South Arabian
  • Noto Sans Old Turkic
  • Noto Sans Oriya
  • Noto Sans Oriya UI
  • Noto Sans Osage
  • Noto Sans Osmanya
  • Noto Sans Pahawh Hmong
  • Noto Sans Phags Pa
  • Noto Sans Phoenician
  • Noto Sans Rejang
  • Noto Sans Runic
  • Noto Sans Samaritan
  • Noto Sans Saurashtra
  • Noto Sans Sharada
  • Noto Sans Shavian
  • Noto Sans Sinhala
  • Noto Sans Sinhala UI
  • Noto Sans Sundanese
  • Noto Sans Syloti Nagri
  • Noto Sans Symbols
  • Noto Sans Symbols 2
  • Noto Sans Syriac
  • Noto Sans Tagalog
  • Noto Sans Tagbanwa
  • Noto Sans Tai Le
  • Noto Sans Tai Tham
  • Noto Sans Tai Viet
  • Noto Sans Takri
  • Noto Sans Tamil
  • Noto Sans Tamil UI
  • Noto Sans Telugu
  • Noto Sans Telugu UI
  • Noto Sans Thaana
  • Noto Sans Thai
  • Noto Sans Thai UI
  • Noto Sans Tifinagh
  • Noto Sans Tirhuta
  • Noto Sans Ugaritic
  • Noto Sans Vai
  • Noto Sans Wancho
  • Noto Sans Warang Citi
  • Noto Sans Yi
  • Noto Serif
  • Noto Serif JP
  • Noto Serif KR
  • Noto Serif SC
  • Noto Serif TC
  • Noto Serif Ahom
  • Noto Serif Armenian
  • Noto Serif Bengali
  • Noto Serif Devanagari
  • Noto Serif Display
  • Noto Serif Ethiopic
  • Noto Serif Georgian
  • Noto Serif Gujarati
  • Noto Serif Hebrew
  • Noto Serif Kannada
  • Noto Serif Khmer
  • Noto Serif Lao
  • Noto Serif Malayalam
  • Noto Serif Myanmar
  • Noto Serif Nyiakeng Puachue Hmong
  • Noto Serif Sinhala
  • Noto Serif Tamil
  • Noto Serif Telugu
  • Noto Serif Thai
  • Noto Serif Tibetan

The Font Check tool allows you to type in a hexadecimal Unicode code point and see the fonts that can render it. It uses the trick of measuring the glyph using an offscreen canvas with a fallback font of Adobe Blank. If the glyph is zero pixels wide, it means it was rendered using the fallback (or is naturally zero-width, so isn't of interest to us anyway).

Using the tool, I worked out the minimal set of Noto fonts needed to render the numerals table. However, there were a few issues:

  1. Some fonts only come in serif variants, not sans serif (and vice versa).

  2. I couldn't find any Noto web fonts that could render the following (though the asterisked scripts were rendered correctly using a fallback font by my browser):

    • Dives Akuru
    • Khmer Divination*
    • Myanmar Tai Laing*
    • Nko*
    • Ottoman Siyaq
    • Sinhala*
    • Sora Sompeng*
    • Tag

  3. Arabic Persian and Arabic Urdu use exactly the same codepoints but use different languages in the HTML mark-up to select different sets of glyphs.

Further notes:

  • I couldn't find any information on how Ogham represented numbers, so I made something up based on tally marks.
  • The Tibetan script has a parallel set of numerals for half values; they're used in stamps, which is a nice coincidence.
  • The Runic Golden Numbers are used in Younger Futhark calendars.
  • Other scripts do have numerals or counting systems, but I excluded many because they are "low radix" (e.g. native Korean)

Monday, 13 September 2021

Machin Postage Stamps 2

Well, I thought I was being clever, didn't I? Using WebP images for Machin stamps. Alas, WebP has only recently been supported by Apple's Safari, so the web page didn't work at all on an older model iPad.

My original HTML was simply:

<img id="picture" class="shadow" src="machin.webp" />

And the source image path was accessed from JavaScript via:

document.getElementById("picture").src

The fix from Brett DeWoody is actually quite elegant. Change the HTML to use picture source sets:

<picture class="shadow">
    <source srcset="machin.webp" type="image/webp" />
    <img id="picture" src="machin.png" />
</picture>

And use the following to interrogate the chosen path after load:

document.getElementById("picture").currentSrc

Wednesday, 25 August 2021

Machin Postage Stamps 1

There is a web page to accompany these blog posts.

I was sorting through some old family papers when I came across my father's stamp collection. He had a number of unsorted UK stamps that I (being me) decided to organise.

Arnold Machin

The Machin series of UK postage stamps are named after the sculptor (Arnold Machin, 1911-1999) who designed them. His profile of Queen Elizabeth II has been used on many coins and stamps:

Pre-Decimal Machin Stamps

The original, pre-decimal stamps to use the Machin profile (from 1967 onwards) came in denominations of:

½d, 1d, 2d, 3d, 4d, 5d, 6d, 7d, 8d, 9d, 10d, 1/-, …

Each denomination had a different colour, although I'm fairly certain there was no systematic colour scheme.

I found all of the denominations, up to and including the shilling, in my father's collection, so I thought about how to mount them in a display. These stamps aren't very valuable, so permanently sticking them to a bit of card isn't so terrible.

My first thought was just a grid:

Or perhaps a circular layout:


At this point, I was reminded of a clock face. Alas, there was never a "11d" Machin stamp, but one could swap the half penny and shilling stamps and use "½d" for eleven o'clock and "1/-" for twelve o'clock:

Here's a mock-up of a clock built around this layout:

I also built a JavaScript demo loosely based on a beautiful CSS-only clock by Nils Rasmusson.

Decimal Machin Stamps

Next I moved on to the Machin stamps used after decimalisation in 1971. These had all the half-penny increments up to and including 13½p, so two rings could be constructed:

Or, with axis-aligned stamps:

These templates are available on the web page as SVGs with absolute measurements: each stamp is 21mm by 24mm. You can print out the desired page at 100% scale and use the templates when mounting the stamps. Unfortunately the "double ring" layouts don't quite fit on a single sheet of A4 so you'll need to crop and rely on symmetry to physically flip the template.

Mounting

I decided to mount 23 stamps (my father never acquired a "11½p" Machin) using the last template within a 240mm-by-300mm frame:

  1. Print out the template at 100% scale.
  2. Carefully cut along three sides of each stamp "window" with a scalpel.
  3. Position the template over the mounting card.
  4. Secure the template to the mounting card with masking tape. Try to avoid sticking the tape directly to the front of the card. (Figure 1)
  5. Stick the appropriate stamps on to the card using double-sided tape through the windows. (Figure 2)
  6. Carefully remove the template and insert the mounting card into the frame. (Figure 3)
Figure 1

Figure 2

Figure 3

There's a conspicuous gap where the missing stamp should go. I could fill it by splurging a couple of quid on ebay, but the gap itself has a story.

Another project would be to affix a cheap battery quartz movement to a similar clock face. I had hoped to use an old CD for the circular face, but I don't think twelve stamps quite fit.

Tuesday, 24 August 2021

Gratuitous Aphorism #8

Experts are just people who have already made all the silly mistakes.

Tuesday, 10 August 2021

Hexworld 3: Compression

Last time, we encoded our 311,040 world map hexel indices as a 622,080-character hexadecimal string as a way of embedding the data in an ASCII JavaScript file. We could use the following script:

hexworld(buffer => {
  const DATA = "00000000...ffffffff";
  buffer.set(DATA.match(/../g).map(x => parseInt(x, 16)));
});

This calls a function hexworld(), defined elsewhere, that takes as its only parameter a decompression function that fills in a pre-allocated Uint8Array buffer with the hexel indices. The "decompression" above consists of splitting the long hexadecimal string into 2-character segments and converting these to numeric values.

We can achieve better compression by using the built-in atob() function:

hexworld(buffer => {
  const DATA = "AAAAAAAA...////////";
  buffer.set(Array.from(atob(DATA), x => x.charCodeAt()));
});

The DATA string is reduced in size from 622,080 ASCII characters to 414,720.

If we look at the map itself, we see that it's ripe for run-length encoding or the like.

One possibility is the LZW compression algorithm. I tried this and came up with data plus decompressor what weigh in at under 32KB of ASCII:

function lzw(src) {
  var dic = Array.from({ length: 256 }, (_, i) => [i]);
  var key, dst = [];
  for (var val of src) {
    var nxt = dic[val] || [...key, key[0]];
    if (key) {
      dic.push([...key, nxt[0]]);
    }
    key = nxt;
    dst.push(...key);
  }
  return dst;
}
hexworld(buffer => {
  const DATA = "00007407...cb8cc80f";
  buffer.set(lzw(DATA.match(/.../g).map(x => parseInt(x, 36))));
});

The lzw() function above decompresses an array of integers into an array of bytes. For completeness, here's the corresponding compression function:

function compress(src) {
  const chr = String.fromCharCode;
  var dic = {};
  var idx = 0;
  do {
    dic[chr(idx)] = idx;
  } while (++idx < 256);
  var dst = [];
  var pre = "";
  for (var val of src) {
    var key = pre + chr(val);
    if (key in dic) {
      pre = key;
    } else {
      dst.push(dic[pre]);
      dic[key] = idx++;
      pre = chr(val);
    }
  }
  dst.push(dic[pre]);
  return dst;
}

In our Hexworld case, the LZW dictionary constructed during compression has about 10,000 entries, so those indices can be stored as three base-36 digits. This is a general-purpose compression scheme, so could a more tailored algorithm produce better results? Remember, we're aiming for something less than 16KB.

One solution I looked at was encoding the data as a sequence of 4-bit nybbles:

hexworld(buffer => {
  const DATA = "8bxQEhER...ER8NEw==";
  var input = Array.from(atob(DATA),
    x => x.charCodeAt().toString(16).padStart(2, "0")).join("");
  var i = 0;
  function read(n) {
    i += n;
    return parseInt(input.slice(i - n, i), 16);
  }
  var land = 0;
  var next = 0;
  var o = 0;
  while (o < buffer.length) {
    var n = read(1);
    switch (n) {
      case 0:
        land = next = read(2);
        continue;
      case 12:
        n = read(1) + 12;
        break;
      case 13:
        n = read(2) + 28;
        break;
      case 14:
        n = read(3) + 284;
        break;
      case 15:
        n = read(4) + 4380;
        break;
    }
    buffer.fill(next, o, o + n);
    o += n;
    next = next ? 0 : land;
  }
});

The algorithm is as follows:

  1. Read the next nybble in the stream
  2. If the nybble is 0, the next two nybbles contain the next "land" hexel index and go back to Step 1
  3. If the nybble is between 1 and 11 inclusive, it is used as the count below
  4. If the nybble is 12, the count to use is the value in the next nybble plus 12
  5. If the nybble is 13, the count to use is the value in the next two nybbles plus 28
  6. If the nybble is 14, the count to use is the value in the next three nybbles plus 284
  7. If the nybble is 15, the count to use is the value in the next four nybbles plus 4380
  8. If the last set of values output were "land", write out "count" indices of "water" (0)
  9. Otherwise, write out "count" indices of the current "land"
  10. Go back to Step 1
This algorithm assumes there are never more than about 70,000 value repetitions, which is more than enough for our map. It weighs in at under 19KB of ASCII text, which is getting very close to our goal. One thing to notice is that the nybbles are encoded into ASCII via atob/btoa which use base-64 and are therefore relatively inefficient.

My final attempt (the one that achieved my goal of data plus decompressor within 16KB of ASCII) uses base-95:

hexworld(buffer => {
  const DATA = ')  )gD}l...$)wA#).n';
  for (var d = Array.from(DATA, x => 126 - x.charCodeAt()),
    i = 0, o = 0, a = 0, b = 0, c = 0; DATA[i]; c = c ? 0 : a) {
    var v = d[i++];
    if (v >= 90) {
      v -= 88;
      while (--v) {
        d[--i] = 0;
      }
    }
    var n = ~~(v / 5);
    if (v %= 5) {
      c = v---4 ? v * 95 + d[i++] : b;
      b = a;
      a = c; 
    }
    switch (n++) {
      case 17:
        n = d[i++] * 95 + 112;
      case 16:
        n += d[i++] - 1;
        break;
    }
    buffer.fill(c, o, o + n);
    o += n;
  }
});

This is slightly golfed, so I'll clarify it below.

[In a perverse way, I quite like the expression "v---4" which is shorthand for "(v--) !== 4"]

The data is encoded in base-95: the number of printable ASCII characters. This means that backslashes and quotes need to be escaped within the string. The choices of (a) using single quotes (as opposed to double- or backquotes) and (b) storing values in reverse order ("126 - x" versus "x - 32") minimize the number of escape backslashes needed for our particular map data.

Lead base-95 values are split into two values: low (0 to 4 inclusive) and high (0 to 18 inclusive). The high value holds the repetition count:

  • High values from 0 to 15 inclusive imply repetition counts from 1 to 16,
  • Sixteen takes the next base-95 value to produce a repetition counts from 17 to 111,
  • Seventeen takes the next two base-95 values to produce a repetition counts from 112 to 9136,
  • Eighteen is a special "repeated zero" value that simulates a lead zero repeated 2 to 6 times based on the low value.
The low value (except for when the high value is eighteen) encodes what should be repeated:
  • Zero alternates between the last land index and the water index (0),
  • One sets the land index to the next base-95 value,
  • Two sets the land index to the next base-95 value plus 95,
  • Three sets the land index to the next base-95 value plus 190,
  • Four alternates between the last land index and the previous land index specified.

This rather ad hoc algorithm is based on the following observations:

  1. Water (hexel index zero) is common throughout the whole map,
  2. If the last land hexel was a particular index, it is likely that the next land will be the same index,
  3. Due to the staggered hexel layout, land indices quite often alternate with either water or the previous land index

I'm sure there's plenty more mileage using bespoke compression, but after a certain point it becomes unhealthy. For example, one option I didn't pursue is re-ordering the country-to-hexel index allocation in order to improve compression. This smells suspiciously like super-optimisation.

One last avenue to explore for compressing the hexel map is the recent JavaScript Compression Stream API, but that's for another time.

        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 = () => {
              process(img);
              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?

          Friday, 30 July 2021

          Hexworld 1: World Map

          I've long been fascinated by hexagonal grids. And maps. So it was probably only a matter of time before I created a world map based on a hexagonal grid: Hexworld.

          I'm certainly not the first to try this, but most of the other attempts I've seen are either low resolution or do not try to capture national boundaries.

          The obvious way of generating such a map is to take an existing one (in this case, an equirectangular projection) and post-process it. I started down this track but quickly discovered that it produces ugly results. As cartographers through the ages have discovered, making political maps (as opposed to maps for navigation) is more of an art-form that a science. So I dusted off my faithful copy of Paint Shop Pro 5 and hand-filled the 100,000 or so hexagons that make up the land masses:


          I initially coloured the regions using five colours (red, green, blue, yellow and pink) and then used a Wikimedia four colour map as the basis for whittling it down to four.

          With the seas coloured cyan (hue 180°), it made sense to use equidistant hues for the remaining four colours: red (324°), green (108°), blue (252°) and yellow (36°).

          Each "hexel" is given one of 241 unique 8-bit indices:

          • One index (0) is reserved for water,
          • 193 indices cover the current full UN member states,
          • 2 indices cover the UN observer states (Vatican and Palestine),
          • 6 indices cover disputed territories (Western Sahara, Taiwan, Abkhazia, Crimea, Kosovo and South Ossetia),
          • 28 indices cover overseas territories belonging to UN states (e.g. Falklands), and
          • One index (255) for Antarctica.

          The lowest two bits of the index encodes the region's colour; except for 0 and 255, which are treated specially. The world is indeed four-colourable!

          Obviously, there's no real advantage in rendering a world map using hexagons instead of rectangular pixels or arbitrary polygons. But it's a fun exercise.


          Wednesday, 5 May 2021

          Colour Pick 3: WebGL

          If you look in the bottom-left corner of the Colour Pick experiment web page, you'll see a checkbox:

          Colour Pick WebGL checkbox

          This is an optimization. It turns out that updating even a few canvases interactively whilst the user is dragging the mouse can produce very "laggy" responses. The problem is that some of the canvases draw non-trivial shapes with each pixel's colour computed on-the-fly. For example:

          CIE L*a*b* gamut canvas

          The canonical way of updating a single pixel in a canvas via JavaScript is something akin to this:

          var context = canvas.getContext("2d");
          context.fillStyle = rgb;
          context.fillRect(x, y, 1, 1);

          This is fine for a few pixels but doesn't scale well. WebGL fragment shaders, on the other hand, are ideal for updating rectangular regions like this on a pixel-by-pixel basis. However, you cannot use both "2d" and "webgl" contexts on the same canvas. Consequently, the JS script, uses an off-screen "webgl" canvas to utilise fragment shaders and then "blits" the result to the on-screen canvas.

          WebGL fragment shaders are only guaranteed to conform to OpenGL Shader Language (GLSL) version 1.00, so you have to be a bit conservative with your shader programming sometimes.

          The "FRAGMENT_SHADER_SOURCE" variable at the top of the colourpick.js script contains the interesting GLSL code to perform the various colour space conversions.

          The first function converts an HSL/HSV-style hue in the range zero to one into a fully-saturated RGB triple:

          vec3 hue2rgb(float h) { float r = abs(h * 6.0 - 3.0) - 1.0; float g = 2.0 - abs(h * 6.0 - 2.0); float b = 2.0 - abs(h * 6.0 - 4.0); return clamp(vec3(r, g, b), 0.0, 1.0); }

          The next function converts a red/green/blue triple to hue/chroma/value based on the work by Sam Hocevar and Emil Persson:

          vec3 rgb2hcv(vec3 rgb) { vec4 p = (rgb.g < rgb.b) ? vec4(rgb.bg,-1.0,2.0/3.0) : vec4(rgb.gb,0.0,-1.0/3.0); vec4 q = (rgb.r < p.x) ? vec4(p.xyw, rgb.r) : vec4(rgb.r, p.yzx); float c = q.x - min(q.w, q.y); float h = abs((q.w - q.y) / (6.0 * c + 1.0e-10) + q.z); return vec3(h, c, q.x); }

          Next we have our first true colour space conversion: HSV to RGB:

          vec3 hsv2rgb(vec3 hsv) { vec3 rgb = hue2rgb(hsv.x); return ((rgb - 1.0) * hsv.y + 1.0) * hsv.z; }

          HSL to RGB is similar:

          vec3 hsl2rgb(vec3 hsl) { vec3 rgb = hue2rgb(hsl.x); return (rgb - 0.5) * (1.0 - abs(2.0 * hsl.z - 1.0)) * hsl.y + hsl.z; }

          HWB (hue/whiteness/blackness) to RGB is also similar:

          vec3 hwb2rgb(vec3 hwb) { vec3 rgb = hue2rgb(hwb.x); return rgb * (1.0 - hwb.y - hwb.z) + hwb.y; }

          Converting "linear" RGB to sRGB requires component-wise conditionals. I'd love to be able to use 'vec3 mix(vec3, vec3, bvec3)' here, but basic WebGL doesn't support it, so we have to make sure that both "branches" of the conditional are finite:

          vec3 lrgb2srgb(vec3 rgb) { vec3 linear = rgb * 12.92; vec3 nonlinear = 1.055 * pow(max(rgb, 0.0), vec3(1.0 / 2.4)) - 0.055; return mix(nonlinear, linear, vec3(lessThan(linear, vec3(0.04045)))); }

          Converting CIE XYZ to linear RGB is a simple matrix multiplication, but basic WebGL doesn't support constant matrix initialization:

          vec3 xyz2lrgb(vec3 xyz) { return vec3(dot(xyz, vec3(+3.2404542, -1.5371385, -0.4985314)), dot(xyz, vec3(-0.9692660, +1.8760108, +0.0415560)), dot(xyz, vec3(+0.0556434, -0.2040259, +1.0572252))); }

          We have to avoid division by zero when converting CIE xyY to CIE XYZ:

          vec3 xyy2xyz(vec3 xyy) { float d = xyy.z / max(xyy.y, 1.0e-10); return vec3(xyy.x * d, xyy.z, (1.0 - xyy.x - xyy.y) * d); }

          We assume a D65 illuminant and a 2-degree standard colormetric observer for converting CIE L*a*b* to CIE XYZ. Again, it would be nice to use 'vec3 mix(vec3, vec3, bvec3)' in the final statement, but basic WebGL doesn't support it:

          vec3 lab2xyz(vec3 lab) { float fy = (lab.x + 16.0) / 116.0; float fx = fy + lab.y * 0.002; float fz = fy - lab.z * 0.005; vec3 fxyz = vec3(fx, fy, fz); vec3 linear = (fxyz * 3132.0 - 432.0) / 24389.0; vec3 nonlinear = fxyz * fxyz * fxyz; return mix(nonlinear, linear, vec3(lessThan(nonlinear, vec3(216.0 / 24389.0)))) * vec3(0.95047,1.0,1.08883); }

          Converting CIE LCHab to CIE L*a*b* is a simple polar-to-cartesian conversion:

          vec3 lchab2lab(vec3 lch) { float hab = radians(lch.z); return vec3(lch.x, lch.y * cos(hab), lch.y * sin(hab)); }

          As is converting CIE LCHuv to CIE LUV (it's actually identical, but we'll keep it distinct):

          vec3 lchuv2luv(vec3 lch) { float huv = radians(lch.z); return vec3(lch.x, lch.y * cos(huv), lch.y * sin(huv)); }

          Converting CIE LUV to CIE XYZ is relatively straightforward (assuming D65 and 2-degrees observer):

          vec3 luv2xyz(vec3 luv) { float p = luv.x * 52.0 / (luv.y + luv.x * 2.571917722678301); float q = luv.x * 39.0 / (luv.z + luv.x * 6.088371938121326); float y = (luv.x > 8.0) ? pow((luv.x + 16.0)/116.0, 3.0) : (luv.x * 27.0/24389.0); float x = y * q * 3.0 / p; float z = x * (p - 1.0) / 3.0 - y * 5.0; return vec3(x, y, z); }

          As noted in the previous post, "hue" in CIE LChab is not the same as "hue" in HSL/HSV/HWB. The following function provides a very close approximation of the latter when given the former without having to perform the complete LChab→LabXYZRGBsRGBHSL calculation chain. Note that both hues are in the range [0..1]:

          float hab2hue(float hab) { const vec4 a = vec4(0.011681489, -0.053365543, -0.00331377, -0.009398634); const vec4 b = vec4(-0.527823872, -1.606202694, 1.054897946, 0.102411421); const float c = 0.91047088; vec4 v = sin(vec4(1.0, 2.0, 3.0, 4.0) * hab * 6.28318530718 + b); return fract(hab + dot(a, v) + c); }

          The CIE LChuv hue to HSL/HSV/HWB hue conversion needs a few more terms in the expansion to be accurate. Again, both hues are in the range [0..1]:

          float huv2hue(float huv) { const vec4 a03 = vec4(0.003125388, -0.033086796, 0.007706313, 0.004791156); const vec4 a47 = vec4(0.001477559, 0.001918466, -0.000558722, 0.001325); const vec4 b03 = vec4(0.006280978, -0.762775675, -0.798412429, -1.390621155); const vec4 b47 = vec4(1.509991161, -1.395042259, 1.09539382, 0.731749525); const float c = 0.957463921; vec4 v03 = sin(vec4(1.0, 2.0, 3.0, 4.0) * huv * 6.28318530718 + b03); vec4 v47 = sin(vec4(5.0, 6.0, 7.0, 8.0) * huv * 6.28318530718 + b47); return fract(huv + dot(a03, v03) + c); }

          The functions above give you the building blocks necessary to convert most colour space points into RGB. The reverse transformations are left as an exercise for the reader. [Hint: I've previously written some of them in HLSL]

          On my PC, using offscreen WebGL techniques speeds up the rendering of all the colour picker canvases by a factor of about twelve which means the response times go from "annoyingly sluggish" to "unnoticeably fast".


          Thursday, 1 April 2021

          Colour Pick 2: History

          Pick a colour, any colour. Now tell me what colour you've chosen.

          It's surprisingly difficult to:

          1. Design a user interface to allow someone to intuitively pick a colour, and
          2. Come up with a description of colours that is meaningful to both humans and computers.
          If we ask a browser to ask the user to pick a colour, we get a control like this:

          Depending on your browser, you may get a dialog like this:

          Colour input dialog from Google Chrome 89 (2021)

          Chrome allows you to switch between RGB, HSL and hexadecimal input schemes. The "result" of the dialog is an HTML hexadecimal colour in the format "#rrggbb", e.g. "#DAA520".

          Is that the state of the art? The Colour Pick experiment is a personal investigation into user interfaces like this.

          A Bit of History

          One of the first graphical editing programs was Sketchpad (1963) but this was designed for a monochrome vector display.

          According to Alvy Ray SmithWilliam Kubitz (1968) and Joan Miller (1969) both created hardware with eight fixed colours, presumably black, blue, red, magenta, green, cyan, yellow and white.

          Until SuperPaint (1973) appeared with its 256-entry, 24-bit RGB palette, the number of colours available meant that a "palette box" was sufficient for most software. SuperPaint introduced the ability to specify a colour via HSB (HSV) sliders:

          SuperPaint tool palette (1973)

          The first PC graphics software used palette boxes because of the colour limitations of early graphics cards such as CGA/EGA:

          PCPaint+ (1985)

          Meanwhile, Apple software was at a similar stage with MacPaint.

          With the advent of 24-bit colour hardware on home and office computers, techniques for picking colours "caught up" with professional image- and video-editing systems. Mechanisms that used the RGB, HSV and HSL colour spaces became the norm:

          Paint Shop Pro 5 (1998)

          The CIE L*a*b* and related colour spaces started to appear in pickers too:

          Adobe Photoshop (after 1990)

          Although Microsoft remains stubbornly old-school:

          Microsoft Paint for Windows 10 (2020)

          Apple's current colour picker has multiple tabs; the first is an HSV circle picker:

          Apple Color Picker (2020)

          An interesting variation on the HSV circle is the discrete hexagonal palette popular on Windows:

          Mechanika Design (2015)

          GIMP has a number of built-in colour pickers. The "triangle wheel" is described in the documentation as based on HSV, but it looks more like HWB to me:

          GIMP 2.10.22 (2020)

          I've tried to systematically encapsulate as many of these ideas as possible in the Colour Pick web page.

          Monday, 29 March 2021

          Intern Ethel P

          I've just received a very suspicious email from "internethelp@<[-redacted-].com". It purports to be assistance for a query I had with an online service. But I know better! It's just another blatant phishing scam by that master cyber-criminal intern Ethel P.

          Friday, 26 March 2021

          Colour Pick 1: Goldenrod

          As part of my investigation into colour names, I've been writing a lot of JavaScript code to perform colour space transformations.

          When I started the colour pick work, I did the "right thing" and refactored all the colour-related functionality into a library. The library is named "goldenrod" after the X11 colour of that name (#DAA520).

          Solidago virgaurea minuta "Dwarf Goldenrod"

          The library is just a plain old bunch of functions and lists organised into three files:

          goldenrod.js

          This script contains the main entry points exposed as fields of the global object "chilliant.goldenrod". They are primarily conversion functions with signatures of the following form:

          float[3] chilliant.goldenrod.{source}_to_{destination}(float value[3])

          Above, "{source}" and "{destination}" are colour space tags as listed in the following table:

          TagColour Spacevalue[0]value[1]value[2]
          sRGBsRGBred
          0.0 to 1.0
          green
          0.0 to 1.0
          blue
          0.0 to 1.0
          HSVHSV
          a.k.a. HSB
          hue
          0.0 to 360.0 degrees
          saturation
          0.0 to 1.0
          value
          0.0 to 1.0
          HSLHSLhue
          0.0 to 360.0 degrees
          saturation
          0.0 to 1.0
          lightness
          0.0 to 1.0
          HWBHWBhue
          0.0 to 360.0 degrees
          whiteness
          0.0 to 1.0
          blackness
          0.0 to 1.0
          CIEXYZCIE 1931 XYZX
          0.0 to 1.0
          Y luminance
          0.0 to 1.0
          Z
          0.0 to 1.0
          CIExyYCIE 1931 xyYx
          0.0 to 100.0
          y
          0.0 to 100.0
          Y luminance
          0.0 to 100.0
          CIELabCIE 1976 L*a*b*L* lightness
          0.0 to 100.0
          a*
          ~ -100.0 to 100.0
          b*
          ~ -100.0 to 100.0
          CIELuvCIE 1976 L*u*v*L* lightness
          0.0 to 100.0
          u*
          ~ -200.0 to 200.0
          v*
          ~ -200.0 to 200.0
          CIELChabCIE 1976 L*C*habL* lightness
          0.0 to 100.0
          C* chroma
          ~ 0.0 to 100.0
          hab hue
          0.0 to 360.0 degrees
          CIELChuvCIE 1976 L*C*huvL* lightness
          0.0 to 100.0
          C* chroma
          ~ 0.0 to 100.0
          huv hue
          0.0 to 360.0 degrees

          Note that for conversions to and from the hexacone colour spaces (HSV/HSL/HWB) the tag "RGB" is used instead of "sRGB" to emphasize the fact that no linearization or delinearization is performed. Thus, the name "chilliant.goldenrod.RGB_to_HSL" is used instead of "chilliant.goldenrod.sRGB_to_HSL".

          All conversions assume a standard D65 illuminant with a two-degree observer angle.

          At the time of writing, not all permutations of source and destination are directly supported, but it's a trivial change to add the missing "chains".

          The additional members of "chilliant.goldenrod" are listed below.

          bool chilliant.goldenrod.within_sRGB(any srgb)

          A function that returns true if, and only if, 'srgb' is an array of at least three numbers all between 0.0 and 1.0 inclusive.

          float[3] chilliant.goldenrod.force_sRGB(any srgb)

          Forces 'srgb' to be a valid sRGB triplet with each element between 0.0 and 1.0. If any channel is greater than one, the whole triplet is scaled back. If 'srgb' cannot be interpretted as a valid triplet, black [0,0,0] is returned.

          float chilliant.goldenrod.deltaE1994(float ref[3], float lab[3])

          Computes the CIELAB ΔE*(1994) colour difference between the two CIEL*a*b* colours. Note that this metric is not symmetric.

          float chilliant.goldenrod.deltaE2000(float ref[3], float lab[3])

          Computes the CIELAB ΔE*(2000) colour difference between the two CIEL*a*b* colours.

          string chilliant.goldenrod.sRGB_to_HEX(float srgb[])

          Converts the sRGB triplet (or quartet with alpha) 'srgb' to a hexadecimal colour in the form "#rrggbb" or "#rrggbbaa".

          float[3] chilliant.goldenrod.HEX_to_sRGB(string hex)

          Converts an hexadecimal colour string in the form "#rgb", "#rgba", "#rrggbb" or "#rrggbbaa" into an sRGB triplet. Any alpha component is ignored.

          string chilliant.goldenrod.version

          A string field containing the goldenrod library version in semver format, e.g. "1.0.0".

          util.js

          This script contains the guts of the functionality of goldenrod exposed as members of the object "chilliant.goldenrod.util". A large portion is based on the formulae outlined on Bruce Lindbloom's excellent site and should be fairly self-explanatory.

          data.js

          This script contains useful datasets exposed as members of the object "chilliant.goldenrod.data".

          float[][] chilliant.goldenrod.data.cie1931_xyy

          The CIE 1931 2-degree chromaticity xyY coordinates from UCL Colour & Vision Research Laboratory. The columns are:

          1. Wavelength in nm,
          2. CIE xyY x-ordinate,
          3. CIE xyY y-ordinate, and
          4. CIE xyY Y luminance.

          object[] chilliant.goldenrod.data.css4

          The CSS 4 colour names taken from Mozilla.

          any[] chilliant.goldenrod.data.centore

          The 267 ISCC-NBS centroids from Paul Centore's site in index order. The columns are:

          1. Name (string),
          2. Munsell colour (string),
          3. Red (int),
          4. Green (int),
          5. Blue (int),
          6. sRGB hexadecimal (string).

          Note that seven of the centroids are outside the sRGB gamut so are missing the final four columns. It would be nice to have the out-of-gamut values for red/green/blue because those points could still be matched via (for instance) Euclidean distance searchers. However, I have been unable to recreate Paul's methodology to fill in the blanks.

          object chilliant.goldenrod.data.cns

          A 627-entry map between the Color Naming System and sRGB values in the form:

          {
            "black": {hex: "#000000", srgb: [0,0,0]},
            ...
            "yellowish orange": {hex: "#CC9333", srgb: [0.8,0.575,0.2]}
          }

          This is a "best guess" at the values based on "A New Color-Naming System for Graphics Languages".

          See "generateCNS()" for details.

          any[] chilliant.goldenrod.data.iscc

          The 260 ISCC-NBS centroids from "data.centore" that are within the sRGB gamut, formatted like "data.cns".

          See "generateISCC()" for details.

          Unit Tests

          The goldenrod library has a minimal unit test suite. The values were checked against Bruce Lindbloom's colour calculator, amongst other tools.

          Tuesday, 23 March 2021

          HSL and CIELCh(ab) Hue Wheels

          I've been looking at colour pickers recently and came across (what I think is) an interesting cul-de-sac.

          HSL colour pickers often have a hue wheel similar to this one from Paint Shop Pro 5 (1998):

          Paint Shop Pro 5 colour picker dialog

          The wheel is rendered as an angle sweeps from 0 to 360 degrees clockwise:

          output = HSL_to_RGB(angle, 100%, 50%)

          The HSL wheel looks like this:

          HSL hue wheel

          Note that the "hue" components of HSL, HSV and HWB are identical. However, this is not the case for the cylindrical version of CIE Lab, a.k.a. CIELCh(ab).

          We can construct the CIELCh(ab) hue wheel as follows:

          rgb = CIELChab_to_sRGB(70, 30, angle)
          hsl = RGB_to_HSL(rgb)
          output = HSL_to_RGB(hsl.h, 100%, 50%)

          We choose a CIE perceptual lightness (L*) of 70 and chroma (C*) of 30 so as not to go out-of-gamut when converting to sRGB. We also implicitly assume the reference white to be the CIE standard illuminant D65 and a 2-degree observer angle.

          We use HSL to fully saturate the output colour to produce the following wheel:

          CIELCh(ab) hue wheel

          Note that the colours have shifted somewhat. This becomes clear if we plot both wheels on the same chart:

          HSL (outer) and CIELCh(ab) (inner) hue wheels

          There's already an excellent comparison of these two wheels by Elle Stone, so I won't go into that here.

          For reasons that will hopefully become clear in a later post, what I really wanted to do was plot the CIELCh(ab) hue wheel efficiently in WebGL. Who wouldn't?

          I coded "CIELChab_to_sRGB" et al in GLSL (more on that later too) but felt a bit queasy performing such complicated arithmetic, particularly when the results get shunted through an RGB-HSL-RGB hack at the end. There must be an efficient way of translating CIELCh(ab) hue to HSL hue, surely?

          If we plot the relationship, we get:

          CIELCh(ab) hue versus HSL hue

          I looks like a fairly linear albeit wavy relationship, doesn't it? So let's plot the difference modulo 360 degrees:

          Difference in hues modulo 360 degrees

          It now looks fairly sinusoidal, so maybe we can approximate it with the sum of a few sine waves.

          If the input is the CIELCh(ab) hue angle in degrees and the desired output is the HSL hue, we can write:

          output = input + c + a0*sin(b0+input) + a1*sin(b1+2*input) + a2*sin(b2+3*input) + …

          It turns out you only need four sine terms to get a good approximation and Microsoft Excel Solver is more that capable of finding appropriate values for the offset "c", scale factors "a0" to "a3" and phase shifts "b0" to "b3".

          I initially coded the formula as a GLSL function that takes the hues in the range zero to one, like so:

          float hstar2hue(float input) {
            // Convert CLIELCh(ab) hue [0..1] to HSV/HSL/HWB hue [0..1]
            float a0 = 0.011681489;
            float a1 = -0.053365543;
            float a2 = -0.00331377;
            float a3 = -0.009398634;
            float b0 = -0.527823872;
            float b1 = -1.606202694;
            float b2 = 1.054897946;
            float b3 = 0.102411421;
            float c = 0.91047088;
            float output = input + c;
            output += a0 * sin(b0 + input * 2.0 * 3.1415926536);
            output += a1 * sin(b1 + input * 4.0 * 3.1415926536);
            output += a2 * sin(b2 + input * 6.0 * 3.1415926536);
            output += a3 * sin(b3 + input * 8.0 * 3.1415926536);
            return mod(output, 1.0);
          }

          But we can take advantage of GLSL's SIMD nature and get:

          float hstar2hue(float input) {
            // Convert CLIELCh(ab) hue [0..1] to HSV/HSL/HWB hue [0..1]
            vec4 a = vec4(0.011681489, -0.053365543, -0.00331377, -0.009398634);
            vec4 b = vec4(-0.527823872, -1.606202694, 1.054897946, 0.102411421);
            float c = 0.91047088;
            vec4 v = sin(vec4(1.0, 2.0, 3.0, 4.0) * input * 6.28318530718 + b);
            return mod(input + dot(a, v) + c, 1.0);
          }

          It's then just a question of converting the HSL-style hue to RGB:

          vec3 hue2rgb(float h) {
            float r = abs(h * 6.0 - 3.0) - 1.0;
            float g = 2.0 - abs(h * 6.0 - 2.0);
            float b = 2.0 - abs(h * 6.0 - 4.0);
            return clamp(vec3(r, g, b), 0.0, 1.0);
          }

          And we've got all the ingredients for a fragment shader to render the CIELCh(ab) hue wheel.

          I'll leave it as an exercise for the reader to work out the reverse HSL-to-CIELCh(ab) hue approximation as well as those for CIELCh(uv).