gsap-animation

安装量: 76
排名: #10322

安装

npx skills add https://github.com/notedit/happy-skills --skill gsap-animation

When to use Use this skill when creating Remotion video compositions that need GSAP's advanced animation capabilities beyond Remotion's built-in interpolate() and spring() . Use GSAP when you need: Complex timeline orchestration (nesting, labels, position parameters like "-=0.5" ) Text splitting animation (SplitText: chars/words/lines with mask reveals) SVG shape morphing (MorphSVG), stroke drawing (DrawSVG), path-following (MotionPath) Advanced easing (CustomEase from SVG paths, RoughEase, SlowMo, CustomBounce, CustomWiggle) Stagger with grid, center/edges distribution Character scramble/decode effects (ScrambleText) Reusable named effects via gsap.registerEffect() Use Remotion native interpolate() when: Simple single-property animations (fade, slide, scale) -- do NOT use GSAP for these Numeric counters/progress bars -- pure math, no timeline needed Standard easing curves Spring physics ( spring() ) GSAP Licensing: All plugins are 100% free since Webflow's 2024 acquisition (SplitText, MorphSVG, DrawSVG, etc.). Setup

In a Remotion project

npm install gsap // src/gsap-setup.ts -- import once at entry point import gsap from 'gsap' ; import { SplitText } from 'gsap/SplitText' ; import { MorphSVGPlugin } from 'gsap/MorphSVGPlugin' ; import { DrawSVGPlugin } from 'gsap/DrawSVGPlugin' ; import { MotionPathPlugin } from 'gsap/MotionPathPlugin' ; import { ScrambleTextPlugin } from 'gsap/ScrambleTextPlugin' ; import { CustomEase } from 'gsap/CustomEase' ; import { CustomBounce } from 'gsap/CustomBounce' ; import { CustomWiggle } from 'gsap/CustomWiggle' ; gsap . registerPlugin ( SplitText , MorphSVGPlugin , DrawSVGPlugin , MotionPathPlugin , ScrambleTextPlugin , CustomEase , CustomBounce , CustomWiggle , ) ; export { gsap } ; Core Hook: useGSAPTimeline The bridge between GSAP and Remotion. Creates a paused timeline, seeks it to frame / fps every frame. import { useCurrentFrame , useVideoConfig } from 'remotion' ; import gsap from 'gsap' ; import { useRef , useEffect } from 'react' ; 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 ) ; useEffect ( ( ) => { if ( ! containerRef . current ) return ; const ctx = gsap . context ( ( ) => { const tl = gsap . timeline ( { paused : true } ) ; buildTimeline ( tl , containerRef . current ! ) ; tlRef . current = tl ; } , containerRef ) ; return ( ) => { ctx . revert ( ) ; tlRef . current = null ; } ; } , [ ] ) ; useEffect ( ( ) => { if ( tlRef . current ) tlRef . current . seek ( frame / fps ) ; } , [ frame , fps ] ) ; return containerRef ; } For SplitText (needs font loading): import { delayRender , continueRender } from 'remotion' ; function useGSAPWithFonts ( 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 ) ; const [ handle ] = useState ( ( ) => delayRender ( ) ) ; useEffect ( ( ) => { document . fonts . ready . then ( ( ) => { if ( ! containerRef . current ) return ; const ctx = gsap . context ( ( ) => { const tl = gsap . timeline ( { paused : true } ) ; buildTimeline ( tl , containerRef . current ! ) ; tlRef . current = tl ; } , containerRef ) ; continueRender ( handle ) ; return ( ) => { ctx . revert ( ) ; } ; } ) ; } , [ ] ) ; useEffect ( ( ) => { if ( tlRef . current ) tlRef . current . seek ( frame / fps ) ; } , [ frame , fps ] ) ; return containerRef ; } 1. Text Animations SplitText Reveal (chars/words/lines) const TextReveal : React . FC < { text : string }

= ( { text } ) => { const containerRef = useGSAPWithFonts ( ( tl , container ) => { const split = SplitText . create ( container . querySelector ( '.heading' ) ! , { type : 'chars,words,lines' , mask : 'lines' , } ) ; 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

) ; } ; Patterns: Pattern SplitText Config Animation Line reveal type: "lines", mask: "lines" from lines: { y: "100%" } Char cascade type: "chars" from chars: { y: 50, opacity: 0, rotationX: -90 } Word scale type: "words" from words: { scale: 0, opacity: 0 } Char + color type: "chars" .from(chars, { y: 50 }).to(chars, { color: "#f00" }) ScrambleText (decode effect) Determinism warning: ScrambleText uses internal random character selection. Use --concurrency=1 when rendering to guarantee frame-perfect reproducibility across renders. const containerRef = useGSAPTimeline ( ( tl , container ) => { tl . to ( container . querySelector ( '.text' ) ! , { duration : 2 , scrambleText : { text : 'DECODED' , chars : '01' , revealDelay : 0.5 , speed : 0.3 } , } ) ; } ) ; Char sets: "upperCase" , "lowerCase" , "upperAndLowerCase" , "01" , or custom string. Text Highlight Box Colored rectangles scale in behind specific words. Uses SplitText for word-level positioning, then absolutely-positioned

boxes at lower z-index. const TextHighlightBox : React . FC < { text : string ; highlights : Array < { wordIndex : number ; color : string } > ; highlightDelay ? : number ; highlightStagger ? : number ; } > = ( { text , highlights , highlightDelay = 0.5 , highlightStagger = 0.3 } ) => { const containerRef = useGSAPWithFonts ( ( tl , container ) => { const textEl = container . querySelector ( '.highlight-text' ) ! ; const split = SplitText . create ( textEl , { type : 'words' } ) ; // Entrance: words fade in tl . from ( split . words , { y : 20 , opacity : 0 , duration : 0.5 , stagger : 0.05 , ease : 'power2.out' , } ) ; // Highlight boxes scale in behind target words highlights . forEach ( ( { wordIndex , color } , i ) => { const word = split . words [ wordIndex ] as HTMLElement ; if ( ! word ) return ; const box = document . createElement ( 'div' ) ; Object . assign ( box . style , { position : 'absolute' , left : ` ${ word . offsetLeft - 4 } px ` , top : ` ${ word . offsetTop - 2 } px ` , width : ` ${ word . offsetWidth + 8 } px ` , height : ` ${ word . offsetHeight + 4 } px ` , background : color , borderRadius : '4px' , zIndex : '-1' , transformOrigin : 'left center' , transform : 'scaleX(0)' , } ) ; textEl . appendChild ( box ) ; tl . to ( box , { scaleX : 1 , duration : 0.3 , ease : 'power2.out' , } , highlightDelay + i * highlightStagger ) ; } ) ; } ) ; return ( < AbsoluteFill style = { { display : 'flex' , alignItems : 'center' , justifyContent : 'center' } } > < div ref = { containerRef } > < p className = " highlight-text " style = { { fontSize : 64 , fontWeight : 'bold' , color : '#fff' , position : 'relative' , maxWidth : '70%' , lineHeight : 1.2 , } } > { text }

</ AbsoluteFill

) ; } ; Props: highlights is an array of { wordIndex, color } targeting specific words (0-indexed from SplitText). 2. SVG Animations MorphSVG (shape morphing) const containerRef = useGSAPTimeline ( ( tl , container ) => { tl . to ( container . querySelector ( '#path' ) ! , { morphSVG : { shape : '#target-path' , type : 'rotational' , map : 'size' } , duration : 1.5 , ease : 'power2.inOut' , } ) ; } ) ; Option Values type "linear" (default), "rotational" map "size" , "position" , "complexity" shapeIndex Integer for point alignment offset DrawSVG (stroke animation) const containerRef = useGSAPTimeline ( ( tl , container ) => { const paths = container . querySelectorAll ( '.logo-path' ) ; tl . from ( paths , { drawSVG : 0 , duration : 1.5 , stagger : 0.2 , ease : 'power2.inOut' } ) . to ( paths , { fill : '#ffffff' , duration : 0.5 } , '-=0.3' ) ; } ) ; Pattern DrawSVG Value Draw from nothing 0 Draw from center "50% 50%" Show segment "20% 80%" Erase "100% 100%" MotionPath (path following) const containerRef = useGSAPTimeline ( ( tl , container ) => { tl . to ( container . querySelector ( '.element' ) ! , { motionPath : { path : container . querySelector ( '#svg-path' ) as SVGPathElement , align : container . querySelector ( '#svg-path' ) as SVGPathElement , alignOrigin : [ 0.5 , 0.5 ] , autoRotate : true , } , duration : 3 , ease : 'power1.inOut' , } ) ; } ) ; 3. 3D Transform Patterns Performance note: Limit to 3-4 simultaneous 3D containers per scene. Each preserve-3d container triggers GPU compositing layers. CardFlip3D const CardFlip3D : React . FC < { frontContent : React . ReactNode ; backContent : React . ReactNode ; flipDelay ? : number ; flipDuration ? : number ; }

= ( { frontContent , backContent , flipDelay = 0.5 , flipDuration = 1.2 } ) => { const containerRef = useGSAPTimeline ( ( tl , container ) => { const card = container . querySelector ( '.card-3d' ) ! ; tl . to ( card , { rotateY : 180 , duration : flipDuration , ease : 'power2.inOut' , } , flipDelay ) ; } ) ; return ( < AbsoluteFill style = { { display : 'flex' , alignItems : 'center' , justifyContent : 'center' } }

< div ref = { containerRef } style = { { perspective : 800 } }

< div className = " card-3d " style = { { width : 500 , height : 320 , position : 'relative' , transformStyle : 'preserve-3d' , } }

{ / Front face / } < div style = { { position : 'absolute' , inset : 0 , backfaceVisibility : 'hidden' , background : '#1e293b' , borderRadius : 16 , display : 'flex' , alignItems : 'center' , justifyContent : 'center' , padding : 32 , } }

{ frontContent } </ div

{ / Back face (pre-rotated 180deg) / } < div style = { { position : 'absolute' , inset : 0 , backfaceVisibility : 'hidden' , background : '#3b82f6' , borderRadius : 16 , transform : 'rotateY(180deg)' , display : 'flex' , alignItems : 'center' , justifyContent : 'center' , padding : 32 , } }

{ backContent } </ div

</ div

</ div

</ AbsoluteFill

) ; } ; PerspectiveEntrance Two elements enter from opposite sides with rotateY, converging to center. const PerspectiveEntrance : React . FC < { leftContent : React . ReactNode ; rightContent : React . ReactNode ; }

= ( { leftContent , rightContent } ) => { const containerRef = useGSAPTimeline ( ( tl , container ) => { const left = container . querySelector ( '.pe-left' ) ! ; const right = container . querySelector ( '.pe-right' ) ! ; tl . from ( left , { x : - 600 , rotateY : 60 , opacity : 0 , duration : 0.8 , ease : 'power3.out' } ) . from ( right , { x : 600 , rotateY : - 60 , opacity : 0 , duration : 0.8 , ease : 'power3.out' } , '-=0.5' ) . to ( left , { rotateY : 0 , duration : 0.4 , ease : 'power2.out' } , '-=0.3' ) . to ( right , { rotateY : 0 , duration : 0.4 , ease : 'power2.out' } , '-=0.3' ) ; } ) ; return ( < AbsoluteFill style = { { display : 'flex' , alignItems : 'center' , justifyContent : 'center' , perspective : 800 } }

< div ref = { containerRef } style = { { display : 'flex' , gap : 40 , alignItems : 'center' } }

< div className = " pe-left "

{ leftContent } </ div

< div className = " pe-right "

{ rightContent } </ div

</ div

</ AbsoluteFill

) ; } ; RotateXTextSwap Outgoing text tilts backward, incoming text falls forward. Uses transformOrigin to pivot from the correct edge. const RotateXTextSwap : React . FC < { textOut : string ; textIn : string ; swapDelay ? : number ; }

= ( { textOut , textIn , swapDelay = 1.0 } ) => { const containerRef = useGSAPWithFonts ( ( tl , container ) => { const outEl = container . querySelector ( '.swap-out' ) ! ; const inEl = container . querySelector ( '.swap-in' ) ! ; // Out: tilt backward tl . to ( outEl , { rotateX : 90 , opacity : 0 , duration : 0.5 , transformOrigin : 'center bottom' , ease : 'power2.in' , } , swapDelay ) ; // In: fall forward tl . fromTo ( inEl , { rotateX : - 90 , opacity : 0 , transformOrigin : 'center top' } , { rotateX : 0 , opacity : 1 , duration : 0.6 , ease : 'power2.out' } , `

${

0.15 } ` ) ; } ) ; return ( < AbsoluteFill style = { { display : 'flex' , alignItems : 'center' , justifyContent : 'center' , perspective : 600 } }

< div ref = { containerRef } style = { { position : 'relative' , textAlign : 'center' } }

< h1 className = " swap-out " style = { { fontSize : 80 , fontWeight : 'bold' , color : '#fff' } }

{ textOut } </ h1

< h1 className = " swap-in " style = { { fontSize : 80 , fontWeight : 'bold' , color : '#3b82f6' , position : 'absolute' , inset : 0 , } }

{ textIn } </ h1

</ div

</ AbsoluteFill

) ; } ; 4. Interaction Simulation CursorClick Simulates a cursor navigating to a target and clicking. Cursor slides in, target depresses, ripple expands. const CursorClick : React . FC < { targetSelector : string ; cursorDelay ? : number ; clickDelay ? : number ; children : React . ReactNode ; }

= ( { targetSelector , cursorDelay = 0.3 , clickDelay = 0.8 , children } ) => { const containerRef = useGSAPTimeline ( ( tl , container ) => { const target = container . querySelector ( targetSelector ) ! ; const cursor = container . querySelector ( '.sim-cursor' ) ! ; const ripple = container . querySelector ( '.sim-ripple' ) ! ; const rect = target . getBoundingClientRect ( ) ; const containerRect = container . getBoundingClientRect ( ) ; const targetX = rect . left - containerRect . left + rect . width / 2 ; const targetY = rect . top - containerRect . top + rect . height / 2 ; // Cursor travels to target tl . fromTo ( cursor , { x : containerRect . width + 40 , y : targetY - 20 } , { x : targetX , y : targetY , duration : clickDelay , ease : 'power2.inOut' } , cursorDelay ) ; // Click: target depresses and releases tl . to ( target , { scale : 0.95 , duration : 0.1 , ease : 'power2.in' } ) . to ( target , { scale : 1 , duration : 0.15 , ease : 'power2.out' } ) ; // Ripple expands (overlaps with click release) tl . fromTo ( ripple , { x : targetX , y : targetY , scale : 0 , opacity : 1 } , { scale : 3 , opacity : 0 , duration : 0.6 , ease : 'power2.out' } , '<-0.1' ) ; } ) ; return ( < AbsoluteFill

< div ref = { containerRef } style = { { position : 'relative' , width : '100%' , height : '100%' } }

{ children } { / Cursor / } < svg className = " sim-cursor " width = " 24 " height = " 24 " viewBox = " 0 0 24 24 " style = { { position : 'absolute' , top : 0 , left : 0 , zIndex : 100 , pointerEvents : 'none' } }

< path d = " M5 3l14 8-6 2-4 6z " fill = "

fff

" stroke = "

000

" strokeWidth = " 1.5 " /> </ svg

{ / Ripple / } < div className = " sim-ripple " style = { { position : 'absolute' , top : 0 , left : 0 , width : 40 , height : 40 , borderRadius : '50%' , border : '2px solid rgba(59,130,246,0.6)' , transform : 'translate(-50%, -50%) scale(0)' , pointerEvents : 'none' , } } /> </ div

</ AbsoluteFill

) ; } ; Props: targetSelector is a CSS selector for the element to "click" within the container. Cursor enters from off-screen right. 5. Transitions Clip-Path Transitions Performance note: Complex clip-path animations (especially polygon ) can slow down frame generation in Remotion's headless Chrome. If render times are high, consider replacing with opacity/transform-based alternatives or simplify to circle / inset shapes. const containerRef = useGSAPTimeline ( ( tl , container ) => { const scene = container . querySelector ( '.scene' ) ! ; // Circle reveal tl . fromTo ( scene , { clipPath : 'circle(0% at 50% 50%)' } , { clipPath : 'circle(75% at 50% 50%)' , duration : 1 , ease : 'power2.out' } ) ; } ) ; Transition From To Circle reveal circle(0% at 50% 50%) circle(75% at 50% 50%) Wipe left polygon(0 0, 0 0, 0 100%, 0 100%) polygon(0 0, 100% 0, 100% 100%, 0 100%) Iris 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) Slide / Crossfade // Slide transition tl . to ( outgoing , { x : '-100%' , duration : 0.6 , ease : 'power2.inOut' } ) . fromTo ( incoming , { x : '100%' } , { x : '0%' , duration : 0.6 , ease : 'power2.inOut' } , 0 ) ; // Crossfade tl . to ( outgoing , { opacity : 0 , duration : 1 } ) . fromTo ( incoming , { opacity : 0 } , { opacity : 1 , duration : 1 } , 0 ) ; 6. Templates Lower Third const LowerThird : React . FC < { name : string ; title : string ; hold ? : number }

= ( { name , title , hold = 4 , } ) => { const containerRef = useGSAPTimeline ( ( tl , container ) => { const bar = container . querySelector ( '.lt-bar' ) ! ; const nameEl = container . querySelector ( '.lt-name' ) ! ; const titleEl = container . querySelector ( '.lt-title' ) ! ; // In tl . fromTo ( bar , { scaleX : 0 , transformOrigin : 'left' } , { scaleX : 1 , duration : 0.4 , ease : 'power2.out' } ) . from ( nameEl , { x : - 30 , opacity : 0 , duration : 0.3 } , '-=0.1' ) . from ( titleEl , { x : - 20 , opacity : 0 , duration : 0.3 } , '-=0.1' ) // Hold . to ( { } , { duration : hold } ) // Out . to ( [ bar , nameEl , titleEl ] , { x : - 50 , opacity : 0 , duration : 0.3 , stagger : 0.05 , ease : 'power2.in' } ) ; } ) ; return ( < AbsoluteFill

< div ref = { containerRef } style = { { position : 'absolute' , bottom : 80 , left : 60 } }

< div className = " lt-bar " style = { { background : '#3b82f6' , padding : '12px 24px' , borderRadius : 4 } }

< 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

</ AbsoluteFill

) ; } ; Title Card const TitleCard : React . FC < { mainTitle : string ; subtitle ? : string }

= ( { mainTitle , subtitle } ) => { const containerRef = useGSAPWithFonts ( ( tl , container ) => { const bgShape = container . querySelector ( '.bg-shape' ) ! ; const titleEl = container . querySelector ( '.main-title' ) ! ; const divider = container . querySelector ( '.divider' ) ! ; tl . from ( bgShape , { scale : 0 , rotation : - 45 , duration : 0.8 , ease : 'back.out(1.7)' } ) ; 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' ) . from ( '.subtitle' , { y : 20 , opacity : 0 , duration : 0.6 } , '-=0.2' ) . from ( divider , { scaleX : 0 , duration : 0.4 , ease : 'power2.out' } , '-=0.3' ) ; } ) ; return ( < AbsoluteFill style = { { background : '#0f172a' , 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

) ; } ; Logo Reveal (DrawSVG) const LogoReveal : React . FC < { svgContent : React . ReactNode ; text ? : string }

= ( { svgContent , text } ) => { const containerRef = useGSAPTimeline ( ( tl , container ) => { const paths = container . querySelectorAll ( '.logo-path' ) ; tl . from ( paths , { drawSVG : 0 , duration : 1.5 , stagger : 0.1 , ease : 'power2.inOut' } ) . to ( paths , { fill : '#fff' , duration : 0.5 } , '-=0.3' ) ; if ( text ) { tl . from ( container . querySelector ( '.logo-text' ) ! , { opacity : 0 , x : - 20 , duration : 0.5 } , '-=0.2' ) ; } } ) ; return ( < AbsoluteFill style = { { background : '#000' , display : 'flex' , alignItems : 'center' , justifyContent : 'center' } }

< div ref = { containerRef } style = { { display : 'flex' , alignItems : 'center' , gap : 24 } }

< svg viewBox = " 0 0 100 100 " width = { 120 } height = { 120 }

{ svgContent } </ svg

{ text && < span className = " logo-text " style = { { fontSize : 48 , color : '#fff' , fontWeight : 'bold' } }

{ text } </ span

} </ div

</ AbsoluteFill

) ; } ; Animated Counter Uses Remotion's interpolate() for deterministic frame-by-frame calculation. No GSAP needed — counters are pure math. import { interpolate , useCurrentFrame , useVideoConfig } from 'remotion' ; const AnimatedCounter : React . FC < { endValue : number ; prefix ? : string ; suffix ? : string ; durationInSeconds ? : number ; }

= ( { endValue , prefix = '' , suffix = '' , durationInSeconds = 2 } ) => { const frame = useCurrentFrame ( ) ; const { fps } = useVideoConfig ( ) ; const value = interpolate ( frame , [ 0 , durationInSeconds * fps ] , [ 0 , endValue ] , { extrapolateRight : 'clamp' , easing : ( t ) => 1 - Math . pow ( 1 - t , 2 ) , // power1.out equivalent } ) ; return ( < div style = { { fontSize : 96 , fontWeight : 'bold' , fontVariantNumeric : 'tabular-nums' } }

{ prefix } { Math . round ( value ) . toLocaleString ( ) } { suffix } </ div

) ; } ; Outro (closing scene) const Outro : React . FC < { headline : string ; tagline ? : string ; logoSvg ? : React . ReactNode }

= ( { headline , tagline , logoSvg , } ) => { const containerRef = useGSAPWithFonts ( ( tl , container ) => { const headlineEl = container . querySelector ( '.outro-headline' ) ! ; const split = SplitText . create ( headlineEl , { type : 'chars' , mask : 'chars' } ) ; tl . from ( split . chars , { y : '100%' , duration : 0.5 , stagger : 0.03 , ease : 'power3.out' } ) ; if ( tagline ) { tl . from ( '.outro-tagline' , { opacity : 0 , y : 15 , duration : 0.5 , ease : 'power2.out' } , '-=0.2' ) ; } if ( logoSvg ) { const paths = container . querySelectorAll ( '.outro-logo path' ) ; tl . from ( paths , { drawSVG : 0 , duration : 1.2 , stagger : 0.08 , ease : 'power2.inOut' } , '-=0.3' ) . to ( paths , { fill : '#fff' , duration : 0.4 } , '-=0.2' ) ; } } ) ; return ( < AbsoluteFill style = { { background : '#0f172a' , display : 'flex' , alignItems : 'center' , justifyContent : 'center' } }

< div ref = { containerRef } style = { { textAlign : 'center' , color : '#fff' } }

< h1 className = " outro-headline " style = { { fontSize : 72 , fontWeight : 'bold' } }

{ headline } </ h1

{ tagline && < p className = " outro-tagline " style = { { fontSize : 28 , opacity : 0.7 , marginTop : 16 } }

{ tagline } </ p

} { logoSvg && < div className = " outro-logo " style = { { marginTop : 40 } }

{ logoSvg } </ div

} </ div

</ AbsoluteFill

) ; } ; SplitScreenComparison Two panels side-by-side with staggered entrance, optional center badge, and left-panel dim effect. const SplitScreenComparison : React . FC < { leftPanel : React . ReactNode ; rightPanel : React . ReactNode ; leftLabel ? : string ; rightLabel ? : string ; centerElement ? : React . ReactNode ; dimLeft ? : boolean ; hold ? : number ; }

= ( { leftPanel , rightPanel , leftLabel , rightLabel , centerElement , dimLeft = false , hold = 2 } ) => { const containerRef = useGSAPTimeline ( ( tl , container ) => { const left = container . querySelector ( '.ssc-left' ) ! ; const right = container . querySelector ( '.ssc-right' ) ! ; const badge = container . querySelector ( '.ssc-badge' ) ; // Staggered entrance tl . from ( left , { x : - 80 , opacity : 0 , duration : 0.6 , ease : 'power2.out' } ) . from ( right , { x : 80 , opacity : 0 , duration : 0.6 , ease : 'power2.out' } , '-=0.4' ) ; // Center badge pop if ( badge ) { tl . from ( badge , { scale : 0 , duration : 0.4 , ease : 'back.out(2)' } , '-=0.2' ) ; } // Hold tl . to ( { } , { duration : hold } ) ; // Dim left, pop right (comparison effect) if ( dimLeft ) { tl . to ( left , { opacity : 0.5 , filter : 'blur(4px)' , duration : 0.5 , ease : 'power2.inOut' } ) . to ( right , { scale : 1.02 , duration : 0.5 , ease : 'power2.out' } , '<' ) ; } } ) ; return ( < AbsoluteFill style = { { display : 'flex' } }

< div ref = { containerRef } style = { { display : 'flex' , width : '100%' , height : '100%' } }

< div className = " ssc-left " style = { { flex : 1 , background : '#1e1e2e' , display : 'flex' , flexDirection : 'column' , alignItems : 'center' , justifyContent : 'center' , padding : 40 , position : 'relative' , } }

{ leftLabel && < div style = { { fontSize : 24 , opacity : 0.6 , marginBottom : 16 , color : '#fff' } }

{ leftLabel } </ div

} { leftPanel } </ div

{ centerElement && ( < div className = " ssc-badge " style = { { position : 'absolute' , left : '50%' , top : '50%' , transform : 'translate(-50%, -50%)' , zIndex : 10 , background : '#3b82f6' , borderRadius : '50%' , width : 60 , height : 60 , display : 'flex' , alignItems : 'center' , justifyContent : 'center' , fontSize : 20 , fontWeight : 'bold' , color : '#fff' , } }

{ centerElement } </ div

) } < div className = " ssc-right " style = { { flex : 1 , background : 'rgba(255,255,255,0.06)' , backdropFilter : 'blur(20px)' , display : 'flex' , flexDirection : 'column' , alignItems : 'center' , justifyContent : 'center' , padding : 40 , } }

{ rightLabel && < div style = { { fontSize : 24 , opacity : 0.6 , marginBottom : 16 , color : '#fff' } }

{ rightLabel } </ div

} { rightPanel } </ div

</ div

</ AbsoluteFill

) ; } ; Props: Set dimLeft: true for comparison scenes where right panel should "win". Left panel uses dark background (

1e1e2e

), right panel uses glassmorphism ( backdropFilter: blur(20px) ). 7. Registered Effects Pre-register effects for fluent timeline API. Import once at entry point. // lib/gsap-effects.ts gsap . registerEffect ( { name : 'textReveal' , effect : ( targets , config ) => { const split = SplitText . create ( targets , { type : 'lines' , mask : 'lines' } ) ; return gsap . from ( split . lines , { y : '100%' , duration : config . duration , stagger : config . stagger , ease : config . ease } ) ; } , defaults : { duration : 0.6 , stagger : 0.15 , ease : 'power3.out' } , extendTimeline : true , } ) ; gsap . registerEffect ( { name : 'charCascade' , effect : ( targets , config ) => { const split = SplitText . create ( targets , { type : 'chars' } ) ; return gsap . from ( split . chars , { y : 50 , opacity : 0 , rotationX : - 90 , duration : config . duration , stagger : config . stagger , ease : config . ease } ) ; } , defaults : { duration : 0.5 , stagger : 0.02 , ease : 'back.out(1.7)' } , extendTimeline : true , } ) ; gsap . registerEffect ( { name : 'circleReveal' , effect : ( targets , config ) => gsap . fromTo ( targets , { clipPath : 'circle(0% at 50% 50%)' } , { clipPath : 'circle(75% at 50% 50%)' , duration : config . duration , ease : config . ease } ) , defaults : { duration : 1 , ease : 'power2.out' } , extendTimeline : true , } ) ; gsap . registerEffect ( { name : 'wipeIn' , effect : ( targets , config ) => gsap . fromTo ( targets , { clipPath : 'inset(0 100% 0 0)' } , { clipPath : 'inset(0 0% 0 0)' , duration : config . duration , ease : config . ease } ) , defaults : { duration : 0.8 , ease : 'power2.inOut' } , extendTimeline : true , } ) ; gsap . registerEffect ( { name : 'drawIn' , effect : ( targets , config ) => gsap . from ( targets , { drawSVG : 0 , duration : config . duration , stagger : config . stagger , ease : config . ease } ) , defaults : { duration : 1.5 , stagger : 0.1 , ease : 'power2.inOut' } , extendTimeline : true , } ) ; gsap . registerEffect ( { name : 'flipCard' , effect : ( targets , config ) => gsap . to ( targets , { rotateY : 180 , duration : config . duration , ease : config . ease } ) , defaults : { duration : 1.2 , ease : 'power2.inOut' } , extendTimeline : true , } ) ; gsap . registerEffect ( { name : 'perspectiveIn' , effect : ( targets , config ) => { const fromX = config . fromRight ? 600 : - 600 ; const fromRotateY = config . fromRight ? - 60 : 60 ; return gsap . from ( targets , { x : fromX , rotateY : fromRotateY , opacity : 0 , duration : config . duration , ease : config . ease , } ) ; } , defaults : { duration : 0.8 , ease : 'power3.out' , fromRight : false } , extendTimeline : true , } ) ; gsap . registerEffect ( { name : 'textHighlight' , effect : ( targets , config ) => gsap . from ( targets , { scaleX : 0 , transformOrigin : 'left center' , duration : config . duration , stagger : config . stagger , ease : config . ease , } ) , defaults : { duration : 0.3 , stagger : 0.3 , ease : 'power2.out' } , extendTimeline : true , } ) ; gsap . registerEffect ( { name : 'cursorClick' , effect : ( targets , config ) => { const tl = gsap . timeline ( ) ; tl . to ( targets , { scale : 0.95 , duration : 0.1 , ease : 'power2.in' } ) . to ( targets , { scale : 1 , duration : 0.15 , ease : 'power2.out' } ) ; return tl ; } , defaults : { } , extendTimeline : true , } ) ; // Simple property animations (fade, slide, scale) should use Remotion's // interpolate() directly — GSAP registered effects are reserved for // operations that Remotion cannot do natively (SplitText, DrawSVG, etc.). Usage: const containerRef = useGSAPTimeline ( ( tl , container ) => { tl . textReveal ( '.title' ) . charCascade ( '.subtitle' , { } , '-=0.3' ) . circleReveal ( '.scene-2' , { } , '+=0.5' ) . flipCard ( '.card-3d' ) . perspectiveIn ( '.panel-left' ) . perspectiveIn ( '.panel-right' , { fromRight : true } , '-=0.5' ) . textHighlight ( '.highlight-box' ) . cursorClick ( '.cta-button' , { } , '+=0.3' ) . drawIn ( '.logo-path' ) ; } ) ; 8. Easing Reference Motion Feel GSAP Ease Use Case Smooth deceleration power2.out Standard entrance Strong deceleration power3.out / expo.out Dramatic entrance Gentle acceleration power2.in Standard exit Smooth both power1.inOut Scene transitions Slight overshoot back.out(1.7) Attention, bounce-in Elastic spring elastic.out(1, 0.5) Logo, playful Bounce CustomBounce Impact, landing Shake/vibrate CustomWiggle Attention, error Organic/jagged RoughEase Tension, glitch Custom curve CustomEase.create("id", "M0,0 C...") Brand-specific Slow in middle slow(0.7, 0.7, false) Cinematic speed ramp GSAP-only eases (no Remotion equivalent): CustomEase, RoughEase, SlowMo, CustomBounce, CustomWiggle, ExpoScaleEase. 9. Combining with react-animation Skill const CombinedScene : React . FC = ( ) => ( < AbsoluteFill

{ / react-animation: visual atmosphere / } < Aurora colorStops = { [ '#3A29FF' , '#FF94B4' ] } /> { / gsap-animation: text + motion / } < GSAPTextReveal text = " Beautiful Motion " /> < GSAPLogoReveal svgContent = { ... } /> { / react-animation: film grain overlay / } < NoiseOverlay opacity = { 0.05 } /> </ AbsoluteFill

) ; Skill Best For react-animation Visual backgrounds (Aurora, Silk, Particles), shader effects, WebGL gsap-animation Text animation, SVG motion, timeline orchestration, transitions, templates 10. Composition Registration export const RemotionRoot : React . FC = ( ) => ( <

< 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 = { { svgContent : < circle className = " logo-path " cx = { 50 } cy = { 50 } r = { 40 } fill = " none " stroke = "

fff

" strokeWidth = { 2 } /> , text : 'BRAND' } } /> < Composition id = " Outro " component = { Outro } durationInFrames = { 120 } fps = { 30 } width = { 1920 } height = { 1080 } defaultProps = { { headline : 'THANK YOU' , tagline : 'See you next time' } } /> { / Social media variants / } < Composition id = " IGStory-TitleCard " component = { TitleCard } durationInFrames = { 150 } fps = { 30 } width = { 1080 } height = { 1920 } defaultProps = { { mainTitle : 'SWIPE UP' } } /> </

) ; 11. Rendering

Default MP4

npx remotion render src/index.ts TitleCard --output out/title.mp4

High quality

npx remotion render src/index.ts TitleCard --codec h264 --crf 15

GIF

npx remotion render src/index.ts TitleCard --codec gif --every-nth-frame 2

ProRes for editing

npx remotion render src/index.ts TitleCard --codec prores --prores-profile 4444

With audio

Use

Transparent background (alpha channel)

npx remotion render src/index.ts Overlay --codec prores --prores-profile 4444 --output out/overlay.mov npx remotion render src/index.ts Overlay --codec vp9 --output out/overlay.webm Transparent Background Formats Format Alpha Support Quality File Size Compatibility ProRes 4444 ( .mov ) Yes Lossless Large Final Cut, Premiere, DaVinci WebM VP9 ( .webm ) Yes Lossy Small Web, Chrome, After Effects MP4 H.264 ( .mp4 ) No Lossy Small Universal playback GIF ( .gif ) 1-bit only Low Medium Web, social Use ProRes 4444 for professional compositing. Use WebM VP9 for web overlays. MP4/H.264 does not support alpha channels.

返回排行榜