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
currentTimechanges - 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)
- Sub-components - Always wrapped with
React.memo - Wrapper div - For positioning and other (absolute, translate, left/top style, etc.)
- Animation variants - Objects defining animation states
- Static data - Positions, configurations that don't change
// 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 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 }
}}