Folds in the operator's rulings: Q1 cultmath-tapes, Q2 samples then resolution, Q3 the shader owns a dynamic ground and the CSS wash is sampled from its result. The nebula is flow-warped (two-phase flow map) with one flow texture owning motion for the warp, the TAA motion vectors, the hue and the stardust. Interval culling encloses the warp by enlarging the ball; interval-ops gains that test. New fourth cut site-stardust: stateless instanced particles plus an opacity-driven gather on the stardust layer, the web form of the raymarch-driven DoF. Budget re-checked: the full stack lands a mid-range phone at S = 4-6, not 8. Docs only. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
206 lines
19 KiB
JSON
206 lines
19 KiB
JSON
{
|
|
"campaign": "cultmath-tapes",
|
|
"cut": "site-ground",
|
|
"revision": 2,
|
|
"title": "Site ground: a per-pixel WebGL2 nebula under every page, flow-warped, interval-culled, TAA-reprojected, budget-controlled by measurement; the CSS wash is sampled from the result, seeds fall back",
|
|
"repo": "GameCult/gamecult-site",
|
|
"branch": "main",
|
|
"base": "535d170",
|
|
"depends_on": [
|
|
"site-masthead:cut_spec:wash-home-only.r1",
|
|
"cultmath-tapes:cut_spec:glsl-lowering.r1",
|
|
"cultmath-tapes:cut_spec:interval-ops.r1"
|
|
],
|
|
"first": [
|
|
"Map: F:/Projects/CultLib/docs/cultmath-interval-ground-cut.md, 'The shader design' (field, culling with the warp, TAA motion-aware, sampled wash, palette input), 'Budget', D4-D8, Q4 and Q5. Rulings in force: site-masthead:ruling:operator-chromeless-column; wash-home-only's authority map (no second wash token, no class toggled by script, no page rule setting a radial alpha). Re-pin base to main after wash-home-only lands; line numbers below are from 535d170; re-read by selector. Stardust and DoF are the next cut: leave the resolve pass with a named, empty composite slot for them (a uniform-gated branch is not needed; a comment and a function stub are).",
|
|
"Aetheria reference, read once, copy nothing: Volumetric.cginc:165-221 (two-phase flow-map warp, tri2 cross-fade), :130-135 (globalFlow); Raymarching/CloudShader.shader:83-104 (integration) and :261-314 (what not to do: gamma 0.5 clip, fixed 0.05 blend, packed depth, the density2 bug at :311).",
|
|
"Engine facts to confirm before writing: how GameCult-Quartz bundles a component's afterDOMLoaded and whether a dynamic import() of /static/ground/ground.js is allowed, else emit a module script tag; site/quartz/static/** served at /static/**; enableSPA false (quartz.config.ts:15). Local build: scripts/quartz/quartz.ps1 dev.",
|
|
"Tokens. Inputs (Q4 a), on body in custom.scss beside --gamecult-wash: --gamecult-nebula-0/1/2 (\"255 138 42\", \"109 96 255\", \"89 183 255\") and --gamecult-nebula-base (#07111a). Outputs with seeds, on :root: --gamecult-ground-0/1/2 (#03070d #07111a #09141f), and for N in 0..2 --gamecult-light-N (rgb triple), -a (0.18/0.16/0.14), -at (\"78% 14%\"/\"18% 10%\"/\"52% 0%\"), -r (18%/24%/28%), plus --gamecult-ground-mean (#07111a). The body background becomes radial-gradient(circle at var(--gamecult-light-0-at), rgb(var(--gamecult-light-0) / calc(var(--gamecult-light-0-a) * var(--gamecult-wash))), transparent var(--gamecult-light-0-r)) x3 over linear-gradient(180deg, var(--gamecult-ground-0) 0%, var(--gamecult-ground-1) 44%, var(--gamecult-ground-2) 100%).",
|
|
"Ritual essays: their seeds go under :root:has(body[data-slug=\"Blog/...\"]) for the three slugs (yellow 255 178 30 .15 at 78% 8% 24rem; cobalt 49 88 255 .22 at 18% 4% 30rem; violet 91 54 184 .19 at 44% 20% 34rem; stops #030414 0%, #070b24 42%, #040514 100%; nebula inputs likewise overridden to yellow/cobalt/violet and base #050614). The ritual block's own background declaration is deleted; it inherits body's.",
|
|
"ground.js owns: WebGL2 context on #gamecult-ground (alpha:false, antialias:false, powerPreference 'low-power'); capability check (WebGL2, EXT_color_buffer_half_float or float); input token read via getComputedStyle(document.body) -> uniforms; the flow texture pass (quarter res, RG16F, two snoise per texel, once per frame); the march pass; the TAA resolve with reprojection; the 8x8 downsample and the async PBO readback; the token writer on document.documentElement.style; the scheduler (30 fps cadence, visibilityState pause, reduced-motion freeze and stop at 32 frames); the budget controller; the debug flag (?ground=debug) and parity flags.",
|
|
"Sampled wash: after the resolve, draw the final frame into an 8x8 RGBA8 target; gl.readPixels into a PIXEL_PACK_BUFFER; gl.fenceSync; on later frames poll getSyncParameter(SYNC_STATUS) and only when SIGNALED call getBufferSubData. Never readPixels to client memory directly (that stalls). At most 2 Hz. Compute the token values as the map states (row means for the stops; light colour and excess at the seed light's centre texel; the mean for --gamecult-ground-mean) and write them with setProperty on :root only when a value moved by more than 1/255. Q5 (a): keep the last written set in sessionStorage under one key and apply it on load before the first frame; it is a cache, and a missing or malformed entry means the seeds.",
|
|
"Budget controller, one function, one state machine: GPU time per frame by EXT_disjoint_timer_query_webgl2 when present, else a fenceSync after the resolve polled to completion, else rAF cadence. Over a 60-frame window: if the median exceeds the budget (4.0 ms), step down: S 16 -> 8 -> 4, then dpr -> 1 (Q2), then remove the canvas (the seeds paint). Never step up within a session. A median rising 50% over 5 minutes also steps down. No storage of the verdict, no userAgent, no maxTouchPoints.",
|
|
"cloud.frag.glsl: #version 300 es; precision highp float; precision highp int; the vendored CultMath.glsl is concatenated by ground.js before this file's body. The field, warp, interval march (ball radius F0 * (Δ/2 + D) with D = FLOW_MAX * PERIOD / 2) and integration exactly as the map's pseudocode; hue from the flow texture's channels mixed over the three nebula inputs; uniforms: resolution, time, dt, frame, jitter, S, intensity, nebula colours, base, Z0/Z1, F0, CUTOFF, K, PERIOD, FLOW_MAX. flow.frag.glsl: the two low-frequency snoise. taa.frag.glsl: motion vector from the flow texture (mv = project(-flow * dt)), history fetched at uv - mv, 3x3 min/max clamp, alpha = max(1/frames, 1/32), out-of-bounds alpha 1; writes new history and the screen (luminance clamp 0.035) via drawBuffers or two draws.",
|
|
"Vendoring (D3): site/quartz/static/ground/CultMath.glsl byte-equal to CultLib shaders/CultMath.glsl at the commit named in its first line. scripts/ground/check-cultmath-glsl.ps1 diffs it against git show <sha>:packages/cultmath/shaders/CultMath.glsl from a CultLib checkout and exits non-zero on drift.",
|
|
"Parity on the device: with ?ground=parity, ground.js evaluates the vendored fixture (CultLib tests/CultMath.Tests/fixtures/glsl-parity.json) one pixel per point, reads back float bit patterns, and reports per function the max ulp distance and the count outside its tolerance class. With ?ground=parity-wash, density and stardust are forced to 0 and the flow frozen; the canvas must equal the CSS seed wash within 2/255 at 16 points on / and on a ritual essay (this pins that the seed values and the shader's base agree at zero density, which is what the fallback promises).",
|
|
"Prove in the browser pane: max relative luminance after 32 frames on / at intensity 1 (<= 0.035); the parity-wash probe; the debug readback (mean evals per pixel, unfinished-ray fraction < 1%); the token writer (after 2 s on /, :root's inline style carries all fifteen tokens and --gamecult-ground-mean, each within the expected range, and getComputedStyle(body).backgroundImage resolves to the sampled values); the motion test (with ?ground=debug-mv the resolve writes |mv| into red: non-zero where flow is non-zero, zero under reduced motion); GPU ms per pass with the timer query at the pane's desktop size and at the mobile preset's pixel count. Phone GPU time is operator verification."
|
|
],
|
|
"deletes": [
|
|
{
|
|
"path": "site/quartz/styles/custom.scss",
|
|
"lines": 6,
|
|
"note": "ritual body block 119-124: its own background (three radials and the linear gradient) goes; its seeds live under :root:has(body[data-slug]) and it inherits body's ground."
|
|
},
|
|
{
|
|
"path": "site/quartz/styles/custom.scss",
|
|
"lines": 9,
|
|
"note": "body 10-27: the six literal colours and three literal stops leave as literals (rewritten as token references in the same lines; counted 9 removed, 9 added)."
|
|
}
|
|
],
|
|
"keeps_moves": [
|
|
"--gamecult-wash and the index override from wash-home-only: untouched; the shader reads it as intensity and the CSS wash still multiplies by it.",
|
|
".page::before (the grid overlay), the tour/Pitch VN ground, the Thing deck's .hero-bg, the masthead, the engine: untouched.",
|
|
"The CSS wash stays the ground when the canvas is absent, not yet drawn, or removed by the controller; it renders the sampled tokens when there are any and the seeds otherwise.",
|
|
"docs/brand-design-language.md 'The ground is a wash, not a flat fill' (69-84): rewritten: the ground is the shader; the CSS wash is sampled from it; the seeds are today's literal wash; the four nebula inputs are where the palette is tuned."
|
|
],
|
|
"adds": [
|
|
"site/quartz/components/GameCultGround.tsx: renders <canvas id=\"gamecult-ground\" aria-hidden=\"true\"></canvas>, mounts /static/ground/ground.js after DOM load; in sharedPageComponents.header after GameCultThemeLock (quartz.layout.ts:19). Its beforeDOMLoaded script applies the sessionStorage token cache to :root if present (Q5 a), so the first paint matches the last sample.",
|
|
"site/quartz/static/ground/ground.js (~400 lines, no dependencies, no build step): context, tokens in and out, flow/march/resolve/downsample passes, PBO readback, scheduler, controller, debug and parity modes, a named composite slot for the stardust cut.",
|
|
"site/quartz/static/ground/flow.frag.glsl, cloud.frag.glsl, taa.frag.glsl, downsample.frag.glsl; a shared 3-vertex full-screen vertex shader inline in ground.js.",
|
|
"site/quartz/static/ground/CultMath.glsl and glsl-parity.json, vendored from CultLib with the pin in the header.",
|
|
"scripts/ground/check-cultmath-glsl.ps1: the vendoring drift check.",
|
|
"custom.scss: the four nebula input tokens on body; the fifteen ground seeds plus --gamecult-ground-mean on :root with the ritual overrides under :root:has(body[data-slug]); #gamecult-ground { position: fixed; inset: 0; width: 100%; height: 100%; z-index: -1; pointer-events: none; }.",
|
|
"docs/brand-design-language.md: the wash section rewritten as above; one paragraph on the ground shader: what it reads (inputs), what it writes (outputs), what it may not hold, and the fallback."
|
|
],
|
|
"file_changes": [
|
|
{
|
|
"location": { "path": "site/quartz/styles/custom.scss", "line": 4, "end_line": 8 },
|
|
"change": ":root block: the fifteen ground seeds and --gamecult-ground-mean declared beside color-scheme."
|
|
},
|
|
{
|
|
"location": { "path": "site/quartz/styles/custom.scss", "line": 10, "end_line": 31 },
|
|
"change": "body: four nebula input tokens declared; radials and the linear gradient reference the ground tokens; alphas keep the wash-home-only calc."
|
|
},
|
|
{
|
|
"location": { "path": "site/quartz/styles/custom.scss", "line": 103, "end_line": 124 },
|
|
"change": "ritual block: background declaration deleted; a :root:has(body[data-slug]) rule carries its seeds and nebula inputs."
|
|
},
|
|
{
|
|
"location": { "path": "site/quartz.layout.ts", "line": 19 },
|
|
"change": "header: [GameCultThemeLock(), GameCultGround(), GameCultMasthead()]."
|
|
},
|
|
{
|
|
"location": { "path": "docs/brand-design-language.md", "line": 69, "end_line": 84 },
|
|
"change": "wash section rewritten: shader-owned ground, sampled wash, seeds, nebula inputs."
|
|
}
|
|
],
|
|
"authority_map": {
|
|
"owner": "The shader owns the ground. custom.scss owns the four nebula inputs, --gamecult-wash and the seed values; ground.js owns the flow texture, the march, the TAA, the readback and the sampled ground tokens on :root; CultLib owns every noise and interval function the field calls.",
|
|
"inputs": [
|
|
"the four nebula tokens and --gamecult-wash (computed on body)",
|
|
"prefers-reduced-motion, visibilityState, canvas size, devicePixelRatio, elapsed time",
|
|
"measured GPU frame time (timer query, fence, or rAF cadence)",
|
|
"the shader's own 8x8 readback",
|
|
"the vendored CultMath.glsl at its pinned CultLib commit"
|
|
],
|
|
"outputs": [
|
|
"one opaque moving canvas under the page, luminance <= 0.035",
|
|
"the fifteen ground tokens and --gamecult-ground-mean on :root inline, at most 2 Hz",
|
|
"debug and parity readbacks (console, flag only)"
|
|
],
|
|
"derived_state": [
|
|
"the ground tokens are derived from the shader's result while it runs; the seeds are the fallback; the CSS wash renders whichever is in force; the shader never reads them",
|
|
"the flow texture is the one owner of motion: warp, hue, motion vectors (and the stardust, next cut) read it",
|
|
"the TAA history is a cache, reprojected by the flow; reset on resize, first frame and budget step",
|
|
"S and dpr are derived from measurement within the session; never stored",
|
|
"the sessionStorage token set is a cache of the last sample; absent or malformed means seeds",
|
|
"CultMath.glsl in the site is derived from CultLib at the pinned commit"
|
|
],
|
|
"forbidden_writers": [
|
|
"no colour, alpha, position or radius literal in ground.js or any GLSL file",
|
|
"no read of --gamecult-ground-* or --gamecult-light-* by the shader or by ground.js, except the seed read before the first readback",
|
|
"no navigator.userAgent, platform, maxTouchPoints, or any device-name branch",
|
|
"no class or attribute set on body or html by ground.js; token writes are setProperty on :root only",
|
|
"no second wash token; no page rule setting a radial alpha; no second flow evaluation outside flow.frag.glsl",
|
|
"no noise or interval function defined in the site's GLSL; only cultmath_* calls",
|
|
"no synchronous readPixels to client memory; no setInterval; one requestAnimationFrame scheduler; localStorage never"
|
|
],
|
|
"shared_paths": [
|
|
"home, every content page and the ritual essays paint one canvas and one token set, in CSS and in the shader",
|
|
"slow device, hidden tab, reduced motion, missing WebGL2 and missing half-float all pass through the one controller",
|
|
"the 8x8 readback is the only path from the shader to CSS; the beforeDOMLoaded cache apply and the live writer set the same tokens the same way",
|
|
"the same Deploy Quartz workflow; static files ship with the overlay"
|
|
],
|
|
"deletion_line": "The ritual block's own background and the nine body literals go in the same commit the tokens are declared. The canvas, four shaders, ground.js and the vendored library are the whole addition; nothing else in the engine or the content changes."
|
|
},
|
|
"verification": {
|
|
"builds": [
|
|
"Deploy Quartz green on the landing commit: gh run list -R GameCult/gamecult-site -w \"Deploy Quartz\" -L 1",
|
|
"scripts/ground/check-cultmath-glsl.ps1 exits 0 against a CultLib checkout at the pinned commit",
|
|
"the built stylesheet contains rgb(var(--gamecult-light-0) / calc(var(--gamecult-light-0-a) * var(--gamecult-wash))) verbatim and a :root:has(body[data-slug=\"Blog/small-ritual-of-reach\"]) rule"
|
|
],
|
|
"tests": [
|
|
{
|
|
"name": "browser pane, /?ground=debug: max relative luminance over the canvas after 32 frames <= 0.035",
|
|
"pins": "text contrast: body text keeps >= 7:1 on every ground pixel."
|
|
},
|
|
{
|
|
"name": "browser pane, /?ground=parity-wash and a ritual essay: canvas vs CSS seed wash within 2/255 at 16 points with density 0 and flow frozen",
|
|
"pins": "the seeds and the shader's base agree; the fallback promise holds."
|
|
},
|
|
{
|
|
"name": "browser pane, / after 2 s: :root inline style carries the sixteen tokens; body's computed backgroundImage resolves to the sampled values; a read 1 s later differs only where the frame changed",
|
|
"pins": "the CSS wash is sampled from the shader, at most 2 Hz, without a stall (no readPixels-to-client in the profiler)."
|
|
},
|
|
{
|
|
"name": "browser pane, /?ground=parity: every fixture function within its tolerance class on WebGL2; numbers and GPU string in the report",
|
|
"pins": "GLSL = C# on a device; the proving ground's third runtime."
|
|
},
|
|
{
|
|
"name": "browser pane, /?ground=debug at desktop and the mobile preset: mean evals per pixel, unfinished rays < 1%, GPU ms per pass; ?ground=debug-mv shows motion where flow is non-zero",
|
|
"pins": "the step budget, the interval saving with the warp, and the reprojection on the shipped field."
|
|
},
|
|
{
|
|
"name": "reduced motion emulated: frames stop at 32, mv is 0, time frozen; hidden tab: no frames; WebGL2 disabled: no canvas in the DOM and the seed wash paints; sessionStorage cleared: seeds paint first",
|
|
"pins": "every fallback goes through the controller and lands on the same CSS ground."
|
|
},
|
|
{
|
|
"name": "git diff base..head --stat: custom.scss, quartz.layout.ts, docs/brand-design-language.md, the new component, static/ground/*, scripts/ground/* only",
|
|
"pins": "no engine, content or masthead change."
|
|
}
|
|
],
|
|
"negative": [
|
|
{
|
|
"pattern": "#[0-9a-fA-F]{6}\\b|rgba?\\(|\\b(255|138|109|183)\\b",
|
|
"scope": "site/quartz/static/ground/*.js and *.glsl: 0 matches."
|
|
},
|
|
{
|
|
"pattern": "gamecult-ground-|gamecult-light-",
|
|
"scope": "site/quartz/static/ground/*.glsl: 0 matches; in ground.js only inside the token writer and the seed read, never in a uniform upload."
|
|
},
|
|
{
|
|
"pattern": "userAgent|navigator\\.platform|maxTouchPoints|localStorage|setInterval|classList|setAttribute\\(",
|
|
"scope": "site/quartz/static/ground/ground.js: 0 matches."
|
|
},
|
|
{
|
|
"pattern": "readPixels\\([^)]*(Uint8Array|Float32Array|Uint16Array)",
|
|
"scope": "ground.js: 0 matches outside the parity and debug readbacks, which are flag-gated and named."
|
|
},
|
|
{
|
|
"pattern": "float cultmath_|vec4 cultmath_|float snoise|hash\\(",
|
|
"scope": "flow/cloud/taa/downsample.frag.glsl: 0 matches; cultmath_* is defined only in the vendored CultMath.glsl."
|
|
},
|
|
{
|
|
"pattern": "radial-gradient\\([^)]*rgba?\\([0-9]",
|
|
"scope": "site/quartz/styles/custom.scss, body rule and ritual block: 0 matches."
|
|
},
|
|
{
|
|
"pattern": "gamecult-titlebar|gamecult-nav-link|sidebar|composite|GameCult-Quartz",
|
|
"scope": "git diff base..head changed lines: 0 matches."
|
|
}
|
|
],
|
|
"operator": [
|
|
"open / on the desktop and on the phone; say whether the nebula's motion and intensity are right (uniforms: PERIOD, FLOW_MAX, intensity)",
|
|
"on the phone with ?ground=debug, read the logged GPU ms and the controller's final S/dpr; say whether it got warm after five minutes",
|
|
"navigate / -> /Blog -> back; say whether the ground is continuous or shows a seed flash (Q5)",
|
|
"open a ritual essay and confirm its ground still reads as its own paper"
|
|
]
|
|
},
|
|
"estimate": {
|
|
"lines_added": 980,
|
|
"lines_removed": 15,
|
|
"dependencies_added": [],
|
|
"dependencies_removed": [],
|
|
"formats_added": ["GLSL ES 3.00 shader files in static/ground", "vendored glsl-parity.json", "one sessionStorage key holding the last sampled token set"],
|
|
"formats_removed": [],
|
|
"targets_added": [],
|
|
"targets_removed": []
|
|
},
|
|
"rulings": [
|
|
"site-masthead:ruling:operator-chromeless-column"
|
|
],
|
|
"questions": [
|
|
"Q4 in the map: palette input tokens (a, recommended)",
|
|
"Q5 in the map: carry the last sampled wash across page loads (a, recommended)"
|
|
]
|
|
}
|