All files / confetti/src/engine pool.ts

100% Statements 20/20
100% Branches 5/5
100% Functions 6/6
100% Lines 18/18

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123                        8x                                                                       47476x                                                                                     98x     98x 98x   98x 45519x 45519x     98x   6879x 6879x 8x 8x     6839x 6839x 6839x     12x     7x        
/**
 * Fixed-size particle pool for `@usefy/confetti`.
 *
 * The engine recycles particle objects through this pool so that the
 * steady-state animation loop performs **zero allocations**: after warm-up,
 * every `fire()` cohort reuses the exact same objects released by earlier
 * cohorts. The pool pre-allocates `initialSize` particles up front and
 * grows on demand (never shrinks).
 */
import type { Particle } from "../types";
 
/** Default number of particles pre-allocated by {@link createParticlePool}. */
export const DEFAULT_POOL_SIZE = 500;
 
/**
 * A recycling pool of {@link Particle} objects.
 *
 * @example
 * ```ts
 * const pool = createParticlePool(100);
 * const p = pool.take();   // reused (or newly grown) particle
 * pool.release(p);         // back to the free list
 * pool.allocated;          // total objects ever created (≥ 100)
 * pool.available;          // currently free
 * ```
 */
export interface ParticlePool {
  /** Get a particle — reuses a free one, allocates only when empty. */
  take(): Particle;
  /** Return a particle to the free list for reuse. */
  release(p: Particle): void;
  /** Total particles allocated by this pool (grows on demand, never shrinks). */
  readonly allocated: number;
  /** Particles currently available for `take()`. */
  readonly available: number;
}
 
/**
 * Create a zeroed particle object with every field present (monomorphic
 * shape — helps the JIT keep the hot loop fast).
 *
 * @example
 * ```ts
 * const p = createParticle();
 * p.life; // 0 (dead until spawnParticle initializes it)
 * ```
 */
export function createParticle(): Particle {
  return {
    x: 0,
    y: 0,
    vx: 0,
    vy: 0,
    fallVelocity: 0,
    gravity: 0,
    drift: 0,
    decay: 1,
    rotation: 0,
    rotationSpeed: 0,
    tumblePhase: 0,
    tumbleSpeed: 0,
    wobblePhase: 0,
    wobbleSpeed: 0,
    wobbleAmp: 0,
    lifetime: 0,
    life: 0,
    opacity: 0,
    color: "#000000",
    shape: "square",
    scalar: 1,
    cohort: -1,
  };
}
 
/**
 * Create a {@link ParticlePool} pre-filled with `initialSize` particles.
 * Internal — each engine owns exactly one (sized by its `poolSize` option).
 *
 * @example
 * ```ts
 * // Inside createConfettiEngine:
 * const pool = createParticlePool(500);
 * const p = pool.take();
 * pool.release(p);
 * pool.take() === p; // true — object identity is reused
 * ```
 */
export function createParticlePool(
  initialSize: number = DEFAULT_POOL_SIZE,
): ParticlePool {
  // Defense in depth: a NaN/Infinity size must never hang preallocation.
  const size = Number.isFinite(initialSize)
    ? Math.max(0, Math.floor(initialSize))
    : DEFAULT_POOL_SIZE;
  const free: Particle[] = [];
  let allocated = 0;
 
  for (let i = 0; i < size; i++) {
    free.push(createParticle());
    allocated++;
  }
 
  return {
    take(): Particle {
      const recycled = free.pop();
      if (recycled) return recycled;
      allocated++;
      return createParticle();
    },
    release(p: Particle): void {
      p.life = 0;
      p.cohort = -1;
      free.push(p);
    },
    get allocated(): number {
      return allocated;
    },
    get available(): number {
      return free.length;
    },
  };
}