0.5
+ ? `rgba(255, 0, 100, ${glitchIntensity * 0.5})`
+ : `rgba(0, 255, 200, ${glitchIntensity * 0.5})`,
+ mixBlendMode: 'screen',
+ }}
+ />
+ );
+ })}
+
+ )}
+
+ {/* Noise texture overlay */}
+ {glitchIntensity > 0.2 && (
+
+ )}
+
+ );
+};
+
+export const glitch = (
+ props: GlitchProps = {}
+): TransitionPresentation
=> {
+ return { component: GlitchPresentation, props };
+};
diff --git a/showcase/transitions/src/presentations/light-leak.tsx b/showcase/transitions/src/presentations/light-leak.tsx
new file mode 100644
index 0000000..df4f207
--- /dev/null
+++ b/showcase/transitions/src/presentations/light-leak.tsx
@@ -0,0 +1,216 @@
+/**
+ * Light Leak Transition
+ *
+ * Cinematic light leak/lens flare effect that washes over the scene.
+ * Creates warmth, nostalgia, and organic film-like quality.
+ *
+ * Best for: Emotional moments, celebrations, warm transitions, film aesthetic
+ */
+import type {
+ TransitionPresentation,
+ TransitionPresentationComponentProps,
+} from '@remotion/transitions';
+import React, { useMemo } from 'react';
+import { AbsoluteFill, interpolate, random } from 'remotion';
+
+export type LightLeakProps = {
+ /** Color temperature: 'warm' (orange/gold), 'cool' (blue/cyan), 'rainbow'. Default: 'warm' */
+ temperature?: 'warm' | 'cool' | 'rainbow';
+ /** Direction the light enters from. Default: 'right' */
+ direction?: 'left' | 'right' | 'top' | 'bottom' | 'center';
+ /** Intensity of the overexposure. Default: 0.8 */
+ intensity?: number;
+ /** Include lens flare artifacts. Default: true */
+ flareArtifacts?: boolean;
+};
+
+const LightLeakPresentation: React.FC<
+ TransitionPresentationComponentProps
+> = ({ children, presentationDirection, presentationProgress, passedProps }) => {
+ const {
+ temperature = 'warm',
+ direction = 'right',
+ intensity = 0.8,
+ flareArtifacts = true,
+ } = passedProps;
+
+ const progress = presentationDirection === 'exiting'
+ ? 1 - presentationProgress
+ : presentationProgress;
+
+ // Light leak sweeps across the scene
+ const leakProgress = useMemo(() => {
+ return interpolate(progress, [0, 0.6, 1], [0, 1, 0.2], {
+ extrapolateLeft: 'clamp',
+ extrapolateRight: 'clamp',
+ });
+ }, [progress]);
+
+ // Scene exposure (brightens during transition)
+ const exposure = useMemo(() => {
+ return interpolate(progress, [0, 0.4, 0.6, 1], [1, 1.3, 1.3, 1], {
+ extrapolateLeft: 'clamp',
+ extrapolateRight: 'clamp',
+ });
+ }, [progress]);
+
+ // Opacity for entering/exiting
+ const opacity = presentationDirection === 'exiting'
+ ? interpolate(progress, [0, 0.6], [1, 0], { extrapolateRight: 'clamp' })
+ : interpolate(progress, [0.4, 1], [0, 1], { extrapolateLeft: 'clamp' });
+
+ // Color gradients based on temperature
+ const getGradientColors = () => {
+ switch (temperature) {
+ case 'warm':
+ return {
+ primary: 'rgba(255, 180, 80, 0.9)',
+ secondary: 'rgba(255, 120, 50, 0.7)',
+ tertiary: 'rgba(255, 220, 150, 0.5)',
+ };
+ case 'cool':
+ return {
+ primary: 'rgba(100, 180, 255, 0.9)',
+ secondary: 'rgba(150, 220, 255, 0.7)',
+ tertiary: 'rgba(200, 240, 255, 0.5)',
+ };
+ case 'rainbow':
+ return {
+ primary: 'rgba(255, 100, 150, 0.8)',
+ secondary: 'rgba(255, 200, 100, 0.6)',
+ tertiary: 'rgba(100, 200, 255, 0.5)',
+ };
+ }
+ };
+
+ const colors = getGradientColors();
+
+ // Calculate gradient position based on direction
+ const getGradientPosition = () => {
+ const pos = leakProgress * 150 - 50; // -50 to 100
+ switch (direction) {
+ case 'left':
+ return `linear-gradient(90deg, ${colors.primary} ${pos}%, ${colors.secondary} ${pos + 20}%, ${colors.tertiary} ${pos + 40}%, transparent ${pos + 60}%)`;
+ case 'right':
+ return `linear-gradient(270deg, ${colors.primary} ${pos}%, ${colors.secondary} ${pos + 20}%, ${colors.tertiary} ${pos + 40}%, transparent ${pos + 60}%)`;
+ case 'top':
+ return `linear-gradient(180deg, ${colors.primary} ${pos}%, ${colors.secondary} ${pos + 20}%, ${colors.tertiary} ${pos + 40}%, transparent ${pos + 60}%)`;
+ case 'bottom':
+ return `linear-gradient(0deg, ${colors.primary} ${pos}%, ${colors.secondary} ${pos + 20}%, ${colors.tertiary} ${pos + 40}%, transparent ${pos + 60}%)`;
+ case 'center':
+ return `radial-gradient(ellipse at center, ${colors.primary} ${pos * 0.5}%, ${colors.secondary} ${pos * 0.7}%, ${colors.tertiary} ${pos}%, transparent ${pos + 30}%)`;
+ }
+ };
+
+ // Flare artifact positions (deterministic)
+ const flarePositions = useMemo(() => {
+ return Array.from({ length: 5 }, (_, i) => ({
+ x: random(`flare-x-${i}`) * 100,
+ y: random(`flare-y-${i}`) * 100,
+ size: 20 + random(`flare-size-${i}`) * 60,
+ delay: random(`flare-delay-${i}`) * 0.3,
+ }));
+ }, []);
+
+ const containerStyle: React.CSSProperties = useMemo(() => ({
+ width: '100%',
+ height: '100%',
+ }), []);
+
+ return (
+
+ {/* Main content with exposure adjustment */}
+
+ {children}
+
+
+ {/* Light leak gradient overlay */}
+
+
+ {/* Soft glow overlay */}
+
+
+ {/* Lens flare artifacts */}
+ {flareArtifacts && leakProgress > 0.2 && (
+
+ {flarePositions.map((flare, i) => {
+ const flareOpacity = interpolate(
+ progress,
+ [flare.delay, flare.delay + 0.3, 0.7, 1],
+ [0, 0.6, 0.6, 0],
+ { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' }
+ );
+ return (
+
+ );
+ })}
+
+ {/* Hexagonal flare (anamorphic style) */}
+
+
+ )}
+
+ {/* Film grain for authenticity */}
+
+
+ );
+};
+
+export const lightLeak = (
+ props: LightLeakProps = {}
+): TransitionPresentation => {
+ return { component: LightLeakPresentation, props };
+};
diff --git a/showcase/transitions/src/presentations/pixelate.tsx b/showcase/transitions/src/presentations/pixelate.tsx
new file mode 100644
index 0000000..40fb986
--- /dev/null
+++ b/showcase/transitions/src/presentations/pixelate.tsx
@@ -0,0 +1,191 @@
+/**
+ * Pixelate Transition
+ *
+ * Digital pixelation/mosaic effect that dissolves the scene into blocks.
+ * Creates a retro gaming or digital artifact aesthetic.
+ *
+ * Best for: Tech themes, retro/gaming content, digital transformations
+ */
+import type {
+ TransitionPresentation,
+ TransitionPresentationComponentProps,
+} from '@remotion/transitions';
+import React, { useMemo, useState } from 'react';
+import { AbsoluteFill, interpolate, random } from 'remotion';
+
+export type PixelateProps = {
+ /** Maximum block size at peak pixelation. Default: 40 */
+ maxBlockSize?: number;
+ /** Include color posterization with pixelation. Default: true */
+ posterize?: boolean;
+ /** Pixelation pattern: 'uniform' or 'random'. Default: 'uniform' */
+ pattern?: 'uniform' | 'random';
+};
+
+const PixelatePresentation: React.FC<
+ TransitionPresentationComponentProps
+> = ({ children, presentationDirection, presentationProgress, passedProps }) => {
+ const {
+ maxBlockSize = 40,
+ posterize = true,
+ pattern = 'uniform',
+ } = passedProps;
+
+ const [filterId] = useState(() => `pixelate-${String(random(null)).slice(2, 10)}`);
+
+ const progress = presentationDirection === 'exiting'
+ ? 1 - presentationProgress
+ : presentationProgress;
+
+ // Pixelation intensity peaks in the middle
+ const pixelIntensity = useMemo(() => {
+ return interpolate(progress, [0, 0.5, 1], [0, 1, 0], {
+ extrapolateLeft: 'clamp',
+ extrapolateRight: 'clamp',
+ });
+ }, [progress]);
+
+ // Block size calculation (starts small, gets big, then small again)
+ const blockSize = useMemo(() => {
+ const minSize = 1;
+ return Math.max(minSize, Math.round(maxBlockSize * pixelIntensity));
+ }, [maxBlockSize, pixelIntensity]);
+
+ // Opacity for entering/exiting
+ const opacity = presentationDirection === 'exiting'
+ ? interpolate(progress, [0, 0.5], [1, 0], { extrapolateRight: 'clamp' })
+ : interpolate(progress, [0.5, 1], [0, 1], { extrapolateLeft: 'clamp' });
+
+ // Posterization reduces color depth
+ const posterizeLevels = posterize
+ ? Math.max(2, Math.round(interpolate(pixelIntensity, [0, 1], [256, 4])))
+ : 256;
+
+ const containerStyle: React.CSSProperties = useMemo(() => ({
+ width: '100%',
+ height: '100%',
+ imageRendering: blockSize > 2 ? 'pixelated' : 'auto',
+ }), [blockSize]);
+
+ // For pattern === 'random', we add noise-based distortion
+ const shouldApplyEffect = pixelIntensity > 0.05;
+
+ return (
+
+
+ {children}
+
+
+ {/* Scanline overlay for CRT effect */}
+ {pixelIntensity > 0.3 && (
+
+ )}
+
+ {/* Color banding effect */}
+ {posterize && pixelIntensity > 0.2 && (
+
+ )}
+
+ {/* SVG filter for pixelation effect */}
+
+
+ );
+};
+
+// Generate posterization lookup table
+function generatePosterizeTable(levels: number): string {
+ const step = 1 / (levels - 1);
+ return Array.from({ length: levels }, (_, i) => (i * step).toFixed(3)).join(' ');
+}
+
+export const pixelate = (
+ props: PixelateProps = {}
+): TransitionPresentation => {
+ return { component: PixelatePresentation, props };
+};
diff --git a/showcase/transitions/src/presentations/rgb-split.tsx b/showcase/transitions/src/presentations/rgb-split.tsx
new file mode 100644
index 0000000..5c5a825
--- /dev/null
+++ b/showcase/transitions/src/presentations/rgb-split.tsx
@@ -0,0 +1,184 @@
+/**
+ * RGB Split Transition
+ *
+ * Chromatic aberration effect that separates RGB channels
+ * with directional displacement. Creates a modern tech aesthetic
+ * reminiscent of CRT displays and retro-futuristic visuals.
+ *
+ * Best for: Tech products, modern branding, energetic transitions
+ */
+import type {
+ TransitionPresentation,
+ TransitionPresentationComponentProps,
+} from '@remotion/transitions';
+import React, { useMemo } from 'react';
+import { AbsoluteFill, interpolate } from 'remotion';
+
+export type RgbSplitProps = {
+ /** Direction of the split: 'horizontal' | 'vertical' | 'diagonal'. Default: 'horizontal' */
+ direction?: 'horizontal' | 'vertical' | 'diagonal';
+ /** Maximum pixel displacement. Default: 30 */
+ displacement?: number;
+ /** Include subtle blur on channels. Default: true */
+ channelBlur?: boolean;
+};
+
+const RgbSplitPresentation: React.FC<
+ TransitionPresentationComponentProps
+> = ({ children, presentationDirection, presentationProgress, passedProps }) => {
+ const {
+ direction = 'horizontal',
+ displacement = 30,
+ channelBlur = true,
+ } = passedProps;
+
+ const progress = presentationDirection === 'exiting'
+ ? 1 - presentationProgress
+ : presentationProgress;
+
+ // Split intensity peaks in the middle
+ const splitIntensity = useMemo(() => {
+ return interpolate(progress, [0, 0.5, 1], [0, 1, 0], {
+ extrapolateLeft: 'clamp',
+ extrapolateRight: 'clamp',
+ });
+ }, [progress]);
+
+ // Calculate channel offsets based on direction
+ const getChannelOffset = (channel: 'red' | 'green' | 'blue') => {
+ const multiplier = channel === 'red' ? -1 : channel === 'blue' ? 1 : 0;
+ const offset = displacement * splitIntensity * multiplier;
+
+ switch (direction) {
+ case 'horizontal':
+ return { x: offset, y: 0 };
+ case 'vertical':
+ return { x: 0, y: offset };
+ case 'diagonal':
+ return { x: offset * 0.7, y: offset * 0.7 };
+ }
+ };
+
+ const redOffset = getChannelOffset('red');
+ const blueOffset = getChannelOffset('blue');
+
+ // Opacity for entering/exiting
+ const opacity = presentationDirection === 'exiting'
+ ? interpolate(progress, [0, 0.4], [1, 0], { extrapolateRight: 'clamp' })
+ : interpolate(progress, [0.6, 1], [0, 1], { extrapolateLeft: 'clamp' });
+
+ // Blur amount for channels (subtle motion blur effect)
+ const blurAmount = channelBlur ? splitIntensity * 2 : 0;
+
+ const containerStyle: React.CSSProperties = useMemo(() => ({
+ width: '100%',
+ height: '100%',
+ position: 'relative',
+ }), []);
+
+ // Only show RGB separation when there's actual displacement
+ const showSplit = splitIntensity > 0.05;
+
+ return (
+
+ {showSplit ? (
+ <>
+ {/* Red channel */}
+ 0 ? `blur(${blurAmount}px)` : undefined,
+ mixBlendMode: 'screen',
+ }}
+ >
+
+ {children}
+
+
+
+ {/* Green channel (center, no offset) */}
+
+
+ {children}
+
+
+
+ {/* Blue channel */}
+ 0 ? `blur(${blurAmount}px)` : undefined,
+ mixBlendMode: 'screen',
+ }}
+ >
+
+ {children}
+
+
+ >
+ ) : (
+
+ {children}
+
+ )}
+
+ {/* SVG filters for channel isolation */}
+
+
+ );
+};
+
+export const rgbSplit = (
+ props: RgbSplitProps = {}
+): TransitionPresentation => {
+ return { component: RgbSplitPresentation, props };
+};
diff --git a/showcase/transitions/src/presentations/zoom-blur.tsx b/showcase/transitions/src/presentations/zoom-blur.tsx
new file mode 100644
index 0000000..6cbfb99
--- /dev/null
+++ b/showcase/transitions/src/presentations/zoom-blur.tsx
@@ -0,0 +1,137 @@
+/**
+ * Zoom Blur Transition
+ *
+ * Radial motion blur combined with scale for high-energy transitions.
+ * Creates a sense of speed, impact, and forward momentum.
+ *
+ * Best for: CTAs, reveals, action sequences, energetic moments
+ */
+import type {
+ TransitionPresentation,
+ TransitionPresentationComponentProps,
+} from '@remotion/transitions';
+import React, { useMemo } from 'react';
+import { AbsoluteFill, interpolate } from 'remotion';
+
+export type ZoomBlurProps = {
+ /** Direction: 'in' zooms toward viewer, 'out' zooms away. Default: 'in' */
+ direction?: 'in' | 'out';
+ /** Maximum blur amount in pixels. Default: 20 */
+ blurAmount?: number;
+ /** Scale multiplier at peak. Default: 1.15 */
+ scaleAmount?: number;
+ /** Origin point for zoom. Default: 'center' */
+ origin?: 'center' | 'top' | 'bottom' | 'left' | 'right';
+};
+
+const ZoomBlurPresentation: React.FC<
+ TransitionPresentationComponentProps
+> = ({ children, presentationDirection, presentationProgress, passedProps }) => {
+ const {
+ direction = 'in',
+ blurAmount = 20,
+ scaleAmount = 1.15,
+ origin = 'center',
+ } = passedProps;
+
+ const progress = presentationDirection === 'exiting'
+ ? 1 - presentationProgress
+ : presentationProgress;
+
+ // Effect intensity peaks in the middle then settles
+ const effectIntensity = useMemo(() => {
+ return interpolate(progress, [0, 0.4, 1], [0, 1, 0], {
+ extrapolateLeft: 'clamp',
+ extrapolateRight: 'clamp',
+ });
+ }, [progress]);
+
+ // Scale animation
+ const scale = useMemo(() => {
+ if (direction === 'in') {
+ // Start small, zoom in
+ return interpolate(
+ progress,
+ [0, 0.5, 1],
+ [1 / scaleAmount, scaleAmount, 1],
+ { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' }
+ );
+ } else {
+ // Start big, zoom out
+ return interpolate(
+ progress,
+ [0, 0.5, 1],
+ [scaleAmount, 1 / scaleAmount, 1],
+ { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' }
+ );
+ }
+ }, [progress, direction, scaleAmount]);
+
+ // Blur tracks with scale movement
+ const blur = blurAmount * effectIntensity;
+
+ // Opacity
+ const opacity = presentationDirection === 'exiting'
+ ? interpolate(progress, [0, 0.5], [1, 0], { extrapolateRight: 'clamp' })
+ : interpolate(progress, [0.5, 1], [0, 1], { extrapolateLeft: 'clamp' });
+
+ // Transform origin based on setting
+ const transformOrigin = useMemo(() => {
+ switch (origin) {
+ case 'top': return 'center top';
+ case 'bottom': return 'center bottom';
+ case 'left': return 'left center';
+ case 'right': return 'right center';
+ default: return 'center center';
+ }
+ }, [origin]);
+
+ const containerStyle: React.CSSProperties = useMemo(() => ({
+ width: '100%',
+ height: '100%',
+ display: 'flex',
+ justifyContent: 'center',
+ alignItems: 'center',
+ overflow: 'hidden',
+ }), []);
+
+ const contentStyle: React.CSSProperties = useMemo(() => ({
+ width: '100%',
+ height: '100%',
+ transform: `scale(${scale})`,
+ transformOrigin,
+ filter: blur > 0.5 ? `blur(${blur}px)` : undefined,
+ opacity,
+ }), [scale, transformOrigin, blur, opacity]);
+
+ return (
+
+
+ {children}
+
+
+ {/* Radial light streak overlay for extra energy */}
+ {effectIntensity > 0.3 && (
+
+ )}
+
+ );
+};
+
+export const zoomBlur = (
+ props: ZoomBlurProps = {}
+): TransitionPresentation => {
+ return { component: ZoomBlurPresentation, props };
+};
diff --git a/showcase/transitions/tsconfig.json b/showcase/transitions/tsconfig.json
new file mode 100644
index 0000000..e88ff13
--- /dev/null
+++ b/showcase/transitions/tsconfig.json
@@ -0,0 +1,14 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "jsx": "react-jsx",
+ "strict": true,
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+ "forceConsistentCasingInFileNames": true,
+ "noEmit": true
+ },
+ "include": ["src/**/*", "../../lib/**/*"]
+}
diff --git a/templates/product-demo/package.json b/templates/product-demo/package.json
index 04f9912..9dcba43 100644
--- a/templates/product-demo/package.json
+++ b/templates/product-demo/package.json
@@ -9,6 +9,9 @@
},
"dependencies": {
"@remotion/cli": "^4.0.0",
+ "@remotion/paths": "^4.0.0",
+ "@remotion/shapes": "^4.0.0",
+ "@remotion/transitions": "^4.0.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"remotion": "^4.0.0"
diff --git a/templates/sprint-review/package.json b/templates/sprint-review/package.json
index 86c9ba4..7d2b6d7 100644
--- a/templates/sprint-review/package.json
+++ b/templates/sprint-review/package.json
@@ -11,6 +11,9 @@
},
"dependencies": {
"@remotion/cli": "^4.0.0",
+ "@remotion/paths": "^4.0.0",
+ "@remotion/shapes": "^4.0.0",
+ "@remotion/transitions": "^4.0.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"remotion": "^4.0.0"