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 });