Perfect Freehand

Draw perfect pressure-sensitive freehand lines.

README

perfect-freehand


Draw perfect pressure-sensitive freehand lines.

🔗 Curious? Try out a demo.

💅 Designer? Check out the Figma Plugin.

💕 Love this library? Consider becoming a sponsor.

Also available in:
- 🐍 Python

Installation


  1. ```bash
  2. npm install perfect-freehand
  3. ```

or

  1. ```bash
  2. yarn add perfect-freehand
  3. ```

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.

  1. ```js
  2. import { getStroke } from 'perfect-freehand'

  3. const inputPoints = [
  4.   [0, 0],
  5.   [10, 5],
  6.   [20, 8],
  7.   // ...
  8. ]

  9. const outlinePoints = getStroke(inputPoints)
  10. ```

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.

  1. ```js
  2. const stroke = getStroke(myPoints, {
  3.   size: 32,
  4.   thinning: 0.7,
  5. })
  6. ```

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.

  1. ```js
  2. const inputPoints = [
  3.   [0, 0, 0.5],
  4.   [10, 5, 0.7],
  5.   [20, 8, 0.8],
  6.   // ...
  7. ]

  8. const outlinePoints = getStroke(inputPoints, {
  9.   simulatePressure: false,
  10. })
  11. ```

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).

  1. ```js
  2. const inputPoints = [
  3.   { x: 0, y: 0, pressure: 0.5 },
  4.   { x: 10, y: 5, pressure: 0.7 },
  5.   { x: 20, y: 8, pressure: 0.8 },
  6.   // ...
  7. ]

  8. const outlinePoints = getStroke(inputPoints, {
  9.   simulatePressure: false,
  10. })
  11. ```

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


  1. ```jsx
  2. import * as React from 'react'
  3. import { getStroke } from 'perfect-freehand'
  4. import { getSvgPathFromStroke } from './utils'

  5. export default function Example() {
  6.   const [points, setPoints] = React.useState([])

  7.   function handlePointerDown(e) {
  8.     e.target.setPointerCapture(e.pointerId)
  9.     setPoints([[e.pageX, e.pageY, e.pressure]])
  10.   }

  11.   function handlePointerMove(e) {
  12.     if (e.buttons !== 1) return
  13.     setPoints([...points, [e.pageX, e.pageY, e.pressure]])
  14.   }

  15.   const stroke = getStroke(points, {
  16.     size: 16,
  17.     thinning: 0.5,
  18.     smoothing: 0.5,
  19.     streamline: 0.5,
  20.   })

  21.   const pathData = getSvgPathFromStroke(stroke)

  22.   return (
  23.     <svg
  24.       onPointerDown={handlePointerDown}
  25.       onPointerMove={handlePointerMove}
  26.       style={{ touchAction: 'none' }}
  27.     >
  28.       {points && <path d={pathData} />}
  29.     </svg>
  30.   )
  31. }
  32. ```

Tip: For implementations in Typescript, see the example project included in this repository.