// parzival.llc/mark/derive.txt // // The mark on parzival.llc is a fixed figure whose bands are weighted by the // current statement's sha256: band i takes byte i of the hash and varies its // thickness within twelve per cent of nominal, as set out below. Outline, // bands and gaps never change. The mark therefore shows when the statement // has been re-issued; the hash in the record shows which statement it is. // // The die (die.json) is the fixed figure: one or more closed polygons per // band, in a 1000-wide coordinate space. It carries no information from the // hash. Only the vertical scale of each band about its own centre line is set // by the hash. Gaps keep their x positions; the outline keeps its shape. // // Usage: node derive.txt [die.json] > mark.svg // node derive.txt die.json --favicon > favicon.svg // // Output is canonical: fixed attribute order, one decimal place, LF line // endings, no whitespace variation. Two runs on the same hash and the same // die produce byte-identical files. Plain Node, no dependencies. 'use strict'; const fs = require('fs'); const MIN = 0.88; // thinnest impression const RANGE = 0.24; // MIN + RANGE = 1.12, the thickest const INK = '#262626'; // foreground, matches the site's text colour function weight(byte) { // byte 0..255 -> factor 0.88..1.12, rounded so the output is stable return Math.round((MIN + RANGE * (byte / 255)) * 10000) / 10000; } function hashBytes(hex) { if (!/^[0-9a-f]{64}$/i.test(hex)) throw new Error('expected a 64-character sha256 hex string'); const out = []; for (let i = 0; i < 64; i += 2) out.push(parseInt(hex.slice(i, i + 2), 16)); return out; } function fmt(n) { // one decimal place, no trailing ".0", no "-0.0" const s = (Math.round(n * 10) / 10).toFixed(1).replace(/\.0$/, ''); return s === '-0' ? '0' : s; } function bandPath(band, factor) { // scale each polygon's y about the band's centre line; x untouched const cy = band.centre_y; const parts = []; for (const poly of band.polygons) { const pts = poly.map(([x, y]) => [x, cy + (y - cy) * factor]); parts.push('M' + pts.map(([x, y]) => fmt(x) + ' ' + fmt(y)).join('L') + 'Z'); } return parts.join(''); } function render(hashHex, die, opts = {}) { const bytes = hashBytes(hashHex); const [vx, vy, vw, vh] = die.viewBox; const lines = []; lines.push(''); lines.push('Parzival LLC'); die.bands.forEach((band, i) => { const f = opts.favicon ? 1 : weight(bytes[i]); lines.push(''); }); lines.push(''); return lines.join('\n') + '\n'; } if (require.main === module) { const args = process.argv.slice(2); const hex = args[0]; const diePath = args.find(a => a.endsWith('.json')) || 'die.json'; const favicon = args.includes('--favicon'); const die = JSON.parse(fs.readFileSync(diePath, 'utf8')); process.stdout.write(render(hex, die, { favicon })); } module.exports = { render, weight, hashBytes };