{
  "slug": "assemble-then-type-flyin",
  "title": "Assemble Then Type Fly-In",
  "tagline": "On an empty dark grid, wordless UI skeletons fly in from all sides to lock together; then letters fly in via 3D rotation, headline first, until the page forms.",
  "description": "On an empty dark grid, wordless UI skeletons fly in from all sides to lock together; then letters fly in via 3D rotation, headline first, until the page forms",
  "category": "effects",
  "tags": [
    "shotcraft",
    "remotion",
    "effects"
  ],
  "tier": "free",
  "license": "Apache-2.0",
  "spec": {
    "width": 1920,
    "height": 1080,
    "fps": 30,
    "duration": 5.7,
    "aspect": "16:9"
  },
  "kind": "remotion",
  "params_schema": {
    "type": "object",
    "properties": {}
  },
  "default_params": {},
  "preview": {
    "poster": "https://gimgs.net/motion/media/assemble-then-type-flyin/poster-d7a10a64d9a3.jpg",
    "video": "https://gimgs.net/motion/media/assemble-then-type-flyin/video-4209606aae59.mp4"
  },
  "variants": [
    {
      "key": "assemble-then-type-flyin",
      "label": "assemble-then-type-flyin",
      "description": "On an empty dark grid, wordless UI skeletons fly in from all sides to lock together; then letters fly in via 3D rotation, headline first, until the page forms.",
      "use": "",
      "video": "https://gimgs.net/motion/media/assemble-then-type-flyin/video-4209606aae59.mp4",
      "poster": "https://gimgs.net/motion/media/assemble-then-type-flyin/poster-d7a10a64d9a3.jpg"
    }
  ],
  "credits": {
    "author": "video-shotcraft (Vincent Wei)",
    "url": "https://github.com/Vincentwei1021/video-shotcraft"
  },
  "page_url": "https://gimgs.net/motion/assemble-then-type-flyin.html",
  "json_url": "https://gimgs.net/motion/assemble-then-type-flyin.json",
  "template_url": "https://gimgs.net/motion/assemble-then-type-flyin/template.html",
  "play_url": null,
  "markdown_url": "https://gimgs.net/motion/assemble-then-type-flyin.md",
  "updated_at": "2026-09-24T07:39:42.131Z",
  "prompt": "## When to use\n\nAn opening where the page/poster \"grows itself\"; a capability showcase for layout/design-tool products; a two-stage narrative from skeleton to finished page.\n\n## Intention\n\nBreak \"page generation\" into two semantically distinct stages: first erect the skeleton (frames, cards, dividers, color blocks — all wordless), then fill in text. Text lands via letter-by-letter 3D rotation because the skeleton stage only uses flat translation — the two stages must differ in motion dimension, or viewers can't read \"this is step two.\"\n\n## Duration & energy\n\n- Duration: About 5.2s (156f@30fps).\n- Energy: Medium-high (the skeleton phase is sparse, the text phase is dense; energy rises steadily toward the ending).\n\n## Shot recipe (original Chinese — timing, easing, parameter table, known pitfalls)\n\n## 意图\n把\"页面生成\"拆成两个语义清楚的阶段：先立骨架（框、卡片、分隔线、\n色块，全部无字），再填文字。文字用逐字 3D 旋转落位，是因为骨架段\n只有平面位移——两段的运动维度必须不同，否则观众读不出\"这是第二步\"。\n\n## 动效核心\n- 6 个骨架件（顶栏 url pill、右侧短线、logo 点阵、模块卡、CTA pill、\n  社交方块）按 `ft` 0.04/0.07/0.10/0.13/0.17/0.20 依次飞入，\n  `seg(ft, ft+0.14, outBack)` 过冲贴合，`from` 位移 60–220px + ±8° 旋转\n- 骨架运动中 `blur(≤2px)` 残影，`a>0.97` 清零为 `none`\n- 13 个文字块按 `start` 0.34→0.76 依次开始：大标题最先（0.34/0.40），\n  然后 logo 字标、装饰大字，最后各处 MONO 小标注\n- 逐字 3D：每个字符持有确定性种子随机的\n  `dx ±170px / dy ±130px / dz −120→−420 / rotateX ±170° / rotateY ±190° /\n  rotateZ ±120°`，走 `perspective(600px) translate3d + rotateX/Y/Z`，\n  `seg(ft, ft+0.13, outCubic)` 落位，`a>=1` 时 transform 直接设 `none`\n- 块内字符间隔自适应：`step = clamp((0.94 − start − 0.13)/字符数, 0.002, 0.012)`\n  ——保证不论块多长都在 t≈0.95 前落完\n- 随机全部来自 `rand(i) = frac(sin(i*127.1 + 311.7) * 43758.5453)`，\n  确定性、可 seek\n\n## 参数表\n| 参数 | 典型值 | 调节手感 |\n|------|--------|----------|\n| 骨架时序 | 6 件，ft 0.04→0.20（间隔 0.03–0.04） | 骨架段占前 35%；压缩到 0.15 内就和文字段糊在一起 |\n| 骨架飞入 | outBack、0.14 时长、位移 60–220px + ±8° | 与 `scanline-assemble-flyin` 同一手法，去掉扫描线后靠时序读顺序 |\n| 骨架残影 | blur ≤2px，a>0.97 设 none | 阈值留 3% 裕量；直接判 a>=1 会因浮点残留一层糊 |\n| 文字起点 | 大标题 0.34/0.40，小标注 0.58→0.76 | 大字先落是可读性要求：小字先落观众会去读小字 |\n| 逐字位移 | dx ±170 / dy ±130 / dz −120→−420 | dz 是\"从远处来\"的主力；只给 dx/dy 就成了平面散入 |\n| 逐字旋转 | rotateX/Y/Z 各 ±120°–190° | 大角度才有\"翻着飞\"的手感；±30° 只像抖动 |\n| perspective | 600px（写在每个字符自己的 transform 里） | 值越小透视越夸张；1200px 以上 3D 感基本消失 |\n| 字间隔 | clamp((0.94−start−0.13)/n, 0.002, 0.012) | 自适应上限 0.012 是\"逐字可辨\"的下限速度 |\n| 落位清零 | a>=1 → transform:'none' | 不清零会留下 13 个块的合成层，长片会掉帧 |\n\n## 已知坑\n- 字符 span 走正常排版流（`display:inline-block`），动画只动 transform——\n  不要用绝对定位逐字摆位，换文案就全乱\n- 空格字符必须显式塞成 `' '`（代码里用 `ch === ' ' ? ' ' : ch`），\n  否则 inline-block 的空 span 宽度归零，词间距消失\n- `start` 是手写的 13 个数值，换文案（尤其字符数变化大时）要回看\n  最后一块是否仍在 t≈0.95 前落完；`step` 只保证块内，不保证块间\n- 内容为中性占位模板（\"The headline for your product here\" / \"Acme Studio\"\n  等），落地全部替换；A_RGB（`159,182,232`）为强调色槽位\n- 与 `scanline-assemble-flyin` 的区别是**没有扫描线**：那张靠扫描线\n  解释顺序，这张靠\"先骨架后文字\"的两段式解释顺序，两者不要混用\n\n## 参考实现\ndemos/effects/assemble-then-type-flyin/\n（AssembleThenTypeFlyin.tsx）\n\n## Reference implementation (Remotion)\n\n- demos/effects/assemble-then-type-flyin/AssembleThenTypeFlyin.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/effects/assemble-then-type-flyin/AssembleThenTypeFlyin.tsx\n\nThe component imports shared fixtures from `demos/_fixtures` and helpers from `assets/lib` in the repo; SKILL.md there documents the render workflow.\n",
  "flow": [
    {
      "step": "Read the recipe",
      "note": "Prompt = when to use, intention, parameter table with typical values and how each one feels, known pitfalls. Treat values as calibrated starting points."
    },
    {
      "step": "Get the code",
      "tool": "git",
      "cmd": "git clone https://github.com/Vincentwei1021/video-shotcraft && cd video-shotcraft && npm install"
    },
    {
      "step": "Register + render",
      "tool": "remotion",
      "cmd": "npx remotion render <CompositionId> out.mp4   # component: demos/effects/assemble-then-type-flyin/AssembleThenTypeFlyin.tsx"
    },
    {
      "step": "Or re-implement",
      "note": "Port the timing/easing from the recipe to HTML + WAAPI (gimgs motion contract) or any engine; keep the same segment windows and easing."
    }
  ],
  "files": [
    {
      "path": "demos/effects/assemble-then-type-flyin/AssembleThenTypeFlyin.tsx",
      "url": "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/effects/assemble-then-type-flyin/AssembleThenTypeFlyin.tsx"
    }
  ],
  "contract": "React/Remotion composition (TSX) from the open-source video-shotcraft library (Apache-2.0). It is not a browser-playable template: clone the repo, register the component in a Remotion project and render with `npx remotion render`. The recipe (prompt) describes timing, easing and parameters so you can also re-implement the shot in any engine.",
  "render": {
    "engine": "remotion",
    "frames": 171,
    "repo": "https://github.com/Vincentwei1021/video-shotcraft",
    "files": [
      "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/effects/assemble-then-type-flyin/AssembleThenTypeFlyin.tsx"
    ],
    "note": "Clone https://github.com/Vincentwei1021/video-shotcraft (npm install). The component lives under demos/{category}/{name}/ and imports shared fixtures from demos/_fixtures and helpers from assets/lib. Copy it (with those imports) into a Remotion project, register a <Composition> with the template's width/height/fps/duration in Root.tsx, then `npx remotion render <CompositionId> out.mp4`. SKILL.md in the repo documents the full Claude Code / Codex workflow. The prompt below is the shot recipe (timing, easing, parameter table, pitfalls) — enough to re-implement the shot in HTML/WAAPI or another engine if you prefer."
  },
  "source": "// ===== demos/effects/assemble-then-type-flyin/AssembleThenTypeFlyin.tsx =====\n// assemble-then-type-flyin — Assemble + Type Fly-in 装配后文字 3D 落位（motion-lab 定稿转原生 Remotion）\n// 空的暗底网格页面上，无文字的组件骨架（框、卡片、分隔线、色块）先从四面八方飞入贴合；\n// 随后各处文字从 3D 空间逐字飞来——每个字符带独立的大角度 rotateX/Y/Z 旋转与纵深位移，\n// 旋转着落到自己应在的位置，先大标题后小标注，全部落位后页面成形。\n// 内容为中性占位模板，强调色可按项目替换。设计坐标 480×270（DesignStage 等比放大）。\nimport React from 'react';\nimport { DesignStage, E, lerp, rand, seg, useT } from '../../_fixtures/Motion';\n\nexport const ASSEMBLE_THEN_TYPE_FLYIN_DURATION = 156; // 5200ms @30fps\n\nconst MONO2 = \"'SF Mono',Menlo,Consolas,monospace\";\nconst SERIF2 = \"Georgia,'Times New Roman',serif\";\n\n/* ---- 组件骨架（全部无文字）：from=飞入起点位移，rot=起始旋转，ft=起飞时刻 ---- */\ntype Shell = { from: [number, number]; rot: number; ft: number };\nconst SH: Record<string, Shell> = {\n  urlPill: { from: [0, -60], rot: 0, ft: 0.04 },\n  topLine: { from: [80, -40], rot: 4, ft: 0.07 },\n  mark: { from: [-140, -30], rot: -8, ft: 0.1 },\n  card: { from: [220, 30], rot: 6, ft: 0.13 },\n  cta: { from: [-70, 120], rot: -4, ft: 0.17 },\n  social: { from: [130, 60], rot: 5, ft: 0.2 },\n};\n\n/* ---- 文字块（逐字 3D 飞入）：段内 i 表示斜体；start=该块首字起飞时刻 ---- */\ntype TSeg = { s: string; i?: boolean };\ntype Block = { x: number; y: number; font: string; color: string; ls: number; start: number; segs: TSeg[]; dy?: number };\n// 大字先落（0.34 起），小标注后落。dy 是基线补偿：29px Georgia 在 transform 缩放\n// 渲染下的基线取整与原 DPR=4 截图差 0.5px，用 translateY 校回（top 会被整像素吸附）。\nconst BLOCKS: Block[] = [\n  { x: 22, y: 64, font: `400 29px ${SERIF2}`, color: '#f2f3f6', ls: 0.3, start: 0.34, segs: [{ s: 'The headline for' }], dy: -0.5 },\n  { x: 22, y: 100, font: `400 29px ${SERIF2}`, color: '#f2f3f6', ls: 0.3, start: 0.4, segs: [{ s: 'your product here', i: true }], dy: -0.5 },\n  { x: 41, y: 35, font: `400 13px ${SERIF2}`, color: '#eceef2', ls: 0, start: 0.47, segs: [{ s: 'Acme ' }, { s: 'Studio', i: true }] },\n  { x: 316, y: 96, font: `italic 400 36px ${SERIF2}`, color: '#f4f5f8', ls: 0, start: 0.5, segs: [{ s: 'sample', i: true }] },\n  { x: 34, y: 172, font: `600 7.5px ${MONO2}`, color: '#e8e9ee', ls: 1.5, start: 0.58, segs: [{ s: 'GET STARTED' }] },\n  { x: 122, y: 173, font: `500 7.5px ${MONO2}`, color: '#6a707c', ls: 1.5, start: 0.62, segs: [{ s: 'DOCS' }] },\n  { x: 24, y: 14, font: `500 7px ${MONO2}`, color: '#8d93a0', ls: 1, start: 0.64, segs: [{ s: 'app.example.com' }], dy: 0.5 },\n  { x: 306, y: 61, font: `500 6.5px ${MONO2}`, color: '#7c828e', ls: 1.5, start: 0.66, segs: [{ s: 'WORK' }] },\n  { x: 432, y: 61, font: `500 6.5px ${MONO2}`, color: '#565b66', ls: 1.5, start: 0.68, segs: [{ s: '04 / 08' }] },\n  { x: 306, y: 164, font: `500 6px ${MONO2}`, color: '#6a707c', ls: 1.5, start: 0.7, segs: [{ s: 'KINETIC TYPE · 04' }] },\n  { x: 22, y: 143, font: `500 6.5px ${MONO2}`, color: '#565b66', ls: 1.5, start: 0.72, segs: [{ s: 'H1 · UI-SERIF / GEORGIA' }] },\n  { x: 18, y: 246, font: `500 6.5px ${MONO2}`, color: '#4c515c', ls: 1.5, start: 0.74, segs: [{ s: 'A PRODUCT OF ACME · ACME LABS, INC.' }] },\n  { x: 369, y: 242, font: `600 7.5px ${MONO2}`, color: '#c9cdd6', ls: 1.5, start: 0.76, segs: [{ s: '@USERNAME' }] },\n];\n\n// 逐字随机飞入参数：k 为全局字符序号（跨块累加，与 effect.js 的 charSeed 一致）\ntype CharP = { dx: number; dy: number; dz: number; rx: number; ry: number; rz: number };\nlet charSeed = 0;\nconst CHAR_PARAMS: CharP[][][] = BLOCKS.map((b) =>\n  b.segs.map((sg) =>\n    Array.from(sg.s, () => {\n      const k = charSeed++;\n      return {\n        dx: (rand(k) - 0.5) * 340,\n        dy: (rand(k + 50) - 0.5) * 260,\n        dz: -120 - rand(k + 99) * 300,\n        rx: (rand(k + 7) - 0.5) * 340,\n        ry: (rand(k + 13) - 0.5) * 380,\n        rz: (rand(k + 23) - 0.5) * 240,\n      };\n    })\n  )\n);\n\nexport const AssembleThenTypeFlyin: React.FC = () => {\n  const t = useT();\n\n  // 阶段一：骨架四方飞入（无字）——outBack 回弹 + 运动残余 blur\n  const shell = ({ from, rot, ft }: Shell): React.CSSProperties => {\n    const a = seg(t, ft, ft + 0.14, E.outBack);\n    const sp = a > 0 && a < 0.97 ? 1 - a : 0;\n    return {\n      position: 'absolute',\n      opacity: t >= ft ? Math.min(1, seg(t, ft, ft + 0.05) * 1.5) : 0,\n      transform: `translate(${lerp(a, from[0], 0)}px,${lerp(a, from[1], 0)}px) rotate(${lerp(a, rot, 0)}deg)`,\n      filter: sp > 0.03 ? `blur(${sp * 2}px)` : 'none',\n    };\n  };\n\n  return (\n    <DesignStage bg=\"#0a0b0e\">\n      <div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(180deg,#101116,#0c0d11)' }}>\n        {/* 底层网格 */}\n        <div\n          style={{\n            position: 'absolute',\n            inset: 0,\n            opacity: 0.5,\n            background:\n              'repeating-linear-gradient(0deg,transparent 0 23px,rgba(255,255,255,.025) 23px 24px),' +\n              'repeating-linear-gradient(90deg,transparent 0 23px,rgba(255,255,255,.025) 23px 24px)',\n          }}\n        />\n        {/* 顶栏 url pill + 右侧短线 */}\n        <div style={{ ...shell(SH.urlPill), left: 18, top: 11, width: 78, height: 14, border: '1px solid #2a2c33', borderRadius: 9 }} />\n        <div style={{ ...shell(SH.topLine), right: 18, top: 16, width: 52, height: 5, background: '#1d1f26', borderRadius: 2 }} />\n        {/* logo 点阵 */}\n        <div style={{ ...shell(SH.mark), left: 24, top: 37, width: 10, height: 10 }}>\n          {[0, 1, 2, 3].map((i) => (\n            <div\n              key={i}\n              style={{\n                position: 'absolute',\n                width: 4,\n                height: 4,\n                borderRadius: '50%',\n                background: '#e8e9ee',\n                left: (i % 2) * 6,\n                top: (i >> 1) * 6,\n              }}\n            />\n          ))}\n        </div>\n        {/* 右侧模块卡（含分隔线与缩略块，无文字） */}\n        <div\n          style={{\n            ...shell(SH.card),\n            left: 296,\n            top: 52,\n            width: 162,\n            height: 150,\n            background: '#121319',\n            border: '1px solid #23252d',\n            borderRadius: 5,\n          }}\n        >\n          <div style={{ position: 'absolute', left: 0, top: 24, width: '100%', height: 1, background: '#1e2028' }} />\n          <div style={{ position: 'absolute', left: 0, top: 104, width: '100%', height: 1, background: '#1e2028' }} />\n          {[0, 1, 2, 3].map((i) => (\n            <div\n              key={i}\n              style={{ position: 'absolute', left: 10 + i * 30, top: 124, width: 24, height: 16, background: '#1a1c23', borderRadius: 2 }}\n            />\n          ))}\n        </div>\n        {/* CTA pill 骨架 */}\n        <div style={{ ...shell(SH.cta), left: 22, top: 166, width: 74, height: 24, border: '1px solid #3a3d46', borderRadius: 12 }} />\n        {/* 社交 x 方块 */}\n        <div style={{ ...shell(SH.social), left: 352, top: 241, width: 11, height: 11, border: '1px solid #3a3d46', borderRadius: 2 }} />\n\n        {/* 阶段二：文字逐字 3D 旋转落位（间隔按块长自适应，保证 t≈0.95 前全部落位） */}\n        {BLOCKS.map((b, bi) => {\n          const n = CHAR_PARAMS[bi].reduce((acc, sgp) => acc + sgp.length, 0);\n          const step = Math.min(0.012, Math.max(0.002, (0.94 - b.start - 0.13) / n));\n          let ci = 0; // 块内字符序号（跨段累计，决定各字的错峰起飞）\n          return (\n            <div\n              key={bi}\n              style={{\n                position: 'absolute',\n                left: b.x,\n                top: b.y,\n                font: b.font,\n                color: b.color,\n                letterSpacing: b.ls,\n                whiteSpace: 'nowrap',\n                transform: b.dy ? `translateY(${b.dy}px)` : undefined,\n              }}\n            >\n              {b.segs.map((sg, si) => (\n                <span key={si} style={sg.i ? { fontStyle: 'italic' } : undefined}>\n                  {Array.from(sg.s, (ch, k) => {\n                    const c = CHAR_PARAMS[bi][si][k];\n                    const ft = b.start + ci++ * step;\n                    const a = seg(t, ft, ft + 0.13, E.outCubic);\n                    return (\n                      <span\n                        key={k}\n                        style={{\n                          display: 'inline-block',\n                          opacity: a > 0 ? Math.min(1, a * 1.8) : 0,\n                          transform:\n                            a >= 1\n                              ? 'none'\n                              : `perspective(600px) translate3d(${lerp(a, c.dx, 0)}px,${lerp(a, c.dy, 0)}px,${lerp(a, c.dz, 0)}px) ` +\n                                `rotateX(${lerp(a, c.rx, 0)}deg) rotateY(${lerp(a, c.ry, 0)}deg) rotateZ(${lerp(a, c.rz, 0)}deg)`,\n                        }}\n                      >\n                        {ch === ' ' ? '\\u00a0' : ch}\n                      </span>\n                    );\n                  })}\n                </span>\n              ))}\n            </div>\n          );\n        })}\n      </div>\n    </DesignStage>\n  );\n};\n\n\n"
}