Compare commits
No commits in common. "34e2bb3fcf42dc7859ad0589da5dadcb2f8f1df1" and "3e704824477fe7531dd103b176414332d824b330" have entirely different histories.
34e2bb3fcf
...
3e70482447
@ -149,79 +149,3 @@ express — moiré needs a real interference lattice, ringing needs edge-detecte
|
|||||||
macroblock bricks want the 8×8 grid to be *legible*. That's new shader passes, not new
|
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
|
weights. Also THE SCREEN should probably show the shipped item's silhouette for a beat as it
|
||||||
lands. Say the word on round 2.
|
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.
|
|
||||||
|
|||||||
@ -64,39 +64,18 @@ describe('item -> artifact legibility', () => {
|
|||||||
|
|
||||||
describe('codex signatures read as themselves', () => {
|
describe('codex signatures read as themselves', () => {
|
||||||
it.each([
|
it.each([
|
||||||
['static-canister', 'noise'], // bottled analog snow
|
['static-canister', 'noise'], // bottled analog snow
|
||||||
['judder-cams', 'freeze'], // 3:2 pulldown, machined
|
['judder-cams', 'freeze'], // 3:2 pulldown, machined
|
||||||
['macroblock-bricks', 'block'], // the quantizer's confession
|
['macroblock-bricks', 'mosh'], // the quantizer's confession
|
||||||
['coefficient-pack', 'block'], // coeff overdrive reads as the grid
|
|
||||||
['ringing-halos', 'ring'], // Gibbs phenomenon, canned
|
|
||||||
['gibbs-wraith', 'ring'],
|
|
||||||
['moire-crystal', 'moire'], // interference made solid
|
|
||||||
['false-color-speckle', 'chroma'],
|
['false-color-speckle', 'chroma'],
|
||||||
['mosquito-swarm', 'noise'], // literal mosquito noise
|
['mosquito-swarm', 'noise'], // literal mosquito noise
|
||||||
['v-hold-roll', 'roll'],
|
['v-hold-roll', 'roll'],
|
||||||
['silver-frames', 'burn'], // celluloid
|
['silver-frames', 'burn'], // celluloid
|
||||||
['bloom-concentrate', 'mosh'], // motion compounded on itself
|
['bloom-concentrate', 'mosh'], // motion compounded on itself
|
||||||
])('%s reads as %s', (item, param) => {
|
])('%s reads as %s', (item, param) => {
|
||||||
expect(dominant(resolveLedger(tally({ [item]: 40 })).targets)).toBe(param);
|
expect(dominant(resolveLedger(tally({ [item]: 40 })).targets)).toBe(param);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('40 bricks look UNMISTAKABLY unlike 40 slurry (round 2 legibility bar)', () => {
|
|
||||||
const bricks = resolveLedger(tally({ 'macroblock-bricks': 40 })).targets;
|
|
||||||
const slurry = resolveLedger(tally({ 'chroma-slurry': 40 })).targets;
|
|
||||||
// Each must dominate its own signature and carry ~none of the other's.
|
|
||||||
expect(bricks.block).toBeGreaterThan(0.5);
|
|
||||||
expect(slurry.block).toBe(0);
|
|
||||||
expect(slurry.chroma).toBeGreaterThan(0.5);
|
|
||||||
expect(bricks.chroma).toBe(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('keeps the three new artifact classes distinct from each other', () => {
|
|
||||||
const b = resolveLedger(tally({ 'macroblock-bricks': 40 })).targets;
|
|
||||||
const r = resolveLedger(tally({ 'ringing-halos': 40 })).targets;
|
|
||||||
const m = resolveLedger(tally({ 'moire-crystal': 40 })).targets;
|
|
||||||
expect([b.ring, b.moire, r.block, r.moire, m.block, m.ring]).toEqual([0, 0, 0, 0, 0, 0]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('grades chroma subsampling by how much colour was thrown away', () => {
|
it('grades chroma subsampling by how much colour was thrown away', () => {
|
||||||
expect(corruptionFor('chroma-420').potency).toBeGreaterThan(corruptionFor('chroma-422').potency);
|
expect(corruptionFor('chroma-420').potency).toBeGreaterThan(corruptionFor('chroma-422').potency);
|
||||||
expect(corruptionFor('chroma-422').potency).toBeGreaterThan(corruptionFor('chroma-slurry').potency);
|
expect(corruptionFor('chroma-422').potency).toBeGreaterThan(corruptionFor('chroma-slurry').potency);
|
||||||
|
|||||||
@ -36,8 +36,7 @@ const CORRUPTION: Record<string, ItemCorruption> = {
|
|||||||
'chroma-422': { potency: 1.05, weights: { chroma: 1.0, melt: 0.15 } },
|
'chroma-422': { potency: 1.05, weights: { chroma: 1.0, melt: 0.15 } },
|
||||||
'chroma-420': { potency: 1.1, weights: { chroma: 1.0, melt: 0.3 } },
|
'chroma-420': { potency: 1.1, weights: { chroma: 1.0, melt: 0.3 } },
|
||||||
'color-plate': { potency: 0.7, weights: { chroma: 0.75 } },
|
'color-plate': { potency: 0.7, weights: { chroma: 0.75 } },
|
||||||
// DCT coefficients overdriven: quantization, and the grid it confesses to.
|
'coefficient-pack': { potency: 0.7, weights: { freeze: 0.55, noise: 0.35, tear: 0.3 } },
|
||||||
'coefficient-pack': { potency: 0.7, weights: { block: 0.7, freeze: 0.4, noise: 0.2 } },
|
|
||||||
'hf-dust': { potency: 0.8, weights: { noise: 0.9, tear: 0.2 } },
|
'hf-dust': { potency: 0.8, weights: { noise: 0.9, tear: 0.2 } },
|
||||||
'mv-flux': { potency: 1.0, weights: { mosh: 0.9, melt: 0.45 } }, // motion vectors
|
'mv-flux': { potency: 1.0, weights: { mosh: 0.9, melt: 0.45 } }, // motion vectors
|
||||||
'v-hold-roll': { potency: 0.7, weights: { roll: 1.0 } },
|
'v-hold-roll': { potency: 0.7, weights: { roll: 1.0 } },
|
||||||
@ -53,12 +52,10 @@ const CORRUPTION: Record<string, ItemCorruption> = {
|
|||||||
// ---- glitch products (§3) : the signatures. These must read INSTANTLY.
|
// ---- glitch products (§3) : the signatures. These must read INSTANTLY.
|
||||||
melt: { potency: 1.2, weights: { melt: 0.95, mosh: 0.55 } },
|
melt: { potency: 1.2, weights: { melt: 0.95, mosh: 0.55 } },
|
||||||
'bloom-concentrate': { potency: 1.3, weights: { mosh: 0.9, melt: 0.6 } },
|
'bloom-concentrate': { potency: 1.3, weights: { mosh: 0.9, melt: 0.6 } },
|
||||||
// Round 2: these three now have artifact passes of their own. A pile of bricks must
|
'macroblock-bricks': { potency: 1.1, weights: { mosh: 1.0 } },
|
||||||
// read as a blocky grid, not as displacement; halos as ringing, not as static.
|
'ringing-halos': { potency: 0.9, weights: { noise: 0.5, tear: 0.45 } },
|
||||||
'macroblock-bricks': { potency: 1.1, weights: { block: 1.0, mosh: 0.2 } },
|
|
||||||
'ringing-halos': { potency: 0.9, weights: { ring: 1.0, noise: 0.15 } },
|
|
||||||
'static-canister': { potency: 1.0, weights: { noise: 1.0 } },
|
'static-canister': { potency: 1.0, weights: { noise: 1.0 } },
|
||||||
'moire-crystal': { potency: 1.1, weights: { moire: 1.0, chroma: 0.2 } },
|
'moire-crystal': { potency: 1.1, weights: { chroma: 0.6, noise: 0.5, tear: 0.35 } },
|
||||||
'false-color-speckle': { potency: 1.2, weights: { chroma: 1.0, noise: 0.3 } },
|
'false-color-speckle': { potency: 1.2, weights: { chroma: 1.0, noise: 0.3 } },
|
||||||
'judder-cams': { potency: 1.1, weights: { freeze: 1.0 } }, // 3:2 pulldown
|
'judder-cams': { potency: 1.1, weights: { freeze: 1.0 } }, // 3:2 pulldown
|
||||||
'explosive-hits': { potency: 1.5, weights: { mosh: 0.8, burn: 0.6, chroma: 0.5 } },
|
'explosive-hits': { potency: 1.5, weights: { mosh: 0.8, burn: 0.6, chroma: 0.5 } },
|
||||||
@ -69,7 +66,7 @@ const CORRUPTION: Record<string, ItemCorruption> = {
|
|||||||
|
|
||||||
// ---- wildlife (§5)
|
// ---- wildlife (§5)
|
||||||
'mosquito-swarm': { potency: 0.9, weights: { noise: 0.95 } }, // literal mosquito noise
|
'mosquito-swarm': { potency: 0.9, weights: { noise: 0.95 } }, // literal mosquito noise
|
||||||
'gibbs-wraith': { potency: 1.0, weights: { ring: 0.85, noise: 0.25, freeze: 0.25 } },
|
'gibbs-wraith': { potency: 1.0, weights: { noise: 0.55, tear: 0.5, freeze: 0.3 } },
|
||||||
};
|
};
|
||||||
|
|
||||||
const FALLBACK: ItemCorruption = { potency: 0.5, weights: { noise: 0.6 } };
|
const FALLBACK: ItemCorruption = { potency: 0.5, weights: { noise: 0.6 } };
|
||||||
|
|||||||
@ -1,19 +1,16 @@
|
|||||||
/**
|
/**
|
||||||
* The GLYTCH shader stack, ported from the WIMVEE GLYTCH prototype (../index.html).
|
* The GLYTCH shader stack, ported from the WIMVEE GLYTCH prototype (../index.html).
|
||||||
*
|
*
|
||||||
* The 8 original glitch params and their maths are TUNED — the prototype is the reference
|
* The 8 glitch params and their maths are TUNED — the prototype is the reference and
|
||||||
* and the magic numbers are respected verbatim. Everything else here is additive:
|
* the magic numbers are respected verbatim. Only three things are new here:
|
||||||
* u_drift - slow sinusoidal pan of the clean feed (subtle motion to chew on)
|
* u_drift - slow sinusoidal pan of the clean feed (subtle motion to chew on)
|
||||||
* u_brownout - the factory's pain: freeze-frame + dropout bars + desync judder
|
* u_brownout - the factory's pain: freeze-frame + dropout bars + desync judder
|
||||||
* u_flash - one-frame full-white awakening on the first shipment ever
|
* u_flash - one-frame full-white awakening on the first shipment ever
|
||||||
* u_block/u_ring/u_moire (round 2) - artifact classes the original 8 cannot express
|
|
||||||
* u_stamp + u_scram uniforms (r2) - transient overlays composited in screen space
|
|
||||||
*
|
*
|
||||||
* Raw WebGL, not three.js: this is one fullscreen triangle. Pulling three.js in for that
|
* Raw WebGL, not three.js: this is one fullscreen triangle with one texture. Pulling
|
||||||
* would cost more than the whole effect is allowed to cost.
|
* three.js in for that would cost more than the whole effect is allowed to cost.
|
||||||
*/
|
*/
|
||||||
import { BASE_H, BASE_W, type BaseFeed } from './baseTexture';
|
import { BASE_H, BASE_W, type BaseFeed } from './baseTexture';
|
||||||
import type { Overlays } from './overlays';
|
|
||||||
import { PARAMS, type Param, type ParamSet } from './params';
|
import { PARAMS, type Param, type ParamSet } from './params';
|
||||||
|
|
||||||
const VS = `
|
const VS = `
|
||||||
@ -27,10 +24,7 @@ varying vec2 vUv;
|
|||||||
uniform sampler2D tex;
|
uniform sampler2D tex;
|
||||||
uniform float t;
|
uniform float t;
|
||||||
uniform float u_mosh, u_melt, u_chroma, u_tear, u_roll, u_burn, u_noise, u_freeze;
|
uniform float u_mosh, u_melt, u_chroma, u_tear, u_roll, u_burn, u_noise, u_freeze;
|
||||||
uniform float u_block, u_ring, u_moire;
|
|
||||||
uniform float u_drift, u_brownout, u_flash;
|
uniform float u_drift, u_brownout, u_flash;
|
||||||
uniform sampler2D u_stampTex, u_scramTex;
|
|
||||||
uniform float u_stampAmt, u_stampScale, u_scram, u_scramY;
|
|
||||||
|
|
||||||
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){
|
||||||
@ -39,7 +33,6 @@ float vnoise(vec2 p){
|
|||||||
mix(hash(i+vec2(0,1)),hash(i+vec2(1,1)),f.x),f.y);
|
mix(hash(i+vec2(0,1)),hash(i+vec2(1,1)),f.x),f.y);
|
||||||
}
|
}
|
||||||
float fbm(vec2 p){ return 0.5*vnoise(p)+0.25*vnoise(p*2.3)+0.125*vnoise(p*5.1); }
|
float fbm(vec2 p){ return 0.5*vnoise(p)+0.25*vnoise(p*2.3)+0.125*vnoise(p*5.1); }
|
||||||
float lu(vec3 c){ return dot(c, vec3(0.299,0.587,0.114)); }
|
|
||||||
|
|
||||||
void main(){
|
void main(){
|
||||||
// freeze quantizes the timebase everything else animates on.
|
// freeze quantizes the timebase everything else animates on.
|
||||||
@ -89,53 +82,6 @@ void main(){
|
|||||||
col.g = texture2D(tex, uv).g;
|
col.g = texture2D(tex, uv).g;
|
||||||
col.b = texture2D(tex, uv - vec2( ca, ca*0.6)).b;
|
col.b = texture2D(tex, uv - vec2( ca, ca*0.6)).b;
|
||||||
|
|
||||||
// ---- macroblock quantization: the quantizer's confession, 8x8 and LEGIBLE.
|
|
||||||
// The lattice is locked to screen space (vUv) so it stays a rigid grid while melt and
|
|
||||||
// mosh drag the picture underneath it — that rigidity is what makes it read as blocks.
|
|
||||||
if(u_block > 0.001){
|
|
||||||
vec2 g = vec2(80.0, 45.0); // 8x8 source pixels at 640x360
|
|
||||||
vec2 cell = (floor(vUv*g)+0.5)/g;
|
|
||||||
vec3 bcol = texture2D(tex, uv + (cell - vUv)).rgb; // one flat sample per cell
|
|
||||||
float levels = mix(24.0, 3.0, u_block); // flat areas starve as it bites
|
|
||||||
bcol = floor(bcol*levels + 0.5)/levels;
|
|
||||||
col = mix(col, bcol, u_block);
|
|
||||||
vec2 f = fract(vUv*g);
|
|
||||||
float seam = max(step(f.x, 0.07), step(f.y, 0.07));
|
|
||||||
col = mix(col, col*0.55, seam*u_block*0.85);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- ringing halos: Gibbs oscillation ringing OUT from every contrast edge.
|
|
||||||
// Narrow differences only ever light up the 1px boundary, which reads as "slightly
|
|
||||||
// sharper" rather than as haloing — so the gradient is sampled WIDE (out to 7px) to give
|
|
||||||
// the halo room to ring, and the oscillation runs parallel to the edge it came from.
|
|
||||||
if(u_ring > 0.001){
|
|
||||||
vec2 px = vec2(1.0/640.0, 1.0/360.0);
|
|
||||||
vec3 blur = 0.25*(texture2D(tex, uv+vec2(1.6*px.x,0.)).rgb + texture2D(tex, uv-vec2(1.6*px.x,0.)).rgb
|
|
||||||
+ texture2D(tex, uv+vec2(0.,1.6*px.y)).rgb + texture2D(tex, uv-vec2(0.,1.6*px.y)).rgb);
|
|
||||||
float n1 = lu(texture2D(tex, uv+vec2(3.0*px.x,0.)).rgb) - lu(texture2D(tex, uv-vec2(3.0*px.x,0.)).rgb);
|
|
||||||
float n2 = lu(texture2D(tex, uv+vec2(0.,3.0*px.y)).rgb) - lu(texture2D(tex, uv-vec2(0.,3.0*px.y)).rgb);
|
|
||||||
float n3 = lu(texture2D(tex, uv+vec2(7.0*px.x,0.)).rgb) - lu(texture2D(tex, uv-vec2(7.0*px.x,0.)).rgb);
|
|
||||||
float n4 = lu(texture2D(tex, uv+vec2(0.,7.0*px.y)).rgb) - lu(texture2D(tex, uv-vec2(0.,7.0*px.y)).rgb);
|
|
||||||
vec2 grad = vec2(n1 + n3*0.75, n2 + n4*0.75);
|
|
||||||
float edge = clamp(length(grad)*2.2, 0., 1.);
|
|
||||||
vec2 dir = normalize(grad + vec2(1e-4));
|
|
||||||
float proj = dot(dir, vec2(vUv.x*640.0, vUv.y*360.0));
|
|
||||||
float rings = sin(proj*1.15 - tt*5.0); // ethereal, faintly vibrating
|
|
||||||
col += vec3(0.82,0.90,1.0) * rings * edge * u_ring * 0.85;
|
|
||||||
col += (col - blur) * u_ring * 1.5; // overshoot at the edge itself
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- moire: two near-identical lattices beating against the sensor grid
|
|
||||||
if(u_moire > 0.001){
|
|
||||||
float ang = 0.06 + 0.02*sin(tt*0.07);
|
|
||||||
vec2 r = vec2(vUv.x*cos(ang)-vUv.y*sin(ang), vUv.x*sin(ang)+vUv.y*cos(ang));
|
|
||||||
float a = sin(vUv.x*640.0*0.55 + vUv.y*3.0);
|
|
||||||
float b = sin(r.x*640.0*0.575 + r.y*2.0 + tt*0.25);
|
|
||||||
float m = a*b; // the beat: swirling maze lattice
|
|
||||||
vec3 sheen = 0.5+0.5*cos(6.2831*(vec3(0.0,0.33,0.67) + m*1.2 + tt*0.03));
|
|
||||||
col = mix(col, col*0.4 + sheen*0.7, u_moire*clamp(abs(m)*1.5, 0., 1.));
|
|
||||||
}
|
|
||||||
|
|
||||||
// static
|
// static
|
||||||
float sn = hash(uv*vec2(431.0,917.0) + fract(tt)*13.0);
|
float sn = hash(uv*vec2(431.0,917.0) + fract(tt)*13.0);
|
||||||
col = mix(col, vec3(sn), u_noise * 0.7);
|
col = mix(col, vec3(sn), u_noise * 0.7);
|
||||||
@ -164,35 +110,13 @@ void main(){
|
|||||||
// 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);
|
||||||
|
|
||||||
// ---- shipment stamp: screen space (vUv), so a melting feed never drags the placard
|
|
||||||
if(u_stampAmt > 0.001){
|
|
||||||
vec2 halfSize = vec2(0.30, 0.15)*u_stampScale;
|
|
||||||
vec2 suv = (vUv - vec2(0.5, 0.44))/(2.0*halfSize) + 0.5;
|
|
||||||
if(suv.x > 0.0 && suv.x < 1.0 && suv.y > 0.0 && suv.y < 1.0){
|
|
||||||
vec4 s = texture2D(u_stampTex, suv);
|
|
||||||
col = mix(col, s.rgb, s.a*u_stampAmt);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- scram: the tape loses tracking for exactly one frame
|
|
||||||
if(u_scram > 0.5){
|
|
||||||
float halfH = 0.085;
|
|
||||||
float d = vUv.y - u_scramY;
|
|
||||||
if(abs(d) < halfH){
|
|
||||||
vec4 sb = texture2D(u_scramTex, vec2(vUv.x, (d + halfH)/(2.0*halfH)));
|
|
||||||
col = mix(col, sb.rgb, sb.a);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// the awakening: one frame of full white on the first shipment ever
|
// the awakening: one frame of full white on the first shipment ever
|
||||||
col = mix(col, vec3(1.0), u_flash);
|
col = mix(col, vec3(1.0), u_flash);
|
||||||
|
|
||||||
gl_FragColor = vec4(col, 1.0);
|
gl_FragColor = vec4(col, 1.0);
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
type UniformKey =
|
type UniformKey = Param | 't' | 'drift' | 'brownout' | 'flash';
|
||||||
| Param | 't' | 'drift' | 'brownout' | 'flash'
|
|
||||||
| 'stampTex' | 'scramTex' | 'stampAmt' | 'stampScale' | 'scram' | 'scramY';
|
|
||||||
|
|
||||||
export interface DrawState {
|
export interface DrawState {
|
||||||
params: ParamSet;
|
params: ParamSet;
|
||||||
@ -205,16 +129,6 @@ export interface DrawState {
|
|||||||
flash: number;
|
flash: number;
|
||||||
/** nothing shipped yet — the clean feed still carries its chyron */
|
/** nothing shipped yet — the clean feed still carries its chyron */
|
||||||
idle: boolean;
|
idle: boolean;
|
||||||
/** 0..1 opacity of the shipment stamp */
|
|
||||||
stampAmt: number;
|
|
||||||
/** stamp size multiplier — bigger shipments stamp harder */
|
|
||||||
stampScale: number;
|
|
||||||
/** true for exactly one frame per scram event */
|
|
||||||
scram: boolean;
|
|
||||||
/** 0..1 vertical position of the scram bar */
|
|
||||||
scramY: number;
|
|
||||||
/** set when the stamp canvas has been repainted and needs re-upload */
|
|
||||||
stampDirty: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GlitchStack {
|
export interface GlitchStack {
|
||||||
@ -232,11 +146,7 @@ function compile(gl: WebGLRenderingContext, type: number, src: string): WebGLSha
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Returns null when WebGL is unavailable; caller degrades to a dead screen rather than crashing the game. */
|
/** Returns null when WebGL is unavailable; caller degrades to a dead screen rather than crashing the game. */
|
||||||
export function createGlitchStack(
|
export function createGlitchStack(canvas: HTMLCanvasElement, base: BaseFeed): GlitchStack | null {
|
||||||
canvas: HTMLCanvasElement,
|
|
||||||
base: BaseFeed,
|
|
||||||
overlays: Overlays,
|
|
||||||
): GlitchStack | null {
|
|
||||||
const gl = canvas.getContext('webgl', { antialias: false, depth: false, alpha: false });
|
const gl = canvas.getContext('webgl', { antialias: false, depth: false, alpha: false });
|
||||||
if (!gl) {
|
if (!gl) {
|
||||||
console.warn('[screen] no WebGL context — THE SCREEN stays dark');
|
console.warn('[screen] no WebGL context — THE SCREEN stays dark');
|
||||||
@ -259,66 +169,36 @@ export function createGlitchStack(
|
|||||||
gl.enableVertexAttribArray(loc);
|
gl.enableVertexAttribArray(loc);
|
||||||
gl.vertexAttribPointer(loc, 2, gl.FLOAT, false, 0, 0);
|
gl.vertexAttribPointer(loc, 2, gl.FLOAT, false, 0, 0);
|
||||||
|
|
||||||
// All three sources are non-power-of-two: CLAMP_TO_EDGE + LINEAR + no mips is mandatory
|
// 640x360 is non-power-of-two: CLAMP_TO_EDGE + LINEAR + no mips is mandatory in WebGL1.
|
||||||
// in WebGL1. Each texture keeps its own unit for the lifetime of the stack, so the hot
|
const texture = gl.createTexture();
|
||||||
// path never rebinds — it only re-uploads on the frames where a canvas actually changed.
|
gl.bindTexture(gl.TEXTURE_2D, texture);
|
||||||
function makeTex(unit: number, src: TexImageSource): WebGLTexture {
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
||||||
const t = gl!.createTexture()!;
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
||||||
gl!.activeTexture(gl!.TEXTURE0 + unit);
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
||||||
gl!.bindTexture(gl!.TEXTURE_2D, t);
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
||||||
gl!.texParameteri(gl!.TEXTURE_2D, gl!.TEXTURE_WRAP_S, gl!.CLAMP_TO_EDGE);
|
|
||||||
gl!.texParameteri(gl!.TEXTURE_2D, gl!.TEXTURE_WRAP_T, gl!.CLAMP_TO_EDGE);
|
|
||||||
gl!.texParameteri(gl!.TEXTURE_2D, gl!.TEXTURE_MIN_FILTER, gl!.LINEAR);
|
|
||||||
gl!.texParameteri(gl!.TEXTURE_2D, gl!.TEXTURE_MAG_FILTER, gl!.LINEAR);
|
|
||||||
gl!.texImage2D(gl!.TEXTURE_2D, 0, gl!.RGBA, gl!.RGBA, gl!.UNSIGNED_BYTE, src);
|
|
||||||
return t;
|
|
||||||
}
|
|
||||||
|
|
||||||
base.update(0, true);
|
base.update(0, true);
|
||||||
overlays.paintStamp('', 0, '#000'); // allocate the stamp texture at full size up front
|
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, base.canvas);
|
||||||
const baseTex = makeTex(0, base.canvas);
|
|
||||||
const stampTex = makeTex(1, overlays.stampCanvas);
|
|
||||||
makeTex(2, overlays.scramCanvas);
|
|
||||||
|
|
||||||
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');
|
||||||
uni.drift = gl.getUniformLocation(prog, 'u_drift');
|
uni.drift = gl.getUniformLocation(prog, 'u_drift');
|
||||||
uni.brownout = gl.getUniformLocation(prog, 'u_brownout');
|
uni.brownout = gl.getUniformLocation(prog, 'u_brownout');
|
||||||
uni.flash = gl.getUniformLocation(prog, 'u_flash');
|
uni.flash = gl.getUniformLocation(prog, 'u_flash');
|
||||||
uni.stampAmt = gl.getUniformLocation(prog, 'u_stampAmt');
|
|
||||||
uni.stampScale = gl.getUniformLocation(prog, 'u_stampScale');
|
|
||||||
uni.scram = gl.getUniformLocation(prog, 'u_scram');
|
|
||||||
uni.scramY = gl.getUniformLocation(prog, 'u_scramY');
|
|
||||||
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, 'u_stampTex'), 1);
|
|
||||||
gl.uniform1i(gl.getUniformLocation(prog, 'u_scramTex'), 2);
|
|
||||||
|
|
||||||
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, idle, stampAmt, stampScale, scram, scramY, stampDirty }) {
|
draw({ params, timeSec, drift, brownout, flash, idle }) {
|
||||||
// Re-upload the clean feed only when it actually changed (timecode tick / chyron).
|
// Re-upload the clean feed only when it actually changed (timecode tick / chyron).
|
||||||
if (base.update(timeSec * 1000, idle)) {
|
if (base.update(timeSec * 1000, idle)) {
|
||||||
gl.activeTexture(gl.TEXTURE0);
|
|
||||||
gl.bindTexture(gl.TEXTURE_2D, baseTex);
|
|
||||||
gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, gl.RGBA, gl.UNSIGNED_BYTE, base.canvas);
|
gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, gl.RGBA, gl.UNSIGNED_BYTE, base.canvas);
|
||||||
}
|
}
|
||||||
if (stampDirty) {
|
|
||||||
gl.activeTexture(gl.TEXTURE0 + 1);
|
|
||||||
gl.bindTexture(gl.TEXTURE_2D, stampTex);
|
|
||||||
gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, gl.RGBA, gl.UNSIGNED_BYTE, overlays.stampCanvas);
|
|
||||||
}
|
|
||||||
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);
|
||||||
gl.uniform1f(uni.flash, flash);
|
gl.uniform1f(uni.flash, flash);
|
||||||
gl.uniform1f(uni.stampAmt, stampAmt);
|
|
||||||
gl.uniform1f(uni.stampScale, stampScale);
|
|
||||||
gl.uniform1f(uni.scram, scram ? 1 : 0);
|
|
||||||
gl.uniform1f(uni.scramY, scramY);
|
|
||||||
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);
|
||||||
},
|
},
|
||||||
|
|||||||
@ -9,10 +9,9 @@
|
|||||||
* have ever shipped decides the intensity. Shipments arrive as waves, not switches —
|
* have ever shipped decides the intensity. Shipments arrive as waves, not switches —
|
||||||
* every param eases toward its target.
|
* every param eases toward its target.
|
||||||
*/
|
*/
|
||||||
import type { GameData, ItemDef, ScreenFX, SimEvent } from '../contracts';
|
import type { GameData, ScreenFX, SimEvent } from '../contracts';
|
||||||
import { createBaseFeed } from './baseTexture';
|
import { createBaseFeed } from './baseTexture';
|
||||||
import { createGlitchStack, type GlitchStack } from './glitchStack';
|
import { createGlitchStack, type GlitchStack } from './glitchStack';
|
||||||
import { createOverlays } from './overlays';
|
|
||||||
import { resolveLedger, unmappedItems, ESCALATION_ITEMS } from './corruptionMap';
|
import { resolveLedger, unmappedItems, ESCALATION_ITEMS } from './corruptionMap';
|
||||||
import { PARAMS, ease, zeroParams, type ParamSet } from './params';
|
import { PARAMS, ease, zeroParams, type ParamSet } from './params';
|
||||||
|
|
||||||
@ -24,24 +23,10 @@ const BROWNOUT_TAU = 0.12;
|
|||||||
const WOBBLE_THRESHOLD = 0.8;
|
const WOBBLE_THRESHOLD = 0.8;
|
||||||
const WOBBLE_DEPTH = 0.12;
|
const WOBBLE_DEPTH = 0.12;
|
||||||
/** Distinct slow rates per param so the wobble never reads as one global pulse. */
|
/** Distinct slow rates per param so the wobble never reads as one global pulse. */
|
||||||
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];
|
||||||
/** Seconds a shipment stamp stays on the feed. */
|
|
||||||
const STAMP_LIFE = 0.4;
|
|
||||||
/** A burst of shipments must not become a strobe: at most this many stamps wait their turn. */
|
|
||||||
const STAMP_QUEUE_MAX = 2;
|
|
||||||
|
|
||||||
interface Stamp { name: string; count: number; color: string; age: number }
|
|
||||||
|
|
||||||
const clamp01 = (x: number) => Math.min(1, Math.max(0, x));
|
|
||||||
function smoothstep(e0: number, e1: number, x: number): number {
|
|
||||||
const t = clamp01((x - e0) / (e1 - e0));
|
|
||||||
return t * t * (3 - 2 * t);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createScreenFX(): ScreenFX {
|
export function createScreenFX(): ScreenFX {
|
||||||
let stack: GlitchStack | null = null;
|
let stack: GlitchStack | null = null;
|
||||||
const overlays = createOverlays();
|
|
||||||
const items = new Map<string, ItemDef>();
|
|
||||||
const tally = new Map<string, number>();
|
const tally = new Map<string, number>();
|
||||||
const eased: ParamSet = zeroParams();
|
const eased: ParamSet = zeroParams();
|
||||||
const out: ParamSet = zeroParams();
|
const out: ParamSet = zeroParams();
|
||||||
@ -56,12 +41,6 @@ export function createScreenFX(): ScreenFX {
|
|||||||
let pendingFlash = false;
|
let pendingFlash = false;
|
||||||
let hasShipped = false;
|
let hasShipped = false;
|
||||||
|
|
||||||
const stampQueue: Stamp[] = [];
|
|
||||||
let stamp: Stamp | null = null;
|
|
||||||
let stampDirty = false;
|
|
||||||
let pendingScram = false;
|
|
||||||
let scramY = 0.5;
|
|
||||||
|
|
||||||
let lastMs = 0;
|
let lastMs = 0;
|
||||||
let frameCost = 0; // rolling avg ms of this module's CPU work
|
let frameCost = 0; // rolling avg ms of this module's CPU work
|
||||||
|
|
||||||
@ -81,22 +60,12 @@ export function createScreenFX(): ScreenFX {
|
|||||||
hasShipped = true;
|
hasShipped = true;
|
||||||
pendingFlash = true; // the awakening
|
pendingFlash = true; // the awakening
|
||||||
}
|
}
|
||||||
// One stamp per EVENT, not per unit: a 40-unit shipment is one big stamp.
|
|
||||||
const def = items.get(item);
|
|
||||||
stampQueue.push({
|
|
||||||
name: def?.name ?? item.toUpperCase(),
|
|
||||||
count,
|
|
||||||
color: def?.color ?? '#d8ffd8',
|
|
||||||
age: 0,
|
|
||||||
});
|
|
||||||
while (stampQueue.length > STAMP_QUEUE_MAX) stampQueue.shift();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
init(canvas: HTMLCanvasElement, data: GameData) {
|
init(canvas: HTMLCanvasElement, data: GameData) {
|
||||||
const base = createBaseFeed();
|
const base = createBaseFeed();
|
||||||
stack = createGlitchStack(canvas, base, overlays);
|
stack = createGlitchStack(canvas, base);
|
||||||
for (const it of data.items) items.set(it.id, it);
|
|
||||||
|
|
||||||
const unmapped = unmappedItems(data);
|
const unmapped = unmappedItems(data);
|
||||||
if (unmapped.length) {
|
if (unmapped.length) {
|
||||||
@ -105,19 +74,15 @@ export function createScreenFX(): ScreenFX {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Debug/authoring hook — dev-only (round 2 orders), stripped from prod builds.
|
// Debug/authoring hook — LANE-SIM does not ship yet, so THE SCREEN must be
|
||||||
// Console: __fktryScreen.ship('macroblock-bricks', 40)
|
// drivable by hand. Console: __fktryScreen.ship('melt', 40)
|
||||||
if (!import.meta.env.DEV) return;
|
|
||||||
(window as any).__fktryScreen = {
|
(window as any).__fktryScreen = {
|
||||||
ship,
|
ship,
|
||||||
brownout: (on: boolean) => (brownoutTarget = on ? 1 : 0),
|
brownout: (on: boolean) => (brownoutTarget = on ? 1 : 0),
|
||||||
scram: () => { pendingScram = true; scramY = 0.5; },
|
|
||||||
reset: () => {
|
reset: () => {
|
||||||
tally.clear();
|
tally.clear();
|
||||||
ledgerDirty = true;
|
ledgerDirty = true;
|
||||||
hasShipped = false;
|
hasShipped = false;
|
||||||
stamp = null;
|
|
||||||
stampQueue.length = 0;
|
|
||||||
for (const p of PARAMS) eased[p] = 0;
|
for (const p of PARAMS) eased[p] = 0;
|
||||||
},
|
},
|
||||||
stats: () => ({
|
stats: () => ({
|
||||||
@ -137,12 +102,6 @@ export function createScreenFX(): ScreenFX {
|
|||||||
for (const e of events) {
|
for (const e of events) {
|
||||||
if (e.kind === 'shipped') ship(e.item, e.count);
|
if (e.kind === 'shipped') ship(e.item, e.count);
|
||||||
else if (e.kind === 'brownout') brownoutTarget = e.on ? 1 : 0;
|
else if (e.kind === 'brownout') brownoutTarget = e.on ? 1 : 0;
|
||||||
else if (e.kind === 'scram' && e.on) {
|
|
||||||
// A machine scrammed: the tape loses tracking. Bar position is derived from the
|
|
||||||
// event rather than random, so the same run always wipes the same way.
|
|
||||||
pendingScram = true;
|
|
||||||
scramY = 0.15 + (((e.entity * 37 + e.tick * 13) % 100) / 100) * 0.7;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -170,32 +129,9 @@ export function createScreenFX(): ScreenFX {
|
|||||||
out[p] = Math.min(1, Math.max(0, eased[p] + w));
|
out[p] = Math.min(1, Math.max(0, eased[p] + w));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stamp lifecycle: promote the next queued shipment once the current one clears.
|
|
||||||
if (!stamp && stampQueue.length) {
|
|
||||||
stamp = stampQueue.shift()!;
|
|
||||||
overlays.paintStamp(stamp.name, stamp.count, stamp.color);
|
|
||||||
stampDirty = true;
|
|
||||||
}
|
|
||||||
let stampAmt = 0;
|
|
||||||
let stampScale = 1;
|
|
||||||
if (stamp) {
|
|
||||||
const a = stamp.age / STAMP_LIFE;
|
|
||||||
// Punch in hard, hold, then fade — a stamp, not a crossfade.
|
|
||||||
stampAmt = Math.min(1, a / 0.12) * (1 - smoothstep(0.5, 1, a));
|
|
||||||
// Bigger shipments stamp bigger, with a slight overshoot on impact.
|
|
||||||
stampScale = (0.72 + 0.28 * clamp01(stamp.count / 40)) * (1.18 - 0.18 * Math.min(1, a / 0.12));
|
|
||||||
stamp.age += dt;
|
|
||||||
if (stamp.age >= STAMP_LIFE) stamp = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const flash = pendingFlash ? 1 : 0;
|
const flash = pendingFlash ? 1 : 0;
|
||||||
stack.draw({
|
stack.draw({ params: out, timeSec, drift: 1, brownout, flash, idle: !hasShipped });
|
||||||
params: out, timeSec, drift: 1, brownout, flash, idle: !hasShipped,
|
|
||||||
stampAmt, stampScale, scram: pendingScram, scramY, stampDirty,
|
|
||||||
});
|
|
||||||
pendingFlash = false;
|
pendingFlash = false;
|
||||||
pendingScram = false;
|
|
||||||
stampDirty = false;
|
|
||||||
|
|
||||||
frameCost = frameCost * 0.95 + (performance.now() - t0) * 0.05;
|
frameCost = frameCost * 0.95 + (performance.now() - t0) * 0.05;
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,108 +0,0 @@
|
|||||||
/**
|
|
||||||
* Transient overlays composited ON the feed (in screen space, so corruption never drags
|
|
||||||
* them): the shipment stamp and the scram bar.
|
|
||||||
*
|
|
||||||
* Both are painted into small canvases and uploaded only when their event fires — never
|
|
||||||
* per frame. The stamp is small (256x128) precisely so the per-event upload stays cheap.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export const STAMP_W = 256;
|
|
||||||
export const STAMP_H = 128;
|
|
||||||
export const SCRAM_W = 512;
|
|
||||||
export const SCRAM_H = 64;
|
|
||||||
|
|
||||||
export interface Overlays {
|
|
||||||
stampCanvas: HTMLCanvasElement;
|
|
||||||
scramCanvas: HTMLCanvasElement;
|
|
||||||
/** Repaint the stamp for a landed shipment. OSHA-placard deadpan, in the item's chip colour. */
|
|
||||||
paintStamp(name: string, count: number, color: string): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
function roundRect(g: CanvasRenderingContext2D, x: number, y: number, w: number, h: number, r: number) {
|
|
||||||
g.beginPath();
|
|
||||||
g.moveTo(x + r, y);
|
|
||||||
g.arcTo(x + w, y, x + w, y + h, r);
|
|
||||||
g.arcTo(x + w, y + h, x, y + h, r);
|
|
||||||
g.arcTo(x, y + h, x, y, r);
|
|
||||||
g.arcTo(x, y, x + w, y, r);
|
|
||||||
g.closePath();
|
|
||||||
}
|
|
||||||
|
|
||||||
function paintScramBar(g: CanvasRenderingContext2D) {
|
|
||||||
// A white tape-tracking bar with the deadpan legend punched out of it.
|
|
||||||
g.clearRect(0, 0, SCRAM_W, SCRAM_H);
|
|
||||||
g.fillStyle = '#f6f6f2';
|
|
||||||
g.fillRect(0, 0, SCRAM_W, SCRAM_H);
|
|
||||||
// torn edges: the bar should not look like a clean rectangle
|
|
||||||
g.fillStyle = 'rgba(0,0,0,0)';
|
|
||||||
g.globalCompositeOperation = 'destination-out';
|
|
||||||
for (let x = 0; x < SCRAM_W; x += 7) {
|
|
||||||
const t = ((x * 37) % 11) / 11;
|
|
||||||
g.fillRect(x, 0, 7, 3 + t * 5);
|
|
||||||
g.fillRect(x, SCRAM_H - (3 + (1 - t) * 5), 7, 3 + (1 - t) * 5);
|
|
||||||
}
|
|
||||||
g.globalCompositeOperation = 'source-over';
|
|
||||||
g.fillStyle = '#0a0a0c';
|
|
||||||
g.font = "bold 30px 'Courier New', monospace";
|
|
||||||
g.textBaseline = 'middle';
|
|
||||||
const label = 'TRACKING LOST';
|
|
||||||
const w = g.measureText(label).width;
|
|
||||||
g.fillText(label, (SCRAM_W - w) / 2, SCRAM_H / 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createOverlays(): Overlays {
|
|
||||||
const stampCanvas = document.createElement('canvas');
|
|
||||||
stampCanvas.width = STAMP_W;
|
|
||||||
stampCanvas.height = STAMP_H;
|
|
||||||
const sg = stampCanvas.getContext('2d')!;
|
|
||||||
|
|
||||||
const scramCanvas = document.createElement('canvas');
|
|
||||||
scramCanvas.width = SCRAM_W;
|
|
||||||
scramCanvas.height = SCRAM_H;
|
|
||||||
paintScramBar(scramCanvas.getContext('2d')!);
|
|
||||||
|
|
||||||
return {
|
|
||||||
stampCanvas,
|
|
||||||
scramCanvas,
|
|
||||||
paintStamp(name: string, count: number, color: string) {
|
|
||||||
sg.clearRect(0, 0, STAMP_W, STAMP_H);
|
|
||||||
sg.save();
|
|
||||||
// Stamped by hand, badly: a few degrees off true.
|
|
||||||
sg.translate(STAMP_W / 2, STAMP_H / 2);
|
|
||||||
sg.rotate(-0.045);
|
|
||||||
sg.translate(-STAMP_W / 2, -STAMP_H / 2);
|
|
||||||
|
|
||||||
// dark backing so the legend survives on top of white SMPTE bars
|
|
||||||
sg.fillStyle = 'rgba(6,6,10,0.72)';
|
|
||||||
roundRect(sg, 12, 22, STAMP_W - 24, STAMP_H - 44, 6);
|
|
||||||
sg.fill();
|
|
||||||
|
|
||||||
sg.strokeStyle = color;
|
|
||||||
sg.lineWidth = 5;
|
|
||||||
roundRect(sg, 12, 22, STAMP_W - 24, STAMP_H - 44, 6);
|
|
||||||
sg.stroke();
|
|
||||||
|
|
||||||
sg.fillStyle = color;
|
|
||||||
sg.textBaseline = 'middle';
|
|
||||||
// Long codex names have to fit the placard — shrink until they do.
|
|
||||||
let size = 22;
|
|
||||||
sg.font = `bold ${size}px 'Courier New', monospace`;
|
|
||||||
while (sg.measureText(name).width > STAMP_W - 48 && size > 9) {
|
|
||||||
size -= 1;
|
|
||||||
sg.font = `bold ${size}px 'Courier New', monospace`;
|
|
||||||
}
|
|
||||||
const nw = sg.measureText(name).width;
|
|
||||||
sg.fillText(name, (STAMP_W - nw) / 2, STAMP_H / 2 - 12);
|
|
||||||
|
|
||||||
const qty = `×${count}`;
|
|
||||||
sg.font = "bold 26px 'Courier New', monospace";
|
|
||||||
const qw = sg.measureText(qty).width;
|
|
||||||
sg.fillText(qty, (STAMP_W - qw) / 2, STAMP_H / 2 + 20);
|
|
||||||
|
|
||||||
sg.fillStyle = 'rgba(255,255,255,0.5)';
|
|
||||||
sg.font = "9px 'Courier New', monospace";
|
|
||||||
sg.fillText('SHIPPED / NO QUESTIONS', 22, 34);
|
|
||||||
sg.restore();
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@ -1,11 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* The glitch parameters. The first eight are ported verbatim from the WIMVEE GLYTCH
|
* The eight glitch parameters, ported verbatim from the WIMVEE GLYTCH prototype.
|
||||||
* prototype — names and semantics are load-bearing: the fragment shader reads `u_<key>`
|
* Names and semantics are load-bearing: the fragment shader reads `u_<key>` and the
|
||||||
* and the prototype's tuning assumes these exact ranges (0..1 each). Their maths is
|
* prototype's tuning assumes these exact ranges (0..1 each).
|
||||||
* untouched.
|
|
||||||
*
|
|
||||||
* The last three (round 2) are artifact classes the original eight could not express:
|
|
||||||
* they are new shader passes, not new blends of old ones.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const PARAMS = [
|
export const PARAMS = [
|
||||||
@ -17,19 +13,13 @@ export const PARAMS = [
|
|||||||
'burn', // film burn blotches
|
'burn', // film burn blotches
|
||||||
'noise', // analog snow
|
'noise', // analog snow
|
||||||
'freeze', // time quantize + dropout
|
'freeze', // time quantize + dropout
|
||||||
'block', // 8x8 quantization: flat blocks, hard seams, posterized colour
|
|
||||||
'ring', // Gibbs ringing: oscillating halos hugging contrast edges
|
|
||||||
'moire', // interference lattice: beat pattern + rainbow sheen
|
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export type Param = (typeof PARAMS)[number];
|
export type Param = (typeof PARAMS)[number];
|
||||||
export type ParamSet = Record<Param, number>;
|
export type ParamSet = Record<Param, number>;
|
||||||
|
|
||||||
export function zeroParams(): ParamSet {
|
export function zeroParams(): ParamSet {
|
||||||
return {
|
return { mosh: 0, melt: 0, chroma: 0, tear: 0, roll: 0, burn: 0, noise: 0, freeze: 0 };
|
||||||
mosh: 0, melt: 0, chroma: 0, tear: 0, roll: 0, burn: 0, noise: 0, freeze: 0,
|
|
||||||
block: 0, ring: 0, moire: 0,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Frame-rate independent approach toward a target. `tau` = seconds to ~63% of the way. */
|
/** Frame-rate independent approach toward a target. `tau` = seconds to ~63% of the way. */
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user