FEATURES (from a 5-lens audit: UX friction, feature gaps, bug hunt, visual quality, discoverability): - ACES filmic tone mapping + per-preset exposure. grammar.js runs physical sun intensities of 1.2-3.4; with NoToneMapping every lit face clipped to white and a warm key rendered neutral. Single biggest look win in the app. - Real shadows: PCFSoft, a shadow box fitted to the scene's bounds, and plates that no longer double as the shadow catcher (dedicated ShadowMaterial). - A Render button in the scene bar. finalRender had ZERO callers - the app's deliverable was console-only. - Save actually saves the stage you built (gizmo moves, inspector edits), and Save/Load stop lying about what happened. - 2x supersample + lanczos downscale; correct bt709 tagging and faststart. - Frame guide, camera-from-view, dock counts, delete confirmations. DEFECTS FOUND BY ADVERSARIAL REVIEW AND FIXED (all reproduced first): - BLOCKER: save wrote the playhead pose over the authored rest transform of keyed entities, so every save produced a different file. - renders were converted with the bt601 matrix while tagged bt709. - deleting an unkeyed camera stranded its cut -> scene 422s forever. - corner plates showed one photo at two exposures across the fold. - exposure was advertised as keyable but nothing keyed it, so a second lighting preset permanently poisoned the first. - the audio pre-flight could never fire (FastAPI does not route HEAD -> 405). - the two render buttons were not mutually exclusive. - frame guide letterboxed a narrow viewport that the render does not crop. ORCHESTRATOR (round 3): dangling camera cuts are pruned in Timeline.toJSON and skipped by cutCameraAt, rather than trying to keep every undo history clean - deleting a camera spans Stage (no undo) and Timeline (undo), so any older entry can resurrect a cut for a dead camera. Verified against the real validator. Also replaced a VACUOUS test that drained a stack whose only entry was a seeded no-op; timeline_test.mjs now replays both real histories and discriminates. Verified: 4 JS suites + 23 server groups + render client green; blocker repro now preserves the authored rest at every playhead position; save round-trips 200 with zero dangling cuts; zero console errors. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
95 lines
5.8 KiB
JavaScript
95 lines
5.8 KiB
JavaScript
// grammar.js — classic cinema vocabulary as PURE DATA (Lane A owns; every lane reads).
|
||
// No imports, no DOM, no three — safe under plain node. Presets/LLM ops both resolve here,
|
||
// so the button path and the /director path can never drift apart.
|
||
|
||
// Shot sizes: vertical span the framing should cover, as a fraction of subject height,
|
||
// @ where the frame centers on the body (fraction of subject height from the feet).
|
||
export const SHOTS = {
|
||
ECU: { span: 0.12, center: 0.93, label: 'extreme close-up' },
|
||
CU: { span: 0.25, center: 0.87, label: 'close-up' },
|
||
MCU: { span: 0.45, center: 0.75, label: 'medium close-up' },
|
||
MS: { span: 0.65, center: 0.65, label: 'medium shot' },
|
||
MWS: { span: 0.85, center: 0.55, label: 'medium wide' },
|
||
WS: { span: 1.2, center: 0.5, label: 'wide shot' },
|
||
EWS: { span: 2.5, center: 0.5, label: 'extreme wide' },
|
||
};
|
||
|
||
// Camera attitude relative to the aim point. elev = degrees above (+) / below (−) the
|
||
// aim; roll = dutch tilt in degrees; ots = over-the-shoulder of the OTHER subject.
|
||
export const ANGLES = {
|
||
eye: { elev: 0, roll: 0 },
|
||
low: { elev: -18, roll: 0 },
|
||
high: { elev: 22, roll: 0 },
|
||
dutch: { elev: 0, roll: 8 },
|
||
ots_L: { elev: 0, roll: 0, ots: 'L' },
|
||
ots_R: { elev: 0, roll: 0, ots: 'R' },
|
||
};
|
||
|
||
// Full-frame focal lengths (mm). Vertical fov = 2·atan(12/f) — 24mm-tall sensor.
|
||
export const FOCALS = [18, 24, 35, 50, 85];
|
||
|
||
// Lighting looks: key ("sun") color/intensity + boom direction (degrees), hemisphere
|
||
// ambient, a background clear color, and `exposure` — the ACES tone-mapping level for the
|
||
// whole frame. Without it the presets sit ~6 stops apart in key with nothing rebalancing
|
||
// overall level: night/noir/horror read as black holes and day clips. It rides the ambient
|
||
// entity as an ordinary param (applyLight writes it like `bg`) and applyOps emits it as its
|
||
// own generic `light` op so the timeline KEYS it — the renderer's exposure is global, so an
|
||
// unkeyed one would leave the whole earlier stretch of a scene rendering at the later
|
||
// preset's level. Values are what applyLight writes verbatim.
|
||
export const LIGHTS = {
|
||
day: { sun: { color: '#fff6e4', intensity: 3.0, elev: 55, azim: 35 },
|
||
ambient: { color: '#dfe9ff', intensity: 1.15 }, bg: '#8fb0d6', exposure: 1.0 },
|
||
golden_hour: { sun: { color: '#ffb469', intensity: 2.6, elev: 9, azim: -55 },
|
||
ambient: { color: '#ffd9b0', intensity: 0.55 }, bg: '#d98a52', exposure: 1.1 },
|
||
night: { sun: { color: '#7e93d6', intensity: 0.6, elev: 40, azim: 150 },
|
||
ambient: { color: '#31405e', intensity: 0.4 }, bg: '#0a0e1a', exposure: 1.6 },
|
||
noir: { sun: { color: '#ffffff', intensity: 3.4, elev: 52, azim: 75 },
|
||
ambient: { color: '#1c1c24', intensity: 0.15 }, bg: '#08080a', exposure: 1.2 },
|
||
horror: { sun: { color: '#b9d6a4', intensity: 1.7, elev: -30, azim: 10 }, // underlight
|
||
ambient: { color: '#25302b', intensity: 0.25 }, bg: '#05070a', exposure: 1.4 },
|
||
neon: { sun: { color: '#ff4fd8', intensity: 1.9, elev: 25, azim: 120 },
|
||
ambient: { color: '#2de2e6', intensity: 0.9 }, bg: '#150420', exposure: 1.3 },
|
||
overcast: { sun: { color: '#dfe4ea', intensity: 1.2, elev: 70, azim: 0 },
|
||
ambient: { color: '#c7cdd6', intensity: 1.4 }, bg: '#9aa3ad', exposure: 1.0 },
|
||
};
|
||
|
||
// Camera MOVES (M11): each one solves to a START and an END camera state off the same subject.
|
||
// `kind` selects the solver in presets.js. Amounts are FRAME-RELATIVE (multiples of the framed
|
||
// half-height / half-width, which is span·H/2 by definition) so a move reads the same on a
|
||
// close-up and a wide, on an 18mm and an 85mm — never a magic metre count.
|
||
// dolly push/pull along the camera's own view axis; distance of one shot size (rot+fov fixed)
|
||
// truck lateral, camera stays PARALLEL (rot fixed); amount = fraction of the half-frame WIDTH
|
||
// crane vertical, re-aims at the subject; amount = fraction of the half-frame HEIGHT
|
||
// orbit arc around the subject at constant distance; degrees of sweep
|
||
// zoom focal length ONLY — pos+rot identical, fov changes (a zoom is not a dolly)
|
||
// dir: −1 = camera-left / down, +1 = camera-right / up. step: rungs along SHOT_ORDER / FOCALS.
|
||
export const MOVES = {
|
||
push_in: { kind:'dolly', step: 1, label:'push in' },
|
||
pull_out: { kind:'dolly', step: -1, label:'pull out' },
|
||
truck_L: { kind:'truck', dir: -1, amount: 0.55, label:'truck left' },
|
||
truck_R: { kind:'truck', dir: 1, amount: 0.55, label:'truck right' },
|
||
crane_up: { kind:'crane', dir: 1, amount: 1.2, label:'crane up' },
|
||
crane_down: { kind:'crane', dir: -1, amount: 1.2, label:'crane down' },
|
||
orbit_L: { kind:'orbit', dir: -1, degrees: 25, label:'orbit left' },
|
||
orbit_R: { kind:'orbit', dir: 1, degrees: 25, label:'orbit right' },
|
||
zoom_in: { kind:'zoom', step: 1, label:'zoom in' },
|
||
zoom_out: { kind:'zoom', step: -1, label:'zoom out' },
|
||
};
|
||
|
||
// Shot ladder, wide → tight. push_in/pull_out step one rung along it; ends clamp (see DOLLY_CLAMP).
|
||
export const SHOT_ORDER = ['EWS', 'WS', 'MWS', 'MS', 'MCU', 'CU', 'ECU'];
|
||
|
||
// Move timing: `inout` for anything with a human on the wheel, `linear` for mechanical/motion-control.
|
||
export const EASES = ['inout', 'linear'];
|
||
export const MOVE_MAX_DUR = 6; // seconds — default duration is the scene's remainder, capped here
|
||
export const DOLLY_CLAMP = 1.4; // already at ECU/EWS → dolly this factor closer (÷) / farther (×)
|
||
|
||
// Blocking marks, stage-relative. pos marks move (keep current y); face marks rotate.
|
||
export const MARKS = {
|
||
center: { pos: [0, 0, 0] },
|
||
two_shot_L: { pos: [-0.7, 0, 0] },
|
||
two_shot_R: { pos: [0.7, 0, 0] },
|
||
face_camera: { face: 'camera' },
|
||
face_other: { face: 'other' },
|
||
};
|