image-to-toon
v0.1.1
Published
Turn photos into cartoon, comic, painterly or sketch art in the browser. Canvas, zero dependencies.
Maintainers
Readme
image-to-toon
Turn a photo into cartoon, comic, painterly or sketch art — in the browser, on Canvas, with zero runtime dependencies. ESM + CJS builds, full TypeScript declarations.
▶︎ Try it in the live playground →
Upload a photo, switch styles, drag the sliders, download the result. Everything runs in your browser — no upload, no account, no key.
npm i image-to-toonimport { CaricatureEngine } from 'image-to-toon';
const engine = new CaricatureEngine({ config: { mode: 'color', posterizeLevels: 6 } });
engine.subscribe(({ status, progress, error, result }) => render(status, progress, error, result));
await engine.process(file); // File | Blob | dataURL | URL | Canvas | ImageData
await engine.downloadImage({ fileName: 'toon', format: 'png' });
const body = await engine.toFormData({ fieldName: 'file' }); // ready for POST
const row = await engine.toJSON({ format: 'webp' }); // ready for a databaseOne-shot helpers:
import { toonify, toonifyToDataUrl } from 'image-to-toon';
const { canvas } = await toonify(file, { mode: 'grayscale' });
const dataUrl = await toonifyToDataUrl(imageUrl, { edgeStrength: 1, posterizeLevels: 4 });What's inside
- Ingestion —
File,Blob, base64 data URL, remote URL,HTMLImageElement,HTMLCanvasElement,ImageBitmaporImageData, with size / MIME / dimension validation and typed error codes. - Four styles —
cartoon,comic,painting,sketch, plus seven tuned presets. - Pipeline — radial warp → Kuwahara edge-preserving smoothing → Canny-shaped ink (Sobel → non-maximum suppression → hysteresis linking → response curve) → colour grading → luma/chroma cel quantization → style rendering → background replacement.
- Backgrounds —
blur,solid,gradientortransparent, driven by a tunable subject ellipse or a real cutout from aSegmentationDriver. - State —
status,isProcessing,progress,error,result, plussubscribe()andon('start' | 'progress' | 'complete' | 'error' | 'reset' | 'configchange' | 'modechange'). - Export —
downloadImage(),getBlob(),getBase64(),getFile(),getObjectUrl(),getCanvasElement(),getImageData(),toFormData(),toJSON(). - AI drivers —
createReplicateDriver(),createHuggingFaceDriver(),createRemoteAIDriver()andcreateCustomDriver()to delegate to a generative model for a genuinely redrawn caricature. - Filters — every pixel operation is exported and works on plain
ImageData.
Configuration
const engine = new CaricatureEngine({
config: { mode: 'color', edgeStrength: 0.85, posterizeLevels: 6 },
validation: { maxSizeBytes: 5 * 1024 * 1024, acceptedTypes: ['image/png', 'image/jpeg'] },
});
engine.updateConfig({ smoothness: 6, exaggeration: 0.4 }); // merged + clamped
engine.setMode('grayscale');
engine.setStyle('painting'); // cartoon | comic | painting | sketch
engine.applyPreset('marker'); // + comic | painting | sketch | pencil | portraitBackgrounds
engine.setBackground('blur', { backgroundBlur: 20 });
engine.setBackground('solid', { backgroundColor: '#f0b429' });
engine.setBackground('gradient', { backgroundGradientFrom: '#ffd8a8', backgroundGradientTo: '#d0bfff' });
engine.setBackground('transparent'); // cut-out PNGThe subject defaults to a feathered ellipse (subjectX, subjectY, subjectRadiusX,
subjectRadiusY, subjectFeather). For a pixel-accurate cutout install the optional peer dependency
and plug in a driver:
npm i @mediapipe/tasks-visionimport { createMediaPipeSegmentation } from 'image-to-toon';
engine.setSegmentation(createMediaPipeSegmentation());| Field | Default | Range | Effect |
| --- | --- | --- | --- |
| mode | 'color' | 'color' \| 'grayscale' | Colour cartoon or inked pencil look |
| style | 'cartoon' | cartoon / comic / painting / sketch | Rendering algorithm |
| edgeStrength | 0.8 | 0–1 | Opacity of the ink lines (0 disables edges) |
| edgeThickness | 1 | 0–4 | Line dilation radius, in pixels |
| posterizeLevels | 8 | 2–32 | Luminance bands (cel shading) |
| saturation | 1.3 | 0–3 | Colour punch (ignored in grayscale) |
| contrast | 1.1 | 0–3 | Contrast multiplier |
| brightness | 0.02 | −1–1 | Additive brightness |
| smoothness | 3 | 0–12 | Edge-preserving flattening strength |
| adaptiveThreshold | true | boolean | Adds local-mean ink strokes on unevenly lit photos |
| thresholdBias | 0.02 | −0.5–0.5 | Bias for the adaptive threshold |
| exaggeration | 0 | 0–1 | Radial caricature warp around the frame centre |
| maxDimension | 1600 | 64–8192 | Longest output edge; bigger inputs are downscaled |
| supersample | 1 | 1–3 | Render at N× then downscale — anti-aliases strokes; 2 costs ~4× |
| paperTone | 253 | 180–255 | Paper brightness for sketch |
| sketchWash | 0.22 | 0–1 | Colour wash under sketch strokes |
| sketchHatching | 0 | 0–1 | Pencil hatching in shadows — off by default, raise for drawn strokes |
| background | 'original' | original / blur / solid / gradient / transparent | Outside-the-subject treatment |
| backgroundColor / backgroundGradientFrom / backgroundGradientTo | — | CSS colour | Background colours |
| backgroundGradientAngle | 135 | 0–360 | Gradient direction |
| backgroundBlur | 14 | 0–40 | Blur radius for blur |
| subjectX / subjectY / subjectRadiusX / subjectRadiusY / subjectFeather | — | 0–1.5 | Heuristic subject ellipse |
Out-of-range numbers are clamped rather than rejected, so binding sliders straight to
state.config is safe — the engine is always the source of truth. Every numeric field is continuous:
posterizeLevels accepts 6.5, and smoothness / edgeThickness / backgroundBlur accept
fractional radii, interpolating between neighbouring integer radii instead of snapping.
Sharper output
engine.updateConfig({ supersample: 2 }); // render at 2×, average back downStrokes and hatching land on a finer grid and are averaged down, so they arrive
anti-aliased rather than snapping to whole pixels. Quadratic cost — preview at 1,
export at 2. Raising maxDimension (or setting it to 0) retains more detail from
large photos; lowering smoothness and raising posterizeLevels keeps more of it.
State and events
const off = engine.subscribe(({ status, isProcessing, progress, error, result }) => {
// status: 'idle' | 'loading' | 'processing' | 'success' | 'error'
});
engine.on('complete', ({ result }) => console.log(result.durationMs));
engine.on('error', ({ error }) => console.warn(error.code, error.message));
engine.cancel(); // abort an in-flight run
engine.reset({ keepSource: true }); // drop the result, keep the upload
off();Events: start, progress, complete, error, reset, configchange, modechange, statechange.
Errors are always a CaricatureError with a typed code: INVALID_SOURCE, FILE_TOO_LARGE,
UNSUPPORTED_TYPE, DIMENSION_EXCEEDED, DECODE_FAILED, NO_SOURCE, NO_RESULT, ABORTED,
DRIVER_FAILED, EXPORT_FAILED, CANVAS_UNAVAILABLE.
Export and persistence
await engine.downloadImage({ fileName: 'toon', format: 'webp', quality: 0.85 });
const blob = await engine.getBlob({ format: 'jpeg', quality: 0.9 });
const dataUrl = await engine.getBase64(); // full data: URL
const file = await engine.getFile({ fileName: 'toon.png' });
const canvas = engine.getCanvasElement();
// multipart upload
const body = await engine.toFormData({ fieldName: 'file', extraFields: { userId } });
await fetch('/api/caricatures', { method: 'POST', body });
// JSON row
const payload = await engine.toJSON({ format: 'webp' });
// { fileName, mimeType, width, height, mode, config, base64, size, createdAt, driver? }Pluggable AI drivers
Swap the local canvas pipeline for any HTTP backend — Replicate, HuggingFace, or your own service:
Filters restyle a photo; they cannot redraw a face with an exaggerated head and features the way a caricature artist does. For that, route processing through a generative model:
import { createReplicateDriver, createHuggingFaceDriver } from 'image-to-toon';
// Browsers cannot call api.replicate.com directly — proxy it, and keep the token there.
engine.setDriver(createReplicateDriver({ url: '/api/replicate', version: '<model-version-id>' }));
engine.setDriver(createHuggingFaceDriver({ model: 'owner/cartoonizer', apiToken }));
engine.setDriver(null); // back to local canvas processingOr wire any endpoint or SDK yourself:
import { CaricatureEngine, createRemoteAIDriver, createCustomDriver } from 'image-to-toon';
const remote = createRemoteAIDriver({
url: '/api/toonify',
headers: () => ({ Authorization: `Bearer ${getToken()}` }),
fieldName: 'image',
extra: (ctx) => ({ style: ctx.mode }),
});
const custom = createCustomDriver('replicate', async (blob, ctx) => {
ctx.onProgress(0.3);
const output = await replicate.run('owner/model', { input: { image: blob, mode: ctx.mode } });
return output[0]; // Blob | data URL | URL | ImageData | Canvas
});
const engine = new CaricatureEngine({ driver: remote });
engine.setDriver(custom); // or null to go back to local processingUsing the filters directly
Every pixel operation is exported and works on plain ImageData — main thread, worker, or Node:
import { stylize, kuwahara, posterizeCel, sobelGradientColor, nonMaxSuppress, hysteresis } from 'image-to-toon';
stylize(imageData, config); // the whole pipeline, in place
kuwahara(imageData, 4); // edge-preserving smoothing
posterizeCel(imageData, 8); // luma/chroma cel quantization
const field = sobelGradientColor(imageData); // magnitude + direction
const lines = hysteresis(nonMaxSuppress(field, w, h), w, h, 0.12, 0.3);Also available: posterize, boxBlur, adjustColor, toGrayscale, grayscalePlane, sobelMagnitude, adaptiveThresholdMask, dilate,
compositeInk, radialExaggeration, blend, cloneImageData, resolveConfig, loadSource,
isAcceptedFile, validateBlob.
Framework bindings
- React —
image-to-toon-react - Angular —
ngx-image-to-toon
Full monorepo docs and interactive playgrounds: https://github.com/mamta-epili/image-to-toon#readme
License
MIT
