Skip to main content
Glama
template.tsx.j212.2 kB
import React from 'react'; import { AbsoluteFill, useCurrentFrame, useVideoConfig, interpolate, spring } from 'remotion'; interface BeforeAfterSliderProps { startFrame: number; durationInFrames: number; beforeImage: string; afterImage: string; beforeLabel: string; afterLabel: string; orientation: 'horizontal' | 'vertical'; sliderPosition: number; animateSlider: boolean; sliderStartPosition: number; sliderEndPosition: number; showLabels: boolean; labelPosition: 'top' | 'bottom' | 'overlay'; handleStyle: 'default' | 'arrow' | 'circle' | 'bar'; width: number; height: number; position: string; borderRadius: number; } export const BeforeAfterSlider: React.FC<BeforeAfterSliderProps> = ({ startFrame, durationInFrames, beforeImage, afterImage, beforeLabel = 'Before', afterLabel = 'After', orientation = 'horizontal', sliderPosition = 50, animateSlider = true, sliderStartPosition = 0, sliderEndPosition = 100, showLabels = true, labelPosition = 'overlay', handleStyle = 'default', width = 1200, height = 800, position = 'center', borderRadius = 12, }) => { const frame = useCurrentFrame(); const { fps } = useVideoConfig(); // Visibility check if (frame < startFrame || frame >= startFrame + durationInFrames) { return null; } const relativeFrame = frame - startFrame; // Position mappings const positionMap: Record<string, { top?: string; left?: string; right?: string; bottom?: string; transform?: string }> = { center: { top: '50%', left: '50%', transform: 'translate(-50%, -50%)', }, 'top-left': { top: '[[ spacing.spacing.xl ]]', left: '[[ spacing.spacing.xl ]]' }, 'top-center': { top: '[[ spacing.spacing.xl ]]', left: '50%', transform: 'translateX(-50%)' }, 'top-right': { top: '[[ spacing.spacing.xl ]]', right: '[[ spacing.spacing.xl ]]' }, 'center-left': { top: '50%', left: '[[ spacing.spacing.xl ]]', transform: 'translateY(-50%)' }, 'center-right': { top: '50%', right: '[[ spacing.spacing.xl ]]', transform: 'translateY(-50%)' }, 'bottom-left': { bottom: '[[ spacing.spacing.xl ]]', left: '[[ spacing.spacing.xl ]]' }, 'bottom-center': { bottom: '[[ spacing.spacing.xl ]]', left: '50%', transform: 'translateX(-50%)' }, 'bottom-right': { bottom: '[[ spacing.spacing.xl ]]', right: '[[ spacing.spacing.xl ]]' }, }; // Entrance animation const entrance = spring({ frame: relativeFrame, fps: fps, config: { damping: [[ motion.default_spring.config.damping ]], stiffness: [[ motion.default_spring.config.stiffness ]], mass: [[ motion.default_spring.config.mass ]], }, }); const opacity = interpolate(relativeFrame, [0, 15], [0, 1], { extrapolateRight: 'clamp' }); const scaleAnimation = interpolate(entrance, [0, 1], [0.95, 1]); // Slider animation const animationStartFrame = 30; const animationDuration = durationInFrames - 60; // Leave time for entrance and exit const currentSliderPosition = animateSlider ? interpolate( relativeFrame, [animationStartFrame, animationStartFrame + animationDuration], [sliderStartPosition, sliderEndPosition], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' } ) : sliderPosition; // Calculate clip values based on orientation const clipValue = orientation === 'horizontal' ? currentSliderPosition : currentSliderPosition; // Render handle based on style const renderHandle = () => { const handleSize = parseInt('[[ spacing.spacing['2xl'] ]]'); const lineThickness = parseInt('[[ spacing.spacing.xxs ]]'); const baseHandleStyle: React.CSSProperties = { position: 'absolute', zIndex: 20, pointerEvents: 'none', }; if (orientation === 'horizontal') { baseHandleStyle.left = `${currentSliderPosition}%`; baseHandleStyle.top = '50%'; baseHandleStyle.transform = 'translate(-50%, -50%)'; } else { baseHandleStyle.top = `${currentSliderPosition}%`; baseHandleStyle.left = '50%'; baseHandleStyle.transform = 'translate(-50%, -50%)'; } switch (handleStyle) { case 'arrow': return ( <div style={baseHandleStyle}> <div style={{ width: handleSize, height: handleSize, background: 'white', borderRadius: '50%', display: 'flex', alignItems: 'center', justifyContent: 'center', boxShadow: `0 4px 12px [[ colors.shadow.medium ]]`, }} > <span style={{ fontSize: parseInt('[[ typography.font_sizes[typography.default_resolution].sm ]]'), color: '[[ colors.text.on_light ]]' }}> {orientation === 'horizontal' ? '⟷' : '⇕'} </span> </div> </div> ); case 'circle': return ( <div style={baseHandleStyle}> <div style={{ width: handleSize, height: handleSize, background: 'white', borderRadius: '50%', border: '[[ spacing.spacing.xxs ]] solid [[ colors.primary[0] ]]', boxShadow: `0 4px 12px [[ colors.shadow.dark ]]`, }} /> </div> ); case 'bar': return ( <div style={baseHandleStyle}> <div style={{ width: orientation === 'horizontal' ? lineThickness : handleSize, height: orientation === 'horizontal' ? handleSize : lineThickness, background: 'white', borderRadius: '[[ spacing.border_radius.sm ]]', boxShadow: `0 4px 12px [[ colors.shadow.dark ]]`, }} /> </div> ); default: // 'default' return ( <div style={baseHandleStyle}> <div style={{ width: handleSize, height: handleSize, background: 'white', borderRadius: '50%', display: 'flex', alignItems: 'center', justifyContent: 'center', boxShadow: `0 4px 12px [[ colors.shadow.dark ]]`, }} > <div style={{ width: lineThickness, height: handleSize * 0.6, background: '[[ colors.primary[0] ]]', borderRadius: '[[ spacing.border_radius.xs ]]', transform: orientation === 'horizontal' ? 'none' : 'rotate(90deg)', }} /> </div> </div> ); } }; // Label styling const labelStyle: React.CSSProperties = { background: '[[ colors.background.dark ]]', color: 'white', padding: '[[ spacing.spacing.xs ]] [[ spacing.spacing.md ]]', borderRadius: '[[ spacing.border_radius.md ]]', fontSize: parseInt('[[ typography.font_sizes[typography.default_resolution].base ]]'), fontFamily: "'[[ "', '".join(typography.body_font.fonts) ]]'", fontWeight: parseInt('[[ typography.font_weights.semibold ]]'), backdropFilter: 'blur(10px)', }; const contentHeight = showLabels && labelPosition !== 'overlay' ? height - 50 : height; const imageContainerTop = showLabels && labelPosition === 'top' ? 50 : 0; return ( <AbsoluteFill style={{ ...positionMap[position], width, height, opacity, transform: `${positionMap[position].transform || ''} scale(${scaleAnimation})`, }} > <div style={{ width: '100%', height: '100%', borderRadius: `${borderRadius}px`, overflow: 'hidden', filter: `drop-shadow(0 10px 40px [[ colors.shadow.dark ]])`, position: 'relative', display: 'flex', flexDirection: 'column', }} > {/* Top labels */} {showLabels && labelPosition === 'top' && ( <div style={{ display: 'flex', justifyContent: 'space-between', padding: '[[ spacing.spacing.md ]]', background: '[[ colors.background.dark ]]', backdropFilter: 'blur(10px)', }} > <div style={labelStyle}>{beforeLabel}</div> <div style={labelStyle}>{afterLabel}</div> </div> )} {/* Image container */} <div style={{ position: 'relative', flex: 1, width: '100%', overflow: 'hidden', }} > {/* Before image (full) */} <div style={{ position: 'absolute', inset: 0, }} > <img src={beforeImage} style={{ width: '100%', height: '100%', objectFit: 'cover', }} /> </div> {/* After image (clipped) */} <div style={{ position: 'absolute', inset: 0, clipPath: orientation === 'horizontal' ? `inset(0 ${100 - clipValue}% 0 0)` : `inset(0 0 ${100 - clipValue}% 0)`, }} > <img src={afterImage} style={{ width: '100%', height: '100%', objectFit: 'cover', }} /> </div> {/* Divider line */} <div style={{ position: 'absolute', ...(orientation === 'horizontal' ? { left: `${currentSliderPosition}%`, top: 0, bottom: 0, width: '[[ spacing.spacing.xxs ]]', transform: 'translateX(-50%)', } : { top: `${currentSliderPosition}%`, left: 0, right: 0, height: '[[ spacing.spacing.xxs ]]', transform: 'translateY(-50%)', }), background: 'white', boxShadow: '0 0 10px [[ colors.background.dark ]]', zIndex: 10, }} /> {/* Handle */} {renderHandle()} {/* Overlay labels */} {showLabels && labelPosition === 'overlay' && ( <> <div style={{ position: 'absolute', ...(orientation === 'horizontal' ? { top: '[[ spacing.spacing.md ]]', left: '[[ spacing.spacing.md ]]' } : { top: '[[ spacing.spacing.md ]]', left: '[[ spacing.spacing.md ]]' }), ...labelStyle, }} > {beforeLabel} </div> <div style={{ position: 'absolute', ...(orientation === 'horizontal' ? { top: '[[ spacing.spacing.md ]]', right: '[[ spacing.spacing.md ]]' } : { bottom: '[[ spacing.spacing.md ]]', right: '[[ spacing.spacing.md ]]' }), ...labelStyle, }} > {afterLabel} </div> </> )} </div> {/* Bottom labels */} {showLabels && labelPosition === 'bottom' && ( <div style={{ display: 'flex', justifyContent: 'space-between', padding: '[[ spacing.spacing.md ]]', background: '[[ colors.background.dark ]]', backdropFilter: 'blur(10px)', }} > <div style={labelStyle}>{beforeLabel}</div> <div style={labelStyle}>{afterLabel}</div> </div> )} </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