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:
length()
- aggregate sum of all segment lenghts.bbox()
- aggregate bounding box fitting all segments.offset(d)
- aggregate offset function yielding a newPolyBezier
.
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.
.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.