Files
AISkills/flat-artist/particles-and-effects/SKILL.md
T
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

121 lines
9.5 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: 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 ±13px, 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.050.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`.