关于 motion-web
motion-web 是面向创意与营销网站的设计和构建 Skill,供 Agent 从概念推进到成品页面。它同时重视动效编排、页面设计与上线前完善。
主要功能
- 01端到端构建创意网站
- 02设计动效概念与交互编排
- 03规划页面结构、文案和组件
- 04审查生产质量、无障碍与性能
适用场景
- 01根据简短需求制作落地页
- 02参考现有网站构建新页面
- 03检查动效与静态画面设计
- 04完善页面状态和元数据
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ feitangyuan/ motion-web --skill motion-web安装后在 Claude Code 中输入/motion-web确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
LICENSE.gitignoreREADME.mdcases/AUTHORING.mdindex.htmlcases/char-curtain/README.mdindex.htmlcases/ink-crowd/README.mdindex.htmlcases/lyre-crows/README.mdindex.htmlcases/press-stack/README.mdindex.htmlcases/string-clock/README.mdindex.htmlcases/toy-flipbook/README.mdindex.htmlcases/wheel-rail/README.mdindex.htmldata/visual-bank.jsonreferences/baseline-ui.mdbuild-mode.mdaffordance.mdchoreography-arc.mdcomponents.mddesign-slop.mdcomposition-guide.mdimage-asset-pipeline.mdhandfeel.mdlight-3d.md详细介绍
motion-web
以动效为先的创意网页。此技能涵盖所有阶段:参考 → 概念 → 规格 → 构建 → 审核。
此技能用于构建完整网页。 动效能让网页出彩,但它并非全部工作。即使请求完全没有提到动效——“做个落地页”、“帮我把这个官网做出来”——仍然适用此技能,交付内容是一套完整的网站:技术栈、设计令牌、版块、文案、组件、状态、元信息和部署。
优先级——第 1 项和第 2 项同等重要:
- 动效概念与交互编排
- 页面设计——结构、字体比例、配色、间距节奏,以及页面在任何元素动起来之前的样子
- 构建后的页面成品——版块及其文案、组件体系、令牌层、非理想路径下的状态(
page-blueprints.md、components.md、project-setup.md、production-polish.md) - 仅在能强化页面时才使用轻量 3D 或 canvas
- 性能、无障碍、减少动效时的回退方案
在默认风格的页面上放一个出色的交互机制,页面仍然不合格。 动效是优势,不是借口。此技能最常见的失败,是只做了一个巧妙的交互,却把它硬塞进一个无人设计过的布局——如果关闭动效后页面会让你觉得丢脸,那它就还没完成;再怎么调弹簧参数也无济于事。设计不是时间顺序上的第 2 步;它是另一项最终也必须达成的目标。→ references/page-design.md、references/design-slop.md
这是一项面向一类网站的设计与构建技能:创意与营销类网页——落地页、产品页、作品集、工作室网站、营销活动、编辑内容和轻量文档。它能端到端地构建这些网站。它不是面向仪表盘 / 管理后台 / 数据密集型应用界面的技能,也不是完整的 3D 场景生成器(此类任务会转交给 3d-scene-studio)。包含应用界面片段的创意页面——例如价格表、登录界面、控制台截图——属于适用范围;已登录的产品界面则不属于。
Agent 意图检测(重要——请先阅读)
用户往往只会说很少的话。请根据最少的输入判断其意图,并自主采取行动。
信号 → 意图映射
| 用户发送的内容 | 检测到的意图 | 操作 |
|---|---|---|
| “做个网页” / “做个落地页/官网/作品集”,附带主题,无参考 | full-build | 做完整个网站,而不是某个交互效果。 page-blueprints.md → project-setup.md → page-design.md → 构建 → components.md → production-polish.md |
| “这页要放什么” / “该有哪些 section” / “信息架构” / “文案怎么写” | blueprint | page-blueprints.md — 选定主线,写真实文案,标记每一处虚构信息 |
| “导航” / “按钮” / “表单” / “卡片” / “footer” / “FAQ” / “定价表” / “弹窗” / “组件” | component | components.md — 先看方言表,再看状态矩阵。没有涵盖状态的组件就不算完成 |
| “用什么技术栈” / “Astro 还是 Next” / “怎么起项目” / “design token” / “字体” / “部署” | setup | project-setup.md — 页面阶梯 ≠ 动效阶梯;从第 0 级开始逐步攀升 |
| “像 demo 不像成品” / “OG 预览是空的” / “没有 favicon” / “404” / “空状态” / “暗色模式” / “表单没校验” | polish | production-polish.md — 所有项目都可检查;运行 §9,而不是凭估计 |
| 单独一个 URL(没有其他文本) | url-drop | 打开并拆解它(复刻模式步骤 1–2),然后询问:是要重建这个页面,还是将它作为对方某个作品的参考? |
| URL + “做个网页”或“参考这个” | replicate → build | 先拆解 URL(复刻模式),然后为用户自己的内容提出一个动效概念 |
| 参考 + “复刻出来并收录成 case” / “存进 skill” / “做成案例” | case-authoring | cases/AUTHORING.md — 先过阶段 0 门槛:从参考中去掉图片;如果剩下的内容不足以成立,就在构建之前解决素材问题,而不是之后 |
| URL + “复刻” / “做一个类似的” / “参考这个做” | replicate | 拆解 → 映射到模式 → 立即构建。 |
| 论文/文章/博客的 URL | doc-drop | 阅读内容,指出可提取数值的技术;如果用户提出要求,就基于其中一项构建(见下文“文档 / 文章投递”) |
| 模糊需求(“做个很酷的网页” / “做个 portfolio”) | freeform-build | 通过 composition-guide.md 编排 → 独立选择风格和叙事弧线 |
| 仅有主题(“做一个关于 AI 的落地页”) | spec → build | 通过 composition-guide.md 编排 → 提出视觉方向,立即构建 |
| 附有参考资料的详细需求 | spec → build | 编写 MOTION_SPEC.md → 获取批准 → 构建 |
| “审查” / “有什么问题” / “gap” | audit | 检查现有作品的动效/性能/无障碍差距——包括静态帧,参见 design-slop.md |
| “不好看” / “太丑” / “像 AI 做的” / “没设计感” / “普通” | design-audit | 关闭动效后截图,并运行 design-slop.md 中的检查门槛。不要调节弹簧——问题在于静态帧 |
| “版式” / “排版” / “字太小” / “留白” / “结构” / “section 都长一样” | design | page-design.md — 参照当前获奖作品衡量宏观结构、字号比例和间距节奏 |
| 对现有页面说“加上……” | incremental-build | 在当前实现上扩展新的动效 |
| 视频参考(Pinterest/Dribbble/X 短片) | video-ref | 停止——编写任何代码之前,先运行下方的视频参考流程 |
| 对现有动效说“手感差” / “太生硬” / “不跟手” / “四平八稳” | handfeel-tune | 根据 handfeel.md 审查——弹簧、速度耦合、次级运动 |
| 对任何跟随对象(摄像机、光标、轨道、工具提示)说“跟手但总差一点” / “一抖一抖” / 手感随帧率变 | handfeel-tune | 不是 §1 中的弹簧——参见 handfeel.md §7:1-exp(-k·dt)、speed/k 前导项,以及滤波后绝不进行校正 |
| “能不能不用 GSAP” / 只是个入场或视差 / 想要更轻 | build | 先检查第 1.5 级——原生 animation-timeline / animation-trigger / sibling-index() → build-mode.md 阶梯 + pattern-recipes.md #16。Firefox 使用静态终态 |
| 素材是一个真实存在的地方(实拍/扫描),要走进去 | build | 第 5.5 级——先尝试 3D Gaussian splats,再考虑模型 → build-mode.md 阶梯 |
| 卡片/图片要弯折、旗帜、翻页、把截图钉进立体面、“skew 不够用” | warp | 角点映射单应变换 → pattern-recipes.md #17。为每个四边形添加保护,并将边缘倾斜角限制在 asin(h/w)——未设保护的四边形会绘制出一块占满屏幕的区域,且不会报错 |
| 对现有动效说“点不开” / “只有一个能点” / “没人会发现” / “看不到” | affordance-audit | 不要先改命中测试代码。 根据 affordance.md 审查——机制通常没问题;缺失或失效的是操作提示 |
| 一堆图片素材 + “做成一个 3D 场景” / “要有纵深” / “camera 要能走进去” | image-plane-3d | 不要急着用模型。 用平面在真实透视摄像机中构建场景 → image-plane-3d.md(阶梯第 5 级) |
| 对渲染场景说“有拼接感” / “没融进去” / “像贴上去的” | image-plane-3d | 修改美术素材前先测量:按素材批次统计平均 R−B 和亮度,并确认接触阴影存在 → image-plane-3d.md §6–7 |
| “这个该用什么画” / “DOM 还是 canvas” / 页面只会输出 div 和系统字体 / 参考明显不是 DOM 能做的 | render-layer | 先定层,再定机制 → render-layer.md §1。层是一项声明,应写入 case README,并由 verify_case.py --layer 核对 |
| 手绘 / 墨线 / 描边风的 3D,“像画出来的不像渲出来的”,boil / 抖线 / 纸纹 | ink-render | render-layer.md §3 — 深度不连续处描边(关闭法线折痕)、分离静态 warp 和动态 boil、boil 时钟为 11Hz,按 60fps 渲染 |
| 仍需抠图的原始素材 / 渲染图、“抠图”、“素材怎么进场景”、“图糊了” | asset-pipeline | 以图像自身的背景为基准抠图,将图像裁剪到 alpha 边界框,编写脚本生成元数据,并输出每个元素的分辨率预算 → image-asset-pipeline.md |
| 任何无头检查;数值与实际看到的不一致;“怎么验” | verify | 先搭建探测面——可固定时钟、__seek、__hold、状态退出——再进行测量 → verification-harness.md |
| 对滚动时间线说“这一段什么都没发生” / “节奏不好” / “一堆字一下就出来了” | timeline-audit | 将每条轨道对齐到同一滚动轴;节奏停滞是多个轨道的平稳段重合所致,而不是某个图层太慢 |
| timeline-orchestration.md | ||
| 有许多素材的长滚动场景;“卡”、“加载慢”、“内存爆了”、数百个平面 | scene-streaming | 按屏幕窗口激活,分片上传大型资源,在扫描过程中主动让出执行权,并彻底释放资源 → scene-streaming.md |
| 任何包含声音的内容——BGM、环境音、交互音效、“音效太少”、“氛围音听不清但也别吵” | scene-audio | 三条总线(bgm / hand / world)、每个段落单独设置曲线、用离散事件代替音量更大的背景音 → scene-audio.md |
| **绝不要询问用户要使用哪种模式。**自行判断并继续。只有当需求在两种截然不同的输出之间确实存在歧义时,才暂停确认(例如:规格说明还是立即开始构建)。 |
视频参考协议(硬规则 — 不许猜)
触发条件:参考内容是你无法实际观看的视频——Pinterest pin、Dribbble 作品、X 视频、站外短链。标题或文字描述并不包含运动信息。
- **在断定没有线上页面之前,先阅读整个讨论串。**作者几乎总会在回复中贴出 URL,回复内容也能告诉你该作品究竟是不是网页。2026-09-02 的统计:上一个会话将七个视频参考判定为没有 URL,并把第一阶段花在测量画面上;结果发现这七个参考都有线上网站,且都是作者本人发布的;一篇帖子中的四个片段里,有三个最终证实是 Jitter / Figma Motion / Rive 渲染图,完全没有网页实现(
cases/AUTHORING.md第一阶段)。运行safe-x tweet <url> --json,然后解析回复中的所有t.co链接。 - **绝不要凭猜测制作动效。**如果你提取到的只有标题/描述,那你知道的只是主题,而不是动效。仍然动手制作会浪费一整个迭代周期——这是有记录的失败案例(2026-07-06,海鲜主视觉:猜测是“绕圈”,实际是向上斜移;重做了两次)。
- **立即索要 2–3 张关键帧。**用一句话说:“这个视频我打不开,截 2-3 帧关键画面(入场/中间/离场)发我”。
- 编码前,明确说明运动轨迹的几何路径,并取得确认:
- 锚点:元素从哪里进入/到达顶点/离开(角落、三分位)
- 姿态:旋转是沿路径切线变化/保持不变/有所滞后
- 次级反应:还有哪些元素会移动(背景图形、线条、文字),以及如何移动
- 示例:“右下入场 → 中心 → 左上离场的上升对角线,鱼头恒定朝左上”——错误猜测应该在这里被排除,而不是等到写完代码才发现。
- 只有完成上述步骤后,才能对照 pattern-recipes / handfeel.md 并开始构建。
单张截图也遵循同一规则:一帧只能呈现姿态,不能呈现轨迹。如果路径很重要,就索要更多帧。
复刻模式(复刻模式)
用户发送 URL 并说“复刻”/“做一个类似的”/“能不能做这种效果”时——先拆解参考内容,然后开始构建。无需存储,交付物就是页面。
复刻执行路径
-
打开页面,并用三层视角阅读,使用可用的浏览器工具(Claude Preview、
browser-use、Playwright):技术栈(库、缓动、关键帧、CSS 变量——资源包会列出这些信息)、滚动编排(哪个元素固定在哪段滚动区间、轨道及其 px 距离、视差速率、scrub 属性、入场时机——采样约 9 个滚动位置,并比较它们之间的getBoundingClientRect和计算样式),以及 WebGL 层(程序数量、绘制调用、自定义 uniform 名称;如果需要着色器源码,可在页面脚本运行前包装getContext)。两个脚本分别覆盖可测量的部分:scripts/measure_structure.py <url>用于静态页面(区块、导航、按钮、令牌比例、媒体、元数据、移动端溢出),scripts/measure_churn.py <url>用于测量页面在真实滚轮操作下的实际动态程度。URL 必须指向作品本身,不能是作品集列表(awwwards.com/sites/<slug>):列表页会显示作品集自身的界面元素,而且六个不同的获奖作品最终都会得到相同的库、相同的关键帧和相同的字体。 -
拆解——陈述你的发现,并引用具体数字,而非主观印象:
- “GSAP 3.12 + ScrollTrigger + Lenis;
.home-animate__wrapper在 0.25–0.50 区间被固定, 期间.icon横移 +1545px,出场分为 0.25/0.38/0.62 三段 stagger” - “背景是原生 WebGL2,14 个程序、944 次绘制调用,uniform 中有 bloomStrength/blurTexture1-5 和 cloudCoverage/cloudDensity —— 这是 UnrealBloom + 体积云”
绝不能仅凭上下文报告“用了 WebGL”——一个尺寸为 300×150、没有链接程序的上下文,只是 Modernizr 式的能力检测,并非场景。请统计已链接程序和绘制调用。
- “GSAP 3.12 + ScrollTrigger + Lenis;
-
映射到 pattern-recipes——在
references/pattern-recipes.md中找到最接近的配方 -
填补差距——配方中没有而参考作品使用的内容,根据提取的数值从头编写
-
立即构建——搭建页面,运行并进行视觉验证
-
报告——“复刻了 [site] 的核心动效,用了 [X] 实现 [Y],和原版差异是 [Z]”
复刻 ≠ 照搬
- 应该:重现交互模式、动态特征和节奏感
- 不应该:照搬专有品牌颜色、完整文案或可识别的产品界面
- 应该:根据用户自己的内容和配色调整技术方案
- 在报告中说明哪些部分经过调整,哪些部分忠实还原了原作
自由构建模式(用户没有给出任何方向时)
当用户说“做个网页”/“随便做”/“自己想一个主题”,或只给出简单、模糊的主题(例如“做一个 AI agent 平台落地页”)时,这就是发挥创意的邀请。 不要默认使用通用模板,不要采用僵化的一对一映射,也不要提出澄清问题。
自由形式行动路径(运行时创意循环——创造发生在执行中,不在文档里)
用户永远不应需要提供参考。但新颖性也不会来自阅读这些文档——文档是约束和规则,不是答案。 新颖性来自运行时采样的新鲜输入 + 构建、测试、淘汰的循环,其中你自己就是评判者。执行:
- 采样当下(运行时输入,而非记忆):打开本月的 Awwwards SOTD / Godly / SiteInspire 榜单,查看 3–5 个获奖页面本身——看作品,绝不看图库页面。你不是在抄袭,而是在寻找当前的前沿,以便超越它。花 2 分钟。
关注结构和字号比例,不只是效果——这才是决定页面是否显得当下的另一半(
page-design.md§1)。 - 根据主题自身的物理特性,通过生成算子(composition-guide §5)构想 3 种候选机制。淘汰任何与废弃清单相似,或与第 1 步刚看到的内容相似的候选方案。
- 先绘制原型,再做决定:将标志性交互构建成最简的单文件原型(一个 div、一个 rAF 循环、虚构内容,<15 分钟)。不是整个页面——只做机制。
- 亲自测试:运行它,并使用当前可用的浏览器工具进行操作——优先使用 Claude Preview(
preview_start→preview_eval/preview_click→preview_screenshot),browser-use作为备选——来回移动鼠标、快速滚动、截取极端状态的屏幕截图,然后查看证据。问问自己: 会不会玩 10 秒就腻? 截两帧给人看,能不能猜出交互规则? 如果失败——调整算子或选择下一个候选方案。不要让用户替你观察。 - 设计页面——作为单独步骤,在把机制放进去之前完成。
不是“挑个配色就开始写代码”。按顺序产出三项内容:
(a) 蓝图和真实文案 →
references/page-blueprints.md——有哪些版块,以及它们实际写了什么;(b) 宏观结构、字号比例和间距节奏 →references/page-design.md,并在data/visual-bank.json中填写具体数值;(c) 设计令牌层和比例 →references/project-setup.md§2–§5。随后再把机制放入一个已经成立的页面中,这与「一个破动效贴在没设计的页面上」产生的流程正好相反。 - 构建页面,组件及其状态矩阵参照
references/components.md,弹簧参数参照references/motion-tokens.md,整页联动参照 handfeel.md。 - 设计关卡——在展示任何内容之前先执行。 将页面在关闭所有动效的情况下截图(
prefers-reduced-motion,或注释掉该机制),并单独评判这个静态画面:- 如果完全没有动画,这个静态画面能入选 Awwwards 吗?如果诚实的答案是“不行,它只是居中的标题和三张卡片”,说明设计还没完成。
- 运行
references/design-slop.md中的劣质设计检查。只要命中任何一项,就修改,不要交付。 - 只有到这一步之后,才提出关于动效的问题:会不会玩 10 秒就腻? 截两帧能不能猜出交互规则? 第 4 步通过但这一步失败的页面,正是本循环旨在避免的已知失败模式——第 4 步测试的始终只是机制。
- 完成收尾:参照
references/production-polish.md——head、OG、favicon、404、非理想路径下的各种状态,以及 §9 中的十项手动检查。未完成这些就停下的页面只是演示,不是交付成果。 - 封存机制:在废弃清单(composition-guide)中追加一行,让它永远无法再次使用——确保循环始终被迫保持新鲜。
只有当两个保留下来的候选方案确实是不同方向时,才向用户说明;否则就直接构建最好的方案,并在之后解释其隐喻。
文档/文章投递
如果用户投递论文、文章或设计博客:阅读内容,指出其中 2–3 个可提取价值的技巧(库、缓动、时序、着色器思路、布局规则),并提出一个问题——现在就基于其中一个开始构建,还是继续按照需求简报推进?如果用户选择构建,最具特色的模式就成为页面的主要动态概念,并与其他内容一样遵循相同的构建顺序。不存储任何内容;有价值的技巧在交付后会记录到 pattern-recipes.md 中。
模式(供用户明确指定)
spec:将需求简报与参考资料转化为可构建的MOTION_SPEC.mdbuild:实现获批的、以动态效果为先的 Web 体验audit:检查现有的动态效果丰富的页面或实现,找出差距design-audit:问题在于视觉效果,而非动态效果——先评判静态画面replicate:用数字拆解参考页面,再将其模式应用到用户的内容上进行重建freeform-build:用户没有指定方向 → 自主做出创意选择full-build:从 0→1 构建完整网站——蓝图 + 文案、技术栈 + 设计令牌、设计、组件、动态效果、润色blueprint:确定页面需要哪些版块并撰写文案component:设计/构建特定组件及其完整状态矩阵setup:选择页面技术栈、搭建项目框架、设置设计令牌/字体层polish:收尾阶段——元数据、OG、404、非理想路径状态、深色模式、发布前检查
构建顺序(始终遵循)
- 选择页面技术栈 →
references/project-setup.md§1 — 一个 HTML 文件 / Vite / Astro / Next。这是与动画技术栈不同的选择路径;不要让其中一个决定另一个。从第 0 级开始。 0.5 选择动画技术栈级别 →references/build-mode.mdTech Stack Decision Ladder — 选择能实现目标的最低级别;不要因为参考效果“看起来是 3D”就默认使用 R3F,也不要因为场景需要深度就默认使用模型(第 5 级会用图像构建整个场景) - 选择蓝图并撰写文案 →
references/page-blueprints.md— 确定有哪些版块、排列顺序,以及其中实际使用的文字。布局应根据真实文案来设计;使用占位文案意味着页面尚未设计完成。然后选择弧线类型 →references/choreography-arc.md - 设计页面 →
references/page-design.md— 宏观结构、字号比例、配色、间距节奏。具体数值见data/visual-bank.json。这是一个有独立产出的正式步骤,不是清单上的一项:确定关闭动画时页面的样子,并且能够单独为这张静态画面辩护。 2.5 搭建令牌层与比例系统 →references/project-setup.md§2–§5 — 三级令牌、具有明显跃迁的流式字号比例、间距比例、带度量覆盖值的字体,以及@layer顺序。等组件都建好后再做这些,就意味着要逐个修改组件。 - 选择动画令牌 →
references/motion-tokens.md(时长、缓动、弹簧) 3.5 构建组件词汇体系 →references/components.md— 选择设计语汇(创意型或产品型,两者是可量化区分的不同系统),然后为每个交互组件补全状态矩阵。缺少:focus-visible、错误状态或空状态,是最常见的“像 demo”原因。 - 查看模式库 →
references/pattern-recipes.md,了解可复用实现 - 如果场景由图像素材构建,先运行素材流水线,再运行运行时 →
references/image-asset-pipeline.md。抠图、脚本化元数据、每个元素的分辨率预算。针对错误背景抠出的素材,或被放大到 0.83× 的素材,无论着色器怎么处理都无法补救。同时确定拓扑结构(移动摄像机还是移动世界 →image-plane-3d.md§12);如果场景较长,还要确定流式加载策略 →references/scene-streaming.md - 按照
references/build-mode.md实现,同时构建探测界面 →references/verification-harness.md§1(事后补上__seek/__hold/ 可固定时钟的成本,高于一开始就构建它们) - 对于多轨道滚动时间线,将各轨道水平排布 →
references/timeline-orchestration.md;如果页面包含声音 →references/scene-audio.md - 对于任何不明显是按钮的交互元素:按照
references/affordance.md标记交互提示 - 先通过设计关卡,再通过动画关卡 →
references/design-slop.md。关闭所有动画后截屏,先评判这张画面;只有动起来才成立的页面,还没有设计完成。 - 收尾 →
references/production-polish.md— head/OG/favicon、404、非理想路径下的各种状态、深色模式决策,以及 §9 中的十项手动检查。跳过这些,就会让本来不错的页面看起来像 demo。 - 验证桌面端 + 移动端 + 减弱动态效果 + 交互提示检查清单及其数值判定标准
**强制预检——不是可选项,也不是建议:**在编写任何 GSAP、Framer Motion 或 Three.js/R3F 代码之前,先打开 references/build-mode.md 中对应框架的部分(Framework-Specific Rules)和 Failure Patterns to Avoid 表格。即使你确信自己记得这些模式,也必须这样做——这个表格正是为了防止记忆中的模式悄然退化而设的(例如,在 React StrictMode 下使用 GSAP .from(),却没有 gsap.context()/.revert())。跳过这一步,重新发明表中已有的解决方案,这是流程失误,不是风格选择。
只加载所需内容
| 模式 | 阅读这些参考资料 |
|---|---|
spec | motion-spec.md + baseline-ui.md + choreography-arc.md |
| 每次构建,无一例外 | page-design.md + design-slop.md — 静态帧是交付成果的一半 |
| 某个案例类别中的简报,或需要了解其机制 | 先看该案例的 README.md(其中包含可照搬/不许照搬的划分),然后看它的 index.html |
| 每次从 0 到 1 构建网站 | page-blueprints.md + project-setup.md + components.md + production-polish.md — 页面区块与文案、技术栈与令牌、组件状态、收尾润色 |
full-build / “做个网页”,且没有参考资料 | page-blueprints.md + project-setup.md + page-design.md + design-slop.md + composition-guide.md + components.md + production-polish.md |
blueprint / “该有哪些 section” / 信息架构 / 文案 | page-blueprints.md + page-design.md §1 |
component / 导航 / 表单 / 卡片 / footer / 弹窗 | components.md + baseline-ui.md +(如果不是显而易见的按钮,则加上 affordance.md) |
setup / 技术栈 / token / 字体 / 部署 | project-setup.md(motion 技术栈可参考 build-mode.md 中的层级) |
polish / 像 demo / OG / favicon / 404 / 空状态 / 暗色模式 | production-polish.md + baseline-ui.md |
build | build-mode.md + motion-tokens.md + pattern-recipes.md +(如果使用 canvas/WebGL,则加上 light-3d.md) |
image-plane-3d / 由图像素材构建的场景 / “有拼接感” | image-asset-pipeline.md + image-plane-3d.md + light-3d.md + verification-harness.md +(如果场景很长,则加上 scene-streaming.md;如果场景中有可点击内容,则加上 affordance.md) |
scene-streaming / 长滚动大场景 / 卡顿 / 加载慢 | scene-streaming.md + image-asset-pipeline.md §6, §9 |
asset-pipeline / 抠图 / 素材元数据 / 图糊了 | image-asset-pipeline.md(有关运行时如何使用素材,请参阅 image-plane-3d.md §2) |
verify / 构建任何 oracle / 指标与肉眼观察不一致 | verification-harness.md + affordance.md §9 |
timeline-audit / 滚动驱动的多拍编排 / “这一段什么都没发生” | timeline-orchestration.md + choreography-arc.md |
scene-audio / BGM / 环境音 / 交互音效 | scene-audio.md + verification-harness.md §3 |
freeform-build | page-design.md + design-slop.md + composition-guide.md + choreography-arc.md + visual-bank.json + motion-tokens.md + pattern-recipes.md |
audit | build-mode.md + motion-tokens.md |
handfeel-tune / “不跟手”反馈 | handfeel.md + motion-tokens.md |
affordance-audit / “点不开” / “发现不了”反馈 | affordance.md + handfeel.md |
video-ref | (先遵循上述协议),然后看 handfeel.md + pattern-recipes.md |
replicate / url-drop | pattern-recipes.md + handfeel.md + page-design.md + design-slop.md |
| 作品集/个人网站 | page-blueprints.md(作品集蓝图)+ components.md |
核心规则
- 动效是主要的设计材料,而不是最后的润色层。
- 凡是跟随其他对象移动的东西,都不该用弹簧来处理。 摄像机、光标、轨道和工具提示都必须抵达目标并停下。跟随移动目标时的稳态滞后为
speed / k——这是算术问题,不是调参失败;而在滤波器之后写入的修正会在下一帧被抵消(handfeel.md§7)。 - 「飘」是静止比例问题,不是缓动曲线问题。 缓动只会改变读取源的速度,永远不会改变新状态出现的频率。调整曲线前,先测量相邻帧中近乎相同的帧所占比例(
timeline-orchestration.md§11)。 - 从第 1.5 级开始逐级往上走,不要直接跳到第 4 级。 显示效果、视差带、交错动画和滚动侦测导航现在都可用原生 CSS 实现;先编写最终状态,再把时间轴放在
@supports后面,因为 Firefox 目前一个也不支持(build-mode.mdladder、pattern-recipes.md#16)。 - 能构建 ≠ 能发现。 交互不能只在能用时就算完成;只有首次访问者无需别人告知便能发现它,才算完成。每个不显然是按钮的交互元素,都必须能回答
references/affordance.md§1 中的三个问题。 - 测试必须编码用户的抱怨,而不是你对问题的理解。 如果用户说“我打不开”,而你的检查说“它能打开”,那你测的就是另一回事——先弄清用户的问题。
- 先决定什么数字能证明它,再输出那个数字。 截图代价高,也无法解决问题。覆盖率扫描、交接尺寸误差、安全区域包含情况、窗口与停留时间——都可以在一次无头运行中完成(
references/affordance.md§9)。 - 笼统的质量评分比不测试更糟。 每条判定规则都必须对应一条明确陈述的抱怨;审美判断应由人亲自观察成品后作出(
verification-harness.md§0)。 - 先处理资源,再处理运行时。 在由图像构建的场景中,元数据是构建产物,绝不能手动编辑——如果你正在调整
baseline让某个元素的位置看起来合适,问题就在上游(image-asset-pipeline.md)。 - 死拍是多个图层的平坦区段同时出现,不是某个图层太慢。横向阅读滚动时间表(
timeline-orchestration.md§1)。 - 交付物是一整个页面,而不是一个机制。 即使需求简述里没有动效,这项技能仍然适用;目标是完成一个网站——包含有真实文案的各个区段、带完整状态的组件词汇表、令牌层、元信息和 404 页面。只交付一个精美的交互和围绕它搭建的模板,正是这项技能的设计部分要防止的失败。
- 两种组件风格确实是两套可量化的系统。 创意类页面:圆角半径中位数 1 种,带阴影的元素 0 个,37 % 的按钮使用大写,无导航 CTA,页脚为 2 列。产品类页面:圆角半径中位数 8 种,约有 18 个带阴影元素,1 % 使用大写,2 个导航 CTA,页脚为 5 列、含 35 个链接(n=32,
components.md§1)。选定一种并保持一致——用产品类组件搭建创意类页面,本身就是“像 AI 做的”这种抱怨的由来。 - 没有完整状态的组件就不算构建完成。 hover /
:focus-visible/ active / disabled / loading / error / empty / success。缺失的状态几乎总是用户会注意到的那个(components.md§2)。 - 先写文案,再做布局。 布局应根据真实文本长度而定;围绕占位文案排好的页面必须返工。如果用户没有提供事实,就先写草稿,并用
[…]标记所有虚构部分——绝不能编造证明材料(page-blueprints.md§4,design-slop.mdA8)。 - 获奖不等于做得好。 在同一批 32 个网站中,18 个获奖网站里有 2 个在 390px 宽度下出现水平溢出,5 个没有
<h1>,只有 37 % 的图片预留了容器空间。可以借鉴它们的设计语言,但不要照搬它们在基础规范上的缺失(production-polish.md)。 - 投影变换有合法条件,违反条件时会静默失败。 角点固定(
matrix3d)只有在目标四边形保持凸形时才正常;一旦越界,单应变换就会翻转并穿过无穷远,绘制出覆盖整个屏幕的色块,却不会产生控制台错误。既要检查四边形,也要将边缘倾斜角限制在asin(h/w);只做检查会导致明显跳变(pattern-recipes.md#17)。 - 从
cases/中借鉴机制,不要照搬外观。 源码完整提供,是为了避免重复推导求解器;但配色、字体取值、网格、区段顺序和文案都属于对应案例。只换掉标志的案例复刻页是失败做法,不是捷径。 - 绕过事件处理器的探针,测试的是没人会走的路径。 有个案例通过了所有由探针驱动的检查——落点误差为 0.0000°、所有插槽均可到达、两端都已钳制——但面对真实滚轮时完全没有响应。每条判定规则都必须驱动至少一个真实手势;现在
verify_case.py会在每个页面上这样做。 - 占位几何可以通过所有判定规则,呈现出来却毫无内容。 用灰色渐变矩形代替照片,能通过类型比例、死拍和状态矩阵检查,看起来却仍像一张技法示意图。没有任何指标能检测这一点,也不该发明指标——应在开始构建之前决定是否有美术素材,而不是事后再决定(
cases/AUTHORING.mdPhase 0)。 - 如果判定规则固定输入后就开始评估,它测到的就不是人们实际使用的页面。
--strings中的每条断言最初都针对冻结的时钟运行;取消固定后,漂移的终点或始终晃动不停的物体对它们来说都不可见。无论你的机制由什么驱动,都要增加一次让它自由运行的检查。(另外,如果实时读数看起来不对劲,先检查循环是否在运行——隐藏的浏览器标签页会暂停requestAnimationFrame,冻结的模拟会给出看似合理、实际上完全错误的数字:cases/string-clock踩过的坑 #7。) - 内联样式声明的优先级高于样式表中的所有规则,包括
:hover。 用el.style.zIndex设置层叠顺序,会悄然让.thing:hover{z-index:…}失效——hover 看起来仍有反应,因为颜色变化来自同一条生效的规则,但层级提升已经失效,而且没有任何报告。应将每个元素的状态作为自定义属性发送(el.style.setProperty('--z', n)),并让规则读取该属性。 - 如果可以亲自打开参考,就绝不要依据参考的摘要来构建。 上次有人在交接时把视频压缩成了一句话;照着那句话构建后,做出了参考中没有的可调整大小窗口、参考中没有的缓动入场动画,以及每帧移动距离为 0px 的滚动跑马灯。视频一直都在磁盘上。联系表能呈现姿态;只有逐帧观察才能还原轨迹、节奏和静止状态。
- 判定阈值必须来自测量结果或可明确陈述的设计要求,绝不能凭审美决定。 最近邻 CV [0.08, 0.40] 的“避免聚堆”范围看起来很有依据,但参考本身的得分是 0.507:它随机放置卡片,而且让卡片大幅重叠。真正的契约在别处——z 轴顺序就是到达顺序,因此最新的卡片永远不会被埋住——这才值得断言。
- 先决定页面是用什么绘制的,再决定它做什么。 七个案例都用 DOM + CSS 和系统字体栈实现,而所有参考页面都使用了其他方式;这项技能从未问过这个问题,所以所有需求都默认落到了 div 上。渲染层应在案例 README 中声明,
verify_case.py --layer会检查它(references/render-layer.md§1,cases/ink-crowd)。 - 有些数值只存在于源代码中。 决定画面是否呈现手绘感的 11 Hz 抖动时钟,在帧差分中不可见:摄像机移动时位移不足一个像素,95 % 的帧都不相同,自相关曲线平坦。测量像素只会得出一个自信却错误的答案。应获取 bundle 并阅读代码(
cases/ink-crowd§1)。 - “取其感觉,不取其布局”意味着首先要取其动效。 某个实现借用了参考的配色和颗粒效果,却交付了静态页面;它通过了所有底线检查,页面看起来没问题,却毫无动静。动手设计之前,先用数字刻画参考的动效——每个图层的速度,以及任何元素处于运动中的时间比例——并在无输入时检查页面是否仍在运行。
- 全画面相关性无法衡量多图层场景。 以不同速度移动的图层,有些方向相反,最终会抵消成中位偏移 0。应为每个图层分别跟踪一种颜色或特征掩码。
- 检查平铺纹理的周期是否接近周围所有重复尺寸。 1440px 画布上的 11 条带宽度为 130.9px,叠加 128px 的噪声平铺,会产生垂直条纹——这是胶片颗粒绝不会出现的伪影。直接将噪声写入像素可以避免整类问题。
- 指向 3D 场景的指针,必须与内容所在平面相交,而不是与地面相交。 人物高 1.75 个单位;指向人体的射线若与
y = 0相交,落点会远在人物后方,而靠近地平线时则会完全偏离。问题不只是“稍微有点偏”——而是这个机制只在屏幕的一个带状区域内有效,其他地方完全没有响应,但它自己的计数器仍在报告活动(cases/ink-crowd§4,verify_case.py --follow)。 - “固定屏幕”有两种方案,彼此互斥。 劫持滚轮并平移轨道:你可以计算任意进度,但由于祖先元素经过变换,
position:sticky永远不会生效,所以每个固定子元素都必须手动停放。保留正常滚动并叠放sticky; top:0; height:100vh屏幕:固定效果是免费的,但进度必须从文档中反算为(scrollY - offsetTop) / innerHeight。排版前就要选定方案;之后再把其中一种改造成另一种,就等于重写(cases/wheel-rail§2,cases/press-stack§2)。 - 每屏恰好一视口高的粘性堆叠,没有任何停留时间。 每个屏幕只有在一个精确的滚动位置上才会完全显示,因此其中的所有动画都会在屏幕已经开始被覆盖时播放,滚动起来像是死掉了一样。给每个屏幕加上
margin-bottom: 100vh,就能在不改变其盒子尺寸的情况下,增加一个视口的停留时间。随后进度分为两部分:屏幕完整显示时,内容使用(scrollY - top) / vh;只有退出效果才使用(scrollY - top - vh) / vh(cases/press-stack§2)。 - 在判定规则中读取实际布局,不要想当然。
--stack将“每个视口一个屏幕”硬编码;布局一增加停留时间,它就报告 8 个屏幕中有 1 个被固定了——红绿状态的变化完全是检查造成的,不是页面问题。区段位置应来自探针(cases/press-stack§3)。 - 粘性屏幕堆叠会先遮住屏幕底部。 下一个屏幕会从下方升起,因此用
margin-top:auto锚定的内容最先被遮挡。首个版本中的标题、巨大数字和区段分隔线都因为这一个原因消失了。内容应放在上方三分之二的区域(cases/press-stack§2)。 - 专辑封面可以绘制,人物不行。 唱片封套只需一块颜色、两个形状和一行字体;唱片则是同心沟槽加两条细窄的镜面反光弧——横跨唱片的一大片光晕看起来像一块金盘。两者都适合生成,胜过用灰色矩形糊弄。肖像或手则不一样:不要保留对应位置,应围绕它重新设计,而不是叠加基本图形拼出一个人形(
cases/press-stack§4)。 - 密度是一种设计手段,底层设计是密度的来源。 页面即使具备参考中的每一种动效机制,仍然可能显得空:只有 184 个元素,而参考有 584 个。差距不在于文案数量,而在于参考铺在所有内容下方的技术制图底层:细线网格、章节线、范围标记。加入 20 个元素,每个元素都能自行绘制并跟踪指针,同样的屏幕就不再像参考页面的线框图(
cases/wheel-rail§3)。 - 磁盘上的参考视频优先于在线页面。 根据
blink.trade的无头截图构建时,这个案例复现了配色、字体和滚动方式,却还是收到“那个模糊的圆形东西一直没变化”的反馈。视频显示,标志性元素其实是一道雷达扫描线——指针在点状环上转动,拖着一片柔和的楔形光束,并在 0.92 s 内收束消失。静帧无法呈现页面的这一核心特征。只要有视频,就应在构建前观看,即使网站在线可访问也一样(cases/wheel-rail§2b——这是第二次因为没看视频而重做)。 - 输入驱动的动效和环境动效并非二选一。 “交互动效就是鼠标产生的动效”这句修正指的是缺失的部分,绝不意味着可以删掉环境动效层。参考同时具备两者:滚轮和指针驱动布局和每次显示效果,扫描线则始终转动。两者都要断言——真实输入下的变化量,以及完全无输入时仍保持活动(
verify_case.py --rail)。 - 平移的轨道不是编排。 劫持滚轮移动轨道,但所有子元素都静止不动,这只是穿了戏服的滚动条——在有人真正查看页面之前,它还通过了四项
--rail断言。应测量真实滚轮输入下实际发生变化的元素比例:参考为 64 %,被拒绝的实现只有 13 %。既要断言变化量,也要断言部分元素正处于转换中;否则页面即使只会在状态间跳变,也能满足数量要求,看起来仍像幻灯片(cases/wheel-rail§3)。 - 将文本拆分,否则页面就没有可动画的内容。 参考把每个单词都包在一个行内块 span 中,共 296 个,状态为
translateY(8px)/opacity 0,且transition-duration: 0s;也就是说,这些值是逐帧写入的,而不是交给 CSS。这个决定正是 64 % 变化量的大部分来源。自己写入这些值,也正是让滚轮反转时显示效果能反向扫回去的关键(cases/wheel-rail§2)。 - 滚动映射的显示效果若作用于已在屏幕上的内容,第 1 帧就会直接变成“完成”状态。 Hero 示意图的绘制效果一次都没播放,因为其区段静止时
top为 0,进度立即被钳制为 1。静止时已可见的内容需要自己的加载时钟;只有从首屏下方开始的内容才能映射到滚动(cases/wheel-rail§2)。 - 交互动效就是鼠标产生的动效。 只有滚轮和指针,别的都不算。鼠标静止时,页面却按自己的时钟漂移、呼吸和消散,那是屏幕保护程序;它也许满足“添加动效”的简述,却违背了真正的要求。构建前先问清楚由哪种输入驱动页面(
cases/wheel-rail§0)。 - 只读回代码写入的值的探针,无法告诉你屏幕上实际显示了什么。 某条断言要检查硬切换,却读取了
el.style.opacity——即循环设置的内联值;对于新增了transition: opacity 250ms的版本,它仍报告帧翻页效果完美,因为过渡会在内联值之间插值,而读取内联值时会直接跳到目标值。getComputedStyle能捕获过渡中的情况:60 个采样点里有 19 个处于过渡中。这是更底层的探针教训:第一版测试的是意图,而不是渲染(cases/toy-flipbook§5 #2)。 - 低概率效果需要能越过阈值的驱动方式,否则断言根本没有真正执行。 桌面玩具只有在
|v| > 2.2时才会触发弹跳,且触发后还要通过 6 % 的随机判定;指针扫动的峰值恰好为 2.2,所以该测试项报告 0/5,无法区分机制失效和测试从未触发。用能超过阈值的输入驱动它,并且输出峰值;如果从未越过阈值就让测试失败——未运行的测试项必须明确报告,而不是悄然通过或失败(cases/toy-flipbook§5 #3)。 - 注释可以承诺代码从未实现过的行为。 已交付的代码中,
s.v = s.v < -30 ? -s.v * 0.35 : 0下方写着“反弹后静止”;但下落时s.v为正数,因此分支从未执行过,所有玩具都会在接触时直接停住。它看起来没问题——只有知道本该有反弹的人才会发现问题。应断言结果(挤压变形),而不是运动过程,这样失效的分支就会立刻暴露(cases/toy-flipbook§5 #1)。 file://不支持 CSS 遮罩,却允许加载<img>,所以页面可能只有部分效果,整体看起来却几乎正常。mask-image: url(img/x.webp)会从源null发起 CORS 请求;照片成功加载,光照效果却悄然失效,只有控制台中的三行信息能证明这一点。将遮罩内嵌为data:URI,显示图像仍从外部加载——遮罩只包含 alpha 通道,所以 320 px、平坦 RGB 的文件约 4 KB,而被裁切的照片有 75 KB。- 判定规则的基线必须在静止时测量,绝不能等它“稳定下来”——固定等待时间会让一个将指针作用力设为 ZERO 的版本通过所有断言。
verify_case.py会在自己的冒烟测试中触发一次真实滚轮输入;对于将滚轮输入转换为衰减脉冲的页面,硬编码的 2.6 s 等待时间落在 2.7 s 的衰减期间,所以页面仍在自行漂移,之后的所有位移都会被错误地归因于指针。应轮询探针,直到运动低于阈值;如果一直没有稳定下来,就让测试失败。今天足够长的等待时间,只要有人添加了入场动画,明天就可能不再够用(cases/char-curtain§5 #1)。 - 被结构主导的比例,无法测试产生它的作用力。 非各向同性的指针推动(x 方向全强度,y 方向 0.35)看起来可以用“水平位移是否远大于垂直位移”来测试;真实案例的结果是 7.9 倍。但把偏差设为 1.0 的版本测得8.8 倍,反而更高,因为纵向连线会让节点沿着线滑动,从而吸收 y 方向的运动。应断言代码实际实现的结构性要求:推一个列,远处的列必须保持不动(
cases/char-curtain§5 #2)。 - 每帧只计算一次的速率限制,不是真正的速率限制。 程序在循环遍历九个发射器之前读取预算,并将其作为标志传入,因此九个发射器可以在同一帧全部触发:声明的上限为 4,实测为 6,实际却允许 9 个。应在循环内部重新读取预算,并在触发的那一刻再次设防。肉眼看不出超限,这正是只有判定规则才能找到它的原因(
cases/lyre-crows§5 #1)。 - 断言可能会匹配页面在结构上根本无法生成的字符串。 检查完整替换句子的断言,在一个完全正常的页面上失败了,因为尾部节点数量固定,较长的内容都会被截断。应基于交互前拍摄的快照,断言状态发生了变化,而不是断言它变成了某个精确值(
cases/lyre-crows§5 #2)。 - 纯 Canvas 页面不会被底层环境的输入检查发现;这是页面的问题,不是检查的问题。 变化特征由 scrollY、计算样式和
innerText构成,其中不包括 Canvas 像素。一个只有<canvas>的演示,即使动效满满,也可能仍然无法通过“真实滚轮事件没有改变任何内容”的检查;而对于还没有滚动的访客或使用屏幕阅读器的人来说,能获取到的信息也和检查器一样少。只需在 DOM 中加入一个小型实时读数,就能同时解决这两个问题(cases/char-curtain§5 #3)。 - 一百次运行中只发生一次的网络请求故障,就足以让整套测试失去可信度;通常罪魁祸首是 CDN 字体。 两个案例保留了 Google Fonts
<link>,因为换成相似字体会改变设计,而非仅仅移植设计——理由听起来合理,结论却错了。整套测试曾有一次因console/page errors变红,之后又专门重跑了 12 次,全部通过。无法复现的间歇性故障比稳定故障更糟,因为人们遇到它时的第一反应就是不再相信整套测试。保留原始字体,移除网络依赖:获取 OFL 原始字体文件,按各页面实际使用的字符分别制作子集(可变衬线字体从 65.8 → 40.9 KB),再将它们内嵌。 - 通过破坏案例来验证判定规则。
--rail的劫持断言在删掉preventDefault()的版本上也能通过,因为overflow:hidden已经锁定了scrollY。这个检查毫无意义,而且如果不处理就会永远如此。提交每条新判定规则之前,都要在对应案例的故意破坏版本上运行(cases/wheel-rail§3)。 - 在经过变换的祖先元素内部,
position:sticky不会生效。 劫持轨道中的任何固定元素,都必须根据轨道偏移量手动停放。这不是需要调试的 bug,而是规范本身的规定(cases/wheel-rail§2)。 - 后处理流程必须从一开始就暴露调试通道,否则根本无法调试。 “头部渲染成了黑色块”让我去检查胶囊网格;只需看一眼原始颜色缓冲区,就能发现阴影位于人物上方,也就是摄像机基向量反转了。从你写下第一个画面开始,就应暴露原始颜色、线性深度和边缘幅值(
cases/ink-crowd§4)。 - 静止也是规格的一部分。 该参考的 504 帧中有 444 帧没有任何像素变化;它的全部动效预算都花在了物体何时出现上。如果页面的特征是断续跳动,应该断言
document.getAnimations()为空,并检查事件之间的矩形边界完全一致,而不是调整缓动曲线。 - 无法解析的样式表不会报错——它会恢复解析,而恢复过程会吞掉后续代码块。
横幅注释中多写了一个
*/,导致注释提前结束,解析器把后面的:root{}当成垃圾规则的内容吞了进去:23 个令牌未定义,background: var(--ground)在 计算值阶段无效,整个font:简写被丢弃,最终变成 Times 16px——而页面仍通过了verify_case.py中的所有检查(cases/string-clock,踩过的坑 #1)。绝不要在注释中写*/; 现在底层环境会检查所有被引用但未定义的自定义属性,也会检查 body 是否透明。 - 颜色不是
<image>,所以它会让整条声明失效。background-image: <gradient>, #eae3d3无效,整条声明都会被丢弃——背景根本不会绘制,而且没有任何日志。颜色应放在background-color中,并置于渐变下方。 - 匹配不到内容的脚本编辑仍会报告成功。 某次会话中的两个
str.replace调用,因为匹配模式已与文件内容不符,悄然没有修改任何字节。每次脚本编辑后都要断言文件确实发生了变化,否则下一次检查测的还是旧页面。 - 变换中的百分比基于元素自身的盒子解析,而不是基于容器。 对一个 64px 的数字应用
translateY(-40%),结果是 26px,而不是表盘高度的 40%;如果同一个令牌既用作宽度,又用作变换参数(--dial: min(74vh, 100%, 760px)),它在两处的解析结果会不同,因此同一条声明可能在一个断点正确、在另一个断点错误。 - 离散控件由提交状态驱动,而不是由脉冲驱动。 带有档位的表盘收到一次滚轮步进,意思是“前往下一档”,而不是“增加速度”:鼠标报告的
deltaY为 120,触控板则为 600 以上;弹簧又一直在拉回,实际移动距离因此无法预测。也绝不要无条件对滚轮事件调用preventDefault()——到达控件两端时应交还滚动,否则页面无法滚动。 - 不要让动效掩盖薄弱的层级、不易阅读的文字或缺失的内容。
- 静帧也是交付物的一半。 禁用动效后截取页面,并确保你愿意单独为那张静帧辩护。“不好看 / 像 AI 做的”几乎从来不是动效调参问题——先去调弹簧是文档里明确指出的错误做法(
design-slop.md)。 - 先设计页面,再把机制放进去。 宏观结构、字体比例和配色各自都是有独立产出的设计行为,不应只是构建步骤里的一个条款。把机制放进一个已经成立的页面,与在模板上装饰的顺序正好相反;而前者才是有效的顺序。
- 尽可能提取真实动效参数:触发条件、持续时间、缓动、交错方式、变换、媒体条件、库和减少动效行为。
- 检查真实页面之前,只将 X 帖子、视频和截图视为主题信息(Video Reference Protocol)。
- 只有作为网页一部分时,才在此处使用轻量 3D:Hero 场景、产品展示、着色器平面、滚动摄像机、空间卡片或 WebGL-lite 氛围效果。
- 只有画面必须在运行时发生变化时,才需要着色器。 如果用户只是在画面中移动,整个场景都可以由不受光照的
MeshBasicMaterial平面组成——SBS The Boat 的 117 个应用模块中,ShaderMaterial、自定义混合和renderOrder全都为 0(image-plane-3d.md§14)。写下第一个材质之前,先决定要构建哪一种。 - 深度不需要几何体。 无论
(x,y,z)处的对象是网格还是纹理平面,PerspectiveCamera都能对其进行缩短、遮挡、排序和投影。提出模型之前,先问清楚实际需要旋转的是什么;如果答案是“什么都不用转,摄像机只需要移动”,那么整个场景都可以由平面图像组成(references/image-plane-3d.md)。它与拼贴画的区别在于测量,而不是美术。 - 完整的 3D 场景、游戏风格环境、生成模型、重物理场景以及材质/光照流程 → 在排除第 5 级方案之后,转到
3d-scene-studio。 - 复杂构建时使用
MOTION_SPEC.md作为构建契约。快速自由构建则可跳过。 - 只有在桌面端、移动端、减少动效模式、性能和构建/类型检查均已验证后,才能认为实现完成。
- R3F 与 DOM 同步:如果 WebGL Canvas 位于背景中(
pointer-events: none),state.pointer就会失效。应使用全局window.addEventListener('pointermove')获取 NDC 坐标。 - 视差与反投影:如果要将 DOM 元素映射到 WebGL 网格,
Vector3.unproject(camera)会与移动中的视差摄像机冲突。应使用单独的静态占位摄像机进行反投影。 - 滚动形变:不要在 Canvas 上使用 CSS
clip-path。保持 Canvas 为100vw/100vh,通过getBoundingClientRect()跟踪透明的 DOM 占位元素,并对网格的位置/缩放进行线性插值。圆角应使用着色器 SDF,而不是真实几何体。
数据
data/visual-bank.json— 静态优先的视觉设计库:7 种调色板原型、5 套排版 系统、5 种布局网格,每种都包含具体数值(clamp()比例、字间距、网格 CSS)。page-design.md和design-slop.md中的 A4 节均引用此处。选定一种原型并坚持使用;不要混合两种。components.md§1、page-blueprints.md、production-polish.md和page-design.md§2 中引用的测量数据,来自 2026-08 对 32 个网站(18 个获奖创意网站、14 个 产品营销网站)进行的无头运行,使用scripts/measure_structure.py,视口尺寸为 1440×900 和 390×844。这些文件中有摘要;各网站的记录不属于本次发布内容。需要当前基线时,请使用一组新的 URL 重新运行脚本。
案例
cases/ 提供可运行、经过验证的源代码——不同于仅包含技术拆解的库,这里提供了完整页面,而且可以运行。因此,有一条规则至关重要:
机制层照搬,皮层一律不许照搬。 机制——求解器、数学、保护机制、探测界面、
@supports编写顺序——属于工程实现,而非美学标识。可以原样复用;手动重新推导单应矩阵纯属浪费。 外观——调色板、字体数值、网格、区块顺序、文案、虚构品牌——是案例自身的身份。复用这些内容,会得到一个一眼就能认出是此案例、只是换了不同 Logo 的页面,这正是本技能其余部分致力于避免的模板化行为。
每个案例的 index.html 都会在注释横幅中标明边界,其 README.md 会将两者拆分成两个明确的列表(可以照搬 / 不许照搬),并说明每项外观设计决策背后的理由——真正可以迁移的是这些理由,而非具体数值。
| 案例 | 展示内容 | 技术栈 |
|---|---|---|
cases/string-clock/ | 软体与精确端点——将钟表指针表现为两端固定的松弛弹性弦,并用坚硬的尖端弹簧确保读数准确,即使笔画本身会偏移;将 Rods 作为退化情况一并提供;以及从中发现的设计思路(在创意画布上设置产品控件) | 单个 HTML 文件,rAF |
cases/ink-crowd/ | 墨线渲染层——手写 WebGL2(不使用框架或 CDN):将 620 个实例化胶囊体绘制到 MRT 和深度目标,再通过全屏渲染通道仅根据深度不连续处绘制线条(不绘制内部折痕)。两种噪声:一种在每个人物上保持静态,另一种按11 Hz 沸腾时钟变化,而画面以 60 fps 渲染。阴影圆盘和空洞是同一种图元。二值调色板,内联可变字体 | 单个 HTML 文件,WebGL2,rAF |
cases/press-stack/ | 粘性堆叠——与 wheel-rail 相反的方案:不干预原生滚动,九个屏幕各自设置 position:sticky;top:0;height:100vh,让后一个屏幕覆盖前一个。进度从文档中读取((scrollY - offsetTop) / innerHeight),而不是累积计算。文字擦除效果按阅读顺序将单词逐渐点亮,亮度从 0.22 → 1。每个唱片封套和唱片都绘制在画布上,不使用位图 | 单个 HTML 文件,DOM + Canvas 2D,rAF |
cases/wheel-rail/ | 由输入驱动的编排——没有任何内容按计时器播放。拦截 wheel 事件并设置 {passive:false},将其平滑过渡为平移轨道(window.scrollY 保持为 0);指针以 7× 的深度比例驱动三个平面;通过 getPointAtLength 将带标签的节点固定在曲线上;手动定位第 4 幕的曲线场,因为 position:sticky 在经过变换的祖先元素内无法生效 | 单个 HTML 文件,DOM + 内联 SVG,rAF |
cases/toy-flipbook/ | 无需渲染器的纵深旋转——四个精心挑选的预渲染偏航角帧,由弹簧值经 Math.round 硬切换,任何地方都没有过渡效果;画面以 60 fps 渲染,而抖动时钟保持为 11 Hz。一个归一化驱动值为每个图层分别提供增益,包括同一标题中的两行使用相反符号 | 单个 HTML 文件,rAF |
cases/char-curtain/ | 由弦组成而非网格构成的布料——576 个字形分布在 24 列 Verlet 结构上,横向连接数为零;这正是指针可以将其拨开而非压出凹痕的原因。使用弹簧锚点而非固定锚点,通过反射 oldX 实现弹跳;速度同时映射到颜色和透明度,因此静止画面也能显示波纹的位置 | 单个 HTML 文件,Canvas 2D,rAF |
cases/lyre-crows/ | 因果生态,而非时间线——拨动琴弦,释放一个单词;一只受转向控制的乌鸦追逐它,并用捕获的内容重写自己的尾巴。源头限速与产物无条件衰减共同防止循环变成噪声;转向限制的是力而非速度,因此转弯才会形成半径 | 单个 HTML 文件,Canvas 2D,rAF |
每个案例都通过相同的验收底线,并在自己的 README 中列出:没有控制台错误;在 320/375/414/768/1440 下水平溢出为零;只有一个 <h1>;设置了 lang;显示文字与正文文字的字号比 ≥ 4×;≥ 3 种不同的版块形态;没有未定义的自定义属性引用;正文会绘制自己的背景;以及减少动态效果偏好下没有任何内容被隐藏。单屏英雄区域(toy-flipbook)使用 --min-shapes 1,这是英雄区域演示的豁免,不适用于真正的页面。每个案例还会添加一个领域专用判定器。保留了 --beats 和 --detent,但没有关联任何案例:两者都要求页面公开各自的探测契约,因此它们是构建时可参考的模式,而不是可以用于任意页面的检查项:
| 标志 | 断言 | 使用方 |
|---|---|---|
--beats | 扫过滚动轴,将每条轨道按自身范围归一化,并报告所有轨道均未移动的最长时段——这是死拍的操作性定义 | 无对应案例——需要 __probe.seek/tracks |
--detent | 在 36 个位置释放;带吸附功能的控件落在与某个卡位相差 ≤ 0.02° 的位置,稳定下来,触达每个卡位,并在两端限位 | 无对应案例——需要卡位探针 |
--strings | 两项相互制约的断言:每只指针的尖端与真实角度相差 ≤ 1.5°,并且每只指针的下垂幅度仍 ≥ 其弦长的 1.12 倍;此外,使用真实指针以每步 12/60/240 px 的速度甩动时,指针伸展不得超过 3.2 倍,也不得移出边界框;还要进行一次时钟未固定的自由运行测试 | string-clock |
--stack | 粘性定位约定:每个屏幕都必须固定在 top: 0,同时 window.scrollY 持续递增(这与要求其保持为 0 的 --rail 正相反);至少有 3 个不同的地面;字体擦除效果必须在某个时刻显示渐变,并且始终不低于 0.12——一次性点亮所有内容的页面能通过第二项测试,从零开始渐显的页面能通过第一项,只有参考设备能同时通过两项。此外,任何使用位图的元素都会导致测试失败 | press-stack |
--rail | 元素变化、空闲活跃度,加上滚轮劫持:鼠标完全静止 1.5 秒时,仍须至少有 2 个元素持续变化,因为每个动作都等待输入的页面,在有人触碰之前只是一张静态图片。接着,在真实的八轮滚动测试中,至少 35% 的元素必须改变 transform、opacity 或 stroke-dashoffset,并且至少有 4 个元素被捕捉到正在过渡中。第一项断言会捕捉到轨道在移动而所有子元素静止不动的情况;第二项会捕捉到状态跳变的情况。此外,与之前一样,滚轮输入必须被劫持,指针移动必须带来深度效果:真实滚轮必须让轨道移动,同时 window.scrollY 保持为 0,且滚轮事件被取消(单靠 overflow:hidden 将 scrollY 固定为 0 仍会产生过度滚动);一帧后轨道必须可测量地落后于目标,一秒后则必须稳定;从一个角落扫到另一个角落的指针移动,必须使远景平面的移动幅度至少达到近景平面的 1.5 倍 | wheel-rail |
--layer <dom|svg|canvas2d|webgl> | 通过探测实时 canvas 上下文和内联 SVG 图形数量,断言实际渲染层与 README 声明一致,而不是只相信文字描述——声称使用 webgl 却实际使用 div 的案例会失败。此标志还会启用网页字体检查:没有 @font-face 且没有预加载字体即判定失败,因为系统字体是生成页面的标志,其他检查都无法发现(页面自行绘制字形时使用 --draws-own-type) | ink-crowd |
--follow | 由指针引导的运动:瞄准画布上的五个点——包括裸露的地面——并要求人群质心从每个点都向目标靠拢。跟随计数器持续跳动不能证明任何东西真的移动了;只在一个区域有效、其他地方都毫无反应的机制仍能通过其他所有检查 | ink-crowd |
--flipbook | 两项相互制约的断言:在 60 个采样点组成的真实指针扫动过程中,每一帧都必须完全点亮或完全熄灭(交叉淡化会失败),并且扫动必须按顺序经过每一帧,不能跳过(直接跳到结束姿势的控件会失败)。此外,抖动必须以 4 至 25 Hz 的频率重新抽取;滚轮旋转超过触发阈值后,必须让桌面玩具离开桌面,并在落地时被压扁——检测程序会打印腿部的峰值驱动力,若从未超过阈值则判定失败,因此未触发的测试不会悄悄通过 | toy-flipbook |
--curtain | 两项相互制约的断言:真实指针扫动必须使中间列横向移动 ≥ 6px,并且拨动一列后,布料远端的移动幅度必须小于拨动处的 20%。死布料无法通过第一项;带有水平连接的布料能通过第一项,却无法通过第二项。此外,任何连接都不得偏离静止长度超过 35%,且每个锚点都必须回到距固定点 2px 以内。基线会持续轮询,直到布料可测量地静止,而不是等待一段固定时间——参见上面的经验教训 | char-curtain |
--lyre | 两项相互制约的断言:十四次真实拨弦扫动必须释放文字,并且在受到反复操作时,空中文字数量绝不能超过页面声明的上限。装饰性乐器能通过数量上限检查;无限发射器则会让文字泛滥(损坏的副本曾达到 440 个)。此外,只要有猎物浮在空中,乌鸦就必须进入捕猎状态,并将与猎物的距离缩短 ≥ 30%;被计数的捕获必须改变那只乌鸦的尾巴,并让被捕获的文字留在尾巴上 | lyre-crows |
只有通过 verify_case.py 的新案例,才算真正的案例。 |
构建案例前,请先阅读 cases/AUTHORING.md。 其中介绍了 参考 → 复刻 → 收录的流程:准入门槛(去掉图片后,参考资料是否仍然有效?)、测量脚本、四项分类决策、构建规则、判定标准,以及 README 必须说明的内容——设计系统 + 动效系统 + 判据 + 可照搬/不许照搬 + 踩过的坑。
在以下情况下阅读案例:用户的需求属于该类别、你需要了解实现机制,或者你想看一个文档规则实际应用的完整示例。不要打开案例来“从头开始”——从
page-blueprints.md 入手,并从中提取实现机制。
参考文件
| 文件 | 内容 |
|---|---|
references/choreography-arc.md | 5 种页面级动效弧线类型及其动效 token |
references/motion-tokens.md | 时长尺度、缓动词典、弹簧配置速查表、交错模式、变换预算 |
references/pattern-recipes.md | 可直接使用的动效模式实现(Vanilla + Framer + GSAP)——请自行计数,不要相信这里写的数字。包括 #17 角点固定扭曲(真形变,不是 skew)和基于它构建的 #18 Verlet 卡片链 |
references/affordance.md | 能点 ≠ 看得出能点:邀请审查、窗口与停留、绘制包围盒标记、按下时记录/抬起时决定、数值判据、隐形交接 |
references/handfeel.md | 交互手感:欠阻尼弹簧、速度倾斜、滚轮动量防护、果冻摆动、为平面图像伪造光照——另有 §7 后半部分(帧率无关的 exp(-k·dt)、speed/k 稳态滞后及其超前项、为什么滤波后的修正会来回振荡、由加速度驱动的临界阻尼)和 §8 屏幕抖动 |
references/build-mode.md | 实现规则、性能规则、验证清单 |
references/page-design.md | 动效页怎么设计:macrostructure 选择、给动效让位的版式、测量自当前获奖站的字号阶梯与配色规模、滚动叙事的分段、移动端降级 |
references/design-slop.md | 动效页专属的 AI 味闸门:关掉动效看静帧、默认 hero、三栏特性卡、紫色渐变、装饰代替设计、统一 hover 抬升、全页 fade-up、首屏烧光预算 |
references/page-blueprints.md | 这页到底放什么:区块词汇表、7 种蓝图(产品/作品集/工作室/活动/编辑出版/索引/文档)、排序逻辑、每个区块在实际文案中应包含什么、信息架构,以及经测量得出的区块数量与形态多样性数据 |
references/components.md | 组件级 craft:两种有明显可测差异的组件风格(创意型与产品型——圆角、阴影、大写字母、导航 CTA、页脚规模)、每个组件都应具备的状态矩阵,以及导航/hero/按钮/标签/卡片/媒体/表单/折叠面板/对话框/索引/证明/页脚 |
references/project-setup.md | 工程 0→1:页面技术栈阶梯(单个 HTML 文件 → Vite → Astro → Next)、三级 token 层 + Tailwind v4 @theme、使用 clamp() 公式的流体字号与间距尺度、带度量覆盖项的字体加载、@layer 顺序、文件布局、图像、预算与部署 |
references/production-polish.md | 从能跑到成品:完整的 head 区块、OG 图像、404、非理想路径下的状态、无闪烁暗色模式、精致的收尾细节、机器可读结构、分析,以及上线前的十项手动检查 |
references/baseline-ui.md | 无障碍与易用性的底线(焦点、触控目标、无需 hover 的移动端体验)——这是底线,不是设计 |
references/motion-spec.md | MOTION_SPEC.md 模板及条目字段定义 |
references/composition-guide.md | 如何编排自定义设计与动效,而不受僵化模板限制 |
references/shaders-spec.md | WebGL GLSL 着色器、simplex 噪声、渲染优化——以及 §6 中不易察觉的失败:int 与 float 字面量不匹配导致程序失效、缺少 tonemapping/colorspace 导入、DepthTexture 过滤、高频细节的距离衰减、空间连贯的实例化风动 |
references/tactile-texture.md | SVG 黏滞滤镜、CSS Houdini、OffscreenCanvas 和 canvas 颗粒纹理模式 |
references/image-asset-pipeline.md | 图片资产怎么造出来:以图像自身背景为基准抠图、coverage 闸门、包围盒裁剪、线稿 alpha、脚本化的 cx/baseline、没人记录的冠部锚点、每个元素的分辨率预算、纹理密度与展示距离、来源记录 |
references/scene-streaming.md | 大场景怎么装得下:屏幕窗口激活、协作式扫描、节流 DOM 修改、完整释放、分片纹理上传、按可听度流式传输音频 |
references/verification-harness.md | 判据台架:探针契约(__seek/__hold/可固定的时钟/状态退出)、开关差分、为什么瞬移和屏幕差分会说谎、确定性作为前提、采样率、标尺的分辨率下限——以及为什么综合质量分数还不如不做测试 |
references/timeline-orchestration.md | 多轨滚动时间轴:横向阅读时间表、单一缓动层、按视觉关注度分配权重、一个窗口一个事件、速度归零关键帧、寿命长于内容的装置、关键帧、阈值图直方图 |
references/scene-audio.md | 场景声音:三条总线(bgm/hand/world)、每个段落的 LUFS 曲线、闪避、在更响的底层音轨上叠加离散事件、为什么连续量会被感知为氛围、REST 判据、滤波器造成的 20 dB 损耗 |
references/image-plane-3d.md | 图片资产做出真纵深:把平面抠图作为平面置于真实透视相机中——资产元数据契约、长焦镜头、轮廓交错、手动排序的透明度、解析式接触阴影、用色温消除「拼接感」 |
references/light-3d.md | Three.js/R3F/canvas 集成规则 + 置于动态场景之上的 DOM |
脚本
# ── MEASURE STRUCTURE (the static-page half: sections, components, tokens, polish) ─
# Answers "how is it built as a page": sections, nav, buttons, token scales, forms, media, meta, mobile overflow.
python3 scripts/measure_structure.py https://example.com
python3 scripts/measure_structure.py --urls-file urls.txt --out data/structure-bank.json
# Skips URLs already present in the out file, so a killed run resumes.
# ── DECONSTRUCT A VIDEO REFERENCE (no URL to open) ────────────────────────────
# measure_structure needs a loadable page. A clip has none.
python3 scripts/measure_frames.py sheet ref.mp4 # contact sheet — ALWAYS look first
python3 scripts/measure_frames.py palette ref.mp4 -t 8 # ground %, accents, mid-grey scale
python3 scripts/measure_frames.py bands ref.mp4 -t 8 # control heights/widths off the pixels
python3 scripts/measure_frames.py trace ref.mp4 --color red # arc/chord per frame
python3 scripts/measure_frames.py grounds ref.mp4 # section changes + which way the new screen arrives
python3 scripts/measure_frames.py wipe ref.mp4 --from 7.9 --to 10.1 --dark
# ^ the schedule of a per-word reveal, and the one number a still frame cannot give
# you: whether unlit type is hidden (0.00) or merely dimmed (side-b: 0.22).
# ── VERIFY A CASE / ANY BUILT PAGE ────────────────────────────────────────────
# The acceptance floor every case's README claims. Re-runnable, so the claim can't rot.
python3 scripts/verify_case.py cases/string-clock/index.html --strings # exact tip, sagging body
python3 scripts/verify_case.py cases/ink-crowd/index.html --layer webgl --follow # declared layer + pointer-led crowd
python3 scripts/verify_case.py cases/wheel-rail/index.html --rail # hijacked wheel + depth + churn
python3 scripts/verify_case.py cases/press-stack/index.html --stack # sticky dwell + type wipe + drawn art
python3 scripts/verify_case.py cases/toy-flipbook/index.html --min-shapes 1 --flipbook # hard cut AND continuous scrub
python3 scripts/verify_case.py cases/char-curtain/index.html --min-shapes 1 --curtain # parts AND stays independent
python3 scripts/verify_case.py cases/lyre-crows/index.html --min-shapes 1 --lyre # releases AND stays limited
python3 scripts/verify_case.py http://localhost:5173 # works on a dev server too
# ── DOES ANYTHING ACTUALLY MOVE? ──────────────────────────────────────────────
# The check that caught what every oracle above missed twice: a page with all the
# right mechanisms that reads as dead. Drives a REAL wheel; scrollTo teleports
# past the arriving-and-leaving window where the motion lives.
python3 scripts/measure_churn.py https://a-reference.com # characterise it first
python3 scripts/measure_churn.py cases/foo/index.html --against https://a-reference.com
# ── SHIP REAL TYPE, NOT THE SYSTEM STACK ──────────────────────────────────────
# 0 @font-face is the loudest generated-page tell (render-layer.md §2).
python3 scripts/subset_fonts.py list
python3 scripts/subset_fonts.py pack --out /tmp/faces.css \
Grot=Inter-Variable Ital=InstrumentSerif-Italic Mono=IBMPlexMono-Regular
自检(编辑 scripts/ 或任何案例后运行,或用户要求对这个技能进行体检时运行):
cd ~/.claude/skills/motion-web
python3 -c "import json;json.load(open('data/visual-bank.json'))" && echo "json OK"
python3 -m py_compile scripts/*.py && echo "syntax OK"
# then the seven verify_case.py lines above — every one must print VERDICT: PASS
zsh 不会拆分变量。 flags="--layer webgl"; verify "$f" $flags 会将整个字符串作为一个参数传递,
导致 argparse 报告 unrecognized arguments。请直接写出各个标志,或使用 ${=flags}。
输出约定
规范编写工作:根据 references/motion-spec.md 中的模板,输出简洁的 MOTION_SPEC.md。
自由形式构建:跳过规范,立即构建。完成后,告知用户:
- 概念名称及选择它的原因
- 弧线类型及其如何塑造页面节奏
- 最重要的 3 项动效决策
- 需要验证的内容(减少动态效果、移动设备、FPS)
对于 full-build:说明选择的蓝图及原因、页面栈层级、组件风格(创意型还是产品型)、动效概念、所有需要自行设定的事实信息,并逐项明确列出,以及 production-polish.md §9 的结果——哪些检查已通过,哪些无法运行。
对于任何构建:报告完成前先在本地验证。适用时运行构建和类型检查。说明更改内容、已验证的项目,以及仍存在的风险。