Physics Confetti

  • Skia
  • Reanimated
  • Haptics

Colored paper shards thrown up from just below the middle of the canvas, spinning while gravity pulls them back down. Up to 300 of them share one sprite sheet and reach the screen through a single <Atlas> draw.

Position from time alone

Nothing is stepped frame by frame. createConfettiParticles gives each shard a launch angle inside the spread cone, a speed between 50% and 130% of power and a spin rate. transformAt then works out position, rotation and fade from elapsed seconds, so a late frame can't knock a shard off its arc, and gravity, read every frame, can still bend a burst that is already in the air.

const angle = -Math.PI / 2 + (rand() - 0.5) * spread;const speed = power * (0.5 + rand() * 0.8);// …return {  x: p.vx * t,  y: p.vy * t + 0.5 * gravity * t * t,  rot: p.rot0 + p.rotV * t,  alpha: Math.max(0, 1 - t / 3),};

Spinning about the center

Each shard is an RSXform: scale times cosine, scale times sine, and a translation. Skia rotates a sprite about its top-left corner, so the worklet subtracts the rotated half-size and the shard spins about its middle. This draw passes no per-sprite colors, so the fade is folded into the scale, and a shard shrinks away over three seconds instead of turning transparent.

const tr = transformAt(particles[i], t, gravity);const s = particles[i].scale * tr.alpha;const cos = Math.cos(tr.rot) * s;const sin = Math.sin(tr.rot) * s;const half = SPRITE / 2;val.set(  cos,  sin,  originX + tr.x - (cos * half - sin * half),  originY + tr.y - (sin * half + cos * half),);

Replays and the clock

The seed is the fire count run through a multiplicative hash into mulberry32, so the same count always replays the same burst, and a new fire replaces shards still in flight. The frame clock runs for five seconds per burst and then lands, leaving a static picture. An always-running clock was measured at about 20% CPU while idle.

(fireSignal * 2654435761) % 1_000_000_007 || 1,// …scheduleOnUI(() => {  "worklet";  start.set(clock.get());});requestFrames(BURST_MS);

Tuning

  • 150
  • 900
  • 2.2
  • 1500
<ConfettiBurst params={{ count: 150, power: 900, spread: 2.2, gravity: 1500 }} fireSignal={0} />