GSAP Motion Graphics Patterns Research
Reference catalog for building a GSAP-based motion graphics skill for video production. All GSAP plugins are now 100% free (including former Club GreenSock plugins) since Webflow's acquisition in 2024. Rendering pipeline: GSAP + Remotion -- all patterns shown as React components compatible with Remotion's frame-based rendering.
Table of Contents
- GSAP + Remotion Integration
- Core Animation Patterns
- Text Animation Patterns
- SVG/Shape Animation Patterns
- Transition Effects
- Motion Graphics Templates (Remotion Compositions)
- Advanced Techniques
- GSAP Plugins Reference
- Utility Functions
- Reusable Effects System
- GSAP vs After Effects
- Design Implications for Skill
1. GSAP + Remotion Integration
1.1 Core Integration Pattern: useGSAP + Remotion
Remotion renders each frame independently. GSAP timelines must be paused and seeked to the correct time on every frame. The pattern follows the same approach Remotion uses for Anime.js integration.
import { useCurrentFrame, useVideoConfig } from 'remotion';
import { useRef, useEffect, useState } from 'react';
import gsap from 'gsap';
/**
* Core hook: syncs a GSAP timeline with Remotion's frame system.
*
* - Creates a paused timeline (autoplay disabled)
* - On every frame, seeks the timeline to the correct time position
* - Returns the timeline ref so the component can add animations
* - Deterministic: same frame always produces same visual output
*/
function useGSAPTimeline(
buildTimeline: (tl: gsap.core.Timeline, container: HTMLDivElement) => void
) {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const containerRef = useRef<HTMLDivElement>(null);
const tlRef = useRef<gsap.core.Timeline | null>(null);
// Build timeline once on mount (paused)
useEffect(() => {
if (!containerRef.current) return;
const tl = gsap.timeline({ paused: true });
buildTimeline(tl, containerRef.current);
tlRef.current = tl;
return () => {
tl.kill();
};
}, []);
// Seek timeline to current frame's time position
useEffect(() => {
if (!tlRef.current) return;
const timeInSeconds = frame / fps;
tlRef.current.seek(timeInSeconds);
}, [frame, fps]);
return containerRef;
}
1.2 How useCurrentFrame Maps to GSAP Timeline
Remotion Frame System GSAP Timeline
================== ==============
frame = 0 -> tl.seek(0) // start
frame = 15 (@ 30fps) -> tl.seek(0.5) // 0.5 seconds in
frame = 30 (@ 30fps) -> tl.seek(1.0) // 1.0 seconds in
frame = durationInFrames-1 -> tl.seek(totalDur) // end
Conversion: timeInSeconds = frame / fps
tl.seek(timeInSeconds)
Important: tl.seek() is what makes GSAP deterministic in Remotion. Unlike tl.play() which relies on requestAnimationFrame and wall-clock time, seek() jumps directly to a specific time position. The same frame number always produces the same visual.
1.3 Progress-Based Alternative
For normalized 0-1 control (useful when timeline duration is dynamic):
useEffect(() => {
if (!tlRef.current) return;
const progress = frame / durationInFrames;
tlRef.current.progress(progress);
}, [frame, durationInFrames]);
1.4 Determinism Guarantees
GSAP timelines are deterministic when:
- Created paused (
{ paused: true }) - Seeked explicitly (never
play(), neverresume()) - No random values in animation params (use seeded random or fixed values)
- No DOM-dependent calculations at seek time (pre-calculate in build phase)
- No interactive plugins (no Draggable, Observer, Inertia, ScrollTrigger)
Things that break determinism:
Math.random()anywhere in timeline constructiongsap.utils.random()without a seedrequestAnimationFrameloops- Anything that reads live DOM measurements per frame
1.5 Plugin Registration for Remotion
Register GSAP plugins once at the module level (outside React components):
import gsap from 'gsap';
import { SplitText } from 'gsap/SplitText';
import { DrawSVGPlugin } from 'gsap/DrawSVGPlugin';
import { MorphSVGPlugin } from 'gsap/MorphSVGPlugin';
import { MotionPathPlugin } from 'gsap/MotionPathPlugin';
import { ScrambleTextPlugin } from 'gsap/ScrambleTextPlugin';
import { TextPlugin } from 'gsap/TextPlugin';
import { CustomEase } from 'gsap/CustomEase';
import { CustomBounce } from 'gsap/CustomBounce';
import { CustomWiggle } from 'gsap/CustomWiggle';
import { Physics2DPlugin } from 'gsap/Physics2DPlugin';
gsap.registerPlugin(
SplitText,
DrawSVGPlugin,
MorphSVGPlugin,
MotionPathPlugin,
ScrambleTextPlugin,
TextPlugin,
CustomEase,
CustomBounce,
CustomWiggle,
Physics2DPlugin
);
1.6 Flickering Prevention in Remotion
Remotion renders frames in parallel across multiple browser tabs. To prevent flickering:
- Never rely on component state that accumulates across frames
- Never use
useStatefor animation values -- derive everything fromuseCurrentFrame() - GSAP timeline must be rebuilt identically each time the component mounts (since parallel tabs create independent instances)
- Use
delayRender()/continueRender()if fonts or assets must load before GSAP can split/measure text - All animations must be a pure function of frame number
import { delayRender, continueRender } from 'remotion';
// For SplitText: wait for fonts before splitting
function useGSAPWithFonts(buildTimeline) {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const containerRef = useRef<HTMLDivElement>(null);
const tlRef = useRef<gsap.core.Timeline | null>(null);
const [handle] = useState(() => delayRender());
useEffect(() => {
// Wait for fonts, then build timeline
document.fonts.ready.then(() => {
if (!containerRef.current) return;
const tl = gsap.timeline({ paused: true });
buildTimeline(tl, containerRef.current);
tlRef.current = tl;
continueRender(handle);
});
return () => { tlRef.current?.kill(); };
}, []);
useEffect(() => {
if (!tlRef.current) return;
tlRef.current.seek(frame / fps);
}, [frame, fps]);
return containerRef;
}
2. Core Animation Patterns
2.1 Timeline Orchestration in Remotion
import { useCurrentFrame, useVideoConfig, AbsoluteFill } from 'remotion';
const TimelineDemo: React.FC = () => {
const containerRef = useGSAPTimeline((tl, container) => {
const title = container.querySelector('.title');
const subtitle = container.querySelector('.subtitle');
const cta = container.querySelector('.cta');
tl.defaults({ duration: 1, ease: "power2.out" });
tl.from(title, { y: 50, opacity: 0 })
.from(subtitle, { y: 30, opacity: 0 }, "-=0.5")
.from(cta, { scale: 0, opacity: 0 }, "+=0.2");
});
return (
<AbsoluteFill>
<div ref={containerRef}>
<h1 className="title">Main Title</h1>
<p className="subtitle">Subtitle text</p>
<button className="cta">Call to Action</button>
</div>
</AbsoluteFill>
);
};
Position Parameter Syntax:
| Syntax | Meaning |
|---|---|
3 |
Absolute time (3s from timeline start) |
"+=1" |
1 second after previous animation ends |
"-=0.5" |
0.5 seconds before previous animation ends (overlap) |
"<" |
At the start of the previous animation |
"<0.5" |
0.5 seconds after the start of the previous animation |
">-0.2" |
0.2 seconds before the end of the previous animation |
Nested Timelines as Remotion Components:
// Each section is its own function returning a timeline
function buildIntro(tl: gsap.core.Timeline, container: HTMLDivElement) {
const logo = container.querySelector('.logo');
const tagline = container.querySelector('.tagline');
const introTl = gsap.timeline();
introTl.from(logo, { scale: 0, rotation: -180, duration: 1 })
.from(tagline, { y: 20, opacity: 0, duration: 0.5 });
tl.add(introTl);
}
function buildContent(tl: gsap.core.Timeline, container: HTMLDivElement) {
const heading = container.querySelector('.heading');
const body = container.querySelector('.body-text');
const contentTl = gsap.timeline();
contentTl.from(heading, { x: -100, opacity: 0, duration: 0.6 })
.from(body, { y: 50, opacity: 0, duration: 0.5 }, "-=0.3");
tl.add(contentTl, "-=0.5");
}
// Master composition
const MasterScene: React.FC = () => {
const containerRef = useGSAPTimeline((tl, container) => {
buildIntro(tl, container);
buildContent(tl, container);
buildOutro(tl, container);
});
return (
<AbsoluteFill>
<div ref={containerRef}>
<div className="logo">LOGO</div>
<div className="tagline">Tagline</div>
<h1 className="heading">Heading</h1>
<p className="body-text">Body text...</p>
</div>
</AbsoluteFill>
);
};
Labels for Scene Navigation:
// Labels can be used for programmatic navigation and debugging
const tl = gsap.timeline({ paused: true });
tl.addLabel("intro")
.from(logo, { scale: 0, duration: 0.8 })
.addLabel("content", "+=0.2")
.from(text, { opacity: 0, duration: 0.6 })
.addLabel("outro", "+=2"); // 2s hold after text
// In Remotion, seek by time, but labels help organize construction
2.2 Stagger Patterns
const StaggerGrid: React.FC<{ items: string[] }> = ({ items }) => {
const containerRef = useGSAPTimeline((tl, container) => {
const gridItems = container.querySelectorAll('.grid-item');
tl.from(gridItems, {
scale: 0,
opacity: 0,
duration: 0.6,
stagger: {
each: 0.1,
from: "center",
grid: [4, 6],
ease: "power2.in"
}
});
});
return (
<div ref={containerRef} style={{ display: 'grid', gridTemplateColumns: 'repeat(6, 1fr)' }}>
{items.map((item, i) => (
<div key={i} className="grid-item">{item}</div>
))}
</div>
);
};
Stagger from Options:
| Value | Effect |
|---|---|
"start" |
Default, sequential from first |
"center" |
Radiate outward from center |
"edges" |
Start from both edges toward center |
"random" |
Random order (caution: may not be deterministic -- use fixed seed) |
"end" |
Reverse, from last to first |
5 |
Start from index 5 |
[0.5, 0.5] |
Start from 50%/50% position in grid |
Remotion Warning:
from: "random"uses GSAP's internal random which is NOT seeded. For deterministic rendering, use a function-based stagger with seeded randomness instead:
// Deterministic random stagger for Remotion
function seededRandom(seed: number): number {
const x = Math.sin(seed) * 10000;
return x - Math.floor(x);
}
tl.from(items, {
y: 100,
opacity: 0,
stagger: function(index) {
// Deterministic "random-looking" delay
return seededRandom(index * 7919) * 0.5;
}
});
2.3 Easing for Motion Graphics
Standard Eases:
power1throughpower4(light to heavy acceleration)back(slight overshoot),elastic(spring-like),bounce(bouncing)circ(circular),expo(exponential),sine(gentle sine wave)
Each has .in, .out, .inOut variants.
Special Eases (EasePack):
// SlowMo - slow in middle, fast at edges
tl.to(el, { x: 500, ease: "slow(0.7, 0.7, false)" });
// RoughEase - jagged, organic motion
tl.to(el, { x: 100, ease: "rough({strength: 2, points: 50, clamp: true})" });
// ExpoScaleEase - exponential scaling
tl.to(el, { x: 500, ease: "expoScale(0.5, 7, power2.inOut)" });
CustomEase - Arbitrary Curves:
CustomEase.create("myEase", "M0,0 C0.1,0.5 0.2,1 0.4,1 0.6,1 0.8,0.8 1,1");
tl.to(el, { y: -200, ease: "myEase" });
CustomBounce and CustomWiggle:
CustomBounce.create("myBounce", { strength: 0.6, squash: 3, squashID: "myBounce-squash" });
CustomWiggle.create("myWiggle", { wiggles: 8, type: "easeOut" });
tl.to(el, { y: -200, ease: "myBounce" });
tl.to(el, { rotation: 30, ease: "myWiggle" });
GSAP to Remotion Easing Equivalents:
These GSAP eases have approximate Remotion Easing equivalents (useful when choosing NOT to use GSAP for simple animations):
| GSAP Easing | Remotion Easing Equivalent |
|---|---|
power1.out |
Easing.out(Easing.quad) |
power2.out |
Easing.out(Easing.cubic) |
power3.out |
Easing.out(Easing.cubic) |
back.inOut(2) |
Easing.inOut(Easing.back(2)) |
elastic.out(1, 0.3) |
Easing.out(Easing.elastic(1)) |
expo.out |
Easing.out(Easing.exp) |
However, GSAP's easing system is far richer than Remotion's native Easing. Use GSAP eases via the timeline for: CustomEase, RoughEase, SlowMo, ExpoScaleEase, CustomBounce, CustomWiggle.
Recommended Eases for Motion Graphics:
| Use Case | Ease |
|---|---|
| Entrance (slide in) | power2.out or power3.out |
| Exit (slide out) | power2.in |
| Attention/bounce | back.out(1.7) |
| Smooth transition | power1.inOut |
| Dramatic entrance | expo.out |
| Organic motion | sine.inOut |
| Impact/hit | power4.out |
| Text reveal | power2.out or circ.out |
| Logo animation | elastic.out(1, 0.5) |
| Counter/number | power1.out |
3. Text Animation Patterns
3.1 SplitText Plugin in Remotion
SplitText manipulates the DOM to split text into individual elements. In Remotion, this must happen in useEffect after mount, and fonts must be loaded first.
import { AbsoluteFill, delayRender, continueRender } from 'remotion';
import { SplitText } from 'gsap/SplitText';
const TextReveal: React.FC<{ text: string }> = ({ text }) => {
const containerRef = useGSAPWithFonts((tl, container) => {
const heading = container.querySelector('.heading');
// SplitText modifies the DOM -- must happen after mount + font load
const split = SplitText.create(heading, {
type: "chars,words,lines",
mask: "lines" // clip mask for reveal
});
tl.from(split.chars, {
y: 100,
opacity: 0,
duration: 0.6,
stagger: 0.03,
ease: "power2.out"
});
});
return (
<AbsoluteFill style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<div ref={containerRef}>
<h1 className="heading" style={{ fontSize: 80, fontWeight: 'bold' }}>{text}</h1>
</div>
</AbsoluteFill>
);
};
SplitText Key Options:
| Option | Purpose |
|---|---|
type |
"chars", "words", "lines" (comma-separated) |
mask |
Wrap in clip containers: "lines", "words", "chars" |
autoSplit |
Auto re-split on font load or resize (less useful in Remotion -- fixed size) |
deepSlice |
Handle nested elements like <strong> across lines |
propIndex |
Add CSS vars --char, --word, --line per element |
onSplit() |
Callback after split, ideal for animation setup |
Text Reveal Patterns as Remotion Components:
// Pattern 1: Line-by-line reveal with mask
const LineReveal: React.FC<{ text: string; startFrame?: number }> = ({ text, startFrame = 0 }) => {
const containerRef = useGSAPWithFonts((tl, container) => {
const el = container.querySelector('.text');
const split = SplitText.create(el, { type: "lines", mask: "lines" });
tl.from(split.lines, {
y: "100%",
duration: 0.8,
stagger: 0.15,
ease: "power3.out"
});
});
return (
<div ref={containerRef}>
<p className="text" style={{ fontSize: 48 }}>{text}</p>
</div>
);
};
// Pattern 2: Character cascade with 3D rotation
const CharCascade: React.FC<{ text: string }> = ({ text }) => {
const containerRef = useGSAPWithFonts((tl, container) => {
const el = container.querySelector('.title');
const split = SplitText.create(el, { type: "chars" });
tl.from(split.chars, {
y: 50,
opacity: 0,
rotationX: -90,
transformPerspective: 600,
duration: 0.5,
stagger: 0.02,
ease: "back.out(1.7)"
});
});
return (
<div ref={containerRef}>
<h1 className="title" style={{ fontSize: 72 }}>{text}</h1>
</div>
);
};
// Pattern 3: Word-by-word scale
const WordScale: React.FC<{ text: string }> = ({ text }) => {
const containerRef = useGSAPWithFonts((tl, container) => {
const el = container.querySelector('.subtitle');
const split = SplitText.create(el, { type: "words" });
tl.from(split.words, {
scale: 0,
opacity: 0,
duration: 0.4,
stagger: 0.08,
ease: "back.out(2)"
});
});
return (
<div ref={containerRef}>
<h2 className="subtitle" style={{ fontSize: 48 }}>{text}</h2>
</div>
);
};
3.2 ScrambleText Plugin in Remotion
Important Determinism Note: ScrambleText uses internal randomness for character selection. In Remotion's parallel rendering, this can cause different random characters on different tabs. The visual result is generally acceptable since the final resolved text is deterministic and the scramble is inherently "random-looking", but frame-exact reproducibility requires --concurrency=1.
const ScrambleReveal: React.FC<{ text: string }> = ({ text }) => {
const containerRef = useGSAPTimeline((tl, container) => {
const el = container.querySelector('.scramble-text');
tl.to(el, {
duration: 2,
scrambleText: {
text: text,
chars: "01", // binary decode effect
revealDelay: 0.5,
speed: 0.3,
newClass: "revealed"
}
});
});
return (
<div ref={containerRef}>
<p className="scramble-text" style={{ fontFamily: 'monospace', fontSize: 36 }}>
{'X'.repeat(text.length)}
</p>
</div>
);
};
Character Set Options:
"upperCase"- A-Z"lowerCase"- a-z"upperAndLowerCase"- A-Za-z"01"- binary- Custom string like
"!@#$%^&*()"or Japanese characters
3.3 TextPlugin in Remotion
const Typewriter: React.FC<{ text: string; duration?: number }> = ({ text, duration = 3 }) => {
const containerRef = useGSAPTimeline((tl, container) => {
const el = container.querySelector('.typewriter');
tl.to(el, {
duration,
text: text,
ease: "none" // linear for typewriter effect
});
});
return (
<div ref={containerRef}>
<p className="typewriter" style={{ fontFamily: 'monospace', fontSize: 24 }}></p>
</div>
);
};
// Word-by-word variant
const WordByWord: React.FC<{ text: string }> = ({ text }) => {
const containerRef = useGSAPTimeline((tl, container) => {
const el = container.querySelector('.text');
tl.to(el, {
duration: 3,
text: { value: text, delimiter: " " },
ease: "none"
});
});
return (
<div ref={containerRef}>
<p className="text" style={{ fontSize: 32 }}></p>
</div>
);
};
3.4 Kinetic Typography as Remotion Composition
import { Composition, Sequence, AbsoluteFill } from 'remotion';
const KineticTypography: React.FC<{
lines: string[];
style?: 'cascade' | 'impact' | 'wave';
}> = ({ lines, style = 'cascade' }) => {
const containerRef = useGSAPWithFonts((tl, container) => {
lines.forEach((line, i) => {
const el = container.querySelector(`.line-${i}`);
if (!el) return;
const split = SplitText.create(el, {
type: "words,chars",
mask: "words"
});
// Entrance
tl.from(split.words, {
y: "100%",
duration: 0.6,
stagger: 0.08,
ease: "power3.out"
}, i * 1.5)
// Hold for reading
.to({}, { duration: 1 })
// Exit (if not last line)
if (i < lines.length - 1) {
tl.to(split.words, {
y: "-100%",
opacity: 0,
duration: 0.4,
stagger: 0.04,
ease: "power2.in"
});
}
});
});
return (
<AbsoluteFill style={{
display: 'flex', alignItems: 'center', justifyContent: 'center',
background: '#000', color: '#fff'
}}>
<div ref={containerRef}>
{lines.map((line, i) => (
<h1 key={i} className={`line-${i}`}
style={{ fontSize: 72, position: 'absolute', textAlign: 'center' }}>
{line}
</h1>
))}
</div>
</AbsoluteFill>
);
};
4. SVG/Shape Animation Patterns
4.1 MorphSVG Plugin in Remotion
const ShapeMorph: React.FC<{ shapes: string[] }> = ({ shapes }) => {
const containerRef = useGSAPTimeline((tl, container) => {
const path = container.querySelector('#morph-path');
// Sequence through shapes
shapes.forEach((shape, i) => {
if (i === 0) return; // first shape is the starting state
tl.to(path, {
morphSVG: {
shape: shape, // SVG path data string
type: "rotational",
map: "size"
},
duration: 1,
ease: "power2.inOut"
}, `+=${i === 1 ? 0 : 0.5}`);
});
});
return (
<AbsoluteFill style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<div ref={containerRef}>
<svg viewBox="0 0 500 500" width={400} height={400}>
<path id="morph-path" d={shapes[0]} fill="#3b82f6" />
</svg>
</div>
</AbsoluteFill>
);
};
MorphSVG Configuration:
| Option | Purpose |
|---|---|
shape |
Target path data or selector |
type |
"linear" (default) or "rotational" |
shapeIndex |
Point alignment offset |
map |
Segment matching: "size", "position", "complexity" |
origin |
Rotation origin for rotational type |
precision |
Decimal places for output (default: 2) |
4.2 DrawSVG Plugin in Remotion
const LogoDraw: React.FC<{ svgPaths: React.ReactNode }> = ({ svgPaths }) => {
const containerRef = useGSAPTimeline((tl, container) => {
const paths = container.querySelectorAll('.draw-path');
// Draw each path sequentially
tl.from(paths, {
drawSVG: 0,
duration: 1.5,
stagger: 0.2,
ease: "power2.inOut"
})
// Fill in after drawing
.to(paths, {
fill: "#ffffff",
duration: 0.5,
stagger: 0.1
}, "-=0.3");
});
return (
<AbsoluteFill style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', background: '#000' }}>
<div ref={containerRef}>
<svg viewBox="0 0 200 200" width={400} height={400}>
{svgPaths}
</svg>
</div>
</AbsoluteFill>
);
};
DrawSVG Patterns:
// Draw from nothing
tl.from(path, { drawSVG: 0, duration: 2 });
// Draw from center outward
tl.from(path, { drawSVG: "50% 50%", duration: 2 });
// Show only a segment
tl.to(path, { drawSVG: "20% 80%", duration: 2 });
// Erase effect (draw to nothing)
tl.to(path, { drawSVG: "100% 100%", duration: 1 });
4.3 MotionPath Plugin in Remotion
const PathFollower: React.FC = () => {
const containerRef = useGSAPTimeline((tl, container) => {
const element = container.querySelector('.follower');
const path = container.querySelector('#motion-path');
tl.to(element, {
motionPath: {
path: path as SVGPathElement,
align: path as SVGPathElement,
alignOrigin: [0.5, 0.5],
autoRotate: true
},
duration: 3,
ease: "power1.inOut"
});
});
return (
<div ref={containerRef} style={{ position: 'relative', width: '100%', height: '100%' }}>
<svg style={{ position: 'absolute', width: '100%', height: '100%' }}>
<path id="motion-path" d="M 100,300 C 200,100 400,100 500,300"
fill="none" stroke="#333" strokeWidth={2} />
</svg>
<div className="follower" style={{
width: 20, height: 20, borderRadius: '50%',
background: '#3b82f6', position: 'absolute'
}} />
</div>
);
};
4.4 SVG Filter Animations in Remotion
const BlurReveal: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const containerRef = useGSAPTimeline((tl, container) => {
const blur = container.querySelector('#blur');
tl.fromTo(blur,
{ attr: { stdDeviation: 20 } },
{ attr: { stdDeviation: 0 }, duration: 1.5, ease: "power2.out" }
);
});
return (
<div ref={containerRef}>
<svg style={{ position: 'absolute', width: 0, height: 0 }}>
<defs>
<filter id="blur-filter">
<feGaussianBlur id="blur" stdDeviation="20" />
</filter>
</defs>
</svg>
<div style={{ filter: 'url(#blur-filter)' }}>
{children}
</div>
</div>
);
};
5. Transition Effects
5.1 Clip-Path Transitions as Remotion Components
type TransitionType = 'circleReveal' | 'wipeLeft' | 'wipeRight' | 'irisReveal' | 'blinds';
const ClipPathTransition: React.FC<{
type: TransitionType;
children: React.ReactNode;
}> = ({ type, children }) => {
const containerRef = useGSAPTimeline((tl, container) => {
const scene = container.querySelector('.scene');
const transitions: Record<TransitionType, [string, string]> = {
circleReveal: [
"circle(0% at 50% 50%)",
"circle(75% at 50% 50%)"
],
wipeLeft: [
"polygon(0 0, 0 0, 0 100%, 0 100%)",
"polygon(0 0, 100% 0, 100% 100%, 0 100%)"
],
wipeRight: [
"polygon(100% 0, 100% 0, 100% 100%, 100% 100%)",
"polygon(0 0, 100% 0, 100% 100%, 0 100%)"
],
irisReveal: [
"polygon(50% 50%, 50% 50%, 50% 50%, 50% 50%)",
"polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%)"
],
blinds: [
"inset(0 0 100% 0)",
"inset(0 0 0% 0)"
]
};
const [from, to] = transitions[type];
tl.fromTo(scene,
{ clipPath: from },
{ clipPath: to, duration: 0.8, ease: "power2.inOut" }
);
});
return (
<div ref={containerRef}>
<div className="scene">{children}</div>
</div>
);
};
5.2 Scene Transitions Between Sequences
import { Sequence, AbsoluteFill } from 'remotion';
// Slide transition wrapper
const SlideTransition: React.FC<{
direction?: 'left' | 'right' | 'up' | 'down';
outgoing: React.ReactNode;
incoming: React.ReactNode;
transitionDuration?: number; // in frames
}> = ({ direction = 'left', outgoing, incoming, transitionDuration = 15 }) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const containerRef = useGSAPTimeline((tl, container) => {
const out = container.querySelector('.outgoing');
const inc = container.querySelector('.incoming');
const axis = (direction === 'left' || direction === 'right') ? 'x' : 'y';
const outVal = (direction === 'left' || direction === 'up') ? '-100%' : '100%';
const inVal = (direction === 'left' || direction === 'up') ? '100%' : '-100%';
tl.to(out, { [axis]: outVal, duration: transitionDuration / fps, ease: "power2.inOut" })
.fromTo(inc,
{ [axis]: inVal },
{ [axis]: '0%', duration: transitionDuration / fps, ease: "power2.inOut" },
0
);
});
return (
<AbsoluteFill>
<div ref={containerRef} style={{ width: '100%', height: '100%', position: 'relative' }}>
<div className="outgoing" style={{ position: 'absolute', inset: 0 }}>{outgoing}</div>
<div className="incoming" style={{ position: 'absolute', inset: 0 }}>{incoming}</div>
</div>
</AbsoluteFill>
);
};
5.3 Crossfade / Dissolve
const Crossfade: React.FC<{
outgoing: React.ReactNode;
incoming: React.ReactNode;
durationSec?: number;
}> = ({ outgoing, incoming, durationSec = 1 }) => {
const containerRef = useGSAPTimeline((tl, container) => {
const out = container.querySelector('.outgoing');
const inc = container.querySelector('.incoming');
tl.to(out, { opacity: 0, duration: durationSec })
.fromTo(inc, { opacity: 0 }, { opacity: 1, duration: durationSec }, 0);
});
return (
<AbsoluteFill>
<div ref={containerRef} style={{ width: '100%', height: '100%', position: 'relative' }}>
<div className="outgoing" style={{ position: 'absolute', inset: 0 }}>{outgoing}</div>
<div className="incoming" style={{ position: 'absolute', inset: 0 }}>{incoming}</div>
</div>
</AbsoluteFill>
);
};
6. Motion Graphics Templates (Remotion Compositions)
6.1 Lower Third Composition
const LowerThird: React.FC<{
name: string;
title: string;
holdDuration?: number; // seconds
accentColor?: string;
}> = ({ name, title, holdDuration = 4, accentColor = '#3b82f6' }) => {
const containerRef = useGSAPTimeline((tl, container) => {
const bar = container.querySelector('.lt-bar');
const nameEl = container.querySelector('.lt-name');
const titleEl = container.querySelector('.lt-title');
const accent = container.querySelector('.lt-accent');
// Entrance
tl.fromTo(bar,
{ scaleX: 0, transformOrigin: "left center" },
{ scaleX: 1, duration: 0.4, ease: "power2.out" }
)
.from(nameEl, { x: -30, opacity: 0, duration: 0.3, ease: "power2.out" }, "-=0.1")
.from(titleEl, { x: -20, opacity: 0, duration: 0.3, ease: "power2.out" }, "-=0.1")
.from(accent, {
scaleX: 0, transformOrigin: "left", duration: 0.3, ease: "power2.out"
}, "-=0.2")
// Hold
.to({}, { duration: holdDuration })
// Exit
.to([bar, nameEl, titleEl, accent], {
x: -50, opacity: 0, duration: 0.3, ease: "power2.in", stagger: 0.05
});
});
return (
<AbsoluteFill>
<div ref={containerRef} style={{ position: 'absolute', bottom: 80, left: 60 }}>
<div className="lt-bar" style={{
background: accentColor, padding: '12px 24px',
borderRadius: 4, minWidth: 300
}}>
<div className="lt-name" style={{ fontSize: 28, fontWeight: 'bold', color: '#fff' }}>
{name}
</div>
<div className="lt-title" style={{ fontSize: 18, color: 'rgba(255,255,255,0.8)' }}>
{title}
</div>
</div>
<div className="lt-accent" style={{
height: 3, background: '#fff', marginTop: 4, width: 60
}} />
</div>
</AbsoluteFill>
);
};
// Register as Remotion Composition
// <Composition id="LowerThird" component={LowerThird}
// durationInFrames={210} fps={30} width={1920} height={1080}
// defaultProps={{ name: "John Doe", title: "CEO, Company" }} />
6.2 Title Card Composition
const TitleCard: React.FC<{
mainTitle: string;
subtitle?: string;
bgColor?: string;
}> = ({ mainTitle, subtitle, bgColor = '#0f172a' }) => {
const containerRef = useGSAPWithFonts((tl, container) => {
const bgShape = container.querySelector('.bg-shape');
const titleEl = container.querySelector('.main-title');
const subtitleEl = container.querySelector('.subtitle');
const divider = container.querySelector('.divider');
// Background shape
tl.from(bgShape, { scale: 0, rotation: -45, duration: 0.8, ease: "back.out(1.7)" });
// Split + animate title
const split = SplitText.create(titleEl, { type: "chars", mask: "chars" });
tl.from(split.chars, {
y: "100%",
duration: 0.5,
stagger: 0.03,
ease: "power3.out"
}, "-=0.3");
// Subtitle
if (subtitleEl) {
tl.from(subtitleEl, { y: 20, opacity: 0, duration: 0.6, ease: "power2.out" }, "-=0.2");
}
// Divider line
tl.from(divider, { scaleX: 0, duration: 0.4, ease: "power2.out" }, "-=0.3");
});
return (
<AbsoluteFill style={{ background: bgColor, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<div ref={containerRef} style={{ textAlign: 'center', color: '#fff' }}>
<div className="bg-shape" style={{
position: 'absolute', width: 200, height: 200, borderRadius: '50%',
background: 'rgba(59,130,246,0.2)', top: '30%', left: '45%'
}} />
<h1 className="main-title" style={{ fontSize: 80, fontWeight: 'bold', position: 'relative' }}>
{mainTitle}
</h1>
<div className="divider" style={{
width: 80, height: 3, background: '#3b82f6', margin: '20px auto'
}} />
{subtitle && (
<p className="subtitle" style={{ fontSize: 32, opacity: 0.8 }}>{subtitle}</p>
)}
</div>
</AbsoluteFill>
);
};
6.3 Logo Reveal Composition
const LogoReveal: React.FC<{
logoSvgPaths: React.ReactNode;
logoText?: string;
variant?: 'draw' | 'scale' | 'morph';
}> = ({ logoSvgPaths, logoText, variant = 'draw' }) => {
const containerRef = useGSAPTimeline((tl, container) => {
if (variant === 'draw') {
const paths = container.querySelectorAll('.logo-path');
tl.from(paths, { drawSVG: 0, duration: 1.5, stagger: 0.1, ease: "power2.inOut" })
.to(paths, { fill: "#ffffff", duration: 0.5 }, "-=0.3");
} else if (variant === 'scale') {
const logo = container.querySelector('.logo-container');
tl.from(logo, { scale: 0, rotation: -180, duration: 1, ease: "back.out(1.7)" });
}
if (logoText) {
const text = container.querySelector('.logo-text');
tl.from(text, { opacity: 0, x: -20, duration: 0.5, ease: "power2.out" }, "-=0.2");
}
});
return (
<AbsoluteFill style={{ background: '#000', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<div ref={containerRef} style={{ display: 'flex', alignItems: 'center', gap: 24 }}>
<div className="logo-container">
<svg viewBox="0 0 100 100" width={120} height={120}>
{logoSvgPaths}
</svg>
</div>
{logoText && (
<span className="logo-text" style={{ fontSize: 48, color: '#fff', fontWeight: 'bold' }}>
{logoText}
</span>
)}
</div>
</AbsoluteFill>
);
};
6.4 Counter / Data Visualization Composition
const AnimatedCounter: React.FC<{
endValue: number;
prefix?: string;
suffix?: string;
durationSec?: number;
}> = ({ endValue, prefix = '', suffix = '', durationSec = 2 }) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const containerRef = useRef<HTMLDivElement>(null);
const [displayValue, setDisplayValue] = useState('0');
// Use GSAP to tween a proxy object, read its value on each frame
const proxyRef = useRef({ value: 0 });
const tlRef = useRef<gsap.core.Timeline | null>(null);
useEffect(() => {
const tl = gsap.timeline({ paused: true });
tl.to(proxyRef.current, {
value: endValue,
duration: durationSec,
ease: "power1.out"
});
tlRef.current = tl;
return () => { tl.kill(); };
}, [endValue, durationSec]);
useEffect(() => {
if (!tlRef.current) return;
tlRef.current.seek(frame / fps);
setDisplayValue(Math.round(proxyRef.current.value).toLocaleString());
}, [frame, fps]);
return (
<div ref={containerRef} style={{
fontSize: 96, fontWeight: 'bold', fontVariantNumeric: 'tabular-nums'
}}>
{prefix}{displayValue}{suffix}
</div>
);
};
// Bar chart animation
const AnimatedBarChart: React.FC<{ data: { label: string; value: number; color: string }[] }> = ({ data }) => {
const maxValue = Math.max(...data.map(d => d.value));
const containerRef = useGSAPTimeline((tl, container) => {
const bars = container.querySelectorAll('.bar');
tl.from(bars, {
scaleY: 0,
transformOrigin: "bottom",
duration: 0.8,
stagger: 0.1,
ease: "power2.out"
});
});
return (
<div ref={containerRef} style={{ display: 'flex', alignItems: 'flex-end', gap: 16, height: 300 }}>
{data.map((d, i) => (
<div key={i} style={{ textAlign: 'center' }}>
<div className="bar" style={{
width: 60, height: (d.value / maxValue) * 250,
background: d.color, borderRadius: '4px 4px 0 0'
}} />
<span style={{ fontSize: 14, marginTop: 8 }}>{d.label}</span>
</div>
))}
</div>
);
};
6.5 End Screen / Outro Composition
const EndScreen: React.FC<{
channelName: string;
socialLinks: { icon: string; label: string }[];
ctaText?: string;
}> = ({ channelName, socialLinks, ctaText = 'Subscribe' }) => {
const containerRef = useGSAPTimeline((tl, container) => {
const bg = container.querySelector('.outro-bg');
const icons = container.querySelectorAll('.social-icon');
const cta = container.querySelector('.cta-text');
const name = container.querySelector('.channel-name');
tl.from(bg, { opacity: 0, duration: 0.3 })
.from(icons, {
scale: 0, opacity: 0, duration: 0.4,
stagger: 0.1, ease: "back.out(1.7)"
})
.from(cta, { y: 20, opacity: 0, duration: 0.4 })
.from(name, { opacity: 0, duration: 0.3 });
});
return (
<AbsoluteFill>
<div ref={containerRef} style={{
width: '100%', height: '100%',
display: 'flex', flexDirection: 'column',
alignItems: 'center', justifyContent: 'center'
}}>
<div className="outro-bg" style={{
position: 'absolute', inset: 0, background: 'rgba(0,0,0,0.9)'
}} />
<div style={{ position: 'relative', textAlign: 'center' }}>
<div style={{ display: 'flex', gap: 24, marginBottom: 32 }}>
{socialLinks.map((link, i) => (
<div key={i} className="social-icon" style={{
width: 48, height: 48, borderRadius: '50%',
background: '#333', display: 'flex', alignItems: 'center', justifyContent: 'center',
color: '#fff', fontSize: 20
}}>
{link.icon}
</div>
))}
</div>
<p className="cta-text" style={{ fontSize: 32, color: '#fff', marginBottom: 16 }}>{ctaText}</p>
<p className="channel-name" style={{ fontSize: 24, color: '#999' }}>{channelName}</p>
</div>
</div>
</AbsoluteFill>
);
};
6.6 Composition Registration Pattern
import { Composition } from 'remotion';
export const RemotionRoot: React.FC = () => {
return (
<>
<Composition
id="TitleCard"
component={TitleCard}
durationInFrames={120}
fps={30}
width={1920}
height={1080}
defaultProps={{ mainTitle: "HELLO WORLD", subtitle: "A GSAP Motion Story" }}
/>
<Composition
id="LowerThird"
component={LowerThird}
durationInFrames={210}
fps={30}
width={1920}
height={1080}
defaultProps={{ name: "Jane Smith", title: "Creative Director" }}
/>
<Composition
id="LogoReveal"
component={LogoReveal}
durationInFrames={90}
fps={30}
width={1920}
height={1080}
defaultProps={{
logoSvgPaths: <circle className="logo-path" cx={50} cy={50} r={40} fill="none" stroke="#fff" strokeWidth={2} />,
logoText: "BRAND"
}}
/>
{/* Social media format variants */}
<Composition
id="IGStory-TitleCard"
component={TitleCard}
durationInFrames={150}
fps={30}
width={1080}
height={1920}
defaultProps={{ mainTitle: "SWIPE UP", subtitle: "Link in bio" }}
/>
</>
);
};
7. Advanced Techniques
7.1 Physics-Based Animations in Remotion
Physics2D plugin crea
…(truncated)