conductor

CI task system
git clone git://git.finwo.net/app/conductor
Log | Files | Refs | README | LICENSE

icon.test.js (6822B)


      1 // test/icon.test.js - the mark's geometry, and the png it encodes to
      2 
      3 import assert from 'node:assert/strict';
      4 import test from 'node:test';
      5 import zlib from 'node:zlib';
      6 import { DEFAULTS, GRID, mark, png, raster, svg } from '../util/icon.js';
      7 
      8 function parse(buf) {
      9   assert.deepEqual([...buf.subarray(0, 8)], [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
     10   const chunks = [];
     11   let p = 8;
     12   while (p < buf.length) {
     13     const len = buf.readUInt32BE(p);
     14     const type = buf.toString('ascii', p + 4, p + 8);
     15     const data = buf.subarray(p + 8, p + 8 + len);
     16     const stored = buf.readUInt32BE(p + 8 + len);
     17     chunks.push({ type, data, stored, covered: buf.subarray(p + 4, p + 8 + len) });
     18     p += len + 12;
     19   }
     20   const ihdr = chunks.find(c => c.type === 'IHDR');
     21   return {
     22     chunks,
     23     width: ihdr.data.readUInt32BE(0),
     24     height: ihdr.data.readUInt32BE(4),
     25     depth: ihdr.data[8],
     26     colour: ihdr.data[9],
     27     pixels: zlib.inflateSync(Buffer.concat(chunks.filter(c => c.type === 'IDAT').map(c => c.data))),
     28   };
     29 }
     30 
     31 // Greyscale+alpha, so a row is one filter byte then two bytes a pixel.
     32 function pixel(img, x, y) {
     33   const row = y * (1 + img.width * 2) + 1;
     34   return { grey: img.pixels[row + x * 2], alpha: img.pixels[row + x * 2 + 1] };
     35 }
     36 
     37 test('encodes a png every size it is asked for', () => {
     38   for (const size of [32, 180, 512]) {
     39     const img = parse(png(size, raster(size)));
     40     assert.equal(img.width, size);
     41     assert.equal(img.height, size);
     42     assert.equal(img.depth, 8);
     43     assert.equal(img.colour, 4);
     44     assert.equal(img.pixels.length, size * (1 + size * 2));
     45     assert.equal(img.chunks.at(-1).type, 'IEND');
     46   }
     47 });
     48 
     49 test('every chunk carries its own checksum', () => {
     50   const table = new Int32Array(256);
     51   for (let n = 0; n < 256; n++) {
     52     let c = n;
     53     for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
     54     table[n] = c;
     55   }
     56   for (const chunk of parse(png(32, raster(32))).chunks) {
     57     let c = -1;
     58     for (const byte of chunk.covered) c = table[(c ^ byte) & 0xff] ^ (c >>> 8);
     59     assert.equal((c ^ -1) >>> 0, chunk.stored, `${chunk.type} checksum`);
     60   }
     61 });
     62 
     63 test('the background stays transparent and the ink keeps its value', () => {
     64   const img = parse(png(64, raster(64), 0x1c));
     65   for (const [x, y] of [[0, 0], [63, 0], [0, 63], [63, 63]]) {
     66     assert.equal(pixel(img, x, y).alpha, 0, `corner ${x},${y}`);
     67   }
     68   assert.equal(pixel(img, 32, 32).alpha, 0, 'the bowl is hollow');
     69   const left = pixel(img, 12, 32);
     70   assert.equal(left.alpha, 255, 'the left limb is solid');
     71   assert.equal(left.grey, 0x1c);
     72 });
     73 
     74 test('a background composites the mark onto an opaque tile', () => {
     75   const img = parse(png(64, raster(64), 0x1c, 0xf6));
     76   for (let y = 0; y < 64; y++) {
     77     for (let x = 0; x < 64; x++) assert.equal(pixel(img, x, y).alpha, 255);
     78   }
     79   assert.equal(pixel(img, 0, 0).grey, 0xf6);
     80   assert.equal(pixel(img, 12, 32).grey, 0x1c);
     81 });
     82 
     83 test('the mark is symmetric about the horizontal axis', () => {
     84   const size = 256;
     85   const cov = raster(size, mark());
     86   for (let y = 0; y < size; y++) {
     87     for (let x = 0; x < size; x++) {
     88       assert.equal(cov[y * size + x], cov[(size - 1 - y) * size + x], `${x},${y}`);
     89     }
     90   }
     91 });
     92 
     93 test('the break is cut to the inner corner and the limbs end balanced', () => {
     94   const { outer, stroke, stretch } = DEFAULTS;
     95   const K = 1.45;
     96   const inner = outer - stroke;
     97   const shape = mark();
     98   const gapEdge = inner * (K - 1) + stretch;
     99   const trim = (stroke * (2 - K)) / 2;
    100 
    101   // Probes sit 4 units off every edge: (inner, gapEdge) is the corner itself,
    102   // where the diagonal lands on a float that rounds either way.
    103   assert.ok(shape(inner + 4, gapEdge + 4), 'limb present just past the corner');
    104   assert.ok(!shape(inner - 8, gapEdge + 4), 'the hole is not filled in');
    105   assert.ok(!shape(inner + 4, gapEdge - 4), 'the break is open below it');
    106 
    107   assert.ok(shape(outer - trim - 4, gapEdge + 4), 'limb present just inside the slice');
    108   assert.ok(!shape(outer - trim + 4, gapEdge + 4), 'nothing survives the slice');
    109 
    110   const horizontal = outer - trim - inner;
    111   const vertical = outer * (K - 1) + stretch + trim - gapEdge;
    112   assert.ok(Math.abs(horizontal - vertical) < 1e-9, `${horizontal} vs ${vertical}`);
    113   assert.ok(Math.abs(horizontal - (stroke * K) / 2) < 1e-9);
    114 });
    115 
    116 test('the bar clears the break and reaches past the sliced limbs', () => {
    117   const { start, reach, stroke, outer } = DEFAULTS;
    118   const shape = mark();
    119   assert.ok(!shape(start - 2, 0), 'nothing before the bar starts');
    120   assert.ok(shape(start + 2, 0), 'the bar starts where it says');
    121   assert.ok(shape(reach - 2, 0), 'the bar runs to its reach');
    122   assert.ok(!shape(reach + 2, 0), 'and stops there');
    123   assert.ok(reach > outer, 'the bar overshoots the ring');
    124   assert.ok(shape(reach - 2, stroke / 2 - 2), 'bar is a full stroke thick');
    125   assert.ok(!shape(reach - 2, stroke / 2 + 2));
    126 });
    127 
    128 // The svg restates the geometry independently, so it can drift from the
    129 // raster without either looking wrong on its own.
    130 test('the svg draws the same shape as the raster', () => {
    131   const doc = svg();
    132   const nums = (re) => [...doc.match(re)[0].matchAll(/-?[\d.]+/g)].map(Number);
    133   const polys = [...doc.matchAll(/<polygon points="([^"]+)"/g)]
    134     .map(m => m[1].split(' ').map(p => p.split(',').map(Number)));
    135   const rects = [...doc.matchAll(/<rect x="([\d.]+)" y="([\d.]+)" width="([\d.]+)" height="([\d.]+)"/g)]
    136     .map(m => m.slice(1, 5).map(Number));
    137   assert.equal(polys.length, 2);
    138   assert.equal(rects.length, 3);
    139 
    140   const inside = (poly, x, y) => {
    141     let hit = false;
    142     for (let i = 0, j = poly.length - 1; i < poly.length; j = i++) {
    143       const [xi, yi] = poly[i], [xj, yj] = poly[j];
    144       if ((yi > y) !== (yj > y) && x < ((xj - xi) * (y - yi)) / (yj - yi) + xi) hit = !hit;
    145     }
    146     return hit;
    147   };
    148   const within = ([rx, ry, rw, rh], x, y) => x >= rx && x < rx + rw && y >= ry && y < ry + rh;
    149 
    150   const fromSvg = (x, y) => {
    151     let on = inside(polys[0], x, y) && !inside(polys[1], x, y);
    152     if (within(rects[0], x, y)) on = false;
    153     if (within(rects[1], x, y)) on = false;
    154     if (within(rects[2], x, y)) on = true;
    155     return on;
    156   };
    157 
    158   const shape = mark();
    159   let checked = 0;
    160   for (let y = 4; y < GRID; y += 7) {
    161     for (let x = 4; x < GRID; x += 7) {
    162       // Half-unit offsets keep the probes off the shared edges, where the
    163       // polygon rule and the predicate are each free to round either way.
    164       const px = x + 0.5, py = y + 0.5;
    165       assert.equal(fromSvg(px, py), shape(px - GRID / 2, py - GRID / 2), `${px},${py}`);
    166       checked++;
    167     }
    168   }
    169   assert.ok(checked > 20000, `only probed ${checked} points`);
    170 });
    171 
    172 test('the same options always encode to the same bytes', () => {
    173   assert.deepEqual(png(64, raster(64)), png(64, raster(64)));
    174   assert.equal(svg(), svg());
    175 });