108 lines
5.4 KiB
JavaScript
108 lines
5.4 KiB
JavaScript
|
|
// Runs the JS prototype — the numeric oracle — and writes its answers to JSON
|
||
|
|
// for arthur.parity-test to diff against the CLJS port.
|
||
|
|
//
|
||
|
|
// DELETABLE. This file and arthur.parity-test go together, in one commit, once
|
||
|
|
// the CLJS player renders the synthetic take correctly (port-plan step 5). A
|
||
|
|
// parity test pins behaviour while the code moves; keeping it afterwards would
|
||
|
|
// bake the prototype's mistakes into the rewrite and make them permanent.
|
||
|
|
//
|
||
|
|
// Math.random is stubbed to a constant so both sides get the IDENTICAL track:
|
||
|
|
// js/synth.js reads Math.random at call time, not at import time, so assigning
|
||
|
|
// it here — before synthDense is called below — is enough, and js/ stays
|
||
|
|
// untouched. 0.5 makes every (Math.random() - 0.5) jitter term exactly zero,
|
||
|
|
// which is also what `:rand-fn (constantly 0.5)` does on the CLJS side.
|
||
|
|
Math.random = () => 0.5;
|
||
|
|
|
||
|
|
import { writeFileSync } from 'node:fs';
|
||
|
|
import { fileURLToPath } from 'node:url';
|
||
|
|
import { dirname, join } from 'node:path';
|
||
|
|
|
||
|
|
import { RIGID, LIPS_OUTER, EYE_R_RING, BROW_A_RING, FACE_OVAL,
|
||
|
|
subsampleSlots } from '../../../js/landmarks.js';
|
||
|
|
import { fitSimilarity, applySim, fitResidual, procrustesMean,
|
||
|
|
movingAverage, smoothTransforms, offsetRing } from '../../../js/mathutil.js';
|
||
|
|
import { synthDense } from '../../../js/synth.js';
|
||
|
|
import { IndexedRaster, hexToRgb } from '../../../js/raster.js';
|
||
|
|
|
||
|
|
const FRAMES = 72;
|
||
|
|
const track = synthDense(FRAMES);
|
||
|
|
const rigid = track.map((f) => RIGID.map((i) => f[i]));
|
||
|
|
|
||
|
|
// The two the port plan names explicitly, plus everything else in mathutil.js:
|
||
|
|
// a function nobody diffed is a function nobody ported.
|
||
|
|
const ref = procrustesMean(rigid);
|
||
|
|
const tfs = rigid.map((r) => fitSimilarity(r, ref));
|
||
|
|
|
||
|
|
const strip = (p) => ({ x: p.x, y: p.y, z: p.z ?? 0 });
|
||
|
|
const stripTf = (t) => ({ s: t.s, theta: t.theta, tx: t.tx, ty: t.ty });
|
||
|
|
|
||
|
|
// A known transform recovered exactly, which is the same case the CLJS unit test
|
||
|
|
// asserts — here so a disagreement can be localised to the fit rather than to
|
||
|
|
// the track.
|
||
|
|
const knownSrc = [{ x: 0, y: 0 }, { x: 1, y: 0 }, { x: 0, y: 1 }, { x: 2, y: 3 }];
|
||
|
|
const knownTruth = { s: 1.7, theta: 0.6, tx: 4, ty: -2 };
|
||
|
|
const knownDst = knownSrc.map((p) => applySim(knownTruth, p));
|
||
|
|
|
||
|
|
const out = {
|
||
|
|
frames: FRAMES,
|
||
|
|
track: track.map((f) => f.map(strip)),
|
||
|
|
rigidRef: ref.map(strip),
|
||
|
|
transforms: tfs.map(stripTf),
|
||
|
|
residuals: rigid.map((r, i) => fitResidual(tfs[i], r, ref)),
|
||
|
|
smoothed: [0, 1, 2, 5].map((radius) => ({
|
||
|
|
radius, tfs: smoothTransforms(tfs, radius).map(stripTf),
|
||
|
|
})),
|
||
|
|
known: { src: knownSrc, truth: knownTruth, dst: knownDst,
|
||
|
|
fit: stripTf(fitSimilarity(knownSrc, knownDst)) },
|
||
|
|
// tx over the shot is the sway; it is the one-dimensional series the smoothing
|
||
|
|
// knob actually acts on, so it is what movingAverage gets diffed on.
|
||
|
|
movingAverage: [0, 1, 2, 3, 7].map((radius) => ({
|
||
|
|
radius, vals: movingAverage(tfs.map((t) => t.tx), radius),
|
||
|
|
})),
|
||
|
|
offsetRing: [0, 0.5, 2, -1].map((d) => ({
|
||
|
|
d,
|
||
|
|
ring: offsetRing(LIPS_OUTER.map((i) => track[0][i]), d).map(strip),
|
||
|
|
// The degenerate case: a shut lid is a flat sliver and must still open into
|
||
|
|
// a band, and a ring collapsed onto its own centroid must not emit NaN.
|
||
|
|
shutLid: offsetRing([{ x: -10, y: 0 }, { x: 0, y: -0.02 },
|
||
|
|
{ x: 10, y: 0 }, { x: 0, y: 0.02 }], d).map(strip),
|
||
|
|
collapsed: offsetRing([{ x: 0, y: 0 }, { x: 0, y: 0 }, { x: 0, y: 0 }], d).map(strip),
|
||
|
|
})),
|
||
|
|
subsampleSlots: Object.fromEntries(
|
||
|
|
[[20, 4], [20, 6], [20, 8], [20, 10], [20, 16], [16, 4], [16, 6], [16, 12],
|
||
|
|
[10, 4], [10, 6], [10, 10], [36, 8]]
|
||
|
|
.map(([len, n]) => [`${len}/${n}`, subsampleSlots(len, n)])),
|
||
|
|
tables: { RIGID, LIPS_OUTER, EYE_R_RING, BROW_A_RING, FACE_OVAL },
|
||
|
|
// The raster is integer output, so parity here is EXACT equality, not 1e-9.
|
||
|
|
// One scanline drawn one pixel wide of the JS would read as a seam between two
|
||
|
|
// parts rather than as an error, which is why the whole buffer is diffed and
|
||
|
|
// not a pixel count.
|
||
|
|
//
|
||
|
|
// toImageData is not exercised: it needs an ImageData, the CLJS side returns
|
||
|
|
// plain bytes on purpose so domain/ stays DOM-free, and the palette expansion
|
||
|
|
// is asserted directly in arthur.domain.raster-test instead.
|
||
|
|
raster: (() => {
|
||
|
|
const r = new IndexedRaster(64, 48);
|
||
|
|
r.clear(0);
|
||
|
|
// A real mouth ring at raster scale, so the scanline fill is diffed on a
|
||
|
|
// shape with fractional coordinates and non-convex spans rather than on an
|
||
|
|
// axis-aligned box that would agree even if the rounding were wrong.
|
||
|
|
r.fillPoly(LIPS_OUTER.map((i) => ({ x: track[0][i].x * 320 - 100,
|
||
|
|
y: track[0][i].y * 200 - 40 })), 2);
|
||
|
|
r.fillPoly([{ x: 8.5, y: 8.5 }, { x: 56.25, y: 8.5 },
|
||
|
|
{ x: 56.25, y: 40.75 }, { x: 8.5, y: 40.75 }], 1);
|
||
|
|
r.fillDisc(30.4, 24.6, 9.2, 3, 1); // stencilled by the box
|
||
|
|
r.fillDisc(5.5, 44.5, 4, 4); // unstencilled, clipped by the edge
|
||
|
|
r.fillRect(30.49, 24.51, 3, 5, 3); // stencilled by the disc
|
||
|
|
r.fillRect(1, 1, 0, 6); // size 0 draws nothing
|
||
|
|
return { w: r.w, h: r.h, buf: Array.from(r.buf) };
|
||
|
|
})(),
|
||
|
|
paletteRgb: ['#12141c', '#b07a5a', '#7a4f3a', '#24161a', '#d9cfc2',
|
||
|
|
'#c9c3b4', '#4a5468', '#171a22', '#3a2a22'].map(hexToRgb),
|
||
|
|
};
|
||
|
|
|
||
|
|
const here = dirname(fileURLToPath(import.meta.url));
|
||
|
|
const path = join(here, 'oracle.json');
|
||
|
|
writeFileSync(path, JSON.stringify(out));
|
||
|
|
console.log(`oracle: ${FRAMES} frames -> ${path}`);
|