R&D brief (references/design-motion-rnd.md): 2024-2026 design/motion trends, animating-anything craft, Iran-aware asset pipeline, masterpiece + platform playbook. New craft skills: motion-design-principles, scene-transitions, kinetic-typography, video-hooks, particles-and-effects, asset-sourcing — grounded in the Remotion stack. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
9.3 KiB
name, description
| name | description |
|---|---|
| particles-and-effects | 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, andpick(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—colorSchemaprops areaccentColor / 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,
emissiveglow that bloom picks up, 3D confetti with perspective. LetStudioEffectsdo 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)
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)
<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:
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)
// 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? />— animatedfeTurbulence.<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 takescolorSchemacolors so the studio picker recolors the FX, and readsuseLayout()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). - 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 throughmixHex(hex, background, 0.2)so it doesn't blow out.
Pre-ship checklist
- Zero
Math.random/Date.now/useFrame— onlyrand()+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
interpolatehasextrapolateLeft/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, remotion-character-design, remotion-aspect-ratios, remotion-template-composition, remotion-sound-effects, remotion-music-picker, remotion-svg-colors, persian-fonts, remotion-template-catalog, flatrender-template-seo.