Guide

Installation

npm i @baku89/pave

Basic usage

Simple Example

As it supports ES modules, you can load it using the import statement. Symbols such as Path or CubicBezier can be used both as types and as modules (namespaces) consisting of functions related to those types.

import {Path} from '@baku89/pave'

const circle = Path.circle([0, 0], 100)

// For SVG's path element
const d = Path.toSVG(circle)
svgPathElement.setAttribute('d', d)

// For Canvas API
const path2d = Path.toPath2D(circle)
context.stroke(path2d)

Immutability

Pave is functional programming-oriented library and all data is plain and immutable. Information associated with a path, such as the length of the path or its bounding box, is obtained using functions instead of accessing it as a property of the path data itself.

const length = Path.length(rect)
const bounds = Path.bounds(rect)
const normal = Path.normalAtTime(rect, 0.5)

These functions are appropriately memoized, so even if called multiple times for the same path, not all calculations are re-executed. However, if you make destructive changes to the path data and then call these functions, you may not get the correct results.

Therefore, when you want to perform procedural operations such as modifying path data or adding new vertices to the path, you will take one of the following three methods:

  1. Use utility functions that always generate new path data (such as Path.moveTo and Path.lineTo similar to the Canvas API)
  2. Use Path.pen to draw path data in order of vertices
  3. Use a library for manipulating immutable data structures such as immeropen in new window
// 1. Use utility functions
let p = Path.moveTo(Path.empty, [10, 10])
p = Path.lineTo(p, [20, 20])
p = Path.cubicBezierTo(p, [80, 30], [0, 40], [50, 50])
p = Path.closePath(p)

// 2. Use Path.pen()
const p = Path.pen()
	.moveTo([10, 10])
	.lineTo([20, 20])
	.cubicBezierTo([80, 30], [0, 40], [50, 50])
	.close()
	.get()

// 3. Example using immer
import {produce} from 'immer'

const pathA = Path.arc([50, 50], 40, 0, 90)
const pathB = produce(pathA, draft => {
	draft.curves[0].closed = true
})

Or use a library for manipulating immutable data structures such as immeropen in new window:

import {produce} from 'immer'

const pathA = Path.arc([50, 50], 40, 0, 180)
const pathB = produce(pathA, draft => {
	draft.curves[0].closed = true
})

Vector and Transform

In Pave, vectors and matrices are represented as plain 1D arrays of numbers. For example, a position is [x, y], and a 2D affine transformation is [a, b, c, d, tx, ty]. These data can be manipulated using libraries such as Linearlyopen in new window or gl-matrixopen in new window, but the latter allows mutable value changes, so it is recommended to use Linearly, which is designed to work with immutable data like Pave.

Angle

In Pave, angles are represented in degrees. JavaScript’s standard Math and Canvas2DRenderingContext use radians, so if you need to use those functions, convert them to radians by using utilities like Linearly’s rad function. Note that angles in Linearly are also represented in degrees, so calculations in degrees are possible, such as scalar.cos(90) === 0.

import {vec2, mat2d} from 'linearly'

const c = Path.ellipse(vec2.zero, vec2.of(20, 30))
const t = Path.transform(c, mat2d.fromTranslation([50, 50]))

Path Data Structure

In Pave, the representation of paths is not a sequence of drawing commands against a stateful context like the SVG ‘d’ attribute or Canvas API, but always based on vertices. This means there are no operations like moveTo (the M command in SVG) or closePath (the Z in SVG); paths are always composed of a list of tuples of vertex positions and interpolation commands from the last vertex.

Also, the path data structure has the following hierarchy, similar to how in 3D data, meshes consist of collections of polygons, and polygons are formed from collections of vertices.

Path Structure
  • Path: A single Curve or a compound path composed of multiple Curves. It is the most common type to represent shapes in Pave.
  • Curve: Represents a open or closed single stroke.
  • Vertex: Each vertex that makes up the stroke, which consists of end point, command type, and argments for the interpolation.
  • Command: Arguments of interpolation commands excluding the end point.

If you are familiar with TypeScript, it might be easier to understand by looking at the type definitions.

type Path = {paths: Curves[]; fillRule: 'nonzero' | 'evenodd'}
type Curve = {vertices: Vertex[]; closed: boolean}

type Vertex = VertexL | VertexC | VertexA
type VertexL = {point: vec2; command: 'L'}
type VertexC = {
	point: vec2
	command: 'C'
	args: [control1: vec2, control2: vec2]
}
type VertexA = {
	point: vec2
	command: 'A'
	args: [radii: vec2, xRot: vec2, largeArc: boolean, sweep: boolean]
}

In addition to the above hierarchy, there is also a type called Segment, which is a cut-out part of a Curve corresponding to a single command. Unlike Vertex, it includes the start position of interpolation.

type Segment = Vertex & {start: vec2}