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 | 2x 2x 15x 15x 15x 15x 15x 15x 15x 16x 15x 13x 7x 6x 6x 6x 1x 6x 12x 4x 4x 16x 16x 16x 2x 4x 4x 4x 4x | /**
* `imageShape` — image confetti (logos, avatars, stickers).
*
* Async decode is handled through the sprite cache's null-retry contract:
* until the image has loaded, `rasterize` returns `null` and the particle
* simply isn't drawn — no placeholder flash, no throw. Load errors are a
* silent permanent skip (confetti must never take the page down).
*
* SSR-safe: no `Image` construction at import time, and `rasterize`
* returns `null` in environments without one.
*/
import { toFinite, type SpriteEntry, type SpriteShape } from "../types";
/**
* Options for {@link imageShape}.
*
* Provide `width` and/or `height` in CSS px at `scalar: 1`; a missing
* dimension is derived from the image's natural aspect ratio. With neither
* given, the image is sized to {@link DEFAULT_IMAGE_HEIGHT} px tall.
*
* @example
* ```ts
* imageShape("/logo.svg", { height: 16 });
* ```
*/
export interface ImageShapeOptions {
/** Drawn width in CSS px at `scalar: 1`. */
width?: number;
/** Drawn height in CSS px at `scalar: 1`. */
height?: number;
}
/** Default drawn height (CSS px at `scalar: 1`) when no size is given. */
export const DEFAULT_IMAGE_HEIGHT = 12;
let anonymousElementCounter = 0;
/**
* Create an image confetti shape from a URL or an existing
* `HTMLImageElement`. Loading starts lazily on the first draw that needs
* the sprite; particles are skipped (not delayed, not errored) until the
* image is ready.
*
* @param src - Image URL, or an `HTMLImageElement` you manage yourself.
* @param options - Target size, see {@link ImageShapeOptions}.
* @returns A {@link SpriteShape} usable anywhere a `ConfettiShape` goes.
*
* @example
* ```ts
* import { fireConfetti, imageShape } from "@usefy/confetti";
*
* fireConfetti({ shapes: [imageShape("/logo.png", { height: 14 })] });
* ```
*/
export function imageShape(
src: string | HTMLImageElement,
options: ImageShapeOptions = {},
): SpriteShape {
const isElement =
typeof HTMLImageElement !== "undefined" && src instanceof HTMLImageElement;
const url = isElement ? src.src : (src as string);
let img: HTMLImageElement | null = isElement ? src : null;
let failed = false;
let cached: SpriteEntry | null = null;
const identity = url || `element#${++anonymousElementCounter}`;
return {
key: `usefy-image:${identity}|${options.width ?? ""}x${options.height ?? ""}`,
rasterize(): SpriteEntry | null {
if (cached) return cached;
if (failed) return null; // permanent silent skip
if (!img) {
if (typeof Image === "undefined") return null; // SSR — retry client-side
// Note: each imageShape() call owns one Image, so two shapes made
// from the same URL decode it twice. Bounded and deliberate — the
// browser's HTTP cache dedupes the network fetch, the sprite cache
// dedupes per shape key, and sharing decode state across factory
// calls would need module-global bookkeeping for a rare case.
img = new Image();
img.decoding = "async";
img.addEventListener("error", () => {
failed = true;
});
img.src = url;
}
if (!img.complete || !img.naturalWidth) return null; // still decoding
const naturalW = img.naturalWidth;
const naturalH = img.naturalHeight || naturalW;
let width = toFinite(options.width, NaN);
let height = toFinite(options.height, NaN);
if (Number.isNaN(width) && Number.isNaN(height)) {
height = DEFAULT_IMAGE_HEIGHT;
}
if (Number.isNaN(width)) width = (height * naturalW) / naturalH;
if (Number.isNaN(height)) height = (width * naturalH) / naturalW;
cached = { source: img, width, height };
return cached;
},
};
}
|