Side quest 1: give four scenes art direction other than grain

Classic Wave, Silk Ribbon, Kaleido Tunnel and Slow Orb all declared the
`style` trait and honoured it with `col += sigGrain(uv)` and nothing else. A
declared trait is a contract — the disqualification rule in Personality.js is
the only thing keeping off-design scenes out of a track — so honouring it
with dirt meant these four could not take the `texture: 0` opt-out the grain
work introduced. Phase 9 measured their style response at exactly 0. They sat
at texture: 0.35 as a stopgap, which kept speckle on the library's cleanest
scenes purely to keep a gate green.

Each already had the knob; it just was not wired to the track:

- Classic Wave contrasts its wave through a bare smoothstep(0.2, 0.8). The
  transition width now comes from u_sigSoft and u_sigLine, centred on 0.5 so
  changing the hand does not change the exposure. Crest concentration is
  driven separately by u_sigLine, because a track is free to sample the
  scene's own u_softness at zero and the art direction must still show.
- Silk Ribbon's strand width is u_sigLine and its falloff exponent u_sigSoft:
  a sharp track gets a filament with a defined edge, a soft one a haze.
- Kaleido Tunnel drew its grid against a bare 0.42 — a line weight with no
  name. It is now a weight and a feather, both from the track.
- Slow Orb's body edge multiplies the scene's softness by the video's, and
  gains a sigEdge rim so a sharp-handed track gets a defined limb.

All four are now texture: 0. Style response measured against a maximally
soft-handed versus maximally sharp-handed personality: Classic Wave 111,
Silk Ribbon 216, Kaleido Tunnel 206, Slow Orb 102, out of 255 — previously 0.

104/104 checks pass including the slow set. See SIDE-QUESTS.md §1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Dejvino
2026-08-06 19:35:50 +02:00
co-authored by Claude Opus 5
parent 8dfd8392f3
commit 17a583a87f
11 changed files with 641 additions and 17 deletions
+131
View File
@@ -0,0 +1,131 @@
<!DOCTYPE html>
<html>
<head><title>encoder probe</title></head>
<body>
<pre id="out">running…</pre>
<canvas id="c" width="1920" height="1080" style="display:none"></canvas>
<script type="module">
const out = document.getElementById('out');
const log = (m) => { out.textContent += m + '\n'; console.log(m); };
// A cheap animated source. The point is to test the ENCODER, so the frames must
// be genuinely different (a static source would let the encoder coast) but must
// cost nothing to produce — no shaders, no Show, no GPU readback.
const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
function drawFrame(i) {
ctx.fillStyle = `hsl(${(i * 3) % 360} 60% 12%)`;
ctx.fillRect(0, 0, 1920, 1080);
ctx.fillStyle = `hsl(${(i * 7) % 360} 80% 60%)`;
for (let k = 0; k < 40; k++) {
const x = ((i * 11 + k * 137) % 1920);
const y = ((i * 5 + k * 271) % 1080);
ctx.fillRect(x, y, 90, 90);
}
ctx.fillStyle = '#fff';
ctx.font = '120px monospace';
ctx.fillText(String(i), 60, 200);
}
const FPS = 60;
const FRAMES = 600;
const WIDTH = 1920, HEIGHT = 1080, BITRATE = 16_000_000;
const MAX_QUEUE = 8;
function waitForQueue(encoder) {
if (encoder.encodeQueueSize < MAX_QUEUE) return Promise.resolve();
return new Promise((resolve) => {
const onDequeue = () => {
if (encoder.encodeQueueSize < MAX_QUEUE) {
encoder.removeEventListener('dequeue', onDequeue);
resolve();
}
};
encoder.addEventListener('dequeue', onDequeue);
});
}
async function run(label, extra) {
const config = {
codec: 'avc1.640034',
width: WIDTH, height: HEIGHT, bitrate: BITRATE, framerate: FPS,
avc: { format: 'avc' },
...extra,
};
let support;
try {
support = await VideoEncoder.isConfigSupported(config);
} catch (e) {
log(`${label.padEnd(34)} isConfigSupported threw: ${e.message}`);
return;
}
if (!support.supported) {
log(`${label.padEnd(34)} UNSUPPORTED`);
return;
}
let outputs = 0;
let keyframes = 0;
const stamps = [];
let error = null;
const encoder = new VideoEncoder({
output: (chunk) => {
outputs++;
if (chunk.type === 'key') keyframes++;
stamps.push(chunk.timestamp);
},
error: (e) => { error = e; },
});
encoder.configure(config);
const t0 = performance.now();
let encoded = 0;
for (let i = 0; i < FRAMES; i++) {
if (error) break;
drawFrame(i);
await waitForQueue(encoder);
const frame = new VideoFrame(canvas, {
timestamp: Math.round((i * 1e6) / FPS),
duration: Math.round(1e6 / FPS),
});
encoder.encode(frame, { keyFrame: i % (FPS * 2) === 0 });
frame.close();
encoded++;
if (i % 10 === 0) await new Promise((r) => setTimeout(r, 0));
}
try { await encoder.flush(); } catch (e) { error = error || e; }
const ms = performance.now() - t0;
try { encoder.close(); } catch { /* already closed */ }
// Do NOT sort: the order chunks ARRIVE in is the whole point. WebCodecs
// delivers in decode order, so a timestamp that goes backwards means the
// encoder emitted B-frames — and a muxer handed presentation timestamps as
// if they were decode timestamps will reject exactly those chunks.
const period = 1e6 / FPS;
const backwards = stamps.filter((t, i) => i > 0 && t < stamps[i - 1]).length;
const sorted = stamps.slice().sort((a, b) => a - b);
const gaps = sorted.slice(1).map((t, i) => Math.round((t - sorted[i]) / period));
const maxGap = gaps.length ? Math.max(...gaps) : 0;
const verdict = error ? `ERROR ${error.message}`
: backwards ? `REORDERED — ${backwards} chunks arrived out of order (B-frames)`
: outputs === encoded ? 'OK'
: `DROPPED ${encoded - outputs} (${((1 - outputs / encoded) * 100).toFixed(0)}%)`;
log(`${label.padEnd(34)} in ${encoded} → out ${outputs} keys ${keyframes} maxGap ${maxGap} reordered ${backwards} ${(ms / 1000).toFixed(1)}s ${verdict}`);
}
log(`source ${WIDTH}×${HEIGHT} @ ${FPS} · ${FRAMES} frames · bitrate ${BITRATE / 1e6} Mbps\n`);
await run('default (no hints)', {});
await run('prefer-hardware', { hardwareAcceleration: 'prefer-hardware' });
await run('prefer-software', { hardwareAcceleration: 'prefer-software' });
await run('latency=realtime', { latencyMode: 'realtime' });
await run('prefer-hardware + realtime', { hardwareAcceleration: 'prefer-hardware', latencyMode: 'realtime' });
await run('prefer-software + realtime', { hardwareAcceleration: 'prefer-software', latencyMode: 'realtime' });
await run('baseline avc1.42003e', { codec: 'avc1.42003e' });
log('\nDONE');
window.__DONE__ = true;
</script>
</body>
</html>