Repository navigation
Curves
math/curve.mojo, math/path.mojo and math/curve3.mojo. A curve is a function from a number between zero and one to a point in the plane or in space. A path is a run of curves that meet end to end. A shape is a closed path with holes in it.

three.js: Curve, LineCurve, QuadraticBezierCurve, CubicBezierCurve, SplineCurve, EllipseCurve, ArcCurve, CurvePath, Path, Shape, LineCurve3, QuadraticBezierCurve3, CubicBezierCurve3, CatmullRomCurve3.
Curve(kind, points) holds a kind and its control points. Every kind is one struct with a tag, because a Path must hold a list of one type.
| Kind | Points | What it draws |
|---|---|---|
LINE |
2 | The straight run from the first to the second. |
QUADRATIC |
3 | A Bezier curve pulled toward one control point. |
CUBIC |
4 | A Bezier curve pulled toward two control points. |
SPLINE |
2 or more | A Catmull-Rom curve through every point. |
ELLIPSE |
1, the center | An arc of an ellipse. See Ellipses and arcs. |
Four functions build the first four kinds and read better at a call site:
from math.curve import cubic_bezier, line, quadratic_bezier, spline
from math.vector2 import Vector2
var run = line(Vector2(0, 0), Vector2(4, 0))
var arc = quadratic_bezier(Vector2(0, 0), Vector2(1, 2), Vector2(2, 0))
var bend = cubic_bezier(
Vector2(0, 0), Vector2(0, 1), Vector2(1, 1), Vector2(1, 0)
)
var through = spline([Vector2(0, 0), Vector2(1, 1), Vector2(2, 0)])| Member | Meaning |
|---|---|
point(t) -> Vector2 |
The point at t, the curve's own parameter. |
tangent(t) -> Vector2 |
The unit direction at t, from the exact derivative. |
sample(divisions) -> List[Vector2] |
divisions + 1 points at equal steps in t. |
length() -> Length |
How long the curve is. |
lengths(divisions) -> List[Float32] |
How far along each sample is, from zero. |
point_at(u) -> Vector2 |
The point u of the way along by distance. |
tangent_at(u) -> Vector2 |
The unit direction u of the way along by distance. |
spaced_points(divisions) -> List[Vector2] |
Points at equal distances along the curve. |
The three.js names are getPoint, getTangent, getPoints, getLength, getLengths, getPointAt, getTangentAt and getSpacedPoints, in the same order.
t runs from zero to one over the curve's formula. u runs from zero to one over its length. They are not the same number.
A Bezier with its control points bunched at one end crawls there and races at the other. Equal steps in t give points that bunch up. Equal steps in u give points that are equally far apart.
Use point and sample to draw a curve. Use point_at and spaced_points to move something along it.
length and point_at read a table of straight runs across the curve. That table is the only approximation here, and how good it is depends on there being enough runs to follow the curve.
three.js uses 200 for every curve. A Bezier has one arc and 200 runs follow it closely. A spline has one arc per segment. 200 runs across a spline with 400 segments do not sample it sparsely. They land at the same place in every other segment, and measure a curve that is not there. So the count is ARC_DIVISIONS or SEGMENT_SAMPLES per segment, whichever is larger, and arc_divisions() reports it.
Bounded t and u parameters must be finite and in the closed interval from zero to one. NaN, infinity and values outside that interval raise an error. SpaceCurve3 checks its double parameter before conversion to a float. Tangent helpers check the parameter before they clamp the two chord samples.
Evaluation checks mutable curve kinds, Catmull-Rom types and control-point counts before dispatch or indexing. These checks take constant time. They do not scan all control points for each sample.
Sample counts must fit in Int. The APIs check additions and per-segment products before iteration or allocation. This includes arc tables, path resolutions, spaced points and Frenet frames. There is no fixed resolution limit. A path made only of lines still uses one run per line.
ExtraCurve.point3 keeps its unbounded parameter policy. Its mutable kind must be valid. Its tangent and distance helpers use bounded parameters.
three.js measures a tangent across two samples a short step apart. Here every kind has an exact derivative, and tangent evaluates it. A Bezier or a spline is a polynomial, and an ellipse is a sine and a cosine.
The reason is precision. These numbers are Float32, and subtracting two nearby samples cancels most of the digits they have.
Being exact makes a cusp exact too. A quadratic that starts and ends at one point turns back half way along. There is no direction there, and tangent raises rather than returning one that is nearly zero.
ellipse makes three.js's EllipseCurve, and arc makes its ArcCurve. The radii are a Length and the angles are an Angle.
from math.curve import arc, ellipse
from math.vector2 import Vector2
from std.math import pi
from units.si import Angle, Length, METER, RADIAN
var half = arc(
Vector2(0, 0),
Length(1, METER),
Angle(0, RADIAN),
Angle(Float32(pi), RADIAN),
)
var oval = ellipse(
Vector2(1, 2),
Length(3, METER),
Length(1.5, METER),
Angle(0.25, RADIAN),
Angle(2, RADIAN),
clockwise=False,
rotation=Angle(0.5, RADIAN),
)| Argument | Meaning |
|---|---|
center |
The middle of the ellipse. |
x_radius, y_radius
|
The radii along the ellipse's own axes. arc takes one radius. |
start, end
|
The angles the arc runs between, from the ellipse's own x axis. |
clockwise |
True to run the way the angle falls. The default is False. |
rotation |
How far the ellipse's axes turn, anticlockwise. The default is zero. |
The sweep follows the three.js rule. The difference between the two angles is brought into the range zero through one whole turn. A difference of zero there, from two angles that differ, is a whole turn. A clockwise arc then loses one whole turn, so it runs the other way.
A whole turn ends exactly where it starts. In Float32, the three.js formula ends a part in ten million away, and a circle used as an outline does not close.
Path is a pen. It starts somewhere, and every call after that leaves from where the last one stopped.
from math.path import Path
from math.vector2 import Vector2
var pen = Path(Vector2(0, 0))
pen.line_to(Vector2(2, 0))
pen.quadratic_to(Vector2(3, 0), Vector2(3, 1))
pen.cubic_to(Vector2(3, 2), Vector2(2, 3), Vector2(0, 3))
pen.close_path()| Member | Meaning |
|---|---|
move_to(start) |
Put the pen down. Refused once the path has drawn. |
line_to(end) |
Add a LINE curve to end. |
quadratic_to(control, end) |
Add a QUADRATIC curve to end. |
cubic_to(first, second, end) |
Add a CUBIC curve to end. |
spline_thru(points) |
Add a SPLINE from here through every point. |
abs_arc(center, radius, start, end, clockwise) |
Add an arc about center. |
abs_ellipse(center, x_radius, y_radius, start, end, clockwise, rotation) |
Add an arc of an ellipse about center. |
arc(offset, radius, start, end, clockwise) |
Add an arc about a center offset from the pen. |
ellipse(offset, x_radius, y_radius, start, end, clockwise, rotation) |
Add an arc of an ellipse about a center offset from the pen. |
close_path() |
Add the straight run back to the start. |
is_closed() -> Bool |
True if the path ends where it began. |
sample(divisions) -> List[Vector2] |
The whole path as points, divisions per curve. |
length() -> Length |
The sum of its curves' lengths. |
curve_count() -> Int |
How many curves the path is made of. |
current() -> Vector2 |
Where the pen is. |
sample leaves out each curve's first point, because it is where the curve before it ended. A repeated point is a run of no length for whatever reads the list. An ellipse gets twice divisions runs, as in three.js.
An arc does not have to start where the pen is. If it starts somewhere else, the path adds a straight run to the start of the arc first. That is the three.js absellipse rule.
On an empty path, the path starts where the arc starts, and the pen does not have to be down. A whole circle is then a closed outline:
from math.path import Path, Shape
from math.vector2 import Vector2
from std.math import pi
from units.si import Angle, Length, METER, RADIAN
var rim = Path()
rim.abs_arc(
Vector2(0, 0),
Length(1, METER),
Angle(0, RADIAN),
Angle(Float32(2 * pi), RADIAN),
)
var disc = Shape(rim^)three.js lets moveTo start a second run in the middle of a path. Nothing here can draw that, because a broken outline has no inside. move_to is refused once the path has a curve on it, and a second run must be a second path.
Shape is a closed path with holes. ShapeGeometry and ExtrudeGeometry are built from one.
from math.path import Path, Shape
from math.vector2 import Vector2
var outline = Path(Vector2(0, 0))
outline.line_to(Vector2(4, 0))
outline.line_to(Vector2(4, 4))
outline.line_to(Vector2(0, 4))
outline.close_path()
var hole = Path(Vector2(1, 1))
hole.line_to(Vector2(3, 1))
hole.line_to(Vector2(3, 3))
hole.line_to(Vector2(1, 3))
hole.close_path()
var plate = Shape(outline^)
plate.add_hole(hole^)| Member | Meaning |
|---|---|
add_hole(hole) |
Cut a closed path out of the shape. |
hole_count() -> Int |
How many holes the shape has. |
outline_points(divisions) |
The outline as points. |
hole_points(index, divisions) |
One hole as points. |
The winding does not matter. shape_geometry turns the outline clockwise and each hole counter-clockwise before it cuts them up, as three.js does.
math/curve3.mojo has Curve3, the three.js curves in space. It has the same members as Curve, with Vector3 in place of Vector2.
| Kind | Points | Function | three.js |
|---|---|---|---|
LINE3 |
2 | line3(start, end) |
LineCurve3 |
QUADRATIC3 |
3 | quadratic_bezier3(start, control, end) |
QuadraticBezierCurve3 |
CUBIC3 |
4 | cubic_bezier3(start, first, second, end) |
CubicBezierCurve3 |
CATMULL_ROM3 |
2 or more | catmull_rom3(points, closed, curve_type, tension) |
CatmullRomCurve3 |
from math.curve3 import CHORDAL, catmull_rom3
from math.vector3 import Vector3
var track = catmull_rom3(
[Vector3(0, 0, 0), Vector3(1, 2, 0), Vector3(3, 2, 1), Vector3(4, 0, 2)],
closed=True,
curve_type=CHORDAL,
)
var middle = track.point_at(0.5)curve_type is a CatmullRomType. It sets how the spline spaces its points.
| Type | three.js | Spacing |
|---|---|---|
CENTRIPETAL |
'centripetal' |
The square root of the distance between two points. This is the default. |
CHORDAL |
'chordal' |
The distance between two points. |
CATMULLROM |
'catmullrom' |
Even spacing. tension sets the pull toward the neighbors. The default is 0.5. |
Use CENTRIPETAL for points that are far apart in some places and close in others. The uniform spline makes loops where two points are close.
An open spline has no point before its first point or after its last point. three.js reflects the neighbor through the end to make one. It keeps both of these points in one scratch vector. On a curve of two points, the second overwrites the first. Here each end has its own point, so a curve of two points is a straight run.
frenet_frames(segments, closed) is the three.js computeFrenetFrames. It returns a FrenetFrames with segments + 1 tangents, normals and binormals, at equal distances along the curve.
Parallel transport carries each frame to the next, so the frames do not twist where the curve bends. For a closed curve, the twist that builds up is spread back along the curve, so the last frame meets the first. transport_frames(tangents, closed) does this for any list of unit tangents.
tube(curve, radius, tubular_segments, radial_segments, closed) in geometries/tube.mojo is the three.js TubeGeometry. It puts tubular_segments + 1 rings at equal distances along the curve, in the frames of the curve. The defaults are the three.js defaults: 64 along and 8 around.
from geometries.tube import tube
from units.si import Length, METER
var pipe = tube(track, Length(0.1, METER), 64, 8, closed=True)tube(points, radius, radial_segments, closed) still takes a list of points. Both forms use the same frames.
extrude(shape, curve, steps) sweeps a Shape along a Curve3 or a CurvePath3 in the same frames. See Extrude along a path.
CurvePath3 is the three.js CurvePath for curves in space. As in three.js, nothing makes one curve start where the last one ended.
| Member | Meaning |
|---|---|
add(curve) |
Add a curve to the end. |
close_path() |
Add a LINE3 from the end back to the start. |
curve_lengths() -> List[Float32] |
The running total of the lengths, one per curve. |
length() -> Length |
The whole length. |
point(t) -> Vector3 |
The point t of the way along by distance. |
tangent(t) -> Vector3 |
The unit direction t of the way along by distance. |
sample(divisions) -> List[Vector3] |
One run for a line, divisions for any other curve. A repeated point is left out. |
spaced_points(divisions) -> List[Vector3] |
Points at equal distances along the path. |
frenet_frames(segments, closed) |
Frames at equal distances along the path. |
loaders/curve_json.mojo writes and reads curves, paths and shapes as three.js's toJSON and fromJSON do.
var text = shape_to_json(plate) # a shape needs a uuid
var again = shape_from_json(text)
var spine = curve3_from_json(curve3_to_json(curve))| Function | Meaning |
|---|---|
curve_to_json(curve), curve_from_json(text)
|
A curve in the plane: LineCurve, QuadraticBezierCurve, CubicBezierCurve, SplineCurve or EllipseCurve. An ArcCurve reads as its ellipse. |
curve3_to_json(curve), curve3_from_json(text)
|
A curve in space: LineCurve3, QuadraticBezierCurve3, CubicBezierCurve3 or CatmullRomCurve3. |
write_path, read_path
|
A Path, with its currentPoint. |
write_shape, read_shape
|
A Shape, with its uuid and its holes. |
write_curve_path3, read_curve_path3
|
A CurvePath3, as a CurvePath. |
A curve writes arcLengthDivisions as 200, three.js's default. The reader ignores another value, because this port always measures with 200 divisions. A shape here must be closed. The reader closes an open outline or hole with a straight run back to its start.
Shape.uuid is three.js's uuid. A scene's shapes library names each shape by it. See Scene JSON.
| Mistake | Answer |
|---|---|
| A kind or a Catmull-Rom type that does not exist. | An error at construction. |
| Points that do not match the kind. | An error at construction. |
| Every point the same point. | An error at construction. |
| An ellipse with a radius that is not positive. | An error at construction. |
| An ellipse whose two angles are the same. | An error at construction. |
A closed Curve3 that is not a Catmull-Rom spline. |
An error at construction. |
t or u outside zero through one. |
An error. |
| A tangent where the curve turns back. | An error. |
| Fewer than one division. | An error. |
| Drawing before the pen is down. | An error. |
| Moving the pen after it has drawn. | An error. |
| Closing an empty or a closed path. | An error. |
| Frames where two steps point opposite ways. | An error. |
| An open outline or an open hole. | An error. |
three.js clamps t, returns an empty list, or builds a shape with no area. Each of those hides a caller that has lost track of the order.
Tutorials
How-to
Reference
- Scene graph
- Rotations
- Cameras
- Geometry
- Geometry addons
- Lofts and clipping groups
- Sculptor
- Procedural generators
- Meshes and assets
- Lines
- Points and sprites
- Helpers
- Scene objects
- Animated objects
- Raycasting
- Curves and paths
- Animation
- Skinning
- Materials
- Node materials
- TSL functions
- Lights
- Lighting addons
- Fog
- Textures
- Renderer
- Renderer hooks and material flags
- Rasterization
- GPU backend
- Render target
- Post-processing
- GPU computation
- Compute nodes
- Progressive light map
- Gaussian splats
- Voxel global illumination
- SVG renderer
- Image files
- Image decode queue
- Model files
- More model files
- Exporters
- Scene JSON
- Windowing and controls
- Math
- Norm consumers
- Math addons
- Units
- Coverage tool
- Commands
- Examples
- Benchmarks
- Extensions
- Shared physics
- Physics query snapshots
- Continuous collision
- Tick force restoration
- Water
- Streamline
- Animals
- Buildings
- CARLA
- Femur
- Tibia
- Fibula
- Patella
- Knee
- Muscles
- Vessels
- Lymph
- Nerves
- Integument
- Leg
- Foot
- Pelvis
- Torso
- Arm
- Hand
- Head
- Genome
- Game humanoid
- Humanoid fidelity
- Audio-aligned game faces
- Converted assets
- Mesh quality
- Segment inertia
Explanation
- Software rasterizer
- Scene graph as an array
- Linear color
- Normals are not colors
- Perspective correction
- Fixed-point coverage
- Per-fragment shading
- Mipmaps
- Sorted transparency
- Types and checks
- Render hooks as a trait
- Shared CPU and GPU code
- Bands and threads
- PNG structure
- Extensions beside the port
- CUDA teardown hang
- Mojo compiler hang