perfect-freehand

repository·main·Indexed 26 days ago

https://github.com/steveruizok/perfect-freehand

A library for generating smooth, pressure-sensitive freehand stroke outlines. It converts input points into a polygon suitable for rendering via SVG, Canvas, or other technologies. Key features include the getStroke function for outline generation, options for thinning, smoothing, and tapering, and support for both simulated and real stylus pressure.

Tokens
3K
Snippets
10
Records
16
Agent score
88%

What's inside perfect-freehand

  1. Convert stroke points to SVG path data

    main

    The getStroke function returns an array of points representing the outline of a stroke. To render these points in an SVG, you can use a helper function to convert the points into an SVG path data string. This string can then be used in an <path d={pathData} /> element.

    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
    }
    
    // Usage:
    const outlinePoints = getStroke(inputPoints)
    const pathData = getSvgPathFromStroke(outlinePoints)
  2. Flatten a stroke polygon

    main

    By default, the polygon paths returned by the library may include self-crossings. To remove these and render a 'flattened' polygon, use the polygon-clipping package to perform a union operation on the stroke points.

    import polygonClipping from 'polygon-clipping'
    
    function getFlatSvgPathFromStroke(stroke) {
      const faces = polygonClipping.union([stroke])n
      const d = []
    
      faces.forEach((face) =>
        face.forEach((points) => {
          d.push(getSvgPathFromStroke(points))
        })
      )
    
      return d.join(' ')
    }
  3. Set up the local development environment

    main

    To contribute to or work on the library locally, follow these steps:

    1. Clone the repository.
    2. Run yarn in the folder root to install dependencies.
    3. Run yarn start to start the local development server.

    Note: The development server is located in packages/dev, while the library and its tests are in packages/perfect-freehand.

    yarn
    yarn start
  4. Configure getStroke options

    main

    You can customize the stroke appearance using a StrokeOptions object passed as the second argument to getStroke.

    Main Options:

    • size (number, default: 8): The base diameter of the stroke.
    • thinning (number, default: .5): The effect of pressure on size. Set to 0 for a steady line, or a negative number to make the stroke thinner with pressure.
    • smoothing (number, default: .5): How much to soften edges.
    • streamline (number, default: .5): How much to streamline the stroke.
    • simulatePressure (boolean, default: true): Whether to simulate pressure based on velocity.
    • easing (function, default: t => t): An easing function applied to each point's pressure.
    • last (boolean, default: true): If true, the end is drawn at the last input point.
    • start / end (object): Tapering options for the start or end of the line.

    Tapering Options (start and end):

    • cap (boolean, default: true): Whether to draw a cap. (Note: cap has no effect if taper is > 0).
    • taper (number | boolean, default: 0): The distance to taper. If true, the taper covers the total length of the stroke.
    • easing (function, default: t => t): Easing function for the tapering effect.
    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,
      },
    })
  5. Use getStrokePoints and getStrokeOutlinePoints for advanced usage

    main

    For more control, you can use the internal functions that getStroke uses.

    1. getStrokePoints: Accepts input points and returns an array of objects containing { point, pressure, vector, distance, runningLength }.
    2. getStrokeOutlinePoints: Accepts the output from getStrokePoints and returns the final [x, y] outline points.

    This is useful if you need to access intermediate data like the path's total length (runningLength).

    import { getStrokePoints, getStrokeOutlinePoints } from 'perfect-freehand'
    import samplePoints from "./samplePoints.json"
    
    const strokePoints = getStrokePoints(samplePoints)
    const outlinePoints = getStrokeOutlinePoints(strokePoints)
  6. Use the getStroke function

    main

    The getStroke function generates an array of outline points for a polygon based on an array of input points. These points can be provided as an array of arrays [x, y, pressure] or an array of objects { x, y, pressure }.

    By default, pressure is simulated based on velocity. To use real pressure from a stylus, provide the pressure as the third value in the point array and set simulatePressure: false in the options.

    import { getStroke } from 'perfect-freehand'
    
    // Using array of arrays with simulated pressure
    const inputPoints = [
      [0, 0],
      [10, 5],
      [20, 8],
    ]
    const outlinePoints = getStroke(inputPoints)
    
    // Using array of arrays with real pressure
    const realPressurePoints = [
      [0, 0, 0.5],
      [10, 5, 0.7],
      [20, 8, 0.8],
    ]
    const outlinePointsReal = getStroke(realPressurePoints, {
      simulatePressure: false,
    })
    
    // Using array of objects
    const objectPoints = [
      { x: 0, y: 0, pressure: 0.5 },
      { x: 10, y: 5, pressure: 0.7 },
      { x: 20, y: 8, pressure: 0.8 },
    ]
    const outlinePointsObj = getStroke(objectPoints, {
      simulatePressure: false,
    })
  7. Generate stroke outline points with getStroke

    main

    The getStroke function is the primary API for generating an array of points that describe a polygon surrounding a set of input points. This polygon can be used to render pressure-sensitive, smoothed strokes.

    Input points can be provided as an array of arrays [x, y, pressure] or an array of objects { x, y, pressure }. The pressure property is optional.

    Options

    OptionTypeDescription
    sizenumberThe base size (diameter) of the stroke.
    thinningnumberThe effect of pressure on the stroke's size.
    smoothingnumberHow much to soften the stroke's edges.
    easingfunctionAn easing function to apply to each point's pressure.
    simulatePressurebooleanWhether to simulate pressure based on velocity.
    startobjectCap, taper, and easing for the start of the line.
    endobjectCap, taper, and easing for the end of the line.
    lastbooleanWhether to handle the points as a completed stroke.
  8. Configure stroke appearance with StrokeOptions

    main

    The StrokeOptions interface defines how a stroke is rendered. You can pass this object to getStroke or getStrokePoints to control size, pressure effects, smoothing, and line endings.

    Key properties include:

    • size: The base diameter of the stroke.
    • thinning: The effect of pressure on the stroke's size.
    • smoothing: How much to soften the stroke's edges.
    • streamline: Smoothing factor for the path.
    • easing: A function (pressure: number) => number to apply to each point's pressure.
    • simulatePressure: If true, simulates pressure based on velocity.
    • start / end: Configuration for the beginning and end of the line, supporting cap (boolean), taper (number or boolean), and easing (function (distance: number) => number).
    • last: If true, treats the points as a completed stroke.
  9. Use the StrokeOptions type

    main

    If you are using TypeScript, you can import the StrokeOptions type to define your configuration object outside of the getStroke call.

    import { StrokeOptions, getStroke } from 'perfect-freehand'
    
    const options: StrokeOptions = {
      size: 16,
    }
    
    const stroke = getStroke(myPoints, options)