Draw perfect pressure-sensitive freehand lines.
đź”— Try out a demo.
đź’° Using this library in a commercial product? Consider becoming a sponsor.
npm install perfect-freehand
or
yarn add perfect-freehand
This package exports a function named getStroke
that:
- accepts an array of points and an (optional) options object
- returns a stroke outline as an array of points formatted as
[x, y]
import { getStroke } from 'perfect-freehand'
You may format your input points as array or an object. In both cases, the value for pressure is optional (it will default to .5
).
getStroke([
[0, 0, 0],
[10, 5, 0.5],
[20, 8, 0.3],
])
getStroke([
{ x: 0, y: 0, pressure: 0 },
{ x: 10, y: 5, pressure: 0.5 },
{ x: 20, y: 8, pressure: 0.3 },
])
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 | 0 | The distance to taper. |
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.
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.
function getSvgPathFromStroke(stroke) {
if (!stroke.length) return ''
const d = stroke.reduce(
(acc, [x0, y0], i, arr) => {
const [x1, y1] = arr[(i + 1) % arr.length]
acc.push(x0, y0, (x0 + x1) / 2, (y0 + y1) / 2)
return acc
},
['M', ...stroke[0], 'Q']
)
d.push('Z')
return d.join(' ')
}
To use this function, first use perfect-freehand to turn your input points into a stroke outline, then pass the result to getSvgPathFromStroke
.
import { getStroke } from 'perfect-freehand'
const myStroke = getStroke(myInputPoints)
const pathData = getSvgPathFromStroke(myStroke)
You could then pass this string either to an SVG path element:
<path d={pathData} />
Or, if you are rendering with HTML Canvas, you can pass the result to a Path2D
constructor).
const myPath = new Path2D(pathData)
ctx.fill(myPath)
To render a stroke as a "flattened" polygon, add the polygon-clipping
package and use the following function together with the getSvgPathFromStroke
.
import polygonClipping from 'polygon-clipping'
function getFlatSvgPathFromStroke(stroke) {
const poly = polygonClipping.union([stroke])
const d = []
for (let face of poly) {
for (let points of face) {
d.push(getSvgPathFromStroke(points))
}
}
return d.join(' ')
}
Tip: For implementations in Typescript, see the example project included in this repository.
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>
)
}
For advanced usage, the library also exports smaller functions that getStroke
uses to generate its outline points.
A function that accepts an array of points (formatted either as [x, y, pressure]
or { x: number, y: number, pressure: number}
) and a streamline value. 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)
A function that accepts an array of points (formatted as { point, pressure, vector, distance, runningLength }
, i.e. the output of getStrokePoints
) 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)
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)
Please open an issue for support.
Have an idea or casual question? Visit the discussion page.