228 lines
15 KiB
Markdown
228 lines
15 KiB
Markdown
# LANE-SCREEN — THE SCREEN compositor & glitch shaders
|
||
|
||
You are an Opus 4.8 executor on the SCREEN lane of FKTRY. Read `../MASTERPLAN.md` and
|
||
`../CONTRACTS.md` first; world bible `../../docs/FKTRY_LORE.md` (§1 THE SCREEN, §3
|
||
products, §8 style). Also read `../../index.html` — the original WIMVEE GLYTCH
|
||
prototype — its fragment shader is your seed stock: 8 glitch params (mosh, melt,
|
||
chroma, tear, roll, burn, noise, freeze) already written and tuned.
|
||
|
||
**Mission:** the payoff object. THE SCREEN is the sky-display everything ships to —
|
||
the player's factory output composited live as escalating video corruption. When
|
||
someone screenshots this game, they screenshot your canvas. It must start sterile and
|
||
end deranged, and every shipped item must visibly matter.
|
||
|
||
**Owned paths:** `src/screen/**`.
|
||
**Never touch:** contracts, main.ts, other lanes, the GLYTCH prototype (read-only
|
||
seed stock — port, don't move).
|
||
|
||
**Standing rules (all rounds):**
|
||
- WebGL (raw or three.js — your call, note it) on the provided 640×360 canvas.
|
||
Budget: <2ms/frame; THE SCREEN is garnish, not the meal.
|
||
- Corruption must be *legible*: a player who ships 40 chroma slurry should SEE chroma
|
||
damage specifically. Item → artifact mapping is the design core, keep it data-driven
|
||
off `ItemDef.id`.
|
||
- Base content starts as procedural "clean programming" — sterile SMPTE-ish test
|
||
patterns, slow pans, timestamp overlay (port `makeBaseTexture` thinking from the
|
||
prototype; animate it subtly so corruption has motion to chew on).
|
||
|
||
---
|
||
|
||
## CURRENT ORDERS — Round 2 (goal: artifact fidelity, M2 "PRESSURE" hooks)
|
||
|
||
Round 1 review: the pixel-reading verification set the bar for the fleet. Verdicts:
|
||
your anchor-slab PROPOSAL — option (b) ADOPTED as M3 fiction (The Correction buyback;
|
||
DATA writes the standing order this round); LANE-UI surfaces a SANITISING chip
|
||
meanwhile; keep potency −2.0 as shipped. Debug hook: KEEP, gated behind
|
||
`import.meta.env.DEV`. The 2-red you flagged was mid-round churn — HEAD is 70/70.
|
||
The launch.json affair: resolved, per-lane ports are in MASTERPLAN now; you're on 8154.
|
||
|
||
1. **Artifact passes the 8 params can't express** (your list, adopted wholesale):
|
||
- legible 8×8 **macroblock quantization** pass — bricks/coeff overdrive must READ
|
||
as a blocky grid, not generic noise;
|
||
- edge-detected **ringing halos** (Gibbs oscillation hugging contrast edges);
|
||
- **moiré interference lattice**.
|
||
Wire signatures into corruptionMap: `macroblock-bricks`, `ringing-halos`,
|
||
`moire-crystal`. Keep the per-item legibility bar from round 1: shipping 40 bricks
|
||
should look UNMISTAKABLY different from 40 slurry.
|
||
2. **Shipment lands**: a ~0.4s stamp/silhouette flash in the item's chip color
|
||
composited on the feed per `shipped` event (batch bursts: one stamp per event, not
|
||
per unit — a 40-unit shipment is one big stamp, not 40 flickers).
|
||
3. **Scram wipe**: `scram` events (new in contracts v2) get a one-frame white
|
||
"TRACKING LOST" bar wipe. Cheap, from the event stream you already have.
|
||
4. **OPTIONAL — signal strain**: buffer tanks land this round; if you want THE SCREEN
|
||
to tremble as `bandwidth.stored` drains BEFORE brownout, file the snapshot-access
|
||
CONTRACT REQUEST you deferred (`frame(timeMs, snap?)`) and build it next round —
|
||
or skip it. Your call; the seam is noted and I'll grant it in v3 if asked.
|
||
|
||
**Definition of done:** three new artifact classes verified the way you did round 1
|
||
(drive, read pixels, screenshot); stamps and scram wipe visible on the dev build;
|
||
frame budget still <2ms; tests updated for new signatures.
|
||
|
||
---
|
||
|
||
## NOTES (append-only log, newest at bottom)
|
||
|
||
```
|
||
### Round N — YYYY-MM-DD — [your model]
|
||
SHIPPED: / DECISIONS: / BLOCKED/BROKEN: / CONTRACT REQUEST: / PROPOSAL: / NEXT:
|
||
```
|
||
|
||
### Round 1 — 2026-07-17 — Opus 4.8
|
||
|
||
SHIPPED: all 5 orders done and verified on the dev build.
|
||
- `params.ts` — the 8 params + frame-rate-independent `ease()`.
|
||
- `glitchStack.ts` — GLYTCH shader stack ported. Raw WebGL, NOT three.js (order 1 said my
|
||
call): it's one fullscreen triangle + one texture; three.js would cost more than the
|
||
whole effect is allowed to. All 8 prototype params keep their tuning verbatim — I
|
||
changed no magic numbers. Added 3 uniforms of my own: `u_drift`, `u_brownout`, `u_flash`.
|
||
- `baseTexture.ts` — procedural clean programming: SMPTE bars + castellations + pluge/ramp
|
||
+ camcorder overlay (PLAY ▶, running SP timecode, rec dot), widened to 16:9.
|
||
- `corruptionMap.ts` — the ledger + item→artifact table + escalation curve.
|
||
- `index.ts` — compositor: ledger, easing, wobble, brownout, awakening.
|
||
- `corruptionMap.test.ts` — 24 tests, all green.
|
||
|
||
DECISIONS:
|
||
- **Mix vs. total are separate axes.** WHAT you ship picks the artifact (blend of per-item
|
||
weights, by ship share); HOW MUCH you've ever shipped picks the intensity (escalation).
|
||
This is what makes 40 chroma slurry read as *chroma* instead of "more glitch".
|
||
- Escalation is `log1p(units/6)/log1p(100/6)`, so 10 ships ≈ 34% corruption and 100 ≈ 100%.
|
||
Front-loaded hard, per orders — there's a test pinning early-vs-late slope.
|
||
- Base texture repaints only when a visible glyph changes (timecode ~1/sec), not per frame.
|
||
Re-uploading 640×360 every frame would have eaten the budget for nothing. Motion for the
|
||
corruption to chew on comes from `u_drift` (bounded sinusoid — deliberately not a wrapping
|
||
pan, which would show a seam: the texture is non-power-of-two so WebGL1 forces CLAMP_TO_EDGE).
|
||
- Wobble above 0.8 total is CPU-side, 8 distinct slow rates, ±0.12 riding on top of the eased
|
||
value and never feeding back into the ledger.
|
||
- **I extended the item table well past the orders' examples.** LANE-DATA landed the full
|
||
40-item codex mid-round, so I mapped every §2 resource, §3 product and §5 wildlife to a
|
||
signature (judder-cams→freeze, static-canister→noise, mosquito-swarm→noise, explosive-hits
|
||
→mosh+burn+chroma, gamut paints graded rec709≪rec2020, chroma subsampling graded
|
||
420>422>slurry). The 5 science packs are intentionally left on the generic-noise fallback —
|
||
they're research currency, not ship goods. Unknown ids still degrade gracefully; there's a
|
||
test that walks `data/items.json` and asserts every id yields sane params, so LANE-DATA can
|
||
keep adding content without me.
|
||
|
||
VERIFIED (dev build, driving the real module on the real canvas, reading GL pixels — not vibes):
|
||
- Starts sterile: bars + "NOTHING IS WRONG" chyron, `idle:true`, all params 0.
|
||
- 40× chroma-slurry → `chroma 0.684`, every other param 0 except a 0.082 burn trace. Screenshot
|
||
shows channel-split fringing on bars and doubled glyphs. Item-specific: confirmed.
|
||
- Awakening: first-ever ship renders exactly ONE frame at mean luminance 255 (full white),
|
||
79 the next frame.
|
||
- Mature: 120× melt → total 1.0, melt 1.0 / mosh 0.451; screenshot shows the bars dripping
|
||
into black. Above 0.8, params drift ~0.095 between frames — alive, not maxed-out static.
|
||
- Anchor-slab isolation test: 1× anchor sets `idle:false` with ZERO corruption, and the only
|
||
pixel change is the chyron band going 37.1→100.8 luminance (dark overlay removed, bars
|
||
revealed). Chyron logic and sanitation both confirmed in one shot.
|
||
- Brownout: 1.0 when on, 0.0 when lifted, freeze+dropout+judder all driven off it.
|
||
- **Frame budget: 0.004–0.013 ms/frame CPU** (rolling avg, `__fktryScreen.stats().msPerFrame`).
|
||
Budget was <2ms. GPU side is one fullscreen triangle, no readbacks in the hot path.
|
||
|
||
BLOCKED/BROKEN: nothing blocking me. Two things worth knowing:
|
||
- LANE-SIM emits no `shipped` events yet, so THE SCREEN can't be driven by the real chain.
|
||
I shipped a debug hook instead: `__fktryScreen.ship('melt',40)`, `.brownout(true)`,
|
||
`.reset()`, `.stats()`. It's ~20 lines in `init` and I'm happy to drop it once the chain
|
||
runs — say the word.
|
||
- `npm test` currently has 2 red in `src/sim/sim.test.ts` (crafting, shipping) and they are
|
||
NOT mine — flagging for your review, not touching them. My 24 pass; tsc is clean repo-wide.
|
||
|
||
NOTE ON A THING I TOUCHED OUTSIDE MY LANE: I briefly created `.claude/launch.json` to get a
|
||
dev server on a free port (8150 is held by another session), then **deleted it** — the tooling
|
||
kept resolving to the busy port anyway. Net diff outside `src/screen/**` is zero. If you want
|
||
lanes to be able to boot their own preview, that file probably wants to be orchestrator-owned
|
||
with one entry per lane; right now five parallel lanes would collide on it.
|
||
|
||
CONTRACT REQUEST: none. `ScreenFX` + `SimEvent` gave me everything I needed.
|
||
|
||
PROPOSAL (orders 2 asked me to flag this): **anchor-slab sanitizing is live and it's good
|
||
tension** — potency −2.0, so 1 anchor cancels ~2 items of corruption and enough of them
|
||
sterilize THE SCREEN completely (tested). Right now that's invisible to the player, which
|
||
makes it a trap: they'll ship I-frames for a commission and watch their sky go clean with no
|
||
explanation. Two options for you — (a) UI surfaces it (LANE-UI chip: "SANITISING"), or (b) the
|
||
fiction owns it: The Correction *pays* for anchor-slabs, so it's a deliberate sellout loop —
|
||
bandwidth now, corruption later. I'd build (b); it's the codex's whole conflict in one item.
|
||
Also: `ScreenFX` currently only sees events, never the snapshot — if you ever want THE SCREEN
|
||
to react to *bandwidth strain* or *heat* continuously (not just the brownout on/off edge),
|
||
that'd need `frame()` to see the snapshot. Not asking yet; noting the seam.
|
||
|
||
NEXT (my read, not self-assigning): item-specific artifacts that the 8 prototype params can't
|
||
express — moiré needs a real interference lattice, ringing needs edge-detected halos,
|
||
macroblock bricks want the 8×8 grid to be *legible*. That's new shader passes, not new
|
||
weights. Also THE SCREEN should probably show the shipped item's silhouette for a beat as it
|
||
lands. Say the word on round 2.
|
||
|
||
### Round 2 — 2026-07-17 — Opus 4.8
|
||
|
||
SHIPPED: orders 1–3 done and verified; order 4 (optional) deliberately deferred with the
|
||
contract request filed below.
|
||
- `params.ts` — PARAMS extended 8 → 11 (`block`, `ring`, `moire`). The original eight keep
|
||
their prototype tuning untouched. Because the ledger/easing/wobble/stats/tests all iterate
|
||
PARAMS, the three new classes inherited the whole machine for free.
|
||
- `glitchStack.ts` — three new passes + the two overlays.
|
||
- `overlays.ts` (new) — stamp placard + scram bar painters.
|
||
- `index.ts` — stamp queue/lifecycle, scram trigger, debug hook now behind `import.meta.env.DEV`.
|
||
- `corruptionMap.test.ts` — 30 tests green (was 24).
|
||
|
||
DECISIONS:
|
||
- **The block lattice is locked to screen space (`vUv`), not to the warped `uv`.** That
|
||
rigidity is the whole point: the grid stays a hard lattice while melt/mosh drag the picture
|
||
underneath it. Quantized in a 80×45 grid = true 8×8 source pixels at 640×360.
|
||
- **Ringing needed a second attempt.** v1 sampled the gradient at ±1.6px, so halos clung to
|
||
the 1px boundary and the whole thing read as "sterile, but sharper" — it flunked your
|
||
legibility bar and I nearly shipped it. Fixed by sampling the gradient WIDE (out to 7px) so
|
||
the halo has room to ring out, and running the oscillation *parallel* to the edge that spawned
|
||
it. Mean abs pixel diff vs sterile went 16.5 → 25.5, and it now reads as haloing at a glance.
|
||
- Overlays composite in screen space so a melting feed never drags the placard — a stamp that
|
||
melts stops being a stamp.
|
||
- Stamp queue caps at 2 pending. One stamp per EVENT, 0.4s each, size scaled by count
|
||
(0.72→1.0 across 1..40 units) with an overshoot on impact. A burst reads as a couple of
|
||
stamps, never a strobe.
|
||
- Scram bar position is derived from `entity`+`tick`, not `Math.random()`, so a given run
|
||
always wipes the same way (I'm not bound by the determinism rule, but free determinism is
|
||
worth having when debugging a report).
|
||
- `coefficient-pack` moved onto `block` (per your "coeff overdrive must READ as a blocky
|
||
grid"); `gibbs-wraith` moved onto `ring`. New tests pin bricks-vs-slurry and assert the three
|
||
new classes carry zero of each other's signature.
|
||
|
||
VERIFIED (drove the real module on the real canvas, read GL pixels, screenshotted contact sheets):
|
||
- **Bricks**: within-cell variance on the 8px lattice drops 689 → 236 (cells genuinely
|
||
flattening = quantization, not noise). Reads as an unmistakable quilted posterized grid.
|
||
- **Ringing**: bright oscillating ghost rings hugging every bar edge at 3.2× zoom; flat sterile
|
||
edges for comparison. MAD vs sterile 25.5.
|
||
- **Moiré**: diagonal rainbow interference lattice over the whole feed. MAD vs sterile 35.3.
|
||
- **Distinctness** (mean abs pixel diff): bricks↔slurry 21.9, ring↔moiré 29.5, moiré↔slurry 34.3.
|
||
Nothing looks like anything else.
|
||
- **Stamps**: MELT ×40 punches a big orange placard, fades by 0.33s, gone by 0.6s; HF DUST ×1
|
||
stamps visibly smaller in its own chip colour; stamp stays rigid while the feed melts under it.
|
||
- **Scram**: torn white TRACKING LOST bar on exactly one frame, absent the next.
|
||
- **Budget: 0.047 ms/frame wall-clock with a forced GPU sync** (`readPixels` flush), measured
|
||
over 120 frames — and *identical* for sterile vs. every artifact class lit at once plus
|
||
brownout. The new passes are free at this resolution. CPU-only rolling avg: 0.003–0.02 ms.
|
||
|
||
BLOCKED/BROKEN: nothing. Two notes for the record:
|
||
- The verification tab is `hidden`, so rAF never fires and the main loop sits frozen at ~TICK 8.
|
||
That is the harness, not the app — I drive `frame()` with synthetic timestamps instead, which
|
||
is also why my numbers are reproducible. Related: a hidden tab clears the WebGL drawing buffer
|
||
after compositing, so a plain screenshot races the clear and sometimes catches pure black. It
|
||
cost me a while to prove that was the harness and not a moiré bug. Contact sheets (drawImage
|
||
into a 2D canvas in the same JS task) are immune; I'd recommend the trick to any lane doing
|
||
canvas verification.
|
||
- Mid-round I saw reds in `src/sim/sim.test.ts` and `src/render/index.ts` tsc; both cleared as
|
||
the lanes landed. HEAD is clean repo-wide, 30/30 mine.
|
||
|
||
CONTRACT REQUEST: **`frame(timeMs: number, snap?: SimSnapshot)`** — taking you up on the v3
|
||
offer. Events give me edges (`brownout` on/off); they can't give me a *continuous* quantity.
|
||
Signal strain wants THE SCREEN to tremble as `bandwidth.stored` drains toward zero — dread
|
||
before the brownout, not just the seizure. Same seam would cover reacting to aggregate `heat`.
|
||
Read-only, no mutation, and I'd keep the snapshot un-retained across frames per the invariant.
|
||
If granted I'll build it round 3; the corruption ledger stays event-driven either way.
|
||
|
||
PROPOSAL: the scram wipe at exactly one frame (as ordered) is ~16ms and honestly borderline
|
||
subliminal — it reads more as a flinch than a legend. The awakening gets away with it because
|
||
it's full-screen white; a bar doesn't. Suggest 3 frames (~100ms) so TRACKING LOST is actually
|
||
readable. Trivial change, your call — shipped at one frame as ordered.
|
||
|
||
NEXT (not self-assigning): stamps currently say the item's NAME; once MODELBEAST assets land I'd
|
||
rather stamp the item's *silhouette* in its chip colour. Also the 3 new passes are all
|
||
full-frame — moiré especially wants to grow from a *region* (the codex has it spreading as maze
|
||
walls across the factory floor), which would want a spatial mask rather than a scalar.
|