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>
121 lines
9.5 KiB
Markdown
121 lines
9.5 KiB
Markdown
---
|
||
name: particles-and-effects
|
||
description: How to add production-value FX — confetti, sparkles, bokeh, light leaks, dust, smoke, glow, lens flare, film grain, chromatic aberration, vignette, camera shake — to FlatRender Remotion templates, in both 2D (SVG/CSS) and 3D (@remotion/three). Use when a template needs atmosphere, finishing texture, particle systems, or a celebratory/cinematic hit. Every effect is a deterministic function of useCurrentFrame() — never Math.random.
|
||
---
|
||
|
||
# Particles & effects for Remotion
|
||
|
||
Project: `services/remotion/` (Remotion 4 + `@remotion/three`, R3F v9, `gl="angle"`). Effects are the **8th finishing layer** — the thing that separates "made in a tool" from "made by a studio." A flat, ungrainy, perfectly-locked frame reads as AI/template. Imperfect-by-design wins.
|
||
|
||
## The one non-negotiable rule
|
||
Render is headless Chrome sampling frames out of order, in parallel. **Every particle position, every grain offset, every flicker MUST derive from `useCurrentFrame()`.** Never `Math.random()`, `Date.now()`, `useFrame` (R3F), `useState`, or `useEffect` motion. Use `rand(seed)` from `src/lib/anim.ts` for stable per-index pseudo-randomness, and `rand(i + frame)`-style offsets when you want it to *move*. Re-render twice → identical bytes, or it's wrong.
|
||
|
||
Helpers you build on:
|
||
- `anim.ts` — `rand(i)` (deterministic 0..1), `hexToRgba(hex,a)`, `mixHex(a,b,t)`.
|
||
- `aspect.ts` — `useLayout()` → `isWide/isSquare/isTall`, `vmin(n)`, `unit`, and `pick(wide,square,tall)`. **Scale particle COUNT and SIZE per aspect** — a tall 9:16 needs fewer, bigger sparkles than a wide 16:9.
|
||
- `branding.ts` — `colorSchema` props are `accentColor / secondaryColor / backgroundColor / textColor`. FX color comes from these so the studio recolors them.
|
||
- `three-kit.tsx` — `StudioEnv`, `StudioLights`, `StudioFloor`, `StudioEffects` (bloom+DOF+vignette), `Confetti3D`.
|
||
|
||
## 2D vs 3D — pick per effect
|
||
- **2D (SVG/CSS)** is the default: cheap, crisp, no WebGL. Confetti, sparkles, grain, light leaks, vignette, aberration, camera shake — all better/cheaper in 2D as an `<AbsoluteFill>` overlay, even on top of a 3D scene.
|
||
- **3D (@remotion/three)** when the effect must respond to scene lighting/depth: volumetric bloom, real bokeh/DOF, `emissive` glow that bloom picks up, 3D confetti with perspective. Let `StudioEffects` do bloom/DOF/vignette in ONE component — don't re-roll them.
|
||
- Persian text NEVER goes in 3D — keep it as a 2D overlay above `<ThreeCanvas>`.
|
||
|
||
## Effect → recipe table
|
||
|
||
| Effect | Layer | Core technique | Determinism |
|
||
|---|---|---|---|
|
||
| **Confetti (2D)** | overlay | N `<rect>`/`<path>`, `rand(i)` for x/rot/color; `y` = `(frame*speed + rand(i)*span) % span` | `rand(i)` seed |
|
||
| **Confetti (3D)** | scene | reuse `Confetti3D` from three-kit | built-in |
|
||
| **Sparkles / shine** | overlay | 4-point star SVG, twinkle `opacity = abs(sin((frame+rand(i)*60)/12))`, scale pulse | `rand(i)` |
|
||
| **Bokeh** | bg | big blurred radial-gradient circles drifting on `sin(frame/period)`, low opacity, `mix-blend:screen` | per-circle seed |
|
||
| **Light leaks** | overlay | warm radial/linear gradient sweeping across via `interpolate(frame,...)` translate, `mix-blend:screen` | frame |
|
||
| **Dust motes** | overlay | tiny dim dots, slow upward drift + lateral `sin`, `rand` size/speed | `rand(i)` |
|
||
| **Smoke / fog** | bg/3D | 2D: layered blurred blobs drifting+scaling; 3D: stacked transparent planes | frame |
|
||
| **Glow** | any | 2D `filter:drop-shadow(0 0 Npx accent)` / `textShadow`; 3D `emissive`+`emissiveIntensity`, `toneMapped={false}`, let bloom bloom it | static |
|
||
| **Lens flare** | overlay | bright core + chromatic ring sprites along a line from a light point, opacity by angle/frame | frame |
|
||
| **Film grain** | top | SVG `feTurbulence` with per-frame `seed`, `mix-blend:overlay`, low opacity — MUST animate or it looks frozen | frame |
|
||
| **Chromatic aberration** | top | duplicate layer, offset R/B channels ±1–3px, strongest at impact frames | frame |
|
||
| **Vignette** | top | `boxShadow: inset 0 0 vmin(600) rgba(0,0,0,.6)` or `StudioEffects` in 3D | static |
|
||
| **Camera shake** | root | translate whole frame by `rand(frame)`-driven jitter, decaying after an impact | `rand(frame)` |
|
||
|
||
## Deterministic particle field (the pattern to memorize)
|
||
```tsx
|
||
const frame = useCurrentFrame();
|
||
const { vmin, pick } = useLayout();
|
||
const count = pick(60, 48, 36); // fewer on tall
|
||
{Array.from({ length: count }).map((_, i) => {
|
||
const x = rand(i) * 100; // % of width
|
||
const drift = Math.sin((frame + rand(i + 9) * 200) / 40) * 3;
|
||
const fall = (frame * (0.3 + rand(i + 1) * 0.5) + rand(i + 5) * 120) % 120;
|
||
const twinkle = Math.abs(Math.sin((frame + rand(i + 2) * 60) / 12));
|
||
return <div key={i} style={{
|
||
position: "absolute", left: `${x + drift}%`, top: `${fall - 10}%`,
|
||
width: vmin(6), height: vmin(6), opacity: twinkle,
|
||
background: i % 2 ? accentColor : secondaryColor,
|
||
transform: `rotate(${frame * 2 + rand(i) * 360}deg)`,
|
||
}} />;
|
||
})}
|
||
```
|
||
Notice: `rand(i)` = stable identity per particle; `frame` = motion; `% span` = seamless wrap; aspect drives count via `pick`.
|
||
|
||
## Animated film grain (SVG — the cheapest authenticity layer)
|
||
```tsx
|
||
<svg style={{ position: "absolute", inset: 0, mixBlendMode: "overlay", opacity: 0.08 }}>
|
||
<filter id="grain">
|
||
<feTurbulence type="fractalNoise" baseFrequency="0.9"
|
||
numOctaves="2" seed={frame % 100} stitchTiles="stitch" />
|
||
</filter>
|
||
<rect width="100%" height="100%" filter="url(#grain)" />
|
||
</svg>
|
||
```
|
||
`seed={frame % 100}` is what makes it crawl. Keep opacity 0.05–0.12. For paper/vignette use `mix-blend:multiply` instead.
|
||
|
||
## Chromatic aberration & impact-driven FX
|
||
Aberration should be **strongest at impacts** (a hard cut, the hero reveal, a confetti burst) and near-zero otherwise:
|
||
```tsx
|
||
const ab = interpolate(frame, [hit - 2, hit, hit + 8], [0, vmin(4), 0], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });
|
||
```
|
||
Render the content twice, offset the red copy `translateX(-ab)` `mix-blend:screen` and the blue copy `translateX(+ab)`. Same `interpolate` curve also drives a one-shot camera-shake amplitude — things calm down fast.
|
||
|
||
## Camera shake (subtle continuous + impact)
|
||
```tsx
|
||
// continuous "frame alive" drift — tiny, always on
|
||
const driftX = Math.sin(frame / 50) * vmin(3) + (rand(frame) - 0.5) * vmin(1);
|
||
// impact shake — decays
|
||
const amp = interpolate(frame, [hit, hit + 12], [vmin(14), 0], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });
|
||
const shake = (rand(frame * 7) - 0.5) * amp;
|
||
// apply to a root <AbsoluteFill style={{ transform: `translate(${driftX+shake}px, ${...}px)` }}>
|
||
```
|
||
A locked, perfectly-still frame reads amateur. A *tiny* always-on drift makes it feel hand-held and alive — keep it under ~`vmin(4)` or it's distracting.
|
||
|
||
## 3D glow & bloom
|
||
Make a material glow into bloom: `<meshStandardMaterial emissive={accentColor} emissiveIntensity={2} toneMapped={false} />`, then mount `<StudioEffects bloom={0.9} />`. For sparkly metal confetti raise `metalness`. Drive every transform off `useCurrentFrame()` (deterministic under ANGLE), rotation = `linear` (mechanical), entrances = `spring` with high mass.
|
||
|
||
## Reusable components — make these, don't inline
|
||
Put shared FX in `src/lib/fx.tsx` so every template gets the same texture:
|
||
- `<GrainOverlay opacity? blend? />` — animated `feTurbulence`.
|
||
- `<Vignette strength? />` — inset boxShadow.
|
||
- `<Confetti2D colors count? burstFrame? />` — burst (spring spread) vs rain (continuous fall) modes.
|
||
- `<Sparkles colors count? area? />` — twinkling 4-point stars.
|
||
- `<Bokeh colors count? />` + `<LightLeak color from to />` — bg/overlay atmosphere.
|
||
- `<Aberration amount /> <CameraShake amount />` — finishing pair, wrap the whole comp.
|
||
Each takes `colorSchema` colors so the studio picker recolors the FX, and reads `useLayout()` for per-aspect count/size.
|
||
|
||
## Restraint — FX amplify a hero, they are not the show
|
||
- One celebratory burst on the **hero moment**, not raining the whole video. Often **silence before** + confetti + sparkle SFX on the same frame (see `../remotion-sound-effects/SKILL.md`).
|
||
- Finishing texture (grain, vignette, drift) is *subtle and always-on*; spectacle (confetti, flare, big aberration) is *brief and on a beat*.
|
||
- Don't stack 6 effects at full strength — that reads as a tool preset. Grain at 0.08, vignette at 0.5, aberration only at impacts.
|
||
- All FX color from `colorSchema`; pass a user's garish hex through `mixHex(hex, background, 0.2)` so it doesn't blow out.
|
||
|
||
## Pre-ship checklist
|
||
- [ ] Zero `Math.random` / `Date.now` / `useFrame` — only `rand()` + `frame`. Re-render twice → identical.
|
||
- [ ] Grain is *animated* (per-frame seed), not frozen.
|
||
- [ ] Particle count & size scale per aspect via `pick`/`vmin` — verified in 16:9, 1:1, 9:16; particles stay in the safe zone, never crop Persian text.
|
||
- [ ] Every `interpolate` has `extrapolateLeft/Right: "clamp"` — no drift, no negative opacity.
|
||
- [ ] Spectacle FX land on a beat / the hero; texture FX are subtle & continuous.
|
||
- [ ] FX colors read from `colorSchema`; a continuous camera drift keeps the frame alive.
|
||
- [ ] 3D glow uses `emissive`+`toneMapped={false}` + `StudioEffects` (not hand-rolled bloom).
|
||
|
||
Related: `../remotion-design-styles/SKILL.md`, `../remotion-character-design/SKILL.md`, `../remotion-aspect-ratios/SKILL.md`, `../remotion-template-composition/SKILL.md`, `../remotion-sound-effects/SKILL.md`, `../remotion-music-picker/SKILL.md`, `../remotion-svg-colors/SKILL.md`, `../persian-fonts/SKILL.md`, `../remotion-template-catalog/SKILL.md`, `../flatrender-template-seo/SKILL.md`.
|