webconsulting-create-documentation

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

webconsulting — Create Documentation

webconsulting — 文档创建方案

End-to-end workflow for building product documentation: help pages, AI-generated illustrations, narrated product tour videos (Remotion + ElevenLabs TTS + Suno AI music), and GitHub README visual documentation.
构建产品文档的端到端工作流:帮助页面、AI生成插图、带旁白的产品导览视频(Remotion + ElevenLabs TTS + Suno AI音乐),以及GitHub README可视化文档。

Prerequisites

前置条件

Required

必需依赖

DependencyInstallPurpose
Node.js 20+Pre-installedRuntime
Remotion
npm install remotion @remotion/cli @remotion/player
Video composition
qrcode.react
npm install qrcode.react
QR codes in end card
tsx
npm install -D tsx
Run TypeScript scripts
依赖项安装方式用途
Node.js 20+预先安装运行时环境
Remotion
npm install remotion @remotion/cli @remotion/player
视频合成
qrcode.react
npm install qrcode.react
末尾卡片中的二维码生成
tsx
npm install -D tsx
运行TypeScript脚本

Optional (for production quality)

可选依赖(用于生产级质量)

DependencyInstallPurpose
ElevenLabs API keyelevenlabs.ioPremium TTS narration (Jony Ive voice)
ffmpeg
brew install ffmpeg
Convert audio to MP3 (smaller files)
macOS
say
+
afconvert
Built-in on macOSFallback TTS (Daniel voice, outputs WAV)
Suno API keyapi.boxAI-generated background music
依赖项安装方式用途
ElevenLabs API密钥elevenlabs.io高级TTS旁白(如Jony Ive风格语音)
ffmpeg
brew install ffmpeg
将音频转换为MP3(减小文件体积)
macOS
say
+
afconvert
macOS系统内置备用TTS(Daniel语音,输出WAV格式)
Suno API密钥api.boxAI生成背景音乐

ElevenLabs Setup

ElevenLabs 配置

  1. Create a free account at elevenlabs.io
  2. Navigate to Profile + API key in the sidebar
  3. Copy your API key
  4. Add to your
    .env
    file:
bash
ELEVENLABS_API_KEY=your-api-key-here
Also add to
.env.example
(commented out) for team documentation:
bash
undefined
  1. elevenlabs.io创建免费账号
  2. 在侧边栏进入Profile + API key页面
  3. 复制你的API密钥
  4. 添加到
    .env
    文件中:
bash
ELEVENLABS_API_KEY=your-api-key-here
同时将其添加到
.env.example
文件中(注释状态),用于团队文档共享:
bash
undefined

ELEVENLABS_API_KEY=your-elevenlabs-api-key

ELEVENLABS_API_KEY=your-elevenlabs-api-key


**Voice selection**: The default voice is "Daniel" (`onwK4e9ZLuTAKqWW03F9`) — British, calm.
Browse [elevenlabs.io/voice-library](https://elevenlabs.io/voice-library) and update
the `VOICE_ID` in `scripts/generate-narration.ts` if a better match is found.

**Free tier limits**: ~10,000 characters/month. The full narration script is ~600 characters,
so you can regenerate ~16 times per month on the free tier.

**语音选择**:默认语音为"Daniel"(ID:`onwK4e9ZLuTAKqWW03F9`)——英式口音,语调沉稳。可以浏览[elevenlabs.io/voice-library](https://elevenlabs.io/voice-library),如果找到更匹配的语音,更新`scripts/generate-narration.ts`中的`VOICE_ID`即可。

**免费版限制**:每月约10,000字符额度。完整旁白脚本约600字符,因此免费版每月可重新生成约16次。

Workflow Overview

工作流概览

1. Create help page       → React component with collapsible sections
2. Generate illustrations → AI image generation tool (GenerateImage)
3. Build Remotion video   → Animated scenes with narration subtitles
4. Generate TTS audio     → ElevenLabs API or macOS `say` fallback
5. Generate music         → Suno AI background music (optional)
6. Render final video     → npm run remotion:render
7. GitHub README          → Embed screenshots + video in README.md
1. 创建帮助页面       → 基于React组件实现可折叠章节
2. 生成插图         → 使用AI图像生成工具(GenerateImage)
3. 构建Remotion视频   → 包含旁白字幕的动画场景
4. 生成TTS音频     → 使用ElevenLabs API或macOS `say`备用方案
5. 生成背景音乐     → 使用Suno AI生成背景音乐(可选)
6. 渲染最终视频     → 执行npm run remotion:render
7. 完善GitHub README          → 在README.md中嵌入截图和视频

Phase 1: Help Page

阶段1:帮助页面

Create an in-app help page at
src/app/(dashboard)/help/page.tsx
.
src/app/(dashboard)/help/page.tsx
路径创建应用内帮助页面。

Structure pattern

结构模式

tsx
// Collapsible section wrapper
function HelpSection({ id, icon, title, description, children, defaultOpen }) {
  const [open, setOpen] = useState(defaultOpen);
  return (
    <Card id={id}>
      <button onClick={() => setOpen(o => !o)}>
        {/* Header with icon, title, description, chevron */}
      </button>
      {open && <CardContent>{children}</CardContent>}
    </Card>
  );
}

// Step-by-step instruction
function Step({ number, title, children }) { /* numbered step */ }

// Pro tip callout
function Tip({ children }) { /* highlighted tip box */ }

// Screenshot placeholder
function Screenshot({ src, alt, caption }) { /* Next/Image with caption */ }
tsx
// 可折叠章节封装组件
function HelpSection({ id, icon, title, description, children, defaultOpen }) {
  const [open, setOpen] = useState(defaultOpen);
  return (
    <Card id={id}>
      <button onClick={() => setOpen(o => !o)}>
        {/* 包含图标、标题、描述和展开/收起箭头的头部 */}
      </button>
      {open && <CardContent>{children}</CardContent>}
    </Card>
  );
}

// 分步说明组件
function Step({ number, title, children }) { /* 带编号的步骤 */ }

// 提示框组件
function Tip({ children }) { /* 高亮显示的提示框 */ }

// 截图占位组件
function Screenshot({ src, alt, caption }) { /* 带标题的Next/Image组件 */ }

Key sections to include

关键章节内容

  1. Table of Contents — auto-generated from section data with anchor links
  2. Getting Started — first-time user orientation (default open)
  3. Feature sections — one per major feature (Dashboard, Clients, Extensions, etc.)
  4. Product Video — embedded
    <video>
    element pointing to rendered MP4
  5. FAQ / Troubleshooting — common questions
  1. 目录 — 从章节数据自动生成,包含锚点链接
  2. 快速入门 — 面向首次用户的引导内容(默认展开)
  3. 功能章节 — 每个主要功能对应一个章节(如仪表盘、客户管理、扩展功能等)
  4. 产品视频 — 嵌入指向渲染完成的MP4文件的
    <video>
    元素
  5. 常见问题/故障排除 — 汇总用户常见问题

Writing guidelines

写作指南

  • Target audience: users with low technical knowledge
  • Write as a senior technical writer — clear, friendly, no jargon
  • Every section has numbered steps with screenshots
  • Include "Tip" callouts for pro tips
  • Use Tailwind v4 syntax (
    mt-3!
    not
    !mt-3
    )
  • 目标受众:技术水平较低的用户
  • 写作风格:以资深技术文档作者的视角,内容清晰、友好,无专业黑话
  • 每个章节都包含带截图的编号步骤
  • 加入"提示"框提供专业技巧
  • 使用Tailwind v4语法(如
    mt-3!
    而非
    !mt-3

Phase 2: Screenshots / Illustrations

阶段2:截图/插图

Since browser automation for screenshots can be unreliable, use AI-generated illustrations.
由于浏览器自动化截图的可靠性较低,建议使用AI生成插图。

Using the GenerateImage tool

使用GenerateImage工具

GenerateImage({
  description: "Modern flat illustration of a monitoring dashboard with
    stat cards showing Total Clients, Extensions, Security Issues.
    Dark theme, clean UI, subtle orange accents.",
  filename: "dashboard.png"
})
Place generated images in
public/help/
:
public/help/
├── dashboard.png
├── clients.png
├── wizard.png
└── product-tour.mp4   (rendered later)
Reference in the help page:
tsx
<Screenshot src="/help/dashboard.png" alt="Dashboard overview" caption="The main dashboard" />
GenerateImage({
  description: "Modern flat illustration of a monitoring dashboard with
    stat cards showing Total Clients, Extensions, Security Issues.
    Dark theme, clean UI, subtle orange accents.",
  filename: "dashboard.png"
})
将生成的图片放置在
public/help/
目录下:
public/help/
├── dashboard.png
├── clients.png
├── wizard.png
└── product-tour.mp4   (后续渲染生成)
在帮助页面中引用:
tsx
<Screenshot src="/help/dashboard.png" alt="Dashboard overview" caption="The main dashboard" />

Phase 3: Remotion Product Video

阶段3:Remotion产品视频

Directory structure

目录结构

remotion/
├── index.ts          # registerRoot entry point
├── Root.tsx          # Composition definitions
└── ProductTour.tsx   # All scenes + narration script
remotion/
├── index.ts          # registerRoot入口文件
├── Root.tsx          # 合成定义
└── ProductTour.tsx   # 所有场景和旁白脚本

Composition setup (
Root.tsx
)

合成配置(
Root.tsx

tsx
import { Composition } from "remotion";
import { ProductTour } from "./ProductTour";

export const RemotionRoot = () => (
  <Composition
    id="ProductTour"
    component={ProductTour}
    durationInFrames={30 * 45}  // 45 seconds at 30fps
    fps={30}
    width={1920}
    height={1080}
  />
);
tsx
import { Composition } from "remotion";
import { ProductTour } from "./ProductTour";

export const RemotionRoot = () => (
  <Composition
    id="ProductTour"
    component={ProductTour}
    durationInFrames={30 * 45}  // 30fps下时长45秒
    fps={30}
    width={1920}
    height={1080}
  />
);

Scene pattern

场景模式

Each scene follows this structure:
tsx
function SceneComponent() {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();

  return (
    <AbsoluteFill style={{ backgroundColor: DARK_BG, padding: 80 }}>
      <SceneAudio scene="scene-name" />           {/* TTS audio */}
      <FadeInText fontSize={56}>Title</FadeInText> {/* Animated heading */}
      {/* Scene-specific content with spring/interpolate animations */}
      <NarrationSubtitle text={NARRATION_SCRIPT.sceneName} />  {/* Subtitles */}
    </AbsoluteFill>
  );
}
每个场景遵循以下结构:
tsx
function SceneComponent() {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();

  return (
    <AbsoluteFill style={{ backgroundColor: DARK_BG, padding: 80 }}>
      <SceneAudio scene="scene-name" />           {/* TTS音频 */}
      <FadeInText fontSize={56}>Title</FadeInText> {/* 动画标题 */}
      {/* 场景特定内容,包含弹簧/插值动画 */}
      <NarrationSubtitle text={NARRATION_SCRIPT.sceneName} />  {/* 字幕 */}
    </AbsoluteFill>
  );
}

Recommended scenes (7 total, ~75 seconds)

推荐场景(共7个,约75秒)

Durations are dynamic — driven by narration audio length (see Phase 5). The video includes a 1.5s lead-in (dark + exponential volume ramp) and a 4s end card with social links that fades to black.
#Scene~DurationContent
Lead-in1.5sDark screen, music fades in (exponential t^2.5)
1Intro13sLogo + tagline + brand identity
2Dashboard12sStat cards with spring animations
3Clients12sClient list with slide-in rows
4Wizard11sStep indicator with progressive activation
5Security11sFeature grid with staggered fade-in
6Outro11sLogo + CTA + credits + website (highlight blog)
7End Card7s4 QR codes (website, GitHub, YouTube, X) + scale-up finale
时长为动态值 — 由旁白音频长度决定(见阶段5)。视频包含1.5秒的开场过渡(黑屏+指数级音量渐变)和4秒的末尾卡片(包含社交链接,渐变为黑屏)。
序号场景预估时长内容
开场过渡1.5秒黑屏,背景音乐淡入(指数曲线t^2.5)
1介绍13秒Logo+标语+品牌标识
2仪表盘12秒带弹簧动画的统计卡片
3客户管理12秒带滑入动画的客户列表
4向导流程11秒逐步激活的步骤指示器
5安全功能11秒带交错淡入动画的功能网格
6结尾11秒Logo+行动号召+致谢+官网链接(突出博客板块)
7末尾卡片7秒4个二维码(官网、GitHub、YouTube、X)+ 缩放收尾动画

Lead-in and end card

开场过渡与末尾卡片

The lead-in uses an exponential volume curve (
Math.pow(t, 2.5)
) because human hearing follows a logarithmic scale (Weber-Fechner law). A linear ramp sounds "sudden" in the middle; the power curve feels perceptually smooth.
The end card shows 4 QR codes with social/website links (staggered slide-up). No fade-to-black — the QR codes stay fully visible so viewers can scan them. In the final 2 seconds, the entire card scales up dramatically (1.0 → 1.5) with the T3 Monitoring logo, creating a confident, cinematic close. The "Find us on" heading scales up in sync but also moves upward (-120px) so it doesn't get covered by the expanding QR code row.
QR codes are rendered inline using
qrcode.react
(
npm install qrcode.react
).
tsx
export const LEAD_IN_FRAMES = Math.round(30 * 1.5); // 45 frames
export const END_CARD_FRAMES = Math.round(30 * 7);   // 210 frames
These are added to
TOTAL_FRAMES
in
Root.tsx
for the composition duration.
开场过渡使用指数音量曲线
Math.pow(t, 2.5)
),因为人类听觉遵循对数规律(韦伯-费希纳定律)。线性渐变会让中间部分听起来“突兀”,而幂曲线能带来感知上的平滑过渡。
末尾卡片展示4个社交/官网链接的二维码(交错滑入)。无渐黑效果 — 二维码保持完全可见,方便观众扫码。在最后2秒,整个卡片从1.0放大到1.5,同时T3 Monitoring Logo同步缩放,营造自信、电影感的收尾效果。“关注我们”标题同步放大并向上移动(-120px),避免被扩展的二维码行遮挡。
二维码使用
qrcode.react
npm install qrcode.react
)内联渲染。
tsx
export const LEAD_IN_FRAMES = Math.round(30 * 1.5); // 45帧
export const END_CARD_FRAMES = Math.round(30 * 7);   // 210帧
这些值会添加到
Root.tsx
TOTAL_FRAMES
中,用于合成时长计算。

End card links

末尾卡片链接

#PlatformHandleHighlight
1Websitewebconsulting.atOrange border,
/blog
callout with description
2GitHub@dirnbauer
3YouTube@webconsulting-curt
4X@KDirnbauer
The website card is emphasized: orange handle text, orange QR border, and a
/blog
pill badge below — glass-style container with orange border, glow shadow, and "Tutorials & deep dives" label.
序号平台账号突出样式
1官网webconsulting.at橙色边框,显示
/blog
标注及描述
2GitHub@dirnbauer
3YouTube@webconsulting-curt
4X@KDirnbauer
官网卡片会被重点突出:橙色账号文本、橙色二维码边框,下方带有
/blog
标签 — 玻璃态容器+橙色边框+发光阴影+“教程与深度解析”说明。

QR code pattern

二维码模式

tsx
import { QRCodeSVG } from "qrcode.react";

<QRCodeSVG value="https://webconsulting.at" size={140}
  bgColor="#ffffff" fgColor="#0f172a" level="M" />
Each social card shows: platform icon + handle, QR code (white bg, dark fg, rounded container), URL text, and optional highlight callout.
tsx
import { QRCodeSVG } from "qrcode.react";

<QRCodeSVG value="https://webconsulting.at" size={140}
  bgColor="#ffffff" fgColor="#0f172a" level="M" />
每个社交卡片包含:平台图标+账号、二维码(白色背景、深色前景、圆角容器)、URL文本,以及可选的突出标注。

Animation toolkit

动画工具集

tsx
// Fade + slide up
const opacity = interpolate(frame - delay, [0, fps * 0.6], [0, 1], {
  extrapolateLeft: "clamp", extrapolateRight: "clamp",
});

// Spring bounce for cards
const scale = spring({ fps, frame: frame - i * 8, config: { damping: 80 } });

// Staggered slide-in for list items
const slideX = interpolate(frame - i * 10, [0, fps * 0.5], [100, 0], {
  extrapolateLeft: "clamp", extrapolateRight: "clamp",
});
tsx
// 淡入+上滑
const opacity = interpolate(frame - delay, [0, fps * 0.6], [0, 1], {
  extrapolateLeft: "clamp", extrapolateRight: "clamp",
});

// 卡片弹簧弹跳效果
const scale = spring({ fps, frame: frame - i * 8, config: { damping: 80 } });

// 列表项交错滑入
const slideX = interpolate(frame - i * 10, [0, fps * 0.5], [100, 0], {
  extrapolateLeft: "clamp", extrapolateRight: "clamp",
});

Phase 4: Narration (Jony Ive Style)

阶段4:旁白(Jony Ive风格)

Voice characteristics

语音特征

  • Tone: Calm, deliberate, aspirational
  • Pace: ~140 words per minute (slow, measured)
  • Style: Short declarative sentences. Dramatic pauses. Marketing-punchy.
  • Influence: Sir Jonathan Ive — think Apple product launch presentations
  • 语调:沉稳、从容、有感染力
  • 语速:约140词/分钟(缓慢、有节奏感)
  • 风格:短句、陈述句。带有戏剧性停顿。营销感十足。
  • 参考风格:乔纳森·伊夫爵士 — 参考苹果产品发布会的旁白风格

Writing narration copy

旁白文案写作

BAD:  "This is a dashboard that shows you statistics about your clients."
GOOD: "Your dashboard. Beautifully considered. Every metric, thoughtfully
       placed, to give you immediate clarity."
反面示例:  "This is a dashboard that shows you statistics about your clients."
正面示例: "Your dashboard. Beautifully considered. Every metric, thoughtfully
       placed, to give you immediate clarity."

Export the narration script (single source of truth)

导出旁白脚本(单一数据源)

Define all narration text in
ProductTour.tsx
as an exported constant. This is the single source of truth for both subtitles and audio generation:
tsx
// remotion/ProductTour.tsx
export const NARRATION_SCRIPT: Record<string, string> = {
  intro: "Imagine seeing every TYPO3 installation you manage...",
  dashboard: "Your dashboard gives you the full picture...",
  // ... one entry per scene
};
Critical: The generate script imports this constant — never duplicate the text:
tsx
// scripts/generate-narration.ts
import { NARRATION_SCRIPT } from "../remotion/ProductTour.js";
const NARRATION = NARRATION_SCRIPT; // Same text for audio + subtitles
This guarantees spoken audio always matches the on-screen subtitles. If you edit
NARRATION_SCRIPT
, just re-run
npm run narration:generate
and
npm run remotion:render
— both audio and subtitles update automatically.
ProductTour.tsx
中定义所有旁白文本为导出常量。这是字幕和音频生成的单一数据源:
tsx
// remotion/ProductTour.tsx
export const NARRATION_SCRIPT: Record<string, string> = {
  intro: "Imagine seeing every TYPO3 installation you manage...",
  dashboard: "Your dashboard gives you the full picture...",
  // ...每个场景对应一条文案
};
关键注意事项:生成脚本必须导入该常量 — 绝对不要重复编写文本:
tsx
// scripts/generate-narration.ts
import { NARRATION_SCRIPT } from "../remotion/ProductTour.js";
const NARRATION = NARRATION_SCRIPT; // 音频和字幕使用相同文本
这能保证语音旁白与屏幕字幕始终保持一致。如果修改
NARRATION_SCRIPT
,只需重新运行
npm run narration:generate
npm run remotion:render
— 音频和字幕会自动更新。

NarrationSubtitle component

NarrationSubtitle组件

Cinematic lower-third with word-by-word reveal, slide-up entrance, and active-word glow. Always reads from
NARRATION_SCRIPT
— update the script and subtitles auto-sync.
tsx
function NarrationSubtitle({ text }: { text: string }) {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();

  // Slide-up + fade entrance (0.4s → 0.9s)
  const fadeIn = interpolate(frame, [fps * 0.4, fps * 0.9], [0, 1], { ... });
  const slideUp = interpolate(frame, [fps * 0.4, fps * 0.9], [20, 0], { ... });

  const words = text.split(" ");
  const framesPerWord = fps / 2.8; // calm pace, ~2.8 words/sec

  return (
    <div style={{ position: "absolute", bottom: 60, opacity: fadeIn,
                  transform: `translateY(${slideUp}px)` }}>
      <div style={{
        maxWidth: 1100, padding: "24px 56px",
        background: "linear-gradient(135deg, rgba(0,0,0,0.72), rgba(20,20,30,0.68))",
        borderRadius: 20, border: "1px solid rgba(255,255,255,0.08)",
        boxShadow: "0 8px 32px rgba(0,0,0,0.4), 0 0 80px rgba(255,120,50,0.04)",
      }}>
        {/* Subtle top accent line */}
        <div style={{ position: "absolute", top: 0, left: 40, right: 40, height: 1,
          background: "linear-gradient(90deg, transparent, rgba(255,255,255,0.15), transparent)" }} />
        {words.map((word, i) => {
          const wordDelay = fps * 0.5 + i * framesPerWord;
          const wordOpacity = interpolate(frame, [wordDelay, wordDelay + framesPerWord * 0.5],
            [0.25, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });
          const isActive = frame >= wordDelay && frame < wordDelay + framesPerWord * 1.2;
          return (
            <span key={i} style={{
              fontSize: 30, fontWeight: 300, color: "#fff",
              fontFamily: "'SF Pro Display', 'Inter', system-ui",
              opacity: isActive ? 1 : wordOpacity, letterSpacing: "0.02em",
              textShadow: isActive ? "0 0 20px rgba(255,255,255,0.15)" : "none",
            }}>{word}</span>
          );
        })}
      </div>
    </div>
  );
}
电影感的下方字幕条,支持逐词显示、滑入入场动画和当前词高亮效果。始终从
NARRATION_SCRIPT
读取文本
— 修改脚本后,字幕会自动同步更新。
tsx
function NarrationSubtitle({ text }: { text: string }) {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();

  // 滑入+淡入入场动画(0.4秒→0.9秒)
  const fadeIn = interpolate(frame, [fps * 0.4, fps * 0.9], [0, 1], { ... });
  const slideUp = interpolate(frame, [fps * 0.4, fps * 0.9], [20, 0], { ... });

  const words = text.split(" ");
  const framesPerWord = fps / 2.8; // 沉稳语速,约2.8词/秒

  return (
    <div style={{ position: "absolute", bottom: 60, opacity: fadeIn,
                  transform: `translateY(${slideUp}px)` }}>
      <div style={{
        maxWidth: 1100, padding: "24px 56px",
        background: "linear-gradient(135deg, rgba(0,0,0,0.72), rgba(20,20,30,0.68))",
        borderRadius: 20, border: "1px solid rgba(255,255,255,0.08)",
        boxShadow: "0 8px 32px rgba(0,0,0,0.4), 0 0 80px rgba(255,120,50,0.04)",
      }}>
        {/* 顶部细微装饰线 */}
        <div style={{ position: "absolute", top: 0, left: 40, right: 40, height: 1,
          background: "linear-gradient(90deg, transparent, rgba(255,255,255,0.15), transparent)" }} />
        {words.map((word, i) => {
          const wordDelay = fps * 0.5 + i * framesPerWord;
          const wordOpacity = interpolate(frame, [wordDelay, wordDelay + framesPerWord * 0.5],
            [0.25, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });
          const isActive = frame >= wordDelay && frame < wordDelay + framesPerWord * 1.2;
          return (
            <span key={i} style={{
              fontSize: 30, fontWeight: 300, color: "#fff",
              fontFamily: "'SF Pro Display', 'Inter', system-ui",
              opacity: isActive ? 1 : wordOpacity, letterSpacing: "0.02em",
              textShadow: isActive ? "0 0 20px rgba(255,255,255,0.15)" : "none",
            }}>{word}</span>
          );
        })}
      </div>
    </div>
  );
}

Subtitle styling principles

字幕设计原则

  • Auto-sync: Component reads
    NARRATION_SCRIPT[scene]
    directly — change the script text and the subtitles update automatically on next render
  • Slide-up entrance: Subtitles slide up 20px while fading in (0.4s–0.9s), feels organic
  • Glass morphism container: Dark gradient background with subtle border and box-shadow, plus a thin accent line at the top for depth
  • Word-by-word reveal: Each word fades from 0.25 to 1.0 opacity at ~2.8 words/second
  • Active word glow: The currently spoken word gets full opacity + a soft white
    text-shadow
  • Typography: SF Pro Display / Inter, weight 300 (light), 30px, generous letter-spacing
  • Positioning: 60px from bottom, centered, max-width 1100px
  • 自动同步:组件直接读取
    NARRATION_SCRIPT[scene]
    — 修改脚本文本后,下次渲染时字幕会自动更新
  • 滑入入场:字幕在淡入的同时向上滑动20px(0.4秒–0.9秒),过渡自然
  • 玻璃态容器:深色渐变背景+细微边框和阴影,顶部带有细装饰线增加层次感
  • 逐词显示:每个单词从0.25淡入到1.0,速度约为2.8词/秒
  • 当前词高亮:正在朗读的单词保持完全不透明度+柔和白色
    text-shadow
  • 排版:SF Pro Display / Inter字体,字重300(轻量),字号30px,字母间距适中
  • 定位:距离底部60px,居中显示,最大宽度1100px

When to update subtitles

字幕更新时机

Subtitles auto-update whenever you change
NARRATION_SCRIPT
in
ProductTour.tsx
. The full pipeline after editing narration text:
bash
undefined
每当修改
ProductTour.tsx
中的
NARRATION_SCRIPT
时,字幕会自动更新。修改旁白文本后的完整流程:
bash
undefined

1. Edit NARRATION_SCRIPT in remotion/ProductTour.tsx

1. 在remotion/ProductTour.tsx中编辑NARRATION_SCRIPT

2. Regenerate audio (subtitles already match the new text)

2. 重新生成音频(字幕已与新文本匹配)

npm run narration:generate
npm run narration:generate

3. Re-render video

3. 重新渲染视频

npm run remotion:render
undefined
npm run remotion:render
undefined

Phase 5: TTS Audio Generation

阶段5:TTS音频生成

Script:
scripts/generate-narration.ts

脚本:
scripts/generate-narration.ts

The script supports two backends and auto-generates
remotion/narration-format.ts
so the video composition always uses the correct file extension.
BackendTriggerOutputQuality
ElevenLabs
ELEVENLABS_API_KEY
set in
.env
.mp3
Production
macOS
say
+
afconvert
No API key, macOS detected
.wav
Development preview
bash
undefined
该脚本支持两种后端,并自动生成
remotion/narration-format.ts
,确保视频合成始终使用正确的文件扩展名。
后端触发条件输出格式质量
ElevenLabs
.env
中已设置
ELEVENLABS_API_KEY
.mp3
生产级
macOS
say
+
afconvert
无API密钥且检测到macOS系统
.wav
开发预览级
bash
undefined

Production: use ElevenLabs (add key to .env first)

生产环境:使用ElevenLabs(需先在.env中添加密钥)

npm run narration:generate
npm run narration:generate

Development: macOS fallback (no key needed)

开发环境:使用macOS备用方案(无需密钥)

npm run narration:generate

The script automatically:
1. Cleans old audio files (prevents mixing formats)
2. Generates audio files in `public/narration/`
3. Probes each file's duration using `afinfo` (macOS) or `ffprobe`
4. Writes `remotion/narration-format.ts` with the correct extension (`"mp3"` or `"wav"`)
5. Writes `remotion/narration-durations.ts` with per-scene frame counts (audio + 2s padding)
6. The Remotion composition imports both configs for synced playback
npm run narration:generate

该脚本会自动执行以下操作:
1. 清理旧音频文件(避免格式混合)
2. 在`public/narration/`目录下生成音频文件
3. 使用`afinfo`(macOS)或`ffprobe`探测每个文件的时长
4. 写入`remotion/narration-format.ts`,包含正确的扩展名(`"mp3"`或`"wav"`)
5. 写入`remotion/narration-durations.ts`,包含每个场景的帧数(音频时长+2秒缓冲)
6. Remotion合成会导入这两个配置文件,实现同步播放

ElevenLabs voice settings for Jony Ive style

实现Jony Ive风格的ElevenLabs语音设置

json
{
  "voice_id": "onwK4e9ZLuTAKqWW03F9",
  "model_id": "eleven_multilingual_v2",
  "voice_settings": {
    "stability": 0.75,
    "similarity_boost": 0.70,
    "style": 0.35,
    "use_speaker_boost": true
  }
}
json
{
  "voice_id": "onwK4e9ZLuTAKqWW03F9",
  "model_id": "eleven_multilingual_v2",
  "voice_settings": {
    "stability": 0.75,
    "similarity_boost": 0.70,
    "style": 0.35,
    "use_speaker_boost": true
  }
}

Audio file location

音频文件位置

public/narration/
├── intro.mp3 (or .wav)
├── dashboard.mp3
├── clients.mp3
├── wizard.mp3
├── security.mp3
└── outro.mp3

remotion/
├── narration-format.ts      ← auto-generated, audio extension ("mp3" | "wav")
└── narration-durations.ts   ← auto-generated, per-scene frame counts + total
public/narration/
├── intro.mp3 (或.wav)
├── dashboard.mp3
├── clients.mp3
├── wizard.mp3
├── security.mp3
└── outro.mp3

remotion/
├── narration-format.ts      ← 自动生成,音频扩展名("mp3" | "wav")
└── narration-durations.ts   ← 自动生成,每个场景的帧数+总时长

Audio-video sync (critical)

音视频同步(关键)

Scene durations are driven by audio length, not hardcoded. The generate script:
  1. Probes each audio file's duration (e.g., intro = 8.5s)
  2. Adds 2s padding for visual animation breathing room
  3. Writes frame count:
    ceil((8.5 + 2) * 30) = 317 frames
  4. ProductTour.tsx
    imports these and uses them for
    Sequence
    durations
Each scene also has:
  • Audio fade-out (last 1s) — prevents abrupt cut-off between scenes
  • Visual fade-to-black (last 0.5s) — smooth scene transitions
  • Total video duration is computed from sum of all scene frames
场景时长由音频长度决定,而非硬编码。生成脚本会:
  1. 探测每个音频文件的时长(例如:介绍场景=8.5秒)
  2. 添加2秒缓冲,用于视觉动画的过渡空间
  3. 计算帧数:
    ceil((8.5 + 2) * 30) = 317帧
  4. ProductTour.tsx
    导入这些值,用于
    Sequence
    组件的时长设置
每个场景还包含:
  • 音频淡出(最后1秒) — 避免场景切换时音频突兀中断
  • 视觉渐黑(最后0.5秒) — 实现平滑的场景过渡
  • 总视频时长由所有场景帧数之和计算得出

Important: format config

重要提示:格式配置

staticFile()
in Remotion does NOT throw when a file is missing — it returns a URL that 404s at render time. That's why the generate script writes
narration-format.ts
to ensure the video only references files that actually exist. Always run
npm run narration:generate
before
npm run remotion:render
.
Remotion中的
staticFile()
在文件缺失时不会抛出错误 — 它会返回一个在渲染时404的URL。因此生成脚本会写入
narration-format.ts
,确保视频仅引用实际存在的文件。在运行
npm run remotion:render
前,务必先运行
npm run narration:generate

Full narration script (prompt)

完整旁白脚本(提示词)

This is the exact text sent to ElevenLabs. Copy and adapt for your product:
The narration text lives in
remotion/ProductTour.tsx
as
NARRATION_SCRIPT
. The generate script imports it — never edit narration text in two places.
To see the current script, read
NARRATION_SCRIPT
in
ProductTour.tsx
. To change it, edit only that file, then re-run:
bash
npm run narration:generate   # Regenerates audio from the updated text
npm run remotion:render      # Subtitles already read from the same source
这是发送给ElevenLabs的完整文本。可根据你的产品需求进行调整:
旁白文本存储在
remotion/ProductTour.tsx
NARRATION_SCRIPT
中。生成脚本会导入该文本 — 绝对不要在两个地方编辑旁白文本
查看当前脚本,请阅读
ProductTour.tsx
中的
NARRATION_SCRIPT
。如需修改,仅编辑该文件,然后重新运行:
bash
npm run narration:generate   # 根据更新后的文本重新生成音频
npm run remotion:render      # 字幕已自动读取同一数据源

Phase 5b: Background Music (Suno AI)

阶段5b:背景音乐(Suno AI)

Setup

配置步骤

  1. Get a Suno API key at api.box (Suno API provider)
  2. Add to
    .env
    :
    SUNO_API_KEY=your-key
  1. api.box获取Suno API密钥(Suno API提供商)
  2. 添加到
    .env
    文件中:
    SUNO_API_KEY=your-key

Generate music

生成背景音乐

bash
npm run music:generate
bash
npm run music:generate

Output: public/music/background.mp3

输出文件:public/music/background.mp3

Config: remotion/music-config.ts (HAS_BACKGROUND_MUSIC = true)

配置文件:remotion/music-config.ts(设置HAS_BACKGROUND_MUSIC = true)


The script:
1. Sends a prompt to Suno's API (model V4_5ALL, instrumental only)
2. Polls for completion (typically 30-60 seconds)
3. Downloads the MP3 to `public/music/background.mp3`
4. Writes `remotion/music-config.ts` so the video knows music exists

该脚本会:
1. 向Suno API发送提示词(模型V4_5ALL,仅器乐)
2. 轮询任务完成状态(通常需要30-60秒)
3. 将MP3文件下载到`public/music/background.mp3`
4. 写入`remotion/music-config.ts`,告知视频背景音乐已存在

Music prompt (adapt for your product)

音乐提示词(可根据产品调整)

Uplifting neoclassical underscore for a modern technology product video.
Bright piano arpeggios in a major key over gentle pizzicato strings and
a soft cello melody. Light, optimistic, and forward-moving — like a TED
talk opening or an Apple product reveal. Subtle modern electronic texture
underneath the classical instruments. No heavy drums, no vocals, no sudden
drops. Tempo around 100 BPM. Engaging but never distracting — true
background music that lifts the mood without competing with narration.
Think Max Richter meets Ólafur Arnalds with a hint of Nils Frahm.
Duration should be around 90 seconds.
Key prompt principles:
  • Specify major key to avoid melancholic/minor tonality
  • Name classic instruments (piano, strings, cello) for warmth and sophistication
  • Add modern electronic texture to keep it contemporary
  • Reference real composers (Max Richter, Ólafur Arnalds, Nils Frahm) for style anchoring
  • Emphasize "background music" and "never distracting" — Suno tends to overpower otherwise
  • Set tempo (100 BPM) — too slow feels sad, too fast feels hectic
Uplifting neoclassical underscore for a modern technology product video.
Bright piano arpeggios in a major key over gentle pizzicato strings and
a soft cello melody. Light, optimistic, and forward-moving — like a TED
talk opening or an Apple product reveal. Subtle modern electronic texture
underneath the classical instruments. No heavy drums, no vocals, no sudden
drops. Tempo around 100 BPM. Engaging but never distracting — true
background music that lifts the mood without competing with narration.
Think Max Richter meets Ólafur Arnalds with a hint of Nils Frahm.
Duration should be around 90 seconds.
提示词关键原则:
  • 指定大调,避免忧郁/小调调性
  • 提及经典乐器(钢琴、弦乐、大提琴),增加温暖感和精致感
  • 添加现代电子纹理,保持内容与时俱进
  • 引用真实作曲家(Max Richter、Ólafur Arnalds、Nils Frahm),锚定风格方向
  • 强调**“背景音乐”“绝不干扰旁白”** — 否则Suno生成的音乐可能会盖过旁白
  • 设置** tempo(100 BPM)** — 过慢会显得悲伤,过快会显得杂乱

Volume ducking

音量闪避

The
BackgroundMusic
component in
ProductTour.tsx
handles automatic volume ducking — music plays at 0.15 volume between scenes and ducks to 0.06 when narration is active. Transitions are smooth 0.5s ramps.
  • Fade in: first 2s of video
  • Fade out: last 3s of video
  • Loop: music loops if shorter than the video
ProductTour.tsx
中的
BackgroundMusic
组件会自动处理音量闪避 — 场景切换时音乐音量为0.15,旁白播放时音量降低至0.06。过渡为0.5秒的平滑渐变。
  • 淡入:视频前2秒
  • 淡出:视频最后3秒
  • 循环:如果音乐时长短于视频,会自动循环播放

Without Suno API key

无Suno API密钥的情况

If no key is set, the video renders without background music (narration only). You can also manually place any MP3 at
public/music/background.mp3
and set
HAS_BACKGROUND_MUSIC = true
in
remotion/music-config.ts
.
如果未设置密钥,视频会在无背景音乐的情况下渲染(仅保留旁白)。你也可以手动将任意MP3文件放置在
public/music/background.mp3
,并在
remotion/music-config.ts
中设置
HAS_BACKGROUND_MUSIC = true

Phase 6: Render & Deploy

阶段6:渲染与部署

bash
undefined
bash
undefined

Full pipeline: narration → music → render

完整流程:旁白 → 音乐 → 渲染

npm run narration:generate # ElevenLabs narration (or macOS fallback) npm run music:generate # Suno background music (optional) npm run remotion:render # Final MP4 with both audio layers
npm run narration:generate # ElevenLabs旁白(或macOS备用方案) npm run music:generate # Suno背景音乐(可选) npm run remotion:render # 包含两层音频的最终MP4文件

Preview in Remotion Studio

在Remotion Studio中预览

npm run remotion:studio
npm run remotion:studio

Output: public/help/product-tour.mp4

输出文件:public/help/product-tour.mp4

The help page references the video:

帮助页面中引用该视频:

<video src="/help/product-tour.mp4" controls />

<video src="/help/product-tour.mp4" controls />

undefined
undefined

package.json scripts

package.json脚本配置

json
{
  "remotion:studio": "npx remotion studio remotion/index.ts",
  "remotion:render": "npx remotion render remotion/index.ts ProductTour public/help/product-tour.mp4",
  "narration:generate": "tsx scripts/generate-narration.ts",
  "music:generate": "tsx scripts/generate-music.ts"
}
json
{
  "remotion:studio": "npx remotion studio remotion/index.ts",
  "remotion:render": "npx remotion render remotion/index.ts ProductTour public/help/product-tour.mp4",
  "narration:generate": "tsx scripts/generate-narration.ts",
  "music:generate": "tsx scripts/generate-music.ts"
}

Phase 7: GitHub README Documentation

阶段7:GitHub README文档

Use the same assets (illustrations, video) to enrich your GitHub
README.md
with visual documentation. This makes the repository professional and immediately communicates value to contributors and potential users.
使用现有资源(插图、视频)丰富GitHub的
README.md
,添加可视化文档。这会让仓库看起来更专业,能立即向贡献者和潜在用户传达产品价值。

Strategy

策略

README.md documentation flow:
1. Re-use existing illustrations from public/help/
2. Generate additional screenshots/GIFs specific to README sections
3. Upload video to GitHub release or external host (too large for git)
4. Embed everything with standard Markdown + HTML
README.md文档流程:
1. 复用public/help/目录下的现有插图
2. 生成针对README章节的额外截图/GIF动图
3. 将视频上传到GitHub Release或外部托管平台(文件过大,不适合存入git)
4. 使用标准Markdown+HTML嵌入所有资源

Embedding screenshots in README

在README中嵌入截图

GitHub renders standard Markdown images. Place screenshots in a
docs/
or
.github/
directory and reference them with relative paths:
markdown
undefined
GitHub支持标准Markdown图片语法。将截图放置在
docs/
.github/
目录下,使用相对路径引用:
markdown
undefined

Dashboard

Dashboard

Dashboard overview
The main dashboard shows all monitored TYPO3 installations at a glance, with health scores and security alerts.

**Best practices for README screenshots:**
- Use 1200px width (GitHub caps display at ~900px, 2× for retina)
- Prefer light theme or show both with `<picture>` for dark mode support
- Add a 1px border or subtle shadow so screenshots don't bleed into the page
- Keep file sizes under 500KB (PNG with compression or JPEG at 85%)
Dashboard overview
The main dashboard shows all monitored TYPO3 installations at a glance, with health scores and security alerts.

**README截图最佳实践:**
- 使用1200px宽度(GitHub显示上限约为900px,2倍分辨率适配视网膜屏幕)
- 优先使用浅色主题,或使用`<picture>`标签同时支持深色/浅色模式
- 添加1px边框或细微阴影,避免截图与页面背景融合
- 控制文件大小在500KB以内(使用压缩PNG或85%质量的JPEG)

Dark/light theme images

深色/浅色主题适配图片

GitHub supports
<picture>
with
prefers-color-scheme
media queries:
html
<picture>
  <source media="(prefers-color-scheme: dark)" srcset="docs/screenshots/dashboard-dark.png">
  <source media="(prefers-color-scheme: light)" srcset="docs/screenshots/dashboard-light.png">
  <img alt="Dashboard overview" src="docs/screenshots/dashboard-light.png" width="800">
</picture>
GitHub支持使用
<picture>
标签结合
prefers-color-scheme
媒体查询:
html
<picture>
  <source media="(prefers-color-scheme: dark)" srcset="docs/screenshots/dashboard-dark.png">
  <source media="(prefers-color-scheme: light)" srcset="docs/screenshots/dashboard-light.png">
  <img alt="Dashboard overview" src="docs/screenshots/dashboard-light.png" width="800">
</picture>

Generating screenshots for README

为README生成截图

You have two approaches:
1. AI-generated illustrations (fast, always available):
GenerateImage({
  description: "Clean screenshot-style illustration of a TYPO3 monitoring
    dashboard. Dark theme, stat cards showing Total Clients: 42,
    Healthy: 38, Warning: 3, Critical: 1. Sidebar navigation visible.
    Modern, polished UI. 1200×675px, 16:9 aspect ratio.",
  filename: "docs/screenshots/dashboard.png"
})
2. Real screenshots via Playwright (pixel-perfect):
typescript
// scripts/capture-screenshots.ts
import { chromium } from "playwright";

async function captureScreenshots() {
  const browser = await chromium.launch();
  const page = await browser.newPage({ viewport: { width: 1200, height: 675 } });

  // Login if needed
  await page.goto("http://localhost:3001");

  // Capture each page
  const pages = [
    { url: "/", name: "dashboard" },
    { url: "/clients", name: "clients" },
    { url: "/extensions", name: "extensions" },
    { url: "/help", name: "help" },
  ];

  for (const p of pages) {
    await page.goto(`http://localhost:3001${p.url}`);
    await page.waitForTimeout(2000); // Let animations settle
    await page.screenshot({ path: `docs/screenshots/${p.name}.png` });
    // Also capture dark mode
    await page.emulateMedia({ colorScheme: "dark" });
    await page.screenshot({ path: `docs/screenshots/${p.name}-dark.png` });
    await page.emulateMedia({ colorScheme: "light" });
  }

  await browser.close();
}

captureScreenshots();
Add to
package.json
:
json
"screenshots:capture": "tsx scripts/capture-screenshots.ts"
有两种实现方式:
1. AI生成插图(快速、随时可用):
GenerateImage({
  description: "Clean screenshot-style illustration of a TYPO3 monitoring
    dashboard. Dark theme, stat cards showing Total Clients: 42,
    Healthy: 38, Warning: 3, Critical: 1. Sidebar navigation visible.
    Modern, polished UI. 1200×675px, 16:9 aspect ratio.",
  filename: "docs/screenshots/dashboard.png"
})
2. 使用Playwright生成真实截图(像素级精准):
typescript
// scripts/capture-screenshots.ts
import { chromium } from "playwright";

async function captureScreenshots() {
  const browser = await chromium.launch();
  const page = await browser.newPage({ viewport: { width: 1200, height: 675 } });

  // 如需登录,在此处添加登录逻辑
  await page.goto("http://localhost:3001");

  // 捕获每个页面的截图
  const pages = [
    { url: "/", name: "dashboard" },
    { url: "/clients", name: "clients" },
    { url: "/extensions", name: "extensions" },
    { url: "/help", name: "help" },
  ];

  for (const p of pages) {
    await page.goto(`http://localhost:3001${p.url}`);
    await page.waitForTimeout(2000); // 等待动画完成
    await page.screenshot({ path: `docs/screenshots/${p.name}.png` });
    // 同时捕获深色模式截图
    await page.emulateMedia({ colorScheme: "dark" });
    await page.screenshot({ path: `docs/screenshots/${p.name}-dark.png` });
    await page.emulateMedia({ colorScheme: "light" });
  }

  await browser.close();
}

captureScreenshots();
添加到
package.json
json
"screenshots:capture": "tsx scripts/capture-screenshots.ts"

Animated GIFs for README

为README生成动画GIF

Short GIFs are more engaging than static screenshots for interactive features (wizard, sorting, filtering). Use Playwright to record and
ffmpeg
to convert:
bash
undefined
短GIF动图比静态截图更适合展示交互功能(如向导流程、排序、筛选)。使用Playwright录制视频,再用
ffmpeg
转换为GIF:
bash
undefined

Record a video clip with Playwright (outputs .webm)

使用Playwright录制视频片段(输出.webm格式)

Then convert to GIF:

然后转换为GIF:

ffmpeg -i recording.webm -vf "fps=12,scale=800:-1:flags=lanczos" -loop 0 docs/screenshots/wizard.gif

Keep GIFs under 5MB. For larger animations, use MP4 with GitHub's video support.
ffmpeg -i recording.webm -vf "fps=12,scale=800:-1:flags=lanczos" -loop 0 docs/screenshots/wizard.gif

控制GIF文件大小在5MB以内。对于较大的动画,可使用MP4格式,利用GitHub的视频支持功能。

Embedding the product tour video

在README中嵌入产品导览视频

GitHub Markdown supports video since 2021. You have three options:
Option A: Upload to GitHub Release (recommended for open source)
bash
undefined
GitHub从2021年开始支持Markdown视频嵌入。有三种可选方案:
方案A:上传到GitHub Release(开源项目推荐)
bash
undefined

Create a release and attach the video

创建Release并上传视频

gh release create v1.0.0 public/help/product-tour.mp4
--title "v1.0.0" --notes "Initial release"

Then link in README:

```markdown
gh release create v1.0.0 public/help/product-tour.mp4
--title "v1.0.0" --notes "Initial release"

然后在README中链接:

```markdown

Product Tour

Product Tour


GitHub auto-embeds the video player when the URL is on its own line.

**Option B: Drag-and-drop into GitHub Issue/PR**

1. Create a GitHub issue or PR
2. Drag the MP4 into the comment editor
3. Copy the resulting `user-attachments` URL
4. Paste into README

```markdown

当URL单独占一行时,GitHub会自动嵌入视频播放器。

**方案B:拖放到GitHub Issue/PR中**

1. 创建GitHub Issue或PR
2. 将MP4文件拖放到评论编辑器中
3. 复制生成的`user-attachments`链接
4. 粘贴到README中

```markdown

Product Tour

Product Tour


**Option C: External host (YouTube, Vimeo)**

If the video is large (>25MB) or you want analytics, upload to YouTube/Vimeo
and embed a thumbnail with a play button:

```markdown

**方案C:外部托管平台(YouTube、Vimeo)**

如果视频文件过大(>25MB)或需要统计数据,可上传到YouTube/Vimeo,然后嵌入带播放按钮的缩略图:

```markdown

Product Tour

Product Tour

Watch the product tour
undefined
Watch the product tour
undefined

Full README template with documentation

包含文档的完整README模板

Here's a recommended structure integrating all visual assets:
markdown
undefined
以下是推荐的README结构,整合了所有可视化资源:
markdown
undefined

Project Name

Project Name

One-line description — aspirational, concise.
<picture> <source media="(prefers-color-scheme: dark)" srcset="docs/screenshots/hero-dark.png"> <img alt="Project screenshot" src="docs/screenshots/hero-light.png" width="800"> </picture>
一句话描述 — 有吸引力、简洁明了。
<picture> <source media="(prefers-color-scheme: dark)" srcset="docs/screenshots/hero-dark.png"> <img alt="Project screenshot" src="docs/screenshots/hero-light.png" width="800"> </picture>

Product Tour

Product Tour

Features

Features

Dashboard

Dashboard

Dashboard Description of the dashboard...
Dashboard 仪表盘功能描述...

Client Management

Client Management

Clients Description of client features...
Clients 客户管理功能描述...

Quick Start

Quick Start

...installation instructions...
undefined
...安装说明...
undefined

Updating documentation on changes

UI变更时更新文档

When the UI changes, re-run the documentation pipeline:
bash
undefined
当UI发生变更时,重新运行文档生成流程:
bash
undefined

1. Capture fresh screenshots (if using Playwright)

1. 捕获新的截图(如果使用Playwright)

npm run screenshots:capture
npm run screenshots:capture

2. Regenerate illustrations (if using AI)

2. 重新生成插图(如果使用AI)

→ Use GenerateImage tool with updated descriptions

→ 使用GenerateImage工具,更新描述文本

3. Update narration if features changed

3. 如果功能变更,更新旁白文本

→ Edit NARRATION_SCRIPT in remotion/ProductTour.tsx

→ 编辑remotion/ProductTour.tsx中的NARRATION_SCRIPT

npm run narration:generate
npm run narration:generate

4. Re-render the product tour video

4. 重新渲染产品导览视频

npm run remotion:render
npm run remotion:render

5. Upload new video to GitHub release

5. 将新视频上传到GitHub Release

gh release upload v1.x.x public/help/product-tour.mp4 --clobber
gh release upload v1.x.x public/help/product-tour.mp4 --clobber

6. Commit updated screenshots

6. 提交更新后的截图

git add docs/screenshots/ README.md git commit -m "docs: update screenshots and video for vX.Y.Z"
undefined
git add docs/screenshots/ README.md git commit -m "docs: update screenshots and video for vX.Y.Z"
undefined

Directory structure for README documentation

README文档的目录结构

project/
├── README.md                          ← Main documentation with embedded visuals
├── docs/
│   ├── screenshots/
│   │   ├── dashboard.png              ← Light theme screenshot
│   │   ├── dashboard-dark.png         ← Dark theme screenshot
│   │   ├── clients.png
│   │   ├── wizard.gif                 ← Animated walkthrough
│   │   ├── hero-light.png             ← Hero image (light)
│   │   ├── hero-dark.png              ← Hero image (dark)
│   │   └── video-thumbnail.png        ← Video poster frame
│   └── SECURITY.md
├── public/help/
│   └── product-tour.mp4              ← Rendered Remotion video
└── scripts/
    ├── capture-screenshots.ts         ← Playwright screenshot automation
    ├── generate-narration.ts          ← TTS generation
    └── generate-music.ts             ← Background music generation
project/
├── README.md                          ← 主文档,嵌入所有可视化资源
├── docs/
│   ├── screenshots/
│   │   ├── dashboard.png              ← 浅色主题截图
│   │   ├── dashboard-dark.png         ← 深色主题截图
│   │   ├── clients.png
│   │   ├── wizard.gif                 ← 动画演示
│   │   ├── hero-light.png             ← 首图(浅色)
│   │   ├── hero-dark.png              ← 首图(深色)
│   │   └── video-thumbnail.png        ← 视频封面图
│   └── SECURITY.md
├── public/help/
│   └── product-tour.mp4              ← 渲染完成的Remotion视频
└── scripts/
    ├── capture-screenshots.ts         ← Playwright截图自动化脚本
    ├── generate-narration.ts          ← TTS生成脚本
    └── generate-music.ts             ← 背景音乐生成脚本

Checklist

检查清单

Documentation Creation:
- [ ] Help page component created at src/app/(dashboard)/help/page.tsx
- [ ] Sidebar navigation updated with Help link
- [ ] AI-generated illustrations in public/help/
- [ ] Remotion project set up in remotion/
- [ ] Narration script written (Jony Ive style)
- [ ] NarrationSubtitle component added to video
- [ ] TTS audio generated (ElevenLabs or macOS fallback)
- [ ] Video rendered to public/help/product-tour.mp4
- [ ] Video embedded in help page
- [ ] All content reviewed by technical writer persona

GitHub README Documentation:
- [ ] Screenshots captured (AI-generated or Playwright)
- [ ] Dark/light variants created with <picture> tags
- [ ] Product tour video uploaded to GitHub release
- [ ] README.md updated with embedded visuals
- [ ] Animated GIFs created for interactive features (optional)
- [ ] Documentation update pipeline documented in scripts
文档创建:
- [ ] 已在src/app/(dashboard)/help/page.tsx创建帮助页面组件
- [ ] 已更新侧边栏导航,添加帮助页面链接
- [ ] 已在public/help/目录下生成AI插图
- [ ] 已在remotion/目录下配置Remotion项目
- [ ] 已编写旁白脚本(Jony Ive风格)
- [ ] 已在视频中添加NarrationSubtitle组件
- [ ] 已生成TTS音频(ElevenLabs或macOS备用方案)
- [ ] 已将视频渲染到public/help/product-tour.mp4
- [ ] 已在帮助页面中嵌入视频
- [ ] 所有内容已通过技术文档作者视角审核

GitHub README文档:
- [ ] 已捕获截图(AI生成或Playwright生成)
- [ ] 已使用<picture>标签创建深色/浅色模式变体
- [ ] 已将产品导览视频上传到GitHub Release
- [ ] 已更新README.md,嵌入所有可视化资源
- [ ] 已为交互功能创建动画GIF(可选)
- [ ] 已在脚本中记录文档更新流程

Additional resources

额外资源

Video & Animation for Documentation (Remotion + GSAP)

文档视频与动画(Remotion + GSAP)

When generating visual documentation, integrating Remotion with GSAP allows for complex, sequenced, and dynamic visual storytelling.
生成可视化文档时,将RemotionGSAP结合使用,可实现复杂、有序且动态的视觉叙事效果。

Prerequisites

前置条件

GSAP must be installed alongside your existing Remotion setup. GSAP's core is free for all use cases; premium plugins require a paid license.
Install GSAP core:
bash
npm install gsap
Optional premium plugins (require GSAP Club license):
bash
undefined
GSAP需与现有Remotion配置一起安装。GSAP核心库可免费用于所有场景;高级插件需要付费许可证。
安装GSAP核心库:
bash
npm install gsap
可选高级插件(需要GSAP Club许可证):
bash
undefined

Install from GSAP's private registry (requires auth token)

从GSAP私有仓库安装(需要授权令牌)

npm install gsap@npm:@gsap/shockingly

Premium plugins used in this guide:
- `MorphSVGPlugin` -- morph between SVG shapes (chart transitions)
- `SplitText` -- split text into characters/words for kinetic typography
- `Flip` -- animate layout changes (code diff walkthroughs)
- `MotionPathPlugin` -- animate along SVG paths (included free since GSAP 3.x)

**Register plugins** in your Remotion entry point (e.g., `src/Root.tsx` or the component file):

```tsx
import gsap from "gsap";
import { MotionPathPlugin } from "gsap/MotionPathPlugin";

gsap.registerPlugin(MotionPathPlugin);
// Add other plugins as needed:
// gsap.registerPlugin(Flip, MorphSVGPlugin, SplitText);
Remotion compatibility notes:
  • GSAP works in Remotion's rendering pipeline because it manipulates DOM elements directly, which Remotion captures frame-by-frame.
  • Always use
    gsap.context()
    for proper React cleanup on unmount.
  • Never use GSAP's
    ScrollTrigger
    in Remotion -- there is no scroll context during rendering.
npm install gsap@npm:@gsap/shockingly

本指南中使用的高级插件:
- `MorphSVGPlugin` -- 在SVG形状之间进行变形动画(如图表过渡)
- `SplitText` -- 将文本拆分为字符/单词,实现动态排版
- `Flip` -- 为布局变更添加动画(如代码差异演示)
- `MotionPathPlugin` -- 沿SVG路径动画(从GSAP 3.x开始免费提供)

**注册插件**在Remotion入口文件中(如`src/Root.tsx`或组件文件):

```tsx
import gsap from "gsap";
import { MotionPathPlugin } from "gsap/MotionPathPlugin";

gsap.registerPlugin(MotionPathPlugin);
// 根据需要添加其他插件:
// gsap.registerPlugin(Flip, MorphSVGPlugin, SplitText);
Remotion兼容性说明:
  • GSAP可在Remotion渲染流程中正常工作,因为它直接操作DOM元素,Remotion会逐帧捕获这些元素。
  • 务必使用
    gsap.context()
    确保React卸载时正确清理。
  • 绝对不要在Remotion中使用GSAP的
    ScrollTrigger
    -- 渲染过程中不存在滚动上下文。

Core Concept: Deterministic Syncing

核心概念:确定性同步

Remotion is frame-based; GSAP is time-based. To ensure videos render correctly without tearing or skipping during export, never let GSAP run on its own timer. Map Remotion's
useCurrentFrame()
to a strictly paused GSAP timeline.
tsx
import { useCurrentFrame, useVideoConfig } from "remotion";
import { useEffect, useMemo, useRef } from "react";
import gsap from "gsap";

export const GsapSequence = () => {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();
  const containerRef = useRef<HTMLDivElement>(null);
  
  // 1. Create a strictly paused timeline
  const timeline = useMemo(() => gsap.timeline({ paused: true }), []);

  // 2. Build your animation sequence (use GSAP seconds)
  useEffect(() => {
    let ctx = gsap.context(() => {
      timeline.to(".box", { x: 200, rotation: 360, duration: 2, stagger: 0.2 });
    }, containerRef);
    
    return () => ctx.revert(); // React cleanup
  }, [timeline]);

  // 3. THE MAGIC: Sync GSAP playhead to Remotion frame
  useEffect(() => {
    timeline.seek(frame / fps); 
  }, [frame, fps, timeline]);

  return (
    <div ref={containerRef} className="box-container">
      <div className="box">Item 1</div>
      <div className="box">Item 2</div>
    </div>
  );
};
Remotion是基于帧的;GSAP是基于时间的。为确保视频导出时无撕裂或跳帧,绝对不要让GSAP使用自身的计时器。将Remotion的
useCurrentFrame()
映射到严格暂停的GSAP时间轴。
tsx
import { useCurrentFrame, useVideoConfig } from "remotion";
import { useEffect, useMemo, useRef } from "react";
import gsap from "gsap";

export const GsapSequence = () => {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();
  const containerRef = useRef<HTMLDivElement>(null);
  
  // 1. 创建严格暂停的时间轴
  const timeline = useMemo(() => gsap.timeline({ paused: true }), []);

  // 2. 构建动画序列(使用GSAP秒数)
  useEffect(() => {
    let ctx = gsap.context(() => {
      timeline.to(".box", { x: 200, rotation: 360, duration: 2, stagger: 0.2 });
    }, containerRef);
    
    return () => ctx.revert(); // React清理逻辑
  }, [timeline]);

  // 3. 关键:将GSAP播放头与Remotion帧同步
  useEffect(() => {
    timeline.seek(frame / fps); 
  }, [frame, fps, timeline]);

  return (
    <div ref={containerRef} className="box-container">
      <div className="box">Item 1</div>
      <div className="box">Item 2</div>
    </div>
  );
};

Creative Directives for AI Agents

AI Agent创作指南

When prompted to create visual documentation, utilize the following animation patterns:
当需要创建可视化文档时,可使用以下动画模式:

1. Diagram Animations

1. 图表动画

  • Data Flow Tracing: Use GSAP
    MotionPathPlugin
    or animate
    stroke-dashoffset
    to show data packets moving along SVG lines between architecture nodes (e.g., Load Balancer → DB).
  • Node Pulses: Add glowing/pulsing loops (
    box-shadow
    and
    scale
    ) to servers/components when they become "active" in the explanation.
  • Elastic Reveals: Animate nodes popping into existence with
    ease: "back.out(1.5)"
    and a
    stagger: 0.1
    .
  • 数据流追踪:使用GSAP
    MotionPathPlugin
    或动画
    stroke-dashoffset
    ,展示数据包沿架构节点间的SVG线条移动(如负载均衡器→数据库)。
  • 节点脉动:当服务器/组件在讲解中变为“活跃”状态时,添加发光/脉动循环动画(
    box-shadow
    scale
    )。
  • 弹性显示:使用
    ease: "back.out(1.5)"
    stagger: 0.1
    ,让节点以弹出的方式出现。

2. Animation of Screenshots

2. 截图动画

  • 3D Exploded View: Separate UI components into layers (background, modal, button) and rotate the parent container in 3D (
    rotateX: 60deg
    ,
    rotateZ: -45deg
    ). Animate the
    translateZ
    of inner layers to pull the interface apart conceptually.
  • Glass Morph Spotlight: Smoothly zoom into an active UI area using a
    clip-path
    circle, while dimming and applying a CSS blur (
    backdrop-filter: blur(10px)
    ) to the rest of the screenshot.
  • Simulated User Journeys: Animate a custom SVG cursor moving across the UI, synchronizing button scaling and ripple effects to simulate clicks.
  • 3D分解视图:将UI组件拆分为多个图层(背景、模态框、按钮),然后旋转父容器到3D视角(
    rotateX: 60deg
    ,
    rotateZ: -45deg
    )。动画内层的
    translateZ
    属性,将界面“拆解”开来,帮助用户理解层级关系。
  • 玻璃态聚光灯:使用
    clip-path
    圆形平滑放大到UI的活跃区域,同时调暗截图其余部分并应用CSS模糊(
    backdrop-filter: blur(10px)
    )。
  • 模拟用户操作:动画自定义SVG光标在UI上移动,同步按钮缩放和波纹效果,模拟用户点击操作。

3. Infographics & Tables

3. 信息图与表格

  • Staggered Row Reveals: Slide and fade in table rows one-by-one (
    stagger: 0.1
    ,
    x: -20
    ,
    opacity: 0
    ) to pace the viewer's reading.
  • Data Odometers: Animate a numeric object's value from 0 to target (using
    onUpdate
    to format text), making financial or performance numbers tick up dynamically.
  • Cell Highlighting: Animate a floating background div that smoothly translates its
    y
    coordinate from row to row to highlight active talking points.
  • 交错行显示:逐行滑入并淡入表格行(
    stagger: 0.1
    ,
    x: -20
    ,
    opacity: 0
    ),控制用户的阅读节奏。
  • 数字计数器:动画数值从0增长到目标值(使用
    onUpdate
    格式化文本),让财务或性能数据动态跳动。
  • 单元格高亮:动画浮动背景div,平滑改变其
    y
    坐标,在不同行之间移动,突出当前讲解的内容。

4. Animations of Charts

4. 图表动画

  • Spring-loaded Bar Charts: Set CSS
    transform-origin: bottom
    . Animate SVG
    <rect>
    scaleY
    from 0 to target with GSAP's
    elastic.out
    easing for a playful reveal.
  • Progressive Charts: Animate SVG circles with
    stroke-dasharray
    and
    stroke-dashoffset
    to draw out pie chart segments proportionally over time.
  • Morphing Data: Use
    MorphSVGPlugin
    to seamlessly transition a bar chart into a line chart, visually connecting two different ways of looking at the same data.
  • 弹簧加载柱状图:设置CSS
    transform-origin: bottom
    。使用GSAP的
    elastic.out
    缓动函数,动画SVG
    <rect>
    scaleY
    从0到目标值,实现活泼的显示效果。
  • 渐进式图表:动画SVG圆形的
    stroke-dasharray
    stroke-dashoffset
    ,按比例逐步绘制饼图扇区。
  • 数据变形:使用
    MorphSVGPlugin
    将柱状图无缝过渡为折线图,直观展示同一数据的不同呈现方式。

5. Code & Text

5. 代码与文本

  • Code Diff Walkthroughs: Use GSAP's
    Flip
    plugin. When code changes, removed lines shrink and glow red, new lines expand and glow green, and unchanged lines glide to their new line numbers smoothly.
  • Kinetic Typography: Use GSAP
    SplitText
    to stagger headers character-by-character.

  • 代码差异演示:使用GSAP的
    Flip
    插件。当代码变更时,删除的行收缩并显示红色发光效果,新增的行展开并显示绿色发光效果,未变更的行平滑移动到新的行号位置。
  • 动态排版:使用GSAP
    SplitText
    将标题按字符逐个交错显示。

Concrete Code Examples (Remotion + GSAP Patterns)

具体代码示例(Remotion + GSAP模式)

Use these blueprints as starting points when generating Remotion + GSAP code to ensure correct CSS contexts, React lifecycle management, and frame syncing.
以下是可复用的代码蓝图,可作为生成Remotion + GSAP代码的起点,确保正确的CSS上下文、React生命周期管理和帧同步。

Example 1: 3D Exploded Screenshot (UI Layers)

示例1:3D分解截图(UI图层)

Requires a strict CSS 3D context. The parent needs
perspective
, and the container needs
transformStyle: "preserve-3d"
.
tsx
import { useCurrentFrame, useVideoConfig } from "remotion";
import { useEffect, useMemo, useRef } from "react";
import gsap from "gsap";

export const ExplodedScreenshot = () => {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();
  const containerRef = useRef<HTMLDivElement>(null);
  const tl = useMemo(() => gsap.timeline({ paused: true }), []);

  useEffect(() => {
    let ctx = gsap.context(() => {
      // 1. Tilt the whole container into isometric space
      tl.to(".wrapper", { rotateX: 60, rotateZ: -45, duration: 1, ease: "power2.inOut" })
        // 2. Explode the layers upward on the Z-axis (Using "<" to sync with rotation end)
        .to(".ui-layer", {
          z: (index) => (index + 1) * 80, // Stagger height based on index
          boxShadow: "0px 20px 40px rgba(0,0,0,0.4)",
          duration: 1.5,
          ease: "back.out(1.2)",
          stagger: 0.2
        }, "-=0.2"); 
    }, containerRef);
    
    return () => ctx.revert(); // Crucial for React StrictMode cleanup
  }, [tl]);

  // Deterministic Frame Sync
  useEffect(() => { tl.seek(frame / fps); }, [frame, fps, tl]);

  return (
    <div ref={containerRef} style={{ perspective: 1000, width: "100%", height: "100%" }}>
      <div className="wrapper" style={{ transformStyle: "preserve-3d", position: "relative", width: 800, height: 600 }}>
        <img src="/bg-layer.png" className="ui-layer" style={{ position: "absolute", inset: 0 }} />
        <img src="/modal-layer.png" className="ui-layer" style={{ position: "absolute", inset: 0 }} />
        <img src="/dropdown-layer.png" className="ui-layer" style={{ position: "absolute", inset: 0 }} />
      </div>
    </div>
  );
};
需要严格的CSS 3D上下文。父容器需要
perspective
属性,子容器需要
transformStyle: "preserve-3d"
属性。
tsx
import { useCurrentFrame, useVideoConfig } from "remotion";
import { useEffect, useMemo, useRef } from "react";
import gsap from "gsap";

export const ExplodedScreenshot = () => {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();
  const containerRef = useRef<HTMLDivElement>(null);
  const tl = useMemo(() => gsap.timeline({ paused: true }), []);

  useEffect(() => {
    let ctx = gsap.context(() => {
      // 1. 将整个容器倾斜到等轴测视角
      tl.to(".wrapper", { rotateX: 60, rotateZ: -45, duration: 1, ease: "power2.inOut" })
        // 2. 将图层沿Z轴向上“拆解”(使用"<"与旋转动画同步结束)
        .to(".ui-layer", {
          z: (index) => (index + 1) * 80, // 根据索引设置交错高度
          boxShadow: "0px 20px 40px rgba(0,0,0,0.4)",
          duration: 1.5,
          ease: "back.out(1.2)",
          stagger: 0.2
        }, "-=0.2"); 
    }, containerRef);
    
    return () => ctx.revert(); // React StrictMode清理的关键
  }, [tl]);

  // 确定性帧同步
  useEffect(() => { tl.seek(frame / fps); }, [frame, fps, tl]);

  return (
    <div ref={containerRef} style={{ perspective: 1000, width: "100%", height: "100%" }}>
      <div className="wrapper" style={{ transformStyle: "preserve-3d", position: "relative", width: 800, height: 600 }}>
        <img src="/bg-layer.png" className="ui-layer" style={{ position: "absolute", inset: 0 }} />
        <img src="/modal-layer.png" className="ui-layer" style={{ position: "absolute", inset: 0 }} />
        <img src="/dropdown-layer.png" className="ui-layer" style={{ position: "absolute", inset: 0 }} />
      </div>
    </div>
  );
};

Example 2: SVG Architecture Diagram (Drawing Data Flows)

示例2:SVG架构图(绘制数据流)

Use native SVG
strokeDasharray
and GSAP
strokeDashoffset
to draw connection lines between nodes without needing external plugins.
tsx
import { useCurrentFrame, useVideoConfig } from "remotion";
import { useEffect, useMemo, useRef } from "react";
import gsap from "gsap";

export const ArchitectureFlow = () => {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();
  const svgRef = useRef<SVGSVGElement>(null);
  const tl = useMemo(() => gsap.timeline({ paused: true }), []);

  useEffect(() => {
    let ctx = gsap.context(() => {
      // Setup: ensure path is hidden initially (Assuming path length ~500)
      gsap.set(".connection-line", { strokeDasharray: 500, strokeDashoffset: 500 });

      // 1. Pop the nodes in
      tl.from(".node", { scale: 0, transformOrigin: "center", duration: 0.5, stagger: 0.3, ease: "back.out(1.5)" })
      // 2. Draw the line between them
        .to(".connection-line", { strokeDashoffset: 0, duration: 1.2, ease: "power2.inOut" })
      // 3. Pulse the receiving node to indicate data arrival
        .to(".node-db", { scale: 1.15, repeat: 1, yoyo: true, duration: 0.2, fill: "#10b981" });
    }, svgRef);
    
    return () => ctx.revert();
  }, [tl]);

  useEffect(() => { tl.seek(frame / fps); }, [frame, fps, tl]);

  return (
    <svg ref={svgRef} width="800" height="400" viewBox="0 0 800 400">
      {/* Connecting Line */}
      <path className="connection-line" d="M 100 200 C 300 200, 500 200, 700 200" stroke="#cbd5e1" strokeWidth="6" fill="none" />
      {/* Nodes */}
      <circle className="node node-client" cx="100" cy="200" r="40" fill="#3b82f6" />
      <rect className="node node-db" x="660" y="160" width="80" height="80" rx="12" fill="#6366f1" />
    </svg>
  );
};
使用原生SVG
strokeDasharray
和GSAP
strokeDashoffset
绘制节点间的连接线,无需外部插件。
tsx
import { useCurrentFrame, useVideoConfig } from "remotion";
import { useEffect, useMemo, useRef } from "react";
import gsap from "gsap";

export const ArchitectureFlow = () => {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();
  const svgRef = useRef<SVGSVGElement>(null);
  const tl = useMemo(() => gsap.timeline({ paused: true }), []);

  useEffect(() => {
    let ctx = gsap.context(() => {
      // 初始化:确保路径初始状态隐藏(假设路径长度约为500)
      gsap.set(".connection-line", { strokeDasharray: 500, strokeDashoffset: 500 });

      // 1. 节点弹出显示
      tl.from(".node", { scale: 0, transformOrigin: "center", duration: 0.5, stagger: 0.3, ease: "back.out(1.5)" })
      // 2. 绘制节点间的连接线
        .to(".connection-line", { strokeDashoffset: 0, duration: 1.2, ease: "power2.inOut" })
      // 3. 接收节点脉动,指示数据到达
        .to(".node-db", { scale: 1.15, repeat: 1, yoyo: true, duration: 0.2, fill: "#10b981" });
    }, svgRef);
    
    return () => ctx.revert();
  }, [tl]);

  useEffect(() => { tl.seek(frame / fps); }, [frame, fps, tl]);

  return (
    <svg ref={svgRef} width="800" height="400" viewBox="0 0 800 400">
      {/* 连接线 */}
      <path className="connection-line" d="M 100 200 C 300 200, 500 200, 700 200" stroke="#cbd5e1" strokeWidth="6" fill="none" />
      {/* 节点 */}
      <circle className="node node-client" cx="100" cy="200" r="40" fill="#3b82f6" />
      <rect className="node node-db" x="660" y="160" width="80" height="80" rx="12" fill="#6366f1" />
    </svg>
  );
};

Example 3: Number Odometer (For Infographics & Tables)

示例3:数字计数器(用于信息图与表格)

To animate a number counting up (e.g., "0" to "10,000") smoothly, animate a dummy object and use
onUpdate
to safely set React state synchronously with the frame.
tsx
import { useCurrentFrame, useVideoConfig } from "remotion";
import { useEffect, useMemo, useState } from "react";
import gsap from "gsap";

export const AnimatedNumber = ({ targetValue = 10000 }) => {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();
  const [displayValue, setDisplayValue] = useState(0);
  
  const tl = useMemo(() => gsap.timeline({ paused: true }), []);

  useEffect(() => {
    const counter = { val: 0 };
    
    // gsap.context isn't strictly necessary for JS objects, but good for consistency
    let ctx = gsap.context(() => {
      tl.to(counter, {
        val: targetValue,
        duration: 2,
        ease: "power2.out",
        onUpdate: () => {
          // Update React state safely as GSAP manipulates the dummy object
          setDisplayValue(Math.round(counter.val));
        }
      });
    });
    
    return () => ctx.revert();
  }, [tl, targetValue]);

  useEffect(() => { tl.seek(frame / fps); }, [frame, fps, tl]);

  return (
    <div style={{ fontSize: "64px", fontFamily: "monospace", fontWeight: "bold" }}>
      ${displayValue.toLocaleString()}
    </div>
  );
};
要实现数字从0平滑增长到目标值(如“0”到“10,000”),可动画一个虚拟对象,并使用
onUpdate
安全地同步设置React状态。
tsx
import { useCurrentFrame, useVideoConfig } from "remotion";
import { useEffect, useMemo, useState } from "react";
import gsap from "gsap";

export const AnimatedNumber = ({ targetValue = 10000 }) => {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();
  const [displayValue, setDisplayValue] = useState(0);
  
  const tl = useMemo(() => gsap.timeline({ paused: true }), []);

  useEffect(() => {
    const counter = { val: 0 };
    
    // 对于JS对象,gsap.context并非必需,但保持一致性更好
    let ctx = gsap.context(() => {
      tl.to(counter, {
        val: targetValue,
        duration: 2,
        ease: "power2.out",
        onUpdate: () => {
          // 当GSAP操作虚拟对象时,安全更新React状态
          setDisplayValue(Math.round(counter.val));
        }
      });
    });
    
    return () => ctx.revert();
  }, [tl, targetValue]);

  useEffect(() => { tl.seek(frame / fps); }, [frame, fps, tl]);

  return (
    <div style={{ fontSize: "64px", fontFamily: "monospace", fontWeight: "bold" }}>
      ${displayValue.toLocaleString()}
    </div>
  );
};