Sfoglia il codice sorgente

feat: 动画能力三件套——分镜卡基本功+镜头语言+产品UI展示动画

- 新增 references/storyboard-basics.md:定格帧十一律+景别体系+能量骨架+轻量分镜卡(八字段/镜),任何动画开工前必出分镜卡
- 新增 references/camera-language.md:zoom/pan/orbit/parallax/转场的预算制+镜间语法+PageCam相机数学+CSS zoom栅格化
- 新增 references/ui-demo-animation.md:产品UI演示单一入口,截图运镜vs HTML重建决策树+UI展示八式
- 新增 assets/cursor.jsx:macOS光标4形状+CursorSprite弧线轨迹(Catmull-Rom+收敛手抖)+ClickRipple双圈解耦,帧确定性
- gsap-recipes.md +§9 Camera Rig实现配方;animation-pitfalls.md +镜头级运动坑
- SKILL.md路由表接入三个新入口;导演稿gate最低要求升级为分镜卡格式

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
alchain 1 mese fa
parent
commit
a1064dfd62

+ 7 - 2
SKILL.md

@@ -30,7 +30,8 @@ description: 花叔Design——用HTML做高保真原型、幻灯片、动画、
 | 提到具体品牌/产品名 | 核心原则#0 事实验证 → §1.a 资产协议 → 标准流程 |
 | 🔴 任何会产出新视觉设计的任务(**无论有没有风格参考、有没有品牌名,100% 必走**) | 三方向硬门:Fallback Phase 1-5 出三版真实初稿等用户选 → 回标准流程 Step 2 |
 | 幻灯片/PPT | 标准流程 + Step 1 deck 交付链 + 「技术红线」架构选型 |
-| 动画/导出 MP4/GIF | 标准流程 + Step 9;**新动画项目默认走 HyperFrames 后端**(选型边界+契约 → `references/hyperframes-backend.md`,GSAP 实现配方 → `references/gsap-recipes.md`);动手前必读 `references/animation-pitfalls.md` |
+| 动画/导出 MP4/GIF | 标准流程 + Step 9;**任何动画开工前先按 `references/storyboard-basics.md` 出轻量分镜卡**(每一镜先是一张会动的封面);镜头级运动(zoom/pan/转场)必读 `references/camera-language.md`;**新动画项目默认走 HyperFrames 后端**(选型边界+契约 → `references/hyperframes-backend.md`,GSAP 实现配方 → `references/gsap-recipes.md`);动手前必读 `references/animation-pitfalls.md` |
+| 🖥️ **宣传的产品有 UI 界面**(产品动画/功能演示/商单,画面主角是一个界面) | 上一行动画链 + **单一入口 `references/ui-demo-animation.md`**(截图运镜 vs HTML 重建决策树 + UI 展示八式 + `assets/cursor.jsx` 光标组件);UI 截图取材走 §1.a 资产协议 |
 | 带解说长视频(≥1分钟) | Step 9.5 → `references/voiceover-pipeline.md` |
 | launch film/品牌宣传片(「Apple级」「超级碗品质」) | **三方向硬门先行**(方向板级初稿,见 Fallback「三方向初稿形态」)→ 用户选定后再写万字 director's notes → `references/launch-film-director-notes.md` |
 | App/iOS 原型 | 「App / iOS 原型专属守则」(覆盖通用规则) |
@@ -369,7 +370,7 @@ description: 花叔Design——用HTML做高保真原型、幻灯片、动画、
 |---|---|---|
 | `brand-spec.md` | §1.a资产协议产物 | 涉及具体品牌/产品的任何设计 |
 | `direction-approved.md` | 三方向真实视觉展示+**用户选择原话**记录(含三版初稿截图路径)。🔴 **没有「已有明确design context」豁免通道**(该通道2026-07-18被实锤滥用后废止)——唯一合法豁免=Fallback「唯一豁免」三种情形,且必须记用户原话/迭代来源 | 实现开工前;**≥45s长片渲染前有hook硬检查**(scripts/design-gate-hook.sh,缺文件block渲染,用户明说跳过用SKIP_DESIGN_GATE=1显式放行) |
-| `导演稿.md`/director's notes | 长片/launch film的分镜与**视觉密度条款**(标准+参照标杆+氛围层清单,见animation-best-practices §6.5) | ≥20s动画开工前;launch film级(品牌宣传片/「Apple级」预期)在此基线上按launch-film-director-notes.md升级为万字notes——导演稿是底线,万字notes是launch film的加强版,不是两套并行要求 |
+| `导演稿.md`/director's notes | 长片/launch film的分镜与**视觉密度条款**(标准+参照标杆+氛围层清单,见animation-best-practices §6.5)。**最低要求=storyboard-basics.md §5的轻量分镜卡格式**(八字段/镜,含[CAMERA]列与验收帧号)| ≥20s动画开工前;<20s动画不强制导演稿但分镜卡照画(storyboard-basics §0);launch film级(品牌宣传片/「Apple级」预期)在此基线上按launch-film-director-notes.md升级为万字notes——分镜卡是底线,万字notes是launch film的加强版,不是两套并行要求 |
 
 **「用户说继续」授权的是进入下一步,不是跳过该步内部的gate**。跳过必须用户明说,且把「用户明示跳过」写进对应gate文件。**弱runtime降级模式不豁免gate文件**——降级第5条允许把检查点问答换成assumption清单,但三个gate文件本身照写(写文件不耗上下文),assumption清单就写进对应gate文件里。
 **两套检查点的衔接**:主干用 🛑 检查点1-5,Fallback 用 🔴 CHECKPOINT(Phase 3.5 图片前置 + logo 子门)。从 Fallback Phase 1-5 走完回到主干 Step 2 时,检查点1(问题清单)已被 Phase 1 的澄清覆盖,**跳过不重复问**;检查点2 起照常执行。
@@ -447,6 +448,7 @@ description: 花叔Design——用HTML做高保真原型、幻灯片、动画、
 | `android_frame.jsx` | Android App mockup | 设备bezel |
 | `macos_window.jsx` | 桌面App mockup | 窗口chrome + 红绿灯 |
 | `browser_window.jsx` | 网页在浏览器里的样子 | URL bar + tab bar |
+| `cursor.jsx` | 产品UI演示里的光标操作叙事 | macOS光标4形状 + CursorSprite弧线轨迹(Catmull-Rom+收敛手抖)+ ClickRipple双圈解耦 + hover联动 + GSAP/Stage双驱动,帧确定性 |
 
 用法:读取对应 assets 文件内容 → inline 进你的 HTML `<script>` 标签 → slot 进你的设计。
 
@@ -464,6 +466,9 @@ description: 花叔Design——用HTML做高保真原型、幻灯片、动画、
 | 做幻灯片 | `references/slide-decks.md` + `assets/deck_index.html`(默认多文件概览墙)+ `scripts/gen_deck_thumbs.mjs`(画廊缩略图)+ `assets/deck_stage.js`(仅 ≤5 页单文件) |
 | 导出可编辑 PPTX(html2pptx 4 条硬约束) | `references/editable-pptx.md` + `scripts/html2pptx.js` |
 | 做动画/motion(**先读 pitfalls**)| `references/animation-pitfalls.md` + `references/animations.md` + `assets/animations.jsx` |
+| ⭐ **动画分镜/画面构图**(任何动画开工前;每一镜先是一张会动的封面:定格帧十一律+景别体系+能量骨架+轻量分镜卡) | `references/storyboard-basics.md`(launch-film 导演稿是它的重装版) |
+| ⭐ **镜头语言/运镜**(zoom/pan/orbit/parallax/转场;预算制+镜间语法+PageCam 相机数学+CSS zoom 栅格化) | `references/camera-language.md`(设计判断)+ `gsap-recipes.md` §9 Camera Rig(实现) |
+| ⭐ **产品UI展示动画**(画面主角是一个界面:截图vs重建决策树+UI展示八式+typing+光标+3D巡览) | `references/ui-demo-animation.md` + `assets/cursor.jsx` |
 | **HyperFrames 渲染后端**(新动画默认;选型边界/合成契约/老demo迁移/check流程) | `references/hyperframes-backend.md` |
 | **设计语言的 GSAP 实现配方**(easing 映射/运动语言8条/五段叙事骨架/seek 安全规则) | `references/gsap-recipes.md` |
 | **动画的正向设计语法**(Anthropic 级叙事/运动/节奏/表达风格)| `references/animation-best-practices.md`(5 段叙事+Expo easing+运动语言 8 条+3 种场景配方)|

+ 350 - 0
assets/cursor.jsx

@@ -0,0 +1,350 @@
+/**
+ * Cursor — 产品UI演示光标组件包
+ *
+ * 配合 browser_window.jsx / macos_window.jsx 使用,配方与参数出处见
+ * references/ui-demo-animation.md 八式④(轨迹算法:animation-best-practices §3.5;
+ * ripple 参数:shotcraft·type-and-filter + 解耦配方;seek 安全规则:gsap-recipes §6)。
+ *
+ * 帧确定性:全文件禁 Math.random / Date.now,随机感一律 mulberry32 种子推导。
+ * 同一帧无论 seek 多少次,画面完全一致。
+ *
+ * ── 用法A · Stage 时钟(animations.jsx)─────────────────────────
+ *
+ *   const { Stage, Sprite } = window.Animations;
+ *   const { CursorSprite, ClickRipple, HoverHighlight } = window;
+ *
+ *   <Stage duration={8}>
+ *     <Sprite start={1} end={2.2}>   {/* 光标弧线移到按钮,末段收敛手抖 *\/}
+ *       <CursorSprite points={[[220, 480], [860, 300]]} seed={7} clickAt={0.96} />
+ *     </Sprite>
+ *     <Sprite start={2.1} end={3.0}> {/* 点击涟漪:双圈解耦 *\/}
+ *       <ClickRipple x={860} y={300} color="#D97757" duration={0.9} />
+ *     </Sprite>
+ *   </Stage>
+ *
+ *   hover 联动高亮(时间驱动命中,非事件驱动):
+ *     const sampler = window.CursorKit.buildCursorSampler(points, { seed: 7 });
+ *     const hovered = window.CursorKit.hoverIndexAt(sampler, easedU, [
+ *       { id: 'save', rect: { x: 820, y: 270, w: 96, h: 44 } },
+ *     ]);
+ *     <HoverHighlight rect={{...}} intensity={hovered === 'save' ? 1 : 0} />
+ *
+ *   拖拽:光标传 dragRange={[0.2, 0.8]}(区间内切抓取手型+微缩),
+ *   被拖元素用同一 sampler 采样减去抓取点偏移驱动,光标和元素永远同步。
+ *
+ * ── 用法B · GSAP timeline(HyperFrames 渲染)───────────────────
+ *
+ *   const K = window.CursorKit;
+ *   const sampler = K.buildCursorSampler([[220, 480], [860, 300]], { seed: 7 });
+ *   K.attachCursorTween(tl, '#cursor', sampler, { duration: 1.1, position: 's1+=0.5' });
+ *   K.attachClickTween(tl, '#cursor', { position: '>' });
+ *   K.attachRippleTween(tl, '#rip1', '#rip2', { position: '<' });
+ *   // 别忘了 gsap-recipes §6.3 的首帧保险:注册 timeline 后手动补一次初始 set
+ *
+ * 光标形状:arrow(macOS 箭头,默认)/ hand(可点手型)/ grab(拖拽中)/ text(I-beam)
+ */
+
+/* ══════════════ 工具层(纯函数,两种驱动共用)══════════════ */
+
+function mulberry32(seed) {
+  return function () {
+    seed |= 0; seed = (seed + 0x6d2b79f5) | 0;
+    let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
+    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
+    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
+  };
+}
+
+const CursorEasing = {
+  outCubic: (t) => 1 - Math.pow(1 - t, 3),
+  inOutQuad: (t) => (t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2),
+  inQuad: (t) => t * t,
+};
+
+// Catmull-Rom 单段插值(p1→p2,p0/p3 是相邻控制点)
+function catmullRom(p0, p1, p2, p3, t) {
+  const t2 = t * t, t3 = t2 * t;
+  return [
+    0.5 * ((2 * p1[0]) + (-p0[0] + p2[0]) * t +
+      (2 * p0[0] - 5 * p1[0] + 4 * p2[0] - p3[0]) * t2 +
+      (-p0[0] + 3 * p1[0] - 3 * p2[0] + p3[0]) * t3),
+    0.5 * ((2 * p1[1]) + (-p0[1] + p2[1]) * t +
+      (2 * p0[1] - 5 * p1[1] + 4 * p2[1] - p3[1]) * t2 +
+      (-p0[1] + 3 * p1[1] - 3 * p2[1] + p3[1]) * t3),
+  ];
+}
+
+/**
+ * buildCursorSampler(points, opts) → sample(u) → {x, y}
+ *
+ * - points 只有 2 个时自动插一个偏离中点的控制点做弧线
+ *   (真人鼠标不走直线,best-practices §3.5),偏移方向由 seed 决定
+ * - ≥3 个点走 Catmull-Rom 平滑(huarec 光标平滑同款插值)
+ * - 手抖:两条不可通约频率正弦叠加,幅度 ±wobble px,
+ *   随 u→1 收敛到 0(接近目标时人手会稳)
+ */
+function buildCursorSampler(points, opts) {
+  const o = Object.assign({ seed: 7, wobble: 2, arc: 0.18 }, opts);
+  const rand = mulberry32(o.seed);
+  const ph1 = rand() * 6.283, ph2 = rand() * 6.283;
+  const side = rand() < 0.5 ? -1 : 1;
+
+  let pts = points.map((p) => [p[0], p[1]]);
+  if (pts.length === 2) {
+    const [a, b] = pts;
+    const dx = b[0] - a[0], dy = b[1] - a[1];
+    const mid = [a[0] + dx * 0.5 - dy * o.arc * side, a[1] + dy * 0.5 + dx * o.arc * side];
+    pts = [a, mid, b];
+  }
+  // 首尾补虚拟点,让 Catmull-Rom 覆盖全程
+  const ext = [pts[0], ...pts, pts[pts.length - 1]];
+  const segs = pts.length - 1;
+
+  return function sample(u) {
+    const uu = Math.max(0, Math.min(1, u));
+    const f = uu * segs;
+    const i = Math.min(segs - 1, Math.floor(f));
+    const lt = f - i;
+    const [x0, y0] = catmullRom(ext[i], ext[i + 1], ext[i + 2], ext[i + 3], lt);
+    const damp = o.wobble * (1 - uu);            // 接近目标收敛
+    return {
+      x: x0 + Math.sin(uu * 47.13 + ph1) * damp, // 47.13 / 33.7 不可通约
+      y: y0 + Math.sin(uu * 33.7 + ph2) * damp,
+    };
+  };
+}
+
+// hover 命中:时间驱动的确定性 hit test(不是事件监听)
+function hoverIndexAt(sampler, u, targets, pad) {
+  const p = sampler(u);
+  const m = pad || 0;
+  for (const t of targets) {
+    const r = t.rect;
+    if (p.x >= r.x - m && p.x <= r.x + r.w + m && p.y >= r.y - m && p.y <= r.y + r.h + m) return t.id;
+  }
+  return null;
+}
+
+/**
+ * rippleRingState(tSec, opts) → { scale, opacity }
+ * 双圈 ripple 的单圈状态。扩散与消散解耦(shotcraft 实测配方):
+ *   扩散 out-cubic EXPAND 帧(冲),消散线性 FADE 帧(匀),FADE > EXPAND。
+ * 默认 22f/26f@30fps;紧凑场景(type-and-filter)可压到各 10f。
+ */
+function rippleRingState(tSec, opts) {
+  const o = Object.assign({ delayF: 0, expandF: 22, fadeF: 26, r0: 14, r1: 54, fps: 30 }, opts);
+  const t = tSec - o.delayF / o.fps;
+  if (t < 0) return { scale: o.r0 / o.r1, opacity: 0 };
+  const pe = Math.min(1, t / (o.expandF / o.fps));
+  const pf = Math.min(1, t / (o.fadeF / o.fps));
+  return {
+    scale: (o.r0 + (o.r1 - o.r0) * CursorEasing.outCubic(pe)) / o.r1,
+    opacity: 1 - pf,
+  };
+}
+
+/* ══════════════ 光标形状(SVG,黑体白描边,paintOrder 保准确轮廓)══════════════ */
+
+const CURSOR_PATHS = {
+  // macOS 箭头:左缘垂直、斜边到右翼、带点击尾。热点在 (0,0)
+  arrow: {
+    viewBox: '0 0 17 22',
+    d: 'M1.5 1.5 L1.5 18.6 L6.4 13.9 L9.1 20.3 L11.9 19.1 L9.2 12.8 L14.5 12.8 Z',
+    hotspot: [1.5, 1.5],
+  },
+  // 可点手型(简化食指手)。热点在指尖
+  hand: {
+    viewBox: '0 0 22 24',
+    d: 'M9.2 1.9 c1 0 1.5 .7 1.5 1.6 v6.1 l1 .1 v-4.4 c0-1.9 2.8-1.9 2.8 0 v4.7 l.9 .1 v-3.2 c0-1.8 2.6-1.8 2.6 0 v3.6 l.9 .2 v-1.6 c0-1.6 2.3-1.6 2.3 0 v5.6 c0 4.3-2.9 7.3-7.3 7.3 h-2.1 c-2.9 0-4.5-1.3-5.9-3.7 L3.1 13.4 c-.7-1.2 .8-2.4 1.9-1.5 l2.7 2.3 V3.5 c0-.9 .6-1.6 1.5-1.6 Z',
+    hotspot: [9.9, 1.9],
+  },
+  // 拖拽中(握拳):hand 的收指变体
+  grab: {
+    viewBox: '0 0 22 22',
+    d: 'M5.4 7.2 c0-1.7 2.5-1.7 2.5 0 v2.1 l.9 0 v-3.3 c0-1.8 2.7-1.8 2.7 0 v3.3 l.9 0 v-2.9 c0-1.8 2.6-1.8 2.6 0 v3 l.9 .1 v-1.7 c0-1.6 2.3-1.6 2.3 0 v5.1 c0 4.2-2.8 7-7.1 7 h-1.9 c-2.8 0-4.4-1.2-5.7-3.6 L2.5 13.1 c-.6-1.2 .8-2.3 1.8-1.4 l1.1 .9 Z',
+    hotspot: [10, 8],
+  },
+  // 文本 I-beam。热点在中心
+  text: {
+    viewBox: '0 0 10 22',
+    d: 'M1 1.5 h3 v0 c.4 0 .7 .2 1 .5 c.3-.3 .6-.5 1-.5 h3 v2 h-2.6 c-.2 0-.4 .2-.4 .4 v14.2 c0 .2 .2 .4 .4 .4 H9 v2 H6 c-.4 0-.7-.2-1-.5 c-.3 .3-.6 .5-1 .5 H1 v-2 h2.6 c.2 0 .4-.2 .4-.4 V3.9 c0-.2-.2-.4-.4-.4 H1 Z',
+    hotspot: [5, 11],
+  },
+};
+
+function CursorIcon({ variant = 'arrow', size = 22 }) {
+  const s = CURSOR_PATHS[variant] || CURSOR_PATHS.arrow;
+  return (
+    <svg width={size} height={size * 1.25} viewBox={s.viewBox}
+      style={{ display: 'block', overflow: 'visible' }}>
+      <path d={s.d} fill="#111" stroke="#fff" strokeWidth="1.4"
+        strokeLinejoin="round" style={{ paintOrder: 'stroke' }} />
+    </svg>
+  );
+}
+
+/* ══════════════ Stage 时钟组件(配合 animations.jsx)══════════════ */
+
+/**
+ * CursorSprite — 放在 <Sprite> 内,沿路径移动的光标
+ *
+ * props:
+ *   points     [[x,y],...] 路径点(舞台坐标)。2 个点自动成弧
+ *   seed       随机种子(换 seed = 换一版弧线和手抖)
+ *   wobble     手抖幅度 px(默认 2,best-practices §3.5 的 ±2px)
+ *   ease       进度缓动,默认 inOutQuad(起步加速+到达减速的对称人手感)
+ *   clickAt    0-1,此进度处做点击下压(scale 0.85 dip + 回弹,Anticipation)
+ *   dragRange  [u0,u1],区间内切 grab 手型 + scale 0.94
+ *   variant    基础形状,默认 'arrow'
+ *   size       光标宽 px,默认 22
+ */
+function CursorSprite({
+  points, seed = 7, wobble = 2, ease = CursorEasing.inOutQuad,
+  clickAt = null, dragRange = null, variant = 'arrow', size = 22, style,
+}) {
+  const { useSprite } = window.Animations;
+  const { t } = useSprite();
+  const sampler = React.useMemo(
+    () => buildCursorSampler(points, { seed, wobble }),
+    [JSON.stringify(points), seed, wobble]
+  );
+  const u = ease(t);
+  const p = sampler(u);
+
+  let scale = 1;
+  let shape = variant;
+  if (dragRange && u >= dragRange[0] && u <= dragRange[1]) {
+    shape = 'grab';
+    scale = 0.94;
+  }
+  if (clickAt !== null) {
+    const d = (u - clickAt) / 0.05;              // 点击窗口 ±5% 进度
+    if (d >= 0 && d < 1) scale *= 0.85 + 0.15 * CursorEasing.outCubic(d);      // 回弹
+    else if (d >= -0.6 && d < 0) scale *= 1 - 0.15 * CursorEasing.inQuad(1 + d / 0.6); // 下压
+  }
+
+  const hs = (CURSOR_PATHS[shape] || CURSOR_PATHS.arrow).hotspot;
+  const k = size / 17;                            // 视觉尺寸归一
+  return (
+    <div style={{
+      position: 'absolute', left: 0, top: 0, zIndex: 999, pointerEvents: 'none',
+      transform: `translate(${p.x - hs[0] * k}px, ${p.y - hs[1] * k}px) scale(${scale})`,
+      transformOrigin: `${hs[0] * k}px ${hs[1] * k}px`,
+      filter: 'drop-shadow(0 1px 2px rgba(0,0,0,0.35))',
+      ...style,
+    }}>
+      <CursorIcon variant={shape} size={size} />
+    </div>
+  );
+}
+
+/**
+ * ClickRipple — 双圈同心涟漪(放在独立 <Sprite> 里,从点击帧开始)
+ * 双圈起点差 3f;半径 14→54 / 14→78;扩散 out-cubic 22f、消散线性 26f 解耦。
+ * duration = 所在 Sprite 的时长(秒),用于把本地进度换算回秒。
+ */
+function ClickRipple({ x, y, color = '#D97757', r1 = 54, r2 = 78, duration = 0.9, fps = 30 }) {
+  const { useSprite } = window.Animations;
+  const { t } = useSprite();
+  const tSec = t * duration;
+  const rings = [
+    { rMax: r1, st: rippleRingState(tSec, { delayF: 0, r1, fps }) },
+    { rMax: r2, st: rippleRingState(tSec, { delayF: 3, r1: r2, fps }) },
+  ];
+  return (
+    <div style={{ position: 'absolute', left: x, top: y, zIndex: 998, pointerEvents: 'none' }}>
+      {rings.map((r, i) => (
+        <div key={i} style={{
+          position: 'absolute',
+          left: -r.rMax, top: -r.rMax, width: r.rMax * 2, height: r.rMax * 2,
+          borderRadius: '50%',
+          border: `3px solid ${color}`,
+          boxShadow: `0 0 40px ${color}55`,
+          transform: `scale(${r.st.scale})`,      // 固定尺寸 + scale,不 tween 宽高
+          opacity: r.st.opacity,
+        }} />
+      ))}
+    </div>
+  );
+}
+
+/**
+ * HoverHighlight — 光标 hover 目标的联动高亮
+ * intensity 0→1 由调用方从时间推导(配 hoverIndexAt),本组件只负责渲染:
+ * hairline 描边浮现 + 轻微提亮,光标离开即撤。
+ */
+function HoverHighlight({ rect, intensity = 0, color = '#D97757', radius = 8 }) {
+  if (intensity <= 0) return null;
+  return (
+    <div style={{
+      position: 'absolute', left: rect.x - 3, top: rect.y - 3,
+      width: rect.w + 6, height: rect.h + 6,
+      borderRadius: radius, pointerEvents: 'none',
+      border: `1.5px solid ${color}`,
+      boxShadow: `0 0 0 3px ${color}22`,
+      opacity: intensity,
+      backdropFilter: `brightness(${1 + 0.06 * intensity})`,
+    }} />
+  );
+}
+
+/* ══════════════ GSAP 驱动层(HyperFrames 渲染管线)══════════════ */
+
+/**
+ * attachCursorTween — proxy tween 驱动光标 DOM 元素沿 sampler 路径移动
+ * (gsap-recipes §3.5 的组件化封装;一切由 proxy.u 推导,seek-safe)
+ */
+function attachCursorTween(tl, target, sampler, opts) {
+  const o = Object.assign({ duration: 1.1, ease: 'power1.inOut', position: '>' }, opts);
+  const proxy = { u: 0 };
+  tl.to(proxy, {
+    u: 1, duration: o.duration, ease: o.ease,
+    onUpdate: () => {
+      const p = sampler(proxy.u);
+      gsap.set(target, { x: p.x, y: p.y });
+    },
+  }, o.position);
+  return proxy;
+}
+
+/** attachClickTween — 点击 Anticipation:下压 0.85 再 back.out 回弹 */
+function attachClickTween(tl, target, opts) {
+  const o = Object.assign({ position: '>' }, opts);
+  tl.to(target, { scale: 0.85, duration: 0.08, ease: 'power1.in' }, o.position);
+  tl.to(target, { scale: 1, duration: 0.25, ease: 'back.out' }, '>');
+}
+
+/**
+ * attachRippleTween — 双圈 ripple。ring1/ring2 是两个固定尺寸的圆环元素
+ * (直径 = 2×终态半径,初始 scale = r0/r1),只 tween scale 和 opacity。
+ */
+function attachRippleTween(tl, ring1, ring2, opts) {
+  const o = Object.assign({ r0: 14, r1: 54, r2: 78, fps: 30, position: '>' }, opts);
+  const F = (n) => n / o.fps;
+  [[ring1, o.r1, 0], [ring2, o.r2, 3]].forEach(([el, rMax, delayF]) => {
+    const at = delayF === 0 ? o.position : '<+=' + F(delayF);
+    tl.fromTo(el, { scale: o.r0 / rMax, autoAlpha: 1 },
+      { scale: 1, duration: F(22), ease: 'power3.out' }, at);          // 扩散:冲
+    tl.to(el, { autoAlpha: 0, duration: F(26), ease: 'none' }, '<');   // 消散:匀,解耦
+  });
+}
+
+/* ══════════════ 导出 ══════════════ */
+
+if (typeof window !== 'undefined') {
+  window.CursorIcon = CursorIcon;
+  window.CursorSprite = CursorSprite;
+  window.ClickRipple = ClickRipple;
+  window.HoverHighlight = HoverHighlight;
+  window.CursorKit = {
+    mulberry32,
+    CursorEasing,
+    buildCursorSampler,
+    hoverIndexAt,
+    rippleRingState,
+    attachCursorTween,
+    attachClickTween,
+    attachRippleTween,
+    CURSOR_PATHS,
+  };
+}

+ 1 - 1
references/ai-video-review.md

@@ -35,7 +35,7 @@ uv run ~/.claude/skills/huashu-design/scripts/ai-review-video.py \
 
 ## checklist与严重度
 
-①黑帧/渲染残缺 ②文字裁切/错字 ③元素重叠遮挡 ④叙事连贯(过渡三分类:硬切/淡入淡出/morph
+①黑帧/渲染残缺 ②文字裁切/错字 ③元素重叠遮挡 ④叙事连贯(过渡按 camera-language.md §7 三层词汇识别:六式[流白/穿暗场/虚焦接力/黑场字卡/whip-pan/mask-wipe]、hidden-cut、travel[共享元素归位/字腔穿越];裸切=未包装的硬切,记⚡
 ⑤hero贯穿性 ⑥节奏死段(客观清单+模型判断刻意hold还是真死段)⑦音效打点(onset+画面事件核对)
 ⑧构图失衡/空白
 

+ 27 - 0
references/animation-pitfalls.md

@@ -405,6 +405,30 @@ GSAP 的 `fromTo()` 默认 `immediateRender: true`:build timeline 时就把 fr
 
 **修法**:所有 from 态可见的 `fromTo()` 显式加 `immediateRender: false`;或改成「set 初始隐藏 + to」。自查方式:渲染后抽每幕开头帧,看有没有「不该在场的特效元素」。
 
+## 22. 【镜头】3D/放大模式下文字发糊 —— 放大走 CSS zoom 不走 transform scale
+
+**症状**:用 `transform: scale()` 推近页面(尤其 3D perspective 模式下),文字发糊,倍率越高越糊,2x 以上不可交付。
+
+**根因**:Chromium 按元素的**布局尺寸**栅格化,再把位图放大。scale 只放大位图。
+
+**解法**(shotcraft 判例,全库最贵知识):相机层的放大走 **CSS `zoom` 属性**(布局级缩放,按放大后尺寸重新 layout 并栅格化,文字任意倍率锐利)。坐标换算和完整公式见 `camera-language.md` §3.4、`gsap-recipes.md` §9.2。注意:`zoom` 每帧触发 re-layout,是「禁 tween 布局属性」的唯一合法例外,只许用在 `#world` 相机层;离线逐帧渲染下渲染时长变慢属正常,产物质量优先。配套:全页截图 2x 起,特写另备 4x 切片在推进期 6f 交叉淡入。
+
+## 23. 【镜头】perspective 被中间层打断 —— 3D 瞬间变平
+
+**症状**:设好了 `perspective` + `preserve-3d`,渲出来完全没有 3D 感,所有层平贴。
+
+**根因**:`#camera` 与 3D 子元素之间的**任何中间层**加了 `overflow: hidden`、`filter`、`opacity < 1`、`clip-path` 之一,都会创建新 stacking context,flatten 掉 preserve-3d。
+
+**解法**:3D 模式下滤镜/透明度效果只加在**最内层元素**上;容器链上逐层检查上述四类属性。排查口诀:从 `#camera` 到出问题的元素,中间每一层都 `getComputedStyle` 查一遍这四项。
+
+## 24. 【镜头】pan 露边 —— 平移时露出画布外空白
+
+**症状**:镜头平移/摇镜时画面边缘露出白边或黑边。
+
+**根因**:`#world` 尺寸只做到和视口一样大,镜头一动就出界。
+
+**解法**:`#world` 四周外扩 bleed ≥ 最大 pan 振幅 + 8% 安全边距(camera-language §3.3)。背景层/氛围层要跟着铺满 bleed 区,别只铺视口。自查:把 timeline seek 到每段 pan 的两个端点截图,看四边。
+
 ## 快速自查清单(开工前 5 秒)
 
 - [ ] 每个 `position: absolute` 的父元素都有 `position: relative`?
@@ -428,3 +452,6 @@ GSAP 的 `fromTo()` 默认 `immediateRender: true`:build timeline 时就把 fr
 - [ ] 【HyperFrames】代理 tween 场景注册后补了 `render(0)`?(坑 #19)
 - [ ] 【HyperFrames】check 过了?暗色电影风用 `--no-contrast`,其余四门 0 error?(坑 #20)
 - [ ] 【HyperFrames/GSAP】from 态可见的 `fromTo()` 全部加了 `immediateRender:false`?(坑 #21,B00 实测 4 处幻影)
+- [ ] 【镜头】3D/放大特写走了 CSS `zoom`,没有 scale 放大发糊?(坑 #22)
+- [ ] 【镜头】`#camera` 到 3D 元素的中间层没有 overflow/filter/opacity/clip-path?(坑 #23)
+- [ ] 【镜头】`#world` 外扩了 bleed,pan 端点截图四边无露白?(坑 #24)

+ 8 - 17
references/animations.md

@@ -94,15 +94,12 @@ function SlideIn({ children, from = 'left' }) {
 }
 ```
 
-### 3. 逐字打字机
+### 3. 打字效果(⚠️ 先分清两种场景,别用逐字蹦)
 
-```jsx
-function Typewriter({ text }) {
-  const { t } = useSprite();
-  const charCount = Math.floor(text.length * Math.min(t * 2, 1));
-  return <span>{text.slice(0, charCount)}</span>;
-}
-```
+匀速逐字 Typewriter 是官方反例(best-practices「AI slop」清单:像老电影字幕)。按内容选正解:
+
+- **AI 输出**(token 流式涌现)→ Chunk Reveal:不规律块状涌现,见 `animation-best-practices.md` §4.5 / `gsap-recipes.md` §3.4
+- **用户输入**(真人在输入框打字)→ 3f/字符 + 光标常亮转闪烁 + 偶发退格,见 `ui-demo-animation.md` 八式③
 
 ### 4. 数字计数
 
@@ -235,15 +232,9 @@ function Scene() {
 这个skill做的是**HTML动画**(在浏览器里跑的)。如果最终产出要作为视频素材:
 
 - **短动画/concept demo**:用这里的方法做HTML动画 → 屏幕录制
-- **长视频/叙事**:本 skill 专注 HTML 动画,长视频用 AI 视频生成类 skill 或专业视频软件
+- **长视频/叙事**(5-20 分钟带解说):走 SKILL.md Step 9.5 解说驱动管线(`voiceover-pipeline.md`),不外推给其他工具
 - **motion graphics**:专业的After Effects/Motion Canvas更合适
 
-## 关于Popmotion等库
-
-如果你真的需要物理动画(spring、decay、keyframes with precise timing),我们的engine搞不定,可以fallback到Popmotion:
-
-```html
-<script src="https://unpkg.com/popmotion@11.0.5/dist/popmotion.min.js"></script>
-```
+## 需要物理动画(spring / decay)时
 
-但**先试试我们的engine**。90%的情况够用
+不要引 Popmotion(CDN 在受限网络必挂,违反自包含原则,见 `animation-pitfalls.md` #17)。spring 需求走 GSAP:`elastic.out` / `back.out` 及自定义 springEase 映射见 `gsap-recipes.md` §1.2;落地余震用 dampedSettle 闭式解(`camera-language.md` §9)。

+ 1 - 1
references/apple-gallery-showcase.md

@@ -227,7 +227,7 @@ const T = {
   s4_walloff: [21.1, 21.8], s4_in: [21.8, 22.7], s4_hold: [23.7, 25.0],
 };
 
-// 核心 easing
+// 核心 easing(v9 历史实现用 cubic;新项目主 easing 默认 expoOut,见 best-practices §2 / hero-case-study 模式1 的修正)
 const easeOut = t => 1 - Math.pow(1 - t, 3);
 const easeInOut = t => t < 0.5 ? 4*t*t*t : 1 - Math.pow(-2*t+2, 3)/2;
 function lerp(time, start, end, fromV, toV, easing) {

+ 366 - 0
references/camera-language.md

@@ -0,0 +1,366 @@
+# Camera Language · 运镜导演体系
+
+> **何时读本文件**:画面里出现任何「镜头级」运动之前——zoom / pan / orbit / parallax /
+> 转场 / 定场谢幕,只要动的是「镜头」而不是「元素」,先读这里再写 timeline。
+> 元素怎么动(入场/stagger/物理感)归 `animation-best-practices.md`;
+> 本文件回答的是**镜头什么时候动、动多大、动多久、镜头之间怎么接**。
+> GSAP 侧的可运行实现(rig 容器、PageCam 翻译、对数时长 helper)见
+> `gsap-recipes.md` 的「Camera Rig 配方」节,本文只给设计判断和公式。
+>
+> 参数出处标注约定:**(HuaRec)** = 花录 Studio 运镜导演系统实测参数;
+> **(shotcraft)** = video-shotcraft 106 卡镜头体系;**(实测)** = 本 skill 项目实战;
+> **(推测)** = 通用电影语汇借鉴,参数待实测校准。
+
+---
+
+## §0 · 立论 · 运镜是预算制,不是特效制
+
+大多数 AI 生成动画的运镜是「特效制」思维:哪里能加 zoom 就加 zoom,镜头动得越多越「高级」。
+这是晕和廉价感的共同来源。正确的心智模型来自两条公理 (HuaRec):
+
+- **A1 可见性不变量**:任意时刻,观众该看的东西必须在可视区内(含 8% 安全边距)。
+  违反的镜头宁可降倍率、并镜或不拍。
+- **A2 舒适预算**:每次镜头变化都是一笔注意力消费,必须预算化管理。
+  预算花完了,再好的镜头也不拍。
+
+| 预算项 | 典型值 | 调节手感 |
+|---|---|---|
+| 相邻镜头变化间隔 | ≥2.6-3.0s(克制档 5.0s) | 低于 2.6s 观众开始晕;MTV 式快剪不适用于产品演示 (HuaRec) |
+| 任意 15s 窗口内镜头变化 | ≤4-5 次(克制档 3 次) | 超了就砍最弱动机的那一镜,不是压缩间隔 (HuaRec) |
+| 推进倍率下限 | 1.25x | 低于 1.25x 的 zoom 视觉变化感知不足,纯属晃动,不拍(定场 1.06x 是唯一例外)(HuaRec) |
+| 推进倍率上限 | 2.3x(克制档 1.8x) | 再高像素密度撑不住,先换素材再谈倍率 (HuaRec) |
+| 每分钟镜头数 | 克制 ≤4 镜/分,常规 ≤6 镜/分 | 「留呼吸,不是 MTV」(HuaRec) |
+
+再叠两条风格公理 (shotcraft):
+
+1. **电影感 = 运镜 × 光影 × 节奏 × 声音,不等于炫技动画**。四个维度各自及格,胜过一个维度拉满。
+2. **节奏偏好单向:宁慢勿快**。历史用户反馈全部指向「放慢/停留」,没有一条指向「加快」。
+   拿不准时长时,选长的那个;拿不准要不要动镜时,选不动。
+
+这两条与 best-practices §0.2 的「礼让观众」同源:镜头是替观众的眼睛做决定,
+决定做得越少、越准,观众越信任你。
+
+---
+
+## §1 · 镜头语言词汇表 · 运镜动机决策表
+
+每个镜头动作先问动机:**这一镜替观众回答了什么问题?** 答不上来就不动。
+
+| 镜头 | 动机(什么时候给) | 参数区间 | 禁忌 |
+|---|---|---|---|
+| **push in 推近** | 「接下来看这里」:聚焦一个具体 UI 元素 / 数据 / 关键词;紧张度爬升 | 倍率 1.3 / 1.45 / 1.8 / 2.3 档(见 §4);时长走对数公式;提前 0.15s 进镜 (HuaRec) | <1.25x 不推;滚动 / 切页 / 播视频等全屏级变化期间不推(「滚动时推近会晕」);旁白中段不动镜(见 §6 move on pause)(HuaRec) |
+| **pull out 拉远** | 揭示全貌与上下文:「原来它属于一个更大的系统」;收尾谢幕 | 时长同对数公式;谢幕拉出 0.55s + ≥0.8s 全景停顿 (HuaRec) | 禁「出-进-出」泵动:镜间空隙小于过渡时长时直接接下一镜,不回 1x(见 §5)(HuaRec) |
+| **pan 平移** | 两个中距焦点之间转移(归一化距离 0.22-0.45);一镜扫过多个并列元素 | 联合倍率 ≥1.25 才值得平移;斜向 pan 用双频正弦(X/Y 频率比 0.22:0.35,振幅 30-40px)(HuaRec / 本 skill 既有) | 焦点距离 >0.45 不平移(对角横跳),弃镜;纯单轴 pan 有机械感,优先斜向 |
+| **orbit 环绕** | 单主角质感特写,「实体感」最强的一镜;hero 元素立传 | rotY 主导 + persp 1100-1200px;实测机位 rotX46/rotY−30/rotZ9 → rotX42/rotY26/rotZ−7 (shotcraft) | 一种手法全片只当一次主角 (shotcraft);信息密集画面禁用(机位服务可读性,文字多就正视) |
+| **dolly zoom** | 「世界观反转」的揭示瞬间:主体不变、语境剧变 | 伪配方见 §8:主体钉死,背景 scale 1→2.0-2.5 + opacity ≤0.6 (shotcraft) | 全片最多一次;无叙事落差时用它 = 纯炫技 |
+| **静止** | 文字阅读、真人速度交互演示、信息密集镜头;预算不足时的默认答案 | 品牌字标落定 hold ≥1s;批量动效收尾 0.5s 静止;开场主体动作弧 ≥3s (shotcraft) | 无。静止不动永远是合法选择,「不给镜头」本身就是导演决定 |
+
+两条横向规则:
+
+- **速度感来自加速度,不是匀速快** (shotcraft)。匀速运动读作廉价 PPT;
+  想要「快」的感觉,用短促的加速段 + 长缓冲,不是把整段 duration 砍半。
+- **机位服务可读性** (shotcraft):信息密集镜头正视;文字特写用侧向水平机位
+  (rotY 主导、rotX 很小);禁全局一刀切倾斜;产品宣传片默认不加手持抖动。
+
+---
+
+## §2 · zoom vs dolly 选型 · 3D 真假裁决
+
+「推近」有两种实现,观感完全不同,先选型再写代码:
+
+| 维度 | zoom(scale 缩放) | dolly(perspective + translateZ 前移) |
+|---|---|---|
+| 视差 | 无。所有层等比放大,画面是「一张图被放大」 | 有。近层快、远层慢,画面是「镜头在空间里前进」 |
+| 观感 | 干净、信息型,适合 UI 特写 / 数据聚焦 | 空间感、电影感,适合 hero 展示 / 氛围段落 |
+| 成本 | 低:单个 transform | 高:需要分层结构 + preserve-3d,且有栅格化发糊问题(§3.4) |
+| 选型规则 | 内容是平面信息(界面、文档、图表)→ zoom | 内容有明确的「前景/主体/背景」层次,且这层次值得被看见 → dolly |
+
+**3D 真假裁决**(化解本 skill 两处旧文矛盾的边界线):
+
+- 参与 3D 的元素 **≤8 个** → 用真 translateZ 分层(best-practices §4.7 的黄金角配方照用)
+- 元素 **≥20 个** → 放弃真 3D,用 shadow / blur / 明度差做假深度(hero-case-study 的立场)
+- 8-20 之间 → 问一个问题:这段镜头需要视差吗?需要才上真 3D,不需要就假深度。
+  真 3D 的成本不在写,在调:每加一层就多一组「透视失真 + 文字发糊 + 层级穿插」要排查。
+
+---
+
+## §3 · Camera Rig 实现约定
+
+镜头运动和元素动画**不许抢同一个 transform**。所有镜头级运动收口到一个专职容器上。
+
+### 3.1 分层容器结构
+
+```html
+<div id="viewport">          <!-- 固定视口,overflow: hidden,持有 perspective -->
+  <div id="camera">          <!-- 镜头层:只承载相机 transform,别的什么都不干 -->
+    <div id="world">         <!-- 世界层:所有画面内容住这里,元素动画只动 world 内部 -->
+      ...场景内容...
+    </div>
+  </div>
+  <div id="hud">             <!-- 字幕 / 角标 / chrome:与 #camera 平级,天然不跟镜头动 -->
+  </div>
+</div>
+```
+
+分工铁律:
+
+- `#camera` 上只出现镜头 tween(translate / scale / rotate / zoom 属性),元素入场、stagger、
+  hover 态一律写在 `#world` 内部的元素上。两层互不知晓,镜头随时可以整体重排而不碰元素动画
+- 字幕和 chrome **首选放 `#hud`**,零成本保持静止;只有「必须跟着 world 里某元素走、
+  但字号要恒定」的标注(如跟随 tooltip),才在该元素上做 counter-transform:
+  `scale(1/zoom)` 反向抵消镜头缩放,每帧与相机同步更新
+- **transform-origin 就是推进目标点**:平面 zoom 把 origin 设到目标元素中心再 scale,
+  等价于「镜头对准它推近」。PageCam 模式下由 cx/cy 承担同一职责
+
+### 3.2 PageCam 关键帧模型(shotcraft,2.5D 相机数学)
+
+把镜头状态定义成关键帧对象,镜头运动 = 关键帧之间插值:
+
+```
+{ frame, cx, cy, zoom, rotX, rotY, rotZ, persp }
+```
+
+cx/cy 是**世界坐标系里镜头对准的点**,zoom 是倍率。以 1920×1080 画布为例
+(其他尺寸把 960/540 换成 W/2、H/2):
+
+**平面模式**(无旋转,纯 zoom + pan):
+
+```
+transform: translate(960 − cx·zoom, 540 − cy·zoom) scale(zoom)
+transform-origin: 0 0
+```
+
+**3D 模式**(有 rotX/rotY/rotZ):
+
+```
+外层(#camera): perspective: persp·zoom;  perspective-origin: 960px 540px
+内层(#world):  zoom: {zoom};                        /* 注意是 CSS zoom 属性,见 §3.4 */
+                Tx = 960/zoom − cx;  Ty = 540/zoom − cy
+                transform: translate(Tx, Ty) rotateY() rotateX() rotateZ()
+                transform-origin: cx cy
+                transform-style: preserve-3d
+```
+
+典型机位参数 (shotcraft 实测):全页 zoom 0.78 → 特写 2.6;
+侧拍 rotY34 / rotX8 / persp1200(侧拍优于俯拍,rotY 主导 + rotX 只给一点);
+orbit 起止机位见 §1 表。
+
+### 3.3 rig 施工注意(镜头专属坑)
+
+- **pan 露边**:`#world` 必须比视口大(四周外扩 bleed ≥ 最大 pan 振幅 + 8% 边距),
+  否则平移时露出画布外的空白。这是 A1 可见性公理的反面:不该看的也不能看见
+- **perspective 被打断**:`#camera` 与 `#world` 之间的任何中间层加了
+  `overflow: hidden`、`filter`、`opacity <1` 都会创建新 stacking context,
+  杀掉 preserve-3d,3D 分层瞬间变平。3D 模式下滤镜效果只加在最内层元素上
+- **逐帧越界兜底** (HuaRec):镜头焦点跟随动点时,每帧检查目标是否越出可视区,
+  越出则保持倍率、只沿越界轴做最小修正拉回边界。宁可镜头「让一步」,不许目标出画
+
+### 3.4 CSS zoom 栅格化技法 · 根治 3D 文字发糊(全库最贵知识,shotcraft)
+
+**问题**:3D 模式下用 `transform: scale()` 放大页面,Chromium 按元素的布局尺寸栅格化,
+再把位图放大,文字必糊。倍率越高糊得越狠,2x 以上不可交付。
+
+**解法**:放大不走 `transform: scale`,走 **CSS `zoom` 属性**。`zoom` 是布局级缩放,
+Chromium 按放大后的尺寸重新 layout 并栅格化,文字在任意倍率下保持矢量级锐利。
+§3.2 的 3D 模式公式里内层写 `zoom: {zoom}` 而不是 `scale({zoom})`,正是为此。
+
+配套要点:
+
+| 要点 | 做法 | 出处 |
+|---|---|---|
+| 坐标补偿 | `zoom` 改变布局坐标系,translate 量要除以 zoom:`Tx = 960/zoom − cx`(§3.2 公式已含) | (shotcraft) |
+| 与 reflow 禁令的关系 | gsap-recipes §6.2 禁 tween 布局属性是因为整数 snap 抖动;`zoom` 是整页级缩放,snap 量不可感,且文字锐利收益远大于。**此技法是 §6.2 的唯一合法例外,只用于 `#world` 相机层** | (实测) |
+| 渲染环境 | HyperFrames / Playwright 离线逐帧 seek 渲染下完全适用:每帧重 layout 的耗时不影响产物,只影响渲染时长。实时浏览器 preview 可能掉帧,属正常,以渲染产物为准 | (shotcraft) |
+| 位图素材增强 | 全页截图用 2x 采样;特写元素另备 4x 单独截图,在推进期用 6f 交叉淡入盖住低倍纹理 | (shotcraft) |
+| 景深氛围 | DoF 只做氛围:顶部渐变带 blur + mask,不做逐层真实景深 | (shotcraft) |
+
+---
+
+## §4 · 镜头缓动与时长
+
+### 4.1 缓动词汇
+
+| 场景 | easing | 调节手感 |
+|---|---|---|
+| 主动运镜(推近/拉远,有明确起止点) | `cubic-bezier(0.65,0,0.35,1)` = GSAP `power3.inOut` | 两端都稳,「导演给镜头」的感觉;**绝不线性、绝不弹簧过冲** (HuaRec) |
+| 跟随式运镜(镜头追一个已开始的动作) | `cubic-bezier(0.33,0,0.15,1)` | 出发轻快、刹车极长,镜头像「跟上去」而不是「切过去」;shotcraft 相机默认 |
+| 持续漂移(idle drift、匀速巡览) | `sine.inOut` yoyo 或 `none` | 唯一允许相机匀速的场景(gsap-recipes §1 既有规则);有起止点的动作禁用 |
+| 光标/焦点跟随平滑 | `quickTo` + ~0.15s 平滑;路径插值 Catmull-Rom | 前向+后向 EMA 的零相位思路:跟得紧但不抖 (HuaRec) |
+
+两套默认冲突时的裁决:单次推拉信 HuaRec(power3.inOut),复合移动、
+多段连续镜头信 shotcraft(0.33,0,0.15,1)。
+
+### 4.2 zoom 时长对数公式(固定 duration 是业余感的来源)
+
+所有推拉时长由倍率变化量决定,保证任何幅度的 zoom「视觉速度」一致:
+
+```
+duration = 0.55 × |ln(zoom₂ / zoom₁)| / ln 2      clamp 到 [0.30, 0.94] 秒
+```
+
+1→2x 推近正好 0.55s;1→1.3x 约 0.30s(触底);0.78→2.6x 触顶 0.94s。(HuaRec)
+大景别过渡更久、小景别更短,防「一蹿到位」也防「拖沓」。
+
+### 4.3 zoom 档位表
+
+| 档位 | 倍率 | 用途 | 调节手感 |
+|---|---|---|---|
+| 定场微推 | 1.06x | 仅用于开场定场(§6),观众感知不到 zoom、只感知到「画面活着」 | 唯一允许低于 1.25x 的档位 (HuaRec) |
+| 轻推 | 1.3x | 提示性聚焦:不打断全局阅读,只是「注意这一片」 | |
+| 中推 | 1.45x | 标准 UI 特写:一个面板 / 一段代码 | |
+| 重推 | 1.8x | 单元素特写:一个按钮 / 一个数字 | 克制档的上限 (HuaRec) |
+| 上限 | 2.3x | 极限特写,素材必须扛得住(2x 截图 / 4x 切片,§3.4) | 超过就换素材,不硬推 (HuaRec) |
+
+定景公式 (HuaRec):`scale = 0.8 / max(目标包围盒归一化宽, 高)`,再夹到档位区间。
+内容占可视区 80%,留 20% 呼吸,不顶格。
+
+### 4.4 节奏预算(与 §0 预算表联动)
+
+- 相邻镜头变化间隔 ≥2.6-3.0s;15s 窗口 ≤4-5 次 (HuaRec)
+- 动作前 0.15s 进镜(镜头先到,动作后发生),动作结束后停留 1.2s 再走 (HuaRec)
+- 时长 <1.2s 的孤立小动作不值得单独给镜头,防「点一下泵一下」(HuaRec)
+
+---
+
+## §5 · 镜间语法 · 防晕核心
+
+**晕不是单个镜头造成的,是镜头之间的接法造成的。** 「出-进-出」泵动和远焦点连续横跳
+贡献了绝大多数眩晕感 (HuaRec)。对相邻两镜(间隔 <1.5s),按焦点距离三分:
+
+| 焦点归一化距离 | 接法 | 说明 |
+|---|---|---|
+| <0.22(近) | **并镜** | 合并为一镜:取两目标的联合包围盒重算倍率,一镜看完 |
+| 0.22-0.45(中) | **改平移** | 宁可倍率广一点(联合倍率 ≥1.25),一镜平移过去,不做「出再进」 |
+| >0.45(远/对角) | **弃镜** | 砍掉动机更弱的那一镜。**绝不连拍两个远焦点**,对角横跳是最晕的一种接法 |
+
+补两条时序规则 (HuaRec):
+
+- **间隙短则直接对接**:相邻镜头空隙小于过渡时长时,不回 1.0x,直接从当前倍率过渡到
+  下一镜的倍率和焦点。回 1x 再推是「泵动感」的直接来源
+- **间隙 ≥1.5s 才允许「拉出再推进」**:观众有足够时间在全景里重新定位,出-进才不晕
+
+---
+
+## §6 · 电影开闭幕 · 定场、谢幕、move on pause
+
+三条语法成本极低,作品感提升明显 (HuaRec):
+
+1. **定场(establishing shot)**:片长 >14s 且首个正式镜头在 7s 之后时,
+   开场插入 [0, 3.0s] 的 **1.06x 中心微推**:开机即处于轻推近态,3 秒内缓出落回全景。
+   观众的第一感受是「镜头是活的」,而不是「PPT 开始播放了」
+2. **全景谢幕铁律**:末镜提前收口,留出 0.55s 拉出过渡 + **≥0.8s 全景停顿**。
+   成片永远以全景静止收尾,**绝不在推近态戛然而止**。与 best-practices「戛然而止 + hold」
+   收尾完全兼容:hold 的那一帧必须是全景
+3. **Move on pause**:cut on action, move on pause。有旁白的动画里,镜头移动如果撞在
+   说话中段,向早处吸附到最近的语音静默点(最多前移 0.8s,**只提前不推后**)。
+   观众在听觉空档移动视线的认知成本最低。解说 pipeline(voiceover-pipeline.md)排镜头时
+   直接拿 narration 的分句间隙当吸附点
+
+---
+
+## §7 · 转场语法表 · 三层词汇
+
+转场是独立层:接缝按**能量落差**选型,一个接缝只用一式,转场帧从相邻镜头预算里划走。
+「公认优秀的发布片全程没有一次裸切」(shotcraft)。
+
+### 7.1 shot-transitions 六式(有存在感的转场,用于能量落差大的接缝)
+
+| 式 | 适用能量落差 | 参数 | 坑 |
+|---|---|---|---|
+| 流白 flash-wash | 高→高,段落强转 | 白场 2-4f 峰值,两侧各 5f 渐变 | 白闪只盖切点,不当装饰反复用 |
+| 穿暗场 dip-to-dark | 高→低,情绪降档 | 压暗到 rgba(20,20,20,0.9) 量级,总长 ≤0.6s | 暗场里别停留,观众以为片子结束 |
+| 虚焦接力 defocus-handoff | 中→中,平级话题切换 | 出镜 blur 0→8px 与入镜 blur 8px→0 交叠 ≥8f | blur 大面积 ≤24px(DoF 性能约束) |
+| 黑场字卡 title-card | 章节级分隔 | 字卡 hold ≥1s,前后各 0.3s 过渡 | 全片 ≤2 张,多了像幻灯片 |
+| whip-pan 甩镜 | 低→高,能量急升 | 两端 hold ≥20f → 8f 甩 1.5 屏,峰值 ≥300px/f 才糊得透 (shotcraft) | 慢了就是普通 pan,糊不透反而露拙 |
+| mask-wipe 穿窗 | 空间转移,「穿过一个界面进入另一个」 | 遮罩缘 easing `(0.4,0,0.6,1)` (shotcraft) | 遮罩形状必须来自画面内已有元素(窗口/卡片圆角),凭空的形状是 slop |
+
+### 7.2 hidden-cut 三式(观众察觉不到切过的转场)
+
+| 式 | 做法 | 出处 |
+|---|---|---|
+| flash-cut 白闪跨切 | 白闪跨骑硬切点,两侧各 5f,只盖切点 | (shotcraft,实证参数) |
+| 前景遮挡切 | 一个前景元素(卡片/面板/光标手)扫过全屏的瞬间换景 | (推测:通用电影语汇,参数待实测) |
+| 运动糊切 | 高速运动峰值帧上硬切,两侧运动方向一致,motion blur 吞掉切点 | (推测:通用电影语汇,参数待实测) |
+
+### 7.3 travel 两式(空间连续的转场,能量不落差、场景在移动)
+
+| 式 | 做法 | 坑 |
+|---|---|---|
+| 共享元素归位 | 前镜的某元素(logo/卡片)连续运动到后镜中它的新位置,FLIP 思路的镜头版 | 元素在两镜中必须同一身份,形变过大就断了「同一个东西」的认知 |
+| 字腔穿越 | 镜头推进穿过大字的字腔(O/口/0 的空洞)进入下一场景 | 字腔尺寸要够(≥1/3 屏高),穿越段 zoom 走对数时长上限 0.94s |
+
+选型速查:能量落差大 → 六式挑一;不想被察觉 → hidden-cut;两个场景空间上连续 → travel。
+硬切不是禁用,是「必须被上述任意一层包装过」。
+
+---
+
+## §8 · 多层 parallax 配方 · 伪 dolly-zoom
+
+### 8.1 parallax 层速度系数 (shotcraft)
+
+| 参数 | 典型值 | 调节手感 |
+|---|---|---|
+| 层速度系数 | 远景 0.35 / 中景 0.7 / 近景 1.4(相机位移的倍数) | 相邻层速比 **≥2 倍**才可辨,1.2 倍的差异观众读不出来 |
+| 层数 | ≤4 层 | 超过 4 层的视差没人看得出来,纯浪费预算 |
+| 实现 | 各层根据同一个相机 x/y 乘各自系数 translate | 全部由相机状态推导,seek-safe;别给每层独立 tween |
+
+### 8.2 伪 dolly-zoom(纯 CSS,无需真 3D)
+
+```
+主体:钉死不动(或只做 ≤1.02x 的呼吸)
+背景:scale 1 → 2.0-2.5,同时 opacity 降到 ≤0.6
+```
+
+主体不变、背景涌向观众,产生「世界在逼近而主角凝固」的反转感 (shotcraft)。
+时长给足(≥1.2s),用途见 §1:全片最多一次,留给真正的揭示瞬间。
+
+---
+
+## §9 · 运动派生信号 · blur / 跟随 / 余震
+
+镜头的「速度感」不靠更快的 duration,靠从速度**派生**出来的次级信号 (shotcraft / HuaRec):
+
+| 信号 | 公式 | 参数 |
+|---|---|---|
+| 速度驱动 blur | `v = velocityAt(f)`(中央差分:`(pos(f+1)−pos(f−1))/2`),blur 强度 ∝ v | zoom 速度 >0.6/s 才触发,强度 `min(10, v×5)`px。**只在动的瞬间有模糊,静止帧永远锐利** (HuaRec) |
+| lagged 跟随层 | 跟随层 = 主体在 `f − delay` 处采样 | 阴影滞后 2f、残影滞后 4f (shotcraft)。残影平替 motion blur:5% 路径滞后 + blur(6px) + opacity 0.25·(1−t) |
+| dampedSettle 余震 | `e^(−d·t) · sin(2π·f·t)`,f≈0.1、damping≈0.15 | 镜头急停后的 1-2 个微小残摆,幅度 ≤3px;主动运镜(§4.1 inOut 族)不加,只给 whip-pan / 急刹类 |
+
+三者全部是时间的纯函数(差分、延迟采样、闭式衰减),天然 seek-safe,
+符合 gsap-recipes §6 的确定性要求。
+
+---
+
+## §10 · 运镜自检清单(写完 timeline 后 60 秒)
+
+- [ ] 每个镜头动作都答得出「替观众回答了什么问题」?答不出的删了吗?
+- [ ] 没有 <1.25x 的 zoom(定场 1.06x 除外)?
+- [ ] 相邻镜头间隔 ≥2.6s,15s 窗口 ≤4-5 次?
+- [ ] 所有推拉时长走对数公式,没有拍脑袋的固定 duration?
+- [ ] 推拉 easing 是 power3.inOut,没有 linear / 弹簧过冲?
+- [ ] 相邻镜头按焦点距离做过三分裁决(并镜/平移/弃镜)?没有两连远焦点横跳?
+- [ ] 间隙短的镜头直接对接,没有回 1x 的泵动?
+- [ ] 片长 >14s 且首镜晚于 7s:加了 1.06x 定场微推?
+- [ ] 结尾是全景静止 ≥0.8s,不在推近态收尾?
+- [ ] 有旁白:镜头移动吸附到了语音间隙(只提前 ≤0.8s)?
+- [ ] 镜头运动全部收口在 `#camera` 层,没和元素动画抢 transform?
+- [ ] 3D 文字特写走了 CSS zoom 栅格化,没有 scale 放大发糊?
+- [ ] 每个转场接缝只用一式,全片没有裸切?
+- [ ] parallax 相邻层速比 ≥2 倍、≤4 层?
+- [ ] blur 只出现在运动瞬态,静止帧全部锐利?
+
+---
+
+## §11 · 与其他 reference 的关系
+
+| reference | 分工 | 边界 |
+|---|---|---|
+| `animation-best-practices.md` | 元素怎么动、叙事节奏、品味标准 | 它管「演员」,本文件管「摄影机」;S4 爆发段的「镜头拉远」按本文件 §4 定参数 |
+| `gsap-recipes.md` | 本文件所有规则的 GSAP 可运行实现 | 「Camera Rig 配方」节:rig 容器、PageCam 翻译、对数时长 helper、counter-transform |
+| `animation-pitfalls.md` | 踩坑清单 | 镜头专属坑(scale 发糊 / perspective 被打断 / pan 露边)§3.3-3.4 已覆盖设计侧,pitfalls 收技术侧复现 |
+| `hyperframes-backend.md` | 渲染后端契约 | CSS zoom 技法在离线逐帧渲染下的适用性见 §3.4 |
+| `voiceover-pipeline.md` | 解说驱动长视频 | move on pause(§6.3)的静默点数据从 narration 分句间隙来 |
+| `ai-video-review.md` | 成片评审 | 评审 checklist 的转场分类按 §7 三层词汇扩展 |
+
+**调用顺序**:导演稿 / 分镜阶段读 §0-§2 定预算和词汇 → 写 timeline 前读 §3-§7 定实现约定
+与接缝 → 交付前过 §10 清单。

+ 148 - 0
references/gsap-recipes.md

@@ -651,3 +651,151 @@ proxy 大黑盒里的动画对审计工具是不透明的。
 - [ ] blur / filter 全部走 CSS 变量,动过 blur 的元素有 `will-change: filter`?
 - [ ] sub-composition 里入场全用 `fromTo` 不用 `from`?
 - [ ] `npx hyperframes check` 通过(暗色片 `--no-contrast`,其余 0 error)?
+
+---
+
+## 9 · Camera Rig 配方 · 镜头运动的实现层
+
+为什么:镜头运动和元素动画抢同一个 transform 是运镜混乱的技术根源
+(camera-language.md §3)。所有镜头级 tween 收口到专职 rig 容器,
+相机状态用一个 proxy 对象承载,每帧由它推导出全部相机 DOM 状态,seek-safe。
+
+### 9.1 rig 容器结构(静态骨架)
+
+```html
+<div id="viewport">                <!-- 固定视口 -->
+  <div id="camera">                <!-- 镜头层:只有相机 transform -->
+    <div id="world">...</div>      <!-- 世界层:元素动画只发生在这里面 -->
+  </div>
+  <div id="hud">...</div>          <!-- 字幕/角标:#camera 的兄弟,天然静止 -->
+</div>
+```
+
+```css
+#viewport { position: relative; width: 1920px; height: 1080px; overflow: hidden; }
+#camera   { position: absolute; inset: 0; perspective-origin: 960px 540px; }
+#world    { position: absolute; transform-origin: 0 0; will-change: transform; }
+/* pan 露边保险:#world 尺寸 ≥ 视口 + 最大 pan 振幅 + 8% 边距(camera-language §3.3) */
+```
+
+### 9.2 相机 proxy + PageCam 关键帧翻译
+
+相机是一个普通对象,GSAP tween 它的字段,`onUpdate` 里把状态写进 DOM。
+一切由 cam 推导,回拖也正确(同 §3.4 chunk reveal 的 proxy 思路):
+
+```js
+const cam = { cx: 960, cy: 540, zoom: 1, rotX: 0, rotY: 0, rotZ: 0, persp: 1200 };
+const camEl = document.querySelector("#camera");
+const world = document.querySelector("#world");
+
+// ── 平面模式(纯 zoom + pan,无旋转)──────────────────────────
+function applyCam() {
+  world.style.transform =
+    `translate(${960 - cam.cx * cam.zoom}px, ${540 - cam.cy * cam.zoom}px) scale(${cam.zoom})`;
+  applyCounter();
+}
+
+// ── 3D 模式(有 rotX/rotY/rotZ)· 放大走 CSS zoom 属性,不走 scale ──
+// 布局级缩放让 Chromium 按放大后尺寸栅格化,根治 3D 下文字发糊
+// (camera-language §3.4,全库最贵知识)。zoom 改变坐标系,translate 要除以 zoom。
+function applyCam3d() {
+  camEl.style.perspective = `${cam.persp * cam.zoom}px`;
+  world.style.zoom = cam.zoom;
+  world.style.transformOrigin = `${cam.cx}px ${cam.cy}px`;
+  world.style.transform =
+    `translate(${960 / cam.zoom - cam.cx}px, ${540 / cam.zoom - cam.cy}px)` +
+    ` rotateY(${cam.rotY}deg) rotateX(${cam.rotX}deg) rotateZ(${cam.rotZ}deg)`;
+  applyCounter();
+}
+```
+
+注意:CSS `zoom` 每帧触发 re-layout,是 §6.2 reflow 禁令的**唯一合法例外**,
+只允许用在 `#world` 相机层。HyperFrames / Playwright 离线逐帧渲染下单帧耗时不影响产物;
+实时 preview 掉帧属正常,以渲染产物为准。
+
+### 9.3 对数时长 helper(固定 duration 是业余感的来源)
+
+```js
+// camera-language §4.2:1→2x 正好 0.55s,任何幅度的 zoom 视觉速度一致
+function zoomDur(z1, z2) {
+  return gsap.utils.clamp(0.30, 0.94,
+    0.55 * Math.abs(Math.log(z2 / z1)) / Math.LN2);
+}
+```
+
+### 9.4 镜头段落写法(推近 → hold → 平移 → 谢幕拉出)
+
+镜头 tween 全部驱动 cam,easing 按 camera-language §4.1:
+主动推拉 `power3.inOut`,跟随式 `cubic-bezier(0.33,0,0.15,1)`(自定义 ease 见下)。
+
+```js
+const followEase = gsap.parseEase("0.33,0,0.15,1");   // shotcraft 相机默认
+
+// 定场微推:开机即 1.06x,3s 缓出回全景(片长 >14s 且首镜 >7s 时才加)
+tl.fromTo(cam, { zoom: 1.06 },
+  { zoom: 1, duration: 3.0, ease: "power2.out", onUpdate: applyCam }, 0);
+
+// 推近特写:目标点 (1240, 430),1 → 1.8x,时长由公式给
+tl.to(cam, { cx: 1240, cy: 430, zoom: 1.8,
+  duration: zoomDur(1, 1.8), ease: "power3.inOut", onUpdate: applyCam },
+  "s2_generate");
+// 镜头到位后 hold ≥1.2s 再走(不写 tween 就是 hold)
+
+// 中距焦点转移:不回 1x,直接平移过去(镜间语法:0.22-0.45 改平移)
+tl.to(cam, { cx: 880, cy: 620,
+  duration: 0.7, ease: followEase, onUpdate: applyCam }, "s3_process+=1.5");
+
+// 谢幕:0.55s 拉出 + ≥0.8s 全景停顿,data-duration 覆盖到停顿末尾
+tl.to(cam, { cx: 960, cy: 540, zoom: 1,
+  duration: 0.55, ease: "power3.inOut", onUpdate: applyCam }, "s5_hold");
+
+window.__timelines["main"] = tl;
+applyCam();   // 首帧保险:timeline 停在 t=0 时 onUpdate 不触发(§6.3)
+```
+
+镜头预算不写在代码里,写在排镜时:相邻镜头 tween 起点间隔 ≥2.6s、
+15s 窗口 ≤4-5 个、<1.25x 的 zoom 不排(camera-language §0/§4.4)。
+
+### 9.5 counter-transform · 跟随字幕/标注保持字号恒定
+
+字幕和 chrome 首选放 `#hud`(不跟镜头,零成本)。必须挂在 world 内、
+跟着元素走但字号要恒定的标注,反向抵消镜头缩放:
+
+```js
+const counters = gsap.utils.toArray(".cam-counter");   // 需要恒定字号的标注
+function applyCounter() {
+  const inv = 1 / cam.zoom;
+  counters.forEach((el) => { el.style.transform = `scale(${inv})`; });
+}
+```
+
+`.cam-counter` 自身的入场动画写在其**子元素**上,避免和 counter scale 抢 transform。
+
+### 9.6 多层 parallax · 全部由 cam 推导
+
+每层不给独立 tween,速度系数乘同一个相机位移(层间系数比 ≥2 倍、≤4 层,
+camera-language §8.1),天然同步、天然 seek-safe:
+
+```js
+const LAYERS = [
+  { el: document.querySelector("#bg"),  k: 0.35 },
+  { el: document.querySelector("#mid"), k: 0.7  },
+  { el: document.querySelector("#fg"),  k: 1.4  },
+];
+function applyParallax() {
+  const dx = 960 - cam.cx, dy = 540 - cam.cy;    // 相机位移
+  LAYERS.forEach(({ el, k }) => {
+    el.style.transform = `translate(${dx * k}px, ${dy * k}px)`;
+  });
+}
+// 把 applyParallax() 追加进 applyCam() 末尾即可
+```
+
+### 9.7 Camera Rig 自检(追加到 §8 清单)
+
+- [ ] 镜头 tween 只动 cam proxy,`#world` 内元素没有被相机 tween 碰过?
+- [ ] 注册 timeline 后补了 `applyCam()` 首帧?
+- [ ] 3D 文字特写用了 CSS `zoom`,没有 `scale()` 放大发糊?
+- [ ] `zoom` 属性只出现在 `#world` 上(reflow 例外不扩散)?
+- [ ] 推拉时长全部来自 `zoomDur()`,没有手写常数?
+- [ ] 谢幕拉出后有 ≥0.8s 无 tween 的全景 hold?

+ 2 - 1
references/launch-film-director-notes.md

@@ -81,13 +81,14 @@
 
 ### Part IV · Shot-by-Shot Storyboard(分镜脚本,约 5000-7000 字 · 占 60% 篇幅)
 
-每镜含 10 个字段(缺一不可):
+每镜含 11 个字段(缺一不可):
 
 ```
 SHOT NN · NAME
 [TIMECODE]    起止时间 + 时长
 [FUNCTION]    这一镜在故事弧中的功能(一句话)
 [VISUAL]      画面构图 + 元素位置 + 运动方向
+[CAMERA]      景别(远/全/中/近/特,对应 zoom 档位)+ 运镜动作 + 一句动机;「静止」也要写为什么静止;push-in 必须写具象锚点(词汇与预算见 camera-language.md,景别体系见 storyboard-basics.md §3)
 [TYPE]        排版 spec(字体 / 字号 / 字距 / 行高 / 颜色 / 对齐)
 [ANIM]        每元素 in/out 时机 + easing + duration + stagger + delay
 [AUDIO]       music beat + SFX cue(每镜对应 BGM 节奏 + 必含 SFX 时间表)

+ 300 - 0
references/storyboard-basics.md

@@ -0,0 +1,300 @@
+# Storyboard Basics · 轻量分镜与画面构图
+
+> 任何动画开工前的分镜方法,不论 5 秒还是 50 秒。核心一句话:**每一镜先是一张会动的封面**。
+>
+> 方法论来源:花叔配图库 300+ 张封面实测提炼的定格帧规则(S1-S11)、video-shotcraft 106 张镜头卡的能量骨架判例、HuaRec Studio 运镜导演系统的镜头预算公理。
+
+---
+
+## 0 · 定位:与 launch-film 导演稿的分工
+
+本文件是 `launch-film-director-notes.md` 的**日常轻量版**。launch film 万字导演稿是重装流程,日常动画不需要那套,但**分镜这道工序本身不能省**——省掉的结果是时间表思维(第几秒出什么元素),不是画面思维(这一帧长什么样)。
+
+| 片长 / 类型 | 分镜要求 | 依据 |
+|---|---|---|
+| < 20s 动画、motion graphic、demo | 本文件的**轻量分镜卡**(§5,每镜一行) | 20s 以下不值得写万字,但每镜的画面构图必须先想清楚 |
+| ≥ 20s 动画 | Gate 文件协议要求的 `导演稿.md`,**最低要求 = 本文件的分镜卡格式**(§5 八字段一个不能少),在此之上自由加厚 | SKILL.md「Gate 文件协议」 |
+| launch film / 品牌宣传片 /「Apple 级」预期 | 在分镜卡基线上升级为万字 director's notes | `launch-film-director-notes.md`,它的 Part IV 每镜 10 字段是本文件八字段的重装版 |
+
+触发边界:用户说「快速做个动画」「简单 demo」时不要甩出万字流程,但**分镜卡照画**。分镜卡的成本是十几分钟,跳过它的成本是整片返工(2026-07-17 B00 实测:跳过画面设计直接写代码,动效全绿、视觉被一票打回)。
+
+与既有文件的关系:
+
+- `animation-best-practices.md` 管「怎么动」(节奏、easing、运动语言),本文件管「每一帧长什么样、镜头之间怎么排」,两者正交
+- `cinematic-patterns.md` 的 Scene-based 叙事是本文件的前提:先有 scene 划分,才谈每个 scene 的定格帧
+- 运镜的实现参数(camera rig、zoom 与 dolly 的区别、镜头缓动)→ `camera-language.md`,本文件只在设计层引用它的词汇
+
+---
+
+## 1 · 核心立论:每一镜先是一张会动的封面
+
+配图库做了 300+ 张封面后沉淀出一个判断:**一张好封面 = 1 个具象主角 + ≤3 个活跃元素 + 1 条清晰的视线引导**。动画的每一镜,先按这个标准设计成一张静态封面,再让它动起来。
+
+**封面 = 分镜定格**。检验方式极其具体:随机暂停成片的任何一帧,这一帧应该能直接当封面用。过不了封面测试的帧,说明这一镜的构图没设计,只是元素在时间轴上各自运动的叠加。
+
+为什么动画反而更容易违反这条:动画会用「元素是分时出现的」自我辩护,前一个还没退场后一个已经入场,结果每一帧都拥挤。静态封面没有这个借口,所以封面的纪律恰好是动画最缺的纪律(配图库实测核心启发 #1)。
+
+顺序也因此定死:**先摆定格帧,再编排运动**。§6 的 thumbnail pass 就是这个顺序的可执行版本。
+
+---
+
+## 2 · 定格帧十一律
+
+配图库 S1-S11 规则的动画语境改写。这是本文件的心脏,每一镜的构图设计逐条过。
+
+### 律一 · 封面测试(S1)
+
+每个 scene 的定格帧必须有:**1 个具象主角 + ≤3 个活跃元素 + 1 个清晰视觉焦点**。
+
+「≤3」是**每一帧的不变量**,不是每个 scene 的总量。新元素入场,旧元素必须退场(淡出、退到后景、blur+dim 都算退场),同屏活跃元素预算恒定。写时间轴时对每个时间点数一遍同屏元素,超了就砍入场或提前退场。
+
+> 自检:随手挑三个时间点暂停,数同屏活跃元素,有没有一帧超过 3?
+
+### 律二 · zoom 终点必须是具象锚点(S2)
+
+push-in / zoom 的终点只能是:品牌 logo、关键数字、UI 里的那个按钮、那行代码、人物表情。**背景纹理、氛围元素、装饰图形不配 zoom**。推近一个没有信息的东西,等于告诉观众「这里其实没什么可看的」。
+
+HuaRec 判例补一刀:**倍率 < 1.25x 的 zoom 不值得做**(视觉变化感知不足,纯属晃动;唯一例外是开场 1.06x 定场微推)。设计分镜时每个 [CAMERA] 推近动作都写明锚点是什么,写不出锚点就删掉这次运镜。
+
+> 自检:这一镜 push-in 的终点能用一个名词说出来吗(那个按钮 / 那个数字 / 那个 logo)?说不出就删运镜。
+
+### 律三 · 视线与箭头 = 镜头运动脚本(S3)
+
+静态封面里「人物视线朝向主体物、一张图只放一个箭头」的规则,翻译到动画就是:**定格帧里的引导线,就是这一镜的镜头运动指令**。镜头跟着视线走、沿着箭头推、顺着 UI 的阅读方向 pan。
+
+一个 scene 只有 1 条引导线。定格帧里找不到引导线,说明这一镜根本不知道该怎么动。此时不要硬编一个运镜,回去修构图(配图库核心启发 #3)。
+
+> 自检:把定格帧给人看 3 秒,他的视线路径和你计划的镜头运动一致吗?
+
+### 律四 · 字画分工,两条轨(S4 + 配图库 C 系列)
+
+文字轨和画面轨是两条独立时间轴,分工不重叠:
+
+| 规则 | 内容 | 出处 |
+|---|---|---|
+| 字扛钩子 | 每镜大字 2-6 字硬上限,主文案 2-4 字最佳;字说钩子,画面扛氛围,不让画面硬演文字 | 配图库 C1/C3 实测 |
+| 两拍入场 | 大字入场节奏固定两拍:**动作词先砸,定语色块后补**(「实测」满屏砸下,「Claude 4.8」色块随后贴上) | 配图库 C5 |
+| 字落负空间 | 文字永远落在构图预留的负空间,不压主角、不压焦点 | 配图库 C2 |
+| 实心强色 | 花字必须实心强色;**空心白字 + 彩色描边绝对禁用** | 配图库 G 禁区 |
+
+> 自检:遮掉画面只留字,钩子还在吗;遮掉字只留画面,构图还成立吗?两问都过才算分工干净。
+
+### 律五 · 百分比空间语言(S5)
+
+分镜卡里的构图描述必须写到百分比粒度:「产品截图右上 60% 带透视、人物左下 25%、大字压截图顶部边缘、距四边 10% 安全区」。
+
+这不是仪式感,是工程接口:**百分比语言直接映射 CSS/GSAP 布局参数**,实现 agent 拿到就能翻译,不需要再做构图决策。写「左边放产品右边放字」这种粒度等于没写(gbro 构图模板对比实测:百分比粒度的 prompt 和模糊描述的产出差一个档次)。
+
+> 自检:这一镜的构图描述里有没有至少 3 个百分比数字?没有就还是文学描述,不是 spec。
+
+### 律六 · 前中后景 = parallax 三层(S6)
+
+定格帧设计时显式写出前景 / 中景 / 后景各是什么、谁遮挡谁。这个分层就是天然的 parallax 素材:三层三个速度,遮挡关系的变化就是深度感的来源。
+
+实现参数直接抄 shotcraft 判例:视差层速度系数 0.35 / 0.7 / 1.4,**层间速度比 ≥2 倍才可辨**,层数 ≤4。
+
+> 自检:说得出这一帧的前中后景各是什么、谁遮挡谁吗?说不出就还没分层。
+
+### 律七 · setup → payoff,开场帧摆悬念(S7)
+
+每个 scene 的开场定格帧留一个缺口(未完成的桥、还没揭晓的数字、半屏空白),这一镜的运动负责闭合它。「从 A 到 B」的结构天然就是转场叙事:上一镜的 payoff 可以直接是下一镜的 setup。
+
+反面是「开场帧已经把所有信息摆全,然后元素原地做动效」:那是会动的海报,不是镜头。
+
+> 自检:这一镜的开场帧和结束帧并排放,观众能说出「什么被兑现了」吗?
+
+### 律八 · 物化 + 过程感选意象(S8)
+
+抽象概念必须物化成可见的实物动作,且**优先挑有过程感的动作**:「连接」= 桥一段段搭起来、「降价」= 价格牌 $89 逐字划掉变 $29、「AI 味重」= 稿纸上 stagger 长满整齐的灰方块。
+
+动作本身就是动画脚本:选对了意象,画面焦点和镜头运动会从意象里自己长出来,不需要另外发明动效。方法是先做费曼式翻译(这个概念怎么「发生」?),翻译完再谈画风(配图库核心启发 #4)。
+
+> 自检:这个意象能用一个动词概括吗(搭、划、长、落)?只能用名词描述的意象没有过程感,换一个。
+
+### 律九 · 风格 × 构图正交(S9)
+
+配图库最重要的架构认知,原样迁移:**风格皮肤全片定一次**(色板、字体、材质、背景逻辑),**构图模板逐 scene 换**(这一镜居中特写、下一镜三列网格、再下一镜对角双区)。
+
+scene 切换 = 构图切换,绝不是风格切换。全片换风格是灾难,全片一个构图是催眠。与三方向门的衔接:用户选定的方向板就是那张「定一次」的风格皮肤。
+
+> 自检:任取两镜对比,风格(色板 / 字体 / 材质)应该认不出差别,构图应该一眼不同。反过来了就是架构错了。
+
+### 律十 · 禁区继承(S10)
+
+配图库审美禁区全部继承,且要特别警惕动画的职业病:**发光粒子、数据流、全息 HUD、赛博霓虹是 motion graphics 最顺手的偷懒素材,恰好全在花叔禁区**。做动画时手比做静态图更痒,管住。
+
+科技感的正解 = 真实 UI 截图(3D 悬浮卡片形态)+ 干净大字 + 亮底。深蓝底 #0D1117 + 霓虹 glow 这个组合照旧禁用(细则同 SKILL.md §6.2)。
+
+> 自检:画面里有没有任何元素在「发光」?有就先怀疑自己在用职业病偷懒,逐个论证去留。
+
+### 律十一 · 3 米测试(S11)
+
+任何一帧暂停,画面里最大的词 3 米外可读。**不因为「反正会动」而缩小字号**——观众看动画时给每帧的注意力比看静图更少,字号只能更大不能更小。这条和 `animation-best-practices.md` §6.5 视觉密度条款互为上下限:密度条款防空旷,3 米测试防拥挤时牺牲可读性。
+
+> 自检:把关键帧截图缩到手机屏大小,最大的词还能读吗?
+
+---
+
+## 3 · 景别体系:五档 zoom 映射
+
+传统影视的远全中近特五档,在 HTML 动画里对应五个 zoom 档位(档位数值与 `camera-language.md` §4.3 一致,实现细节以它为准):
+
+| 景别 | zoom 档位 | 看什么 | 典型用途 | 出处 |
+|---|---|---|---|---|
+| 远景 | 0.78x | 全局 + 环境留白 | 开场 establishing、收尾全家福 | shotcraft 全页机位 0.78 |
+| 全景 | 1x(定场微推 1.06x) | 完整界面 / 完整场景 | 叙事基准面,多数镜头的家 | HuaRec 定场 1.06x 判例 |
+| 中景 | 1.3-1.45x | 一个功能区块 | 功能演示的主力景别 | HuaRec 轻推 / 中推档 |
+| 近景 | 1.8x | 单个组件 / 单条数据 | 强调具体交互 | HuaRec 重推档 |
+| 特写 | 2.3x(上限) | 律二的具象锚点 | 关键数字、那个按钮、logo | HuaRec 倍率上限 2.3x |
+
+两点说明:
+
+- 同一个档位可以用 zoom(scale,无视差)也可以用 dolly(perspective + translateZ,有视差)实现,气质完全不同;两者的选型规则和 camera rig 写法在 `camera-language.md`,分镜层只需要在 [CAMERA] 列写清档位和动机
+- 档位是设计词汇不是枷锁:1.5x、2.0x 都合法,档位的作用是让「中景」「特写」这些词在分镜表里有确定的数值含义
+
+### 相邻镜头的景别节奏
+
+| 规则 | 内容 | 依据 |
+|---|---|---|
+| 避免同景别连切 | 相邻两镜同档位,切换没有变化感,读作「画面跳了一下」而不是「换镜头了」;至少差一档 | HuaRec「倍率 <1.25x 不值得拍」推广到镜间 |
+| 避免两级跳 | 远景直切特写(0.78x → 2.3x)会晕;要跳必须是有意为之的 punch-in,且配转场(白闪 / whip-pan)垫住 | HuaRec 舒适预算 |
+| 焦点近则并镜 | 相邻两镜焦点距离很近,合并成一镜,联合包围盒重新定档 | HuaRec 镜间语法 |
+| 焦点中距则平移 | 宁可用低一档的景别一镜平移过去,不做「拉出再推进」的泵动 | HuaRec「改平移」判例 |
+| 焦点远则弃镜 | 对角横跳的两个焦点,绝不连拍两个 zoom,砍掉一个或插全景过渡 | HuaRec「弃镜」判例 |
+| 时长随幅度伸缩 | 景别切换的过渡时长不是常数:`duration = 0.55 × |ln(zoomTo/zoomFrom)| / ln2`,clamp [0.30, 0.94]s;固定 duration 是业余感来源 | HuaRec 对数伸缩公式 |
+| 节奏预算 | 相邻镜头变化间隔 ≥2.6s,任意 15s 窗口内景别变化 ≤4-5 次 | HuaRec 舒适预算 A2 |
+| 谢幕铁律 | 成片永远以全景 / 远景收尾,结尾前 ≥0.8s 全景停顿;**绝不在推近态戛然而止** | HuaRec 谢幕判例 |
+
+---
+
+## 4 · 能量骨架:先划走 hold 预算,再排动效
+
+多镜头片子(≥10s)的镜头排列不是平均分配,套 shotcraft 的 promo-energy-arc 四段位骨架:
+
+| 段位 | 时长占比 | 能量 | 内容 | 硬指标 |
+|---|---|---|---|---|
+| ① 开场 | 8-12% | 低 | 品牌 / 主题亮相 | 字标落定 hold ≥1s |
+| ② 单主角立传 | 12-15% | 低中 · 全片最慢 | 主角一个完整动作弧(入场 → 悬停 → 落位) | 动作弧 ≥3s,质感最高的一段 |
+| ③ 功能爬升 | 55-65% | 中高 ⇄ 低交替 | 每镜绑一个独特功能,能量高低交替排列 | 每 1-2 个功能镜头后插一张**呼吸字卡**(低能量、大留白、2-6 字) |
+| ④ 收场 | 13-16% | 全片峰值 | 全家福 + sign-off | 收尾 hold,谢幕回全景(§3 铁律) |
+
+**排片纪律:先划走 hold / rest 帧预算,再排动效**(shotcraft 填空流程判例)。具体顺序:
+
+1. 列功能清单,数出镜头数 N
+2. 先把不可侵犯的静止时间从总预算里划走:字标 hold ≥1s、批量动效收尾 0.5s 静止、开场动作弧 ≥3s、结尾 ≥0.8s 全景停顿、关键结果前 0.5s 悬停(best-practices §4.4)
+3. 剩下的时间才分给动效,能量高低交替排列,不许连续两镜高能
+4. 逐接缝选转场(§7),转场帧从相邻镜头的预算里划走,不另外加时
+
+呼吸字卡的构图也有定式:2-6 字大字 + 全屏负空间 + 零装饰,它本身就是一张过封面测试的定格帧(活跃元素 = 1),作用是给功能爬升段降能量、给观众消化时间。别把呼吸字卡做成又一个信息镜头,那等于没插。
+
+与 `animation-best-practices.md` §1 五段叙事的关系:Slow-Fast-Boom-Stop 是**单场 / 短片**(≤15s 一口气)的节奏曲线,promo-energy-arc 是**多镜头片**的骨架;15s 以下二选一即可,20s 以上用能量骨架排镜、每一镜内部再用五段叙事的手感。
+
+---
+
+## 5 · 轻量分镜卡:本文件的交付物
+
+每镜一行,八个字段一个不能少。shotcraft 的四列表(#|时间|镜头|关键动效)是最低配,这里扩到八列,其中 [CAMERA] 独立成列(launch film 导演稿的 10 字段后续同步扩为 11,新增的就是这个字段):
+
+```
+| # | 时间 | 景别 | [CAMERA] 运镜+动机 | 画面构图(百分比语言) | 关键动效 | 转场到下一镜 | 验收帧号 |
+```
+
+字段写法要求:
+
+- **时间**:起止秒 + 隐含时长;转场时间含在本镜预算内,不另列(§4 排片纪律第 4 条)
+- **景别**:§3 五档之一 + zoom 数值
+- **[CAMERA]**:运镜动作 + 一句动机;「静止」也是合法运镜,但要写为什么静止;每个 push-in 必须写锚点(律二)
+- **画面构图**:百分比语言(律五),含前中后景分层(律六)
+- **关键动效**:这一镜只讲一个动效(shotcraft「一镜一动效」判例)
+- **转场到下一镜**:§7 决策表选型,不许留空、不许写「直接切」(裸切要写成有意为之的 hidden-cut 才合法)
+- **验收帧号**:**每镜预写 1-2 个帧号**,实现完成后就截这几帧自检(shotcraft「每镜三读 + 预写验收帧号」判例)。预写的意义:验收标准在动手前就定死,不给实现后「看起来还行」的模糊空间
+
+### 示例:12s 产品动画完整分镜表
+
+假想产品:截图整理工具 PicSort。1920×1080 · 30fps · 360 帧。
+
+| # | 时间 | 景别 | [CAMERA] 运镜+动机 | 画面构图(百分比语言) | 关键动效 | 转场到下一镜 | 验收帧号 |
+|---|---|---|---|---|---|---|---|
+| 1 | 0-2.0s | 全景 1x | 静止,末 0.3s 轻推至 1.06x · 定场 + 埋悬念 | 桌面乱截图堆占中部 70%,大字「3000 张截图」落顶部 20% 负空间,距四边 10% 安全区;前景 2 张截图微遮挡中景堆 | 截图 30ms stagger 落桌;大字两拍入场(「3000 张」砸下,「截图」色块后补) | 虚焦接力(乱堆 blur 化开) | f30 / f55 |
+| 2 | 2.0-3.5s | 特写 2.3x | push-in 1x→2.3x · 锚点 = 一张截图右下角的日期角标 | 单张截图占 80% 居中带 2° 透视,日期角标右下 15%;其余截图退后景 blur | 推近同步后景 blur+dim(焦点切换三件套) | 共享元素归位(这张截图缩小飞入下一镜输入框旁) | f85 |
+| 3 | 3.5-6.0s | 中景 1.4x | 水平 pan 跟随光标 · 引导线 = 光标弧线轨迹 | 产品 UI 占 85% 带透视,logo 左上 10%,搜索框水平居中占 55%;光标从左下 25% 弧线入场 | 打字 3f/字符 + 结果 Chunk Reveal;打完呼吸 0.4s | mask-wipe(结果面板边缘展开成下一镜网格) | f130 / f165 |
+| 4 | 6.0-8.5s | 全景 1x | pull-out 1.4x→1x · 动机 = 展示整理后的规模 | 分类网格 3 列占 85%,每列头部一条色签;顶部 15% 负空间留给下一镜数字 | 卡片按列 stagger 入列(列间 30ms),满板后静止 0.5s | 流白 | f210 / f250 |
+| 5 | 8.5-10.5s | 近景 1.8x | 静止 · 关键结果 hold,不抢数字的戏 | 「3000 → 12 类」占中部 60%,动作词最大、定语色块;四周大负空间 | digit-roll 落位(tabular-nums),落定后 hold 0.6s | 共享元素归位(数字缩小上移让位 logo) | f290 |
+| 6 | 10.5-12s | 全景 1x | 静止 · 谢幕,全景收尾 | logo 居中占 12%,slogan 一行在下方 8%,其余全留白 | logo 形变收束(前元素坍缩 → 展开),末帧 hold ≥1s | 无(片尾) | f330 / f359 |
+
+对照检查这张表能看到骨架:镜 1 是设置悬念的 setup(律七),镜 2-4 是功能爬升的景别交替(特写 → 中 → 全,无同档连切、无两级跳),镜 5 是能量峰值 + hold 预算,镜 6 全景谢幕。每镜同屏活跃元素 ≤3。
+
+---
+
+## 6 · Thumbnail Pass:动手写正式代码前的灰盒验证
+
+分镜表是文字,thumbnail pass 把它变成看得见的构图验证。成本半小时以内,返工成本的保险。
+
+**Step 1 · 搭灰盒 HTML**:一个临时 HTML,每个关键帧一个 1920×1080 的 `<section>`。只用纯色块 + 文字标签摆构图:主角一个深灰块标「产品UI 85%」、文字区一个色块标「大字:3000张截图」、前景元素浅灰块。不写任何动效、不选字体、不调色,就是把分镜卡的百分比语言变成可见的块。
+
+灰盒阶段禁止调美(选字体、配色、加阴影都不许):美是方向板已经定掉的事,灰盒只验证构图和节奏。开始调美 = 开始逃避构图问题。
+
+**Step 2 · 只做 3-5 张关键帧**:不是每镜都做,挑能量骨架的关键节点:开场 setup 帧、立传段 hero 帧、爬升段一张代表帧、峰值帧、谢幕帧。
+
+**Step 3 · Playwright 批量截图**:
+
+```bash
+for i in 1 2 3 4 5; do
+  npx -y playwright screenshot "file://$PWD/thumbnails.html#f$i" \
+    "thumbs/f$i.png" --viewport-size=1920,1080
+done
+```
+
+**Step 4 · 验收三问**(对着并排缩略图问):
+
+1. **盖住所有文字标签,5 张图的构图差异还看得出来吗?** 看不出 = 节奏没做出来,逐 scene 换构图模板没执行(律九;best-practices §1 的 thumbnail 自检同源)
+2. **每一张单独过封面测试吗?**(律一:1 主角 + ≤3 元素 + 1 焦点)
+3. **每一张的引导线指得出来吗?** 指不出来的那镜回去修构图,不要往下走(律三)
+
+**Step 5 · 过了再写正式代码**。灰盒 HTML 留在项目目录里当构图基准,实现跑偏时回来对照。
+
+**与三方向门的关系**:三方向硬门的「方向板」(hero 关键帧真实静帧 + 色板 + 气质句)本质就是**首版 thumbnail**。用户选定方向后,thumbnail pass 是把那一张方向板扩展成整片的关键帧序列:方向板定风格皮肤,thumbnail pass 定逐镜构图,正好是律九的两层正交。
+
+---
+
+## 7 · 转场决策表:按叙事关系选型
+
+转场不是装饰,是接缝处的叙事逻辑。先判断相邻两镜的**叙事关系**,再选型。转场词汇的实现参数(时长、easing、遮罩写法)见 `camera-language.md` §7 的三层转场词汇。
+
+| 相邻两镜的叙事关系 | 首选转场 | 备选 | 判例依据 |
+|---|---|---|---|
+| 时间跳跃(「三天后」「整理完成后」) | 黑场字卡 / 流白 | whip-pan | shotcraft 六式:大落差用白 / 黑垫 |
+| 空间平移(同一界面的不同区域) | **一镜平移,根本不切** | hidden-cut | HuaRec「中距离改平移」:宁可广一档一镜过去,不做出-进泵动 |
+| 概念对比(before/after、A vs B) | mask-wipe | 分屏后硬切 + 白闪 | shotcraft mask-wipe 穿窗判例 |
+| 递进 / 因果(setup 的答案在下一镜) | 共享元素归位(上一镜元素飞成下一镜主角) | morph | shotcraft travel 两式;voiceover-pipeline「hero 跨 scene morph 不切」铁律同源 |
+| 能量落差大(呼吸字卡 → 高能镜头) | 流白 / 白闪 FlashCut | whip-pan | shotcraft:接缝按能量落差选型 |
+| 能量落差小(爬升段相邻功能镜) | 虚焦接力 / 交叉淡化 ≥8f | hidden-cut | shotcraft graze-face-tour「段间交叉淡化防黑闪」 |
+
+三条纪律:
+
+1. **一个接缝只用一式**,不叠加(白闪 + whip-pan 一起上是 slop)
+2. **转场帧从相邻镜头预算划走**,不凭空加时长;分镜表的时间列已含转场
+3. **全片零裸切**。shotcraft 判例原话:公认优秀的发布片全程没有一次裸切。要「切」的效果就用 hidden-cut(借满屏元素 / 白帧 / 运动峰值藏切点),那是设计过的切,不是没设计的切
+
+带解说的片子多一条:**move on pause**(HuaRec 判例)。镜头切换和转场吸附到语音间隙,只提前不推后,上限 0.8s,因为观众在听觉空档移动视线的认知成本最低。走 voiceover-pipeline 时用 timeline.json 的真实间隙定切点。
+
+---
+
+## 8 · 开工 checklist(分镜完成的定义)
+
+写代码之前,确认以下全部存在:
+
+- [ ] 分镜表:每镜一行、八字段齐全,存进项目目录(≥20s 时就是 `导演稿.md` 的核心节)
+- [ ] 每镜定格帧过了十一律里适用的条目,至少显式检查律一(封面测试)、律三(引导线)、律五(百分比构图)
+- [ ] 景别列没有同档连切、没有两级跳(§3)
+- [ ] hold / rest 预算先划走了,呼吸字卡插了(§4)
+- [ ] 每个接缝的转场选型写在表里,全片零裸切(§7)
+- [ ] thumbnail pass 的 3-5 张灰盒截图过了验收三问(§6)
+- [ ] 每镜预写了验收帧号,等实现后逐帧对照(§5)
+
+七项全过,分镜阶段结束,进入实现。实现被打回时先分层定位:动效骨架的问题还是视觉工艺的问题(best-practices §6.5 的修复定式),分镜表本身通常不用重写。
+
+---
+
+*成文:2026-07-23 · 来源:配图库 S1-S11 定格帧规则 + video-shotcraft 能量骨架判例 + HuaRec 运镜预算公理*
+*姊妹文件:`camera-language.md`(运镜实现层)· `launch-film-director-notes.md`(重装版)*

+ 357 - 0
references/ui-demo-animation.md

@@ -0,0 +1,357 @@
+# 产品UI展示动画 Playbook
+
+> **这是「宣传的产品有UI界面」时的单一入口。** 商单、发布动画、功能演示,只要画面主角是一个界面,先读这份文件再动手。
+>
+> 核心主张一句话:**产品动画的质感最大来源是「真实UI + 电影运镜」,不是特效。** 科技感靠让观众认出「这就是那个产品」,靠镜头怎么看它,而不是靠粒子、辉光、赛博渐变。一张真实截图配一次克制的推近,胜过十层手搓的假界面。
+>
+> 参数出处标注约定:(shotcraft·卡名)= video-shotcraft 镜头卡实测值;(huarec)= 花录 Studio 运镜导演系统;(best-practices §x)(gsap-recipes §x)= 本 skill 既有 reference。30fps 语境,1f ≈ 33ms。
+>
+> 分工:本文件管「UI 这个主角怎么演」;镜头词汇与运镜动机见 `camera-language.md`;元素级运动语法(easing、stagger、FLIP、Chunk Reveal)见 `animation-best-practices.md`,本文只引用不复写。
+
+---
+
+## §0 两条公理(凌驾于八式所有参数)
+
+| 公理 | 内容 | 出处 |
+|---|---|---|
+| **可见性不变量** | 任意时刻,光标与正在发生的 UI 操作必须在可视区内(含 8% 安全边距)。违反的镜头宁可降倍率、并镜或不拍 | huarec A1 |
+| **视觉语言从产品生长** | 先提取产品自己的设计 tokens(字体/圆角/色板/栅格),全片只准复用或克制扩展。镜头卡只继承运动语法和节奏,皮肤按目标产品重蒙 | shotcraft 公理 1 |
+
+---
+
+## §1 决策树:真实UI截图运镜 vs HTML重建UI
+
+这是本文件里**最影响工作量的一个决策**,两条路差一个量级。默认从最省的路径开始判断:
+
+```
+产品UI要出场
+ │
+ ├─ 界面只需要「被看」(推近/巡览/悬浮/对比)?
+ │   └─ 是 → 【路径一】截图装frame + 2.5D运镜。到此为止,别重建
+ │
+ ├─ 只有少数几个元素需要单独动(一张卡浮起、一行数据滚动)?
+ │   └─ 是 → 【路径三】混合:截图当底 + 关键元素切片重建
+ │
+ └─ 界面本身是叙事主体,元素要逐个登场/响应操作/改状态?
+     └─ 是 → 【路径二】HTML重建。走 build-up 八式②
+```
+
+| 路径 | 做法 | 工作量 | 适用判据 |
+|---|---|---|---|
+| 一 · 截图运镜 | 真实截图放进 `browser_window.jsx` / `macos_window.jsx` 设备框,容器上做 zoom/rotate/pan | 小时级 | 界面是「被观看的对象」;观众不需要看到界面内部产生变化 |
+| 二 · HTML重建 | 按截图逐像素重建可动的 DOM 结构 | 天级 | 元素需要独立时间轴:逐步生成、typing、状态切换、hover 响应 |
+| 三 · 混合 | 全页截图当底层纹理,要动的元素抠成透明底切片叠在原坐标上动 | 半天级 | 90% 画面静态、10% 元素要活。**多数商单的正确答案** |
+
+**混合策略的关键**:切片元素动完必须归位到截图上的真实槽位。目标卡若悬浮在网格上方不落回布局,观众立刻读出「假」(shotcraft·type-and-filter,Q9 判例曾因此近乎整文件重写)。
+
+混合路径的五步操作:
+
+1. 全页 2x 截图铺底,装进设备框(网页产品用 `browser_window.jsx`,桌面 App 用 `macos_window.jsx`)
+2. 要动的元素按 layout.json 坐标抠成透明底切片
+3. 截图底层在切片原位铺「页面底色补丁」盖掉烤入的原元素(spotlight-hero-card 的原位补丁手法:卡起飞后原位铺底色补丁 + 强调色呼吸描边,落地瞬间增亮消失)
+4. 切片叠在补丁上做动画,终点回到 layout.json 槽位
+5. 特写推进段用 4x 高清切片 6f 交叉淡入盖住低倍纹理(shotcraft·PageCam 配套技法)
+
+**设备框选择**:框是「这是真软件」的语境信号,裸截图悬在画布上像贴图。但框也吃掉画面面积,特写推进到 zoom 2x 以上时框已出画,此时可直接用无框切片。
+
+### 素材三件套(走路径一/三之前先采齐)
+
+shotcraft pipeline 阶段 1 的标准采集物(shotcraft·六阶段 pipeline):
+
+| 素材 | 规格 | 用途 |
+|---|---|---|
+| 全页 2x 截图 | 设备像素比 2 起,长页整页截 | 底层纹理;推近后不糊的下限 |
+| 元素透明底切片 | 要单独动的元素逐个抠出,4x 更佳 | 混合路径的「演员」;特写推进期 6f 交叉淡入盖住低倍纹理 |
+| layout.json 坐标表 | 每个切片在全页坐标系里的 `{x,y,w,h}` | 动完归位的「真实槽位」依据;标注/高亮框的定位锚 |
+
+截图来源走 `brand-asset-protocol.md` 的 UI 截图采集协议(App Store 截图、官网 screenshots、演示视频截帧、用户账号实截),质量门槛同样适用「5-10-2-8」。推近特写下文字发糊的根治办法(CSS `zoom` 布局级缩放替代 transform scale)见 `camera-language.md`。
+
+---
+
+## §2 UI展示八式 · 总览
+
+| # | 式 | 一句话 | 路径 | 来源卡 |
+|---|---|---|---|---|
+| ① | 3D展示台 / hero特写 | 一张卡立成全片主角:聚光→推近→悬浮→归位 | 一/三 | spotlight-hero-card |
+| ② | 界面逐步生成 build-up | 界面从无到有:骨架→内容→数据,登场即叙事 | 二 | skeleton-reveal / row-embed / document-typewriter-reveal |
+| ③ | 用户 typing 模拟 | 真人手速打字,光标常亮转闪烁 | 二/三 | type-and-filter |
+| ④ | 光标操作叙事 | 光标当演员:弧线移动、点击 ripple、hover 联动 | 全部 | type-and-filter / collab-cursor-moves |
+| ⑤ | UI状态转场时间轴化 | tab/modal/路由切换写成时间轴上的一段戏 | 二 | command-palette-summon |
+| ⑥ | 界面3D巡览 | 长界面斜置滑过,或镜头贴脸游览 | 一 | steep-tilt-glide / graze-face-tour |
+| ⑦ | 长页滚动叙事 | 长页快滚急刹,停在目标行 | 一/三 | scroll-brake-moves |
+| ⑧ | feature callout 标注 | 标注线生长、高亮框、说明卡、before/after 对比 | 全部 | before-after-slider-scrub 等 |
+
+选型纪律:**一个镜头只讲一式,一式全片只当一次主角**(shotcraft 公理 5)。八式可以在一支片子里出现多式,但各占各的镜头。
+
+---
+
+## §3 八式详解
+
+### ① 3D展示台 / hero特写
+
+把一个核心对象(卡片/面板/模块)立成产品的原子单位。质感最高、节奏最慢的一镜,适合放在开场后的「单主角立传」段。
+
+| 参数 | 典型值 | 调节手感 |
+|---|---|---|
+| 相机机位 | rotY 34° 主导 + rotX 仅 8°,perspective 1200px | 侧拍优于俯拍,「从左侧拍摄而不是从下方」;rotX 一大就变成看桌面 |
+| 推进 | 全页 zoom 0.78 静止一拍,再 16f 推进到 zoom 2.6 | 推进前的静止是「让观众先看到全局」;直接开推没有空间感 |
+| 动作弧 | rise 10f(`cubic-bezier(0.2,1.25,0.3,1)` 过冲)→ 悬停 54f(sin bob 振幅 4px 周期 40f,translateZ 110px)→ reseat 18f 落地 press scale 0.997 | 锁定到落地约 3.3s,质感镜头就要慢到这个量级;初版几乎总是偏快 |
+| 轮廓光束 | SVG rounded-rect 描边跑两圈:第一圈 14f 快而亮,第二圈 20f 慢而弱(opacity 0.62) | 两圈快慢有别才读作「持续扫描」,一圈是眨眼;光束全片只给主角一次 |
+| 双层影 | `0 8·lift px …, 0 46·lift px 90·lift px` 随悬浮高度生长 | 影子不随高度长,悬浮就不成立 |
+| 聚光灯引导 | 游走光经 4 个中间站锁定卡心,光池半径 620→420→360 收拢,锁定瞬间 +6% 脉冲;外部 vignette 0.16→0.42 压暗 | 中间站让「随机照射」可信,直奔目标读作程序化;vignette 是聚光灯的另一半 |
+| 悬空注记(可选) | 卡侧浮现两行衬线注记,translateZ 92px + bob 周期 44f(与卡 40f 相近但不同步) | 「共感」而非镜像同步;注记必须活在同一 3D 空间同一台相机透视,平面叠字会破坏空间统一 |
+
+(以上全部 shotcraft·spotlight-hero-card 实测值)
+
+已知坑:收尾禁「zoom 2.6→2.58」一类尾漂,呼吸必须是真静止;开场多卡群舞撑不起第一印象,直接从单主角+完整动作弧起稿;逐卡 glint 被判例两次否决,光效严格只给主角。
+
+搭配运镜:dolly-in 推近 + 锁定后静止 hold(见 `camera-language.md`)。倍率跨度大于 2x 时长按对数伸缩,禁弹簧过冲(huarec)。
+
+### ② 界面逐步生成 build-up
+
+界面「从无到有」的登场叙事。**build 顺序有语法**:chrome(窗口框/标题栏)→ 骨架(灰条占位)→ 内容块 stagger → 数据(数字/图表最后活)。观众对 skeleton screen 有免费预期,灰条一出现就知道内容要来。
+
+三张来源卡按对象分工:**一个界面**逐级变真用 skeleton-reveal;**一组行/卡片**嵌入列表用 row-embed;**一份文档**被写出来用 document-typewriter-reveal。
+
+| 参数 | 典型值 | 调节手感 |
+|---|---|---|
+| 三级显影 | 涂鸦(每 5f 换种子「煮沸」)→ 换真一拍 8f 加速缩退 + 骨架 spring 弹入 → 骨架行错峰 6f 滚入 → 显影行错峰 13f、行内 12f | 换真一拍必须快而果断,拖长成 crossfade「跃迁」就没了;三级布局必须严格同构,错位读作换了个页面 |
+| 逐词进场 | 2.5f/词 上浮 14px;末行末词 +14f 晚半拍 | 晚半拍是「加载完成」的句号;全部齐落平淡 |
+| 行嵌入 | 第 i 行 cue = 12 + i·9,飞行 12f;`perspective(900px) translateY(−120·air) rotateX(16°·air)` | **rotateX 收平是「嵌入」的关键读感**,纯 translateY 只是「落下」 |
+| 嵌入强调缝 | 底边 2px 强调色缝从中心 5f 展开、8f 淡出 | 给每次嵌入一个确认点,但要淡得快 |
+| 文档逐块 | 第 g 块 cue = 6 + g·3.5,每块 wipe 8f;强调色 caret 只跟最新块 | **「块数×节拍先对预算」是核心算术**,块多了先砍块不加速;「永远只有一个笔尖」,两个 caret 同时闪就是两个作者 |
+
+(skeleton-reveal / row-embed / document-typewriter-reveal 三卡实测值)
+
+已知坑:真内容层贴产品截图时,骨架灰条行高与槽位要按截图量,别按想象排;涂鸦级别不画细节,太像 UI 第一级和第二级就没差了;mock 文案不出现客户/成员真名。
+
+搭配运镜:显影段配 1→1.34 缓推(给「凑近看清」的视线动机);build 全程别加镜头横移,界面在变的时候镜头要稳(huarec:全屏级变化不推)。
+
+### ③ 用户 typing 模拟
+
+模拟真人在输入框/终端打字。与已有的 **Chunk Reveal(AI 流式输出)是两回事**:AI 输出是不规律 chunk 涌现(best-practices §4.5、gsap-recipes §3.4,那套继续用,不在这里复写);用户输入是逐字符、匀稳、带犹豫的人手节奏。写错场景是高频事故:把用户打字做成 chunk 会读作「输入框自己在生成」。
+
+| 参数 | 典型值 | 调节手感 |
+|---|---|---|
+| 打字速度 | 正文 3f/字符;终端 2f/字符;装饰性小字 0.7f/字符 | 「初版嫌快返工后的定稿值」;交互演示按真人操作速度,这是判例级铁律 |
+| 光标状态机 | 打字时**常亮**,打完转 8f 周期闪烁 | 打字中闪烁读作卡顿;常亮→闪烁的切换本身就是「打完了」的信号 |
+| 退格修正 | 偶发一次:多打 1-2 字符,停 4-6f,退格,再打对 | 「人会打错」是最便宜的真实感;但必须预写进脚本(帧确定性),不是运行时随机 |
+| 确认停顿 | 打完到页面响应留 11f(0.37s)呼吸 | 打完立刻响应读作机器自动,观众跟不上因果 |
+| 代码打字 | 终端 2f/字符;语法高亮随打字渐进上色(当前 token 打完即着色),不是打完整段再统一变色 | 统一变色是「粘贴」不是「写代码」;高亮延迟半个 token 内观众无感 |
+| 截图上打字 | 底色补丁盖掉截图里烤入的 placeholder(保留图标),文字层叠上去打 | 直接叠字会和烤入的 placeholder 重影 |
+
+(shotcraft·type-and-filter 实测值)
+
+退格修正的确定性写法(预写脚本,不是运行时抽签):
+
+```js
+// 把「打错→停→退→改」编译成字符事件表,时间轴只是回放
+const script = typeScript("nano-lab", {
+  perChar: 3 / 30,                       // 3f/字符
+  typo: { at: 5, wrong: "0", pauseF: 5 } // 第5字符处打错一个"0",停5f再退
+});
+// script = [{t:0, text:"n"}, {t:0.1, text:"na"}, ... {t, text:"nano-0"},
+//           {t+0.17, text:"nano-"}, {t+0.27, text:"nano-l"}, ...]
+// 渲染层按 t 查表取 text,双向 seek 安全
+```
+
+搭配运镜:打字前相机先上移/推近到输入框(先给镜头再动手,可见性公理);打字期间镜头锁死。
+
+### ④ 光标操作叙事
+
+光标是 UI 演示里唯一的「人」。组件用 `assets/cursor.jsx`(CursorSprite / ClickRipple / hover 联动 hook,两种时钟驱动,API 见该文件头注释)。
+
+轨迹算法不在此复写:贝塞尔弧线 + 收敛手抖见 best-practices §3.5,GSAP proxy 写法见 gsap-recipes §3.5。本式补齐的是**点击与联动的参数**:
+
+| 参数 | 典型值 | 调节手感 |
+|---|---|---|
+| 双圈 ripple | 两圈同心,**起点差 3f**,半径 14→54 / 14→78px | 单圈太轻看不见;差 3f 是「一次点击的涟漪」,差多了像点了两下 |
+| 扩散/消散解耦 | 扩散 out-cubic 22f,消散线性 26f | 扩散要冲、消散要匀;同一条曲线管两件事会「闪一下就没」。紧凑场景可压到各 10f(type-and-filter 用的就是压缩版) |
+| 点击预备 | 光标 scale 0.85(power1.in 约 3f)→ back.out 回弹 | Anticipation 让点击有「按下去」的重量(gsap-recipes §3.5 同款) |
+| hover 联动 | 光标进入目标区,目标同帧亮起(brightness +6% 或 hairline 描边浮现),光标离开即撤 | 目标不响应,光标就只是贴图;联动窗口按轨迹进度声明,不做运行时命中检测 |
+| 光标角色分工 | 操作光标(点击有 payload)vs 表演光标(位移即剧情,具名协作光标双人舞/群演) | 光标要点东西时用本式;纯叙事的协作光标是另一场戏(shotcraft·collab-cursor-moves),同片可共存但别混 |
+
+表演光标两式速查(协作/多人主题时用,参数出自 collab-cursor-moves):
+
+| 式 | 机制 | 关键参数 |
+|---|---|---|
+| dialogue-duet 双人舞 | 蓝/绿两枚具名光标靠近对话、上下分弧绕位交换(R≈270px)、名牌一亮一暗灯光交接、绿光标 easeIn 放大数十倍成转场遮挡 | 全部三次贝塞尔位移,无 linear;两枚同弧同向会有相撞感,上下分弧是「礼让」 |
+| cast-ensemble 群演 | 5 枚彩色光标 delay 0/5/9/13/17f 错峰 spring 飞入,双频正弦漂移驻场(0.055/0.021 rad/f、幅度 ±46/30px),一枚打字 cameo | 名牌晚 12f 淡入才是「人到了自报家门」;聚拢后漂移衰减到 25% 保留,完全静止的光标群读作死机 |
+
+已知坑:光标移动全程必须遵守可见性公理,目标在画面外就先动镜头再动光标;协作光标的名牌颜色=身份编码,全片一致,中途换色观众以为换了人;漂移光标永远不许盖住正在阅读的主内容。
+
+搭配运镜:点击确认后相机 16f 推进(zoom≈2.2)穿透进详情页,是「点击→进入」的标准交棒(type-and-filter);转场接法见 `camera-language.md`。
+
+### ⑤ UI状态转场时间轴化
+
+tab 切换、modal 弹出、页面路由推入。**时间轴驱动的重放和可交互原型是两种代码形态**,从原型改渲染稿时逐项翻译:
+
+| | 可交互原型 | 时间轴重放(渲染用) |
+|---|---|---|
+| 触发 | `addEventListener('click')` | timeline 上的 label / position 参数 |
+| 状态切换 | `classList.add` + CSS transition | 显式 tween(gsap-recipes §6.1 禁区规则) |
+| 打开/关闭 | `display: none` 切换 | `autoAlpha` tween |
+| hover | `:hover` 伪类 | 按轨迹进度声明的联动窗口(八式④) |
+| 随机 | `Math.random()` | mulberry32 种子预生成(gsap-recipes §6.4) |
+
+改稿后 `grep "addEventListener\|classList\|transition:"` 逐条清零。状态必须是时间的纯函数,preview 里看着正常、渲染才穿帮的 bug 全部源于此。
+
+| 参数 | 典型值 | 调节手感 |
+|---|---|---|
+| modal / 命令面板 | 背景 10f 压暗至 rgba(20,20,20,0.45) + blur 10px;面板 −20px→过冲 +8px(9f)→落回(6f);候选行错峰 i·4f | 背景不压暗,面板就没有「浮在上面」;过冲量 8px 是「轻落」,再大变玩具 |
+| tab 切换 | 指示条用 FLIP 滑移(best-practices §4.1),旧内容 5f 淡出下沉 8px,新内容 8f 淡入上浮 | 指示条和内容不同步是廉价感来源:条先走、内容跟半拍 |
+| 路由推入 | 新页从右侧整页推入 12-16f,旧页同向退 30% 距离 + 变暗 | 旧页退小距离(视差)比等距推读感深;等距是「传送带」 |
+
+(modal 参数为 shotcraft·command-palette-summon 实测值)
+
+已知坑:状态转场是「一镜一动效」的重灾区,一次转场里 tab 又切、toast 又弹、数据又滚,观众什么都没看清;一镜只演一次状态变化。
+
+搭配运镜:状态切换瞬间镜头必须静止,切完再动(huarec 镜间语法:变化是注意力消费,别和镜头运动叠加消费)。
+
+### ⑥ 界面3D巡览
+
+展示一个长界面/多屏界面的「空间感」镜头。双卡分工明确,选错卡是这一式的主要事故:
+
+| 卡 | 机制 | 适用 |
+|---|---|---|
+| steep-tilt-glide | **镜头静、页面动**:页面斜置 rotateY −60°,自己匀速滑过画面 | 页面当「展品」列队走过;内容不需要读清,看的是体量和质感 |
+| graze-face-tour | **镜头动、页面静**:页面群悬浮定住,镜头贴脸游览 | 要在巡览中看清局部内容;镜头有「参观者」人格 |
+
+| 参数 | 典型值 | 调节手感 |
+|---|---|---|
+| 斜置角 | rotateY −60°,实战裁决区 55-65° | −45° 被判「不够斜」,>−70° 内容读不清 |
+| 悬浮高度 | 120-180px(graze-face-tour) | 低了贴地没有「陈列」感,高了影子断联 |
+| 群体错峰 | 错开起点但下落重叠并行 | 完全顺序落是「排队打卡」,重叠才是「一批到场」 |
+| 段间衔接 | 交叉淡化 ≥8f | 硬切会黑闪;巡览是连续空间,不许跳切 |
+
+(shotcraft·steep-tilt-glide / graze-face-tour 实测值)
+
+多页同屏陈列的第三变体(shotcraft·page-waterfall-wall):3 列页面瀑布墙,rotateX 20° + perspective 1000px,相邻列 loop 周期差 ≥25%(如 12/9/14s)且中列反向。适合收场段「产品有很多页面」的体量陈述,内容不需要读清。周期差小于 25% 时三列会周期性对齐,「墙」瞬间变成「表格」。
+
+已知坑:两卡机制不可混用,页面和镜头同时动,观众失去参照系(晕);巡览用的页面纹理必须 2x 起,斜置放大后糊得最快。
+
+搭配运镜:这一式本身就是运镜主角,前后镜头要静(能量交替);镜头路径与 orbit 词汇见 `camera-language.md`。
+
+### ⑦ 长页滚动叙事
+
+长落地页/文档/时间线的「快滚急刹」。滚动本身不是内容,**刹车点才是**。
+
+| 参数 | 典型值 | 调节手感 |
+|---|---|---|
+| 刹车曲线 | `Easing.out(Easing.exp)` 一条曲线 50f 走完全程 | 分段减速会「泵」;一条 expo 曲线天然是「猛滚渐停」 |
+| 运动模糊 | blur 由帧间位移差分驱动,速度大糊、停即锐 | 恒定 blur 是「镜头脏了」;只在动的瞬间有模糊,静止帧永远锐利(huarec 同款结论) |
+| 目标行强调 | 停位后目标行 scale 1.03,其余退暗 0.38 | 退暗别到 0,上下文要在;1.03 是「呼吸」不是「弹出」 |
+
+(shotcraft·scroll-brake-moves 实测值)
+
+多刹车点的预算管理(huarec 舒适预算,直接搬):相邻两次「滚动+刹车」间隔 ≥2.6-3.0s;任意 15s 窗口内 ≤4-5 次;每个刹车点停留 ≥1.2s 再开滚。刹车点是注意力消费,比滚动本身贵。
+
+已知坑:滚动期间禁推近,「滚动时推近会晕」(huarec:全屏级变化不推镜头);刹车点没有内容强调(目标行不抬升、其余不退暗)时,观众不知道为什么停在这。
+
+搭配运镜:急刹后可接一次轻推近(1.3x 档)进目标行,但必须在完全停稳之后;档位表见 `camera-language.md`。
+
+### ⑧ feature callout 标注
+
+在真实截图上「指给观众看」。四个子配方,共同原则:**标注是导览员,不是界面的一部分**,风格上要和产品 UI 拉开一层(衬线字/手写感/强调色)。
+
+| 子配方 | 参数 | 调节手感 |
+|---|---|---|
+| 标注线生长 | SVG 路径 stroke-dashoffset 描线,12-18f,out-cubic;先线后字,字在线到达后 5f 淡入 | 线字同出读作贴纸;线是「手指划过去」的时间 |
+| 高亮框 | 目标区 rounded-rect 描边 8f 展开 + 框外压暗 0.3 | 只描边不压暗,视线不聚;压暗超过 0.5 变审讯灯 |
+| 放大镜 | 圆形 loupe 内嵌 2-3x 高清切片(不是 CSS 放大截图底),描边 2px + 软影,8f overshoot 弹出,跟随目标 10-14f 缓移 | loupe 里必须是高分辨率切片,放大糊纹理等于自曝素材差;一镜最多一只 loupe |
+| 连线说明卡 | 说明卡浮在 3D 空间里,translateZ 90px 量级 + bob 周期与主体相近但不同步 | 同一 3D 空间同一台相机;平面叠字被判例否决(spotlight-hero-card 悬空注记同款约束) |
+| before/after slider | 快甩 12f(out-cubic 8%→76% 过冲回弹到 70%)→ 停 18f → 慢扫 48f 到 40% 定格;速度比 5:1;after 层 clip-path 跟杆;手柄速度差分驱动 scaleX 微拉伸峰值 1.18 | 速度比 <3:1 节奏对比不可感;快甩宣告「变了」,慢扫证明「变在哪」;慢扫停 40% 让 after 留在定格里 |
+
+(slider 为 shotcraft·before-after-slider-scrub 实测值)
+
+已知坑:before/after 两版必须同布局同机位,否则读作两个页面;before 用真实旧态(注入旧数据/关闭功能),不手搓「故意丑」的假 before;角标放内容区,别压侧栏头像。
+
+搭配运镜:标注出现前镜头先推到 1.3-1.45x 档让目标区占屏(huarec:内容占可视区 80%),标注期间镜头锁死。
+
+---
+
+## §3.9 八式在全片能量骨架里的落位
+
+单式只是镜头,成片要按 promo-energy-arc 四段位排(shotcraft·导演层三件套;分镜纪律「hold/rest 帧预算先划走再排动效」同样适用):
+
+| 能量段 | 时长占比 | 放哪几式 | 理由 |
+|---|---|---|---|
+| ① 品牌开场 | 8-12% | 不放 UI 式 | 字标 hold ≥1s,UI 别抢开场 |
+| ② 单主角立传 | 12-15% | ①hero特写 或 ②build-up | 质感最高节奏最慢的一镜,全片只有一个位置 |
+| ③ 功能爬升 | 55-65% | ③④⑤⑦⑧ 交替,每镜绑一个独特功能 | 中高低能量交替;⑧callout 是低能量呼吸位的好填充 |
+| ④ 发布会收场 | 13-16% | ⑥3D巡览(全家福变体) | 全片能量峰值,多屏同框陈列后 sign-off |
+
+声音钩子速查(画面锁定后才做,钉帧写相对表达式,shotcraft 阶段 5):
+
+| 式 | 钉什么音 |
+|---|---|
+| ① hero特写 | 弹起 whoosh-big、光束 sparkle、reseat 一声 snap,三动作三专属音 |
+| ② build-up | 换真一拍 pop,逐行显影各一声极轻 tick,末词晚半拍处轻 chime 收束 |
+| ③ typing | keyboard 音与打字段严格等长(按字符数截);不足截、超长裁 |
+| ④ 点击 | click 是全片最响的一声(响度分层的顶),ripple 无声 |
+| ⑤ 状态转场 | modal 弹出一声柔 pop,路由推入一声 whoosh-fast |
+| ⑥⑦ 巡览/滚动 | 匀速段无声或极轻 hum,急刹一声闷 thud |
+| ⑧ callout | 标注线生长配极轻 draw 摩擦音;slider 快甩 whoosh + 回弹 tick,慢扫无声 |
+
+---
+
+## §4 节奏铁律(从 shotcraft 判例继承,UI 演示专用)
+
+1. **交互演示按真人操作速度走。** 打字 3f/字符、点击前有移动、响应前有呼吸。交互镜头的第一版几乎总是偏快,起稿就按人速(判例 R3,type-and-filter 因此返工)。
+2. **批量动效收尾 0.5s 静止。** 网格收敛完、列表嵌完、面板落完,满板静止半秒再走下一镜。
+3. **退场必须错峰。** 非目标元素按阅读序 0.4f 间隔错峰淡出,「同时消失读作页面崩溃」,哪怕只差 0.4f 也够(type-and-filter)。
+4. **一镜一动效。** 一个镜头只讲一个 UI 行为;同屏两个动效在抢戏,观众两个都没看见。
+5. **收尾真静止。** 呼吸位、hold 帧里禁任何尾漂(zoom 微变、opacity 微调都算);「静」是设计出来的一拍,不是没排到动作。
+
+---
+
+## §5 与三方向门 / 资产协议的衔接
+
+**三方向板怎么出**:产品 UI 动画的三方向不是三套视觉皮肤,而是**同一份 UI 素材的三种镜头叙事诠释**。素材三件套采集一次,三个方向复用。例如同一张产品截图:方向 A 走八式①(单主角 hero 特写立传)、方向 B 走八式②(build-up 从无到有讲生成能力)、方向 C 走八式⑥+⑦(巡览+滚动叙事讲体量)。方向板各附 2-3 帧关键帧 thumbnail,让花叔选的是「怎么讲这个界面」,不是「哪张图好看」。三方向门是 100% 硬门,指定风格也不豁免(SKILL.md 既有规则)。
+
+**UI 截图取材走 `brand-asset-protocol.md`**:数字产品的 UI 截图在该协议里就是一等公民资产(识别度贡献极高),采集渠道、5-10-2-8 质量门槛、brand-spec.md 固化全部照走。本文件的增量只有一条:采集时按 §1 的素材三件套规格执行(2x 全页 + 透明底切片 + layout.json),一次采齐,运镜和重建两条路径都够用。
+
+**禁手搓假 UI**:找不到真实截图时按协议兜底(向用户索取实截 / 官方演示视频截帧),不用 mockup 生成器凑,不用 CSS 画一个「像那么回事」的界面。我们在表达的是这个产品,不是「一个产品」。
+
+**例外只有一个**:走了路径二 HTML 重建时,重建稿本身就是「以真实截图为基准的复刻」,必须对照截图逐区块校(字体、圆角、间距、图标都从截图量),重建完成后与截图并排截帧对比。重建出一个「大概像」的界面,比直接用截图更伤,观众对自己天天用的产品界面误差极敏感。
+
+---
+
+## §6 交付前自检(UI 演示专项,补充 best-practices §7)
+
+- [ ] 走了 §1 决策树?没有在「截图就够」的场景里重建 HTML?
+- [ ] 切片元素动完归位到 layout.json 的真实槽位?
+- [ ] 光标与操作全程在可视区内(含 8% 边距)?
+- [ ] 用户打字是逐字符 3f 节奏,AI 输出才是 Chunk Reveal,两者没用混?
+- [ ] 光标打字时常亮、打完才闪?点击有双圈 ripple?
+- [ ] 状态转场里没有残留 addEventListener / classList / CSS transition?
+- [ ] 批量退场错峰 ≥0.4f,收尾满板静止 0.5s?
+- [ ] 全片只有一个镜头用了轮廓光/glint,且只给主角?
+- [ ] before/after 同布局同机位,before 是真实旧态?
+- [ ] 三方向板是同一 UI 素材的三种镜头诠释,不是三套皮肤?
+
+---
+
+## §7 常见失败模式速查
+
+| 症状 | 根因 | 回哪节 |
+|---|---|---|
+| 「科技感不够」于是加粒子/辉光 | 方向就错了,质感缺口在真实 UI 和运镜 | 开篇主张 + §1 |
+| 界面像贴图悬在画布上 | 没装设备框、没有影子语言 | §1 设备框 + 八式①双层影 |
+| 推近后文字发糊 | 截图倍率不足或栅格化分辨率问题 | §1 三件套 + camera-language.md |
+| 交互「像脚本不像人」 | 打字/点击/响应全按机器速度跑 | 八式③④ + §4 铁律 1 |
+| preview 正常、渲染穿帮 | 事件驱动状态混进了时间轴 | 八式⑤对照表 + gsap-recipes §6 |
+| 观众说「有点晕」 | 镜头和页面同时动,或刹车点超预算 | 八式⑥⑦ + huarec 预算 |
+
+---
+
+## 附 · 与其他文件的关系
+
+| 文件 | 关系 |
+|---|---|
+| `camera-language.md` | 镜头词汇、运镜动机、camera rig 实现。本文件说「配什么镜头」,那边说「镜头怎么做」 |
+| `animation-best-practices.md` | 元素级运动语法总纲。§3.5 鼠标轨迹、§4.1 FLIP、§4.5 Chunk Reveal 被本文件引用 |
+| `gsap-recipes.md` | 实现层翻译。§3.4/§3.5 proxy 配方、§6 seek 安全规则是本文件所有配方的执行前提 |
+| `brand-asset-protocol.md` | UI 截图的取材协议。本文件 §1 三件套是它的规格化扩展 |
+| `assets/cursor.jsx` | 八式④的组件实现,配合 `browser_window.jsx` / `macos_window.jsx` 使用 |
+| `apple-gallery-showcase.md` | 多产出同屏陈列走那边;单产品 UI 叙事走本文件 |

+ 3 - 3
references/video-export.md

@@ -22,7 +22,7 @@
 | 格式 | 规格 | 适合场景 | 典型大小(30s) |
 |---|---|---|---|
 | MP4 25fps | 1920×1080 · H.264 · CRF 18 | 公众号嵌入、视频号、YouTube | 1-2 MB |
-| MP4 60fps | 1920×1080 · minterpolate 插帧 · H.264 · CRF 18 | 高帧率展示、B站、作品集 | 1.5-3 MB |
+| MP4 60fps | 1920×1080 · 默认帧复制(兼容稳)· H.264 · CRF 18;高质量插帧需显式 `--minterpolate`;走 Stage 时钟的用 render-video-seek.js 直录真 60fps | 高帧率展示、B站、作品集 | 1.5-3 MB |
 | GIF | 960×540 · 15fps · palette 优化 | Twitter/X、README、Slack 预览 | 2-4 MB |
 
 ## 工具链
@@ -210,11 +210,11 @@ GIF 只能 256 色。一次 pass 的 GIF 会把全动画色彩压到 256 色通
 | 文件 | 格式 | 规格 | 大小 |
 |---|---|---|---|
 | foo.mp4 | MP4 | 1920×1080 · 25fps · H.264 | X MB |
-| foo-60fps.mp4 | MP4 | 1920×1080 · 60fps(运动插帧)· H.264 | X MB |
+| foo-60fps.mp4 | MP4 | 1920×1080 · 60fps(默认帧复制;插帧版会注明)· H.264 | X MB |
 | foo.gif | GIF | 960×540 · 15fps · palette 优化 | X MB |
 
 **说明**
-- 60fps 用 minterpolate 做运动估计插帧,transform 动画效果好
+- 60fps 默认帧复制(兼容性好);显式要求时才用 minterpolate 插帧(transform 动画效果好,复杂画面易出伪影);真 60fps 用 render-video-seek.js 逐帧 seek 直录
 - GIF 用 palette 优化,30s 动画可压到 3MB 左右
 
 要换尺寸或帧率说一声。