• Stars
    star
    3,435
  • Rank 13,010 (Top 0.3 %)
  • Language
    HTML
  • License
    MIT License
  • Created almost 4 years ago
  • Updated over 1 year ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Draw perfect pressure-sensitive freehand lines.

Screenshot

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:

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.

Screenshot

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.

Edit perfect-freehand-example

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.

Edit perfect-freehand-example

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 false 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 to 0.

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

Icons

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

More Repositories

1

perfect-arrows

Draw perfect arrows between points and shapes.
TypeScript
2,515
star
2

state-designer

State management with statecharts.
HTML
619
star
3

perfect-cursors

Perfect interpolation for multiplayer cursors.
TypeScript
494
star
4

telestrator

A disappearing drawing tool for your screen.
TypeScript
351
star
5

globs

A globs-based vector editor.
TypeScript
237
star
6

perfect-freehand-dart

Draw perfect freehand lines—in Flutter.
Dart
236
star
7

rko

A state manager with undo, redo and persistence.
TypeScript
211
star
8

gotcha

Turn your Framer prototype into its own live developer spec.
CoffeeScript
114
star
9

liquorstore

A reactive store.
TypeScript
107
star
10

kdtype

A typing game for kids.
TypeScript
71
star
11

trashly

A reactive store.
TypeScript
53
star
12

polyclip-js

A JavaScript implementation of the Greiner-Hormann clipping algorithm.
TypeScript
47
star
13

arrows-playground

A canvas-based arrows playground.
TypeScript
37
star
14

state-designer-ide

A design environment for State Designer.
TypeScript
34
star
15

framer-moreutils

Expand Utils with some handy helper functions.
JavaScript
31
star
16

framework

A general-purpose component kit for Framer.
JavaScript
30
star
17

fontloader

Painlessly, reliably load local and web fonts into Framer prototypes.
CoffeeScript
30
star
18

arena-2022

An isometric game.
TypeScript
25
star
19

finder-toolbar-shortcuts

Easy shortcuts for Finder's toolbar.
Rich Text Format
25
star
20

react-motion-asteroids

An astroids-like game in React using Framer Motion.
TypeScript
24
star
21

personal-blog

A personal blog.
TypeScript
24
star
22

figma-plugin-perfect-freehand

A Figma plugin for drawing perfect freehand strokes.
TypeScript
24
star
23

replisketch

A collaborative drawing app built with Replicache.
TypeScript
19
star
24

framer-tools

Do good stuff fast in Framer X from the command line.
JavaScript
18
star
25

framer-controller

Control a Framer X component through overrides.
TypeScript
18
star
26

framer-button

A customizable button class for Framer prototypes.
CoffeeScript
15
star
27

tetris-react-state-designer

A Tetris implementation using React and State Designer.
TypeScript
14
star
28

framer-sublime-text

A Framer UI / Color Scheme / Syntax for Sublime Text
14
star
29

brush-engine

A brush engine for the browser.
TypeScript
13
star
30

together

A multiplayer experience.
TypeScript
13
star
31

inventory-react-state-designer

An inventory system in React and State Designer.
TypeScript
12
star
32

framer-md

A Material Design UI kit for Framer.
JavaScript
12
star
33

short-story

Small, self-contained interactive component demos.
JavaScript
12
star
34

framer-icon

Create SVG icons using this very simple module.
CoffeeScript
10
star
35

react-turtle

Turtle Graphics for React.
JavaScript
10
star
36

olc_rust_sketches

Learning rust with the olc Pixel Game Engine.
Rust
10
star
37

gnrng

A minimal seeded random number generator.
TypeScript
10
star
38

ActionLayer

An ActionLayer extends Layer, adding properties and functions designed to simplify managing events in Framer.
CoffeeScript
9
star
39

react-decal

A miniature canvas game engine in React.
TypeScript
9
star
40

quick-docs

Docs, quick.
JavaScript
8
star
41

framer-layout

Layout with grids in Framer.
CoffeeScript
8
star
42

arena-game

A tactical combat game in React.
TypeScript
6
star
43

react-use-maho

A state management tool based on statecharts.
TypeScript
6
star
44

snowcraft

Snowcraft brood war by Steve Ruiz.
TypeScript
6
star
45

FocusComponent

Control events among a group of layers.
CoffeeScript
5
star
46

state-designer-examples

Created with CodeSandbox
TypeScript
5
star
47

flow-docs

Docs for the flow component
JavaScript
4
star
48

MonokaiFade

Example of the monokai.nl-style fade effect.
TypeScript
3
star
49

perfect-freehand-signature

A pressure-based vector signature component.
3
star
50

learn-docs

Docs for the Learn Design System
JavaScript
3
star
51

bendy-arrows-playground

Created with CodeSandbox
TypeScript
3
star
52

short-story-sjs

Beautiful component previews for design, docs and demos.
TypeScript
2
star
53

unstyled-challenge

Design the web with strict limits on style.
JavaScript
2
star
54

steveruizok.github.io

UI/UX Portfolio
CSS
2
star
55

react-iso-engine

Isometric world engine written in React.
TypeScript
2
star
56

docs-mdx-cms

JavaScript
2
star
57

nextjs-content-starter

A Next.js static site with MDX.
TypeScript
2
star
58

brushy-brushy

TypeScript
2
star
59

stencil-refuge

App running on Refuge Restrooms API, built in Stencil.
JavaScript
1
star
60

emoji-picker-site

A tiny site for emoji picking.
CSS
1
star
61

st-lookbook

Webcomponents for previewing other components.
TypeScript
1
star
62

stencil-router-redux-demo

An example stencil project with stencil-router and stencil-redux working together.
HTML
1
star
63

framer-atomic-tutorial

Designing with progressive complexity, step-by-step in Framer.
JavaScript
1
star
64

react-use-three

React Hooks for three.js
TypeScript
1
star
65

docs-sites

Collection of small docs sites.
JavaScript
1
star
66

ds-docs-starter

A starter for design system documentation.
JavaScript
1
star
67

component-preview

A micro storybook-like environment for previewing components.
TypeScript
1
star
68

exp-apollo-hooks-todo

Apollo's todo list example with Apollo hooks.
JavaScript
1
star
69

framer-loupe-data

Nice website for designing with data at Framer Loupe.
JavaScript
1
star