f83d657844
- .claude/skills/flat-artist: the bundled FlatRender template-creation suite (orchestrator + 16 sub-skills + design/motion R&D), mirrors the Gitea AISkills repo. - services/remotion Root.tsx/templates.tsx: register the 3D templates + Three3DTest. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
122 lines
9.6 KiB
Markdown
122 lines
9.6 KiB
Markdown
---
|
||
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: `<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.
|
||
|
||
```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
|
||
<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)
|
||
```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`)
|
||
<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
|
||
```tsx
|
||
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
|
||
```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" });
|
||
<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)
|
||
```tsx
|
||
// 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 `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/SKILL.md`) 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 <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/SKILL.md`).
|
||
|
||
## 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 `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/SKILL.md`, `../remotion-aspect-ratios/SKILL.md`, `../remotion-design-styles/SKILL.md`, `../remotion-sound-effects/SKILL.md`, `../remotion-music-picker/SKILL.md`, `../remotion-character-design/SKILL.md`, `../remotion-svg-colors/SKILL.md`, `../persian-fonts/SKILL.md`, `../flatrender-template-seo/SKILL.md`.
|