[screen] round 6: repairs ease the ledger + green-tick placards; hospital-white mite sterility creep
- repaired events decrement the ledger by one unit of the item (theft, not boredom-decay) and stamp a small perfect green-tick placard — The Correction signs its work; kept distinct from the crooked chip-coloured shipment stamp - parity-mite activity grows a hospital-white sterile patch via growthMask (its 3rd/4th customer): desaturate + lift to clinical white + faint tile grid, the inverse of corruption and meant to feel worse - DEV hook: .mite() .repair() plus .frame()/.canvas() pixel-verification drivers - +3 repair-semantics tests (screen suite 33); order-3 STANDARD lines are DATA's copy, my chyron wraps them (verified live, no clipping) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
eafd31f263
commit
32ff942fd5
@ -503,3 +503,84 @@ NEXT (not self-assigning): the tuner has no UI yet — LANE-UI's dock is what ma
|
|||||||
findable at all, and until then the radio is only reachable from the console. Also `growthMask`
|
findable at all, and until then the radio is only reachable from the console. Also `growthMask`
|
||||||
now has two customers (moiré, wildlife) and is ready for a third: HF dust piles are regional in
|
now has two customers (moiré, wildlife) and is ready for a third: HF dust piles are regional in
|
||||||
the codex and `dust-pile` wildlife already arrives in the same event I'm consuming.
|
the codex and `dust-pile` wildlife already arrives in the same event I'm consuming.
|
||||||
|
|
||||||
|
### Round 6 — 2026-07-20 — Opus 4.8
|
||||||
|
|
||||||
|
SHIPPED: all 3 orders. The sky now notices The Correction. `growthMask` gained its third and
|
||||||
|
fourth customers (order 3 round-3 groundwork paying off — no new shader machinery, new weights).
|
||||||
|
- `overlays.ts` — `paintRepair()` + `repairCanvas` (224×112). The Correction's uniform: hospital
|
||||||
|
white, faction green tick, machined square. Deliberately the inverse of the shipment stamp
|
||||||
|
(hand-thumped, crooked, chip-coloured, "SHIPPED / NO QUESTIONS").
|
||||||
|
- `glitchStack.ts` — two additions: the **mite sterility** pass (`u_mite`/`u_miteSeed`, reusing
|
||||||
|
`growthMask`) and the **repair placard** composite (`u_repair`, texture unit 5). The mite pass
|
||||||
|
sits on TOP of the corruption stack because it is *erasure*, not another artifact.
|
||||||
|
- `index.ts` — `repaired` events ease the ledger and queue a placard; parity-mite tracking drives
|
||||||
|
the sterile creep (snapshot truth / event fallback, same pattern as the mosquito swarms); repair
|
||||||
|
placard lifecycle in its own slot. DEV hook gained `.mite()`, `.repair()`, and — new — `.frame()`
|
||||||
|
+ `.canvas()` drivers (see the harness note below).
|
||||||
|
- `corruptionMap.test.ts` — +3 tests pinning the repair semantics. Screen suite 33 (was 30);
|
||||||
|
my two lanes together 81 green (screen 72 + audio band 9).
|
||||||
|
|
||||||
|
DECISIONS:
|
||||||
|
- **Repairs are THEFT; boredom is ROT — kept structurally distinct, per the order.** A `repaired`
|
||||||
|
event decrements the *ledger* by exactly one unit of the item (units drop by that item's
|
||||||
|
potency — pixel-verified: melt ×20 → repair → units 24.0→22.8, tally 20→19). Boredom only fades
|
||||||
|
the *display* and leaves the tally intact (recoverable). So a factory being eaten by mites
|
||||||
|
permanently loses corruption it can never fade back, while an idle factory snaps straight back
|
||||||
|
the moment it ships. I deliberately do NOT refresh the boredom clock on a repair — a factory
|
||||||
|
being quietly corrected is not a factory that is producing.
|
||||||
|
- **The mite patch is the inverse of corruption and it reads WORSE, as ordered.** Where the
|
||||||
|
mosquito shimmer *darkens* a region with crawling noise, the mite patch *desaturates and lifts
|
||||||
|
toward clinical white* with a faint manufactured tile grid. Pixel-verified against a heavy melt
|
||||||
|
feed: the bloom region went luminance 22.9→168.8 and saturation 0.695→0.025. A dead, too-clean
|
||||||
|
hole in your rot is more unsettling than more rot — the codex's whole point about The Correction.
|
||||||
|
- **The repair placard parks low-right (vUv 0.80,0.80), small (0.15×0.082 half-size), 0.55s** so it
|
||||||
|
never fights the centre shipment stamp — a burst of ships and a burst of corrections can co-occur
|
||||||
|
and stay legible as two separate voices.
|
||||||
|
- Both new spatial effects reuse `growthMask` verbatim (round-3 template): moiré, mosquito, AND
|
||||||
|
now mite sterility all share one fbm growth front. Zero new shader machinery for order 2.
|
||||||
|
|
||||||
|
VERIFIED (drove the REAL module on the REAL 640×360 canvas, synthetic timebase, GL pixel reads —
|
||||||
|
the harness tab is `hidden` so rAF is throttled, the round-2/3 gotcha; I pump `frame()` and
|
||||||
|
`drawImage` in the same JS task):
|
||||||
|
- **Repair**: units eased by exactly melt's potency (1.2); placard region flips corrupted
|
||||||
|
(lum 68) → hospital-white placard (rgb 228/232/228, lum 230). repair flag + tally decrement both
|
||||||
|
confirmed.
|
||||||
|
- **Mite creep**: localized bloom (grid cell 3,4 — NOT the whole frame), lum +146, saturation
|
||||||
|
→0.025. `mite` eases to 0.83 from size 0.85. Both source-of-truth paths exercised: event
|
||||||
|
fallback (`.mite()`) AND `snapshot.wildlife` parity-mite.
|
||||||
|
- **Chyron / order 3**: DATA landed 8 mite-acknowledging STANDARD lines (30→38). Pixel-checked the
|
||||||
|
live wrap: short lines 1 row, the 96-char lines wrap to exactly 2 rows, rightmost glyph at x613
|
||||||
|
inside the 640 canvas — no clipping. Word-wrap (orchestrator's fix) intact; I did not touch it.
|
||||||
|
The mumble is untouched — I never edited `amPropaganda`; it derives syllables from the line text.
|
||||||
|
- **Budget: 0.034 ms/frame** worst case — 2000 entities, corruption total 1.0, strain 0.94 +
|
||||||
|
fever 0.57 + pallor 0.82 + mosquito 0.89 + mite 0.89 + brownout, every param lit. ~59× under the
|
||||||
|
2ms budget. The two new shader branches and one new texture are free at this resolution.
|
||||||
|
|
||||||
|
BLOCKED/BROKEN: nothing in my lane.
|
||||||
|
- **Transient full-suite red, NOT mine (round-4 lesson holds).** A combined `npm test` briefly
|
||||||
|
showed 3 reds — `band.test.ts` (STANDARD line >96 chars), `sim/mite.test.ts`, and
|
||||||
|
`sim/reference.test.ts`. All three are the shared-tree race: DATA was mid-save on
|
||||||
|
`data/stations.json` (I caught it at exactly 96 chars, its own budget) and SIM has uncommitted
|
||||||
|
`mite.test.ts` + `constants.ts`/`index.ts` edits in flight. Re-running JUST my lanes: 81/81
|
||||||
|
green. I did NOT bare-`git stash` to chase it (my own round-4 near-miss).
|
||||||
|
- HEAD advanced under me twice mid-round (a `[ui] round 6` commit landed while I worked). Re-probed
|
||||||
|
before writing this; all claims above are against the tree as it stands now.
|
||||||
|
|
||||||
|
HARNESS NOTE: I added DEV-only `.frame(ms, snap)` and `.canvas()` to `__fktryScreen`. Round 2/3
|
||||||
|
both lamented having no clean way to pump the real module in a throttled hidden tab; this makes
|
||||||
|
pixel verification a two-line console call for any lane. Stripped from prod (`import.meta.env.DEV`).
|
||||||
|
|
||||||
|
CONTRACT REQUEST: none. `repaired` + `parity-mite` (v8 contracts) gave me everything.
|
||||||
|
|
||||||
|
COORDINATION (order 3, resolved): DATA owns the STANDARD copy; I own its rendering. The 8 new
|
||||||
|
lines are theirs and are already in `data/stations.json`. My only ask of them is a standing one:
|
||||||
|
please keep STANDARD lines ≤96 chars — `band.test.ts` asserts it and the chyron wraps to 2 lines
|
||||||
|
at that budget; a longer line spills to a 3rd line (the font steps down to 13px) but starts to
|
||||||
|
crowd the feed. The newest line is exactly 96, so we're at the edge.
|
||||||
|
|
||||||
|
PROPOSAL: the repair placard and the ratification card both wear the green tick — that's
|
||||||
|
intentional (one faction, one uniform), but a player seeing their first repair might read the
|
||||||
|
small placard as "another commission done" rather than "something took my work". If that lands
|
||||||
|
ambiguous in playtest, the fix is a one-word change to the placard legend ("CORRECTED" is already
|
||||||
|
doing the work, but "REVERTED" or "RECLAIMED" is colder). Flagging, not pre-emptively changing.
|
||||||
|
|||||||
@ -145,6 +145,35 @@ describe('anchor slabs sanitize', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('repairs ease the ledger by one unit of the item (The Correction, v6)', () => {
|
||||||
|
// A `repaired` event decrements the tally by one unit in index.ts; this pins the CONTRACT
|
||||||
|
// that decrement is built on — "eases total corruption by its item's contribution".
|
||||||
|
it("removing one unit eases net units by exactly that item's per-unit contribution", () => {
|
||||||
|
const before = resolveLedger(tally({ melt: 20 }));
|
||||||
|
const after = resolveLedger(tally({ melt: 19 }));
|
||||||
|
expect(before.units - after.units).toBeCloseTo(corruptionFor('melt').potency, 5);
|
||||||
|
expect(after.total).toBeLessThan(before.total);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('correcting a hard-hitting item eases more than correcting a weak one', () => {
|
||||||
|
const heavy =
|
||||||
|
resolveLedger(tally({ 'explosive-hits': 20 })).units -
|
||||||
|
resolveLedger(tally({ 'explosive-hits': 19 })).units;
|
||||||
|
const light =
|
||||||
|
resolveLedger(tally({ 'gamut-paint-rec709': 20 })).units -
|
||||||
|
resolveLedger(tally({ 'gamut-paint-rec709': 19 })).units;
|
||||||
|
expect(heavy).toBeGreaterThan(light);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('a repair never drives corruption below sterile (theft floors at clean)', () => {
|
||||||
|
// ship one, correct one → back to the NOTHING IS WRONG floor, never past it.
|
||||||
|
const { units, total, targets } = resolveLedger(tally({ melt: 0 }));
|
||||||
|
expect(units).toBe(0);
|
||||||
|
expect(total).toBe(0);
|
||||||
|
for (const p of PARAMS) expect(targets[p]).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('param hygiene', () => {
|
describe('param hygiene', () => {
|
||||||
it('never emits a param outside 0..1, even shipping everything at once', () => {
|
it('never emits a param outside 0..1, even shipping everything at once', () => {
|
||||||
const everything = tally({
|
const everything = tally({
|
||||||
|
|||||||
@ -34,8 +34,11 @@ uniform float u_strain, u_fever, u_pallor;
|
|||||||
uniform vec2 u_moireSeed;
|
uniform vec2 u_moireSeed;
|
||||||
uniform float u_wildlife;
|
uniform float u_wildlife;
|
||||||
uniform vec2 u_wildlifeSeed;
|
uniform vec2 u_wildlifeSeed;
|
||||||
uniform sampler2D u_stampTex, u_scramTex, u_cardTex, u_titleTex;
|
uniform float u_mite;
|
||||||
|
uniform vec2 u_miteSeed;
|
||||||
|
uniform sampler2D u_stampTex, u_scramTex, u_cardTex, u_titleTex, u_repairTex;
|
||||||
uniform float u_stampAmt, u_stampScale, u_scram, u_scramY, u_card, u_title;
|
uniform float u_stampAmt, u_stampScale, u_scram, u_scramY, u_card, u_title;
|
||||||
|
uniform float u_repair, u_repairScale;
|
||||||
|
|
||||||
float hash(vec2 p){ return fract(sin(dot(p, vec2(127.1,311.7)))*43758.5453); }
|
float hash(vec2 p){ return fract(sin(dot(p, vec2(127.1,311.7)))*43758.5453); }
|
||||||
float vnoise(vec2 p){
|
float vnoise(vec2 p){
|
||||||
@ -225,6 +228,22 @@ void main(){
|
|||||||
col *= 1.0 - vig*u_pallor*0.75;
|
col *= 1.0 - vig*u_pallor*0.75;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- mite sterility: The Correction's hospital-white cleanliness creeping outward from heavy
|
||||||
|
// parity-mite activity. Same growth-mask template as moire/wildlife, but the INVERSE of
|
||||||
|
// corruption — it desaturates and washes the region toward clinical white with a faint
|
||||||
|
// manufactured tile grid. It sits on TOP of the corruption stack because it is erasure: your
|
||||||
|
// mess is yours, this is your work being quietly deleted. Meant to feel WORSE than rot.
|
||||||
|
if(u_mite > 0.001){
|
||||||
|
float mmask = growthMask(vUv, u_miteSeed, u_mite*0.7) * clamp(u_mite*1.7, 0.0, 1.0);
|
||||||
|
if(mmask > 0.001){
|
||||||
|
vec3 sterile = mix(vec3(lu(col)), vec3(0.93,0.96,0.95), 0.72); // desaturate → cool white
|
||||||
|
sterile = mix(sterile, vec3(0.98), 0.35); // lift toward clinical bright
|
||||||
|
float tile = max(step(0.93, fract(vUv.x*64.0)), step(0.93, fract(vUv.y*36.0)));
|
||||||
|
sterile = mix(sterile, sterile*0.9, tile*0.6); // faint manufactured grid
|
||||||
|
col = mix(col, sterile, mmask*0.9);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// faint scanlines for vibe (constant, not scored)
|
// faint scanlines for vibe (constant, not scored)
|
||||||
col *= 0.92 + 0.08 * sin(vUv.y * 360.0 * 3.1415);
|
col *= 0.92 + 0.08 * sin(vUv.y * 360.0 * 3.1415);
|
||||||
|
|
||||||
@ -238,6 +257,17 @@ void main(){
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- repair placard: The Correction signs one unit off the ledger. Small, perfect, screen
|
||||||
|
// space (uncorrupted), parked low-right so it never fights the shipment stamp for the centre.
|
||||||
|
if(u_repair > 0.001){
|
||||||
|
vec2 halfSize = vec2(0.150, 0.082)*u_repairScale;
|
||||||
|
vec2 ruv = (vUv - vec2(0.80, 0.80))/(2.0*halfSize) + 0.5;
|
||||||
|
if(ruv.x > 0.0 && ruv.x < 1.0 && ruv.y > 0.0 && ruv.y < 1.0){
|
||||||
|
vec4 rp = texture2D(u_repairTex, ruv);
|
||||||
|
col = mix(col, rp.rgb, rp.a*u_repair);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---- scram: the tape loses tracking for exactly one frame
|
// ---- scram: the tape loses tracking for exactly one frame
|
||||||
if(u_scram > 0.5){
|
if(u_scram > 0.5){
|
||||||
float halfH = 0.085;
|
float halfH = 0.085;
|
||||||
@ -268,9 +298,9 @@ void main(){
|
|||||||
|
|
||||||
type UniformKey =
|
type UniformKey =
|
||||||
| Param | 't' | 'drift' | 'brownout' | 'flash' | 'strain' | 'fever' | 'pallor' | 'moireSeed'
|
| Param | 't' | 'drift' | 'brownout' | 'flash' | 'strain' | 'fever' | 'pallor' | 'moireSeed'
|
||||||
| 'wildlife' | 'wildlifeSeed'
|
| 'wildlife' | 'wildlifeSeed' | 'mite' | 'miteSeed'
|
||||||
| 'stampTex' | 'scramTex' | 'cardTex' | 'titleTex'
|
| 'stampTex' | 'scramTex' | 'cardTex' | 'titleTex' | 'repairTex'
|
||||||
| 'stampAmt' | 'stampScale' | 'scram' | 'scramY' | 'card' | 'title';
|
| 'stampAmt' | 'stampScale' | 'scram' | 'scramY' | 'card' | 'title' | 'repair' | 'repairScale';
|
||||||
|
|
||||||
export interface DrawState {
|
export interface DrawState {
|
||||||
params: ParamSet;
|
params: ParamSet;
|
||||||
@ -297,6 +327,10 @@ export interface DrawState {
|
|||||||
wildlife: number;
|
wildlife: number;
|
||||||
/** where the swarm shimmer grows from, in screen uv */
|
/** where the swarm shimmer grows from, in screen uv */
|
||||||
wildlifeSeed: [number, number];
|
wildlifeSeed: [number, number];
|
||||||
|
/** 0..1 hospital-white sterile creep around heavy parity-mite activity (the inverse of rot) */
|
||||||
|
mite: number;
|
||||||
|
/** where the sterile patch grows from, in screen uv */
|
||||||
|
miteSeed: [number, number];
|
||||||
/** 0..1 opacity of the ratification card */
|
/** 0..1 opacity of the ratification card */
|
||||||
card: number;
|
card: number;
|
||||||
/** set when the card canvas has been repainted and needs re-upload */
|
/** set when the card canvas has been repainted and needs re-upload */
|
||||||
@ -315,6 +349,12 @@ export interface DrawState {
|
|||||||
scramY: number;
|
scramY: number;
|
||||||
/** set when the stamp canvas has been repainted and needs re-upload */
|
/** set when the stamp canvas has been repainted and needs re-upload */
|
||||||
stampDirty: boolean;
|
stampDirty: boolean;
|
||||||
|
/** 0..1 opacity of the repair placard */
|
||||||
|
repair: number;
|
||||||
|
/** repair placard size multiplier (slight overshoot on impact, like the stamp) */
|
||||||
|
repairScale: number;
|
||||||
|
/** set when the repair canvas has been repainted and needs re-upload */
|
||||||
|
repairDirty: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GlitchStack {
|
export interface GlitchStack {
|
||||||
@ -378,11 +418,13 @@ export function createGlitchStack(
|
|||||||
overlays.paintStamp('', 0, '#000'); // allocate the stamp texture at full size up front
|
overlays.paintStamp('', 0, '#000'); // allocate the stamp texture at full size up front
|
||||||
overlays.paintCard('reel', '');
|
overlays.paintCard('reel', '');
|
||||||
overlays.paintTitle();
|
overlays.paintTitle();
|
||||||
|
overlays.paintRepair(''); // allocate the repair placard texture up front
|
||||||
const baseTex = makeTex(0, base.canvas);
|
const baseTex = makeTex(0, base.canvas);
|
||||||
const stampTex = makeTex(1, overlays.stampCanvas);
|
const stampTex = makeTex(1, overlays.stampCanvas);
|
||||||
makeTex(2, overlays.scramCanvas);
|
makeTex(2, overlays.scramCanvas);
|
||||||
const cardTex = makeTex(3, overlays.cardCanvas);
|
const cardTex = makeTex(3, overlays.cardCanvas);
|
||||||
const titleTex = makeTex(4, overlays.titleCanvas);
|
const titleTex = makeTex(4, overlays.titleCanvas);
|
||||||
|
const repairTex = makeTex(5, overlays.repairCanvas);
|
||||||
|
|
||||||
const uni = {} as Record<UniformKey, WebGLUniformLocation | null>;
|
const uni = {} as Record<UniformKey, WebGLUniformLocation | null>;
|
||||||
uni.t = gl.getUniformLocation(prog, 't');
|
uni.t = gl.getUniformLocation(prog, 't');
|
||||||
@ -397,10 +439,14 @@ export function createGlitchStack(
|
|||||||
uni.moireSeed = gl.getUniformLocation(prog, 'u_moireSeed');
|
uni.moireSeed = gl.getUniformLocation(prog, 'u_moireSeed');
|
||||||
uni.wildlife = gl.getUniformLocation(prog, 'u_wildlife');
|
uni.wildlife = gl.getUniformLocation(prog, 'u_wildlife');
|
||||||
uni.wildlifeSeed = gl.getUniformLocation(prog, 'u_wildlifeSeed');
|
uni.wildlifeSeed = gl.getUniformLocation(prog, 'u_wildlifeSeed');
|
||||||
|
uni.mite = gl.getUniformLocation(prog, 'u_mite');
|
||||||
|
uni.miteSeed = gl.getUniformLocation(prog, 'u_miteSeed');
|
||||||
uni.stampAmt = gl.getUniformLocation(prog, 'u_stampAmt');
|
uni.stampAmt = gl.getUniformLocation(prog, 'u_stampAmt');
|
||||||
uni.stampScale = gl.getUniformLocation(prog, 'u_stampScale');
|
uni.stampScale = gl.getUniformLocation(prog, 'u_stampScale');
|
||||||
uni.scram = gl.getUniformLocation(prog, 'u_scram');
|
uni.scram = gl.getUniformLocation(prog, 'u_scram');
|
||||||
uni.scramY = gl.getUniformLocation(prog, 'u_scramY');
|
uni.scramY = gl.getUniformLocation(prog, 'u_scramY');
|
||||||
|
uni.repair = gl.getUniformLocation(prog, 'u_repair');
|
||||||
|
uni.repairScale = gl.getUniformLocation(prog, 'u_repairScale');
|
||||||
for (const p of PARAMS) uni[p] = gl.getUniformLocation(prog, `u_${p}`);
|
for (const p of PARAMS) uni[p] = gl.getUniformLocation(prog, `u_${p}`);
|
||||||
|
|
||||||
gl.uniform1i(gl.getUniformLocation(prog, 'tex'), 0);
|
gl.uniform1i(gl.getUniformLocation(prog, 'tex'), 0);
|
||||||
@ -408,13 +454,16 @@ export function createGlitchStack(
|
|||||||
gl.uniform1i(gl.getUniformLocation(prog, 'u_scramTex'), 2);
|
gl.uniform1i(gl.getUniformLocation(prog, 'u_scramTex'), 2);
|
||||||
gl.uniform1i(gl.getUniformLocation(prog, 'u_cardTex'), 3);
|
gl.uniform1i(gl.getUniformLocation(prog, 'u_cardTex'), 3);
|
||||||
gl.uniform1i(gl.getUniformLocation(prog, 'u_titleTex'), 4);
|
gl.uniform1i(gl.getUniformLocation(prog, 'u_titleTex'), 4);
|
||||||
|
gl.uniform1i(gl.getUniformLocation(prog, 'u_repairTex'), 5);
|
||||||
|
|
||||||
gl.viewport(0, 0, canvas.width, canvas.height);
|
gl.viewport(0, 0, canvas.width, canvas.height);
|
||||||
gl.uniform1f(uni.drift, 1);
|
gl.uniform1f(uni.drift, 1);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
draw({ params, timeSec, drift, brownout, flash, era, chyron, strain, fever, pallor,
|
draw({ params, timeSec, drift, brownout, flash, era, chyron, strain, fever, pallor,
|
||||||
moireSeed, wildlife, wildlifeSeed, stampAmt, stampScale, scram, scramY, stampDirty,
|
moireSeed, wildlife, wildlifeSeed, mite, miteSeed,
|
||||||
|
stampAmt, stampScale, scram, scramY, stampDirty,
|
||||||
|
repair, repairScale, repairDirty,
|
||||||
card, cardDirty, title, titleDirty }) {
|
card, cardDirty, title, titleDirty }) {
|
||||||
// Re-upload the clean feed only when it actually changed (timecode tick / era / chyron).
|
// Re-upload the clean feed only when it actually changed (timecode tick / era / chyron).
|
||||||
if (base.update({ timeMs: timeSec * 1000, era, chyron })) {
|
if (base.update({ timeMs: timeSec * 1000, era, chyron })) {
|
||||||
@ -437,6 +486,11 @@ export function createGlitchStack(
|
|||||||
gl.bindTexture(gl.TEXTURE_2D, titleTex);
|
gl.bindTexture(gl.TEXTURE_2D, titleTex);
|
||||||
gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, gl.RGBA, gl.UNSIGNED_BYTE, overlays.titleCanvas);
|
gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, gl.RGBA, gl.UNSIGNED_BYTE, overlays.titleCanvas);
|
||||||
}
|
}
|
||||||
|
if (repairDirty) {
|
||||||
|
gl.activeTexture(gl.TEXTURE0 + 5);
|
||||||
|
gl.bindTexture(gl.TEXTURE_2D, repairTex);
|
||||||
|
gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, gl.RGBA, gl.UNSIGNED_BYTE, overlays.repairCanvas);
|
||||||
|
}
|
||||||
gl.uniform1f(uni.t, timeSec);
|
gl.uniform1f(uni.t, timeSec);
|
||||||
gl.uniform1f(uni.drift, drift);
|
gl.uniform1f(uni.drift, drift);
|
||||||
gl.uniform1f(uni.brownout, brownout);
|
gl.uniform1f(uni.brownout, brownout);
|
||||||
@ -449,10 +503,14 @@ export function createGlitchStack(
|
|||||||
gl.uniform2f(uni.moireSeed, moireSeed[0], moireSeed[1]);
|
gl.uniform2f(uni.moireSeed, moireSeed[0], moireSeed[1]);
|
||||||
gl.uniform1f(uni.wildlife, wildlife);
|
gl.uniform1f(uni.wildlife, wildlife);
|
||||||
gl.uniform2f(uni.wildlifeSeed, wildlifeSeed[0], wildlifeSeed[1]);
|
gl.uniform2f(uni.wildlifeSeed, wildlifeSeed[0], wildlifeSeed[1]);
|
||||||
|
gl.uniform1f(uni.mite, mite);
|
||||||
|
gl.uniform2f(uni.miteSeed, miteSeed[0], miteSeed[1]);
|
||||||
gl.uniform1f(uni.stampAmt, stampAmt);
|
gl.uniform1f(uni.stampAmt, stampAmt);
|
||||||
gl.uniform1f(uni.stampScale, stampScale);
|
gl.uniform1f(uni.stampScale, stampScale);
|
||||||
gl.uniform1f(uni.scram, scram ? 1 : 0);
|
gl.uniform1f(uni.scram, scram ? 1 : 0);
|
||||||
gl.uniform1f(uni.scramY, scramY);
|
gl.uniform1f(uni.scramY, scramY);
|
||||||
|
gl.uniform1f(uni.repair, repair);
|
||||||
|
gl.uniform1f(uni.repairScale, repairScale);
|
||||||
for (const p of PARAMS) gl.uniform1f(uni[p], params[p]);
|
for (const p of PARAMS) gl.uniform1f(uni[p], params[p]);
|
||||||
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
||||||
},
|
},
|
||||||
|
|||||||
@ -31,6 +31,8 @@ const WOBBLE_DEPTH = 0.12;
|
|||||||
const WOBBLE_RATE = [0.13, 0.19, 0.11, 0.23, 0.17, 0.29, 0.31, 0.07, 0.09, 0.37, 0.05];
|
const WOBBLE_RATE = [0.13, 0.19, 0.11, 0.23, 0.17, 0.29, 0.31, 0.07, 0.09, 0.37, 0.05];
|
||||||
/** Seconds a shipment stamp stays on the feed. */
|
/** Seconds a shipment stamp stays on the feed. */
|
||||||
const STAMP_LIFE = 0.4;
|
const STAMP_LIFE = 0.4;
|
||||||
|
/** The repair placard lingers a touch longer than a shipment stamp — a signature, read at leisure. */
|
||||||
|
const REPAIR_LIFE = 0.55;
|
||||||
/** A burst of shipments must not become a strobe: at most this many stamps wait their turn. */
|
/** A burst of shipments must not become a strobe: at most this many stamps wait their turn. */
|
||||||
const STAMP_QUEUE_MAX = 2;
|
const STAMP_QUEUE_MAX = 2;
|
||||||
/** Round 3: one frame was a flinch, not a legend. Three frames is readable. */
|
/** Round 3: one frame was a flinch, not a legend. Three frames is readable. */
|
||||||
@ -53,6 +55,7 @@ const TITLE_FADE = 0.6;
|
|||||||
const FLASH_SECONDS = 0.5;
|
const FLASH_SECONDS = 0.5;
|
||||||
|
|
||||||
interface Stamp { name: string; count: number; color: string; age: number }
|
interface Stamp { name: string; count: number; color: string; age: number }
|
||||||
|
interface Repair { name: string; age: number }
|
||||||
interface Swarm { pos: { x: number; y: number }; size: number }
|
interface Swarm { pos: { x: number; y: number }; size: number }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -120,6 +123,9 @@ export function createScreenFX(): ScreenFX {
|
|||||||
const stampQueue: Stamp[] = [];
|
const stampQueue: Stamp[] = [];
|
||||||
let stamp: Stamp | null = null;
|
let stamp: Stamp | null = null;
|
||||||
let stampDirty = false;
|
let stampDirty = false;
|
||||||
|
const repairQueue: Repair[] = [];
|
||||||
|
let repair: Repair | null = null;
|
||||||
|
let repairDirty = false;
|
||||||
let scramFrames = 0;
|
let scramFrames = 0;
|
||||||
let scramY = 0.5;
|
let scramY = 0.5;
|
||||||
|
|
||||||
@ -143,6 +149,9 @@ export function createScreenFX(): ScreenFX {
|
|||||||
let wildlife = 0; // eased regional-shimmer amount
|
let wildlife = 0; // eased regional-shimmer amount
|
||||||
let wildlifeSeed: [number, number] = [0.5, 0.5];
|
let wildlifeSeed: [number, number] = [0.5, 0.5];
|
||||||
const swarms = new Map<number, Swarm>(); // event-tracked fallback when no snapshot
|
const swarms = new Map<number, Swarm>(); // event-tracked fallback when no snapshot
|
||||||
|
let mite = 0; // eased hospital-white sterile creep amount
|
||||||
|
let miteSeed: [number, number] = [0.5, 0.5];
|
||||||
|
const mites = new Map<number, Swarm>(); // event-tracked parity-mite fallback
|
||||||
let titleAge = 0; // boot title sequence clock
|
let titleAge = 0; // boot title sequence clock
|
||||||
let titleDirty = true; // painted once at stack creation; upload on the first frame
|
let titleDirty = true; // painted once at stack creation; upload on the first frame
|
||||||
let flashAge = -1; // <0 = not flashing
|
let flashAge = -1; // <0 = not flashing
|
||||||
@ -183,8 +192,11 @@ export function createScreenFX(): ScreenFX {
|
|||||||
while (stampQueue.length > STAMP_QUEUE_MAX) stampQueue.shift();
|
while (stampQueue.length > STAMP_QUEUE_MAX) stampQueue.shift();
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
let liveCanvas: HTMLCanvasElement | null = null;
|
||||||
|
|
||||||
|
const api: ScreenFX = {
|
||||||
init(canvas: HTMLCanvasElement, data: GameData) {
|
init(canvas: HTMLCanvasElement, data: GameData) {
|
||||||
|
liveCanvas = canvas;
|
||||||
const base = createBaseFeed();
|
const base = createBaseFeed();
|
||||||
stack = createGlitchStack(canvas, base, overlays);
|
stack = createGlitchStack(canvas, base, overlays);
|
||||||
for (const it of data.items) items.set(it.id, it);
|
for (const it of data.items) items.set(it.id, it);
|
||||||
@ -210,6 +222,19 @@ export function createScreenFX(): ScreenFX {
|
|||||||
if (on) swarms.set(id, { pos, size: 0.7 });
|
if (on) swarms.set(id, { pos, size: 0.7 });
|
||||||
else swarms.delete(id);
|
else swarms.delete(id);
|
||||||
},
|
},
|
||||||
|
mite: (on: boolean, id = 1, pos = { x: -4, y: 5 }, size = 0.7) => {
|
||||||
|
if (on) mites.set(id, { pos, size });
|
||||||
|
else mites.delete(id);
|
||||||
|
},
|
||||||
|
repair: (item: string, count = 1) => {
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
const cur = tally.get(item) ?? 0;
|
||||||
|
if (cur > 0) { tally.set(item, cur - 1); ledgerDirty = true; }
|
||||||
|
const def = items.get(item);
|
||||||
|
repairQueue.push({ name: def?.name ?? item.toUpperCase(), age: 0 });
|
||||||
|
while (repairQueue.length > STAMP_QUEUE_MAX) repairQueue.shift();
|
||||||
|
}
|
||||||
|
},
|
||||||
reset: () => {
|
reset: () => {
|
||||||
tally.clear();
|
tally.clear();
|
||||||
ledgerDirty = true;
|
ledgerDirty = true;
|
||||||
@ -228,6 +253,10 @@ export function createScreenFX(): ScreenFX {
|
|||||||
forcedEra = null;
|
forcedEra = null;
|
||||||
swarms.clear();
|
swarms.clear();
|
||||||
wildlife = 0;
|
wildlife = 0;
|
||||||
|
mites.clear();
|
||||||
|
mite = 0;
|
||||||
|
repair = null;
|
||||||
|
repairQueue.length = 0;
|
||||||
for (const p of PARAMS) eased[p] = 0;
|
for (const p of PARAMS) eased[p] = 0;
|
||||||
},
|
},
|
||||||
research: (tech: string) => {
|
research: (tech: string) => {
|
||||||
@ -246,6 +275,8 @@ export function createScreenFX(): ScreenFX {
|
|||||||
fever: +fever.toFixed(3),
|
fever: +fever.toFixed(3),
|
||||||
pallor: +pallor.toFixed(3),
|
pallor: +pallor.toFixed(3),
|
||||||
wildlife: +wildlife.toFixed(3),
|
wildlife: +wildlife.toFixed(3),
|
||||||
|
mite: +mite.toFixed(3),
|
||||||
|
repair: repair ? 1 : 0,
|
||||||
card: cardAge >= 0 ? 1 : 0,
|
card: cardAge >= 0 ? 1 : 0,
|
||||||
title: +titleOpacity(titleAge).toFixed(3),
|
title: +titleOpacity(titleAge).toFixed(3),
|
||||||
flash: +flashEnvelope(flashAge).toFixed(3),
|
flash: +flashEnvelope(flashAge).toFixed(3),
|
||||||
@ -256,6 +287,11 @@ export function createScreenFX(): ScreenFX {
|
|||||||
params: Object.fromEntries(PARAMS.map((p) => [p, +out[p].toFixed(3)])),
|
params: Object.fromEntries(PARAMS.map((p) => [p, +out[p].toFixed(3)])),
|
||||||
}),
|
}),
|
||||||
escalationItems: ESCALATION_ITEMS,
|
escalationItems: ESCALATION_ITEMS,
|
||||||
|
// Verification driver: a hidden harness tab throttles rAF, so pump the REAL module with
|
||||||
|
// a synthetic timebase and read pixels in the same JS task (prior-round contact-sheet
|
||||||
|
// trick — a hidden tab clears the WebGL buffer after compositing otherwise).
|
||||||
|
frame: (ms: number, snap?: SimSnapshot) => api.frame(ms, snap),
|
||||||
|
canvas: () => liveCanvas,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -278,6 +314,25 @@ export function createScreenFX(): ScreenFX {
|
|||||||
// (debug hook). When a snapshot IS present, frame() reads it as the source of truth.
|
// (debug hook). When a snapshot IS present, frame() reads it as the source of truth.
|
||||||
if (e.on === 'spawned') swarms.set(e.id, { pos: e.pos, size: 0.6 });
|
if (e.on === 'spawned') swarms.set(e.id, { pos: e.pos, size: 0.6 });
|
||||||
else swarms.delete(e.id);
|
else swarms.delete(e.id);
|
||||||
|
} else if (e.kind === 'wildlife' && e.wildlife === 'parity-mite') {
|
||||||
|
// Parity-mites drive the sterile creep. Same fallback contract as the swarms above:
|
||||||
|
// snapshot.wildlife is truth when present, these events cover the no-snapshot case.
|
||||||
|
if (e.on === 'spawned') mites.set(e.id, { pos: e.pos, size: 0.6 });
|
||||||
|
else mites.delete(e.id);
|
||||||
|
} else if (e.kind === 'repaired') {
|
||||||
|
// The Correction corrected one unit of your work. This is THEFT, not decay: ease the
|
||||||
|
// LEDGER down by exactly that item's per-unit contribution (a real, permanent removal —
|
||||||
|
// unlike boredom, which only fades the display and leaves the tally intact). Then let
|
||||||
|
// it sign the job with a small green-tick placard. We deliberately do NOT refresh the
|
||||||
|
// boredom clock: a factory being quietly eaten is not a factory that is producing.
|
||||||
|
const cur = tally.get(e.item) ?? 0;
|
||||||
|
if (cur > 0) {
|
||||||
|
tally.set(e.item, cur - 1);
|
||||||
|
ledgerDirty = true;
|
||||||
|
}
|
||||||
|
const def = items.get(e.item);
|
||||||
|
repairQueue.push({ name: def?.name ?? e.item.toUpperCase(), age: 0 });
|
||||||
|
while (repairQueue.length > STAMP_QUEUE_MAX) repairQueue.shift();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -339,6 +394,20 @@ export function createScreenFX(): ScreenFX {
|
|||||||
if (worst) wildlifeSeed = seedFromPos(worst.pos);
|
if (worst) wildlifeSeed = seedFromPos(worst.pos);
|
||||||
wildlife = ease(wildlife, worst ? clamp01(worst.size) : 0, dt, 1.0);
|
wildlife = ease(wildlife, worst ? clamp01(worst.size) : 0, dt, 1.0);
|
||||||
|
|
||||||
|
// Mite sterility: same worst-of pattern, but for parity-mites. The sterile creep grows
|
||||||
|
// around the heaviest infestation; snapshot is truth when present, events are the fallback.
|
||||||
|
let worstMite: Swarm | null = null;
|
||||||
|
if (live) {
|
||||||
|
for (const w of live) {
|
||||||
|
if (w.kind !== 'parity-mite') continue;
|
||||||
|
if (!worstMite || w.size > worstMite.size) worstMite = { pos: w.pos, size: w.size };
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (const m of mites.values()) if (!worstMite || m.size > worstMite.size) worstMite = m;
|
||||||
|
}
|
||||||
|
if (worstMite) miteSeed = seedFromPos(worstMite.pos);
|
||||||
|
mite = ease(mite, worstMite ? clamp01(worstMite.size) : 0, dt, 1.0);
|
||||||
|
|
||||||
const timeSec = timeMs / 1000;
|
const timeSec = timeMs / 1000;
|
||||||
const displayTotal = total * fresh;
|
const displayTotal = total * fresh;
|
||||||
const wobble =
|
const wobble =
|
||||||
@ -375,6 +444,23 @@ export function createScreenFX(): ScreenFX {
|
|||||||
if (stamp.age >= STAMP_LIFE) stamp = null;
|
if (stamp.age >= STAMP_LIFE) stamp = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Repair placard lifecycle — its own slot so it never fights the shipment stamp. Same
|
||||||
|
// punch-in-hold-fade shape, held a touch longer because it is a signature to be read.
|
||||||
|
if (!repair && repairQueue.length) {
|
||||||
|
repair = repairQueue.shift()!;
|
||||||
|
overlays.paintRepair(repair.name);
|
||||||
|
repairDirty = true;
|
||||||
|
}
|
||||||
|
let repairAmt = 0;
|
||||||
|
let repairScale = 1;
|
||||||
|
if (repair) {
|
||||||
|
const a = repair.age / REPAIR_LIFE;
|
||||||
|
repairAmt = Math.min(1, a / 0.1) * (1 - smoothstep(0.6, 1, a));
|
||||||
|
repairScale = 1.12 - 0.12 * Math.min(1, a / 0.1); // small, perfect, slight impact overshoot
|
||||||
|
repair.age += dt;
|
||||||
|
if (repair.age >= REPAIR_LIFE) repair = null;
|
||||||
|
}
|
||||||
|
|
||||||
// The card holds at full opacity and then cuts — no fade. The corruption crashes back in
|
// The card holds at full opacity and then cuts — no fade. The corruption crashes back in
|
||||||
// rather than seeping back; a dissolve would make it look negotiable.
|
// rather than seeping back; a dissolve would make it look negotiable.
|
||||||
let card = 0;
|
let card = 0;
|
||||||
@ -400,16 +486,20 @@ export function createScreenFX(): ScreenFX {
|
|||||||
|
|
||||||
stack.draw({
|
stack.draw({
|
||||||
params: out, timeSec, drift: 1, brownout, flash, era, chyron,
|
params: out, timeSec, drift: 1, brownout, flash, era, chyron,
|
||||||
strain, fever, pallor, moireSeed, wildlife, wildlifeSeed,
|
strain, fever, pallor, moireSeed, wildlife, wildlifeSeed, mite, miteSeed,
|
||||||
stampAmt, stampScale, scram: scramFrames > 0, scramY, stampDirty,
|
stampAmt, stampScale, scram: scramFrames > 0, scramY, stampDirty,
|
||||||
|
repair: repairAmt, repairScale, repairDirty,
|
||||||
card, cardDirty, title, titleDirty,
|
card, cardDirty, title, titleDirty,
|
||||||
});
|
});
|
||||||
if (scramFrames > 0) scramFrames--;
|
if (scramFrames > 0) scramFrames--;
|
||||||
stampDirty = false;
|
stampDirty = false;
|
||||||
|
repairDirty = false;
|
||||||
cardDirty = false;
|
cardDirty = false;
|
||||||
titleDirty = false;
|
titleDirty = false;
|
||||||
|
|
||||||
frameCost = frameCost * 0.95 + (performance.now() - t0) * 0.05;
|
frameCost = frameCost * 0.95 + (performance.now() - t0) * 0.05;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
return api;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,6 +12,9 @@ export const SCRAM_W = 512;
|
|||||||
export const SCRAM_H = 64;
|
export const SCRAM_H = 64;
|
||||||
export const CARD_W = 640;
|
export const CARD_W = 640;
|
||||||
export const CARD_H = 360;
|
export const CARD_H = 360;
|
||||||
|
/** The repair placard is small on purpose — it's a signature, not a broadcast. */
|
||||||
|
export const REPAIR_W = 224;
|
||||||
|
export const REPAIR_H = 112;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The eras as the codex names them (§1). The card announces the STANDARD, not the tech id —
|
* The eras as the codex names them (§1). The card announces the STANDARD, not the tech id —
|
||||||
@ -34,12 +37,19 @@ export interface Overlays {
|
|||||||
scramCanvas: HTMLCanvasElement;
|
scramCanvas: HTMLCanvasElement;
|
||||||
cardCanvas: HTMLCanvasElement;
|
cardCanvas: HTMLCanvasElement;
|
||||||
titleCanvas: HTMLCanvasElement;
|
titleCanvas: HTMLCanvasElement;
|
||||||
|
repairCanvas: HTMLCanvasElement;
|
||||||
/** Repaint the stamp for a landed shipment. OSHA-placard deadpan, in the item's chip colour. */
|
/** Repaint the stamp for a landed shipment. OSHA-placard deadpan, in the item's chip colour. */
|
||||||
paintStamp(name: string, count: number, color: string): void;
|
paintStamp(name: string, count: number, color: string): void;
|
||||||
/** Repaint the ratification card. The Correction's uniform: hospital white, green tick. */
|
/** Repaint the ratification card. The Correction's uniform: hospital white, green tick. */
|
||||||
paintCard(era: string, tech: string): void;
|
paintCard(era: string, tech: string): void;
|
||||||
/** Paint the boot title card. Alpha channel matters — it dissolves over the feed. */
|
/** Paint the boot title card. Alpha channel matters — it dissolves over the feed. */
|
||||||
paintTitle(): void;
|
paintTitle(): void;
|
||||||
|
/**
|
||||||
|
* Repaint the repair placard — The Correction signing one unit of your work off the ledger.
|
||||||
|
* Small, perfect (no crooked stamp), hospital white with the faction green tick: deliberately
|
||||||
|
* the opposite of the hand-thumped, chip-coloured shipment stamp. Rot vs theft, made legible.
|
||||||
|
*/
|
||||||
|
paintRepair(name: string): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function roundRect(g: CanvasRenderingContext2D, x: number, y: number, w: number, h: number, r: number) {
|
function roundRect(g: CanvasRenderingContext2D, x: number, y: number, w: number, h: number, r: number) {
|
||||||
@ -95,11 +105,17 @@ export function createOverlays(): Overlays {
|
|||||||
titleCanvas.height = CARD_H;
|
titleCanvas.height = CARD_H;
|
||||||
const tg = titleCanvas.getContext('2d')!;
|
const tg = titleCanvas.getContext('2d')!;
|
||||||
|
|
||||||
|
const repairCanvas = document.createElement('canvas');
|
||||||
|
repairCanvas.width = REPAIR_W;
|
||||||
|
repairCanvas.height = REPAIR_H;
|
||||||
|
const rg = repairCanvas.getContext('2d')!;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
stampCanvas,
|
stampCanvas,
|
||||||
scramCanvas,
|
scramCanvas,
|
||||||
cardCanvas,
|
cardCanvas,
|
||||||
titleCanvas,
|
titleCanvas,
|
||||||
|
repairCanvas,
|
||||||
|
|
||||||
paintTitle() {
|
paintTitle() {
|
||||||
// The game finally says its own name. A CRT burn-in: chunky phosphor letters with a
|
// The game finally says its own name. A CRT burn-in: chunky phosphor letters with a
|
||||||
@ -220,5 +236,50 @@ export function createOverlays(): Overlays {
|
|||||||
sg.fillText('SHIPPED / NO QUESTIONS', 22, 34);
|
sg.fillText('SHIPPED / NO QUESTIONS', 22, 34);
|
||||||
sg.restore();
|
sg.restore();
|
||||||
},
|
},
|
||||||
|
paintRepair(name: string) {
|
||||||
|
// The Correction's uniform (codex §8): hospital white, green tick, and the placid
|
||||||
|
// confidence of a compliance stamp. NOT rotated — where the shipment stamp is thumped
|
||||||
|
// on by a tired hand, this is machined on square. That contrast is the whole read.
|
||||||
|
rg.clearRect(0, 0, REPAIR_W, REPAIR_H);
|
||||||
|
|
||||||
|
// clean white placard with a thin green rule
|
||||||
|
rg.fillStyle = '#f2f5f2';
|
||||||
|
roundRect(rg, 6, 6, REPAIR_W - 12, REPAIR_H - 12, 5);
|
||||||
|
rg.fill();
|
||||||
|
rg.strokeStyle = '#2f7d3f';
|
||||||
|
rg.lineWidth = 3;
|
||||||
|
roundRect(rg, 6, 6, REPAIR_W - 12, REPAIR_H - 12, 5);
|
||||||
|
rg.stroke();
|
||||||
|
|
||||||
|
// the green tick — the faction signature
|
||||||
|
rg.strokeStyle = '#2f7d3f';
|
||||||
|
rg.lineWidth = 5;
|
||||||
|
rg.lineCap = 'round';
|
||||||
|
rg.lineJoin = 'round';
|
||||||
|
rg.beginPath();
|
||||||
|
rg.moveTo(26, 40);
|
||||||
|
rg.lineTo(38, 52);
|
||||||
|
rg.lineTo(60, 26);
|
||||||
|
rg.stroke();
|
||||||
|
|
||||||
|
rg.textBaseline = 'middle';
|
||||||
|
rg.fillStyle = '#12160f';
|
||||||
|
rg.font = "bold 20px 'Courier New', monospace";
|
||||||
|
rg.fillText('CORRECTED', 74, 40);
|
||||||
|
|
||||||
|
// the item that was quietly reverted — shrink long codex names to fit the placard
|
||||||
|
rg.fillStyle = '#5c6b5c';
|
||||||
|
let size = 15;
|
||||||
|
rg.font = `${size}px 'Courier New', monospace`;
|
||||||
|
while (rg.measureText(name).width > REPAIR_W - 40 && size > 8) {
|
||||||
|
size -= 1;
|
||||||
|
rg.font = `${size}px 'Courier New', monospace`;
|
||||||
|
}
|
||||||
|
rg.fillText(name, 26, 74);
|
||||||
|
|
||||||
|
rg.fillStyle = '#8a998a';
|
||||||
|
rg.font = "9px 'Courier New', monospace";
|
||||||
|
rg.fillText('THE CORRECTION · NO ACTION REQUIRED', 26, 94);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user