All files / confetti/src/shapes pathShape.ts

100% Statements 9/9
100% Branches 7/7
100% Functions 2/2
100% Lines 7/7

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                                                                  2x                                                                 9x 9x       4x 3x 2x 2x        
/**
 * `pathShape` — fully custom vector confetti from a `Path2D` factory.
 *
 * Unlike sprite shapes, path shapes are filled with each particle's own
 * color (from the burst's `colors` palette) and drawn with full
 * rotation/tumble — the go-to for brand marks that should inherit the
 * confetti palette.
 *
 * SSR-safe: the factory runs lazily on first draw, and environments
 * without `Path2D` simply skip drawing.
 */
import { toFinite, type PathShape } from "../types";
import { DEFAULT_PATH_SHAPE_SIZE } from "../engine/draw";
 
/**
 * Options for {@link pathShape}.
 *
 * @example
 * ```ts
 * pathShape(makeHeart, { size: 14, key: "heart@1" });
 * ```
 */
export interface PathShapeOptions {
  /** Drawn size of the unit box in CSS px at `scalar: 1`. Default `10`. */
  size?: number;
  /**
   * Explicit identity key. Defaults to an auto-generated unique key — pass
   * one only if you want two `pathShape(...)` calls to be treated as the
   * same shape.
   */
  key?: string;
}
 
let pathShapeCounter = 0;
 
/**
 * Create a custom vector confetti shape from a `Path2D` factory. Draw your
 * path in the **unit box** `[0,1]×[0,1]` — the renderer centers it on the
 * particle and scales it to `size × scalar` px. The factory runs once
 * (memoized); the same `Path2D` object is reused for every particle and
 * frame.
 *
 * @param factory - Builds the unit-box `Path2D`. Called lazily, once.
 * @param options - Size/key, see {@link PathShapeOptions}.
 * @returns A {@link PathShape} usable anywhere a `ConfettiShape` goes.
 *
 * @example
 * ```ts
 * import { fireConfetti, pathShape } from "@usefy/confetti";
 *
 * const triangle = pathShape(() => {
 *   const p = new Path2D();
 *   p.moveTo(0.5, 0);
 *   p.lineTo(1, 1);
 *   p.lineTo(0, 1);
 *   p.closePath();
 *   return p;
 * }, { size: 12 });
 *
 * fireConfetti({ shapes: [triangle] });
 * ```
 */
export function pathShape(
  factory: () => Path2D,
  options: PathShapeOptions = {},
): PathShape {
  let cached: Path2D | null = null;
  return {
    key: options.key ?? `usefy-path:${++pathShapeCounter}`,
    size: Math.max(0.1, toFinite(options.size, DEFAULT_PATH_SHAPE_SIZE)),
    path(): Path2D | null {
      if (cached) return cached;
      if (typeof Path2D === "undefined") return null; // SSR/old env — skip
      cached = factory();
      return cached;
    },
  };
}