perfect-freehand
Draw perfect pressure-sensitive freehand lines from an array of input points, with no rendering dependencies.
Repository Health
Technical Analysis
perfect-freehand is a small, dependency-free TypeScript library that turns an array of raw input points — from a mouse, touch, or stylus — into the outline points of a natural-looking, pressure-sensitive stroke. Its main export, getStroke, accepts points as either [x, y, pressure] tuples or {x, y, pressure} objects and returns a polygon (as Vec2[]) that you render however you like: as an SVG path, an HTML Canvas Path2D, or any other 2D graphics target.
Internally the library is split into two composable stages — getStrokePoints, which adjusts, streamlines, and measures the input points, and getStrokeOutlinePoints, which walks pressure-derived radii around the centerline to build the final outline — both of which are also exported individually for advanced use. Options let you tune the stroke’s base size, pressure thinning, edge smoothing, streamlining, and independent tapering/cap behavior at the start and end of the line, plus optional pressure simulation for devices (like a plain mouse) that don’t report real pressure.
It’s become the de facto stroke-rendering engine behind whiteboard and freehand-drawing tools in the JavaScript ecosystem (including tldraw, from the same author), and has been ported to Dart, Python, Rust, and Odin by the community.
What You Get
- A single
getStroke(points, options)function that returns outline points ready to be turned into an SVG path or CanvasPath2D - Support for both array-tuple (
[x, y, pressure]) and object ({x, y, pressure}) point formats, with pressure optional in both - Automatic pressure simulation from point velocity for input devices that don’t report real pressure
- Independently configurable tapering, easing, and caps for the start and end of a stroke
- Lower-level
getStrokePointsandgetStrokeOutlinePointsexports for building custom pipelines on top of the core algorithm - Zero runtime dependencies and dual ESM/CJS builds with full TypeScript types
Common Use Cases
- Whiteboard and freehand-drawing canvases that need natural-looking ink from mouse, touch, or stylus input
- Signature capture for e-signature or form-signing widgets
- Freehand annotation layers over documents, images, or diagrams
- Custom illustration or sketching tools built on SVG or HTML Canvas
Under The Hood
Architecture
The library lives inside a Yarn workspaces monorepo as packages/perfect-freehand (the published package) alongside packages/dev, a Vite-based example app. The core is a simple three-stage functional pipeline with no classes or shared mutable state: getStroke() (in getStroke.ts) is a thin composition of getStrokePoints(), which adjusts raw input into StrokePoint[] objects carrying computed vectors, distances, and running length, and getStrokeOutlinePoints(), which walks pressure-derived radii around the centerline to build the final outline polygon, handling tapering, caps, and sharp-corner detection along the way. Supporting modules (vec.ts for 2D vector math, getStrokeRadius.ts for pressure-to-radius conversion, constants.ts for tunables) are pure and dependency-free. Because getStrokePoints and getStrokeOutlinePoints are exported independently of getStroke, the StrokePoint shape is a de facto public contract — changing it would ripple through both stages and any consumer building a custom pipeline on top of them.
Tech Stack
Written entirely in TypeScript with zero runtime dependencies. The library is built with Rolldown for dual ESM/CJS output plus a separate tsc pass to emit declaration files, tested with Vitest (including a dedicated vitest bench benchmark suite), and linted with ESLint via typescript-eslint and eslint-config-prettier. The surrounding monorepo uses Yarn 4 workspaces with the lazyrepo task runner to orchestrate the library build and the packages/dev example app, plus Husky and lint-staged for pre-commit formatting/linting and commitlint for conventional-commit messages.
Code Quality
Each core function has a corresponding Vitest spec with snapshot assertions (getStroke.spec.ts, getStrokePoints.spec.ts, getStrokeOutlinePoints.spec.ts, getStrokeRadius.spec.ts), exercised against fixture inputs covering edge cases like a single point, two points, duplicate points, and larger point sets, plus a seeded RNG for reproducible randomized inputs. Types are used consistently throughout (StrokeOptions, StrokePoint, Vec2) with no loose typing observed in the sampled files. CI runs the build and test suite across Node 20.x and 22.x and runs a separate dependency security audit job on every push and pull request.
What Makes It Unique Rather than approximating a variable-width line with filled circles or naive quadratic segments, perfect-freehand computes an explicit outline polygon by tracking pressure-derived radii and running length on both sides of the stroke centerline, with configurable tapering and easing independently at each end and optional pressure simulation from velocity for devices with no real pressure input. It deliberately returns raw outline points rather than SVG or Canvas calls, keeping rendering entirely up to the consumer — a specific design choice that has let it become a shared stroke engine reused across SVG-, canvas-, and native-rendering drawing tools, including community ports to Dart, Python, Rust, and Odin.
Used by 2 apps in this directory
Documenso
Digital Signiture
Self-hosted, open-source DocuSign alternative with legally binding PDF signatures, multi-party workflows, and a full REST and tRPC API.
Dyad
AI Development · Productivity · AI Code Assistants
Build AI-powered apps locally with your own keys, your own models, and zero cloud lock-in.