Files
Soroush Asadi 4ffbcac9ee refactor: bundle the whole template suite under flat-artist/ + fix references
flat-artist is now the single container: all 16 template skills + the R&D
references/ moved inside flat-artist/. Cross-references updated — the orchestrator
points to bundled `<name>/SKILL.md`, sub-skills point to `../<name>/SKILL.md`,
and the R&D report path is relative. README catalog updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 19:31:53 +03:30

9.6 KiB
Raw Permalink Blame History

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 framenever 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.

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 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:

// 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.