Motion · Kết

UI biến hình thành logo thương hiệu

Hai kiểu UI biến thành logo — icon-flip-bloom icon lật phẳng trục Y nở thành hình hoa mark + wordmark vào từng chữ, và input-morph-assemble ô nhập liệu co lại thành viên nang, ba hạt hình học tụ thành một cánh logo.

1920×108016:930 fps4.39s · 132 frameremotionGiấy phép: Apache-2.0#shotcraft#remotion#outro

Nguồn: video-shotcraft (Vincent Wei) ↗ · Apache-2.0 · Composition Remotion (React/TSX) — có video preview, không có player trong trình duyệt

Biến thể

icon-flip-bloom
Icon rung báo trước hai cái → ép phẳng trục Y bằng scaleX thành đường thẳng đứng (bóng mờ hai lớp + blur) → tại điểm mỏng nhất hoán đổi thực thể, bung ra bằng spring thành mark hình hoa 5 cánh → mark dịch trái nhường chỗ, wordmark vào từng chữ
Sản phẩm một icon; icon và logo khác hình dạng nhiều, cần cảm giác ảo thuật "lật mình biến hình"
input-morph-assemble
Con trỏ bấm gửi → chữ bay đi → ô nhập liệu nội suy spring năm đại lượng x/y/w/h/r thành viên nang bo tròn → ba hạt hình học lệch nhịp rơi từ ngoài khung tụ thành một cánh trừu tượng → cả nhóm thở
Phim mà UI hình chữ nhật như ô nhập/thẻ là nhân vật chính xuyên suốt; logo có thể tháo rời thành các hạt hình học

Khi nào dùng

Nhịp cuối trước khi kết phim bằng thương hiệu; bằng chứng thị giác cho "cái UI bạn dùng hằng ngày chính là thương hiệu này"

Ý đồ

Thư viện kết phim bằng thương hiệu đã có ba con đường: brand-ink-open là mở đầu vẽ mực dập nổi, morph-from-primitive là hạt hình học→khung chứa UI (chiều ngược lại), outro-group-photo-launch là các phần tử quây lại chụp ảnh chung (tụ hợp mà không biến hình). Thẻ này bổ sung con đường thứ tư: phần tử UI của sản phẩm tự biến hình thành biểu tượng thương hiệu — kiểu A là icon lật mình biến thành logo (hoán đổi thực thể cùng vị trí), kiểu B là ô nhập liệu xuất hiện xuyên suốt phim tụ lại sau lần gửi cuối cùng thành một cánh logo (nhiều phần tử vào vị trí lắp ráp). Khán giả thấy không phải "logo xuất hiện", mà là "thứ vừa dùng hóa ra chính là nó" — bản thân chuỗi nhân quả đã là luận chứng thương hiệu.

Thời lượng & năng lượng

  • Thời lượng: A khoảng 4.3s (130f) / B khoảng 4.7s (140f)
  • Năng lượng: Trung bình cao (điểm nhấn kết phim, một lần biến hình trọn vẹn kể hết câu chuyện)

Công thức cảnh quay (bản gốc tiếng Trung — timing, easing, bảng tham số, lỗi hay gặp)

意图

品牌收尾库里已有三条路:brand-ink-open 是开场描画压印、 morph-from-primitive 是图元→UI 容器(方向相反)、 outro-group-photo-launch 是元素围拢合影(聚而不变)。本卡补第四条: 产品 UI 元素自己变形成品牌符号——A 式是图标翻一个身变成 logo (同位实体交换),B 式是全片反复出现的输入框在最后一次发送后集结成 logo 单瓣(多元素落位拼装)。观众看到的不是"logo 出现了",而是 "刚才用的东西原来就是它",因果链本身就是品牌论证。

两式选型

| 式 | 做法 | 适用 | |----|------|------| | A icon-flip-bloom | 图标 anticipation 晃两下→Y 轴 scaleX 压扁成竖线(双层拖影+blur)→最薄处实体交换、spring 绽放成 5 瓣花形 mark→mark 左移让位、wordmark 逐字落定 | 单图标产品;图标与 logo 形态差异大、需要"翻过去变身"的魔术感 | | B input-morph-assemble | 光标点发送→文字飞走→输入框 x/y/w/h/r 五量 spring 插值成圆角胶囊→三粒图元错峰从画外落下拼成抽象单瓣→整组呼吸 | 输入框/卡片等矩形 UI 是全片主角的片子;logo 可拆解为几何图元组合 |

参数表

| 参数 | 典型值 | 调节手感 | |------|--------|----------| | A anticipation | 12–34f 倾斜 -12°/+14°/-18°→0°,inOut(sin),transformOrigin 钉底部 78% | 幅度递增才读作"蓄力";只晃一下读不出预备动作 | | A 翻扁 | 12f Easing.in(cubic),scaleX 1→0.04;两层残影 opacity 0.22/0.14 + blur 6px 滞后 0.12/0.24 | 加速入是"甩"的关键;匀速翻读作 flip 卡片。0.04 是最薄处,实体交换在这帧做 | | A 绽放 | spring(damping 11, stiffness 130);5 瓣 ellipse 角度从 -90°(全竖直重叠)张到均匀分布,长 20→38、宽 3→15 同步生长 | 花瓣从闭合竖线张开才接得住"翻过最薄处";直接 scale 整朵会断因果 | | A wordmark | mark 16f 左移 -420px 让位;逐字 2.2f 错峰、每字 10f | wordmark 落定应对照原片:各字母由大变小落位,大的时候模糊、落位时清晰(scale+blur 联动逐字落定),不是横向扫出;demo 的 translateX -70→0 + blur 16→0 横扫是待改项 | | B 发送反馈 | 光标 22f 移入按下 scale 0.82 + 按钮闪白 6f;文字 12f Easing.in 加速右上飞出(+700,-380)+ rotate -10° + 淡出 | 飞走必须加速——匀速飞读作平移不读作"发出去了" | | B morph | 34f 起 spring(damping 13, stiffness 90),860×120 r26 → 300×108 r54 五量同步;白描边随进度填成实心 | 五量必须同一个 spring 驱动,分开插值会中途散架 | | B 三粒落下 | 56/70/84f 错峰 14f,各自 spring(damping 12) 从画外 -260px 落到预排位 | 错峰是"集结"的节奏感;齐落读作下雨 | | B 落定呼吸 | 108f 起整组 scale 1±0.03 正弦(0.18 rad/f),transformOrigin 钉瓣心 | 呼吸给"活的 logo"收尾;没有呼吸落定即死帧 |

已知坑

  • demo 在灰阶/占位素材上调校通过——参数是调校起点非实战定稿,

首次实战须以真实素材回验

  • A 式实体交换必须发生在 scaleX ≤0.05 的最薄帧——提前换观众看见两个图形叠影,魔术穿帮
  • A 式 wordmark 按上表改逐字 scale+blur 落定后,错峰间隔可能要拉大(每字有独立的"由大变小"动作,2.2f 可能读不清),改完按可感性判例正常速度看片
  • B 式拼的是抽象单瓣(泪滴+胶囊组合)不是真 Slack logo——实战换成自家 logo 的图元分解,图元数 3–4 粒为宜,多了读作粒子不读作拼装
  • 变形中途别叠其他动画(morph-from-primitive 同判例):morph 本身是全部信息
  • 与 outro-group-photo-launch 分工:那是全片元素飞来围住字标合影(聚),本卡是 UI 元素自己变成品牌符号(变);同片收尾二选一
  • 声音:A 翻扁一声"唰"、绽放落定"嗒";B 发送一声 send、每粒落位一声 pop、呼吸段静

参考实现

demos/outro/ui-to-brand-morph/ (IconFlipBloomLogo.tsx / InputMorphsIntoLogo.tsx) 原片出处:A perplexity-promo 88–91.5s / B slack-promo 40–41s

实现状态:两式均有参考实现与 Gallery 动态样片。

Mã nguồn tham chiếu (Remotion)

  • demos/outro/ui-to-brand-morph/IconFlipBloomLogo.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/outro/ui-to-brand-morph/IconFlipBloomLogo.tsx
  • demos/outro/ui-to-brand-morph/InputMorphsIntoLogo.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/outro/ui-to-brand-morph/InputMorphsIntoLogo.tsx

Component import fixture chung từ demos/_fixtures và helper từ assets/lib của repo; xem SKILL.md trong repo để biết quy trình render.

Mọi thứ trên trang này đều có bản máy đọc. Đưa agent link JSON, hoặc thêm MCP server gimgs rồi bảo nó tìm motion template.

  1. Tải JSON — trong đó có công thức cảnh quay (timing, easing, bảng tham số, lỗi hay gặp) và toàn bộ source TSX.
  2. Clone repo video-shotcraft; component import fixture chung từ demos/_fixtures và helper từ assets/lib.
  3. Đăng ký component thành <Composition> trong project Remotion rồi render bằng `npx remotion render`, hoặc dựng lại công thức bằng engine của bạn.
curl -s https://gimgs.net/motion/ui-to-brand-morph.json
git clone https://github.com/Vincentwei1021/video-shotcraft
# component: demos/outro/ui-to-brand-morph/IconFlipBloomLogo.tsx, demos/outro/ui-to-brand-morph/InputMorphsIntoLogo.tsx
npx remotion render <CompositionId> out.mp4
claude mcp add --transport http gimgs https://gimgs.net/mcp
# then: search_motion_templates / get_motion_template("ui-to-brand-morph")

Prompt

Use the video-shotcraft shot recipe "UI biến hình thành logo thương hiệu" (Remotion composition, Apache-2.0) published on gimgs.net.
Recipe JSON (prompt = timing/easing/parameter table/pitfalls, files = TSX source): https://gimgs.net/motion/ui-to-brand-morph.json
Raw TSX: https://gimgs.net/motion/ui-to-brand-morph/template.html
Repository: https://github.com/Vincentwei1021/video-shotcraft  (component: demos/outro/ui-to-brand-morph/IconFlipBloomLogo.tsx, demos/outro/ui-to-brand-morph/InputMorphsIntoLogo.tsx; imports demos/_fixtures and assets/lib)
Spec: 1920x1080, 30 fps, 4.39s

1. Read the JSON and follow its "prompt" section (when to use, intention, parameter table, known pitfalls).
2. Adapt it for: <describe what you want>.
3. Either clone the repo, register the component as a <Composition> in a Remotion project and run npx remotion render <CompositionId> out.mp4 — or re-implement the recipe in your own engine (HTML/WAAPI works: keep the same timing and easing).

JSON · Markdown · llms.txt

13,728 chars · Apache-2.0Mở template gốc ↗
// ===== demos/outro/ui-to-brand-morph/IconFlipBloomLogo.tsx =====
// icon-flip-bloom-logo —— 图标 Y 轴翻转压扁成竖线,绽放成花形 mark + wordmark 扫出
// 源:perplexity-promo 88–91.5s。笑脸 laptop 图标 anticipation 晃两下 →
// 沿 Y 轴翻转压扁成竖线(拖影/模糊)→ 翻过最薄处绽放花形 mark(花瓣张开)→
// wordmark 从 mark 右侧带方向模糊逐段扫出(字符从糊到锐利)。
import React from 'react';
import { AbsoluteFill, useCurrentFrame, interpolate, Easing, spring, useVideoConfig } from 'remotion';
import { G } from '../../_fixtures/Fixtures';

const FONT = 'Helvetica, Arial, sans-serif';
const INK = '#20808d'; // 花形 mark 用一点 teal,其余灰阶

const SmileLaptop: React.FC<{ size: number }> = ({ size }) => (
  <svg width={size} height={size} viewBox="0 0 40 40">
    <rect x={7} y={6} width={26} height={20} rx={3.5} fill="#fff" stroke={G.ink} strokeWidth={3} />
    <circle cx={15.5} cy={13.5} r={2} fill={G.ink} />
    <circle cx={24.5} cy={13.5} r={2} fill={G.ink} />
    <path d="M14 18.5 Q20 23.5 26 18.5" stroke={G.ink} strokeWidth={2.8} fill="none" strokeLinecap="round" />
    <path d="M3.5 31.5 L36.5 31.5" stroke={G.ink} strokeWidth={3.6} strokeLinecap="round" />
  </svg>
);

// 5 瓣抽象花形 mark,bloom: 0(闭合竖线)→1(全开)
const FlowerMark: React.FC<{ size: number; bloom: number }> = ({ size, bloom }) => {
  const petals = 5;
  return (
    <svg width={size} height={size} viewBox="-50 -50 100 100">
      {Array.from({ length: petals }).map((_, i) => {
        // 从竖直方向(-90°)向两侧展开到均匀分布
        const finalAngle = -90 + (i - (petals - 1) / 2) * (360 / petals);
        const angle = interpolate(bloom, [0, 1], [-90, finalAngle]);
        const len = interpolate(bloom, [0, 1], [20, 38]);
        const wid = interpolate(bloom, [0, 1], [3, 15]);
        return (
          <ellipse
            key={i}
            cx={0}
            cy={-len / 2}
            rx={wid / 2}
            ry={len / 2}
            fill={INK}
            opacity={0.92}
            transform={`rotate(${angle + 90})`}
          />
        );
      })}
      <circle r={interpolate(bloom, [0, 1], [2, 9])} fill={G.ink} />
    </svg>
  );
};

const WORD = 'perplexity';

export const IconFlipBloomLogo: React.FC = () => {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();

  // ---- 时间轴 ----
  // 0–10: 图标登场(淡入微弹)
  // 12–34: anticipation 倾斜蓄力晃两下
  // 34–46: Y 轴翻转压扁成竖线(scaleX -> 0.04,带拖影)
  // 46–62: 翻过最薄处绽放花形(bloom 0->1 带过冲)
  // 64–100: mark 左移让位 + wordmark 逐字符方向模糊扫出
  const FLIP_START = 34;
  const FLIP_MID = 46;
  const BLOOM_END = 62;
  const WORD_START = 66;

  // 登场
  const inT = spring({ frame, fps, config: { damping: 13, stiffness: 140, mass: 0.8 } });

  // anticipation:两次倾斜摆动,幅度递增(-10° / +14°),最后向反方向压一下蓄力
  const wobble =
    interpolate(frame, [12, 18, 24, 30, FLIP_START], [0, -12, 14, -18, 0], {
      extrapolateLeft: 'clamp',
      extrapolateRight: 'clamp',
      easing: Easing.inOut(Easing.sin),
    });

  // 翻转前半:scaleX 1 -> 0.04(加速入),伴随拖影
  const flipIn = interpolate(frame, [FLIP_START, FLIP_MID], [0, 1], {
    extrapolateLeft: 'clamp',
    extrapolateRight: 'clamp',
    easing: Easing.in(Easing.cubic),
  });
  const iconScaleX = interpolate(flipIn, [0, 1], [1, 0.04]);

  // 绽放:spring 过冲
  const bloomSpring = spring({
    frame: frame - FLIP_MID,
    fps,
    config: { damping: 11, stiffness: 130, mass: 0.9 },
  });
  const bloom = frame < FLIP_MID ? 0 : bloomSpring;
  // mark 从竖线厚度撑开:scaleX 0.04 -> 1
  const markScaleX = interpolate(bloom, [0, 1], [0.04, 1]);

  // mark 左移让位(wordmark 登场时)
  const shift = interpolate(frame, [WORD_START - 2, WORD_START + 16], [0, 1], {
    extrapolateLeft: 'clamp',
    extrapolateRight: 'clamp',
    easing: Easing.out(Easing.cubic),
  });
  const markX = interpolate(shift, [0, 1], [0, -420]);

  const showIcon = frame < FLIP_MID;

  // 拖影帧(翻转期间画 2 个残影)
  const ghosts = frame >= FLIP_START && frame < FLIP_MID ? [0.12, 0.24] : [];

  return (
    <AbsoluteFill style={{ background: G.bg, alignItems: 'center', justifyContent: 'center' }}>
      <div style={{ position: 'relative', width: 1920, height: 400, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
        {/* 图标 / mark 容器 */}
        <div
          style={{
            position: 'absolute',
            left: 960 + markX,
            top: 200,
            transform: 'translate(-50%, -50%)',
          }}
        >
          {showIcon ? (
            <>
              {ghosts.map((g, i) => {
                const gs = Math.min(1, iconScaleX + g);
                return (
                  <div
                    key={i}
                    style={{
                      position: 'absolute',
                      left: '50%',
                      top: '50%',
                      transform: `translate(-50%, -50%) scaleX(${gs})`,
                      opacity: 0.22 - i * 0.08,
                      filter: 'blur(6px)',
                    }}
                  >
                    <SmileLaptop size={340} />
                  </div>
                );
              })}
              <div
                style={{
                  transform: `scale(${inT}) rotate(${wobble}deg) scaleX(${iconScaleX})`,
                  transformOrigin: 'center 78%',
                  filter: flipIn > 0.3 ? `blur(${flipIn * 5}px)` : 'none',
                  opacity: inT,
                }}
              >
                <SmileLaptop size={340} />
              </div>
            </>
          ) : (
            <div style={{ transform: `scaleX(${markScaleX})` }}>
              <FlowerMark size={340} bloom={bloom} />
            </div>
          )}
        </div>

        {/* wordmark:逐字符方向模糊扫出 */}
        <div
          style={{
            position: 'absolute',
            left: 960 + markX + 230,
            top: 200,
            transform: 'translateY(-50%)',
            display: 'flex',
            fontFamily: FONT,
            fontWeight: 700,
            fontSize: 150,
            color: G.ink,
            letterSpacing: 2,
          }}
        >
          {WORD.split('').map((ch, i) => {
            const cT = interpolate(frame, [WORD_START + i * 2.2, WORD_START + i * 2.2 + 10], [0, 1], {
              extrapolateLeft: 'clamp',
              extrapolateRight: 'clamp',
              easing: Easing.out(Easing.cubic),
            });
            return (
              <span
                key={i}
                style={{
                  display: 'inline-block',
                  opacity: cT,
                  transform: `translateX(${(1 - cT) * -70}px)`,
                  filter: `blur(${(1 - cT) * 16}px)`,
                }}
              >
                {ch}
              </span>
            );
          })}
        </div>
      </div>
    </AbsoluteFill>
  );
};


// ===== demos/outro/ui-to-brand-morph/InputMorphsIntoLogo.tsx =====
// input-morphs-into-logo —— slack-promo 40–41s
// 消息输入框(一行文字 + 发送键)点击发送:文字飞走,输入框收缩变形成
// 圆角胶囊;上方依次落下 圆、胶囊、小圆,四粒元素集结排成抽象 logo
// 单瓣(泪滴 + 胶囊的抽象组合,非真 Slack logo),落定呼吸。
import React from 'react';
import {
  AbsoluteFill,
  useCurrentFrame,
  useVideoConfig,
  interpolate,
  spring,
  Easing,
} from 'remotion';

const BG = '#3d1f47'; // 深梅紫
const CX = 960;
const CY = 560;

// 落定后的抽象单瓣布局(横向胶囊在中,上方泪滴圆、右侧胶囊、下方小圆)
// 单瓣 = 主胶囊(输入框变) + 大圆 + 竖胶囊 + 小圆 集结成花瓣角
const FINAL = {
  mainPill: { x: CX - 150, y: CY + 10, w: 300, h: 108, r: 54 }, // 横胶囊
  bigDot: { x: CX - 204, y: CY + 10, d: 108 },                  // 左端圆(与胶囊左帽相切,构成泪滴感)
  vPill: { x: CX + 96, y: CY - 152, w: 108, h: 260, r: 54 },    // 右上竖胶囊
  smallDot: { x: CX + 150, y: CY - 226, d: 76 },                // 竖胶囊顶上的小圆
};

export const InputMorphsIntoLogo: React.FC = () => {
  const f = useCurrentFrame();
  const { fps } = useVideoConfig();

  // —— 时间轴 ——
  const CLICK = 22;      // 光标按下发送
  const FLY = 26;        // 文字飞走
  const MORPH = 34;      // 输入框开始收缩变形
  const DROPS = [56, 70, 84]; // 大圆 / 竖胶囊 / 小圆 依次落下
  const SETTLE = 108;    // 全部落定,开始呼吸

  // 输入框初始几何
  const box0 = { x: CX - 430, y: CY - 60, w: 860, h: 120, r: 26 };

  // 光标移入 + 点击
  const cursorX = interpolate(f, [0, CLICK], [1500, box0.x + box0.w - 60], {
    extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: Easing.out(Easing.cubic),
  });
  const cursorY = interpolate(f, [0, CLICK], [900, box0.y + 60], {
    extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: Easing.out(Easing.cubic),
  });
  const press = f >= CLICK && f <= CLICK + 4 ? 0.82 : 1;
  const cursorGone = interpolate(f, [FLY + 4, FLY + 12], [1, 0], {
    extrapolateLeft: 'clamp', extrapolateRight: 'clamp',
  });

  // 发送键按下反馈
  const btnFlash = f >= CLICK && f <= CLICK + 6 ? 1 : 0;

  // 文字飞走:整行向右上飞出 + 加速
  const flyT = interpolate(f, [FLY, FLY + 12], [0, 1], {
    extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: Easing.in(Easing.cubic),
  });

  // 输入框 → 主胶囊 morph(几何插值,带一点弹性落定)
  const m = spring({ frame: f - MORPH, fps, config: { damping: 13, stiffness: 90, mass: 0.9 } });
  const bx = interpolate(m, [0, 1], [box0.x, FINAL.mainPill.x]);
  const by = interpolate(m, [0, 1], [box0.y, FINAL.mainPill.y - FINAL.mainPill.h / 2 + 60 - 60]);
  const bw = interpolate(m, [0, 1], [box0.w, FINAL.mainPill.w]);
  const bh = interpolate(m, [0, 1], [box0.h, FINAL.mainPill.h]);
  const br = interpolate(m, [0, 1], [box0.r, FINAL.mainPill.r]);
  const bColor = m; // 边框白框 → 实心暖白

  // 三粒元素依次从画外上方落下(spring 落定,带轻微过冲)
  const dropSpring = (i: number) =>
    spring({ frame: f - DROPS[i], fps, config: { damping: 12, stiffness: 110, mass: 0.85 } });

  // 落定呼吸:整瓣轻微 scale 脉动
  const breathe = f >= SETTLE ? 1 + 0.03 * Math.sin((f - SETTLE) * 0.18) : 1;

  const dropY = (finalY: number, s: number) => interpolate(s, [0, 1], [-260, finalY]);

  const s0 = dropSpring(0);
  const s1 = dropSpring(1);
  const s2 = dropSpring(2);

  const WHITE = '#fdf6ee';

  return (
    <AbsoluteFill style={{ background: BG, fontFamily: 'Helvetica, Arial, sans-serif', overflow: 'hidden' }}>
      <AbsoluteFill style={{ transform: `scale(${breathe})`, transformOrigin: `${CX}px ${CY - 40}px` }}>
        {/* 主体:输入框 → 主胶囊 */}
        <div
          style={{
            position: 'absolute',
            left: bx,
            top: by,
            width: bw,
            height: bh,
            borderRadius: br,
            border: `4px solid ${WHITE}`,
            background: `rgba(253,246,238,${bColor})`,
            boxSizing: 'border-box',
            display: 'flex',
            alignItems: 'center',
            padding: '0 34px',
            boxShadow: m > 0.6 ? '0 0 60px rgba(253,246,238,0.25)' : 'none',
          }}
        >
          {/* 一行文字(发送后飞走) */}
          <div
            style={{
              fontSize: 44,
              color: WHITE,
              whiteSpace: 'nowrap',
              opacity: (1 - flyT) * (1 - m),
              transform: `translate(${flyT * 700}px, ${-flyT * 380}px) rotate(${-flyT * 10}deg)`,
            }}
          >
            Ready, set, go!
            {/* 光标闪烁 */}
            <span style={{ opacity: Math.floor(f / 8) % 2 === 0 && f < FLY ? 1 : 0 }}>|</span>
          </div>
          {/* 发送键 */}
          <div
            style={{
              marginLeft: 'auto',
              width: 72,
              height: 72,
              borderRadius: 18,
              background: btnFlash ? '#ffffff' : 'rgba(253,246,238,0.9)',
              opacity: 1 - m,
              display: 'flex',
              alignItems: 'center',
              justifyContent: 'center',
              transform: `scale(${press})`,
              flexShrink: 0,
            }}
          >
            {/* 纸飞机三角 */}
            <svg width={34} height={34} viewBox="0 0 34 34">
              <path d="M3 17 L31 4 L20 30 L15 19 Z" fill={BG} />
            </svg>
          </div>
        </div>

        {/* 大圆(第一粒,落在主胶囊左端形成泪滴组合) */}
        {f >= DROPS[0] && (
          <div
            style={{
              position: 'absolute',
              left: FINAL.bigDot.x - FINAL.bigDot.d / 2,
              top: dropY(FINAL.bigDot.y - FINAL.bigDot.d / 2, s0),
              width: FINAL.bigDot.d,
              height: FINAL.bigDot.d,
              borderRadius: '50%',
              background: WHITE,
              boxShadow: '0 0 40px rgba(253,246,238,0.2)',
            }}
          />
        )}

        {/* 竖胶囊(第二粒) */}
        {f >= DROPS[1] && (
          <div
            style={{
              position: 'absolute',
              left: FINAL.vPill.x - FINAL.vPill.w / 2,
              top: dropY(FINAL.vPill.y - FINAL.vPill.h / 2, s1),
              width: FINAL.vPill.w,
              height: FINAL.vPill.h,
              borderRadius: FINAL.vPill.r,
              background: WHITE,
              boxShadow: '0 0 40px rgba(253,246,238,0.2)',
            }}
          />
        )}

        {/* 小圆(第三粒,压在竖胶囊顶端旁) */}
        {f >= DROPS[2] && (
          <div
            style={{
              position: 'absolute',
              left: FINAL.smallDot.x - FINAL.smallDot.d / 2,
              top: dropY(FINAL.smallDot.y - FINAL.smallDot.d / 2, s2),
              width: FINAL.smallDot.d,
              height: FINAL.smallDot.d,
              borderRadius: '50%',
              background: '#e8b84b',
              boxShadow: '0 0 40px rgba(232,184,75,0.35)',
            }}
          />
        )}
      </AbsoluteFill>

      {/* 光标 */}
      <div
        style={{
          position: 'absolute',
          left: cursorX,
          top: cursorY,
          opacity: cursorGone,
          transform: `scale(${press})`,
          zIndex: 50,
        }}
      >
        <svg width={40} height={44} viewBox="0 0 40 44">
          <path
            d="M4 2 L4 34 L13 26 L19 40 L26 37 L20 23 L32 22 Z"
            fill="#ffffff"
            stroke={BG}
            strokeWidth={2}
          />
        </svg>
      </div>
    </AbsoluteFill>
  );
};


  1. Read the recipe
    Prompt = when to use, intention, parameter table with typical values and how each one feels, known pitfalls. Treat values as calibrated starting points.
  2. Get the code
    tool: git
    git clone https://github.com/Vincentwei1021/video-shotcraft && cd video-shotcraft && npm install
  3. Register + render
    tool: remotion
    npx remotion render <CompositionId> out.mp4 # component: demos/outro/ui-to-brand-morph/IconFlipBloomLogo.tsx, demos/outro/ui-to-brand-morph/InputMorphsIntoLogo.tsx
  4. Or re-implement
    Port the timing/easing from the recipe to HTML + WAAPI (gimgs motion contract) or any engine; keep the same segment windows and easing.
paramtypedefaultdescription
—

Cùng danh mục