image-to-toon
One Canvas engine, three published packages
Stack
Frontend
- TypeScript across three packages
- Canvas 2D pixel pipeline, no WebGL dependency
- React hook and drop-in component
- Angular standalone component and service
- Three Vite playgrounds — vanilla, React, Angular
Backend & Data
- No backend: every pixel is processed in the browser
- Zero runtime dependencies in the core
- ESM and CJS builds with full type declarations
- Angular package built Ivy partial-compiled via ng-packagr
- Turborepo across an npm and pnpm workspace
- Pluggable segmentation and AI drivers behind an interface
Problem
Image-effect libraries tend to arrive welded to one framework, or to be a thin wrapper over a hosted API that needs a key and uploads the photo. I wanted the transformation itself to be the library — pure pixels, no network, no account — with the framework bindings as thin adapters rather than separate rewrites.
What I built
- A framework-agnostic Canvas engine published to npm with zero runtime dependencies: Kuwahara edge-preserving smoothing, luma and chroma cel quantization, colour-aware Sobel edges with a smoothstep ink curve, adaptive thresholding and a radial warp.
- Four styles — cartoon, comic, painting and sketch — with seven tuned presets and around a dozen live parameters.
- React and Angular packages over the same core: a `useCaricature()` hook with a drop-in component, and an Angular standalone component with a service.
- Background replacement — blur, solid, gradient or transparent — with a tunable subject ellipse by default and a `SegmentationDriver` interface for a real cutout.
- Input validation with typed error codes: size limits, a MIME allow-list and maximum source dimensions, so a bad file fails predictably rather than mid-pipeline.
- Three interactive playgrounds, one per binding, published to GitHub Pages.
Technical decisions & trade-offs
- The core knows nothing about React or Angular. Both bindings are adapters over the same engine, so a fix lands once and a third framework is a new package rather than a fork.
- Everything runs on the client. No upload, no key, no account — which removes the privacy question entirely for a tool whose input is somebody’s photograph.
- Heavy dependencies are peer, not bundled: MediaPipe and imgly are optional segmentation drivers behind an interface, so installing the core costs nothing.
- Canvas 2D rather than WebGL, because the pipeline is filter-bound rather than geometry-bound and it runs everywhere without a context negotiation.
- The engine returns a canvas, a blob, base64 and FormData, so persisting a result is the caller’s decision rather than the library’s.
Challenges solved
- Kuwahara smoothing is quadratic in kernel radius; keeping it interactive on a large photo meant working in a downscaled pass and compositing back rather than filtering at full resolution.
- Three packages, one version: the bindings depend on an exact core version, so a release that updates one and not the others is a broken install rather than a subtle bug.
- Angular ships Ivy partial-compiled FESM2022 while the others are tsup ESM and CJS — one repository, two build systems, without letting the type surfaces drift apart.
Outcome
Three packages on npm under MIT, and a live playground where the whole pipeline runs in the visitor’s browser with nothing to install and no key to obtain.