# Advanced Remotion Templates Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Extend the caption animation system with 4 new highlight styles, 2 new segment transitions, 3 new animation fields, and ship 2 system presets ("Шортс" and "Подкаст").
**Architecture:** Additive changes only — extend existing `CaptionStyleSchema` with new enum values and fields, implement new animation rendering in `Captions.tsx` using Remotion primitives, seed 2 new system presets via Alembic migration, and add new form controls to the frontend StyleEditor.
**Tech Stack:** Remotion (`interpolate()`, `spring()`), ElysiaJS (Elysia `t.*` validation), FastAPI + Pydantic + Alembic, Next.js + react-hook-form
**Spec:** `docs/superpowers/specs/2026-03-21-advanced-remotion-templates-design.md`
---
## File Map
### Remotion Service (`remotion_service/`)
| Action | File | Responsibility |
|--------|------|----------------|
| Modify | `server/types/CaptionStyleSchema.ts` | Add new enum values + 3 new fields to Elysia validation schema |
| Modify | `src/types/caption_style.d.ts` | Mirror TypeScript type changes |
| Modify | `src/components/Captions.tsx` | Implement 4 new highlight renderers, 2 new transitions, word entrance logic, rotation, text-transform |
### Backend (`cofee_backend/`)
| Action | File | Responsibility |
|--------|------|----------------|
| Modify | `cpv3/modules/captions/schemas.py` | Extend Pydantic `CaptionAnimationStyle` with new Literal values + 3 fields |
| Create | `alembic/versions/e6f7a8b9c0d1_seed_shorts_podcast_presets.py` | Seed 2 new system presets |
### Frontend (`cofee_frontend/`)
| Action | File | Responsibility |
|--------|------|----------------|
| Modify | `src/features/project/CaptionSettingsStep/StyleEditor.tsx` | Add new select options + 3 new form fields to AnimationFields |
---
## Task 1: Extend Remotion Schema & Types
**Files:**
- Modify: `remotion_service/server/types/CaptionStyleSchema.ts` (lines 30-47)
- Modify: `remotion_service/src/types/caption_style.d.ts` (lines 20-26)
- [ ] **Step 1: Update Elysia validation schema**
In `remotion_service/server/types/CaptionStyleSchema.ts`, replace the `CaptionAnimationStyle` object (lines 30-47) with:
```typescript
export const CaptionAnimationStyle = t.Object({
highlight_style: t.Union(
[
t.Literal("color"),
t.Literal("scale"),
t.Literal("underline"),
t.Literal("color_scale"),
t.Literal("pop_in"),
t.Literal("karaoke"),
t.Literal("bounce"),
t.Literal("glow_pulse"),
],
{ default: "color" },
),
highlight_scale: t.Number({ default: 1.1 }),
segment_transition: t.Union(
[
t.Literal("fade"),
t.Literal("slide"),
t.Literal("none"),
t.Literal("zoom_in"),
t.Literal("drop_in"),
],
{ default: "fade" },
),
fade_duration_frames: t.Number({ default: 3 }),
animation_speed: t.Number({ default: 1.0 }),
word_entrance: t.Union(
[t.Literal("none"), t.Literal("pop"), t.Literal("typewriter")],
{ default: "none" },
),
highlight_rotation_deg: t.Number({ default: 0 }),
text_transform: t.Union(
[t.Literal("none"), t.Literal("uppercase"), t.Literal("lowercase")],
{ default: "none" },
),
});
```
- [ ] **Step 2: Update TypeScript type definitions**
In `remotion_service/src/types/caption_style.d.ts`, replace the `CaptionAnimationStyle` type (lines 20-26) with:
```typescript
export type CaptionAnimationStyle = {
highlight_style:
| "color"
| "scale"
| "underline"
| "color_scale"
| "pop_in"
| "karaoke"
| "bounce"
| "glow_pulse";
highlight_scale: number;
segment_transition: "fade" | "slide" | "none" | "zoom_in" | "drop_in";
fade_duration_frames: number;
animation_speed: number;
word_entrance: "none" | "pop" | "typewriter";
highlight_rotation_deg: number;
text_transform: "none" | "uppercase" | "lowercase";
};
```
- [ ] **Step 3: Type-check Remotion service**
Run: `cd remotion_service && bunx tsc --noEmit`
Expected: PASS — no type errors (existing code uses only the old enum values, which are still present)
- [ ] **Step 4: Commit**
```bash
cd remotion_service
git add server/types/CaptionStyleSchema.ts src/types/caption_style.d.ts
git commit -m "feat(remotion): extend CaptionAnimationStyle schema with new highlight styles, transitions, and fields"
```
---
## Task 2: Implement New Highlight Styles in Captions.tsx
**Files:**
- Modify: `remotion_service/src/components/Captions.tsx` (lines 57-136 — `StyledWord` component)
This is the largest task. We modify the `StyledWord` component to handle 4 new highlight styles. The current `isCurrent` block (lines 86-133) handles `color`, `scale`, `underline`, `color_scale`. We add `pop_in`, `karaoke`, `bounce`, `glow_pulse`.
- [ ] **Step 1: Add spring import**
At the top of `remotion_service/src/components/Captions.tsx`, add `spring` to the Remotion import (line 2):
```typescript
import { interpolate, spring, useVideoConfig } from "remotion";
```
- [ ] **Step 2: Add `useVideoConfig` to StyledWord**
Inside `StyledWord` (line 69), add `fps` extraction right after the destructure:
```typescript
const { fps } = useVideoConfig();
```
This is needed for `spring()` calls which require fps.
- [ ] **Step 3: Implement `pop_in` highlight style**
In `StyledWord`, inside the `if (isCurrent)` block (after the existing `underline` branch at line 132), add:
```typescript
if (animation.highlight_style === "pop_in") {
const wordDuration = wordFrameTime.end - wordFrameTime.start;
const scale =
wordDuration > MIN_INTERPOLATE_SPAN * 2
? spring({
fps,
frame: currentFrame - wordFrameTime.start,
config: { damping: 12, stiffness: 200 },
durationInFrames: Math.min(Math.ceil(wordDuration / 2), 15),
})
: 1;
const finalScale = interpolate(scale, [0, 1], [0, animation.highlight_scale]);
baseStyle.transform = `scale(${finalScale})`;
}
```
- [ ] **Step 4: Implement `karaoke` highlight style**
After the `pop_in` block, add:
```typescript
if (animation.highlight_style === "karaoke") {
const wordDuration = wordFrameTime.end - wordFrameTime.start;
const progress =
wordDuration > MIN_INTERPOLATE_SPAN
? interpolate(
currentFrame,
[wordFrameTime.start, wordFrameTime.end],
[0, 100],
{ extrapolateLeft: "clamp", extrapolateRight: "clamp" },
)
: 100;
baseStyle.background = `linear-gradient(to right, ${textStyle.highlight_color} ${progress}%, ${textStyle.text_color} ${progress}%)`;
baseStyle.WebkitBackgroundClip = "text";
baseStyle.WebkitTextFillColor = "transparent";
baseStyle.backgroundClip = "text";
// Override the color set above — karaoke uses gradient instead
baseStyle.color = undefined;
}
```
- [ ] **Step 5: Implement `bounce` highlight style**
After the `karaoke` block, add:
```typescript
if (animation.highlight_style === "bounce") {
const wordDuration = wordFrameTime.end - wordFrameTime.start;
const scale =
wordDuration > MIN_INTERPOLATE_SPAN * 2
? spring({
fps,
frame: currentFrame - wordFrameTime.start,
config: { damping: 8, stiffness: 180 },
durationInFrames: Math.min(Math.ceil(wordDuration), 20),
})
: 1;
const finalScale = interpolate(scale, [0, 1], [1, animation.highlight_scale]);
baseStyle.transform = `scale(${finalScale})`;
}
```
- [ ] **Step 6: Implement `glow_pulse` highlight style**
After the `bounce` block, add:
```typescript
if (animation.highlight_style === "glow_pulse") {
const wordDuration = wordFrameTime.end - wordFrameTime.start;
const pulse =
wordDuration > MIN_INTERPOLATE_SPAN * 2
? interpolate(
currentFrame,
[
wordFrameTime.start,
wordFrameTime.start + wordDuration * 0.25,
wordFrameTime.start + wordDuration * 0.5,
wordFrameTime.start + wordDuration * 0.75,
wordFrameTime.end,
],
[4, 12, 4, 12, 4],
{ extrapolateLeft: "clamp", extrapolateRight: "clamp" },
)
: 8;
baseStyle.textShadow = `0 0 ${pulse}px ${textStyle.highlight_color}, 0 0 ${pulse * 2}px ${textStyle.highlight_color}`;
}
```
- [ ] **Step 7: Add `highlight_rotation_deg` support**
After all highlight style branches (still inside `if (isCurrent)`), add rotation support:
```typescript
if (animation.highlight_rotation_deg > 0) {
const existingTransform = baseStyle.transform || "";
baseStyle.transform = `${existingTransform} rotate(${animation.highlight_rotation_deg}deg)`.trim();
}
```
- [ ] **Step 8: Type-check**
Run: `cd remotion_service && bunx tsc --noEmit`
Expected: PASS
- [ ] **Step 9: Commit**
```bash
cd remotion_service
git add src/components/Captions.tsx
git commit -m "feat(remotion): implement pop_in, karaoke, bounce, glow_pulse highlight styles + rotation"
```
---
## Task 3: Implement Word Entrance, Text Transform, and New Transitions
**Files:**
- Modify: `remotion_service/src/components/Captions.tsx`
- [ ] **Step 1: Add `useVideoConfig` to the Captions component**
At the top of the `Captions` component (around line 186), add fps extraction for `spring()` calls in transitions:
```typescript
const { fps: videoFps } = useVideoConfig();
```
This is needed by `drop_in` transition (Step 4) and must be declared before use.
- [ ] **Step 2: Implement `word_entrance` in StyledWord**
In `StyledWord`, BEFORE the `if (isCurrent)` block (around line 86), add word entrance logic. This controls how words appear before they're spoken. The spring also applies when the word is `isCurrent` (just started being spoken) so the pop-in is smooth regardless of highlight style:
```typescript
// Word entrance: controls visibility/scale of words before their start frame
const entrance = style.animation.word_entrance ?? "none";
if (entrance !== "none" && currentFrame < wordFrameTime.start) {
// Word hasn't been spoken yet — hide it
if (entrance === "pop") {
baseStyle.transform = "scale(0)";
baseStyle.opacity = 0;
} else if (entrance === "typewriter") {
baseStyle.opacity = 0;
}
} else if (entrance === "pop" && currentFrame >= wordFrameTime.start) {
// Word has been spoken (or is being spoken right now) — spring it in
const framesSinceStart = currentFrame - wordFrameTime.start;
const popScale = spring({
fps,
frame: framesSinceStart,
config: { damping: 12, stiffness: 200 },
durationInFrames: 10,
});
// Set as base transform — highlight styles will append to it if needed
baseStyle.transform = `scale(${popScale})`;
}
```
Note: The pop spring applies to BOTH `isCurrent` and past words. When `isCurrent`, the highlight style block below may overwrite `baseStyle.transform` (e.g., bounce sets its own scale). This is intentional — the highlight animation takes precedence once the word is active. For highlight styles that don't set transform (like `color` or `karaoke`), the pop spring provides the entrance animation.
- [ ] **Step 3: Implement `zoom_in` and `drop_in` segment transitions**
In the `Captions` component, after the existing `slide` transition block (around line 247), add both new transitions and a `scale` variable:
```typescript
let scale = 1;
if (transition === "zoom_in" && !hasShortSegment) {
opacity = interpolate(
currentFrame,
hasFadePlateau
? [start, fadeIn, fadeOut, end]
: [start, middleFrame, end],
hasFadePlateau ? [0, 1, 1, 0] : [0, 1, 0],
{ extrapolateLeft: "clamp", extrapolateRight: "clamp" },
);
scale = interpolate(
currentFrame,
hasFadePlateau
? [start, fadeIn, fadeOut, end]
: [start, middleFrame, end],
hasFadePlateau ? [0.8, 1, 1, 1.2] : [0.8, 1, 1.2],
{ extrapolateLeft: "clamp", extrapolateRight: "clamp" },
);
}
if (transition === "drop_in" && !hasShortSegment) {
opacity = interpolate(
currentFrame,
hasFadePlateau
? [start, fadeIn, fadeOut, end]
: [start, middleFrame, end],
hasFadePlateau ? [0, 1, 1, 0] : [0, 1, 0],
{ extrapolateLeft: "clamp", extrapolateRight: "clamp" },
);
const dropSpring = spring({
fps: videoFps,
frame: currentFrame - start,
config: { damping: 10, stiffness: 150 },
durationInFrames: Math.min(fadeDuration * 3, 20),
});
// Spring goes 0→1, we want -50→0
translateY = interpolate(dropSpring, [0, 1], [-50, 0]);
}
```
Note: `videoFps` comes from `useVideoConfig()` added in Step 1.
- [ ] **Step 4: Implement `text_transform` and update `segmentStyle.transform`**
In the `Captions` component (line 250, inside the inline style mode block), rebuild `segmentStyle` to include `textTransform` and the composite `transform` (accounting for translateY + scale):
```typescript
const textTransformValue = styleConfig.animation.text_transform ?? "none";
const segmentStyle: React.CSSProperties = {
opacity,
transform: [
translateY !== 0 ? `translateY(${translateY}px)` : "",
scale !== 1 ? `scale(${scale})` : "",
].filter(Boolean).join(" ") || undefined,
display: "flex",
flexDirection: "column",
alignItems: "center",
textAlign: "center",
width: "100%",
padding: background.bg_padding_px,
background: background.bg_color,
borderRadius: background.bg_border_radius_px,
textTransform: textTransformValue !== "none" ? textTransformValue : undefined,
};
```
- [ ] **Step 5: Update the CSS theme mode segment div to include scale**
In the CSS theme mode return (line 291), update the style to use the same composite transform:
```typescript
return (
```
- [ ] **Step 6: Type-check**
Run: `cd remotion_service && bunx tsc --noEmit`
Expected: PASS
- [ ] **Step 7: Visually verify in Remotion Studio**
Run: `cd remotion_service && bun run dev`
Open Remotion Studio in browser. Test each new animation by modifying the composition props in the studio UI:
- Set `highlight_style` to each of: `pop_in`, `karaoke`, `bounce`, `glow_pulse`
- Set `segment_transition` to each of: `zoom_in`, `drop_in`
- Set `word_entrance` to `pop` and `typewriter`
- Set `text_transform` to `uppercase`
- Set `highlight_rotation_deg` to `3`
Verify each renders without errors and the visual effect matches the spec description.
- [ ] **Step 8: Commit**
```bash
cd remotion_service
git add src/components/Captions.tsx
git commit -m "feat(remotion): implement word entrance, text transform, zoom_in and drop_in transitions"
```
---
## Task 4: Dynamic Font Loading
**Files:**
- Modify: `remotion_service/src/components/Captions.tsx` (lines 1-12 — imports and font loading)
Currently, only Lobster is loaded (line 3 + line 12). The new presets use Montserrat and Inter. We need to dynamically load the correct font based on `styleConfig.text.font_family`.
- [ ] **Step 1: Replace static font loading with dynamic loading**
Replace lines 1-12 of `Captions.tsx`:
```typescript
import React from "react";
import { interpolate, spring, useVideoConfig } from "remotion";
import { loadFont as loadLobster } from "@remotion/google-fonts/Lobster";
import { loadFont as loadInter } from "@remotion/google-fonts/Inter";
import { loadFont as loadMontserrat } from "@remotion/google-fonts/Montserrat";
import { loadFont as loadRoboto } from "@remotion/google-fonts/Roboto";
import { loadFont as loadOpenSans } from "@remotion/google-fonts/OpenSans";
import {
LineWithFrames,
SegmentWithFrames,
WordWithFrames,
} from "@/types/transcription";
import { CaptionStyleConfig } from "@/types/caption_style";
import { useTheme } from "@/hooks/useTheme";
// Load all supported fonts — Remotion deduplicates, only loads what's used
loadLobster();
loadInter();
loadMontserrat();
loadRoboto();
loadOpenSans();
```
Note: Remotion's `loadFont()` is idempotent and only triggers one network request per font. Loading all 5 is safe and keeps the component simple. The alternative (dynamic loading based on styleConfig) adds complexity for no real benefit since all 5 fonts are small.
- [ ] **Step 2: Install missing font packages (if needed)**
Run: `cd remotion_service && bunx tsc --noEmit`
If any `@remotion/google-fonts/*` imports fail, the packages are already included in `@remotion/google-fonts` — they're subpath exports, not separate packages. If there's an error, check:
Run: `cd remotion_service && bun run dev`
The fonts should load in the Remotion Studio preview.
- [ ] **Step 3: Commit**
```bash
cd remotion_service
git add src/components/Captions.tsx
git commit -m "feat(remotion): load Inter, Montserrat, Roboto, OpenSans fonts alongside Lobster"
```
---
## Task 5: Extend Backend Schema
**Files:**
- Modify: `cofee_backend/cpv3/modules/captions/schemas.py` (lines 37-42)
- [ ] **Step 1: Update CaptionAnimationStyle Pydantic model**
Replace lines 37-42 in `cofee_backend/cpv3/modules/captions/schemas.py`:
```python
class CaptionAnimationStyle(Schema):
highlight_style: Literal[
"color", "scale", "underline", "color_scale",
"pop_in", "karaoke", "bounce", "glow_pulse",
] = "color"
highlight_scale: float = 1.1
segment_transition: Literal["fade", "slide", "none", "zoom_in", "drop_in"] = "fade"
fade_duration_frames: int = 3
animation_speed: float = 1.0
word_entrance: Literal["none", "pop", "typewriter"] = "none"
highlight_rotation_deg: float = 0
text_transform: Literal["none", "uppercase", "lowercase"] = "none"
```
- [ ] **Step 2: Lint and type-check**
Run: `cd cofee_backend && uv run ruff check cpv3/modules/captions/schemas.py && uv run ruff format cpv3/modules/captions/schemas.py`
Expected: PASS or auto-formatted
- [ ] **Step 3: Verify existing tests still pass**
Run: `cd cofee_backend && uv run pytest tests/integration/ -x -q 2>&1 | tail -5`
Expected: All existing tests pass (new fields have defaults, so backward compatible)
- [ ] **Step 4: Commit**
```bash
cd cofee_backend
git add cpv3/modules/captions/schemas.py
git commit -m "feat(backend): extend CaptionAnimationStyle with new highlight styles, transitions, and fields"
```
---
## Task 6: Seed New System Presets (Alembic Migration)
**Files:**
- Create: `cofee_backend/alembic/versions/e6f7a8b9c0d1_seed_shorts_podcast_presets.py`
- [ ] **Step 1: Create migration file**
Create `cofee_backend/alembic/versions/e6f7a8b9c0d1_seed_shorts_podcast_presets.py`:
```python
"""seed shorts and podcast system presets
Revision ID: e6f7a8b9c0d1
Revises: d5e6f7a8b9c0
Create Date: 2026-03-21 12:00:00.000000
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = "e6f7a8b9c0d1"
down_revision: Union[str, None] = "d5e6f7a8b9c0"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
SHORTS_PRESET = {
"id": "00000000-0000-4000-a000-000000000004",
"user_id": None,
"name": "Шортс",
"description": "Жирные субтитры для вертикальных видео — TikTok, Reels, Shorts",
"is_system": True,
"style_config": {
"text": {
"font_family": "Montserrat",
"font_size": 72,
"font_weight": 700,
"text_color": "#FFFFFF",
"highlight_color": "#FFE500",
"text_shadow": "3px 3px 0px #000000",
"text_stroke_width": 3,
"text_stroke_color": "#000000",
},
"layout": {
"vertical_position": "bottom",
"horizontal_alignment": "center",
"padding_px": 20,
"max_width_pct": 85,
"lines_per_screen": 1,
},
"animation": {
"highlight_style": "bounce",
"highlight_scale": 1.15,
"highlight_rotation_deg": 3,
"word_entrance": "pop",
"segment_transition": "zoom_in",
"fade_duration_frames": 3,
"animation_speed": 1.0,
"text_transform": "uppercase",
},
"background": {
"bg_color": "transparent",
"bg_blur_px": 0,
"bg_glow_color": None,
"bg_border_radius_px": 0,
"bg_padding_px": 0,
},
},
"preview_url": None,
"is_active": True,
}
PODCAST_PRESET = {
"id": "00000000-0000-4000-a000-000000000005",
"user_id": None,
"name": "Подкаст",
"description": "Чистые субтитры для подкастов и интервью — караоке-подсветка, фон с размытием",
"is_system": True,
"style_config": {
"text": {
"font_family": "Inter",
"font_size": 44,
"font_weight": 400,
"text_color": "#E0E0E0",
"highlight_color": "#FFFFFF",
"text_shadow": "1px 1px 3px rgba(0,0,0,0.7)",
"text_stroke_width": 0,
"text_stroke_color": "#000000",
},
"layout": {
"vertical_position": "bottom",
"horizontal_alignment": "center",
"padding_px": 20,
"max_width_pct": 90,
"lines_per_screen": 2,
},
"animation": {
"highlight_style": "karaoke",
"highlight_scale": 1.0,
"highlight_rotation_deg": 0,
"word_entrance": "none",
"segment_transition": "fade",
"fade_duration_frames": 5,
"animation_speed": 1.0,
"text_transform": "none",
},
"background": {
"bg_color": "rgba(0,0,0,0.5)",
"bg_blur_px": 8,
"bg_glow_color": None,
"bg_border_radius_px": 12,
"bg_padding_px": 16,
},
},
"preview_url": None,
"is_active": True,
}
def upgrade() -> None:
# Idempotent: check if presets with these names already exist before inserting
conn = op.get_bind()
caption_presets = sa.table(
"caption_presets",
sa.column("id", sa.UUID()),
sa.column("user_id", sa.UUID()),
sa.column("name", sa.String()),
sa.column("description", sa.Text()),
sa.column("is_system", sa.Boolean()),
sa.column("style_config", sa.JSON()),
sa.column("preview_url", sa.String()),
sa.column("is_active", sa.Boolean()),
)
for preset in [SHORTS_PRESET, PODCAST_PRESET]:
exists = conn.execute(
sa.select(sa.func.count())
.select_from(caption_presets)
.where(caption_presets.c.name == preset["name"])
.where(caption_presets.c.is_system == True) # noqa: E712
).scalar()
if not exists:
op.bulk_insert(caption_presets, [preset])
def downgrade() -> None:
conn = op.get_bind()
caption_presets = sa.table(
"caption_presets",
sa.column("id", sa.UUID()),
sa.column("is_active", sa.Boolean()),
)
for preset_id in [SHORTS_PRESET["id"], PODCAST_PRESET["id"]]:
conn.execute(
caption_presets.update()
.where(caption_presets.c.id == preset_id)
.values(is_active=False)
)
```
- [ ] **Step 2: Apply migration**
Run: `cd cofee_backend && uv run alembic upgrade head`
Expected: Migration applies successfully, 2 new rows in `caption_presets`
- [ ] **Step 3: Verify presets exist via API**
Run: `curl -s http://localhost:8000/api/captions/presets/ -H "Authorization: Bearer
" | python3 -m json.tool | grep -E '"name"'`
Expected: Should list "Классические", "Неон", "Минимализм", "Шортс", "Подкаст"
(If backend isn't running, verify via direct DB query instead)
- [ ] **Step 4: Commit**
```bash
cd cofee_backend
git add alembic/versions/e6f7a8b9c0d1_seed_shorts_podcast_presets.py
git commit -m "feat(backend): seed Шортс and Подкаст system presets"
```
---
## Task 7: Extend Frontend StyleEditor
**Files:**
- Modify: `cofee_frontend/src/features/project/CaptionSettingsStep/StyleEditor.tsx` (lines 38-71 — FormValues type, lines 73-106 — DEFAULT_VALUES, lines 360-452 — AnimationFields)
- [ ] **Step 1: Regenerate API types from updated backend schema**
Run: `cd cofee_frontend && bun run gen:api-types`
Expected: `src/shared/api/__generated__/openapi.types.ts` updated with new animation fields
(Backend must be running with the schema changes applied for this to work. If not available, proceed — the local `FormValues` type is what the form uses.)
- [ ] **Step 2: Update FormValues type**
In `StyleEditor.tsx`, replace lines 57-63 (the `animation` section of `FormValues`):
```typescript
animation: {
highlight_style:
| "color"
| "scale"
| "underline"
| "color_scale"
| "pop_in"
| "karaoke"
| "bounce"
| "glow_pulse";
highlight_scale: number;
segment_transition: "fade" | "slide" | "none" | "zoom_in" | "drop_in";
fade_duration_frames: number;
animation_speed: number;
word_entrance: "none" | "pop" | "typewriter";
highlight_rotation_deg: number;
text_transform: "none" | "uppercase" | "lowercase";
}
```
- [ ] **Step 3: Update DEFAULT_VALUES**
In `StyleEditor.tsx`, replace lines 92-98 (the `animation` section of `DEFAULT_VALUES`):
```typescript
animation: {
highlight_style: "color" as const,
highlight_scale: 1.2,
segment_transition: "fade" as const,
fade_duration_frames: 5,
animation_speed: 1.0,
word_entrance: "none" as const,
highlight_rotation_deg: 0,
text_transform: "none" as const,
},
```
- [ ] **Step 4: Add new options to AnimationFields**
In the `AnimationFields` component, add the 4 new highlight style options to the existing `