Motion · Tương tác

Luồng phản hồi AI

Bảng phản hồi AI đưa ra một câu tóm tắt dễ đọc trước, rồi các dòng bằng chứng kèm icon trạng thái lần lượt đổ vào, gom lại thành trạng thái hoàn tất.

1920×108016:930 fps5.06s · 152 frameremotionGiấy phép: Apache-2.0#shotcraft#remotion#interaction

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

Khi nào dùng

Cảnh tạo kết quả cho AI assistant/agent/search/copilot; nhấn mạnh "kết luận đến trước, bằng chứng theo sau, hoàn tất nhiệm vụ".

Ý đồ

Biến "AI đang làm việc" thành một chuỗi nhân quả dễ đọc, không phải màn cuộn log: người xem hiểu tóm tắt câu trả lời trước, sau đó thấy bằng chứng/tác vụ con lần lượt được bổ sung, cuối cùng trạng thái hoàn tất xác nhận vòng việc này đã xong.

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

  • Thời lượng: Khoảng 4–5s (120–150f, gồm ≥15f giữ hình ở trạng thái hoàn tất).
  • Năng lượng: Trung bình cao (thông tin liên tục tăng thêm, nhưng khả năng đọc ưu tiên hơn phô diễn tốc độ).

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)

意图

把“AI 正在工作”拍成可读的因果链,而不是日志刷屏:观众先看懂答案摘要, 再看到证据/子任务逐条补齐,最后由完成态确认这轮工作结束。

动效核心

  • 用目标产品真实响应面板截图做 backplate;动态区截图前隐藏,按 layout 坐标重叠回真实槽位(Q1/Q9)
  • 面板进入后摘要句先以短 wipe/淡入落定,落定后至少 12f 才起首条证据
  • 6–8 条证据行自下方 14–24px + 轻微 blur 逐行汇入;间隔逐渐收紧但保留可数节拍
  • 行体先停,状态图标晚 2–4f 由 pending 转 running/done,形成主次拖拽层级

(模式已沉为 assets/lib/helpers/motion.ts lagged)

  • 末条完成后只做一次面板级完成脉冲,随后全画面静止 ≥15f;不逐行 glint

参数表

| 参数 | 典型值 | 调节手感 | |------|--------|----------| | 摘要 | cue 18,10–14f reveal;摘要落定后 hold ≥12f,首行 cue ≥42 | 摘要与首行粘连会失去“结论先到”的叙事;正常速度必须能先读到一句完整结论 | | 行节拍 | cue[i]=42+[0,11,21,30,38,45,51,56][i],每行 10–14f | 间隔 11→5f 逐渐收紧,表达工作加速;不要密到所有行同时糊成一团(R2) | | 行入场 | y 18px→0,blur 6px→0,opacity 0→1,bezier(0.2,0.75,0.25,1) | 位移须过正常速度肉眼阈值;大幅横飞会像通知列表,不像证据归档 | | 状态图标 | 比行体 cue 晚 3f;pending 环 → running 缺口环 → done 实心勾,6–8f | 图标是状态回执,不抢行文本;同帧切换全部图标会读作假 loading | | 完成收束 | 末图标 done 后 6f,面板边框/摘要底色 opacity 0.25→0.55→0.25 共 10f | 只做面板级一次,不逐行发光(Q4);完成脉冲后留 ≥15f 真静止(R1) | | 相机 | 信息密集默认正视,zoom 1.04→1.0 微退;暗场斜拍仅作情绪变体 | Linear 参考片的斜拍不是通用命令;文字读不清时立即回正视(Q6) |

声音

摘要落定配一发 transition-soft;前 3–4 行用短 pop 轻声递减 0.32→0.20,后半加速密集段合并成一条低音量 whoosh-fast,避免逐行机枪感; 最终完成态用 click-camera 的轻量版本(约 vol 0.3)。声音在画面锁定后钉帧。

已知坑

  • 逐字符流式打字会把视线困在字符层;宣传片时长里优先按“语义块”揭示摘要,

真正展示输入操作时另用 type-and-filter

  • 行体和状态图标若同帧落定,会像静态列表整体切换;保留 2–4f 拖拽层级
  • 行终点必须是截图里真实列表槽位,不在面板上方悬浮;动态内容也必须用虚构数据
  • 此卡借鉴 Linear Agent 发布片 t≈13–17s(进化轮 #3 breakdown #6),非用户判例;

参数未经本库实战项目验证,首次使用必须按 P1 在正常速度下看片,不以逐帧 diff 代替体感

参考实现

demos/interaction/ai-stream-response/ (StreamResponse.tsx)

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

  • demos/interaction/ai-stream-response/StreamResponse.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/interaction/ai-stream-response/StreamResponse.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/ai-stream-response.json
git clone https://github.com/Vincentwei1021/video-shotcraft
# component: demos/interaction/ai-stream-response/StreamResponse.tsx
npx remotion render <CompositionId> out.mp4
claude mcp add --transport http gimgs https://gimgs.net/mcp
# then: search_motion_templates / get_motion_template("ai-stream-response")

Prompt

Use the video-shotcraft shot recipe "Luồng phản hồi AI" (Remotion composition, Apache-2.0) published on gimgs.net.
Recipe JSON (prompt = timing/easing/parameter table/pitfalls, files = TSX source): https://gimgs.net/motion/ai-stream-response.json
Raw TSX: https://gimgs.net/motion/ai-stream-response/template.html
Repository: https://github.com/Vincentwei1021/video-shotcraft  (component: demos/interaction/ai-stream-response/StreamResponse.tsx; imports demos/_fixtures and assets/lib)
Spec: 1920x1080, 30 fps, 5.06s

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

7,572 chars · Apache-2.0Mở template gốc ↗
// ===== demos/interaction/ai-stream-response/StreamResponse.tsx =====
import React from 'react';
import {
  AbsoluteFill,
  Easing,
  Img,
  interpolate,
  useCurrentFrame,
} from 'remotion';
import backplate from './agent-stream.jpg';

const ROW_CUES = [42, 53, 63, 72, 80, 87, 93];
const ROWS = [
  ['Indexed the workspace', '128 files'],
  ['Mapped the authentication flow', '12 modules'],
  ['Checked recent error traces', 'No blockers'],
  ['Matched API contracts to handlers', '24 routes'],
  ['Verified permission boundaries', '6 roles'],
  ['Cross-checked release notes', '3 changes'],
  ['Prepared an implementation plan', 'Ready'],
] as const;

const clamp = {extrapolateLeft: 'clamp', extrapolateRight: 'clamp'} as const;
const ease = Easing.bezier(0.2, 0.75, 0.25, 1);

const CheckIcon: React.FC<{progress: number}> = ({progress}) => {
  const ringOpacity = interpolate(progress, [0, 0.45, 0.75], [0.36, 1, 0], clamp);
  const checkOpacity = interpolate(progress, [0.62, 1], [0, 1], clamp);
  const checkScale = interpolate(progress, [0.62, 1], [0.72, 1], {...clamp, easing: ease});

  return (
    <div style={{position: 'relative', width: 27, height: 27, flex: '0 0 auto'}}>
      <div
        style={{
          position: 'absolute', inset: 2, borderRadius: 99,
          border: '2px solid rgba(183, 190, 199, .6)',
          borderTopColor: '#d4ff77', opacity: ringOpacity,
          transform: `rotate(${progress * 100}deg)`,
        }}
      />
      <div
        style={{
          position: 'absolute', inset: 1, borderRadius: 99,
          background: '#b8f36a', opacity: checkOpacity,
          transform: `scale(${checkScale})`,
          display: 'grid', placeItems: 'center',
          boxShadow: '0 0 16px rgba(184,243,106,.18)',
        }}
      >
        <svg width="16" height="16" viewBox="0 0 16 16" fill="none">
          <path d="M3.4 8.3 6.6 11l6-6" stroke="#15200d" strokeWidth="2.1" strokeLinecap="round" strokeLinejoin="round"/>
        </svg>
      </div>
    </div>
  );
};

const EvidenceRow: React.FC<{cue: number; title: string; meta: string; index: number}> = ({cue, title, meta, index}) => {
  const frame = useCurrentFrame();
  const body = interpolate(frame, [cue, cue + 12], [0, 1], {...clamp, easing: ease});
  const status = interpolate(frame, [cue + 3, cue + 11], [0, 1], {...clamp, easing: ease});
  return (
    <div
      style={{
        position: 'absolute', left: 0, right: 0, top: index * 68,
        height: 58, borderRadius: 13,
        border: '1px solid rgba(255,255,255,.065)',
        background: 'rgba(255,255,255,.025)',
        display: 'flex', alignItems: 'center', padding: '0 20px', gap: 15,
        opacity: body,
        transform: `translateY(${18 * (1 - body)}px)`,
        filter: `blur(${6 * (1 - body)}px)`,
      }}
    >
      <CheckIcon progress={status}/>
      <div style={{fontSize: 21, color: '#e8eaed', letterSpacing: '-0.015em', flex: 1}}>{title}</div>
      <div style={{fontSize: 16, color: status > 0.78 ? '#a9c77f' : '#777f89', fontVariantNumeric: 'tabular-nums'}}>{meta}</div>
    </div>
  );
};

export const StreamResponse: React.FC = () => {
  const frame = useCurrentFrame();
  const panelIn = interpolate(frame, [0, 16], [0, 1], {...clamp, easing: ease});
  const summary = interpolate(frame, [18, 30], [0, 1], {...clamp, easing: ease});
  const camera = interpolate(frame, [0, 100], [1.04, 1], {...clamp, easing: ease});
  const pulse = interpolate(frame, [110, 115, 120], [0.25, 0.55, 0.25], clamp);
  const complete = interpolate(frame, [110, 120], [0, 1], {...clamp, easing: ease});

  return (
    <AbsoluteFill style={{background: '#07090a', fontFamily: 'Inter, ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif', overflow: 'hidden'}}>
      <AbsoluteFill style={{transform: `scale(${camera})`}}>
        <Img src={backplate} style={{width: '100%', height: '100%', objectFit: 'cover', filter: 'brightness(.58) saturate(.76) blur(1px)', opacity: .7}}/>
        <AbsoluteFill style={{background: 'radial-gradient(circle at 50% 44%, rgba(6,9,10,.08) 10%, rgba(4,6,7,.72) 92%)'}}/>
      </AbsoluteFill>

      <div
        style={{
          position: 'absolute', left: 344, top: 72, width: 1232, height: 936,
          borderRadius: 29, overflow: 'hidden',
          background: 'linear-gradient(145deg, rgba(25,29,32,.985), rgba(12,15,17,.99))',
          border: `1px solid rgba(184,243,106,${pulse})`,
          boxShadow: '0 46px 100px rgba(0,0,0,.58), inset 0 1px 0 rgba(255,255,255,.06)',
          opacity: panelIn,
          transform: `translateY(${12 * (1 - panelIn)}px) scale(${.985 + .015 * panelIn})`,
        }}
      >
        <div style={{height: 76, display: 'flex', alignItems: 'center', padding: '0 34px', borderBottom: '1px solid rgba(255,255,255,.065)'}}>
          <div style={{width: 29, height: 29, borderRadius: 9, background: '#e9edf0', color: '#15191b', display: 'grid', placeItems: 'center', fontWeight: 800, fontSize: 17}}>A</div>
          <div style={{marginLeft: 13, fontSize: 22, fontWeight: 590, color: '#f1f3f4'}}>Ask Atlas</div>
          <div style={{marginLeft: 'auto', display: 'flex', alignItems: 'center', gap: 9, color: '#7d858e', fontSize: 15}}>
            <span style={{width: 7, height: 7, background: '#b8f36a', borderRadius: 99}}/> Workspace agent
          </div>
        </div>

        <div style={{padding: '28px 38px 34px'}}>
          <div style={{height: 48, borderRadius: 12, background: 'rgba(255,255,255,.048)', display: 'flex', alignItems: 'center', padding: '0 18px', color: '#b9bec4', fontSize: 18}}>
            Review this codebase and identify the safest implementation path
          </div>

          <div style={{height: 119, marginTop: 21, borderBottom: '1px solid rgba(255,255,255,.07)', overflow: 'hidden', position: 'relative'}}>
            <div style={{position: 'absolute', inset: 0, opacity: summary, transform: `translateY(${10 * (1 - summary)}px)`, clipPath: `inset(0 ${100 * (1 - summary)}% 0 0)`}}>
              <div style={{color: '#838b94', fontSize: 15, textTransform: 'uppercase', letterSpacing: '.12em', marginBottom: 10}}>Result summary</div>
              <div style={{color: '#f3f4f5', fontSize: 26, fontWeight: 510, lineHeight: 1.35, letterSpacing: '-.022em'}}>
                The codebase is ready for a focused, low-risk implementation.
              </div>
            </div>
          </div>

          <div style={{position: 'relative', height: 466, marginTop: 17}}>
            {ROWS.map(([title, meta], index) => <EvidenceRow key={title} cue={ROW_CUES[index]} title={title} meta={meta} index={index}/>)}
          </div>

          <div style={{height: 60, marginTop: 2, borderTop: '1px solid rgba(255,255,255,.07)', display: 'flex', alignItems: 'flex-end'}}>
            <div style={{display: 'flex', alignItems: 'center', gap: 11, opacity: complete, transform: `translateY(${5 * (1 - complete)}px)`}}>
              <div style={{width: 24, height: 24, borderRadius: 99, background: '#b8f36a', display: 'grid', placeItems: 'center'}}>
                <svg width="14" height="14" viewBox="0 0 16 16" fill="none"><path d="M3.4 8.3 6.6 11l6-6" stroke="#15200d" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"/></svg>
              </div>
              <span style={{color: '#dce7d0', fontSize: 17, fontWeight: 560}}>Analysis complete</span>
              <span style={{color: '#69717a', fontSize: 15}}>7 checks finished · ready to build</span>
            </div>
          </div>
        </div>
      </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/interaction/ai-stream-response/StreamResponse.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