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/effectsoupOr 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
);
}