Skip to content
Back to skills

Carocut Builder Compositor

ASecurity

Remotion Shot 组件实现。将 storyboard 转化为 React 组件。包含关键的帧计算规则(Math.round 强制)、interpolate 安全规则(最小 1 帧)、1080p 视觉标准、动画编排模式、音视频同步架构、组件复用模式。这是技术密度最高的 skill。

  • 124 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added June 8, 2026
ai-agentstypescriptpythongobashreactvuenodespringtestingapi

Works with

  • cli
  • api

Security analysis

A100/100

Scanned June 8, 2026

npx -y skills add bilibili/carocut --skill carocut-builder-compositor --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Carocut Builder Compositor?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Carocut Builder Compositor
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/bilibili-carocut-builder-compositor/badge)](https://www.skillsdirectory.com/skills/bilibili-carocut-builder-compositor)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: carocut-builder-compositor
description: Remotion Shot 组件实现。将 storyboard 转化为 React 组件。包含关键的帧计算规则(Math.round 强制)、interpolate 安全规则(最小 1 帧)、1080p 视觉标准、动画编排模式、音视频同步架构、组件复用模式。这是技术密度最高的 skill。
---

# Shot Compositor

Implements storyboard as Remotion React components. This is the core implementation phase where planning documents become working code.

**For additional Remotion API patterns, see `carocut-builder-remotion-ref` skill.**

---

## Incremental Mode

When receiving amendment instructions rather than building from scratch:

- **Modify specific shots:** Edit only the targeted shot component files. Do not regenerate unaffected shots.
- **Add new shots:** Create new `Shot{NNN}_*.tsx` files. Update the chapter index and main Composition to include them. Adjust `from` offsets for all subsequent shots in the same chapter.
- **Update frame timing:** When VO durations change, recalculate affected shot durations and propagate changes through all Duration Sync Points (see below).

---

## Implementation Order

Must follow this sequence to avoid dependency issues:

```dot
digraph implementation_order {
    rankdir=TB;
    "1. Shared Components" [shape=box];
    "2. Shot Components" [shape=box];
    "3. Audio Layers" [shape=box];
    "4. Main Composition" [shape=box];

    "1. Shared Components" -> "2. Shot Components";
    "2. Shot Components" -> "3. Audio Layers";
    "3. Audio Layers" -> "4. Main Composition";
}
```

1. **Shared Components** - FlatDecorations, Card, DataTable, BarChart, plus motion-graphics from `resources.yaml` `components` section
2. **Shot Components** - Chapter-by-chapter implementation
3. **Audio Layers** - VoiceoverLayer, BackgroundMusicLayer, SfxLayer
4. **Main Composition** - Assembly with TransitionSeries

### Motion-Graphics Components

Read `manifests/resources.yaml` → `components` section. Each `type: motion-graphics` entry describes a visual element that needs a React component in `src/components/`.

**Implementation rules:**
- Read the `description` field for visual intent (what to show, what data to include)
- Read the `used_in` field to know which shots will import this component
- Cross-reference with `storyboard.yaml` → corresponding shots' `visual_description` for layout and composition context
- Use primitives (AnimatedText, AnimatedChart, DynamicBackground, etc.) as building blocks
- Component naming: derive from the description, use PascalCase (e.g., "三级上报流程图" → `EscalationFlowDiagram.tsx`)

---

## Primitives 组件库

template-project 预置了 `src/primitives/` 组件库,提供电影级的视觉原子能力。
**所有 shot 实现必须优先使用 primitives 组件,禁止从零实现已有 primitive 覆盖的功能。**

### 导入

```tsx
import {
  KenBurns,
  AnimatedText,
  AnimatedChart,
  Transition,
  BreathingSpace,
  CinematicBackdrop,
  SplitScreen,
  DynamicBackground,
  MaskReveal,
  VideoClip,
  ParticleSystem,
  GlitchEffect,
  EdgeDissolve,
  NeonText,
  FilmGrain,
  GlowEffect,
  FlashOverlay,
  ColorSweep,
} from '../primitives';
```

### Primitive Selection Heuristics

When a shot needs one of these visual jobs, prefer the matching primitive before creating a custom component:

| Visual job | Prefer these primitives |
|-----------|-------------------------|
| 静态图片运镜 | `KenBurns` |
| 标题/数字/强调文字 | `AnimatedText`, `NeonText` |
| 数据图表 | `AnimatedChart` |
| 背景层 | `DynamicBackground`, `BreathingSpace`, `CinematicBackdrop` |
| 氛围纹理 | `ParticleSystem`, `FilmGrain`, `GlowEffect` |
| 失真/崩解 | `GlitchEffect`, `EdgeDissolve` |
| 全屏强调/扫屏 | `FlashOverlay`, `ColorSweep` |
| 几何揭示 | `MaskReveal`, `Transition` |
| 视频素材 | `VideoClip` |

**Rule:** If the shot only needs one of the jobs above, do not invent a new effect component.

### KenBurns - 静态图片运镜

为静态图片添加电影级运镜效果,避免画面呆板。

**关键 Props:**

| Prop | Type | 说明 |
|------|------|------|
| src | string | 图片路径(staticFile) |
| effect | string | 运镜预设,共 10 种 + custom |
| scaleFrom | number | 起始缩放比例 |
| scaleTo | number | 结束缩放比例 |
| objectFit | string | 图片填充方式 |

**effect 可选值:** `zoom-in`, `zoom-out`, `pan-left`, `pan-right`, `pan-up`, `pan-down`, `zoom-in-top-left`, `zoom-in-top-right`, `zoom-in-bottom-left`, `zoom-in-bottom-right`, `custom`

**Storyboard 映射:** `camera_movement` → `effect`(`static` 时不使用 KenBurns)

```tsx
<Sequence from={startFrame} durationInFrames={duration} name="Shot-No.">
  <KenBurns src={IMAGES.hero} effect="zoom-in" />
</Sequence>
```

### AnimatedText - 文字动画

6 种动画模式,覆盖标题、正文、数据等文字出场需求。

**模式与关键 Props:**

| mode | 关键 Props | 用途 |
|------|-----------|------|
| typewriter | text, speed | 逐字打字效果 |
| fade-up | text, unit("char"\|"word"\|"line") | 从下方淡入 |
| fade-down | text, unit | 从上方淡入 |
| spring-in | text, unit | 弹性出现 |
| highlight | text, highlights, highlightColor | 关键词高亮 |
| counter | from, to, suffix, prefix | 数字滚动计数 |

```tsx
// 标题弹性出现
<AnimatedText mode="spring-in" text="核心发现" unit="char" style={{ fontSize: 72, fontWeight: 700 }} />

// 数字计数
<AnimatedText mode="counter" from={0} to={2136} suffix="个" style={{ fontSize: 96 }} />

// 关键词高亮
<AnimatedText mode="highlight" text="这是一个重要的发现" highlights={["重要"]} highlightColor="#FBBF24" />
```

### AnimatedChart - 数据动画

数据可视化动画组件,替代静态 Recharts 图表。

**关键 Props:**

| Prop | Type | 说明 |
|------|------|------|
| type | "bar" \| "horizontal-bar" \| "progress-ring" | 图表类型 |
| data | { label, value, color }[] | 数据数组 |
| stagger | number | 各数据项动画间隔(秒) |

```tsx
<AnimatedChart
  type="horizontal-bar"
  data={[
    { label: "React", value: 85, color: "#61DAFB" },
    { label: "Vue", value: 60, color: "#4FC08D" },
  ]}
  stagger={0.1}
/>
```

### Transition - 转场效果

7 种转场效果,用于 shot 之间的视觉过渡。

**关键 Props:**

| Prop | Type | 说明 |
|------|------|------|
| type | string | 转场类型,共 7 种 |
| direction | "in" \| "out" | 入场或离场 |
| durationSec | number | 转场持续时间(秒) |

**type 可选值:** `circle-wipe`, `diagonal-wipe`, `iris`, `curtain`, `blinds`, `zoom-fade`, `dissolve-blur`

**Storyboard 映射:** `transition_in.type` → `type`,`transition_in.duration_ms` → `durationSec`

```tsx
<Transition type="circle-wipe" durationSec={0.8}>
  <ShotContent />
</Transition>
```

### BreathingSpace - 呼吸段

用于章节间的视觉休息,让观众消化信息。

**关键 Props:**

| Prop | Type | 说明 |
|------|------|------|
| variant | string | 呼吸段样式,共 5 种 |
| color | string | 前景色 |
| backgroundColor | string | 背景色 |
| text | string | 可叠加的章节标题文字 |
| textStyle | CSSProperties | 文字样式 |

**variant 可选值:** `gradient`, `fade-black`, `fade-white`, `particles`, `radial-pulse`

**Storyboard 映射:** `breathing=true` 时整个 shot 使用 BreathingSpace

```tsx
<BreathingSpace
  variant="particles"
  color="#ffffff"
  backgroundColor="#0a0a0a"
  text="第二章"
  textStyle={{ fontSize: 48, fontWeight: 300, color: '#ffffff' }}
/>
```

### SplitScreen - 分屏

同时展示两个内容区域,用于对比、前后对照等场景。

**关键 Props:**

| Prop | Type | 说明 |
|------|------|------|
| layout | "horizontal" \| "vertical" \| "pip" | 分屏布局方式 |
| left / right | ReactNode | 左右内容(horizontal/vertical 布局) |
| leftLabel / rightLabel | string | 分屏标签 |

```tsx
<SplitScreen
  layout="horizontal"
  left={<KenBurns src={IMAGES.before} effect="zoom-in" />}
  right={<KenBurns src={IMAGES.after} effect="zoom-in" />}
  leftLabel="2007"
  rightLabel="2024"
/>
```

### DynamicBackground - 动态背景

为 shot 提供动态背景层,增加视觉丰富度和电影感。

**关键 Props:**

| Prop | Type | 说明 |
|------|------|------|
| variant | string | 背景样式,共 6 种 |
| colors | string[] | 渐变颜色数组 |
| intensity | number | 效果强度(用于 vignette) |

**variant 可选值:** `flowing-gradient`, `mesh-gradient`, `grid`, `dots`, `vignette`, `aurora`

**关键:** `vignette` 可叠加在任何内容上方增加电影感暗角效果。

```tsx
// 作为 shot 背景
<DynamicBackground variant="mesh-gradient" colors={COLORS.gradient}>
  {/* shot 内容放在 children 中 */}
  <div style={{ padding: 80 }}>
    <AnimatedText mode="fade-up" text="标题" />
  </div>
</DynamicBackground>

// 叠加暗角
<DynamicBackground variant="vignette" intensity={0.5} />
```

### CinematicBackdrop / ParticleSystem / FilmGrain - 氛围层

用于 breathing shot、章节间隙、低信息密度镜头的电影感底层。

- `CinematicBackdrop`:克制、稀疏、中心呼吸光
- `ParticleSystem`:漂浮/上升/径向粒子气氛
- `FilmGrain`:全局颗粒,降低“太干净”的数字感

```tsx
<AbsoluteFill>
  <CinematicBackdrop baseColor="#020617" glowColor={COLORS.primary} />
  <ParticleSystem pattern="float" opacity={0.22} color="#dbeafe" />
  <FilmGrain intensity={0.03} />
</AbsoluteFill>
```

### GlitchEffect / EdgeDissolve - 失真层

用于故障、回忆破碎、信息污染、虚实边界崩塌等镜头。

```tsx
<GlitchEffect intensity={0.35} burst burstInterval={1.6} burstDuration={0.12}>
  <EdgeDissolve intensity={0.55} baseColor="rgba(2, 6, 23, 1)">
    <ShotContent />
  </EdgeDissolve>
</GlitchEffect>
```

### NeonText / GlowEffect - 风格化标题层

当 storyboard 指向 synthetic UI、警报、科技感提示、强风格标题时优先使用。

```tsx
<AbsoluteFill style={{ justifyContent: 'center', alignItems: 'center' }}>
  <GlowEffect color="#67e8f9" intensity={0.25}>
    <div style={{ textAlign: 'center' }}>
      <NeonText text="SIGNAL LOST" color="cyan" flicker />
    </div>
  </GlowEffect>
</AbsoluteFill>
```

### FlashOverlay / ColorSweep - 强调层

用于冲击式章节切换、记忆闪回、扫屏强调,不必为这类全屏 2D 效果单独再写 transition 组件。

```tsx
<AbsoluteFill>
  <ShotContent />
  <ColorSweep color="#FBBF24" direction="left-to-right" />
  <FlashOverlay color="#ffffff" intensity={0.7} />
</AbsoluteFill>
```

### MaskReveal - 遮罩揭示

通过遮罩动画揭示内容,适合重点内容的戏剧性呈现。

**关键 Props:**

| Prop | Type | 说明 |
|------|------|------|
| shape | string | 遮罩形状,共 10 种 |
| durationSec | number | 揭示动画时长(秒) |

**shape 可选值:** `circle`, `ellipse`, `rectangle`, `diamond`, `wipe-left`, `wipe-right`, `wipe-up`, `wipe-down`, `split-horizontal`, `split-vertical`

```tsx
<MaskReveal shape="circle" durationSec={1.0}>
  <Img src={IMAGES.reveal} style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
</MaskReveal>
```

### VideoClip - 视频嵌入

嵌入视频片段,支持裁剪、变速、音量控制和淡入淡出。

**关键 Props:**

| Prop | Type | 说明 |
|------|------|------|
| src | string | 视频文件路径 |
| playbackRate | number | 播放速率 |
| volume | number | 音量(0-1) |
| muted | boolean | 是否静音 |
| startFromSec | number | 视频起始秒数 |
| endAtSec | number | 视频结束秒数 |
| fadeInSec | number | 淡入时长(秒) |
| fadeOutSec | number | 淡出时长(秒) |
| objectFit | string | 视频填充方式 |
| overlay | string | 叠加颜色层 |

```tsx
<VideoClip
  src={VIDEO.demo_recording}
  fadeInSec={0.5}
  fadeOutSec={0.5}
  playbackRate={1.5}
  volume={0}
  overlay="rgba(0,0,0,0.2)"
/>
```

### Storyboard → 组件映射规则

| storyboard 字段 | 组件/属性 | 说明 |
|----------------|----------|------|
| camera_movement | KenBurns.effect | static 时不使用 KenBurns,其他值直接映射 |
| framing | 构图尺寸和元素比例 | ECU=主体占满画面, LS=主体占画面 1/3 |
| pacing | 动画 duration 和 stagger | slow=长时值+慢stagger, fast=短时值+快stagger |
| visual_tension | spring.damping 和动画幅度 | 低张力=高damping(柔和), 高张力=低damping(弹跳) |
| transition_in.type | Transition.type | 直接映射 |
| breathing=true | BreathingSpace / CinematicBackdrop | 低信息密度章节停顿优先使用二者之一 |
| audio_visual_relation | Audio Sequence.from 偏移 | lead-visual: 画面提前; lead-audio: 音频提前 |
| visual_tension ≥ 0.7 | Glow / distortion / texture layers | 优先考虑 `GlowEffect`、`GlitchEffect`、`EdgeDissolve`、`FilmGrain` |
| synthetic / warning / HUD 风格标题 | NeonText | 不要手写发光文字阴影 |

### 电影感强制规则(MANDATORY)

1. **禁止静态图片**:所有 `<Img>` 必须包裹在 `<KenBurns>` 中(除非图片是 UI 元素如图标)
2. **禁止静态文字**:所有首次出现的文字必须使用 `<AnimatedText>`(已显示的文字可以保持静态)
3. **禁止静态图表**:数据展示必须使用 `<AnimatedChart>`,禁止静态 Recharts
4. **必须有底层氛围背景**:每个 shot 的底层必须使用 `<DynamicBackground>`、`<BreathingSpace>` 或 `<CinematicBackdrop>` 之一
5. **暗角规则**:在非纯白背景的 shot 上叠加 `<DynamicBackground variant="vignette" intensity={0.4} />`。高张力 shot (visual_tension ≥ 0.7) 强烈建议使用。纯白背景除外。
6. **呼吸段必须实现**:storyboard 中 breathing=true 的 shot 必须使用 `<BreathingSpace>`
7. **呼吸段音频处理**:breathing shot 期间,BGM 应通过 `interpolate` 淡出到 volume 0.05;下一个 shot 开始时 BGM 淡入恢复。不播放旁白。
8. **质感层优先复用**:颗粒、发光、扫屏、故障、边缘崩解优先使用 `FilmGrain`、`GlowEffect`、`ColorSweep`、`GlitchEffect`、`EdgeDissolve`

---

## Critical Rules

### Frame Calculation (MANDATORY)

**All frame values must be integers.** Floating-point values cause animation jitter or crashes.

```typescript
// CORRECT - always use Math.round()
const startFrame = Math.round(delaySec * fps);
const startFrame = secToFrames(delaySec);  // from timing.ts

// WRONG - produces float
const startFrame = delaySec * fps;  // 0.3 * 30 = 8.999...
```

### interpolate Safety (MANDATORY)

The `inputRange` array must be strictly monotonically increasing. Equal values crash.

**Error message:**
```
inputRange must be strictly monotonically increasing but got [27, 27]
```

**Cause:** Zero-length content producing zero duration:

```typescript
// DANGEROUS - empty text causes zero duration
const typingDuration = secToFrames(line.text.length * 0.02);  // = 0 if text.length = 0
interpolate(frame, [start, start + typingDuration], ...);  // [27, 27] CRASH
```

**Solution:**

```typescript
// SAFE - guarantee minimum 1-frame duration
const typingDuration = Math.max(secToFrames(line.text.length * 0.02), 1);

// Alternative: skip interpolation for zero-length content
const charsVisible = line.text.length === 0
  ? 0
  : Math.floor(interpolate(frame, [start, end], [0, line.text.length], {
      extrapolateLeft: "clamp",
      extrapolateRight: "clamp",
    }));
```

**Rule:** Any dynamic calculation that feeds into interpolate must be wrapped in `Math.max(..., 1)`.

### extrapolate Clamp (MANDATORY)

Always add clamp to prevent values outside range:

```typescript
interpolate(frame, [startFrame, endFrame], [0, 1], {
  extrapolateLeft: "clamp",
  extrapolateRight: "clamp",
});
```

---

## 1080p Visual Standards

### Font Size Guidelines

Video viewing distance requires larger fonts than desktop UI.

| Element | Font Size | Example |
|---------|-----------|---------|
| Main title | 72-84px | Cover title, chapter headers |
| Subtitle | 48-56px | Card titles, section headers |
| Body text | 24-28px | Paragraphs, descriptions |
| Table content | 20-24px | Data cells |
| Annotation | 16-20px | Badges, footnotes |
| Code | 18-22px | Monospace blocks |

**Audit command:**
```bash
grep -rn "fontSize:" src/ --include="*.tsx" | grep -E "fontSize:\s*[0-9]{1,2}[^0-9]"
```

### Text Color Contrast

On light backgrounds, use dark text. Light text on light backgrounds becomes unreadable after video compression.

```typescript
// Text color hierarchy
export const COLORS = {
  textDark: "#020617",      // Titles, emphasis (near black)
  textPrimary: "#0F172A",   // Body text (dark blue-black)
  textSecondary: "#334155", // Secondary info (dark gray)
  // NEVER use lighter colors on white/light backgrounds
};
```

**Rule:** On `flatGray`, `flatBlue`, white backgrounds, use only `textDark` or `textPrimary`.

---

## Background Layer Architecture

> **优先使用 `<DynamicBackground>`**:新 shot 应使用 primitives 组件库中的 `DynamicBackground` 替代手动组合背景层。仅在 DynamicBackground 不满足需求时使用下面的手动方式。

### 推荐方式(使用 DynamicBackground)

```tsx
<AbsoluteFill>
  <DynamicBackground variant="mesh-gradient" colors={COLORS.gradient}>
    {/* Content */}
    {children}
  </DynamicBackground>
  <DynamicBackground variant="vignette" intensity={0.4} />
</AbsoluteFill>
```

### 手动方式 Layer Stack (bottom to top)

```tsx
<AbsoluteFill>
  <GradientBackground />                    {/* 1. Gradient base */}
  <GridDots style={{ opacity: 0.2 }} />     {/* 2. Texture layer */}
  <GaussianBlobs style={{ opacity: 0.3 }} /> {/* 3. Optional: depth */}

  {/* Content layers */}
  <div style={{ position: 'relative', zIndex: 1 }}>
    {children}
  </div>
</AbsoluteFill>
```

### Available Decorations

| Component | Purpose | Opacity Range |
|-----------|---------|---------------|
| `GradientBackground` | Soft gradient base | 1.0 |
| `GridDots` | Grid texture | 0.15-0.3 |
| `GaussianBlobs` | Floating color blobs | 0.2-0.4 |
| `FloatingCircles` | Animated circles | 0.3-0.5 |
| `TechGridBackground` | Technical aesthetic | 0.2-0.3 |
| `BezierCurveDecoration` | Flowing curves | 0.4-0.6 |

**Principle:** Background opacity should never compete with content readability.

---

## Shot Duration Management

### Shots Without Voiceover

Shots without voiceover need explicit duration. Do not use default values.

| Shot Type | Minimum Duration | Rationale |
|-----------|------------------|-----------|
| Image display | 4-5 seconds | Viewer comprehension |
| Code (10 lines or less) | 6-8 seconds | Reading time |
| Code (more than 10 lines) | 8-12 seconds | Complex content |
| QR code / CTA | 6-7 seconds | Action time |
| Data table | 5-8 seconds | Analysis time |
| Chart animation | 4-6 seconds | Animation + comprehension |

### Voiceover-Based Duration

```typescript
function calculateShotDuration(voIds: string[], bufferMs = 700): number {
  const totalVoMs = voIds.reduce((sum, id) => sum + (VO_DURATIONS[id] || 0), 0);
  return msToFrames(totalVoMs + bufferMs);
}

// Example
const shotDuration = calculateShotDuration(['VO_005', 'VO_006', 'VO_007']);
```

### Duration Sync Points

When modifying shot durations, update ALL of these:

1. `SHOT_DURATIONS` constant (or equivalent timing config)
2. `<Sequence from={} durationInFrames={}>` for the shot
3. `<Sequence from={}>` for ALL subsequent shots in same chapter
4. Chapter duration exports (`CHAPTER1_DURATION`, etc.)
5. Root composition `durationInFrames`

---

## Animation Timing

### Staggered Entry Pattern

Elements should enter sequentially, not simultaneously:

```typescript
{items.map((item, index) => {
  const delay = 0.5 + index * 0.12;  // 0.12s between each
  const opacity = interpolate(
    frame,
    [secToFrames(delay), secToFrames(delay + 0.3)],
    [0, 1],
    { extrapolateLeft: "clamp", extrapolateRight: "clamp" }
  );
  const translateY = interpolate(
    frame,
    [secToFrames(delay), secToFrames(delay + 0.3)],
    [20, 0],
    { extrapolateLeft: "clamp", extrapolateRight: "clamp" }
  );

  return (
    <div key={index} style={{ opacity, transform: `translateY(${translateY}px)` }}>
      {item}
    </div>
  );
})}
```

### Common Animation Combinations

| Effect | Properties |
|--------|------------|
| Fade in + rise | `opacity: 0->1`, `translateY: 20->0` |
| Fade in + scale | `opacity: 0->1`, `scale: 0.95->1` |
| Slide from left | `translateX: -30->0` |
| Pop in | `scale: 0->1.05->1` (spring) |

### Easing Functions

```typescript
import { Easing, spring } from 'remotion';

// Smooth deceleration
const progress = interpolate(frame, [0, 30], [0, 1], {
  easing: Easing.out(Easing.cubic),
});

// Spring animation
const { fps } = useVideoConfig();
const springProgress = spring({
  frame,
  fps,
  config: { damping: 12, stiffness: 100 },
});
```

---

## Audio Architecture

### Three-Layer Model

```
+-------------------------------------+
|  SFX Layer (transitions, accents)   |  Volume: 0.3-0.5
+-------------------------------------+
|  VO Layer (voiceover)               |  Volume: 1.0
+-------------------------------------+
|  BGM Layer (background music)       |  Volume: 0.1-0.2
+-------------------------------------+
```

### Audio Positioning

**Use absolute frame calculation, not nesting:**

```typescript
// CORRECT - Absolute positioning
const voStartFrame = computeShotStartFrame('shot_005') + msToFrames(200);

// WRONG - Nested positioning (fragile, breaks on refactor)
<Sequence from={shotStart}>
  <Sequence from={200}>
    <Audio ... />
  </Sequence>
</Sequence>
```

### BGM Volume Fading

```typescript
const bgmVolume = interpolate(
  frame,
  [0, secToFrames(2), totalFrames - secToFrames(3), totalFrames],
  [0, 0.15, 0.15, 0],
  { extrapolateLeft: "clamp", extrapolateRight: "clamp" }
);
```

---

## Static Assets

### staticFile() Usage

```typescript
import { staticFile, Img, Audio } from 'remotion';
import { KenBurns } from '../primitives';

// Images - 优先使用 KenBurns 包裹(电影感强制规则)
<KenBurns src={staticFile("images/diagram.png")} effect="zoom-in" />

// 仅 UI 图标等小元素可直接使用 Img
<Img src={staticFile("images/icon.png")} />

// Audio - use Remotion's Audio component
<Audio src={staticFile("audio/vo/VO_001.wav")} />

// NOT native HTML elements
// <img src="/images/diagram.png" />  // WRONG
// <audio src="/audio/vo/VO_001.wav" />  // WRONG
```

**Why:** Remotion needs to track asset loading state for accurate frame rendering.

### Path Resolution

```
staticFile("audio/vo/VO_001.wav")
-> public/audio/vo/VO_001.wav
```

Paths are relative to `public/` directory. Case-sensitive.

---

## Sprite Sheet Animation

### SpriteSheet Component Pattern

Use this pattern to render sprite sheet animations in Remotion. The component slices a single sprite sheet image into individual frames using CSS `background-position`.

```tsx
import { useCurrentFrame, useVideoConfig, staticFile, Img } from "remotion";

interface SpriteSheetProps {
  /** Path relative to public/ (e.g. "images/robot_sprite.png") */
  src: string;
  /** Number of columns in the sprite grid */
  cols: number;
  /** Number of rows in the sprite grid */
  rows: number;
  /** Which row to animate (0-indexed) */
  row: number;
  /** Number of frames in this animation row */
  frameCount: number;
  /** Playback rate: frames per second for the sprite animation */
  spriteFrameRate?: number;
  /** Display width in pixels */
  width: number;
  /** Display height in pixels */
  height: number;
  /** Whether the sprite has a transparent background (pre-processed via validate_sprite tool with fix_chroma=true). Informational only - does not perform runtime chroma removal. */
  transparent?: boolean;
}

const SpriteSheet: React.FC<SpriteSheetProps> = ({
  src,
  cols,
  rows,
  row,
  frameCount,
  spriteFrameRate = 12,
  width,
  height,
  transparent = true,
}) => {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();

  // Calculate which sprite frame to show
  const spriteFrame = Math.floor((frame / fps) * spriteFrameRate) % frameCount;

  // background-position offsets
  const bgWidth = cols * width;
  const bgHeight = rows * height;
  const offsetX = -(spriteFrame * width);
  const offsetY = -(row * height);

  return (
    <div
      style={{
        width,
        height,
        backgroundImage: `url(${staticFile(src)})`,
        backgroundSize: `${bgWidth}px ${bgHeight}px`,
        backgroundPosition: `${offsetX}px ${offsetY}px`,
        backgroundRepeat: "no-repeat",
        imageRendering: "pixelated",
      }}
    />
  );
};
```

### Chroma Key (Magenta Removal)

For sprite sheets with magenta (#ff00ff) backgrounds, apply chroma key removal during the asset pipeline phase. Convert magenta to transparency using Python before migrating to `public/`:

```python
from PIL import Image
import numpy as np

img = Image.open("sprite.png").convert("RGBA")
data = np.array(img)
magenta = (data[:,:,0] > 240) & (data[:,:,1] < 15) & (data[:,:,2] > 240)
data[magenta] = [0, 0, 0, 0]
Image.fromarray(data).save("sprite_transparent.png")
```

After chroma key removal, the sprite sheet has a transparent background and can be rendered directly without additional processing.

### Usage Example

```tsx
// Robot walking animation from row 1 of an 8x4 sprite sheet
<SpriteSheet
  src="images/robot_sprite.png"
  cols={8}
  rows={4}
  row={1}
  frameCount={8}
  spriteFrameRate={12}
  width={256}
  height={256}
/>
```

### Sprite Animation Timing

| Animation Type | Recommended spriteFrameRate | Notes |
|----------------|----------------------------|-------|
| Idle / breathing | 6-8 fps | Slow, subtle motion |
| Walking | 10-12 fps | Natural pace |
| Running | 14-16 fps | Fast motion |
| Jumping | 12 fps | Single action, may not loop |

### Integration with Shot Components

```tsx
const ShotWithCharacter: React.FC = () => {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();

  // Switch animation based on timeline
  const walkStart = Math.round(1.0 * fps);
  const runStart = Math.round(3.0 * fps);

  let animRow = 0; // idle
  if (frame >= runStart) animRow = 2; // run
  else if (frame >= walkStart) animRow = 1; // walk

  return (
    <AbsoluteFill>
      <GradientBackground />
      <div style={{ position: "absolute", bottom: 100, left: "50%", transform: "translateX(-50%)" }}>
        <SpriteSheet
          src="images/robot_sprite.png"
          cols={8} rows={4}
          row={animRow}
          frameCount={8}
          spriteFrameRate={12}
          width={256} height={256}
        />
      </div>
    </AbsoluteFill>
  );
};
```

---

## SVG Guidelines

### ViewBox Setup

```tsx
<svg viewBox="0 0 1920 1080" width="100%" height="100%">
  {/* All coordinates designed for 1920x1080 */}
</svg>
```

### Container Pattern

```tsx
<AbsoluteFill>
  <svg viewBox="0 0 1920 1080" style={{ width: '100%', height: '100%' }}>
    {/* SVG content */}
  </svg>
</AbsoluteFill>
```

**Do NOT:**
- Use `width={1920}` as DOM attribute
- Use pixel units without viewBox context
- Animate complex paths without testing performance

---

## Data Visualization

### Highlighting Important Data

| Technique | Implementation |
|-----------|---------------|
| Background color | `flatGreen` background + `success` border |
| Icon prefix | star BEST, checkmark |
| Font weight | `fontWeight: 700-800` |
| Size increase | 20-30% larger than surrounding |

```tsx
{isHighlighted && (
  <tr style={{
    backgroundColor: COLORS.flatGreen,
    borderLeft: `4px solid ${COLORS.success}`,
  }}>
    <td style={{ fontWeight: 700 }}>{data}</td>
  </tr>
)}
```

---

## Single-Image Shot Enhancement

**Problem:** A shot with just one image lacks information density.

**Solution:** Upgrade to composite layout using primitives:

| Enhancement | 组件 | Purpose |
|-------------|------|---------|
| 图片运镜 | `KenBurns` | 让图片产生运动感,避免静帧 |
| 统计数字 | `AnimatedText mode="counter"` | 数据动画展示 |
| 分屏对比 | `SplitScreen` | 前后对照或并列展示 |
| 动态背景 | `DynamicBackground` | 底层视觉丰富度 |
| 遮罩揭示 | `MaskReveal` | 戏剧性图片呈现 |
| 暗角叠加 | `DynamicBackground variant="vignette"` | 电影感 |

### 实现模板

```tsx
const ShotWithImage: React.FC = () => {
  return (
    <AbsoluteFill>
      {/* 1. 动态背景 */}
      <DynamicBackground variant="mesh-gradient" colors={COLORS.gradient}>
        <div style={{ display: 'flex', height: '100%', padding: 60 }}>
          {/* 左侧:文字和数据 */}
          <div style={{ flex: 1, display: 'flex', flexDirection: 'column', justifyContent: 'center' }}>
            <AnimatedText mode="spring-in" text="标题" style={{ fontSize: 72, fontWeight: 700 }} />
            <AnimatedText mode="fade-up" text="描述文字" style={{ fontSize: 28, marginTop: 20 }} />
            <div style={{ display: 'flex', gap: 40, marginTop: 40 }}>
              <AnimatedText mode="counter" from={0} to={1200} suffix="万" style={{ fontSize: 48 }} />
              <AnimatedText mode="counter" from={0} to={96} suffix="%" style={{ fontSize: 48 }} />
            </div>
          </div>
          {/* 右侧:图片运镜 */}
          <div style={{ flex: 1 }}>
            <KenBurns src={IMAGES.hero} effect="zoom-in" />
          </div>
        </div>
      </DynamicBackground>
      {/* 叠加暗角 */}
      <DynamicBackground variant="vignette" intensity={0.4} />
    </AbsoluteFill>
  );
};
```

### Layout Template

```
+---------------------------------------------+
|  [Title]                                    |
|  [Subtitle]                                 |
+---------+---------+---------+---------------+
| Stat 1  | Stat 2  | Stat 3  |               |
+---------+---------+---------+   [Image]     |
| +-------+  +-------+        |               |
| | Card  |  | Card  |        |               |
| +-------+  +-------+        |               |
| +-------+  +-------+        +---------------+
| | Card  |  | Card  |        | Summary       |
| +-------+  +-------+        |               |
+-----------------------------+---------------+
```

---

## Project Structure

```
src/
  primitives/           # 预置组件库(电影级视觉原子)
    index.ts           # Barrel export
    KenBurns.tsx
    AnimatedText.tsx
    AnimatedChart.tsx
    Transition.tsx
    BreathingSpace.tsx
    SplitScreen.tsx
    DynamicBackground.tsx
    MaskReveal.tsx
    VideoClip.tsx
    CinematicBackdrop.tsx
    ParticleSystem.tsx
    GlitchEffect.tsx
    EdgeDissolve.tsx
    NeonText.tsx
    FilmGrain.tsx
    GlowEffect.tsx
    FlashOverlay.tsx
    ColorSweep.tsx
  components/           # Reusable components
    index.ts           # Barrel export
    FlatDecorations.tsx
    Card.tsx
    DataTable.tsx
    BarChart.tsx
  shots/               # Chapter/shot components
    Chapter1/
      index.tsx        # Chapter entry point
      Shot001_TitleCard.tsx
      Shot002_*.tsx
    Chapter2/
  audio/               # Audio layer components
    BackgroundMusicLayer.tsx
    VoiceoverLayer.tsx
    SfxLayer.tsx
  lib/                 # Utilities
    constants.ts       # Colors, fonts, FPS
    timing.ts          # Frame calculation
    resourceMap.ts     # Asset path mapping
  Composition.tsx      # Main composition
  Root.tsx             # Remotion entry
```

### Naming Conventions

| Type | Pattern | Example |
|------|---------|---------|
| Shot file | `Shot{NNN}_{Description}.tsx` | `Shot001_TitleCard.tsx` |
| Component | PascalCase | `DataTable.tsx` |
| Utility | camelCase | `timing.ts` |
| Constant | UPPER_SNAKE | `FPS`, `SHOT_DURATIONS` |

---

## Workflow

1. **Read manifests/storyboard.yaml**
2. **Implement shared components first**
   - Background decorations
   - Card, Badge components
   - Data visualization components
3. **Implement shots chapter by chapter**
   - Follow storyboard visual descriptions
   - Apply animation timing patterns
   - Run type check after each chapter
4. **Implement audio layers**
   - VoiceoverLayer with absolute positioning
   - BackgroundMusicLayer with fade in/out
   - SfxLayer for transitions
5. **Assemble main Composition**
   - TransitionSeries for chapter transitions
   - Calculate total duration

---

## User Communication

### Progress Report

```
分镜实现进度:

已完成:
  - 共享组件 (7 个)
  - Chapter 1: 5/5 镜头
  - Chapter 2: 3/8 镜头

当前: Shot008_DataVisualization

类型检查: 通过
预览: http://localhost:3000

预计剩余: Chapter 2 (5 镜头), Chapter 3 (10 镜头), Chapter 4 (8 镜头)
```

### Completion Report

```
分镜实现完成。

统计:
  - 共享组件: 12 个
  - 章节: 4 个
  - 镜头: 38 个
  - 音频层: 3 个

总时长: 7834 帧 (4分21秒 @ 30fps)

类型检查: 通过
预览: http://localhost:3000

准备进入预览审查阶段。
```

---

## Common Mistakes

| Error | Cause | Fix |
|-------|-------|-----|
| `inputRange must be strictly monotonically increasing` | Equal inputRange values | `Math.max(duration, 1)` |
| `TS6133: declared but never read` | Unused variable | Delete unused code |
| Shot cuts off early | Sequence duration mismatch | Sync all duration references |
| Audio offset | Duration changed but offset not | Adjust VO_SHOT_MAP offsets |
| Studio white screen | Component render error | Check DevTools console |
| Blurry text | Font size too small | Increase to 20px+ minimum |
| Low contrast text | Light text on light bg | Use textPrimary/textDark |

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.