bg
#07080b
Ink Night · page and field background.
万物皆点集,交互即映射。给个人网站与公开发布的 WebGL 动画使用的设计系统:页面底下是一张点阵,名字、模型、定理、证明全部由点组成,所有交互都是从一个点集到另一个点集的映射。世界为 夜 Night。
| 部件 | 文件 | 说明 |
|---|---|---|
| 点场 Field(签名) | src/gl/field.js | GPU 模拟 65,536 点(手机 25,600);弹簧入集、ABC 流过渡、指针场、按住熔化、点击冲击波、拖拽旋转 3D 集 |
| 点集 Sets | src/gl/sets.js | 点阵、5×7 点阵字、洛伦兹吸引子、三叶结、面心立方晶体、克莱因瓶、斐波那契球 |
| 定理之瀑 Cascade(签名) | src/gl/cascade.js | 依赖 DAG 以瀑布倾泻;悬停显示上游(橙)与下游(冰蓝);点击生长证明树。展示页有两个版本:自由探索(小而透明的说明)与巡游(随滚动按依赖顺序逐个悬停 16 个关键结果,每两格滚轮一个,无侧栏) |
| 证明树 ProofTree(签名) | src/gl/prooftree.js | 把 DAG 展开成以结论为根、公理为叶的证明树;相机到达时开始生长,旁边进度条同步结晶;swap(id) 旋转转场,展示页用它随滚动连播 6 棵证明树 |
| 舞台 Stage | src/gl/stage.js | 单一 WebGL 上下文、滚动驱动的相机站点、辉光、自适应分辨率、不可见时停机 |
| 光标光环 | src/dom/cursor.js | 光标的点集在可操作控件轮廓上重组,兼作键盘焦点指示 |
| 控件 | src/dom/controls.js | 标签页(点群指示器)、开关(气态/晶态)、滑块(点列)、进度(结晶) |
| 点阵字与加载器 | src/dom/dottext.js | 字符串之间逐点重排;洛伦兹轨迹加载器 |
| 命令面板与消息 | src/dom/command.js | ⌘K / Ctrl+K;消息带点阵标签 |
| 声音 | src/core/sound.js | 合成音色,默认关闭 |
| 数据 | src/data/analysis.js | 43 条实分析结果的依赖图(ZFC → Picard–Lindelöf / Peano) |
| 令牌与样式 | src/tokens.css · src/components.css | 颜色、字体、间距、组件样式 |
Set-Location lattice
npm install
node serve.mjs 4187 # http://127.0.0.1:4187/showcase/
node build.mjs # 生成 dist/lattice-showcase.html,并刷新 project/ 与 dist/catalog/
依赖只在本目录:three@0.186.1(运行时,被打包进单文件)、esbuild、playwright-core(仅截图与测试,使用本机 Chrome/Edge)。
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);
// 任意点集:返回 N×4 的 Float32Array(xyz + 类别 w:0 隐藏 · 0.35 点阵 · 1 点亮)
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 });
依赖图数据只需 { id, en, zh, st, deps, kind, topic, leaf? }:new Cascade({ theorems, depth, topicOrder })、new ProofTree({ byId }).grow(id)。换成自己的主题图(课程、项目、因子)即可复用两个签名页面。
DOM 控件保留原生语义,只在上面画点:mountTabs(el)、mountSwitch(btn)、mountSlider(input)、mountProgress(el).set(0.4)、mountDotText(el).set("QED")、mountLoader(el)、mountCommands([...])、toast("…", { tag: "OK" })、mountCursor()。
| 脚本 | 作用 |
|---|---|
tools/build-project.mjs | 从源码生成 project/(令牌解析自 src/tokens.css、window.Lattice 经典脚本包、组件预览来自 catalog/),再调用 ds-viewer |
tools/author-catalog.py | 组件卡的源文件(预览与说明)写入 catalog/components/ |
tools/smoke.mjs | 交互冒烟测试(30 项,含减少动态) |
tools/check-dist.mjs · tools/check-catalog.mjs | 单文件与查看页的无头检查 |
tools/capture.mjs | 逐帧确定的截图与对比图 |
project/ 与 dist/ 都是生成物,不要手改。
在地址后加 ?record,时间只由 lattice.renderAt(t) 推进,逐帧确定。tools/capture.mjs 是可直接改写的逐帧截图示例:
node tools/capture.mjs # 夜间世界的 6 个场景 + 对比图,输出到 outputs/captures/
mailto:you@example.com(带 data-placeholder)。src/data/analysis.js。?world=paper / ?world=ultramarine 仍可用于实验,但不是本系统的承诺世界。Guideline · design.md
Creative North Star: "The Living Lattice"
Every page stands on one lattice of dots. Nothing is decoration: a name is a set of lit lattice sites, a theorem is a cluster of points, a dependency is a stream of points falling from a premise into what it proves, a proof is a tree of points growing from its conclusion up to its axioms. Interaction is always a map from one set to another. Points are released one by one into a divergence-free ABC flow and then pulled into their new set by springs, so every transition pours instead of cutting.
The system was built for a personal site and for publishable WebGL clips. It speaks to professionals (quant, AI and agent engineering, mathematics, games) who judge depth quickly, so the spectacle always carries real content: the Lorenz attractor with its real parameters, a real dependency graph of real analysis, real proof trees. Night is the committed world: points of light on deep ink, with one warm signal and one cool signal.
It rejects the dark dev-portfolio default (a floating blob, a neon accent, a grid of project cards) and the generic "particles morph into random models" demo. Two worlds were explored and set aside: the bronze luopan formation (符阵) and the paper and ultramarine renditions of this lattice.
Key Characteristics:
Near-black ink carries points of warm bone light; two signal hues mark direction and never mix roles.
The Two Directions Rule. Orange means "needs" (upstream, premise, action); ice blue means "enables" (downstream, consequence). Never swap them and never introduce a third signal hue.
The Bone-Not-White Rule. Points and text are warm bone (#ece7da), never #fff; pure white appears only as the primary button's hover.
Matrix Font: Doto 900 (5×7-style dot-matrix face), and the system's own 5×7 bitmap for text drawn as points Body Font: Atkinson Hyperlegible Next (with Noto Sans SC, system-ui) Chinese: Noto Sans SC 700
Character: The dot-matrix voice is the lattice speaking: captions, counters, gesture names, status lines. The reading voice was chosen for legibility at 12 px over moving points (the waterfall and tree labels), not for fashion.
<h1> in the DOM.The Matrix Size Rule. Doto only at 15px and above. Below that, dot-matrix lettering turns to noise; use the body face.
The Text-in-DOM Rule. Text never lives only inside WebGL. Particle lettering is aria-hidden and mirrored by real text; 3D labels are DOM elements positioned over their points.
The No-Stray-Selection Rule. Dragging on the field never selects text: field sections are user-select: none and a drag the field claims cancels selection; panels and readable copy stay selectable.
Full-bleed field stations alternate with scrolling content. The hero is one viewport: particle wordmark in the upper third, thesis and gesture legend lower-left, set caption with a matrix index and "Next set" lower-right. While the hero scrolls away, its lattice scrolls 1:1 with the page (the camera follows the section), so the wordmark never slides under the text; only after that do its points rain down into the waterfall. The page then runs: waterfall to explore (300vh, a small transparent note on the left), the tour (16 landmark results, one per two wheel notches, about 200px of scroll), the proof tree (grows when the camera arrives, with a progress bar in its panel), the proof sequence (one tree per ~110vh, a half-turn between trees), the instruments and the close. 3D subjects are shifted right with a camera view offset (0.17–0.2 of the width) so text and subject never overlap. The instruments section is an opaque two-column bench (1200px container, 18px gaps) that hides the field (the GPU pauses whenever no field section is on screen).
Spacing steps are 4, 8, 12, 16, 24, 32, 48, 64 and 96px, with more space above a heading than below. Below 760px: panels stop being sticky and scroll away so the waterfall and tree get the whole screen (the proof-sequence HUD stays sticky on a panel backing and the stand sits lower), the tour card moves below the node, the wordmark breaks onto two lines, the lattice is 46 columns and the field drops to 25,600 points.
Depth is spatial, not shadowed. It comes from the perspective camera, parallax on the pointer, points lifting off the lattice, and bloom on hot points (UnrealBloom, strength 0.55, threshold 0.62). Panels float on the field with a 1px hairline inset and a 10px backdrop blur. The only shadows are deep ambient ones under overlays (card: 0 18px 40px -20px rgb(0 0 0 / 0.7); command palette: 0 30px 80px -30px rgb(0 0 0 / 0.9)).
The Light-Is-Heat Rule. Glow is earned by energy: a point glows orange because it is fast, melting, lit or on a highlighted path, never as a resting decoration.
Actions are pills (999px). Panels and specimens have gently rounded corners (12px); small targets 6px. Every mark inside the field is a round point; lines exist only as dotted strands of points (dependencies, branches, dotted rules under inputs). Nodes carry their kind in shape: foundations are square blocks of dots, definitions are rings, theorems are filled Fibonacci discs, axiom leaves are five-petal rosettes.
The Point Rule. If it is visible, it is made of points. No gradients, blobs or solid strokes as decoration; a line is a sequence of dots.
tree.progress()).tree.swap(id)): the stand turns half a revolution, the old tree falls as dust, the new one grows.role=tab buttons with arrow-key support. The indicator is a 14×2 swarm under the selected tab; on change it pours to the new tab with a stagger and a short burst of flow.role=switch. Off: 19 points float as a gas across the whole track, each on its own slow Lissajous path. On: they converge on the right and lock into an orange hexagonal crystal (each flashes as it locks), with an orange inset on the track. Turning off melts the crystal outward. A large variant (.lx-switch--lg, 136×54) is for demonstrations; mountSwitch() returns { set(on, { quiet }), on }.role=progressbar, three rows of dots. Unfinished work is a gas floating loosely around the bar (smooth Lissajous drift). Each newly finished part is built from it: its points converge, lock into the lattice with a white flash, and stay fixed in orange. Indeterminate: a band of order sweeps through. Completion plays a chord.prefers-reduced-motion (morphs snap, the pointer field and auto-advance stop).11 tokens · Night. One committed world: points of light on deep ink. Orange means needs/action, ice blue means enables; never swap them.
--font-uiAa 永 123"Atkinson Hyperlegible Next", "Noto Sans SC", system-ui, sans-serif--font-dotAa 永 123"Doto", "Atkinson Hyperlegible Next", monospace--font-zhAa 永 123"Noto Sans SC", "Microsoft YaHei", "PingFang SC", sans-serifs-14pxs-28pxs-312pxs-416pxs-524pxs-632pxs-748pxs-864pxs-996pxr-16pxr-212pxr-pill999pxt-fast160msCSS transitions; the field and dots run on the shared clock.t-mid320msCSS transitions; the field and dots run on the shared clock.t-slow700msCSS transitions; the field and dots run on the shared clock.easecubic-bezier(0.16, 1, 0.3, 1)Default settle.ease-in-outcubic-bezier(0.65, 0, 0.35, 1)Exits.pitch9pxDot pitch for DOM dot patterns (dotted rules, underlines).定理之瀑:一张依赖图(DAG)按最长路径深度排成瀑布,前提在上、结论在下。每条依赖是一股从前提落进结论的水滴流,背景的雾点持续下落。
{ id, en, zh, st, deps, kind, topic },kind 决定节点形状(foundation 方块、definition 环、theorem 实心圆)。new Cascade({ theorems, depth: depths(), topicOrder }),放进 stage,相机站点 id 用 fallTop / fallBottom。hover(id 或 null)、pick(点击的 id)、frame(每帧节点的屏幕坐标,用于摆放 DOM 标签)。cascade.focus(id) 让键盘或列表也能点亮同样的上下游。LabelLayer),并提供一个可键盘访问的完整列表。GPU 点场:每个点都被弹簧拴在当前点集里的一个目标上。换集时,点按扫描顺序逐个松开,先进入 ABC 流(一个无散度的欧拉方程解)再被新集拉回,所以过渡是「倒」过去的,而不是跳过去。
new Field(stage) → field.useSets(Lattice.sets) → stage.add(field) → field.onResize(stage)。field.keep(build, opts):build(lattice) 返回 N×4 的 Float32Array(xyz + 类别 w:0 隐藏、0.35 点阵、1 点亮)。窗口尺寸变化时会用同一个 build 重建。L.rest()、L.lit([{map: sets.matrixText('HI', 2), col, row}])、L.shape(sets.lorenz(N, 2))(以及 torusKnot、crystal、klein、sphere)。opts:is3d(随集旋转、可拖拽)、sweep(释放扫描方向)、dur、flow、lit(点亮点的尺寸倍数)、alpha。press、tap、heat(0–1)、release(detail.melted)。prefers-reduced-motion 下过渡瞬间完成,指针场停止。证明树:把依赖图从一个结论展开成证明树。结论是树根(在下),前提逐层向上长成枝,叶子是公理;同一引理只长一次,之后出现处标 ↺。枝的粗细与其下叶子数的平方根成正比。
new ProofTree({ byId }) 放进 stage(站点 id tree),tree.grow(id) 开始生长,旧树会像落叶一样凋落。grow、hover(节点或 null)、pick(点击节点的定理 id,可用来以它为根重新生长)、frame(节点屏幕坐标与开花时刻)。leaf: true 标出证明止步的地方(例如 ℝ 的完备性)。tree.progress() 返回生长进度(0–1),适合接一个 mountProgress 进度条放在旁边。tree.swap(id) 是旋转转场:展台转半圈,旧树落叶,新树生长;用于随滚动逐个展示证明树。光标由一个点和它周围的 30 个点组成。经过任何可操作的元素(以及键盘聚焦时),这组点会离开指针,沿控件的圆角轮廓重组并缓慢绕行;按下时向外迸散;在点场上按住时,环会逐渐变成橙色。
mountCursor() 一次即可;返回 { setHeat(v) },点场的 heat 事件接到这里。data-no-halo 可以让某个区域不被吸附;加 data-halo 让非交互元素也能吸附。按钮是 44px 高的胶囊。主按钮用骨白底、墨色字;强调按钮用信号橙,每个视图最多一个;幽灵按钮是透明底加描边;禁用按钮只剩淡色字和细线。
.lx-icon),不要用字符 → 或 @ 代替图标。滑块:一排点。已选部分为橙色,其余为淡色;七个点组成的拇指绕行,并把相邻的点推开。
<input type="range"> 放在 .lx-slider 里(透明,负责交互与无障碍),mountSlider(input) 负责绘制。开关:关闭时,点作为气体在整条轨道里自由游离;打开时,点汇聚到右端并锁定成橙色的六边形晶体(每个点落位时闪一下)。同一组点,不同的相。
<button class="lx-switch" aria-checked="false"><span class="lx-switch__track"></span>…</button>,mountSwitch(btn)。switch:change(detail 为布尔值)。标签页的指示器是一个 14×2 的点群。切换时点群按顺序松开,带着一小段湍流流向新的标签。
role="tablist" / role="tab" 结构;mountTabs(listEl),方向键切换。tabs:change(detail.index、detail.value)。文本框:没有边框,只有一条每 9px 一个点的虚线。聚焦时点变成橙色并沿线流动;出错时点距收紧到 5px,下方文字说明缺了什么。
加载器:一条洛伦兹轨迹,画成渐隐的点迹,并缓慢旋转。它是系统里「正在思考」的状态。
mountLoader(el);元素自动获得 role="status"。进度条:三行点。未完成的部分是在整条进度条里自由游离的气态点;每完成一段,就从气体里抽出点,汇聚到该段的格点上,落位时闪一下并固定为橙色。不定进度是一道有序的带子扫过;全部完成时奏一个和弦。
mountProgress(el) 返回 { set(v), indeterminate(on) };元素带 role="progressbar" 与 aria-valuenow。点阵字:5×7 位图字形画成点。换字符串时,新字仍需要的点滑到新位置,多余的飞走,缺的补进来。适合计数器、编号和短标签。
mountDotText(el, { pitch }) 返回 { set(text) };原文本写入 aria-label。.、-、·。命令面板:⌘K / Ctrl+K 打开的模态对话框。当前行由一小群点标出,上下移动时点群流到新行;输入按子序列过滤。
mountCommands([{ id, key, en, zh?, hint?, run }]) 返回 { open, close }。key 用一个汉字或字母作为行首标记,不要用符号字符代替图标。消息:右下角出现,带一个会逐点重排的点阵标签,几秒后模糊淡出。容器是 role="status" 的礼貌播报区域。
toast(message, { tag = 'OK', ms = 2800 })。