Visual automation in both of its scopes — exhibit-scope tracks measured from activation and system-scope tracks measured from an instance's own instantiation, so a track written against spawn-relative time behaves the same whether the spawn happens at four seconds or at four minutes — together with the spawned-system lifecycle, the camera, and the seven post-effects of 19.4. Automation on a missing target object now raises ERR_INVALID_REFERENCE rather than an unguarded TypeError, per 19.1. Post-effect channel math is clamped explicitly before assignment and the device blur radius is capped against a pixel budget, so a large projected radius cannot turn a legal exhibit into a frame-long stall. Trail fade tapers per segment from the item's own head opacity to the tail factor, which is what 18.8 describes; the previous flat alpha ignored the item's ramped opacity entirely. The canvas backend consumes the per-object buffer grants the engine allocates, rasterizing a buffered node once and compositing it once under its own blend and alpha, rather than compositing fill, stroke, glow, and filter separately and double-blending the overlap. Glow is drawn as a halo around the result rather than painted over it, and text now gets the glow branch it was silently missing. Traces 19.7.3 and 19.7.4 are implemented as specified rather than approximated: the two-spawn four-second offset assertion, and the four negative target cases that separate ERR_INVALID_REFERENCE from ERR_UNSUPPORTED_TARGET. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01ShxxFqFmCUDQnQvFNm4TKy
72 lines
6.9 KiB
JavaScript
72 lines
6.9 KiB
JavaScript
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
import { resolve } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { bundleRuntime } from './build-xzbt.mjs';
|
|
import { visualChallenges, visualExhibits } from './visual-challenge-fixtures.mjs';
|
|
import { validateExhibit } from '../src/runtime/validator.js';
|
|
|
|
export function buildVisualAcceptance() {
|
|
const root = fileURLToPath(new URL('..', import.meta.url));
|
|
const fixtures = [...visualChallenges, ...visualExhibits];
|
|
mkdirSync(resolve(root, 'exhibits/visual-challenges'), { recursive: true });
|
|
mkdirSync(resolve(root, 'prototypes/phase4'), { recursive: true });
|
|
for (const fixture of fixtures) {
|
|
const result = validateExhibit(fixture.document);
|
|
if (!result.valid) throw new Error(JSON.stringify(result.errors));
|
|
writeFileSync(resolve(root, fixture.challenge ? 'exhibits/visual-challenges' : 'exhibits', `${fixture.id}.xzbt`), JSON.stringify(fixture.document, null, 2) + '\n');
|
|
}
|
|
const data = JSON.stringify(fixtures).replace(/</g, '\\u003c');
|
|
const html = `<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
<title>XZBT Visual Challenge</title><style>
|
|
body{margin:0;background:#101820;color:#e5eee9;font:15px system-ui}header{padding:20px 28px;border-bottom:1px solid #33464b}h1{font-size:22px;margin:0 0 12px}select,button{font:inherit;padding:8px;margin-right:8px;background:#24373c;color:inherit;border:1px solid #607572;border-radius:5px}main{padding:24px}canvas{display:block;width:100%;height:min(70vh,800px);background:#000}p{color:#a9bfba}label{margin-right:16px}output{font-variant-numeric:tabular-nums}#notes{white-space:pre-wrap;font:12px monospace}</style>
|
|
<header><h1>Visual Challenge / XZBT 0.1</h1><select id="fixture"></select><button id="restart">Restart seed</button><button id="pause">Pause</button><button id="burst">Spawn burst</button><button id="export">Export observation</button></header>
|
|
<main><canvas id="canvas"></canvas><p id="description"></p><label>Test audio signal <input id="energy" type="range" min="0" max="1" step="0.01" value="0.6"></label><label>Display state <input id="level" type="range" min="0" max="1" step="0.01" value="0.7"></label><output id="time"></output><p>This page supplies synthetic audio signals for reproducible visual checks. It does not establish live audio acceptance or benchmark results. Judge the composition on your display and record observations below.</p><textarea id="observation" rows="3" cols="80" placeholder="Visual observations, fixture ID, display, browser and hardware"></textarea><pre id="notes"></pre></main>
|
|
<script>${bundleRuntime(false)}
|
|
const acceptanceFixtures = ${data};
|
|
const acceptanceSelect = document.getElementById('fixture');
|
|
for (const fixture of acceptanceFixtures) { const option = document.createElement('option'); option.value = fixture.id; option.textContent = (fixture.challenge ? fixture.challenge + '. ' : '') + fixture.document.meta.name; acceptanceSelect.append(option); }
|
|
let acceptanceResolution, acceptanceRng, acceptanceVisual, acceptanceLast, acceptanceAccumulator = 0, acceptancePaused = false, acceptanceWarnings = [];
|
|
function acceptanceStart() {
|
|
acceptanceVisual?.deactivate(); acceptanceResolution?.dispose();
|
|
const fixture = acceptanceFixtures.find(f => f.id === acceptanceSelect.value);
|
|
acceptanceWarnings = []; acceptanceRng = new SeededRNG(fixture.document.runtime.seed);
|
|
acceptanceResolution = new ResolutionEngine(fixture.document, acceptanceRng);
|
|
acceptanceVisual = new VisualSubsystem({ diagnostics: { warn: (code, message) => { acceptanceWarnings.push({ code, message }); document.getElementById('notes').textContent = acceptanceWarnings.map(w => w.code + ': ' + w.message).join('\\n'); } } });
|
|
acceptanceVisual.attach(document.getElementById('canvas')); acceptanceVisual.activate(fixture.document, { resolution: acceptanceResolution, rng: acceptanceRng });
|
|
if (fixture.id === 'transient-impact') acceptanceVisual.engine.spawn('burst');
|
|
document.getElementById('description').textContent = fixture.document.meta.description;
|
|
document.getElementById('burst').disabled = !fixture.document.visuals.systems.burst;
|
|
document.getElementById('notes').textContent = ''; acceptanceLast = undefined; acceptanceAccumulator = 0;
|
|
}
|
|
function acceptanceFrame(now) {
|
|
const elapsed = acceptanceLast === undefined ? 0 : Math.min(100, now - acceptanceLast); acceptanceLast = now;
|
|
if (!acceptancePaused && !document.hidden) {
|
|
acceptanceAccumulator += elapsed;
|
|
while (acceptanceAccumulator >= 1000 / 60) {
|
|
const energy = Number(document.getElementById('energy').value);
|
|
acceptanceResolution.signals.set('signals.audio.energy', energy); acceptanceResolution.signals.set('signals.audio.low', energy);
|
|
if (acceptanceResolution.document.state?.level) acceptanceResolution.setState('state.level', Number(document.getElementById('level').value));
|
|
acceptanceResolution.advance(1000 / 60); acceptanceVisual.advance(1000 / 60); acceptanceAccumulator -= 1000 / 60;
|
|
}
|
|
}
|
|
acceptanceVisual.render({ logicalMilliseconds: acceptanceResolution.logicalMilliseconds });
|
|
document.getElementById('time').textContent = (acceptanceResolution.logicalMilliseconds / 1000).toFixed(1) + ' s';
|
|
requestAnimationFrame(acceptanceFrame);
|
|
}
|
|
acceptanceSelect.onchange = acceptanceStart; document.getElementById('restart').onclick = acceptanceStart;
|
|
document.getElementById('pause').onclick = event => { acceptancePaused = !acceptancePaused; event.target.textContent = acceptancePaused ? 'Resume' : 'Pause'; };
|
|
document.getElementById('burst').onclick = () => acceptanceVisual.engine.spawn('burst');
|
|
document.addEventListener('visibilitychange', () => { acceptanceLast = undefined; acceptanceAccumulator = 0; });
|
|
document.getElementById('export').onclick = () => {
|
|
const report = { fixture: acceptanceSelect.value, seed: acceptanceRng.rootSeed, browser: navigator.userAgent, display: { width: innerWidth, height: innerHeight, dpr: devicePixelRatio }, logicalMilliseconds: acceptanceResolution.logicalMilliseconds, warnings: acceptanceWarnings, observation: document.getElementById('observation').value, acceptance: 'user observation only; no automatic visual pass', exportedAt: new Date().toISOString() };
|
|
const url = URL.createObjectURL(new Blob([JSON.stringify(report, null, 2)], { type: 'application/json' })); const link = document.createElement('a'); link.href = url; link.download = 'xzbt-visual-observation.json'; link.click(); setTimeout(() => URL.revokeObjectURL(url), 1000);
|
|
};
|
|
acceptanceStart(); requestAnimationFrame(acceptanceFrame);
|
|
</script></html>`;
|
|
const path = resolve(root, 'prototypes/phase4/XZBT-visual-acceptance.html');
|
|
writeFileSync(path, html); return { path, count: fixtures.length, html };
|
|
}
|
|
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
const result = buildVisualAcceptance(); console.log(`Built ${result.path}; ${result.count} fixtures.`);
|
|
}
|