Skip to main content
The core factory function that creates a new engine instance. This is what every framework adapter wraps internally, and what you use directly for vanilla JS or unsupported frameworks.
import { createLogoSoup } from "@sanity-labs/logo-soup";

const engine = createLogoSoup();

Returns

LogoSoupEngine — an object with the following methods:

engine.process(options)

Triggers a processing run. Loads images, measures them, and produces normalized dimensions. Cancels any in-flight work from a previous process() call.
options
ProcessOptions
required
The processing options. See Options Reference for full details.
engine.process({
  logos: [
    { src: "/logos/acme.svg", alt: "Acme Corp" },
    { src: "/logos/globex.svg", alt: "Globex" },
  ],
  baseSize: 48,
  scaleFactor: 0.5,
  densityAware: true,
  densityFactor: 0.5,
  cropToContent: false,
  contrastThreshold: 10,
  backgroundColor: "#ffffff",
});
Behavior:
  • If logos is empty, transitions directly to "ready" with an empty array (synchronous).
  • If all logos are already cached, re-normalizes from cache (synchronous, no network requests).
  • Otherwise, transitions to "loading", fetches images, measures them, and transitions to "ready" or "error".
  • Calling process() again while loading cancels the previous run. Only the latest call’s results are emitted.

engine.subscribe(listener)

Subscribes to state changes. Returns an unsubscribe function.
listener
() => void
required
A callback invoked whenever the engine’s state changes. The listener receives no arguments — call getSnapshot() inside it to read the current state.
Returns: () => void — call this to unsubscribe.
const unsubscribe = engine.subscribe(() => {
  const state = engine.getSnapshot();
  console.log(state.status, state.normalizedLogos.length);
});

// Later:
unsubscribe();
The subscribe(callback) → unsubscribe shape is designed to plug directly into framework reactivity primitives: React’s useSyncExternalStore, Vue’s shallowRef, Svelte’s createSubscriber, Solid’s from(), and Angular’s signal().

engine.getSnapshot()

Returns the current immutable state snapshot. Returns: LogoSoupState
const { status, normalizedLogos, error } = engine.getSnapshot();
getSnapshot() must return the same reference (===) when state hasn’t changed. This is a hard requirement for React’s useSyncExternalStore — if it returns a new object each time, React enters an infinite re-render loop. The engine handles this internally.

engine.destroy()

Cleans up the engine: revokes blob URLs created by cropToContent, cancels in-flight image loading, clears the measurement cache, and removes all subscribers.
engine.destroy();
Always call destroy() when you’re done with the engine. Framework adapters handle this automatically on component unmount / scope disposal. After calling destroy(), the engine ignores subsequent process() calls.

Types

LogoSoupEngine

type LogoSoupEngine = {
  process(options: ProcessOptions): void;
  subscribe(listener: () => void): () => void;
  getSnapshot(): LogoSoupState;
  destroy(): void;
};

LogoSoupState

type LogoSoupState = {
  status: "idle" | "loading" | "ready" | "error";
  normalizedLogos: NormalizedLogo[];
  error: Error | null;
};
StatusDescription
"idle"Initial state, before process() is called
"loading"Images are being fetched and measured
"ready"Normalization complete, normalizedLogos is populated
"error"All images failed to load, error is set
If some images fail but others succeed, the engine transitions to "ready" with the successful logos. "error" only occurs when every image fails.

ProcessOptions

type ProcessOptions = {
  logos: (string | LogoSource)[];
  baseSize?: number;            // default: 48
  scaleFactor?: number;         // default: 0.5
  contrastThreshold?: number;   // default: 10
  densityAware?: boolean;       // default: true
  densityFactor?: number;       // default: 0.5
  cropToContent?: boolean;      // default: false
  backgroundColor?: BackgroundColor;
};
See Options Reference for detailed descriptions of each field.
type NormalizedLogo = {
  src: string;
  alt: string;
  originalWidth: number;
  originalHeight: number;
  contentBox?: BoundingBox;
  normalizedWidth: number;
  normalizedHeight: number;
  aspectRatio: number;
  pixelDensity?: number;
  visualCenter?: VisualCenter;
  croppedSrc?: string;
};
See Options Reference for field descriptions.

Caching Behavior

The engine caches image measurements by URL. This means:
  • Changing baseSize or scaleFactor re-normalizes from cache (synchronous, no network).
  • Changing contrastThreshold, densityAware, or backgroundColor invalidates the cache because these affect the measurement itself.
  • Adding new logos only loads the new ones; previously cached logos are reused.
  • Removing logos prunes their cache entries and revokes any associated blob URLs.

Example: Full Lifecycle

import { createLogoSoup, getVisualCenterTransform } from "@sanity-labs/logo-soup";

const engine = createLogoSoup();

// 1. Subscribe to state changes
const unsubscribe = engine.subscribe(() => {
  const { status, normalizedLogos, error } = engine.getSnapshot();

  switch (status) {
    case "loading":
      console.log("Loading logos...");
      break;
    case "ready":
      console.log("Ready:", normalizedLogos.length, "logos");
      for (const logo of normalizedLogos) {
        const transform = getVisualCenterTransform(logo, "visual-center-y");
        console.log(`  ${logo.alt}: ${logo.normalizedWidth}x${logo.normalizedHeight}`, transform);
      }
      break;
    case "error":
      console.error("Failed:", error?.message);
      break;
  }
});

// 2. Process logos
engine.process({
  logos: [
    { src: "/logos/acme.svg", alt: "Acme" },
    { src: "/logos/globex.svg", alt: "Globex" },
  ],
});
// → "Loading logos..."
// → "Ready: 2 logos"

// 3. Re-process with different size (uses cache, synchronous)
engine.process({
  logos: [
    { src: "/logos/acme.svg", alt: "Acme" },
    { src: "/logos/globex.svg", alt: "Globex" },
  ],
  baseSize: 96,
});
// → "Ready: 2 logos" (instant, no loading state)

// 4. Clean up
unsubscribe();
engine.destroy();