video-coder
Expert React video scene component creator for educational content. Builds production-grade, visually distinctive components using framer-motion animations, pixel-precise positioning, and optimized performance patterns. Follows strict component format with React.memo, threshold-based state updates, and module-level definitions. Outputs self-contained TSX components with proper timing sync, 60fps performance, and comprehensive reference-based implementation.
$ Installer
git clone https://github.com/outscal/video-generator /tmp/video-generator && cp -r /tmp/video-generator/.claude/skills/video-coder ~/.claude/skills/video-generator// tip: Run this command in your terminal to install the skill
name: video-coder description: Expert React video scene component creator for educational content. Builds production-grade, visually distinctive components using framer-motion animations, pixel-precise positioning, and optimized performance patterns. Follows strict component format with React.memo, threshold-based state updates, and module-level definitions. Outputs self-contained TSX components with proper timing sync, 60fps performance, and comprehensive reference-based implementation.
Video Coder
Parent div vs motion.div example:
{/* PARENT DIV: positioning, static transforms (rotation, flip), z-index */}
<div
className="absolute top-[300px] left-[500px] -translate-x-1/2 -translate-y-1/2 z-[10]"
style={{ transform: 'rotate(45deg) scaleX(-1)' }}
>
{/* MOTION.DIV: animations only (opacity, scale, movement) */}
<motion.div
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.5 }}
>
<svg>...</svg>
</motion.div>
</div>
| Property | Where to Apply |
|---|---|
top, left, z-index | Parent div (className) |
rotation, flipX, flipY | Parent div (style.transform) |
opacity, scale animations | motion.div |
x, y movement animations | motion.div |
export default Scene{N};
Where `{N}` is the scene number (e.g., `Scene0`, `Scene1`, `Scene2`)
`currentTime` is the global value of time with respect to the video start.
</export-pattern>
</required-structure>
---
<sub-components>
### Sub-Components (CRITICAL)
**All sub-components MUST use `React.memo`** and be defined at module level (outside the main Scene component).
<react-memo>
#### Why React.memo is Required
- Video components re-render 60 times per second as `currentTime` changes
- Without `React.memo`, sub-components re-render unnecessarily causing animation jitter
- Module-level definitions ensure stable references across renders
</react-memo>
<sub-component-pattern>
// CORRECT: Module-level with React.memo + wrapper div for positioning
const TreeNode = React.memo(({
value,
position,
isVisible
}: {
value: string;
position: { x: number; y: number };
isVisible: boolean;
}) => (
<div
className="absolute -translate-x-1/2 -translate-y-1/2"
style={{ left: `${position.x}px`, top: `${position.y}px` }}
>
<motion.div
animate={isVisible ? "visible" : "hidden"}>
{value}
</motion.div>
</div>
));
// WRONG: Defined inside component (causes jitter)
export default function Scene0({ currentTime }: SceneProps) {
// ❌ Never define components here
const TreeNode = ({ value }) => <div>{value}</div>;
}
</sub-component-pattern>
<module-level-definitions>
#### What Goes at Module Level (Outside Component)
1. **Sub-components** - Always wrapped with `React.memo`
2. **Wrapper div** - For positioning and other (absolute, translate, left/top style, etc.)
3. **Animation variants** - Objects defining animation states
4. **Static data** - Positions, configurations that don't change
```typescript
// Animation variants at module level
const fadeVariants = {
hidden: { opacity: 0 },
visible: { opacity: 1, transition: { duration: 0.5 } }
};
// Static positions at module level
const nodePositions = {
node1: { x: 576, y: 540 },
node2: { x: 1344, y: 540 }
};
// Sub-component at module level with React.memo + wrapper div
const InfoCard = React.memo(({ title, position, isVisible }: { title: string; position: { x: number; y: number }; isVisible: boolean }) => (
<div
className="absolute -translate-x-1/2 -translate-y-1/2"
style={{ left: `${position.x}px`, top: `${position.y}px` }}
>
<motion.div variants={fadeVariants} animate={isVisible ? "visible" : "hidden"}>
{title}
</motion.div>
</div>
));
import React, { useMemo } from 'react';
import { motion } from 'framer-motion';
interface SceneProps {
currentTime: number;
}
// Animation variants at module level
const nodeVariants = {
hidden: { scale: 0, opacity: 0 },
visible: { scale: 1, opacity: 1, transition: { duration: 0.4 } }
};
// Static data at module level
const nodePositions = {
node1: { x: 576, y: 540 },
node2: { x: 1344, y: 540 }
};
// Sub-component at module level with React.memo
const TreeNode = React.memo(({
value,
position,
isVisible
}: {
value: string;
position: { x: number; y: number };
isVisible: boolean;
}) => (
<div
className="absolute -translate-x-1/2 -translate-y-1/2"
style={{ left: `${position.x}px`, top: `${position.y}px` }}
>
<motion.div
variants={nodeVariants}
initial="hidden"
animate={isVisible ? "visible" : "hidden"}
className="w-20 h-20 rounded-full bg-white flex items-center justify-center"
>
{value}
</motion.div>
</div>
));
// Main Scene component with React.memo
const Scene0 = React.memo(function Scene0({ currentTime }: SceneProps) {
// Threshold-based state updates
const states = useMemo(() => ({
showNode1: currentTime >= 1000,
showNode2: currentTime >= 2000,
}), [Math.floor(currentTime / 42)]);
return (
<div className="relative w-full h-full bg-gray-900">
<TreeNode value="A" position={nodePositions.node1} isVisible={states.showNode1} />
<TreeNode value="B" position={nodePositions.node2} isVisible={states.showNode2} />
</div>
);
});
export default Scene0;
What They Do
Let you set custom sizes, spacing, colors, borders, radii, typography, and positioning instantly.
Examples
w-[37px]h-[3.5rem]p-[18px]m-[2.75rem]bg-[#1a73e8]text-[22px]border-[3px]rounded-[14px]gap-[22px]top-[42px]z-[25]
CRITICAL: Follow these patterns to prevent animation jittering and re-rendering issues.
React video components re-render up to 60 times per second. Unstable references cause animations to restart, creating visual jitter.
Define sub-components, animation variants, and static data outside the parent component for stable references.
// Animation variants at module level
const nodeVariants = { hidden: { scale: 0 }, visible: { scale: 1 } };
// Static data at module level
const nodePositions = { node1: { x: 576, y: 540 }, node2: { x: 740, y: 540 } };
// Sub-component at module level (see <complete-example> for full pattern)
const TreeNode = React.memo(({ value, position, isVisible }) => (
<div style={{ left: `${position.x}px`, top: `${position.y}px` }}>
<motion.div animate={isVisible ? "visible" : "hidden"}>{value}</motion.div>
</div>
));
See <complete-example> above for the full implementation pattern.
Update states every 42ms using Math.floor(currentTime / 42) to prevent excessive re-renders while matching 24fps video output.
// State updates inside components
const states = useMemo(() => ({
showTitle: currentTime >= 1000,
showGrid: currentTime >= 2000,
fadeOut: currentTime >= 9000
}), [Math.floor(currentTime / 42)]);
// Computed collections inside components
const visibleItems = useMemo(() => {
const visible = new Set<string>();
if (currentTime >= 1000) visible.add('item1');
if (currentTime >= 2000) visible.add('item2');
return visible;
}, [Math.floor(currentTime / 42)]);
// Static data created once at mount
const particles = useMemo(() =>
Array.from({ length: 40 }, () => ({
x: Math.random() * 100,
y: Math.random() * 100
})),
[] // Empty deps = created once
);
Pass all dependencies as explicit props for React.memo to work correctly.
const TreeNode = React.memo(({
value,
position,
showTree // Explicit prop, not derived from currentTime inside
}: {
value: string;
position: { x: number; y: number };
showTree: boolean;
}) => (
<div
className="absolute -translate-x-1/2 -translate-y-1/2"
style={{ left: `${position.x}px`, top: `${position.y}px` }}
>
<motion.div animate={showTree ? "visible" : "hidden"}>
{value}
</motion.div>
</div>
));
// In parent: derive state, pass as prop
<TreeNode value="50" position={{ x: 960, y: 540 }} showTree={states.showTree} />
FOR SHAPES/TEXT/ICONS: Position: Always refers to element's CENTER point
FOR PATHS: All coordinates are ABSOLUTE screen positions No position/size fields needed (implied by path coordinates)
ROTATION 0° = pointing up (↑) 90° = pointing right (→) 180° = pointing down (↓) 270° = pointing left (←)
Positive values = clockwise rotation Negative values = counter-clockwise (-90° same as 270°)
EXAMPLE (1920×1080 viewport) Screen center: x = 960, y = 540 Top-center: x = 960, y = 100 Bottom-left quadrant: x = 480, y = 810 Right edge center: x = 1820, y = 540 Position at any pixel value using the same pattern:
{/* Content layer - Landscape center: 540px, 960px */}
Be thorough in studying any animation pattern you're using in your scene.
| Type | Description | Use Case |
|---|---|---|
| Tween | Duration-based, precise timing | Coordinated animations, sync with audio |
| Spring | Physics-based, bounce/elasticity | Interactive UI, natural motion |
| Inertia | Momentum-based deceleration | Drag interactions, swipe gestures |
transition={{
duration?: number, // Seconds (default: 0.3)
ease?: string | array, // Easing function (default: "easeInOut")
delay?: number, // Delay in seconds
repeat?: number, // Number of repeats (Infinity for loop)
repeatType?: "loop" | "reverse" | "mirror",
times?: number[], // Keyframe timing [0, 0.5, 1]
}}
| Ease | Behavior | Use Case |
|---|---|---|
linear | Constant speed | Mechanical motion, loading indicators |
easeIn | Slow → fast | Exit animations, falling objects |
easeOut | Fast → slow | Entrances, coming to rest |
easeInOut | Slow → fast → slow | Default for most UI animations |
circIn/Out/InOut | Sharper circular curve | Snappy, aggressive motion |
backIn | Pulls back, then forward | Anticipation effects |
backOut | Overshoots, then settles | Bouncy clicks, attention-grabbing |
backInOut | Both effects combined | Playful, game UI |
anticipate | Dramatic pullback | Hero entrances, launch effects |
steps(n) | Discrete steps | Pixel art, frame-by-frame |
Note: Cannot mix bounce with stiffness/damping/mass.
// Snappy transition={{ type: "spring", stiffness: 400, damping: 30 }}
// Soft transition={{ type: "spring", stiffness: 60, damping: 10 }}
<motion.div
drag
dragConstraints={{ left: 0, right: 400 }}
dragTransition={{
power?: number, // Deceleration rate (default: 0.8)
timeConstant?: number, // Duration in ms (default: 700)
bounceStiffness?: number, // Boundary spring (default: 500)
bounceDamping?: number, // Boundary damping (default: 10)
}}
/>
// Static orientation (asset facing a direction) - use wrapper div
// Static orientation with flip
Animated rotation (rotation that changes over time):
// Clockwise rotation (positive degrees) <motion.div animate={{ rotate: 90 }} transition={{ duration: 1 }} />
// Anti-clockwise rotation (negative degrees) <motion.div animate={{ rotate: -90 }} transition={{ duration: 1 }} />
// Continuous clockwise spin <motion.div animate={{ rotate: 360 }} transition={{ duration: 2, repeat: Infinity, ease: "linear" }} />
// Continuous anti-clockwise spin <motion.div animate={{ rotate: -360 }} transition={{ duration: 2, repeat: Infinity, ease: "linear" }} />
// Custom pivot point <motion.div style={{ transformOrigin: "top left" }} animate={{ rotate: 45 }} />
rotate- 2D rotation in degrees (positive = clockwise, negative = anti-clockwise)rotateX,rotateY- 2D axis rotationtransformOrigin- pivot point (default:"center")
For animating elements along SVG paths, see the dedicated path-following.md.
Important: When a path has both path-draw and follow-path animations, apply the same easing to both to keep them synchronized.
transition={{
x: { type: "spring", stiffness: 200 },
opacity: { duration: 0.5, ease: "easeOut" },
scale: { type: "spring", bounce: 0.6 }
}}
Repository
