Every contributor and build step follows THIS file. Do not invent alternative shapes. The 99 effect
modules are already extracted (deterministically, verbatim) into src/effects/. Build everything
else to fit them.
src/effects/shader/<id>.js×11 —export default { meta, glsl: '<void main(){...}>' }src/effects/canvas/<id>.js×82 —export default { meta, draw: function(ctx,w,h,t,mx,my,state){...} }src/effects/dom/<id>.js×6 —export default { meta, selector, _raw, init: null }(NEEDS adaptation, see §4)src/registry.js— GENERATED barrel:export const REGISTRY = { id: module },export const IDSsrc/runtime/preamble.js—export const PRE(verbatim GLSL header:#version 300 es, precision, uniformsiResolution/iTime/iMouse,out vec4 o, helpershash/noise/fbm)src/runtime/noise.js—export const NOISE = { vhash, vnoise }. Canvas effects that callthis.vhash/this.vnoise(onlytopo,heatmap) require the runtime to invokedrawwiththis === NOISE.build/sections.js—export const SECTIONS(12, ordered)build/taxonomy.js— closedTAGS_ALL,VIBES,validateTaxonomy(meta)
Fields already present in every module (stub values where authored fields are blank):
id, kind, name, nameLocal, section, summary, description, descriptionZh, tags, vibe, culture, accuracyNote, perf{gpu,cpu,mobileSafe}, interactive{followsCursor,trigger}, reducedMotion, deterministic, state, license, attribution.
- Authored (metadata pass fills):
summary(≤120 chars, vibe-first: mood+visual, NOT tech),description(1–3 EN sentences),tags(4–8 fromTAGS_ALL),vibe(1–4 fromVIBES),culture(clean string or null),accuracyNote(set ONLY for procedural approximations of real cultural patterns), and corrections tointeractive.trigger(none|hover|click|scroll|auto),reducedMotion(freeze|static|crossfade|animate),deterministic(false for unseededMath.random/livenew Date()),state({persistent:true,notes}for effects that don't clear each frame),attribution({author,url}whenlicense==='custom'). - Pre-filled deterministically (refine only if wrong):
id, kind, name, nameLocal, section, descriptionZh, perf, interactive.followsCursor, license='MIT'. source,snippet,mountSnippet,runtimeNotes,url,previeware DERIVED by build — never authored, never stored in modules.
JSON Schema lives at schema/effect.schema.json (build it from this list).
scheduler.js: ONErequestAnimationFrameloop for all mounted effects.IntersectionObserversetscanvas.__vis; skip drawing off-screen tiles. Eachdraw(t)wrapped intry/catch(one effect erroring never breaks others).t= seconds (performance.now()/1000from a shared origin).ResizeObserverkeeps physical size in sync;DPR = Math.min(2, devicePixelRatio||1). ReadsmatchMedia('(prefers-reduced-motion: reduce)'); under reduce, honor each effect'smeta.reducedMotion(defaultfreeze= render exactly one frame then stop scheduling it).gl.js:makeGL(canvas, glsl)builds a WebGL2 context usingPRE + glsl, full-screen triangle VS (in vec2 p; void main(){ gl_Position=vec4(p,0,1); }), uniformsiResolution(physical px),iTime(s),iMouse(physical px, Y-flipped, defaults to center when no pointer). Keep a module-level live-context counter; refuse beyond cap (default 8) returningnull+ a clearconsole.error(do NOT throw). Decrement on stop/webglcontextlost.canvas.js: per-tile 2D context withctx.setTransform(dpr,0,0,dpr,0,0)(coords in CSS px). Callmodule.draw.call(NOISE, ctx, w, h, t, mx, my, state)—this===NOISEsothis.vhash/this.vnoiseresolve.mx,myin CSS px,-9999when not hovering.state= a per-mount{}persisted across frames.dom.js: dispatches an adapted dom effect'sinit(root).mount.js:mount(el, id, opts) -> handlewherehandle.stop()releases everything (rAF dereg, GL context + counter decrement, observers, listeners, timers). Route byREGISTRY[id].meta.kind. This lifecycle is what the original demo lacked — get it right (no leaks on repeated mount/stop).index.js:export { mount, stopAll, REGISTRY, IDS, listEffects }.listEffects()returns the meta array.
buildSnippet(module) -> { snippet, mountSnippet }. snippet is a COMPLETE zero-dependency,
paste-anywhere block that runs with no imports. One switch (kind):
- shader: a
<canvas>+ inlined minimalmakeGLharness with thePREstring + the effect'sglsl+ a self-contained rAF loop + pointer handling. Must run standalone. - canvas: a
<canvas>+ inlinedsetTransform(dpr)+ a self-contained rAF loop calling thedrawbody. If the effect usesthis.vhash/this.vnoise, inlinevhash+vnoiseand calldraw.call({vhash,vnoise}, …). - dom: the sentinel HTML (from
selector) + the adaptedinitinlined. Prependattribution(author/url) andaccuracyNoteas code comments so they travel with copied code.mountSnippet: short form for runtime users —import { mount } from 'fx-lab'; mount(el, '<id>');.
Each src/effects/dom/<id>.js has _raw (the original initXxx(){...} using this.root,
this.domSteps, timers). Rewrite into init(root) => ({ step?(t){}, stop(){} }):
this.root -> root; pushes to this.domSteps -> return a step(t); event listeners + timers
must be removable in stop(). Keep behavior identical. Delete _raw and init:null when done.
Import src/registry.js. Validate every meta against schema/effect.schema.json AND
validateTaxonomy. FAIL the build (non-zero exit) on: duplicate id; unknown tag/vibe;
license==='custom' with null attribution. Then derive source/snippet/mountSnippet/
runtimeNotes/url and write into site/:
site/fx-index.json—{ name, version, homepage, sections, effects:[meta-without-source + url + preview] }(validate vsschema/manifest.schema.json)site/effects/<id>.json— full meta + source + snippet + mountSnippet + runtimeNotes (self-contained)site/effects/<id>.md— H1name (kind)→ blockquote (visual + mood) → metadata block →## Drop-in snippet(fenced) →## Runtime notes→## Known limitations(accuracyNote + non-determinism/accumulator + WebGL cap for shaders)site/llms.txt— H1FX Lab; blockquote (what it is + runtime contract in one breath); one body paragraph printing theVIBESvocabulary once; then 12## <section.label>with lines- [name](<homepage>/effects/<id>.md): <summary>; a trailing## Optionalfor the heaviest/niche effects (perf.cpu==='high' or mobileSafe===false) so short-context agents can skip.site/llms-full.txt— all<id>.mdconcatenated with---; inline canvas snippets; for shaders link the glsl in<id>.jsonrather than inlining.- copy
schema/effect.schema.jsonintosite/schema/.runtimeNotesassembled from kind+perf+state+cap (shaders: "Browsers cap ~8–16 live WebGL2 contexts; safe up to ~8 shader effects per page.").
Reads fx-index.json, renders 12 sections of tiles, mounts each via the runtime (dist/fx-lab.esm.js or src/index.js during dev). <head> MUST carry
<link rel="alternate" type="text/markdown" href="/llms.txt"> and <meta name="llms-txt" content="/llms.txt">. Respect prefers-reduced-motion.
(1) Node: import registry, assert each meta validates + taxonomy passes + no duplicate id.
(2) Headless (Playwright, skip gracefully if unavailable): load each generated snippet standalone in a page, wait 2 rAF, assert no uncaught error and a non-blank canvas where applicable. The snippet is the ONLY use path — this guards "paste = runs". Skip pixel asserts where deterministic===false.
Repo root = the directory containing package.json. ESM everywhere ("type":"module"). No runtime deps.
site/ is generated — never hand-edit except index.html is a template that reads the manifest.
No MCP, no CLI (non-goals). The paste IS the install.