Security News
ESLint is Now Language-Agnostic: Linting JSON, Markdown, and Beyond
ESLint has added JSON and Markdown linting support with new officially-supported plugins, expanding its versatility beyond JavaScript.
perfect-freehand
Advanced tools
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:
[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 SVG data. While you can use getStroke
's data to render strokes with an HTML canvas (via the Path2D element) or with SVG paths, these new functions will allow you to create paths in other rendering technologies.
getStrokePoints
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)
getOutlinePoints
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)
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)
Please open an issue for support.
Have an idea or casual question? Visit the discussion page.
FAQs
Draw perfect pressure-sensitive freehand strokes.
We found that perfect-freehand demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
ESLint has added JSON and Markdown linting support with new officially-supported plugins, expanding its versatility beyond JavaScript.
Security News
Members Hub is conducting large-scale campaigns to artificially boost Discord server metrics, undermining community trust and platform integrity.
Security News
NIST has failed to meet its self-imposed deadline of clearing the NVD's backlog by the end of the fiscal year. Meanwhile, CVE's awaiting analysis have increased by 33% since June.