{
  "slug": "ui-to-brand-morph",
  "title": "UI morphs into brand mark",
  "tagline": "Two UI-to-brand morphs — icon-flip-bloom, an icon flips flat on Y and blooms into a flower mark with the wordmark settling letter by letter, and input-morph-assemble, an input field shrinking into a pill as three primitives assemble into a logo petal.",
  "description": "Two UI-to-brand morphs — icon-flip-bloom, an icon flips flat on Y and blooms into a flower mark with the wordmark settling letter by letter, and input-morph-a",
  "category": "outro",
  "tags": [
    "shotcraft",
    "remotion",
    "outro"
  ],
  "tier": "free",
  "license": "Apache-2.0",
  "spec": {
    "width": 1920,
    "height": 1080,
    "fps": 30,
    "duration": 4.39,
    "aspect": "16:9"
  },
  "kind": "remotion",
  "params_schema": {
    "type": "object",
    "properties": {}
  },
  "default_params": {},
  "preview": {
    "poster": "https://gimgs.net/motion/media/ui-to-brand-morph/poster-889ea3594839.jpg",
    "video": "https://gimgs.net/motion/media/ui-to-brand-morph/video-1dbc4e06a259.mp4"
  },
  "variants": [
    {
      "key": "icon-flip-bloom",
      "label": "icon-flip-bloom",
      "description": "The icon anticipates with two wobbles → flattens on Y via scaleX into a vertical line (double-trail with blur) → at its thinnest, swaps entities and springs open into a 5-petal flower mark → the mark shifts left to make room, the wordmark settling letter by letter",
      "use": "Single-icon products; icons whose form differs a lot from the logo, needing a \"flip over and transform\" magic feel",
      "video": "https://gimgs.net/motion/media/ui-to-brand-morph/video-1dbc4e06a259.mp4",
      "poster": "https://gimgs.net/motion/media/ui-to-brand-morph/poster-889ea3594839.jpg"
    },
    {
      "key": "input-morph-assemble",
      "label": "input-morph-assemble",
      "description": "The cursor clicks send → the text flies away → the input field's x/y/w/h/r spring-interpolate into a rounded pill → three primitives drop in from off-screen in staggered order and assemble into an abstract single petal → the whole group breathes",
      "use": "Films where a rectangular UI element like an input/card is the throughline; a logo that decomposes into geometric primitives",
      "video": "https://gimgs.net/motion/media/ui-to-brand-morph/video-ab76de6d7a11.mp4",
      "poster": "https://gimgs.net/motion/media/ui-to-brand-morph/poster-input-morph-assemble-355c8f5ffdd3.jpg"
    }
  ],
  "credits": {
    "author": "video-shotcraft (Vincent Wei)",
    "url": "https://github.com/Vincentwei1021/video-shotcraft"
  },
  "page_url": "https://gimgs.net/motion/ui-to-brand-morph.html",
  "json_url": "https://gimgs.net/motion/ui-to-brand-morph.json",
  "template_url": "https://gimgs.net/motion/ui-to-brand-morph/template.html",
  "play_url": null,
  "markdown_url": "https://gimgs.net/motion/ui-to-brand-morph.md",
  "updated_at": "2026-09-24T07:40:33.564Z",
  "prompt": "## When to use\n\nThe last beat before a brand outro; visual proof that \"the UI you use every day is this brand\"\n\n## Intention\n\nThe brand-outro library already has three routes: brand-ink-open is an opening ink-press reveal, morph-from-primitive is primitive-to-UI-container (the opposite direction), and outro-group-photo-launch is elements gathering for a group photo (assembling without transforming). This card adds a fourth: **the product's own UI element transforms itself into the brand mark** — style A is an icon flipping over into the logo (a same-slot entity swap), style B is the input field that's recurred throughout the film assembling, after its last send, into a single logo petal (multi-element landing and assembly). What the viewer sees isn't \"a logo appeared,\" it's \"the thing I was just using turns out to be it\" — the causal chain itself is the brand argument.\n\n## Duration & energy\n\n- Duration: A: ~4.3s (130f) / B: ~4.7s (140f)\n- Energy: Medium-high (a punctuation beat at the close, one complete morph telling the whole story)\n\n## Shot recipe (original Chinese — timing, easing, parameter table, known pitfalls)\n\n## 意图\n品牌收尾库里已有三条路：brand-ink-open 是开场描画压印、\nmorph-from-primitive 是图元→UI 容器（方向相反）、\noutro-group-photo-launch 是元素围拢合影（聚而不变）。本卡补第四条：\n**产品 UI 元素自己变形成品牌符号**——A 式是图标翻一个身变成 logo\n（同位实体交换），B 式是全片反复出现的输入框在最后一次发送后集结成\nlogo 单瓣（多元素落位拼装）。观众看到的不是\"logo 出现了\"，而是\n\"刚才用的东西原来就是它\"，因果链本身就是品牌论证。\n\n## 两式选型\n| 式 | 做法 | 适用 |\n|----|------|------|\n| A icon-flip-bloom | 图标 anticipation 晃两下→Y 轴 scaleX 压扁成竖线（双层拖影+blur）→最薄处实体交换、spring 绽放成 5 瓣花形 mark→mark 左移让位、wordmark 逐字落定 | 单图标产品；图标与 logo 形态差异大、需要\"翻过去变身\"的魔术感 |\n| B input-morph-assemble | 光标点发送→文字飞走→输入框 x/y/w/h/r 五量 spring 插值成圆角胶囊→三粒图元错峰从画外落下拼成抽象单瓣→整组呼吸 | 输入框/卡片等矩形 UI 是全片主角的片子；logo 可拆解为几何图元组合 |\n\n## 参数表\n| 参数 | 典型值 | 调节手感 |\n|------|--------|----------|\n| A anticipation | 12–34f 倾斜 -12°/+14°/-18°→0°，inOut(sin)，transformOrigin 钉底部 78% | 幅度递增才读作\"蓄力\"；只晃一下读不出预备动作 |\n| A 翻扁 | 12f Easing.in(cubic)，scaleX 1→0.04；两层残影 opacity 0.22/0.14 + blur 6px 滞后 0.12/0.24 | 加速入是\"甩\"的关键；匀速翻读作 flip 卡片。0.04 是最薄处，实体交换在这帧做 |\n| A 绽放 | spring(damping 11, stiffness 130)；5 瓣 ellipse 角度从 -90°（全竖直重叠）张到均匀分布，长 20→38、宽 3→15 同步生长 | 花瓣从闭合竖线张开才接得住\"翻过最薄处\"；直接 scale 整朵会断因果 |\n| A wordmark | mark 16f 左移 -420px 让位；逐字 2.2f 错峰、每字 10f | **wordmark 落定应对照原片：各字母由大变小落位，大的时候模糊、落位时清晰（scale+blur 联动逐字落定），不是横向扫出**；demo 的 translateX -70→0 + blur 16→0 横扫是待改项 |\n| B 发送反馈 | 光标 22f 移入按下 scale 0.82 + 按钮闪白 6f；文字 12f Easing.in 加速右上飞出（+700,-380）+ rotate -10° + 淡出 | 飞走必须加速——匀速飞读作平移不读作\"发出去了\" |\n| B morph | 34f 起 spring(damping 13, stiffness 90)，860×120 r26 → 300×108 r54 五量同步；白描边随进度填成实心 | 五量必须同一个 spring 驱动，分开插值会中途散架 |\n| B 三粒落下 | 56/70/84f 错峰 14f，各自 spring(damping 12) 从画外 -260px 落到预排位 | 错峰是\"集结\"的节奏感；齐落读作下雨 |\n| B 落定呼吸 | 108f 起整组 scale 1±0.03 正弦（0.18 rad/f），transformOrigin 钉瓣心 | 呼吸给\"活的 logo\"收尾；没有呼吸落定即死帧 |\n\n## 已知坑\n- demo 在灰阶/占位素材上调校通过——参数是调校起点非实战定稿，\n  首次实战须以真实素材回验\n- A 式实体交换必须发生在 scaleX ≤0.05 的最薄帧——提前换观众看见两个图形叠影，魔术穿帮\n- A 式 wordmark 按上表改逐字 scale+blur 落定后，错峰间隔可能要拉大（每字有独立的\"由大变小\"动作，2.2f 可能读不清），改完按可感性判例正常速度看片\n- B 式拼的是抽象单瓣（泪滴+胶囊组合）不是真 Slack logo——实战换成自家 logo 的图元分解，图元数 3–4 粒为宜，多了读作粒子不读作拼装\n- 变形中途别叠其他动画（morph-from-primitive 同判例）：morph 本身是全部信息\n- 与 outro-group-photo-launch 分工：那是全片元素飞来围住字标合影（聚），本卡是 UI 元素自己变成品牌符号（变）；同片收尾二选一\n- 声音：A 翻扁一声\"唰\"、绽放落定\"嗒\"；B 发送一声 send、每粒落位一声 pop、呼吸段静\n\n## 参考实现\ndemos/outro/ui-to-brand-morph/\n（IconFlipBloomLogo.tsx / InputMorphsIntoLogo.tsx）\n原片出处：A perplexity-promo 88–91.5s / B slack-promo 40–41s\n\n实现状态：两式均有参考实现与 Gallery 动态样片。\n\n## Reference implementation (Remotion)\n\n- demos/outro/ui-to-brand-morph/IconFlipBloomLogo.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/outro/ui-to-brand-morph/IconFlipBloomLogo.tsx\n- demos/outro/ui-to-brand-morph/InputMorphsIntoLogo.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/outro/ui-to-brand-morph/InputMorphsIntoLogo.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/outro/ui-to-brand-morph/IconFlipBloomLogo.tsx, demos/outro/ui-to-brand-morph/InputMorphsIntoLogo.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/outro/ui-to-brand-morph/IconFlipBloomLogo.tsx",
      "url": "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/outro/ui-to-brand-morph/IconFlipBloomLogo.tsx"
    },
    {
      "path": "demos/outro/ui-to-brand-morph/InputMorphsIntoLogo.tsx",
      "url": "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/outro/ui-to-brand-morph/InputMorphsIntoLogo.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": 132,
    "repo": "https://github.com/Vincentwei1021/video-shotcraft",
    "files": [
      "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/outro/ui-to-brand-morph/IconFlipBloomLogo.tsx",
      "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/outro/ui-to-brand-morph/InputMorphsIntoLogo.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/outro/ui-to-brand-morph/IconFlipBloomLogo.tsx =====\n// icon-flip-bloom-logo —— 图标 Y 轴翻转压扁成竖线，绽放成花形 mark + wordmark 扫出\n// 源：perplexity-promo 88–91.5s。笑脸 laptop 图标 anticipation 晃两下 →\n// 沿 Y 轴翻转压扁成竖线（拖影/模糊）→ 翻过最薄处绽放花形 mark（花瓣张开）→\n// wordmark 从 mark 右侧带方向模糊逐段扫出（字符从糊到锐利）。\nimport React from 'react';\nimport { AbsoluteFill, useCurrentFrame, interpolate, Easing, spring, useVideoConfig } from 'remotion';\nimport { G } from '../../_fixtures/Fixtures';\n\nconst FONT = 'Helvetica, Arial, sans-serif';\nconst INK = '#20808d'; // 花形 mark 用一点 teal，其余灰阶\n\nconst SmileLaptop: React.FC<{ size: number }> = ({ size }) => (\n  <svg width={size} height={size} viewBox=\"0 0 40 40\">\n    <rect x={7} y={6} width={26} height={20} rx={3.5} fill=\"#fff\" stroke={G.ink} strokeWidth={3} />\n    <circle cx={15.5} cy={13.5} r={2} fill={G.ink} />\n    <circle cx={24.5} cy={13.5} r={2} fill={G.ink} />\n    <path d=\"M14 18.5 Q20 23.5 26 18.5\" stroke={G.ink} strokeWidth={2.8} fill=\"none\" strokeLinecap=\"round\" />\n    <path d=\"M3.5 31.5 L36.5 31.5\" stroke={G.ink} strokeWidth={3.6} strokeLinecap=\"round\" />\n  </svg>\n);\n\n// 5 瓣抽象花形 mark，bloom: 0(闭合竖线)→1(全开)\nconst FlowerMark: React.FC<{ size: number; bloom: number }> = ({ size, bloom }) => {\n  const petals = 5;\n  return (\n    <svg width={size} height={size} viewBox=\"-50 -50 100 100\">\n      {Array.from({ length: petals }).map((_, i) => {\n        // 从竖直方向(-90°)向两侧展开到均匀分布\n        const finalAngle = -90 + (i - (petals - 1) / 2) * (360 / petals);\n        const angle = interpolate(bloom, [0, 1], [-90, finalAngle]);\n        const len = interpolate(bloom, [0, 1], [20, 38]);\n        const wid = interpolate(bloom, [0, 1], [3, 15]);\n        return (\n          <ellipse\n            key={i}\n            cx={0}\n            cy={-len / 2}\n            rx={wid / 2}\n            ry={len / 2}\n            fill={INK}\n            opacity={0.92}\n            transform={`rotate(${angle + 90})`}\n          />\n        );\n      })}\n      <circle r={interpolate(bloom, [0, 1], [2, 9])} fill={G.ink} />\n    </svg>\n  );\n};\n\nconst WORD = 'perplexity';\n\nexport const IconFlipBloomLogo: React.FC = () => {\n  const frame = useCurrentFrame();\n  const { fps } = useVideoConfig();\n\n  // ---- 时间轴 ----\n  // 0–10: 图标登场（淡入微弹）\n  // 12–34: anticipation 倾斜蓄力晃两下\n  // 34–46: Y 轴翻转压扁成竖线（scaleX -> 0.04，带拖影）\n  // 46–62: 翻过最薄处绽放花形（bloom 0->1 带过冲）\n  // 64–100: mark 左移让位 + wordmark 逐字符方向模糊扫出\n  const FLIP_START = 34;\n  const FLIP_MID = 46;\n  const BLOOM_END = 62;\n  const WORD_START = 66;\n\n  // 登场\n  const inT = spring({ frame, fps, config: { damping: 13, stiffness: 140, mass: 0.8 } });\n\n  // anticipation：两次倾斜摆动，幅度递增（-10° / +14°），最后向反方向压一下蓄力\n  const wobble =\n    interpolate(frame, [12, 18, 24, 30, FLIP_START], [0, -12, 14, -18, 0], {\n      extrapolateLeft: 'clamp',\n      extrapolateRight: 'clamp',\n      easing: Easing.inOut(Easing.sin),\n    });\n\n  // 翻转前半：scaleX 1 -> 0.04（加速入），伴随拖影\n  const flipIn = interpolate(frame, [FLIP_START, FLIP_MID], [0, 1], {\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n    easing: Easing.in(Easing.cubic),\n  });\n  const iconScaleX = interpolate(flipIn, [0, 1], [1, 0.04]);\n\n  // 绽放：spring 过冲\n  const bloomSpring = spring({\n    frame: frame - FLIP_MID,\n    fps,\n    config: { damping: 11, stiffness: 130, mass: 0.9 },\n  });\n  const bloom = frame < FLIP_MID ? 0 : bloomSpring;\n  // mark 从竖线厚度撑开：scaleX 0.04 -> 1\n  const markScaleX = interpolate(bloom, [0, 1], [0.04, 1]);\n\n  // mark 左移让位（wordmark 登场时）\n  const shift = interpolate(frame, [WORD_START - 2, WORD_START + 16], [0, 1], {\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n    easing: Easing.out(Easing.cubic),\n  });\n  const markX = interpolate(shift, [0, 1], [0, -420]);\n\n  const showIcon = frame < FLIP_MID;\n\n  // 拖影帧（翻转期间画 2 个残影）\n  const ghosts = frame >= FLIP_START && frame < FLIP_MID ? [0.12, 0.24] : [];\n\n  return (\n    <AbsoluteFill style={{ background: G.bg, alignItems: 'center', justifyContent: 'center' }}>\n      <div style={{ position: 'relative', width: 1920, height: 400, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>\n        {/* 图标 / mark 容器 */}\n        <div\n          style={{\n            position: 'absolute',\n            left: 960 + markX,\n            top: 200,\n            transform: 'translate(-50%, -50%)',\n          }}\n        >\n          {showIcon ? (\n            <>\n              {ghosts.map((g, i) => {\n                const gs = Math.min(1, iconScaleX + g);\n                return (\n                  <div\n                    key={i}\n                    style={{\n                      position: 'absolute',\n                      left: '50%',\n                      top: '50%',\n                      transform: `translate(-50%, -50%) scaleX(${gs})`,\n                      opacity: 0.22 - i * 0.08,\n                      filter: 'blur(6px)',\n                    }}\n                  >\n                    <SmileLaptop size={340} />\n                  </div>\n                );\n              })}\n              <div\n                style={{\n                  transform: `scale(${inT}) rotate(${wobble}deg) scaleX(${iconScaleX})`,\n                  transformOrigin: 'center 78%',\n                  filter: flipIn > 0.3 ? `blur(${flipIn * 5}px)` : 'none',\n                  opacity: inT,\n                }}\n              >\n                <SmileLaptop size={340} />\n              </div>\n            </>\n          ) : (\n            <div style={{ transform: `scaleX(${markScaleX})` }}>\n              <FlowerMark size={340} bloom={bloom} />\n            </div>\n          )}\n        </div>\n\n        {/* wordmark：逐字符方向模糊扫出 */}\n        <div\n          style={{\n            position: 'absolute',\n            left: 960 + markX + 230,\n            top: 200,\n            transform: 'translateY(-50%)',\n            display: 'flex',\n            fontFamily: FONT,\n            fontWeight: 700,\n            fontSize: 150,\n            color: G.ink,\n            letterSpacing: 2,\n          }}\n        >\n          {WORD.split('').map((ch, i) => {\n            const cT = interpolate(frame, [WORD_START + i * 2.2, WORD_START + i * 2.2 + 10], [0, 1], {\n              extrapolateLeft: 'clamp',\n              extrapolateRight: 'clamp',\n              easing: Easing.out(Easing.cubic),\n            });\n            return (\n              <span\n                key={i}\n                style={{\n                  display: 'inline-block',\n                  opacity: cT,\n                  transform: `translateX(${(1 - cT) * -70}px)`,\n                  filter: `blur(${(1 - cT) * 16}px)`,\n                }}\n              >\n                {ch}\n              </span>\n            );\n          })}\n        </div>\n      </div>\n    </AbsoluteFill>\n  );\n};\n\n\n// ===== demos/outro/ui-to-brand-morph/InputMorphsIntoLogo.tsx =====\n// input-morphs-into-logo —— slack-promo 40–41s\n// 消息输入框（一行文字 + 发送键）点击发送：文字飞走，输入框收缩变形成\n// 圆角胶囊；上方依次落下 圆、胶囊、小圆，四粒元素集结排成抽象 logo\n// 单瓣（泪滴 + 胶囊的抽象组合，非真 Slack logo），落定呼吸。\nimport React from 'react';\nimport {\n  AbsoluteFill,\n  useCurrentFrame,\n  useVideoConfig,\n  interpolate,\n  spring,\n  Easing,\n} from 'remotion';\n\nconst BG = '#3d1f47'; // 深梅紫\nconst CX = 960;\nconst CY = 560;\n\n// 落定后的抽象单瓣布局（横向胶囊在中，上方泪滴圆、右侧胶囊、下方小圆）\n// 单瓣 = 主胶囊(输入框变) + 大圆 + 竖胶囊 + 小圆 集结成花瓣角\nconst FINAL = {\n  mainPill: { x: CX - 150, y: CY + 10, w: 300, h: 108, r: 54 }, // 横胶囊\n  bigDot: { x: CX - 204, y: CY + 10, d: 108 },                  // 左端圆（与胶囊左帽相切，构成泪滴感）\n  vPill: { x: CX + 96, y: CY - 152, w: 108, h: 260, r: 54 },    // 右上竖胶囊\n  smallDot: { x: CX + 150, y: CY - 226, d: 76 },                // 竖胶囊顶上的小圆\n};\n\nexport const InputMorphsIntoLogo: React.FC = () => {\n  const f = useCurrentFrame();\n  const { fps } = useVideoConfig();\n\n  // —— 时间轴 ——\n  const CLICK = 22;      // 光标按下发送\n  const FLY = 26;        // 文字飞走\n  const MORPH = 34;      // 输入框开始收缩变形\n  const DROPS = [56, 70, 84]; // 大圆 / 竖胶囊 / 小圆 依次落下\n  const SETTLE = 108;    // 全部落定，开始呼吸\n\n  // 输入框初始几何\n  const box0 = { x: CX - 430, y: CY - 60, w: 860, h: 120, r: 26 };\n\n  // 光标移入 + 点击\n  const cursorX = interpolate(f, [0, CLICK], [1500, box0.x + box0.w - 60], {\n    extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: Easing.out(Easing.cubic),\n  });\n  const cursorY = interpolate(f, [0, CLICK], [900, box0.y + 60], {\n    extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: Easing.out(Easing.cubic),\n  });\n  const press = f >= CLICK && f <= CLICK + 4 ? 0.82 : 1;\n  const cursorGone = interpolate(f, [FLY + 4, FLY + 12], [1, 0], {\n    extrapolateLeft: 'clamp', extrapolateRight: 'clamp',\n  });\n\n  // 发送键按下反馈\n  const btnFlash = f >= CLICK && f <= CLICK + 6 ? 1 : 0;\n\n  // 文字飞走：整行向右上飞出 + 加速\n  const flyT = interpolate(f, [FLY, FLY + 12], [0, 1], {\n    extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: Easing.in(Easing.cubic),\n  });\n\n  // 输入框 → 主胶囊 morph（几何插值，带一点弹性落定）\n  const m = spring({ frame: f - MORPH, fps, config: { damping: 13, stiffness: 90, mass: 0.9 } });\n  const bx = interpolate(m, [0, 1], [box0.x, FINAL.mainPill.x]);\n  const by = interpolate(m, [0, 1], [box0.y, FINAL.mainPill.y - FINAL.mainPill.h / 2 + 60 - 60]);\n  const bw = interpolate(m, [0, 1], [box0.w, FINAL.mainPill.w]);\n  const bh = interpolate(m, [0, 1], [box0.h, FINAL.mainPill.h]);\n  const br = interpolate(m, [0, 1], [box0.r, FINAL.mainPill.r]);\n  const bColor = m; // 边框白框 → 实心暖白\n\n  // 三粒元素依次从画外上方落下（spring 落定，带轻微过冲）\n  const dropSpring = (i: number) =>\n    spring({ frame: f - DROPS[i], fps, config: { damping: 12, stiffness: 110, mass: 0.85 } });\n\n  // 落定呼吸：整瓣轻微 scale 脉动\n  const breathe = f >= SETTLE ? 1 + 0.03 * Math.sin((f - SETTLE) * 0.18) : 1;\n\n  const dropY = (finalY: number, s: number) => interpolate(s, [0, 1], [-260, finalY]);\n\n  const s0 = dropSpring(0);\n  const s1 = dropSpring(1);\n  const s2 = dropSpring(2);\n\n  const WHITE = '#fdf6ee';\n\n  return (\n    <AbsoluteFill style={{ background: BG, fontFamily: 'Helvetica, Arial, sans-serif', overflow: 'hidden' }}>\n      <AbsoluteFill style={{ transform: `scale(${breathe})`, transformOrigin: `${CX}px ${CY - 40}px` }}>\n        {/* 主体：输入框 → 主胶囊 */}\n        <div\n          style={{\n            position: 'absolute',\n            left: bx,\n            top: by,\n            width: bw,\n            height: bh,\n            borderRadius: br,\n            border: `4px solid ${WHITE}`,\n            background: `rgba(253,246,238,${bColor})`,\n            boxSizing: 'border-box',\n            display: 'flex',\n            alignItems: 'center',\n            padding: '0 34px',\n            boxShadow: m > 0.6 ? '0 0 60px rgba(253,246,238,0.25)' : 'none',\n          }}\n        >\n          {/* 一行文字（发送后飞走） */}\n          <div\n            style={{\n              fontSize: 44,\n              color: WHITE,\n              whiteSpace: 'nowrap',\n              opacity: (1 - flyT) * (1 - m),\n              transform: `translate(${flyT * 700}px, ${-flyT * 380}px) rotate(${-flyT * 10}deg)`,\n            }}\n          >\n            Ready, set, go!\n            {/* 光标闪烁 */}\n            <span style={{ opacity: Math.floor(f / 8) % 2 === 0 && f < FLY ? 1 : 0 }}>|</span>\n          </div>\n          {/* 发送键 */}\n          <div\n            style={{\n              marginLeft: 'auto',\n              width: 72,\n              height: 72,\n              borderRadius: 18,\n              background: btnFlash ? '#ffffff' : 'rgba(253,246,238,0.9)',\n              opacity: 1 - m,\n              display: 'flex',\n              alignItems: 'center',\n              justifyContent: 'center',\n              transform: `scale(${press})`,\n              flexShrink: 0,\n            }}\n          >\n            {/* 纸飞机三角 */}\n            <svg width={34} height={34} viewBox=\"0 0 34 34\">\n              <path d=\"M3 17 L31 4 L20 30 L15 19 Z\" fill={BG} />\n            </svg>\n          </div>\n        </div>\n\n        {/* 大圆（第一粒，落在主胶囊左端形成泪滴组合） */}\n        {f >= DROPS[0] && (\n          <div\n            style={{\n              position: 'absolute',\n              left: FINAL.bigDot.x - FINAL.bigDot.d / 2,\n              top: dropY(FINAL.bigDot.y - FINAL.bigDot.d / 2, s0),\n              width: FINAL.bigDot.d,\n              height: FINAL.bigDot.d,\n              borderRadius: '50%',\n              background: WHITE,\n              boxShadow: '0 0 40px rgba(253,246,238,0.2)',\n            }}\n          />\n        )}\n\n        {/* 竖胶囊（第二粒） */}\n        {f >= DROPS[1] && (\n          <div\n            style={{\n              position: 'absolute',\n              left: FINAL.vPill.x - FINAL.vPill.w / 2,\n              top: dropY(FINAL.vPill.y - FINAL.vPill.h / 2, s1),\n              width: FINAL.vPill.w,\n              height: FINAL.vPill.h,\n              borderRadius: FINAL.vPill.r,\n              background: WHITE,\n              boxShadow: '0 0 40px rgba(253,246,238,0.2)',\n            }}\n          />\n        )}\n\n        {/* 小圆（第三粒，压在竖胶囊顶端旁） */}\n        {f >= DROPS[2] && (\n          <div\n            style={{\n              position: 'absolute',\n              left: FINAL.smallDot.x - FINAL.smallDot.d / 2,\n              top: dropY(FINAL.smallDot.y - FINAL.smallDot.d / 2, s2),\n              width: FINAL.smallDot.d,\n              height: FINAL.smallDot.d,\n              borderRadius: '50%',\n              background: '#e8b84b',\n              boxShadow: '0 0 40px rgba(232,184,75,0.35)',\n            }}\n          />\n        )}\n      </AbsoluteFill>\n\n      {/* 光标 */}\n      <div\n        style={{\n          position: 'absolute',\n          left: cursorX,\n          top: cursorY,\n          opacity: cursorGone,\n          transform: `scale(${press})`,\n          zIndex: 50,\n        }}\n      >\n        <svg width={40} height={44} viewBox=\"0 0 40 44\">\n          <path\n            d=\"M4 2 L4 34 L13 26 L19 40 L26 37 L20 23 L32 22 Z\"\n            fill=\"#ffffff\"\n            stroke={BG}\n            strokeWidth={2}\n          />\n        </svg>\n      </div>\n    </AbsoluteFill>\n  );\n};\n\n\n"
}