点阵 Lattice
Everything is a set of points; every interaction is a map between sets. A name is lit lattice sites, a theorem is a cluster of points, a dependency is a stream falling from premise to consequence, a proof is a tree growing from conclusion to axioms. Points are released in a sweep into a divergence-free ABC flow, then springs pull them into the new set, so transitions pour instead of cut.
World: Night — warm bone points on deep ink, one warm signal (orange = needs/upstream/action), one cool signal (ice blue = enables/downstream). Version bundled here: 0.2.1, source ruiqichenbiec/design-systems, MIT. Stack: three.js 0.186.1 + WebGL2, plain ES modules, no framework.
Read order
| When | Read |
|---|---|
| Before any visual decision | references/DESIGN.md (tokens, rules, component specs) |
| Product intent and constraints | references/PRODUCT.md |
| Component-by-component behaviour | references/components/*.md |
| Wiring the API | references/lattice-readme.md |
| Known limits and verification history | references/VERIFICATION.md |
Non-negotiable rules
- Point Rule — if it is visible in the field, it is made of points. No gradients, blobs or solid strokes as decoration; a line is a sequence of dots.
- Two Directions Rule — orange for needs/upstream/premise/action, ice blue for enables/downstream/consequence. Never swap, never add a third signal hue.
- Bone-Not-White Rule — points and text are
#ece7da, never#fff; pure white only as the primary button's hover. - Light-Is-Heat Rule — glow is earned by speed, melting, focus or a highlighted path, never resting decoration.
- Matrix Size Rule — Doto only at 15px and above; below that use the body face.
- Text-in-DOM Rule — text never lives only inside WebGL. Particle lettering is
aria-hiddenand mirrored by real text; 3D labels are DOM elements positioned over their points. - No-Stray-Selection Rule — dragging the field never selects text; readable panels stay selectable.
- Real or nothing — every 3D set is a real mathematical object with its parameters stated (σ = 10, ρ = 28, β = 8/3); every graph uses real or clearly labelled data.
Tokens
Source: assets/src/tokens.css (also assets/src/components.css).
| Role | Value |
|---|---|
| Ink Night / raised ink | #07080b / #0d0f14 |
| Bone point / dim / faint | #ece7da / #9d988c (7:1) / #6c685f (3.6:1, decoration only) |
| Signal orange / hover | #ff5a1f (5.9:1) / #ff7440 (hover only) |
| Ice blue | #7fd8ff (11:1) |
| Hairline / strong hairline | rgb(236 231 218 / .14) / / .3 |
| Type | Matrix: Doto 900 15–16px 0.06em. Body: Atkinson Hyperlegible Next 17/400/1.6, max 60ch. Headline clamp(28px,3vw,40px) 600. Title 21/600. UI 15/500. Note 14. Small 13. Label 12. Chinese: Noto Sans SC 700 |
| Space | 4, 8, 12, 16, 24, 32, 48, 64, 96 px; more above a heading than below |
| Radius | 6 small targets, 12 panels, 999 pills |
| Motion | 160 fast, 320 mid, 700 slow; cubic-bezier(.16,1,.3,1); dot pitch 9px |
| Panel | rgb(7 8 11 / .78), 10px backdrop blur, 1px hairline inset, padding 26 (20 on phones) |
Core recipes
Stage + Field (the point field)
import { Stage } from "./src/gl/stage.js";
import { Field } from "./src/gl/field.js";
import * as SETS from "./src/gl/sets.js";
import { WORLDS } from "./src/core/theme.js";
const stage = new Stage(canvas, [
{ id: "hero", el: ".hero", pos: [0, 0, 10], look: [0, 0, 0] },
], WORLDS.night);
const field = new Field(stage);
field.useSets(SETS);
stage.add(field);
field.onResize(stage);
field.keep((L) => L.lit([{ map: SETS.matrixText("HELLO", 2), col: 10, row: 12 }]), { dur: 1.5 });
field.keep((L) => L.shape(SETS.lorenz(field.N, 2)), { is3d: true, alpha: 0.22 });
- Field budget: 65,536 points desktop, 25,600 phones.
keep(build, opts)takes a builder returning an N×4Float32Array(xyz + categoryw:0hidden,0.35lattice,1lit). The same builder re-runs on resize. opts:is3d,sweep,dur,flow,lit,alpha.- Built-in sets:
L.rest(),L.lit([...]),L.shape(sets.lorenz(N, r)), plustorusKnot,crystal,klein,sphere,matrixText,rasterText. - Events:
press,tap,heat(0–1),release(detail.melted). A tap is a shockwave; hold melts the local points, release after melting maps to the next set. - Any
Float32Arrayof targets is a valid set — use it for your own data.
Dependency graph + proof tree
Data contract per theorem: { id, en, zh, st, deps, kind, topic, leaf? }.
import { Cascade } from "./src/gl/cascade.js";
import { ProofTree } from "./src/gl/prooftree.js";
const cascade = new Cascade({ theorems, depth, topicOrder }); // depth: Map id→layer
const tree = new ProofTree({ byId }); // byId: Map id→theorem
tree.grow(id); // grow a conclusion to its axioms
tree.swap(id); // half-turn transition to another proof
tree.progress(); // 0–1 bloom progress for a crystallising bar
- Cascade lays out by longest-path depth; premises above consequences, topics as depth lanes. Edges are static dot strands plus falling droplets.
- Hover: premises orange, consequences ice blue, everything else dims; a card shows the statement. Click: grow that result's proof tree.
- Proof tree: conclusion at the base, axioms at the leaves, shared lemmas once with ↺ references, branch thickness ∝ √leaves, growth by depth with bright tips, ice-blue sap running back down on completion and a final chord.
DOM controls (native semantics + dots on top)
import {
mountTabs, mountSwitch, mountSlider, mountProgress,
mountDotText, mountLoader, mountCommands, toast, mountCursor,
} from "./src/index.js";
mountTabs(list); // role=tab, arrow keys, 14×2 dot swarm indicator
mountSwitch(btn); // gas → orange hexagonal crystal
mountSlider(input); // native range, 7-point orbiting thumb
mountProgress(el).set(0.4); // gas drift → crystallised dots
mountDotText(el).set("QED"); // reflow dot by dot between strings
mountLoader(el); // Lorenz trajectory trail
mountCommands([...]); // ⌘K / Ctrl+K palette
toast("Copied", { tag: "OK" }); // dot-matrix tag
mountCursor(); // cursor point set re-forms on control outlines
Keep the native element (<button role="switch" aria-checked>, <input type="range">, role="tab", role="progressbar"). The dots are a layer over real controls, not a replacement. Field sections use user-select: none.
Deterministic recording
Add ?record to the URL: time advances only through lattice.renderAt(t), so frames are reproducible for video capture. tools/capture.mjs in the source repo is the reference frame-stepping script (not bundled here).
Component behaviour to preserve
| Component | Behaviour |
|---|---|
| Field | GPU spring targets; morph = release sweep → ABC flow → springs; pointer pushes/swirls/wakes; hold melts; tap shockwave; drag rotates 3D sets |
| Cascade | dependency DAG as waterfall; hover lights upstream/downstream; click grows a proof tree; topics are depth lanes; background mist falls between streams |
| Tour | same waterfall, no side panel; scrolling walks landmark results in dependency order, one per ~200px, camera glides, card beside the node, matrix counter |
| Proof tree | grows when the camera arrives; progress bar crystallises; drag to turn; click a node to re-root; sequence section swaps trees with a half-turn |
| Cursor halo | 30 points around the cursor; re-form along the focus target's rounded outline, doubling as keyboard focus; press bursts; fine pointers only |
| Buttons | pills 44px, 0 20px; primary bone fill → white hover; accent orange, one per view; ghost hairline; disabled faint; active scale(.96) |
| Tabs | real tabs, arrow keys; the selected indicator is a 14×2 swarm that pours to the new tab with a stagger |
| Switch | off = 19 points drifting as gas on Lissajous paths; on = converge right and lock into an orange hexagonal crystal with flashes; off melts outward; mountSwitch returns {set, on} |
| Slider | invisible native range on top; filled dots orange, unfilled dim, seven-point orbiting thumb pushes neighbours |
| Progress | unfinished = gas drifting around the bar; each finished piece converges and locks with a white flash; indeterminate = band of order sweeps; completion chord |
| Matrix text / loader | dot-by-dot reflow with shared strokes sliding; loader is one fading Lorenz trail |
| Inputs | no box; dotted 1px rule every 9px; focus turns dots orange and flowing; error tightens pitch to 5px orange and states what is missing |
| Cards / palette / toast | 12px panels, ink 78% + 10px blur + hairline; palette on raised ink with a small swarm on the active row; toasts with a reflowing matrix tag |
| Navigation | fixed header, matrix monogram, one Chinese character per link (orange when current), sound and ⌘K ghost pills; ink gradient once scrolled; mobile keeps monogram + tools |
Layout, motion, performance
- Full-bleed field stations alternate with scrolling content. Hero is one viewport: particle wordmark upper third, thesis and gesture legend lower-left, set caption lower-right. While the hero scrolls away, its lattice moves 1:1 with the page so the wordmark never slides under text; only after that do points rain into the waterfall.
- Depth is spatial: perspective camera stations, pointer parallax, points lifting off the lattice, bloom on hot points (strength 0.55, threshold 0.62). The only shadows are ambient ones under overlays.
- Panels are not sticky below 760px so the field and tree get the screen; the lattice drops to 46 columns and 25,600 points.
prefers-reduced-motion: morphs snap, pointer field and auto-advance stop, and the page must remain fully usable as a static rendition.- The GPU stays idle when no field section is visible. Cap DPR (≤1.5 mobile, ≤2 desktop), keep a particle budget and degrade gracefully; ignore throttled frames >100ms in adaptive resolution.
Do / Don't
Do: derive, don't decorate; make every transition a map; label sets with real parameters; mirror particle text in the DOM; use real dependency or data graphs.
Don't: add third signal hues; use Doto below 15px or for body copy; use pure white; put reading text on top of the 3D subject (shift the camera view offset instead); reintroduce the rejected bronze formation or paper/ultramarine worlds as the default; ship decorative glow.
Verification
The bundled assets/showcase is the reference page and imports ../src with the import map expecting assets/node_modules/three:
cd assets && npm install # [email protected]
python3 -m http.server 4187 # then open http://127.0.0.1:4187/showcase/
Check: field initialises with 65,536 points, pointer push/swirl, hold-melt → release switches set, cascade hover card, click grows a proof tree, tabs/switch/slider keyboard, ⌘K palette, toast, no console errors, and the same pass under prefers-reduced-motion: reduce.
The source repo also has tools/smoke.mjs, tools/check-dist.mjs and tools/capture.mjs (single-file build, 44 interaction checks, deterministic screenshots). They are not bundled; do not claim them unless you run them from the original repo.
Not verified anywhere yet: real iPhone/Android and Safari, trustworthy real-time FPS in a throttled panel, human listening of synthesized sound. Do not claim those.
Asset map
assets/src/ tokens.css, components.css, index.js, core/ gl/ dom/ data/
assets/showcase/ reference page: index.html, main.js, page.css
references/DESIGN.md full tokens, hierarchy, layout, component specs, rules
references/components/ one spec per component (14 files)
references/lattice-readme.md, PRODUCT.md, VERIFICATION.md
assets/THREE-LICENSE.txt, LICENSE-design-systems