// whip-pan-transition | whip-pan transition
// Two pages pan across the frame together, hiding the cut point: the outgoing page whips
// left while the incoming page whips in from the right; during the overlap, speed-stretch
// scaleX(1.12) and motion blur rise and fall with velocity.
// Beat (120f @30fps): 0–20 outgoing hold → 20–56 whip (Easing.bezier(0.7,0,0.2,1))
// → 56–120 incoming page settles into true stillness for 64f ≥ 40f. Frame-determined, no randomness.
//
// Uses TransitionSeries from @remotion/transitions to demo a full-page whip —
// the cut hides inside the whip, more directional than a white flash, suited to high-tempo segments.
import React from 'react';
import {
AbsoluteFill,
Easing,
interpolate,
useCurrentFrame,
useVideoConfig,
} from 'remotion';
import { FakeDashboard } from '../../_fixtures/Fixtures';
// Whip: a page translates [-travel, 0] (outgoing) or [0, travel] (incoming) within [start,end].
// A sine velocity curve drives stretch and blur — velocity peaks in the middle, still at both ends.
const useWhip = (frame: number, start: number, end: number) => {
const p = interpolate(frame, [start, end], [0, 1], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
easing: Easing.bezier(0.7, 0, 0.2, 1),
});
const velocity = Math.sin(Math.PI * p); // 0→1→0
return { p, velocity };
};
// Outgoing page: whips left from its position
const ExitPage: React.FC<{ frame: number; start: number; end: number }> = ({
frame,
start,
end,
}) => {
const { p, velocity } = useWhip(frame, start, end);
const x = interpolate(p, [0, 1], [0, -110], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
});
const stretch = 1 + velocity * 0.12;
const blur = velocity * 24;
return (
0 ? `blur(${blur}px)` : undefined,
}}
>
);
};
// Incoming page: whips in from the right
const EnterPage: React.FC<{ frame: number; start: number; end: number }> = ({
frame,
start,
end,
}) => {
const { p, velocity } = useWhip(frame, start, end);
const x = interpolate(p, [0, 1], [110, 0], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
});
const stretch = 1 + velocity * 0.12;
const blur = velocity * 24;
return (
0 ? `blur(${blur}px)` : undefined,
}}
>
);
};
export const WhipPanTransition: React.FC = () => {
const frame = useCurrentFrame();
const { durationInFrames } = useVideoConfig();
const HOLD = 20; // outgoing hold
const SWEEP = 36; // whip segment
const WHIP_START = HOLD;
const WHIP_END = HOLD + SWEEP;
// After the whip: page B renders straight (masks removed; leftover transforms would break true stillness)
if (frame >= WHIP_END) {
return (
);
}
// Outgoing hold: pure page A
if (frame < WHIP_START) {
return (
);
}
// Whip segment: both pages in frame
return (
);
};