Bezier.js, for doing Bezier curve things

A library for performing Bezier curve computation and, if you add in your own drawing code (like the HTML canvas), drawing curves in a useful manner.

This library works both client side (i.e. in the browser) and server side (e.g. as a node.js module).

Install using npm or yarn, download the library here, or head over to Github for the project page.

This is an interactive API

The rest of this page explains the Bezier.js API, with interactive graphics to illustrate what a function does. Because what's the point of a library for manipulating Bezier curves if you can't manipulate them? You can click-drag all the points to see how the curves behave under the different functions that can act on them.

Basically: the graphics on this page want you to play with them. They're not static images.

new Bezier(...)

Quadratic and cubic 2D/3D Bezier curve constructor.

For quadratic curves, the contructor can take either 6 or 9 numerical arguments (for 2d and 3d curves respectively) or 3 {x:(num),y:(num),z:(num)} coordinate objects. The z property for coordinates is optional, and controls whether the resulting curve is 2d or 3d.

For cubic curves, the contructor can take either 8 or 12 numerical arguments (for 2d and 3d curves respectively) or 4 {x:(num),y:(num),z:(num)} coordinate objects. The z property for coordinates is optional, and controls whether the resulting curve is 2d or 3d.

Bezier.quadraticFromPoints(p1,p2,p3,t) / Bezier.cubicFromPoints(p1,p2,p3,t,d1)

Create a curve through three points.

The points p1 through p3 are required, all additional arguments are optional. In both cases t defaults to 0.5 when omitted.

The cubic value d1 indicates the strut length for building a cubic curve, with the full strut being length d1 * (1-t)/t. If omitted, a length based on B--C is used.

The illustrations show both quadratic and cubic curves going through three fixed points, but with different t values specified (0.2, 0.3, 0.4, and 0.5). For the cubic example, d1 has not between explicitly specified. (no skeleton is shown for the cubic curves, as the additional lines crowd the illustration too much).

.getLUT(steps)

Generates a LookUp Table of coordinates on the curve, spaced at parametrically equidistance intervals. If steps is given, the LUT will contain steps+1 coordinates representing the coordinates from t=0 to t=1 at interval 1/steps.

If steps is omitted, a default value of steps=100 is used.

.length()

Calculates the length of this Bezier curve. Length is calculated using numerical approximation, specifically the Legendre-Gauss quadrature algorithm.

.get(t) and .compute(t)

Calculates a point on the curve, for a given t value between 0 and 1 (inclusive). .get is an alias for .compute. The illustration graphics show the point for t=0.5 highlighted on both curves.

.derivative(t)

Calculates the curve tangent at the specified t value. Note that this yields a not-normalized vector {x: dx, y: dy}.

.normal(t)

Calculates the curve normal at the specified t value. Note that this yields a normalised vector {x: nx, y: ny}.

In 2d, the normal is simply the normalised tangent vector, rotated by a quarter turn. In 3d, the normal is the normalised tangent vector rotated by a quarter turn through the tangential plane.

.split(t) and .split(t1,t2)

When only a single t value is given, this function will split a curve at t=... into two new curves that together are equivalent to the original curve.

When two t values are supplied, the curve is split on t1, after which the resulting second subcurve is split on (a scaled) t2, yielding a new curve that is equivalent to the original curve over the interval [t1,t2].

.extrema()

Calculates all the extrema on a curve. Extrema are calculated for each dimension, rather than for the full curve, so that the result is not the number of convex/concave transitions, but the number of those transitions for each separate dimension.

This function yields an object {x: [num, num, ...], y: [...], z: [...], values: [...]} where each dimension lists the array of t values at which an extremum occurs, z exists only if the curve was a 3d curve, and the values property is the aggregate of the t values across all dimensions.

These points can be used to determine the reach of a curve.

.inflections()

Calculates all the inflection points on on a curve. That is, all points where the curvature of the curve changes sign.

This function yields an array of t values at which inflections occur.

Note that quadratic curves by definition cannot have inflections.

.curvature(t)

Calculates the curvature of the curve at point t, using the curvature formula:


         |     x'y" - y'x"    |
  κ(t) = | ------------------ |
         |  (x'² + y'²)^(3/2) |

This function yields an object { k:number, r:number} in which the value k is the curvature at point t and r is the radius of that curvature, equal to 1/k. Note that an infinite curvature, e.g. when k=0, is represented as r=0 as well, rather than as some infinity value.

.bbox()

Calculates (if not cached) the bounding box for this curve, based on its hull coordinates and its extrema.

.hull(t)

Generates all hull points, at all iterations, for an on-curve point at the specified t-value. For quadratic curves, this generates a point[6], and for cubic curves, this generates a point[10], where the first iteration is [0,1,2] and [0,1,2,3] respectively, the second iteration is [3,4] and [4,5,6] respectively, the third iteration is [5] (the on-curve point for quadratic curves) and [7,8] respectively, and the fourth iteration (for cubic curves only) is [9].

.project(point)

Finds the on-curve point closest to the specific off-curve point, using a two-pass projection test based on the curve's LUT. A distance comparison finds the closest match, after which a fine interval around that match is checked to see if a better projection can be found.

.offset(d) and .offset(t, d)

If called only with a distance argument, this function creates a new curve, offset along the curve normals, at distance d. Note that deep magic lies here and the offset curve of a Bezier curve cannot ever be another Bezier curve. As such, this function "cheats" and yields an array of curves which, taken together, form a single continuous curve equivalent to what a theoretical offset curve would be.

If both a distance and a t value are given, a coordinate is returned instead, representing the point on the curve at t=..., offset along its normal by a distance d.

.reduce()

Reduces a curve to a collection of "simple" subcurves, where a simpleness is defined as having all control points on the same side of the baseline (cubics having the additional constraint that the control-to-end-point lines may not cross), and an angle between the end point normals no greater than 60 degrees.

The main reason this function exists is to make it possible to scale curves. As mentioned in the offset function, curves cannot be offset without cheating, and the cheating is implemented in this function. The array of simple curves that this function yields can safely be scaled.

.arcs() and .arcs(threshold)

Approximates a Bezier curve as a sequence of circular arcs. An optional threshold argument controls how well an arc needs to fit to still be considered a reasonable approximation. The higher the threshold, the less accurate an arc fit is allowed. If no explicit threshold is set, a value of 0.5 is used.

This operation is only supported in 2d (for now).

Arcs come with an .interval property, with two values: interval.start and interval.end, which represent the on-curve t values of the interval that an arc covers on the original curve.

.scale(d)

Scales a curve with respect to the intersection between the end point normals. Note that this will only work if that point exists, which is only guaranteed for simple segments.

.outline(d), .outline(d1,d2), and .outline(d1,d2,d3,d4)

This generates a curve's outline at distance d along the curve normal and anti-normal. The result is an array of curves that taken together form the outline path for this curve. The caps are cubic beziers with the control points oriented to form a straight line.

This function yields a PolyBezier object, which has a property .curves that houses all the outline segments in sequence, and has a partial Bezier API:

When only one distance value is given, the outline is generated at distance d on both the normal and anti-normal. If two distance values are given, the outline is generated at distance d1 on along the normal, and d2 along the anti-normal.

Both graphics show the plain outline in red, with the result of calling the PolyBezier's outline.offset() with values 10 and -10 in light blue. Note that the PolyBezier offset yields "gaps" between discontinuities. How to deal with these gaps is up to you, and options involve arc connections with the original outline's connecting vertex as center, Bezier connections with controls along the line segments, linear extensions of the segments along the tangents, etc.

graduated outlines, using .outline(d1,d2,d3,d4)

Graduated offsetting is achieved by using four distances measures, where d1 is the initial offset along the normal, d2 the initial distance along the anti-normal, d3 the final offset along the normal, and d4 the final offset along the anti-normal.

The offsets are graduated (near-)linearly with distance along the curve, and it should be noted that quadratic curves can only be offset as graduated curve by first raising it to a cubic curve and then running through the offsetting algorithm. While the code does this automatically, be aware that quadratic curves come with limitations on their expressiveness.

.outlineshapes(d), .outlineshapes(d1, d2), and .outlineshapes(d1, d2, curveIntersectionThreshold)

This generates a curve's outline as a series of shapes, rather than as a path sequence. Each shape is an object {startcap: (bezier), forward: (bezier), endcap: (bezier), back: (bezier)}. Additionally, each cap has a .virtual attribute to indicate whether it a true cap for the original curve's outline, or an intermediary cap somewhere inside the collection of outline shapes.

When only one distance value is given, the shape's curve's outlines are generated at distance d on both the normal and anti-normal. If two distance values are given, the shape's curve's outlines are generated at distance d1 on along the normal, and d2 along the anti-normal.

Finally, shapes have an .intersections(othershape) function for finding intersections between shapes rather than between individual curves. If curveIntersectionThreshold is provided, it will be used for precision of curve to curve intersections.

.intersects(), .intersects(line), .intersects(curve), and .intersects(curve, curveIntersectionThreshold)

.intersects()

Without arguments, this function checks for self-intersection. This means it has no meaning for quadratic curves, which can't self intersect without being a degenerate curve (i.e. having coordinates that all lie on the same line, thus not actually being a "curve" so much as a "bizar way to draw a line"). Intersections are yielded as an array of float/float strings, where the two floats are separated by the character / and both floats corresponding to t values on the curve at which the intersection is found.

.intersects(line)

Finds the intersections between this curve an some line {p1: {x:... ,y:...}, p2: ... }. The intersections are an array of t values on this curve.

Curves are first aligned (translation/rotation) such that the curve's first coordinate is (0,0), and the curve is rotated so that the intersecting line coincides with the x-axis. Doing so turns "intersection finding" into plain "root finding".

As a root finding solution, the roots are computed symbolically for both quadratic and cubic curves, using the standard square root function which you might remember from high school, and the absolutely not standard Cardano's algorithm for solving the cubic root function.

.intersects(curve) and .intersects(curve, curveIntersectionThreshold)

Finds the intersections between this curve and another. Intersections are yielded as an array of float/float strings, where the two floats are separated by the character /, the first floats corresponds to the t value on this curve, and the second float corresponds to the t value on the other curve.

Curve/curve intersection uses an interative process, where curves are subdivided at the midpoint, and bounding box overlap checks are performed between the resulting smaller curves. Any overlap is marked as a pair to resolve, and the "divide and check overlap" step is repeated. Doing this enough times "homes in" on the actual intersections, such that with infinite divisions, we can get an arbitrarily close approximation of the t values involved. Thankfully, repeating the process a low number of steps is generally good enough to get reliable values (typically 10 steps yields more than acceptable precision). When curveIntersectionThreshold is provided, this will be used for bounding box comparisons in x and y dimensions so that precision can be specified, otherwise a default value of .5 will be used.