Continuity rule: discrete params hold per cue; add DESIGN.md + AGENTS.md

Actors and objects change only at camera cuts. int/bool/discrete
params skip drift, lookahead and reactivity; discrete slowAxis steps
on the section-held journey. Venues fade instead of toggling on
interpolated popMul; roadside density is arrangement-only.

DESIGN.md collects the project's design decisions; AGENTS.md points
agents at it and the build-visualizer loop.
This commit is contained in:
Dejvino
2026-09-20 16:42:44 +02:00
parent 6e70f95d4d
commit a99a7cbe49
15 changed files with 308 additions and 31 deletions
+27 -3
View File
@@ -1,12 +1,12 @@
import { createLayer } from '../engine/Layer.js';
import { Rng } from '../engine/rng.js';
import { clampValue } from '../params/schema.js';
import { clampValue, isDiscreteParam } from '../params/schema.js';
import { paletteShiftAt } from './paletteArc.js';
import { shiftPalette, lerpPalettes } from './palette.js';
import { frameShot, neutralFraming } from './framing.js';
import { planGaze, gazeAt } from './Camera.js';
import { planVenueShots, venueShotAt, describeVenueShot } from './venueShots.js';
import { storyStateAt, NEUTRAL_STATE } from './Story.js';
import { storyStateAt, storyForSection, NEUTRAL_STATE } from './Story.js';
import { subjectIndexOf, isGround } from './stack.js';
import { groundPersonalityFrom } from '../scenes/surface.js';
import { derivePalettePlan, directorByName } from './directors.js';
@@ -383,6 +383,10 @@ export class ArcDriver {
for (const [name, def] of Object.entries(spec.module.params || {})) {
if (def.type === 'palette' || def.type === 'bool' || def.fixed) continue;
if (def.noDrift || def.rate) continue; // see schema.js RATE_FLAG
// Continuity rule (see schema.js isDiscreteParam): a drifting count
// pops members mid-shot. Discrete params hold for the whole cue and
// change only at cuts, via the per-section/variant sample.
if (isDiscreteParam(def)) continue;
const [lo, hi] = def.range || [0, 1];
plan.push({
name,
@@ -517,10 +521,28 @@ export class ArcDriver {
const p = Math.max(0, Math.min(1, time / duration));
journey = p * p * (3 - 2 * p);
}
// Discrete params step their slow-axis travel at cuts instead of
// sliding it: the section-held journey moves only at section
// boundaries (which are always cuts), and without a story the cue's
// own start frame is the step. Either way nothing pops mid-shot.
// See params/schema.js isDiscreteParam.
let journeyStepped = null;
const steppedJourney = () => {
if (journeyStepped !== null) return journeyStepped;
if (this.look.story) {
journeyStepped = storyForSection(this.look.story, cue.sectionIndex).journey;
} else {
const duration = Math.max(1e-6, this.track.duration);
const pc = Math.max(0, Math.min(1, (cue.startFrame / this.track.fps) / duration));
journeyStepped = pc * pc * (3 - 2 * pc);
}
return journeyStepped;
};
for (const item of this._slowAxisFor(spec.module)) {
const base = out[item.name];
if (typeof base !== 'number') continue;
out[item.name] = clampValue(item.def, base + item.travel * (journey - 0.5));
const j = isDiscreteParam(item.def) ? steppedJourney() : journey;
out[item.name] = clampValue(item.def, base + item.travel * (j - 0.5));
}
for (const item of this._driftPlan(cue.sectionIndex, cue.variant, slot)) {
@@ -545,6 +567,7 @@ export class ArcDriver {
const target = next.params;
for (const [name, def] of Object.entries(spec.module.params || {})) {
if (def.type === 'palette' || typeof out[name] !== 'number') continue;
if (isDiscreteParam(def)) continue;
if (typeof target[name] !== 'number') continue;
out[name] = clampValue(def, out[name] + (target[name] - out[name]) * slope);
}
@@ -553,6 +576,7 @@ export class ArcDriver {
// of their range so the build still reads as a build.
for (const [name, def] of Object.entries(spec.module.params || {})) {
if (typeof out[name] !== 'number') continue;
if (isDiscreteParam(def)) continue;
if (def.bias !== 'energy' && def.bias !== 'density') continue;
const hi = (def.range || [0, 1])[1];
out[name] = clampValue(def, out[name] + (hi - out[name]) * slope * 0.5);