Files
flatrender/.claude/skills/flat-artist/scene-transitions/SKILL.md
T
soroush.asadi f83d657844
CI/CD / CI · Web (tsc) (push) Successful in 1m19s
CI/CD / Deploy · full stack (push) Failing after 12s
chore(skills+remotion): add flat-artist skill bundle; register 3D templates
- .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>
2026-06-21 19:39:25 +03:30

122 lines
9.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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 (1220f). 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 **1220f**; 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 23f 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`.