Giter Club home page Giter Club logo

perfect-freehand's Introduction

Screenshot

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.

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

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

perfect-freehand's People

Contributors

aaronsantiago avatar calebeby avatar gilesvangruisen avatar miles-crighton avatar mudcube avatar redemption198 avatar steveruizok avatar todepond avatar yaojingguo avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

perfect-freehand's Issues

[feature] layer / group separation

Hey this is so cool, as is your infinite canvas tool tl-draw.

This feature request, may well be ignorance on my part, but when trying out the .com example, any change I made to settings was propagated to both past and future drawings. I Think the solve is adding another layer of indirection, to allow each shape to have an optional ref to settings for that specific drawing, rather than to all drawings.

Rather than manually doing this; I'm wondering if it's a feature you've considered, or have another way to support using what-is?

syncing pointer to SVG

Really love this tool; draws beautifully!

Something I'm having trouble with is keeping the pointer events / coords synced to the SVG.

It appears I can only get an accurate pointer locationโ€”directly under the cursorโ€”if the SVG's viewBox matches its width and height in px. This can be addressed by dynamically updating the viewBox, as in this example: https://codesandbox.io/s/objective-violet-e9ck1?file=/src/App.vue.

As you can see, I do this viewBox translation on mounting the component with the setDimensions function. The remaining issue regards responsive design. If the browser is resized or I zoom outโ€”and the SVG changes size as a resultโ€”this goes out of sync again. I have tried using a resizeObserver to address this (commented out) but it means the drawing/path nolonger scales with the SVG.

Is there something about the tool that I don't understand or have missed that would make this easier? Or would I have to do something like add a viewBox-to-path-coords translation layer ๐Ÿ˜ฃ?

Thanks!

[Bug] "yarn start" fails to start

$ node --version
v18.19.0
$ yarn --version
1.22.21
$ yarn start
yarn run v1.22.21
$ lerna run start --stream --parallel
lerna notice cli v3.22.1
lerna info Executing command in 2 packages: "yarn run start"
dev: $ node ./esbuild.config.mjs --dev tsc --watch
perfect-freehand: $ node scripts/dev & tsc --watch --incremental --emitDeclarationOnly --declarationMap --outDir dist/types
dev:
dev: Serving ๐Ÿ›
dev:
dev: Local โ†’ http://localhost:5420
dev:
dev: Network โ†’ http://10.0.0.3:5420
dev:
dev:  > src/state/shapes/draw.tsx:14:26: error: Could not resolve "perfect-freehand" (mark it as external to exclude it from the bundle)
dev:     14 โ”‚ import { getStroke } from 'perfect-freehand'
dev:        โ•ต                           ~~~~~~~~~~~~~~~~~~
dev:    ../../node_modules/perfect-freehand/package.json:28:16: note: The module "./dist/esm/index.mjs" was not found on the file system
dev:     28 โ”‚       "import": "./dist/esm/index.mjs"
dev:        โ•ต                 ~~~~~~~~~~~~~~~~~~~~~~
perfect-freehand:
9:35:41 PM - Starting compilation in watch mode...
dev: error Command failed with exit code 1.
dev: info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
lerna ERR! yarn run start exited 1 in 'dev'
lerna WARN complete Waiting for 1 child process to exit. CTRL-C to exit immediately.
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.

commit 315d598

[feature] currentcolor option in color palette

Request to add option to set the color currentcolor.

Useful in website:

  • to set SVG color in ancestor mark-up
  • to allow for site theming (ie: switching from light to dark mode)

? eyedropper icon?

Get points for the "center" line?

Hi! I'm trying to use this library with three.js and was wondering: is there a way to get points for the "center" (I'm assuming) the library uses to generate the ouline of the stroke? I'm of course not talking about the points provided as an input but the points of the line after smoothing and streamlining are applied.

IMG_AE20CCFBCB5E-1

While it's possible in three.js to generate a 3D mesh in the same way you generate a SVG shape, it would be convenient for a lot of cases and approaches to also have the points of the "centeral" line, without width and miters.

Thanks!

[feature] Supporting intermediate control points

Hi @steveruizok!

Firstly, thanks for your amazing work on the perfect-freehand itself and on tldraw; โ€“ย it's been exciting to see and try with all the recent AI features.

Just wanted to ask if you'd be interested in accepting and reviewing a feature PR? We've been using perfect-freehand for a while, and it looks like we might have an interesting use case where we may need the smoothed and streamlined stroke (generated by getStroke()) to follow a set of intermediate control points. Technically, you've already added support for using the end points of the stroke as control points (the option called last) โ€“ย would you be interested in your library supporting intermediate control points as well?

[Bug] The Troke drawing doesn't look good in the html canvas

signaturecanvas
I have an option for getStroke but the resulting stroke doesn't feel smooth.

const options = {
  size: 3,
  thinning: 0.2,
  smoothing: 1,
  streamline: 0.1,
  easing: (t) => t,
  start: {
    taper: 0,
    easing: (t) => t,
    cap: true,
  },
  end: {
    taper: 10,
    easing: (t) => t,
    cap: true,
  },
};

My canvas have
width={440} ; height={220}

Can you help me make it more beautiful?

getStroke cannot handle a single input point if there's no tapering or it's the last point

Firstly, thanks for the amazing library Steve!

I've noticed an issue with the latest release where a single point will result in a stroke of NaNs if either the taper is 0 or the last parameter is false. E.g:

const stroke = getStroke([ [1, 1, 0] ], {
        size: 1,
        thinning: 0.6,
        smoothing: 0.5,
        streamline: 0.5,
        start: {
            taper: 0,
        },
        end: {
            taper: 0,
        },
        simulatePressure: true,
        last: false,
});
// [ [ NaN, NaN ], [ NaN, NaN ],  [ NaN, NaN ], [ NaN, NaN ],  [ NaN, NaN ], [ NaN, NaN ],  [ NaN, NaN ], [ NaN, NaN ],  [ NaN, NaN ], [ NaN, NaN ],  [ NaN, NaN ]]

This means that single clicks don't create a stroke.
I've tested this in the example app and while it works in your hosted version it doesn't seem to work locally, so I assume the hosted version is on a previous library version?

As far as I can gather it's due to the following line - I guess it's trying to take a unit vector of a point with 0 length, but I'm not really sure what the fix should be:

vec.per(vec.uni(vec.vec(lastPoint.point, firstPoint.point))),

How to draw continuous lines?

First of all, very awesome library! Thank you for creating this.

So this isn't really an issue but just want to ask if there's an easy way to do this.

I've tried the React example and it works perfectly!

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.preventDefault()
    setPoints([[e.pageX, e.pageY, e.pressure]])
  }

  function handlePointerMove(e) {
    if (e.buttons === 1) {
      e.preventDefault()
      setPoints([...points, [e.pageX, e.pageY, e.pressure]])
    }
  }

  return (
    <svg
      onPointerDown={handlePointerDown}
      onPointerMove={handlePointerMove}
      style={{ touchAction: "none" }}
    >
      {points && (
        <path
          d={getSvgPathFromStroke(
            getStroke(points)
          )}
        />
      )}
    </svg>
  )
}

But this one clears the previous line because there is only one path right? I hope there's an easy example of making it continuous, like in your demo. Thank you!

Pointer events cancelled by touch-action on Chrome

Pointer events (e.g. pointermove) are being cancelled by a browser's touch action. This breaks Chrome on Android (or desktop Chrome with touch simulation enabled).

An easy fix is to put the following property to the svg element:

touch-action: none;

Before

image

After

image

Updating from 1.0.4 to 1.0.16 changed the appearance of the output significantly

Did something change in the algorithm? I bumped the package today to fix a bug (1.0.4 was generating invalid polygons in certain case) โ€” good news is the bug is gone, but the shape has changed significantly with the same settings.

Any insight on what changed?

1.0.4:
image

1.0.16:
image

getStroke(coords)

[
  [-696, 1059],
  [-696, 1059],
  [-674, 857],
  [-607, 520],
  [-562, 116],
  [-472, -289],
  [-427, -603],
  [-404, -626],
  [-404, -536],
  [-404, -266],
  [-427, -64],
  [-427, -42],
  [-337, -177],
  [-157, -469],
  [23, -738],
  [247, -1008],
  [382, -1143],
  [405, -1165],
  [405, -1120],
  [405, -985],
  [427, -626],
  [427, -469],
  [427, -424],
  [472, -491],
  [539, -603],
  [697, -761],
  [809, -918],
  [854, -963],
  [877, -963],
  [877, -918],
  [921, -693],
  [944, -379],
  [989, -132],
  [989, 71],
  [989, 205],
  [989, 228]
];

settings:

{
              smoothing: 1,
              streamline: 1,
              thinning: 0,
              start: {
                cap: true,
                taper: 0
              },
              size: 235.883074766576
            }

Dotting my i's and j's sometimes doesn't work

First, awesome library (again!).

I was enjoying myself, swirling and curving away, when suddenly I noticed a few of my i's lost their dots.

macOS Mac Mini M1
Chrome
Drawing with a mouse

Here's a screen cap:

j.j.j.j.jjji.mov

Manually creating paths?

I'm trying to figure out how to use this library to create lines from some simple points that I generate algorithmically. However, it doesn't seem to deal well with minimal points that aren't curves.

example:

 const strokes = getStroke(
        [
          { x: 10, y: 10, pressure: 0.5 },
          { x: 10, y: 86, pressure: 0.5 },
          { x: 62, y: 86, pressure: 0.5 },
          { x: 62, y: 10, pressure: 0.5 },
        ],
        {
          size: 4,
          thinning: 0,
          smoothing: 0,
          streamline: 0,
          simulatePressure: false,
          start: {
            cap: false,
            taper: 0,
          },
          end: {
            cap: false,
            taper: 0,
          },
        }
      );

I would expect a U shape, however what appears is a sharp V shape. ๐Ÿค”

[Bug] Explicit ESM declaration

This module is being used by Drauu which has an integration with VueUse and can be used with Nuxt (Vue). Since Nuxt 3 uses Vite as a bundler by default and it only allows the use of ESM modules, when it tries to bundle perfect-freehand the following error occurs:

import { getStroke } from "perfect-freehand";
^^^^^^^^^
SyntaxError: Named export 'getStroke' not found. The requested module 'perfect-freehand' is a CommonJS module, which may not support all module.exports as named exports.
CommonJS modules can always be imported via the default export, for example using:

import pkg from 'perfect-freehand';
const { getStroke } = pkg;

I was able to "fix" it by adding the following lines into the package.json inside node_modules:

"type": "module",
"exports": {
".": {
"types": "./dist/types/index.d.ts",
"import": "./dist/esm/index.js"
}
}

I'm not sure if this may cause some problems that's why I'm opening an Issue

Auto-write files to G-Drive / iCloud

Saving to local keeps files safe.
But today I just lost an entire project because my computer didn't throw the native UI to save a document.

Saving in real time to iCloud / G-Drive / Dropbox etc. could be extremely valuable to not lose work progress.

Suggestion: clear selection

As a user, I'd like to be able to drag-and-select an area on the canvas and delete a certain path. Motivation: Sometimes, the path I want to delete is too far back in history. I don't want to undo other work, but I also don't want to clear everything.

Maybe there could be a select tool/mode that users can toggle. In this mode, drawing is disabled, and dragging the mouse instead selects an area on the screen. The individual selection can then be deleted (I suppose there would need to be a separate icon for this to avoid confusion with the clear-all button).

By the way, thanks for making this excellent tool :)

[feature] Ability to reset drawing settings?

It would be useful if there was a reset function that resets my customised drawing settings back to the default.

For example, if I arbitrarily decrease the size, increase the thickness, increase the streamline, and change the easing to something else, I want the ability to reset these settings back to the default in one shot, rather than having to manually adjust them back one-by-one.

Thanks!

Suggestion: Skip duplicate points

getStroke() returns array withNaN values If first two points are same (mousedown+mousemove) which apparently happens frequently (1 out of 5). I don't know if it's just at the beginning of a stroke or at any two consecutive points in the stroke.

While I've already worked around the problem by ignoring new point if it's same as previous, I thought you may want to add the logic internally to avoid future issues as it can result in misleading behaviors.

BTW, thanks for sharing this wonderful library. It's great.

change color of lines

All of my lines in my canvas change color when i change for one line.
this is how i set color for line. in my uselayoutEffect i add this line :
`
context.fillStyle = lineColor;

`
linecolor is a state that i get of user .

[Bug] Crashing on ios 12

Hi,
This project doesn't seem to work on ios 12... many peoples still using this browser version. What is the current global compatibility ?
Thanks

[Bug] Sveltekit throws "Syntax Error: Unexpected token 'export'"

I've been getting the error below in my Sveltekit project:

SyntaxError: Unexpected token 'export'
    at internalCompileFunction (node:internal/vm:73:18)
    at wrapSafe (node:internal/modules/cjs/loader:1153:20)
    at Module._compile (node:internal/modules/cjs/loader:1205:27)
    at Module._extensions..js (node:internal/modules/cjs/loader:1295:10)
    at Module.load (node:internal/modules/cjs/loader:1091:32)
    at Module._load (node:internal/modules/cjs/loader:938:12)
    at cjsLoader (node:internal/modules/esm/translators:283:17)
    at ModuleWrap.<anonymous> (node:internal/modules/esm/translators:233:7)
    at ModuleJob.run (node:internal/modules/esm/module_job:217:25)
    at async ModuleLoader.import (node:internal/modules/esm/loader:308:24)

The only way I could seem to make it work is by adding another entry to the package.json file for svelte:

  "exports": {
    ".": {
      "types": "./dist/types/index.d.ts",
      "require": "./dist/cjs/index.js",
      "import": "./dist/esm/index.js",
      "svelte": "./dist/esm/index.js"
    }

I took some hints from one of Captain Codeman's Svelte-Signature-pad's commit here..

Potential Bug Risks and Anti-Patterns

Description

Hi @steveruizok ๐Ÿ‘‹

I ran DeepSource Static Code Analysis upon the Project, the results for which are available here.

The Static Code Analysis Tool found potential bugs and anti-patterns in the Code, that can be detrimental at a later point in time with respect to the Project. DeepSource helps you to automatically find and fix issues in your code during code reviews. This tool looks for anti-patterns, bug risks, performance problems, and raises issues.

Some of the notable issues are:

  • Prefer ternary operator here
  • Found non-null assertions here
  • Detected unused variables here
  • Detected empty functions here
  • Found weak hashing functions here

There are plenty of other issues in relation to Bug Discovery and Anti-Patterns which you would be interested to take a look at.

If you would like to integrate DeepSource to autofix some of the commonly occurring issues, I can help you set that up :)

[Bug] When drawing to a canvas, fast circles result in frayed lines

Hello,

Thanks for your work on this amazing library. I want to use it in combination with a canvas but I have the following phenomenon

video_2021-10-22_11-16-48.mp4

As you can see, the first straight and slow lines work perfectly, but as soon as I start drawing quick circles the lines start to fray. Is there anything I can do to circumvent that? Let me know if you need any additional informations.

[documentation] Documentation of "last" parameter default doesn't match code

In the current version of the README, the last option is documented as having a default of true:

Property Type Default Description
last boolean true Whether the stroke is complete.

However, in the code - getStrokeOutlinePoints and getStrokePoints - the actual default of the last option (assigned to the isComplete variable) is set to false.

I'm not sure what the intention is here, but to preserve the behaviour of the library it might be best to leave the default as false and update the documentation to match.

[Bug] Jaggy lines not smoothed

Really cool library! I'm having trouble with the quality of the smoothing:

jaggy.mov

do you have any idea what could be causing this type of jaggyness?

options:

const stroke = getStroke(pixelCoords, {
      smoothing: 0.99,
      streamline: 0.99,
      size: Math.max(24 * scale, 1),
    });

input:

[{"x":424,"y":350},{"x":424,"y":350},{"x":425,"y":349},{"x":426,"y":348},{"x":427,"y":347},{"x":428,"y":347},{"x":430,"y":346},{"x":431,"y":346},{"x":432,"y":345},{"x":433,"y":345},{"x":435,"y":344},{"x":436,"y":344},{"x":437,"y":344},{"x":438,"y":344},{"x":439,"y":344},{"x":439,"y":344},{"x":440,"y":344},{"x":440,"y":344},{"x":441,"y":344},{"x":442,"y":344},{"x":443,"y":344},{"x":445,"y":344},{"x":450,"y":346},{"x":453,"y":346},{"x":458,"y":348},{"x":461,"y":348},{"x":463,"y":348},{"x":469,"y":349},{"x":475,"y":350},{"x":478,"y":350},{"x":480,"y":351},{"x":483,"y":351},{"x":485,"y":352},{"x":490,"y":353},{"x":491,"y":354},{"x":493,"y":355},{"x":495,"y":355},{"x":496,"y":356},{"x":497,"y":356},{"x":497,"y":356},{"x":497,"y":357},{"x":497,"y":357},{"x":498,"y":357},{"x":498,"y":357},{"x":498,"y":357},{"x":498,"y":358},{"x":498,"y":358},{"x":499,"y":358},{"x":499,"y":359},{"x":500,"y":359},{"x":500,"y":360},{"x":500,"y":361},{"x":501,"y":362},{"x":501,"y":363},{"x":502,"y":364},{"x":502,"y":365},{"x":503,"y":367},{"x":503,"y":368},{"x":503,"y":370},{"x":503,"y":371},{"x":503,"y":372},{"x":503,"y":372},{"x":503,"y":373},{"x":503,"y":374},{"x":503,"y":375},{"x":503,"y":376},{"x":503,"y":377},{"x":502,"y":378},{"x":502,"y":379},{"x":501,"y":380},{"x":500,"y":380},{"x":500,"y":381},{"x":500,"y":382},{"x":500,"y":382},{"x":499,"y":382},{"x":499,"y":382},{"x":498,"y":383},{"x":498,"y":383},{"x":497,"y":383},{"x":495,"y":384},{"x":495,"y":384},{"x":494,"y":384},{"x":493,"y":384},{"x":493,"y":384},{"x":493,"y":384},{"x":493,"y":384},{"x":492,"y":384},{"x":492,"y":384},{"x":491,"y":384},{"x":490,"y":384},{"x":488,"y":384},{"x":487,"y":384},{"x":487,"y":384},{"x":486,"y":384},{"x":485,"y":384},{"x":484,"y":384},{"x":484,"y":385},{"x":483,"y":385},{"x":482,"y":385},{"x":481,"y":385},{"x":478,"y":386},{"x":474,"y":386},{"x":470,"y":386},{"x":467,"y":386},{"x":464,"y":386},{"x":461,"y":386},{"x":458,"y":386},{"x":454,"y":386},{"x":452,"y":386},{"x":451,"y":386},{"x":449,"y":386},{"x":448,"y":386},{"x":447,"y":386},{"x":447,"y":386},{"x":446,"y":386},{"x":446,"y":386},{"x":445,"y":386},{"x":445,"y":386},{"x":445,"y":386},{"x":445,"y":387},{"x":445,"y":387},{"x":445,"y":387},{"x":445,"y":387},{"x":443,"y":389},{"x":440,"y":391},{"x":439,"y":392},{"x":438,"y":394},{"x":437,"y":395},{"x":436,"y":397},{"x":435,"y":400},{"x":434,"y":401},{"x":433,"y":402},{"x":433,"y":404},{"x":433,"y":406},{"x":433,"y":407},{"x":433,"y":408},{"x":433,"y":409},{"x":433,"y":410},{"x":433,"y":410},{"x":433,"y":410},{"x":433,"y":411},{"x":433,"y":411},{"x":433,"y":411},{"x":433,"y":411},{"x":433,"y":411},{"x":434,"y":411},{"x":434,"y":411},{"x":434,"y":411},{"x":434,"y":411},{"x":434,"y":411},{"x":434,"y":411},{"x":434,"y":411},{"x":434,"y":411},{"x":434,"y":411},{"x":434,"y":411},{"x":434,"y":411},{"x":435,"y":411},{"x":435,"y":411},{"x":435,"y":411},{"x":435,"y":411},{"x":435,"y":411},{"x":435,"y":411},{"x":435,"y":411},{"x":435,"y":410},{"x":435,"y":410},{"x":436,"y":410},{"x":436,"y":410},{"x":436,"y":410},{"x":436,"y":410},{"x":436,"y":410},{"x":437,"y":409},{"x":437,"y":409},{"x":437,"y":409},{"x":437,"y":409},{"x":437,"y":409},{"x":437,"y":409},{"x":437,"y":409},{"x":438,"y":409},{"x":438,"y":409},{"x":438,"y":409},{"x":438,"y":409},{"x":438,"y":409},{"x":438,"y":408},{"x":439,"y":408},{"x":439,"y":408},{"x":439,"y":408},{"x":439,"y":408},{"x":440,"y":408},{"x":440,"y":407},{"x":441,"y":407},{"x":441,"y":407},{"x":442,"y":407},{"x":442,"y":406},{"x":442,"y":406},{"x":442,"y":406},{"x":443,"y":406},{"x":443,"y":406},{"x":443,"y":406},{"x":443,"y":406},{"x":443,"y":406},{"x":444,"y":406},{"x":444,"y":406},{"x":444,"y":406},{"x":444,"y":406},{"x":445,"y":406},{"x":445,"y":406},{"x":446,"y":406},{"x":447,"y":405},{"x":448,"y":405},{"x":449,"y":405},{"x":450,"y":405},{"x":451,"y":405},{"x":452,"y":405},{"x":454,"y":405},{"x":455,"y":405},{"x":456,"y":405},{"x":456,"y":405},{"x":456,"y":404},{"x":456,"y":404},{"x":457,"y":404},{"x":457,"y":404},{"x":457,"y":404},{"x":457,"y":404},{"x":458,"y":404},{"x":458,"y":404},{"x":458,"y":404},{"x":458,"y":404},{"x":458,"y":404},{"x":459,"y":404},{"x":459,"y":404},{"x":459,"y":404},{"x":459,"y":404},{"x":459,"y":404},{"x":459,"y":404},{"x":459,"y":404},{"x":460,"y":404},{"x":460,"y":404},{"x":460,"y":404},{"x":460,"y":404},{"x":460,"y":404},{"x":460,"y":405},{"x":460,"y":405},{"x":460,"y":405},{"x":460,"y":405},{"x":460,"y":405},{"x":460,"y":405},{"x":460,"y":405},{"x":460,"y":405},{"x":460,"y":406},{"x":460,"y":406},{"x":460,"y":406},{"x":460,"y":406},{"x":460,"y":406},{"x":460,"y":406},{"x":460,"y":406},{"x":460,"y":406},{"x":460,"y":406},{"x":460,"y":406},{"x":460,"y":406},{"x":460,"y":406},{"x":460,"y":407},{"x":460,"y":407},{"x":460,"y":407},{"x":460,"y":407},{"x":460,"y":407},{"x":460,"y":408},{"x":460,"y":408},{"x":460,"y":408},{"x":460,"y":408},{"x":459,"y":408},{"x":459,"y":408},{"x":459,"y":408},{"x":459,"y":408},{"x":459,"y":408},{"x":459,"y":408},{"x":459,"y":408},{"x":459,"y":408},{"x":459,"y":408},{"x":459,"y":408},{"x":459,"y":408},{"x":459,"y":408},{"x":459,"y":409},{"x":459,"y":409},{"x":459,"y":409},{"x":459,"y":409},{"x":459,"y":409},{"x":459,"y":409},{"x":459,"y":410},{"x":459,"y":410},{"x":459,"y":410},{"x":459,"y":410},{"x":459,"y":410},{"x":459,"y":410},{"x":459,"y":410},{"x":459,"y":410},{"x":459,"y":410},{"x":459,"y":410},{"x":459,"y":411},{"x":459,"y":411},{"x":459,"y":411},{"x":459,"y":411},{"x":459,"y":411},{"x":459,"y":411},{"x":459,"y":411},{"x":459,"y":411},{"x":459,"y":411},{"x":459,"y":412},{"x":459,"y":412},{"x":459,"y":412},{"x":459,"y":412},{"x":459,"y":412},{"x":459,"y":412},{"x":459,"y":412},{"x":459,"y":412},{"x":459,"y":413},{"x":459,"y":413},{"x":459,"y":413},{"x":459,"y":413},{"x":459,"y":414},{"x":459,"y":414},{"x":459,"y":415},{"x":459,"y":415},{"x":459,"y":415},{"x":459,"y":415},{"x":459,"y":415},{"x":459,"y":416},{"x":459,"y":416},{"x":459,"y":416},{"x":459,"y":416},{"x":459,"y":416},{"x":459,"y":416},{"x":459,"y":416},{"x":459,"y":416},{"x":459,"y":416},{"x":459,"y":416},{"x":459,"y":416},{"x":459,"y":417},{"x":459,"y":417},{"x":459,"y":417},{"x":459,"y":417},{"x":459,"y":417},{"x":459,"y":417},{"x":460,"y":417},{"x":460,"y":417},{"x":460,"y":418},{"x":460,"y":418},{"x":460,"y":418},{"x":460,"y":418},{"x":460,"y":418},{"x":461,"y":418},{"x":461,"y":418},{"x":461,"y":418},{"x":461,"y":418},{"x":461,"y":418},{"x":461,"y":418},{"x":461,"y":418},{"x":461,"y":418},{"x":462,"y":418},{"x":462,"y":418},{"x":463,"y":418},{"x":463,"y":418},{"x":463,"y":418},{"x":463,"y":418},{"x":464,"y":418},{"x":464,"y":418},{"x":465,"y":418},{"x":465,"y":418},{"x":467,"y":417},{"x":468,"y":417},{"x":470,"y":416},{"x":471,"y":415},{"x":473,"y":415},{"x":474,"y":414},{"x":476,"y":414},{"x":477,"y":413},{"x":479,"y":412},{"x":481,"y":411},{"x":483,"y":410},{"x":484,"y":409},{"x":484,"y":409},{"x":484,"y":408},{"x":485,"y":408},{"x":486,"y":407},{"x":487,"y":406},{"x":488,"y":405},{"x":488,"y":404},{"x":489,"y":404},{"x":490,"y":403},{"x":490,"y":403},{"x":490,"y":402},{"x":491,"y":401},{"x":493,"y":400},{"x":493,"y":400},{"x":495,"y":398},{"x":496,"y":397},{"x":499,"y":395},{"x":502,"y":393},{"x":506,"y":393},{"x":510,"y":392},{"x":515,"y":392},{"x":520,"y":392},{"x":525,"y":392},{"x":529,"y":392},{"x":532,"y":393},{"x":535,"y":393},{"x":536,"y":395},{"x":536,"y":399},{"x":535,"y":403},{"x":533,"y":407},{"x":531,"y":410},{"x":529,"y":412},{"x":527,"y":415},{"x":525,"y":416},{"x":523,"y":417},{"x":521,"y":417},{"x":517,"y":418},{"x":515,"y":418},{"x":512,"y":418},{"x":508,"y":419},{"x":504,"y":420},{"x":497,"y":422},{"x":493,"y":424},{"x":482,"y":429},{"x":475,"y":433},{"x":468,"y":437},{"x":464,"y":440},{"x":462,"y":441},{"x":462,"y":441},{"x":462,"y":441},{"x":463,"y":442},{"x":465,"y":446},{"x":467,"y":447},{"x":468,"y":448},{"x":471,"y":451},{"x":473,"y":452},{"x":474,"y":452},{"x":474,"y":451},{"x":475,"y":451},{"x":476,"y":450},{"x":476,"y":449},{"x":476,"y":448},{"x":477,"y":448},{"x":477,"y":448},{"x":477,"y":448},{"x":477,"y":448},{"x":477,"y":447},{"x":477,"y":447},{"x":477,"y":446},{"x":477,"y":445},{"x":477,"y":445},{"x":477,"y":445},{"x":477,"y":444},{"x":477,"y":444},{"x":477,"y":444},{"x":478,"y":444},{"x":478,"y":444},{"x":478,"y":444},{"x":478,"y":444},{"x":478,"y":443},{"x":479,"y":443},{"x":479,"y":443},{"x":479,"y":442},{"x":480,"y":442},{"x":480,"y":442},{"x":481,"y":441},{"x":483,"y":441},{"x":485,"y":440},{"x":486,"y":439},{"x":488,"y":438},{"x":489,"y":438},{"x":490,"y":438},{"x":491,"y":438},{"x":492,"y":437},{"x":494,"y":437},{"x":496,"y":437},{"x":497,"y":437},{"x":497,"y":437},{"x":497,"y":437},{"x":499,"y":437},{"x":501,"y":438},{"x":504,"y":438},{"x":507,"y":438},{"x":509,"y":438},{"x":510,"y":438},{"x":511,"y":438},{"x":513,"y":437},{"x":515,"y":437},{"x":518,"y":435},{"x":521,"y":431},{"x":523,"y":428},{"x":525,"y":426},{"x":530,"y":421},{"x":536,"y":415},{"x":539,"y":411},{"x":542,"y":407},{"x":545,"y":404},{"x":546,"y":401},{"x":547,"y":399},{"x":546,"y":399},{"x":546,"y":397},{"x":546,"y":393},{"x":546,"y":388},{"x":546,"y":376},{"x":545,"y":370},{"x":545,"y":364},{"x":544,"y":359},{"x":544,"y":353},{"x":544,"y":347},{"x":544,"y":344},{"x":544,"y":343},{"x":544,"y":343}]

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.