npm packages

Use the EffectSoup engine in your own app.

The same deterministic, browser-safe image pipeline is available as three TypeScript packages. No AI, no cloud uploads, no framework lock-in.

Install everything with the meta-package:

pnpm add @effectsoup/effectsoup

Or install the individual packages if you only need part of the engine:

pnpm add @effectsoup/core @effectsoup/presets @effectsoup/worker

@effectsoup/core

Pure TypeScript image-processing primitives. No DOM, no framework dependencies, and safe to run in a browser or Node.js.

Key exports

PixelBuffer, createPixelBuffer, clonePixelBuffer, toGrayscale, adjustBrightnessContrast, applyDuotone, dither, halftone, noise, glow, edge, ascii, stipple, glitch, waveSlice, ...

The core currency is a PixelBuffer:

type PixelBuffer = { width: number; height: number; data: Uint8ClampedArray };
import { createPixelBuffer, toGrayscale } from "@effectsoup/core";
import type { PixelBuffer } from "@effectsoup/core";

// Create a buffer and fill it from a canvas.
const canvas = document.querySelector("canvas") as HTMLCanvasElement;
const ctx = canvas.getContext("2d")!;

const width = canvas.width;
const height = canvas.height;
const source: PixelBuffer = createPixelBuffer(width, height);

const imageData = ctx.getImageData(0, 0, width, height);
source.data.set(imageData.data);

// Run a core primitive in-place.
toGrayscale(source);

// Draw the result back to the canvas.
ctx.putImageData(
  new ImageData(source.data, source.width, source.height),
  0,
  0
);

@effectsoup/presets

Product presets that bundle core primitives into tunable pipelines. Each preset exposes an Intensity slider and optional advanced controls.

Key exports

allPresets, freePresets, premiumPresets, getPresetById, migratePresetId, EffectPreset, ResolvedPresetParameters

Run a preset by resolving its parameters and building a pipeline:

import { getPresetById } from "@effectsoup/presets";
import type { PixelBuffer } from "@effectsoup/core";

const preset = getPresetById("dotHalftone")!;

// Map the product Intensity slider (0-100) to the preset's internal params.
const params = preset.intensityMapper(75, {});

// Build and run the pipeline.
const pipeline = preset.createPipeline(params);
const output: PixelBuffer = pipeline(source, params);

Override advanced controls by starting from the schema defaults:

const preset = getPresetById("dotHalftone")!;

// Start from the schema defaults, then override individual controls.
const overrides = preset.advancedControlSchema.reduce(
  (acc, control) => {
    acc[control.id] = control.defaultValue;
    return acc;
  },
  {} as Record<string, number | string | boolean>
);

// Only override the values you care about.
overrides.dotSize = 8;

const params = preset.intensityMapper(75, overrides);
const output = preset.createPipeline(params)(source, params);

@effectsoup/worker

Web Worker client that runs heavy rendering off the main thread. Handles job versioning, cancellation of stale renders, and buffer transfer.

Key exports

EffectsWorkerClient, RenderOptions, RenderCallbacks

Create a client pointed at the worker script, then render:

import { EffectsWorkerClient } from "@effectsoup/worker";

// Point the client at the worker script. The exact path depends on your
// bundler (Vite, webpack, esbuild, Next.js, etc.).
const client = new EffectsWorkerClient(
  new URL("@effectsoup/worker/dist/worker.js", import.meta.url)
);

const output = await client.render({
  presetId: "dotHalftone",
  resolvedParameters: params,
  source,
  crop: {
    aspectRatio: "original",
    zoom: 1,
    offsetX: 0,
    offsetY: 0
  },
  targetWidth: 1200,
  targetHeight: 1600
});

// Clean up when you're done.
client.terminate();

The exact worker script URL depends on your bundler. In Vite you can import with ?worker; in other setups use the package's dist/worker.js entry.

End-to-end example

Decode a canvas, run a preset in a worker, and draw the result back.

import { EffectsWorkerClient } from "@effectsoup/worker";
import { getPresetById } from "@effectsoup/presets";
import { createPixelBuffer } from "@effectsoup/core";

async function renderPhoto(canvas: HTMLCanvasElement, presetId: string) {
  const ctx = canvas.getContext("2d")!;
  const source = createPixelBuffer(canvas.width, canvas.height);
  source.data.set(ctx.getImageData(0, 0, canvas.width, canvas.height).data);

  const preset = getPresetById(presetId)!;
  const params = preset.intensityMapper(80, {});

  const client = new EffectsWorkerClient(
    new URL("@effectsoup/worker/dist/worker.js", import.meta.url)
  );

  const output = await client.render({
    presetId,
    resolvedParameters: params,
    source,
    crop: { aspectRatio: "original", zoom: 1, offsetX: 0, offsetY: 0 },
    targetWidth: canvas.width,
    targetHeight: canvas.height
  });

  client.terminate();

  ctx.putImageData(
    new ImageData(output.data, output.width, output.height),
    0,
    0
  );
}