--- name: scene-transitions description: How to choreograph transitions BETWEEN scenes (and shots within a scene) in FlatRender Remotion templates — cut, dissolve, wipe, clip-path mask, morph, match-cut, shape transition, camera push, zoom/whip-blur — built from primitives. Use whenever a template has more than one scene/beat, when one element must hand off to the next, or when stitching multi-scene sequences so they feel seamless instead of slideshow-y. Read before sequencing scenes. --- # Scene transitions for Remotion A multi-scene template lives or dies on its *joins*. A hard slideshow of fades reads as "made in a tool"; a transition that carries motion, color, or a shape across the cut reads as "made by a studio". We have **no `@remotion/transitions` package** (not a dependency) and asset CDNs are geo-blocked — so every transition is built from primitives: ``, `interpolate`/`spring`, CSS `clipPath`/`maskImage`, blend modes, and (for 3D) a camera move driven by `useCurrentFrame()`. Everything is a pure function of `frame` — **never** `useFrame`, `Math.random`, `Date.now` (use `rand()` from `lib/anim.ts`). ## The one structural rule: overlap, don't abut A clean transition needs scenes to **overlap** for the transition window (12–20f). Don't place ``s back-to-back — give the outgoing scene a tail and the incoming a head that share the window. ```tsx import { Sequence, useVideoConfig } from "remotion"; const { fps } = useVideoConfig(); const sec = (s: number) => Math.round(s * fps); // never hardcode 30 const T = sec(0.5); // transition window // Scene A holds frames 0..120, Scene B starts at 120-T so they cross-fade ``` Inside each scene, derive a local progress from `useCurrentFrame()` (already 0-based inside a `Sequence`) for its *in* and *out* phases. ## Transition catalog — what to build, when, and how | Transition | فارسی | Feel / when | Build (primitive) | |---|---|---|---| | **Cut** | برش | Hard, energetic, beat-synced, brutalist/anti-design | No overlap; `` ends, next begins. Snap a 1f flash or shake for punch. | | **Dissolve / crossfade** | محو | Calm, elegant, photo decks, luxury | Outgoing `opacity 1→0`, incoming `0→1` over the window, `clamp` both. | | **Wipe** | پاک‌کن | Directional energy, news/promo | `clipPath: inset()` on the incoming layer sweeps a hard edge (see below). | | **Clip-path mask reveal** | ماسک | Premium reveals, shape brand moment | Animate a `circle()`/`polygon()` `clipPath` open over the new scene. | | **Morph** | ریخت‌گردانی | Liquid/organic, kinetic trend | Animate SVG `path d` (`flubber`-style) or `feGaussianBlur`+`feColorMatrix` gooey merge. | | **Match-cut** | برش تطبیقی | Storytelling, "made by a studio" | A shape/element at the SAME position+size in both scenes; cut while it's identical. | | **Shape transition** | گذار شکلی | Brand mark grows into scene | A circle/blob scales up to fill frame (color = `accent`), then the new scene is revealed inside it. | | **Camera push / dolly** | حرکت دوربین (۳بعدی) | Cinematic, 3D logo/product | Move the R3F camera `position.z` / target between two staged setups by `frame`. | | **Zoom / whip blur** | زوم/تار حرکتی | Fast, hype, music, TikTok | Scale up + `filter: blur()` on out, scale-down + blur-out on in; peak blur ON the cut. | ### Wipe (clip-path inset) ```tsx import { useCurrentFrame, interpolate, Easing } from "remotion"; const f = useCurrentFrame(); // local frame in the incoming Sequence const p = interpolate(f, [0, sec(0.45)], [0, 100], { extrapolateLeft: "clamp", extrapolateRight: "clamp", easing: Easing.bezier(0.16, 1, 0.3, 1), }); // RTL-aware: wipe in from the right for Persian (mirror the direction in `en`) ``` Soften the edge with a leading gradient strip (a thin `accent` bar riding `p`) for a "luminance wipe". ### Clip-path circle / shape reveal ```tsx const r = interpolate(spring({ frame: f, fps, config: { mass: 0.6, damping: 14 } }), [0, 1], [0, 150]); // 150% covers corners ``` For a brand shape transition: render a full-frame circle filled with `colorSchema.accent` scaling up over Scene A, then swap to Scene B *masked by the same circle* — the brand color carries the cut. ### Zoom / whip-blur ```tsx // outgoing tail const out = interpolate(f, [0, T], [1, 1.4], { extrapolateRight: "clamp" }); const blurOut = interpolate(f, [0, T], [0, vmin(24)], { extrapolateRight: "clamp" }); // incoming head (local frame): scale 1.25→1, blur 24→0 — peak blur of BOTH meets on the cut ``` `vmin` comes from `useLayout()` (`lib/aspect.ts`) so the blur reads the same in all three aspects. ### Camera push (3D, @remotion/three) ```tsx // inside — drive the camera off frame, NOT useFrame const z = interpolate(spring({ frame: f, fps, config: { mass: 2.5, damping: 26 } }), [0, 1], [7, 3.2]); // dolly in, heavy = weight useThree(({ camera }) => { camera.position.z = z; camera.updateProjectionMatrix(); }); ``` Use `StudioEnv/StudioLights/StudioFloor/StudioEffects` from `lib/three-kit.tsx`; let DOF + bloom + vignette sell the move. Camera moves use `ease-in-out`/heavy spring; never linear (linear is only for continuous orbit/rotation). ## Match-cut & seamless choreography (the studio-grade joins) The eye forgives a cut if **something continues across it**. Carry one of: - **Position+scale** — a circle bottom-left in Scene A is a circle bottom-left, same size, in Scene B. Cut while identical. (Classic match-cut.) - **Color** — Scene A ends on a full-frame `accent` wash; Scene B opens from that wash. Use `mixHex`/`hexToRgba` (`lib/anim.ts`) so it's palette-driven. - **Motion vector** — text exits stage-left at speed `v`; the next element enters from stage-right at the same `v`. Momentum reads as continuity. - **A mask** — the shape that wiped scene A out is the shape scene B wipes in with. For a full template: write a **beat list first** (logo in → tagline → 3 features cascade → CTA → out), assign one transition per join, and make adjacent joins *differ* (don't dissolve every cut) but **rhyme** (reuse the brand shape/color). Vary cut length and build to the hero moment — pacing is a transition too. ## Timing & easing (the difference between pro and slideshow) - **Window:** scene transition **12–20f**; whip/cut feels best at the short end, dissolve/camera at the long end. - **Entrances ease-out** (`Easing.out(Easing.quint)` / `Easing.bezier(0.16,1,0.3,1)`); **exits ease-in and always SHARPER than the entrance** — scenes leave faster than they arrive. - **A→B on-screen / camera = ease-in-out.** **Linear ONLY** for continuous rotation/marquee. - Snap transition `from` frames to the **music beats** (`remotion-music-picker`) so cuts land on downbeats. - Per-aspect: tighten the window on `isWide` (reads faster), loosen on `isTall`. Use the proposed `pick(wide,square,tall)` helper on `Layout` when it lands; until then branch on `isWide/isSquare/isTall`. ## Reusable transition components Build these once in `lib/` and reuse across templates — each takes an `enter`/`exit` phase and a window: ```tsx // CrossFade.tsx — wrap any scene; computes its own in/out from frame + duration export const Dissolve: React.FC<{ children: React.ReactNode; win: number }> = ({ children, win }) => { const f = useCurrentFrame(); const { durationInFrames } = useVideoConfig(); // length of THIS Sequence const o = Math.min( interpolate(f, [0, win], [0, 1], { extrapolateRight: "clamp" }), interpolate(f, [durationInFrames - win, durationInFrames], [1, 0], { extrapolateLeft: "clamp" }), ); return {children}; }; ``` Make sibling wrappers `Wipe`, `CircleReveal`, `WhipZoom`, `ShapeWipe` with the same `(children, win, dir)` contract so a template can swap transitions by changing one wrapper. Keep the SFX hook in mind: a whoosh 2–3f before the cut + an impact ON it (`remotion-sound-effects`). ## Pre-ship transition checklist - [ ] No back-to-back ``s where a join should be smooth — scenes **overlap** the window. - [ ] Every join has a chosen transition with an *intent* (energy/calm/brand), not a default fade everywhere. - [ ] At least one join **carries** position, color, motion, or a mask across the cut (not all isolated fades). - [ ] All `interpolate` have `clamp` on both ends (the #1 drift bug). - [ ] Exits are sharper than entrances; nothing linear except continuous motion. - [ ] Cuts snapped to beats; whoosh-in + impact-on-cut wired. - [ ] Verified in 16:9 / 1:1 / 9:16 — wipe direction & blur amount read the same (`vmin`, not px); Persian RTL wipes from the right. - [ ] Colors via `colorSchema` (`mixHex`/`hexToRgba`), never hardcoded; deterministic (re-render twice → identical). Related: `remotion-template-composition`, `remotion-aspect-ratios`, `remotion-design-styles`, `remotion-sound-effects`, `remotion-music-picker`, `remotion-character-design`, `remotion-svg-colors`, `persian-fonts`, `flatrender-template-seo`.