Skip to content

Commit a9ad7b9

Browse files
committed
fx-lab v0.1: 99 zero-dependency web effects + LLM-discoverable catalog
- Runtime: single rAF scheduler (visibility-gated, prefers-reduced-motion), mount/stop lifecycle, WebGL2 context guard, framework-agnostic ESM. - 99 effects (11 shader / 82 canvas / 6 dom) as co-located meta+code modules. - Build fans out a single source of truth into a gallery + llms.txt / llms.zh.txt + fx-index.json manifest + per-effect .md/.json with self-contained snippets. - Gallery: lazy mount/unmount, loading effect, EN/中文 i18n. - Tests: validate (schema + taxonomy gates) + headless render of every snippet. License: MIT.
0 parents  commit a9ad7b9

440 files changed

Lines changed: 32439 additions & 0 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/pages.yml

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
name: GitHub Pages
2+
3+
on:
4+
push:
5+
branches: [main]
6+
7+
permissions:
8+
contents: read
9+
pages: write
10+
id-token: write
11+
12+
concurrency:
13+
group: pages
14+
cancel-in-progress: false
15+
16+
jobs:
17+
build-and-test:
18+
runs-on: ubuntu-latest
19+
steps:
20+
- name: Checkout
21+
uses: actions/checkout@v4
22+
23+
- name: Setup Node.js
24+
uses: actions/setup-node@v4
25+
with:
26+
node-version: '20'
27+
28+
- name: Install dependencies
29+
run: npm ci
30+
31+
- name: Validate
32+
run: node build/validate.mjs
33+
34+
- name: Build
35+
run: node build/build.mjs
36+
37+
- name: Test
38+
run: node test/smoke.mjs
39+
40+
deploy:
41+
needs: build-and-test
42+
runs-on: ubuntu-latest
43+
environment:
44+
name: github-pages
45+
url: ${{ steps.deployment.outputs.page_url }}
46+
steps:
47+
- name: Checkout
48+
uses: actions/checkout@v4
49+
50+
- name: Setup Node.js
51+
uses: actions/setup-node@v4
52+
with:
53+
node-version: '20'
54+
55+
- name: Install dependencies
56+
run: npm ci
57+
58+
- name: Build site
59+
run: node build/build.mjs
60+
61+
- name: Upload artifact
62+
uses: actions/upload-pages-artifact@v3
63+
with:
64+
path: site
65+
66+
- name: Deploy to GitHub Pages
67+
id: deployment
68+
uses: actions/deploy-pages@v4

.gitignore

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
node_modules/
2+
.DS_Store
3+
*.log
4+
.npmrc
5+
# internal planning/roadmap doc (AI-workflow + model-selection notes) — kept local, not published
6+
PLAN.md
7+
test-results/
8+
playwright-report/
9+
.playwright/
10+
# build outputs are intentionally committed (dist/, site/) for zero-infra GitHub Pages

CONTRACT.md

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
# fx-lab — build contract (authoritative spec)
2+
3+
Every contributor and build step follows THIS file. Do not invent alternative shapes. The 99 effect
4+
modules are already extracted (deterministically, verbatim) into `src/effects/`. Build everything
5+
else to fit them.
6+
7+
## 0. Ground truth already in the repo
8+
9+
- `src/effects/shader/<id>.js` ×11 — `export default { meta, glsl: '<void main(){...}>' }`
10+
- `src/effects/canvas/<id>.js` ×82 — `export default { meta, draw: function(ctx,w,h,t,mx,my,state){...} }`
11+
- `src/effects/dom/<id>.js` ×6 — `export default { meta, selector, _raw, init: null }` (NEEDS adaptation, see §4)
12+
- `src/registry.js` — GENERATED barrel: `export const REGISTRY = { id: module }`, `export const IDS`
13+
- `src/runtime/preamble.js``export const PRE` (verbatim GLSL header: `#version 300 es`, precision, uniforms `iResolution`/`iTime`/`iMouse`, `out vec4 o`, helpers `hash/noise/fbm`)
14+
- `src/runtime/noise.js``export const NOISE = { vhash, vnoise }`. Canvas effects that call `this.vhash`/`this.vnoise` (only `topo`, `heatmap`) require the runtime to invoke `draw` with `this === NOISE`.
15+
- `build/sections.js``export const SECTIONS` (12, ordered)
16+
- `build/taxonomy.js` — closed `TAGS_ALL`, `VIBES`, `validateTaxonomy(meta)`
17+
18+
## 1. Effect metadata schema (the `meta` object)
19+
20+
Fields already present in every module (stub values where authored fields are blank):
21+
`id, kind, name, nameLocal, section, summary, description, descriptionZh, tags, vibe, culture,
22+
accuracyNote, perf{gpu,cpu,mobileSafe}, interactive{followsCursor,trigger}, reducedMotion,
23+
deterministic, state, license, attribution`.
24+
25+
- **Authored** (metadata pass fills): `summary` (≤120 chars, **vibe-first**: mood+visual, NOT tech), `description` (1–3 EN sentences), `tags` (4–8 from `TAGS_ALL`), `vibe` (1–4 from `VIBES`), `culture` (clean string or null), `accuracyNote` (set ONLY for procedural approximations of real cultural patterns), and corrections to `interactive.trigger` (`none|hover|click|scroll|auto`), `reducedMotion` (`freeze|static|crossfade|animate`), `deterministic` (false for unseeded `Math.random`/live `new Date()`), `state` (`{persistent:true,notes}` for effects that don't clear each frame), `attribution` (`{author,url}` when `license==='custom'`).
26+
- **Pre-filled deterministically** (refine only if wrong): `id, kind, name, nameLocal, section, descriptionZh, perf, interactive.followsCursor, license='MIT'`.
27+
- `source`, `snippet`, `mountSnippet`, `runtimeNotes`, `url`, `preview` are **DERIVED by build** — never authored, never stored in modules.
28+
29+
JSON Schema lives at `schema/effect.schema.json` (build it from this list).
30+
31+
## 2. Runtime — `src/runtime/`
32+
33+
- `scheduler.js`: ONE `requestAnimationFrame` loop for all mounted effects. `IntersectionObserver` sets `canvas.__vis`; skip drawing off-screen tiles. Each `draw(t)` wrapped in `try/catch` (one effect erroring never breaks others). `t` = seconds (`performance.now()/1000` from a shared origin). `ResizeObserver` keeps physical size in sync; `DPR = Math.min(2, devicePixelRatio||1)`. Reads `matchMedia('(prefers-reduced-motion: reduce)')`; under reduce, honor each effect's `meta.reducedMotion` (default `freeze` = render exactly one frame then stop scheduling it).
34+
- `gl.js`: `makeGL(canvas, glsl)` builds a WebGL2 context using `PRE + glsl`, full-screen triangle VS (`in vec2 p; void main(){ gl_Position=vec4(p,0,1); }`), uniforms `iResolution`(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) returning `null` + a clear `console.error` (do NOT throw). Decrement on stop/`webglcontextlost`.
35+
- `canvas.js`: per-tile 2D context with `ctx.setTransform(dpr,0,0,dpr,0,0)` (coords in CSS px). Call `module.draw.call(NOISE, ctx, w, h, t, mx, my, state)``this===NOISE` so `this.vhash/this.vnoise` resolve. `mx,my` in CSS px, **`-9999` when not hovering**. `state` = a per-mount `{}` persisted across frames.
36+
- `dom.js`: dispatches an adapted dom effect's `init(root)`.
37+
- `mount.js`: **`mount(el, id, opts) -> handle` where `handle.stop()` releases everything** (rAF dereg, GL context + counter decrement, observers, listeners, timers). Route by `REGISTRY[id].meta.kind`. This lifecycle is what the original demo lacked — get it right (no leaks on repeated mount/stop).
38+
- `index.js`: `export { mount, stopAll, REGISTRY, IDS, listEffects }`. `listEffects()` returns the meta array.
39+
40+
## 3. snippet.js (build/) — the field an LLM pastes
41+
42+
`buildSnippet(module) -> { snippet, mountSnippet }`. `snippet` is a COMPLETE zero-dependency,
43+
paste-anywhere block that runs with no imports. One `switch (kind)`:
44+
- **shader**: a `<canvas>` + inlined minimal `makeGL` harness with the `PRE` string + the effect's `glsl` + a self-contained rAF loop + pointer handling. Must run standalone.
45+
- **canvas**: a `<canvas>` + inlined `setTransform(dpr)` + a self-contained rAF loop calling the `draw` body. **If the effect uses `this.vhash/this.vnoise`, inline `vhash`+`vnoise` and call `draw.call({vhash,vnoise}, …)`.**
46+
- **dom**: the sentinel HTML (from `selector`) + the adapted `init` inlined.
47+
Prepend `attribution` (author/url) and `accuracyNote` as code comments so they travel with copied code.
48+
`mountSnippet`: short form for runtime users — `import { mount } from 'fx-lab'; mount(el, '<id>');`.
49+
50+
## 4. The 6 dom effects need adaptation (runtime task)
51+
52+
Each `src/effects/dom/<id>.js` has `_raw` (the original `initXxx(){...}` using `this.root`,
53+
`this.domSteps`, timers). Rewrite into `init(root) => ({ step?(t){}, stop(){} })`:
54+
`this.root` -> `root`; pushes to `this.domSteps` -> return a `step(t)`; event listeners + timers
55+
must be removable in `stop()`. Keep behavior identical. Delete `_raw` and `init:null` when done.
56+
57+
## 5. build.mjs (build/) — fan-out, with GATES
58+
59+
Import `src/registry.js`. Validate every `meta` against `schema/effect.schema.json` AND
60+
`validateTaxonomy`. **FAIL the build (non-zero exit) on:** duplicate `id`; unknown tag/vibe;
61+
`license==='custom'` with null `attribution`. Then derive `source`/`snippet`/`mountSnippet`/
62+
`runtimeNotes`/`url` and write into `site/`:
63+
- `site/fx-index.json``{ name, version, homepage, sections, effects:[meta-without-source + url + preview] }` (validate vs `schema/manifest.schema.json`)
64+
- `site/effects/<id>.json` — full meta + source + snippet + mountSnippet + runtimeNotes (self-contained)
65+
- `site/effects/<id>.md` — H1 `name (kind)` → blockquote (visual + mood) → metadata block → `## Drop-in snippet` (fenced) → `## Runtime notes``## Known limitations` (accuracyNote + non-determinism/accumulator + WebGL cap for shaders)
66+
- `site/llms.txt` — H1 `FX Lab`; blockquote (what it is + runtime contract in one breath); one body paragraph printing the `VIBES` vocabulary once; then 12 `## <section.label>` with lines `- [name](<homepage>/effects/<id>.md): <summary>`; a trailing `## Optional` for the heaviest/niche effects (perf.cpu==='high' or mobileSafe===false) so short-context agents can skip.
67+
- `site/llms-full.txt` — all `<id>.md` concatenated with `---`; inline canvas snippets; for shaders link the glsl in `<id>.json` rather than inlining.
68+
- copy `schema/effect.schema.json` into `site/schema/`.
69+
`runtimeNotes` assembled from kind+perf+state+cap (shaders: "Browsers cap ~8–16 live WebGL2 contexts; safe up to ~8 shader effects per page.").
70+
71+
## 6. gallery — site/index.html
72+
73+
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
74+
`<link rel="alternate" type="text/markdown" href="/llms.txt">` and `<meta name="llms-txt" content="/llms.txt">`. Respect `prefers-reduced-motion`.
75+
76+
## 7. tests — test/smoke.mjs
77+
78+
(1) Node: import registry, assert each `meta` validates + taxonomy passes + no duplicate id.
79+
(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`.
80+
81+
## 8. Paths & conventions
82+
83+
Repo root = the directory containing `package.json`. ESM everywhere (`"type":"module"`). No runtime deps.
84+
`site/` is generated — never hand-edit except `index.html` is a template that reads the manifest.
85+
No MCP, no CLI (non-goals). The paste IS the install.

LICENSE

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
MIT License
2+
3+
Copyright (c) 2026 fx-lab contributors
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.
22+
23+
Individual effects carry a per-effect `license` field in their metadata (MIT or CC0 for
24+
originals; third-party ports are marked `custom` with attribution). See each effect's page.

README.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# fx-lab
2+
3+
99 zero-dependency web effects — **WebGL2 shaders · Canvas-2D · DOM** — as a live gallery and an
4+
**LLM-friendly catalog**. Find an effect by vibe, copy one self-contained snippet, paste it anywhere.
5+
6+
- **Gallery:** `https://<host>/` (live tiles, 12 sections)
7+
- **Library:** `import { mount } from 'fx-lab'; const h = mount(el, 'aurora'); h.stop();`
8+
9+
## LLM Usage
10+
11+
If you are an LLM/agent looking for a visual effect, start here:
12+
13+
- **[`/llms.txt`](./site/llms.txt)** — slim index: every effect as one vibe-first line, grouped by section. Read this to pick.
14+
- **[`/llms-full.txt`](./site/llms-full.txt)** — the whole catalog in one file (metadata + paste-in snippets).
15+
- **[`/fx-index.json`](./site/fx-index.json)** — machine-readable manifest; filter by `tags` / `vibe` / `perf.mobileSafe`.
16+
- **`/effects/<id>.md`** and **`/effects/<id>.json`** — per effect: full metadata + a self-contained, zero-dependency `snippet` you can paste directly into a page.
17+
18+
Flow: fetch `/llms.txt` → map the user's intent to a `vibe` + visual tag → pick a line → fetch
19+
`/effects/<id>.json` → paste its `snippet`. One index fetch, one detail fetch, zero install.
20+
21+
## Status
22+
23+
v0.1. See [`CONTRACT.md`](./CONTRACT.md) for the build contract. Run `npm run build` to regenerate
24+
`site/`, `npm run dev` to preview the gallery, `npm test` to validate + render every effect.
25+
License: MIT (per-effect license in each effect's metadata).

0 commit comments

Comments
 (0)