R&D brief (references/design-motion-rnd.md): 2024-2026 design/motion trends, animating-anything craft, Iran-aware asset pipeline, masterpiece + platform playbook. New craft skills: motion-design-principles, scene-transitions, kinetic-typography, video-hooks, particles-and-effects, asset-sourcing — grounded in the Remotion stack. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
9.4 KiB
name, description
| name | description |
|---|---|
| scene-transitions | 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: <Sequence>, 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 <Sequence>s back-to-back — give the outgoing scene a tail and the incoming a head that share the window.
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
<Sequence from={0} durationInFrames={120}><SceneA /></Sequence>
<Sequence from={120 - T} durationInFrames={120}><SceneB /></Sequence>
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; <Sequence> 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)
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`)
<AbsoluteFill style={{ clipPath: `inset(0 0 0 ${100 - p}%)` }}><SceneB/></AbsoluteFill>
Soften the edge with a leading gradient strip (a thin accent bar riding p) for a "luminance wipe".
Clip-path circle / shape reveal
const r = interpolate(spring({ frame: f, fps, config: { mass: 0.6, damping: 14 } }),
[0, 1], [0, 150]); // 150% covers corners
<AbsoluteFill style={{ clipPath: `circle(${r}% at 50% 50%)` }}><SceneB/></AbsoluteFill>
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
// outgoing tail
const out = interpolate(f, [0, T], [1, 1.4], { extrapolateRight: "clamp" });
const blurOut = interpolate(f, [0, T], [0, vmin(24)], { extrapolateRight: "clamp" });
<AbsoluteFill style={{ transform: `scale(${out})`, filter: `blur(${blurOut}px)` }}><SceneA/></AbsoluteFill>
// 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)
// inside <ThreeCanvas> — 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
accentwash; Scene B opens from that wash. UsemixHex/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 samev. 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
fromframes to the music beats (remotion-music-picker) so cuts land on downbeats. - Per-aspect: tighten the window on
isWide(reads faster), loosen onisTall. Use the proposedpick(wide,square,tall)helper onLayoutwhen it lands; until then branch onisWide/isSquare/isTall.
Reusable transition components
Build these once in lib/ and reuse across templates — each takes an enter/exit phase and a window:
// 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 <AbsoluteFill style={{ opacity: o }}>{children}</AbsoluteFill>;
};
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
<Sequence>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
interpolatehaveclampon 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.