Top Related Projects
Virtual whiteboard for sketching hand-drawn like diagrams
whiteboard SDK / infinite canvas SDK
Create and animate hand-drawn annotations on a web page
Javascript Canvas Library, SVG-to-Canvas (& canvas-to-SVG) Parser
draw.io is a JavaScript, client-side editor for general diagramming.
Quick Overview
Perfect Freehand is a JavaScript library that creates smooth, natural-looking freehand lines for drawing applications. It uses an advanced algorithm to generate high-quality, pressure-sensitive strokes that closely mimic the appearance of real pen or brush strokes.
Pros
- Produces highly realistic and smooth freehand lines
- Supports pressure sensitivity for varying line thickness
- Lightweight and easy to integrate into existing projects
- Works well with both mouse and touch input devices
Cons
- Limited customization options for advanced use cases
- May have performance issues with very long or complex strokes
- Requires additional setup for use with certain frameworks or environments
- Documentation could be more comprehensive for advanced features
Code Examples
Creating a basic freehand line:
import { getStroke } from 'perfect-freehand'
const points = [
[0, 0],
[10, 5],
[20, 8],
[30, 4],
[40, 10]
]
const stroke = getStroke(points)
Customizing stroke options:
const stroke = getStroke(points, {
size: 16,
thinning: 0.5,
smoothing: 0.5,
streamline: 0.5,
})
Using with pressure sensitivity:
const pointsWithPressure = [
[0, 0, 0.5],
[10, 5, 0.7],
[20, 8, 0.9],
[30, 4, 0.6],
[40, 10, 0.8]
]
const stroke = getStroke(pointsWithPressure, {
size: 16,
thinning: 0.5,
pressureStart: 0.5,
pressureEnd: 1,
})
Getting Started
To use Perfect Freehand in your project:
-
Install the library:
npm install perfect-freehand
-
Import and use in your code:
import { getStroke } from 'perfect-freehand' const points = [[0, 0], [10, 10], [20, 20]] const stroke = getStroke(points) // Use the stroke data to render your line // (e.g., with Canvas or SVG)
Competitor Comparisons
Virtual whiteboard for sketching hand-drawn like diagrams
Pros of Excalidraw
- Full-featured whiteboard application with a wide range of tools and features
- Collaborative functionality for real-time multi-user editing
- Extensive library of pre-made shapes and elements
Cons of Excalidraw
- Larger codebase and more complex setup for integration
- May have higher performance overhead due to its comprehensive feature set
- Less focused on freehand drawing specifically
Code Comparison
Excalidraw (React component usage):
import { Excalidraw } from "@excalidraw/excalidraw";
<Excalidraw
initialData={initialData}
onChange={(elements, state) => console.log("Elements:", elements, "State:", state)}
/>
Perfect Freehand (Basic usage):
import { getStroke } from "perfect-freehand"
const points = [[0, 0], [10, 5], [20, 8]]
const stroke = getStroke(points)
Perfect Freehand focuses solely on generating smooth freehand strokes, while Excalidraw provides a complete whiteboard solution. Perfect Freehand is more lightweight and easier to integrate for specific freehand drawing needs, whereas Excalidraw offers a broader set of features but requires more setup and resources.
whiteboard SDK / infinite canvas SDK
Pros of tldraw
- More comprehensive drawing and diagramming tool with a full-featured UI
- Supports collaborative editing and real-time multiplayer functionality
- Includes advanced features like shape recognition and image import
Cons of tldraw
- Larger codebase and more complex to integrate into existing projects
- Potentially higher learning curve for developers due to its extensive feature set
- May have higher performance overhead for simple freehand drawing tasks
Code Comparison
perfect-freehand:
import { getStrokePoints, getStroke } from 'perfect-freehand'
const points = [[0, 0], [10, 10], [20, 20]]
const stroke = getStroke(points)
tldraw:
import { Tldraw, useFileSystem } from '@tldraw/tldraw'
function App() {
const fileSystemEvents = useFileSystem()
return <Tldraw {...fileSystemEvents} />
}
perfect-freehand focuses specifically on creating smooth, natural-looking freehand strokes, while tldraw provides a complete drawing and diagramming solution with a rich set of tools and features. perfect-freehand is more suitable for projects that require only freehand drawing functionality, whereas tldraw is better suited for applications needing a full-featured drawing and diagramming tool with collaboration capabilities.
Create and animate hand-drawn annotations on a web page
Pros of rough-notation
- Focuses on creating animated, hand-drawn-style annotations for web content
- Offers a variety of annotation styles (underline, circle, box, etc.)
- Easy to integrate with existing web projects
Cons of rough-notation
- Limited to annotation-specific use cases
- Less control over the freehand drawing style
- May not be suitable for complex drawing or illustration tasks
Code comparison
rough-notation:
import { annotate } from 'rough-notation';
const element = document.querySelector('#myElement');
const annotation = annotate(element, { type: 'underline' });
annotation.show();
perfect-freehand:
import { getStroke } from 'perfect-freehand';
const points = [[0, 0], [10, 20], [20, 30]];
const stroke = getStroke(points);
Summary
rough-notation is ideal for adding animated, hand-drawn annotations to web content, offering various styles and easy integration. However, it's limited to annotation use cases and provides less control over the drawing style. perfect-freehand, on the other hand, focuses on creating smooth, natural-looking freehand strokes, making it more suitable for complex drawing tasks but potentially requiring more setup for simple annotations.
Javascript Canvas Library, SVG-to-Canvas (& canvas-to-SVG) Parser
Pros of Fabric.js
- Comprehensive canvas manipulation library with a wide range of features
- Supports object-oriented programming for canvas elements
- Extensive documentation and community support
Cons of Fabric.js
- Larger file size and potentially heavier performance impact
- Steeper learning curve due to its extensive feature set
- May be overkill for simple drawing or freehand applications
Code Comparison
Perfect Freehand:
import { getStroke } from "perfect-freehand"
const points = [[0, 0], [10, 5], [20, 8]]
const stroke = getStroke(points, {
size: 8,
thinning: 0.5,
smoothing: 0.5,
streamline: 0.5,
})
Fabric.js:
var canvas = new fabric.Canvas('canvas');
var path = new fabric.Path('M 0 0 L 200 100 L 170 200 z');
path.set({ fill: 'red', stroke: 'green', opacity: 0.5 });
canvas.add(path);
Perfect Freehand is focused specifically on creating smooth, natural-looking freehand lines, while Fabric.js is a more comprehensive canvas manipulation library that can handle various shapes, objects, and interactions. Perfect Freehand is lighter and easier to use for freehand drawing, while Fabric.js offers more extensive features for complex canvas applications.
draw.io is a JavaScript, client-side editor for general diagramming.
Pros of drawio
- Full-featured diagramming tool with a wide range of shapes and connectors
- Supports multiple diagram types (flowcharts, UML, network diagrams, etc.)
- Offers both web-based and desktop applications
Cons of drawio
- Larger codebase and more complex setup for integration
- May be overkill for simple freehand drawing needs
- Steeper learning curve for users due to extensive features
Code comparison
drawio (XML-based diagram definition):
<mxGraphModel>
<root>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<mxCell id="2" value="Start" style="rounded=1;" vertex="1" parent="1">
<mxGeometry x="120" y="120" width="80" height="40" as="geometry"/>
</mxCell>
</root>
</mxGraphModel>
perfect-freehand (JavaScript):
import { getStroke } from "perfect-freehand"
const points = [[0, 0], [10, 5], [20, 8]]
const stroke = getStroke(points, {
size: 16,
thinning: 0.5,
smoothing: 0.5,
streamline: 0.5,
})
Summary
drawio is a comprehensive diagramming tool suitable for complex diagrams and various use cases, while perfect-freehand focuses specifically on creating smooth, natural-looking freehand strokes. drawio offers more features but has a steeper learning curve, whereas perfect-freehand provides a simpler API for freehand drawing functionality.
Convert
designs to code with AI
Introducing Visual Copilot: A new AI model to turn Figma designs to high quality code using your components.
Try Visual CopilotREADME
Draw perfect pressure-sensitive freehand lines.
ð Curious? Try out a demo.
ð Designer? Check out the Figma Plugin.
ð Flutterer? There's now a dart version of this library, too.
ð Love this library? Consider becoming a sponsor.
Table of Contents
Installation
npm install perfect-freehand
or
yarn add perfect-freehand
Introduction
This package exports a function named getStroke
that will generate the points for a polygon based on an array of points.
To do this work, getStroke
first creates a set of spline points (red) based on the input points (grey) and then creates outline points (blue). You can render the result any way you like, using whichever technology you prefer.
Usage
To use this library, import the getStroke
function and pass it an array of input points, such as those recorded from a user's mouse movement. The getStroke
function will return a new array of outline points. These outline points will form a polygon (called a "stroke") that surrounds the input points.
import { getStroke } from 'perfect-freehand'
const inputPoints = [
[0, 0],
[10, 5],
[20, 8],
// ...
]
const outlinePoints = getStroke(inputPoints)
You then can render your stroke points using your technology of choice. See the Rendering section for examples in SVG and HTML Canvas.
You can customize the appearance of the stroke shape by passing getStroke
a second parameter: an options object containing one or more options. See the Options section for a full list of available options.
const stroke = getStroke(myPoints, {
size: 32,
thinning: 0.7,
})
The appearance of a stroke is effected by the pressure associated with each input point. By default, the getStroke
function will simulate pressure based on the distance between input points.
To use real pressure, such as that from a pen or stylus, provide the pressure as the third number for each input point, and set the simulatePressure
option to false
.
const inputPoints = [
[0, 0, 0.5],
[10, 5, 0.7],
[20, 8, 0.8],
// ...
]
const outlinePoints = getStroke(inputPoints, {
simulatePressure: false,
})
In addition to providing points as an array of arrays, you may also provide your points as an array of objects as show in the example below. In both cases, the value for pressure is optional (it will default to .5
).
const inputPoints = [
{ x: 0, y: 0, pressure: 0.5 },
{ x: 10, y: 5, pressure: 0.7 },
{ x: 20, y: 8, pressure: 0.8 },
// ...
]
const outlinePoints = getStroke(inputPoints, {
simulatePressure: false,
})
Note: Internally, the getStroke
function will convert your object points to array points, which will have an effect on performance. If you're using this library ambitiously and want to format your points as objects, consider modifying this library's getStrokeOutlinePoints
to use the object syntax instead (e.g. replacing all [0]
with .x
, [1]
with .y
, and [2]
with .pressure
).
Example
import * as React from 'react'
import { getStroke } from 'perfect-freehand'
import { getSvgPathFromStroke } from './utils'
export default function Example() {
const [points, setPoints] = React.useState([])
function handlePointerDown(e) {
e.target.setPointerCapture(e.pointerId)
setPoints([[e.pageX, e.pageY, e.pressure]])
}
function handlePointerMove(e) {
if (e.buttons !== 1) return
setPoints([...points, [e.pageX, e.pageY, e.pressure]])
}
const stroke = getStroke(points, {
size: 16,
thinning: 0.5,
smoothing: 0.5,
streamline: 0.5,
})
const pathData = getSvgPathFromStroke(stroke)
return (
<svg
onPointerDown={handlePointerDown}
onPointerMove={handlePointerMove}
style={{ touchAction: 'none' }}
>
{points && <path d={pathData} />}
</svg>
)
}
Tip: For implementations in Typescript, see the example project included in this repository.
Documentation
Options
The options object is optional, as are each of its properties.
Property | Type | Default | Description |
---|---|---|---|
size | number | 8 | The base size (diameter) of the stroke. |
thinning | number | .5 | The effect of pressure on the stroke's size. |
smoothing | number | .5 | How much to soften the stroke's edges. |
streamline | number | .5 | How much to streamline the stroke. |
simulatePressure | boolean | true | Whether to simulate pressure based on velocity. |
easing | function | t => t | An easing function to apply to each point's pressure. |
start | { } | Tapering options for the start of the line. | |
end | { } | Tapering options for the end of the line. | |
last | boolean | true | Whether the stroke is complete. |
Note: When the last
property is true
, the line's end will be drawn at the last input point, rather than slightly behind it.
The start
and end
options accept an object:
Property | Type | Default | Description |
---|---|---|---|
cap | boolean | true | Whether to draw a cap. |
taper | number or boolean | 0 | The distance to taper. If set to true, the taper will be the total length of the stroke. |
easing | function | t => t | An easing function for the tapering effect. |
Note: The cap
property has no effect when taper
is more than zero.
getStroke(myPoints, {
size: 8,
thinning: 0.5,
smoothing: 0.5,
streamline: 0.5,
easing: (t) => t,
simulatePressure: true,
last: true,
start: {
cap: true,
taper: 0,
easing: (t) => t,
},
end: {
cap: true,
taper: 0,
easing: (t) => t,
},
})
Tip: To create a stroke with a steady line, set the
thinning
option to0
.
Tip: To create a stroke that gets thinner with pressure instead of thicker, use a negative number for the
thinning
option.
Other Exports
For advanced usage, the library also exports smaller functions that getStroke
uses to generate its outline points.
getStrokePoints
A function that accepts an array of points (formatted either as [x, y, pressure]
or { x: number, y: number, pressure: number}
) and (optionally) an options object. Returns a set of adjusted points as { point, pressure, vector, distance, runningLength }
. The path's total length will be the runningLength
of the last point in the array.
import { getStrokePoints } from 'perfect-freehand'
import samplePoints from "./samplePoints.json'
const strokePoints = getStrokePoints(samplePoints)
getOutlinePoints
A function that accepts an array of points (formatted as { point, pressure, vector, distance, runningLength }
, i.e. the output of getStrokePoints
) and (optionally) an options object, and returns an array of points ([x, y]
) defining the outline of a pressure-sensitive stroke.
import { getStrokePoints, getOutlinePoints } from 'perfect-freehand'
import samplePoints from "./samplePoints.json'
const strokePoints = getStrokePoints(samplePoints)
const outlinePoints = getOutlinePoints(strokePoints)
Note: Internally, the getStroke
function passes the result of getStrokePoints
to getStrokeOutlinePoints
, just as shown in this example. This means that, in this example, the result of outlinePoints
will be the same as if the samplePoints
array had been passed to getStroke
.
StrokeOptions
A TypeScript type for the options object. Useful if you're defining your options outside of the getStroke
function.
import { StrokeOptions, getStroke } from 'perfect-freehand'
const options: StrokeOptions = {
size: 16,
}
const stroke = getStroke(options)
Tips & Tricks
Freehand Anything
While this library was designed for rendering the types of input points generated by the movement of a human hand, you can pass any set of points into the library's functions. For example, here's what you get when running Feather Icons through getStroke
.
Rendering
While getStroke
returns an array of points representing the outline of a stroke, it's up to you to decide how you will render these points.
The function below will turn the points returned by getStroke
into SVG path data.
const average = (a, b) => (a + b) / 2
function getSvgPathFromStroke(points, closed = true) {
const len = points.length
if (len < 4) {
return ``
}
let a = points[0]
let b = points[1]
const c = points[2]
let result = `M${a[0].toFixed(2)},${a[1].toFixed(2)} Q${b[0].toFixed(
2
)},${b[1].toFixed(2)} ${average(b[0], c[0]).toFixed(2)},${average(
b[1],
c[1]
).toFixed(2)} T`
for (let i = 2, max = len - 1; i < max; i++) {
a = points[i]
b = points[i + 1]
result += `${average(a[0], b[0]).toFixed(2)},${average(a[1], b[1]).toFixed(
2
)} `
}
if (closed) {
result += 'Z'
}
return result
}
To use this function, first run your input points through getStroke
, then pass the result to getSvgPathFromStroke
.
const outlinePoints = getStroke(inputPoints)
const pathData = getSvgPathFromStroke(outlinePoints)
You could then pass this string of SVG path data either to an SVG path element:
<path d={pathData} />
Or, if you are rendering with HTML Canvas, you can pass the string to a Path2D
constructor).
const myPath = new Path2D(pathData)
ctx.fill(myPath)
Flattening
By default, the polygon's paths include self-crossings. You may wish to remove these crossings and render a stroke as a "flattened" polygon. To do this, install the polygon-clipping
package and use the following function together with the getSvgPathFromStroke
.
import polygonClipping from 'polygon-clipping'
function getFlatSvgPathFromStroke(stroke) {
const faces = polygonClipping.union([stroke])
const d = []
faces.forEach((face) =>
face.forEach((points) => {
d.push(getSvgPathFromStroke(points))
})
)
return d.join(' ')
}
Development & Contributions
To work on this library:
- clone this repo
- run
yarn
in the folder root to install dependencies - run
yarn start
to start the local development server
The development server is located at packages/dev
. The library and its tests are located at packages/perfect-freehand
.
Pull requests are very welcome!
Community
Support
Need help? Please open an issue for support.
Discussion
Have an idea or casual question? Visit the discussion page.
License
- MIT
- ...but if you're using
perfect-freehand
in a commercial product, consider becoming a sponsor. ð°
Author
Top Related Projects
Virtual whiteboard for sketching hand-drawn like diagrams
whiteboard SDK / infinite canvas SDK
Create and animate hand-drawn annotations on a web page
Javascript Canvas Library, SVG-to-Canvas (& canvas-to-SVG) Parser
draw.io is a JavaScript, client-side editor for general diagramming.
Convert
designs to code with AI
Introducing Visual Copilot: A new AI model to turn Figma designs to high quality code using your components.
Try Visual Copilot