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>
PropertyWhere to Apply
top, left, z-indexParent div (className)
rotation, flipX, flipYParent div (style.transform)
opacity, scale animationsmotion.div
x, y movement animationsmotion.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.

TypeDescriptionUse Case
TweenDuration-based, precise timingCoordinated animations, sync with audio
SpringPhysics-based, bounce/elasticityInteractive UI, natural motion
InertiaMomentum-based decelerationDrag 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]
}}
EaseBehaviorUse Case
linearConstant speedMechanical motion, loading indicators
easeInSlow → fastExit animations, falling objects
easeOutFast → slowEntrances, coming to rest
easeInOutSlow → fast → slowDefault for most UI animations
circIn/Out/InOutSharper circular curveSnappy, aggressive motion
backInPulls back, then forwardAnticipation effects
backOutOvershoots, then settlesBouncy clicks, attention-grabbing
backInOutBoth effects combinedPlayful, game UI
anticipateDramatic pullbackHero entrances, launch effects
steps(n)Discrete stepsPixel 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 rotation
  • transformOrigin - 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 }
}}