drawface-live / dev /runtime_test.html
ingon1's picture
sync from a4623ed60bfdc48500dc98de93020c59fe69bf34
4635d45 verified
Raw
History Blame Contribute Delete
5.4 kB
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>runtime tests</title>
</head>
<body>
<h1>DrawFace runtime unit tests</h1>
<pre id="result">running…</pre>
<!-- Standalone: imports only DOM/pure modules (no tracker.js, no network). -->
<script type="module">
import {
Hysteresis, TriStateEye, eyeKeyForUserSide, pickMouth,
} from "../js/pipeline.js";
import { prepareCharacter, composeCharacter } from "../js/compositor.js";
// Match tests/test_camera_mapping.py fixtures (camelCase for the web config).
const EYES_CFG = {
closeThreshold: 0.45, openThreshold: 0.30,
halfCloseThreshold: 0.20, halfOpenThreshold: 0.12,
};
const MOUTH_CFG = {
jawClosed: 0.06, jawMid: 0.16, jawLarge: 0.32,
puckerThreshold: 0.40, funnelThreshold: 0.35, smileThreshold: 0.30,
};
let n = 0;
const eq = (got, want, msg) => {
n++;
const a = JSON.stringify(got), b = JSON.stringify(want);
if (a !== b) throw new Error(`${msg}: got ${a}, want ${b}`);
};
const ok = (cond, msg) => { n++; if (!cond) throw new Error(msg); };
const throws = (fn, msg) => {
n++;
try { fn(); } catch { return; }
throw new Error(`${msg}: expected throw`);
};
function testTriStateBands() {
const e = new TriStateEye(EYES_CFG);
eq(e.update(0.05), "open", "tristate start open");
eq(e.update(0.25), "half", "tristate crossed half_close");
eq(e.update(0.15), "half", "tristate inside half gap stays half");
eq(e.update(0.50), "closed", "tristate crossed full close");
eq(e.update(0.35), "closed", "tristate inside full gap stays closed");
eq(e.update(0.25), "half", "tristate below open_threshold -> half");
eq(e.update(0.10), "open", "tristate below half_open -> open");
}
function testHysteresisFlicker() {
const h = new Hysteresis(0.45, 0.30);
eq(h.update(0.40), false, "hyst below close stays open");
eq(h.update(0.50), true, "hyst crosses close");
eq(h.update(0.35), true, "hyst in gap stays closed");
eq(h.update(0.29), false, "hyst below open reopens");
eq(h.update(0.31), false, "hyst in gap stays open");
}
function testMouthLadder() {
eq(pickMouth({ jawOpen: 0.0 }, MOUTH_CFG), "closed", "mouth closed");
eq(pickMouth({ jawOpen: 0.02, mouthSmileLeft: 0.5, mouthSmileRight: 0.5 }, MOUTH_CFG), "smile", "mouth smile");
eq(pickMouth({ jawOpen: 0.10 }, MOUTH_CFG), "I", "mouth I");
eq(pickMouth({ jawOpen: 0.20 }, MOUTH_CFG), "E", "mouth E");
eq(pickMouth({ jawOpen: 0.40 }, MOUTH_CFG), "A", "mouth A");
eq(pickMouth({ jawOpen: 0.10, mouthPucker: 0.5 }, MOUTH_CFG), "U", "mouth U");
eq(pickMouth({ jawOpen: 0.20, mouthPucker: 0.5 }, MOUTH_CFG), "O", "mouth O (pucker)");
eq(pickMouth({ jawOpen: 0.20, mouthFunnel: 0.5 }, MOUTH_CFG), "O", "mouth O (funnel)");
}
function testMapping() {
eq(eyeKeyForUserSide("left", true), "L", "mirror user-left -> L");
eq(eyeKeyForUserSide("right", true), "R", "mirror user-right -> R");
eq(eyeKeyForUserSide("left", false), "R", "anatomical user-left -> R");
eq(eyeKeyForUserSide("right", false), "L", "anatomical user-right -> L");
for (const mirror of [true, false]) {
const sides = new Set([eyeKeyForUserSide("left", mirror), eyeKeyForUserSide("right", mirror)]);
ok(sides.size === 2 && sides.has("L") && sides.has("R"), `mapping exclusive (mirror=${mirror})`);
}
throws(() => eyeKeyForUserSide("up", true), "invalid side rejected");
}
function coloredCanvas(color) {
const c = document.createElement("canvas");
c.width = 512; c.height = 512;
const x = c.getContext("2d");
x.fillStyle = color; x.fillRect(0, 0, 512, 512);
return c;
}
function testCompositor() {
const canvases = { "base.png": coloredCanvas("#ffffff") };
for (const side of ["L", "R"]) {
canvases[`eye_${side}_open.png`] = coloredCanvas("#00ff00");
canvases[`eye_${side}_closed.png`] = coloredCanvas("#ff0000");
}
for (const k of ["closed", "A", "E", "I", "O", "U"]) canvases[`mouth_${k}.png`] = coloredCanvas("#0000ff");
// eye_*_half and mouth_smile intentionally absent -> optional fallback.
const char = prepareCharacter({ name: "t", manifest: {}, canvases });
ok(char.eyes.L.half === char.eyes.L.open, "half falls back to open");
ok(char.mouths.smile === char.mouths.closed, "smile falls back to closed");
const a = composeCharacter(char, "open", "open", "closed");
const b = composeCharacter(char, "open", "open", "closed");
ok(a === b, "compose memoized: same state -> identical object");
const c = composeCharacter(char, "closed", "open", "closed");
ok(c !== a, "compose: different state -> different object");
}
try {
testTriStateBands();
testHysteresisFlicker();
testMouthLadder();
testMapping();
testCompositor();
document.title = `RUNTIME OK ${n} assertions`;
document.getElementById("result").textContent = `RUNTIME OK — ${n} assertions passed`;
} catch (err) {
document.title = `RUNTIME FAIL ${err.message}`;
document.getElementById("result").textContent = `RUNTIME FAIL — ${err.message}`;
throw err;
}
</script>
</body>
</html>