diff --git a/docs/superpowers/specs/2026-06-07-reconciled-simulator-alteration-slice-design.md b/docs/superpowers/specs/2026-06-07-reconciled-simulator-alteration-slice-design.md index 7be4186..103e9ff 100644 --- a/docs/superpowers/specs/2026-06-07-reconciled-simulator-alteration-slice-design.md +++ b/docs/superpowers/specs/2026-06-07-reconciled-simulator-alteration-slice-design.md @@ -273,12 +273,26 @@ calibration. The re-bake is a later offline-pipeline task. ## 8. Open questions (for the plan, not blockers) -- **Calibration curve shape** — the panel exposes the params; the final - `DEFAULT_CALIBRATION` values are settled by eye, not fixed here. -- **Grade vs. Left overlay interaction** — with a runtime overlay the grade need *not* - tint the HUD (unlike the baked path); decide in the renderer whether the Left overlay - sits above or below the grade. Default: **above** (HUD stays legible regardless of - mood). Revisit by eye. +- **Calibration curve shape** — **RESOLVED (session 0010, by eye).** + `DEFAULT_CALIBRATION` is **locked** to unity gains + a linear variant map + (`mood_gain=1.0`, `overlay_gain=1.0`, `right_variant_map=(0,1,2,3,4)`), as a + deliberate choice: with the dark-grade fix below, full knob is peaceful on + every axis (POC + sim), so full tilt = full look and the 5 notches map 1:1 to + the 5 discrete Right bakes. This also closes the **session-0006** convention + question — knobs run 0=off..4=max, equal Dark/Light = identity; no + "centered at 2 = no push." Guarded by `test_default_calibration_is_locked`. +- **Grade vs. Left overlay interaction** — **RESOLVED: overlay above the grade.** + The simulator composites the Left HUD (SVG) above the mood grade and the cool + tint, so the HUD stays legible regardless of mood. The Pi renderer should do + the same. +- **Dark-pole grade look** — **FIXED (session 0010).** The first by-eye pass found + the sim's dark grade used a full-frame `hue-rotate(-200deg)`, which turned the + rock orange and trees purple — the disorienting look rejected in 0008, not the + peaceful POC `dark_frame`. Replaced with darken + slight desaturate on the video + filter plus a `multiply`-blended deep-blue wash (`#tint`) that lifts shadows + toward blue while preserving natural greens. The Pi renderer (later slice) will + do proper grading; this matches the approved POC dark look closely enough to tune + by eye in the sim. - **Crossfade timing in the browser** — a simple opacity crossfade is enough for tuning; the real timing engine is a later slice. - **Placeholder fidelity** — how close the strength-1–3 placeholders should look to real diff --git a/player/alteration.py b/player/alteration.py index 4a1b045..d343367 100644 --- a/player/alteration.py +++ b/player/alteration.py @@ -42,7 +42,25 @@ class Calibration: right_variant_map: tuple = (0, 1, 2, 3, 4) -DEFAULT_CALIBRATION = Calibration() +# The LOCKED calibration (session 0010, 2026-06-07) — settled by eye in the +# simulator, closing the open session-0006 knob->strength decision. Convention: +# every experience knob runs 0 = off .. 4 = max, and equal Dark/Light = identity +# (raw footage); there is no "centered at 2 = no push" coordinate. +# - mood_gain = 1.0: full Dark/Light knob reaches the full mood grade. By-eye +# evidence (POC renders + sim, after the dark-grade fix this session) shows +# full tilt is peaceful on every axis, so no softening is warranted. +# - overlay_gain = 1.0: full Left = opacity 1.0; the HUD is legible, not +# overwhelming, sitting above the grade. +# - right_variant_map linear: the 5 knob notches map 1:1 onto the 5 discrete +# pre-baked Right strengths (0 = raw base). +# These are unity/linear by deliberate choice, not as placeholders. The curve +# stays parameterized so a future re-bake or a different feel is one edit away; +# test_default_calibration_is_locked guards the values from drifting silently. +DEFAULT_CALIBRATION = Calibration( + mood_gain=1.0, + overlay_gain=1.0, + right_variant_map=(0, 1, 2, 3, 4), +) @dataclass(frozen=True) diff --git a/simulator/static/app.js b/simulator/static/app.js index 3a4b77f..f2cd708 100644 --- a/simulator/static/app.js +++ b/simulator/static/app.js @@ -1,7 +1,7 @@ // Thin renderer: post controls+calibration -> RenderPlan; render grade, Right // variant crossfade, and the live Left overlay. All math stays in Python. const $ = (id) => document.getElementById(id); -const vid = $("vid"), overlay = $("overlay"), black = $("black"), readout = $("readout"); +const vid = $("vid"), tint = $("tint"), overlay = $("overlay"), black = $("black"), readout = $("readout"); let clip = null; // active clip manifest entry let currentVariant = -1; // last loaded Right strength @@ -19,12 +19,16 @@ function variantFile(strength) { } function applyGrade(tone) { - // Light: warm + brighten; Dark: cool + darken; 0: raw. + // Light: warm + brighten (sepia). Dark: cool + darken via a multiply-blended + // blue wash (#tint) that lifts shadows toward blue while keeping natural + // greens — the peaceful POC dark look, NOT a full-frame hue spin. const warm = tone > 0 ? tone : 0, cool = tone < 0 ? -tone : 0; - const bright = 1 + 0.25 * tone, sat = 1 + 0.15 * Math.abs(tone); + const bright = 1 + 0.25 * warm - 0.35 * cool; + const sat = 1 + 0.15 * warm - 0.30 * cool; vid.style.filter = - `brightness(${bright}) saturate(${sat}) ` + - `sepia(${(warm * 0.5).toFixed(3)}) hue-rotate(${(-cool * 200).toFixed(0)}deg)`; + `brightness(${bright.toFixed(3)}) saturate(${sat.toFixed(3)}) ` + + `sepia(${(warm * 0.5).toFixed(3)})`; + tint.style.opacity = (cool * 0.6).toFixed(3); } function loadVariant(strength) { diff --git a/simulator/static/index.html b/simulator/static/index.html index c868754..df5eaf8 100644 --- a/simulator/static/index.html +++ b/simulator/static/index.html @@ -12,6 +12,7 @@
+
diff --git a/simulator/static/style.css b/simulator/static/style.css index 66daf67..5d6bd46 100644 --- a/simulator/static/style.css +++ b/simulator/static/style.css @@ -7,6 +7,9 @@ main { display: flex; gap: 1rem; padding: 1rem; flex-wrap: wrap; } .screen { position: relative; width: 100%; aspect-ratio: 16 / 9; background: #000; border-radius: 6px; overflow: hidden; } #vid { width: 100%; height: 100%; object-fit: cover; transition: opacity 0.15s ease; } +#tint { position: absolute; inset: 0; pointer-events: none; opacity: 0; + background: #28425f; mix-blend-mode: multiply; + transition: opacity 0.2s ease; } #overlay { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; transition: opacity 0.2s ease; } .anno-box { fill: none; stroke: #6cf; stroke-width: 0.4; vector-effect: non-scaling-stroke; } diff --git a/tests/test_player_alteration.py b/tests/test_player_alteration.py index 01509f4..051916d 100644 --- a/tests/test_player_alteration.py +++ b/tests/test_player_alteration.py @@ -77,6 +77,16 @@ def test_whole_brain_dark_corner_stacks_grade_substrate_and_overlay(): assert not plan.is_identity +def test_default_calibration_is_locked(): + # Session 0010: the knob->strength calibration is LOCKED to these values, + # settled by eye in the simulator (closes the open session-0006 decision). + # This pins the literal constants so they can't drift silently; changing the + # locked feel is a deliberate edit here + in alteration.py. + assert DEFAULT_CALIBRATION.mood_gain == 1.0 + assert DEFAULT_CALIBRATION.overlay_gain == 1.0 + assert DEFAULT_CALIBRATION.right_variant_map == (0, 1, 2, 3, 4) + + def test_default_calibration_is_behavior_preserving(): # DEFAULT_CALIBRATION must reproduce the original three helpers exactly. for left in range(5):