Motion · Tương tác

Viên kính đọc chính tả gõ chữ

Trên nền đen, một viên kính bật ra to hơn 1.25 lần rồi hạ dần đúng cỡ, ánh sáng nhấn quét bên trong; con trỏ đi trước, chữ giữ chỗ hiện ra khi gõ, ánh sáng tắt dần theo tiến độ, kết thúc thành thanh kính tối trung tính.

1920×108016:930 fps2.17s · 65 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 xuất hiện của ô nhập liệu giọng nói/AI; gợi ý tương tác kiểu "nói với nó"; một nhịp chuyển tiếp yên tĩnh giữa các đoạn năng lượng cao.

Ý đồ

Kể một câu chuyện nhỏ bằng "ánh sáng tắt dần theo nhập liệu": khi chờ nó phát sáng (chờ bạn nói), vừa mở lời nó liền nhường ánh sáng cho chữ. Trong 1.7s chỉ làm ba việc — bật ra, gõ chữ, ánh sáng lui. Bất kỳ chuyển động thừa nào (nảy, đổi màu, hoạt hình icon) đều khiến nhịp này không còn là dấu lặng.

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

  • Thời lượng: Khoảng 1.7s (50f@30fps)
  • Năng lượng: Thấp (nhịp yên tĩnh nhất toàn phim, chỉ có ánh sáng đang lui)

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)

意图

用"光随输入熄灭"讲一件很小的事:待命时它在发光(等你说话),你一开口 它就把光让给文字。1.7s 里只做三件事——弹出、打字、光退。任何额外动作 (弹跳、色变、图标动画)都会让这一拍不再是休止符。

动效核心

  • 胶囊高 46px、圆角 23px、padding: 0 8px 0 16px,定宽

PW = 文本实测宽 + 168px(≈整句宽度的 2 倍),不随打字伸缩

  • 出场 scale 1.25→1,seg(0→0.22, outCubic)(≈0.36s);

opacity 在前 0.025 就跳满——先"在了"再落位

  • 内嵌强调色光是一层 linear-gradient(90deg, α0 → α.35 @42% → α.95 @100%)

的左暗右亮渐层(光在胶囊内部,不是外部光晕)

  • 打字:光标 t≈0.025 先行出现,字符 seg(0.06→0.73) 匀速铺完 18 字符,

尾段保持;光标 0.75→0.8 撤掉

  • 光熄灭 g = 1 − seg(0.08→0.76, inOutQuad),与打字进度同窗口;

g 同时驱动三层阴影收敛:内描边 0.16+0.1g、内顶光 0.03+0.04g、外泛光 26g px / 0.28g

  • 右端 30px 描边圆角方块内 5 根竖条声波,基准高 [13,7,10,6,9]

(左高右低),叠 ±1.6·sin(t·18 + i·1.7) 极轻呼吸

参数表

| 参数 | 典型值 | 调节手感 | |------|--------|----------| | 胶囊定宽 | 文本实测宽 + 168px(≈句宽 2 倍) | 定宽是关键:随字伸缩会读作 chip 而不是输入条 | | 出场缩放 | scale 1.25→1,t 0→0.22 outCubic | 1.25 倍是"从观众面前落回去"的量;1.5 倍就成了 pop 弹窗 | | opacity 时序 | 前 0.025 跳满(早于缩放落位) | 同步淡入会让它像"渐渐出现",失掉那一下轻微的实感 | | 内嵌光 | 90deg、0 → .35@42% → .95@100% | 光必须在胶囊内部;改成外 glow 就成了"待通知"而不是"待输入" | | 打字 | 18 字符,t 0.06→0.73 匀速 | 匀速 ≈ 3f/字符;加 ease 就读作"有人在犹豫" | | 光熄灭 | 1 → 0,t 0.08→0.76 inOutQuad | 与打字同窗口是全片唯一的因果:光让位给文字 | | 阴影收敛 | 内描边 0.16+0.1g / 内顶光 0.03+0.04g / 外泛光 26g px | 三层都挂在同一个 g 上,收尾自然落到"中性深色玻璃条" | | 声波竖条 | 5 根、基准 [13,7,10,6,9]、±1.6px、频率 18 | 幅度到 4px 就成了"正在录音"的活跃态,与"待命"矛盾 | | 光标 | t≈0.025 先行、0.75→0.8 撤 | 光标先于文字出现,是"输入框已就绪"的信号 |

已知坑

  • 定宽靠一个 visibility:hidden 的测量节点算 offsetWidth,字体必须

与正式文本完全一致(400 21px + letter-spacing:.3px), 差一档就宽度不对

  • 声波竖条是持续呼吸(不受 t 分段控制),所以首尾帧的高度不同——

用作 loop 素材时要把频率 18 调成 2π 的整数倍

  • 打字进度 floor(seg(…)*len + 1e-6),那个 epsilon 是防止末字符在

t=0.73 整点因浮点误差不出现,别删

  • ACCENT_RGB(默认 146,126,212 紫)内嵌光与外泛光共用,

换项目品牌色只改这一个变量

  • 占位句 "Speak or type here"(18 字符);换句要保持字符数接近,

否则匀速打字的节奏和定宽比例都要重调

参考实现

demos/interaction/glass-pill-dictation-typing/ (GlassPillDictationTyping.tsx)

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

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

Prompt

Use the video-shotcraft shot recipe "Viên kính đọc chính tả gõ chữ" (Remotion composition, Apache-2.0) published on gimgs.net.
Recipe JSON (prompt = timing/easing/parameter table/pitfalls, files = TSX source): https://gimgs.net/motion/glass-pill-dictation-typing.json
Raw TSX: https://gimgs.net/motion/glass-pill-dictation-typing/template.html
Repository: https://github.com/Vincentwei1021/video-shotcraft  (component: demos/interaction/glass-pill-dictation-typing/GlassPillDictationTyping.tsx; imports demos/_fixtures and assets/lib)
Spec: 1920x1080, 30 fps, 2.17s

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

5,384 chars · Apache-2.0Mở template gốc ↗
// ===== demos/interaction/glass-pill-dictation-typing/GlassPillDictationTyping.tsx =====
// glass-pill-dictation-typing — Glass Pill Dictation 玻璃胶囊听写(motion-lab 定稿转原生 Remotion)
// 纯黑底上一条定宽玻璃胶囊:整条以约 1.25 倍略大弹出后缓落到位,胶囊内部自左暗到右亮
// 铺一层强调色光(ACCENT 变量,默认紫);光标先行出现,随后打字出现占位句
// 「Speak or type here」,光随打字进度渐渐熄灭,收尾成中性深色玻璃条;右端是描边圆角
// 方块里的竖条声波图标——全片最安静的一拍。
// 设计坐标 480×270(DesignStage 等比放大),参数表数值以此坐标系标定。
import React, { useLayoutEffect, useRef, useState } from 'react';
import { DesignStage, E, lerp, seg, useT } from '../../_fixtures/Motion';

export const GLASS_PILL_DICTATION_TYPING_DURATION = 50; // 1650ms @30fps

// 模板强调色:实际使用时按项目品牌色替换这一个变量(内嵌光 + 外泛光共用)
const ACCENT_RGB = '146,126,212';
const UI = '-apple-system,system-ui,"Segoe UI",sans-serif';
const PH = 46; // 胶囊高度
const ICON = 30; // 右端声波图标方块尺寸
const BASE = [13, 7, 10, 6, 9]; // 竖条基准高度(左高右低的听写图标)
const TEXT = 'Speak or type here'; // 占位句,长度贴近原片(19→18 字符),打字节奏不变

export const GlassPillDictationTyping: React.FC = () => {
  const t = useT();

  // 定宽:原片胶囊宽度约为整句文本宽度的 2 倍,不随词伸缩——挂载时实测一次整句宽度
  const measRef = useRef<HTMLDivElement>(null);
  const [textW, setTextW] = useState(180); // 兜底估算值,useLayoutEffect 实测后覆盖(先于首帧绘制)
  useLayoutEffect(() => {
    if (measRef.current) setTextW(measRef.current.offsetWidth);
  }, []);
  const PW = Math.round(textW + 168);

  // 出场:整体略大(~1.25x)快速浮现,约 0.45s 内缓落到位
  const s = lerp(seg(t, 0, 0.22, E.outCubic), 1.25, 1);
  // 打字:caret 先行(~t0.03),字符 t0.06→0.73 匀速铺完,尾段保持
  const n = Math.floor(seg(t, 0.06, 0.73) * TEXT.length + 1e-6);
  const caretO = seg(t, 0.025, 0.045) * (1 - seg(t, 0.75, 0.8));
  // 强调色光随打字进度渐熄;同步收掉描边亮度的一点富余
  const g = 1 - seg(t, 0.08, 0.76, E.inOutQuad);

  return (
    <DesignStage bg="#000">
      <div
        style={{
          position: 'absolute',
          inset: 0,
          background: '#000',
          overflow: 'hidden',
          // 暗部校色:原样片 x264 把外泛光的暗尾(1~2 级灰)压成纯 0,本渲染的
          // 泛光尾巴因此比样片亮一档、拖得更远。contrast(1.008) 等价于黑端减 ~1 级
          // (255*0.008/2≈1)、中间调不动,按对照帧实测校准(f1 0.9205→0.9844)。
          filter: 'contrast(1.008)',
        }}
      >
        {/* 居中走 translateX:PW 为奇数时圆心在 .5 半像素上,left/flex 会被布局
            取整偏 0.5 设计像素;transform 矩阵不吸附,能与原片圆心逐像素对齐 */}
        <div
          style={{
            position: 'absolute',
            left: 0,
            top: (270 - PH) / 2,
            height: PH,
            width: PW,
            borderRadius: PH / 2,
            boxSizing: 'border-box',
            display: 'flex',
            alignItems: 'center',
            padding: '0 8px 0 16px',
            overflow: 'hidden',
            background: '#0d0d13',
            willChange: 'transform,opacity',
            opacity: seg(t, 0, 0.025),
            transform: `translateX(${(480 - PW) / 2}px) scale(${s})`,
            boxShadow: `inset 0 0 0 1px rgba(255,255,255,${0.16 + 0.1 * g}), inset 0 14px 22px rgba(255,255,255,${0.03 + 0.04 * g}), 0 0 ${26 * g}px rgba(${ACCENT_RGB},${0.28 * g})`,
          }}
        >
          {/* 内嵌强调色光:左暗右亮的渐层,随打字进度熄灭(原片光在胶囊内部,无外部光晕) */}
          <div
            style={{
              position: 'absolute',
              inset: 0,
              pointerEvents: 'none',
              opacity: g,
              background: `linear-gradient(90deg,rgba(${ACCENT_RGB},0) 0%,rgba(${ACCENT_RGB},.35) 42%,rgba(${ACCENT_RGB},.95) 100%)`,
            }}
          />
          <div
            style={{
              position: 'relative',
              font: `400 21px ${UI}`,
              color: '#f4f4f5',
              whiteSpace: 'pre',
              letterSpacing: 0.3,
              flex: 'none',
            }}
          >
            {TEXT.slice(0, n)}
          </div>
          <div
            style={{
              position: 'relative',
              width: 2,
              height: 23,
              borderRadius: 1,
              background: '#eaeaec',
              marginLeft: 2,
              flex: 'none',
              opacity: caretO,
            }}
          />
          <div
            style={{
              position: 'relative',
              marginLeft: 'auto',
              width: ICON,
              height: ICON,
              borderRadius: 9,
              boxSizing: 'border-box',
              border: '1.5px solid rgba(255,255,255,.5)',
              background: 'rgba(255,255,255,.05)',
              display: 'flex',
              alignItems: 'center',
              justifyContent: 'center',
              flex: 'none',
            }}
          >
            {/* 声波竖条轻微呼吸(原片几乎静止,仅微动) */}
            <div style={{ display: 'flex', alignItems: 'center', gap: 2.2, height: '100%' }}>
              {BASE.map((h, i) => (
                <div
                  key={i}
                  style={{
                    width: 1.5,
                    borderRadius: 1.5,
                    background: '#ececee',
                    height: h + 1.6 * Math.sin(t * 18 + i * 1.7),
                  }}
                />
              ))}
            </div>
          </div>
        </div>
        {/* 隐藏测量条:与正文同字体同字距,量一次整句宽度以定胶囊定宽 */}
        <div
          ref={measRef}
          style={{
            position: 'absolute',
            visibility: 'hidden',
            whiteSpace: 'pre',
            font: `400 21px ${UI}`,
            letterSpacing: 0.3,
            top: -999,
          }}
        >
          {TEXT}
        </div>
      </div>
    </DesignStage>
  );
};


  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/glass-pill-dictation-typing/GlassPillDictationTyping.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