Skip to main content
Glama
template.tsx.j212.6 kB
{/* chuk-motion/src/chuk_motion/components/animations/PanelCascade/template.tsx.j2 */} import React from 'react'; import { AbsoluteFill, useCurrentFrame, interpolate, Easing, spring, useVideoConfig } from 'remotion'; interface PanelCascadeProps { children: React.ReactNode[]; cascadeType?: string; staggerDelay?: number; startFrame: number; durationInFrames: number; } export const PanelCascade: React.FC<PanelCascadeProps> = ({ children, cascadeType = 'from_edges', staggerDelay = 0.08, startFrame, durationInFrames, }) => { const frame = useCurrentFrame(); const { fps, width, height } = useVideoConfig(); const relativeFrame = frame - startFrame; // Don't render if outside the time range if (frame < startFrame || frame >= startFrame + durationInFrames) { return null; } // Motion tokens - token-first approach const DURATION_FAST = [[ motion.duration.fast.frames_30fps ]]; const DURATION_MEDIUM = [[ motion.duration.medium.frames_30fps ]]; // Easing from tokens const easeOutExpo = [[ motion.easing.ease_out_expo.curve ]]; const easeOutBack = [[ motion.easing.ease_out_back.curve ]]; // Spring config from tokens const bouncySpring = { damping: [[ motion.spring_configs.bouncy.config.damping ]], mass: [[ motion.spring_configs.bouncy.config.mass ]], stiffness: [[ motion.spring_configs.bouncy.config.stiffness ]], }; // Convert stagger delay to frames const staggerFrames = Math.round(staggerDelay * 30); // Helper function to create custom easing const createEasing = (curve: number[]): Easing.EasingFunction => { return Easing.bezier(curve[0], curve[1], curve[2], curve[3]); }; // Calculate grid dimensions (assuming square-ish grid) const panelCount = React.Children.count(children); const cols = Math.ceil(Math.sqrt(panelCount)); const rows = Math.ceil(panelCount / cols); // Helper to get panel position in grid const getPanelPosition = (index: number) => { const row = Math.floor(index / cols); const col = index % cols; return { row, col }; }; // Helper to calculate distance from center const getDistanceFromCenter = (index: number) => { const { row, col } = getPanelPosition(index); const centerRow = (rows - 1) / 2; const centerCol = (cols - 1) / 2; return Math.sqrt(Math.pow(row - centerRow, 2) + Math.pow(col - centerCol, 2)); }; // Helper to get edge distance (for from_edges cascade) const getEdgeDistance = (index: number) => { const { row, col } = getPanelPosition(index); const distFromLeft = col; const distFromRight = cols - 1 - col; const distFromTop = row; const distFromBottom = rows - 1 - row; return Math.min(distFromLeft, distFromRight, distFromTop, distFromBottom); }; // ============================================================================ // RENDER PANELS WITH CASCADE ANIMATIONS // ============================================================================ return ( <AbsoluteFill style={{ display: 'grid', gridTemplateColumns: `repeat(${cols}, 1fr)`, gridTemplateRows: `repeat(${rows}, 1fr)`, gap: parseInt('[[ spacing.spacing.md ]]'), padding: parseInt('[[ spacing.spacing.xl ]]'), pointerEvents: 'none' }}> {React.Children.map(children, (child, index) => { let panelDelay = 0; let animationProps: any = {}; // ============================================================================ // FROM_EDGES CASCADE // ============================================================================ if (cascadeType === 'from_edges') { const edgeDist = getEdgeDistance(index); panelDelay = edgeDist * staggerFrames; const opacity = interpolate( relativeFrame, [panelDelay, panelDelay + DURATION_MEDIUM], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: createEasing(easeOutExpo), } ); // Determine direction based on closest edge const { row, col } = getPanelPosition(index); const distFromLeft = col; const distFromRight = cols - 1 - col; const distFromTop = row; const distFromBottom = rows - 1 - row; const minDist = Math.min(distFromLeft, distFromRight, distFromTop, distFromBottom); let translateX = 0; let translateY = 0; if (minDist === distFromLeft) translateX = -50; else if (minDist === distFromRight) translateX = 50; else if (minDist === distFromTop) translateY = -50; else translateY = 50; const currentTranslateX = interpolate( relativeFrame, [panelDelay, panelDelay + DURATION_MEDIUM], [translateX, 0], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: createEasing(easeOutExpo), } ); const currentTranslateY = interpolate( relativeFrame, [panelDelay, panelDelay + DURATION_MEDIUM], [translateY, 0], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: createEasing(easeOutExpo), } ); animationProps = { opacity, transform: `translate(${currentTranslateX}px, ${currentTranslateY}px)`, }; } // ============================================================================ // FROM_CENTER CASCADE // ============================================================================ else if (cascadeType === 'from_center') { const centerDist = getDistanceFromCenter(index); panelDelay = centerDist * staggerFrames; const opacity = interpolate( relativeFrame, [panelDelay, panelDelay + DURATION_MEDIUM], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: createEasing(easeOutExpo), } ); const scale = interpolate( relativeFrame, [panelDelay, panelDelay + DURATION_MEDIUM], [0.5, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: createEasing(easeOutExpo), } ); animationProps = { opacity, transform: `scale(${scale})`, }; } // ============================================================================ // BOUNCE_IN CASCADE // ============================================================================ else if (cascadeType === 'bounce_in') { panelDelay = index * staggerFrames; const progress = spring({ frame: Math.max(0, relativeFrame - panelDelay), fps, config: bouncySpring, }); const opacity = interpolate(progress, [0, 1], [0, 1]); const scale = interpolate(progress, [0, 1], [0.3, 1]); animationProps = { opacity, transform: `scale(${scale})`, }; } // ============================================================================ // SEQUENTIAL_LEFT CASCADE // ============================================================================ else if (cascadeType === 'sequential_left') { panelDelay = index * staggerFrames; const opacity = interpolate( relativeFrame, [panelDelay, panelDelay + DURATION_FAST], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: createEasing(easeOutExpo), } ); const translateX = interpolate( relativeFrame, [panelDelay, panelDelay + DURATION_FAST], [-30, 0], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: createEasing(easeOutExpo), } ); animationProps = { opacity, transform: `translateX(${translateX}px)`, }; } // ============================================================================ // SEQUENTIAL_RIGHT CASCADE // ============================================================================ else if (cascadeType === 'sequential_right') { panelDelay = (panelCount - 1 - index) * staggerFrames; const opacity = interpolate( relativeFrame, [panelDelay, panelDelay + DURATION_FAST], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: createEasing(easeOutExpo), } ); const translateX = interpolate( relativeFrame, [panelDelay, panelDelay + DURATION_FAST], [30, 0], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: createEasing(easeOutExpo), } ); animationProps = { opacity, transform: `translateX(${translateX}px)`, }; } // ============================================================================ // SEQUENTIAL_TOP CASCADE // ============================================================================ else if (cascadeType === 'sequential_top') { panelDelay = index * staggerFrames; const opacity = interpolate( relativeFrame, [panelDelay, panelDelay + DURATION_FAST], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: createEasing(easeOutExpo), } ); const translateY = interpolate( relativeFrame, [panelDelay, panelDelay + DURATION_FAST], [-30, 0], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: createEasing(easeOutExpo), } ); animationProps = { opacity, transform: `translateY(${translateY}px)`, }; } // ============================================================================ // WAVE CASCADE // ============================================================================ else if (cascadeType === 'wave') { const { row, col } = getPanelPosition(index); const wavePosition = row + col; panelDelay = wavePosition * staggerFrames; const opacity = interpolate( relativeFrame, [panelDelay, panelDelay + DURATION_MEDIUM], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: createEasing(easeOutExpo), } ); const translateY = interpolate( relativeFrame, [panelDelay, panelDelay + DURATION_MEDIUM], [40, 0], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: createEasing(easeOutExpo), } ); const scale = interpolate( relativeFrame, [panelDelay, panelDelay + DURATION_MEDIUM], [0.9, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: createEasing(easeOutExpo), } ); animationProps = { opacity, transform: `translateY(${translateY}px) scale(${scale})`, }; } // Default fallback else { panelDelay = index * staggerFrames; const opacity = interpolate( relativeFrame, [panelDelay, panelDelay + DURATION_MEDIUM], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', } ); animationProps = { opacity }; } return ( <div key={index} style={animationProps}> {child} </div> ); })} </AbsoluteFill> ); };

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/chrishayuk/chuk-mcp-remotion'

If you have feedback or need assistance with the MCP directory API, please join our Discord server