curve
import curve from "curve";
import math3d from "math3d";
let c = curve.cubic_Vec2(math3d.vec2(0.0, 0.0), math3d.vec2(0.0, 4.0),
math3d.vec2(4.0, 4.0), math3d.vec2(4.0, 0.0));
let mid = c.at(0.5); // (2, 3)
let arc = c.arc(); // distance -> parameter
let p = c.at(arc.param(2.5)); // 2.5 units along the curvecurve has the curves a game moves things along: Bézier segments,
Hermite segments, Catmull-Rom splines through points, uniform B-splines,
a table that turns distance along a curve into its parameter (so motion
keeps an even speed), keyframe tracks, and sprite-sheet frame tracks.
Everything is f32, on math3d’s vectors, and every value is plain (no
heap): keep it in a var and copy it like a struct.
Each curve comes in four forms: f32, Vec2, Vec3 and Vec4, the
type’s name at the end (cubic_f32, cubic_Vec3, Track_Vec4). Below,
T stands for any of them.
Bézier segments
| Function or method | Gives |
|---|---|
cubic_T(p0, p1, p2, p3) -> Cubic_T | A cubic Bézier from p0 to p3, pulled toward p1 and p2 |
quadratic_T(p0, p1, p2) -> Quadratic_T | A quadratic Bézier from p0 to p2, pulled toward p1 |
c.at(t) -> T | The point at t, 0 to 1: exactly the first point at 0 and the last at 1 |
c.tangent(t) -> T | The derivative at t: the direction of travel, and the speed for t going 0 to 1 in unit time |
c.split(t) | The two halves at t, .first (0 to t) and .second (t to 1), as curves of the same kind |
c.arc() -> ArcTable | A cubic’s arc-length table (below) |
q.to_cubic() -> Cubic_T | A quadratic as the same curve in cubic form |
let c = curve.cubic_f32(0.0, 0.0, 10.0, 10.0);
let s = c.split(0.25);
// s.first runs 0..0.25 of c, s.second 0.25..1; s.first.p3 == s.second.p0
let q = curve.quadratic_Vec2(math3d.vec2(0.0, 0.0), math3d.vec2(2.0, 4.0), math3d.vec2(4.0, 0.0));
let top = q.at(0.5); // (2, 2)
let dir = q.tangent(0.0); // (4, 8)A Cubic_T is the shape every cubic segment ends up in: the other kinds
below are made into one, so at, tangent, split and arc work on all
of them.
Hermite, Catmull-Rom and B-spline segments
| Function | Gives |
|---|---|
hermite_T(p0, m0, p1, m1) -> Cubic_T | From p0 leaving with tangent m0 to p1 arriving with tangent m1 |
catmull_rom_T(p0, p1, p2, p3, alpha) -> Cubic_T | The Catmull-Rom segment from p1 to p2, with p0 and p3 as neighbours |
bspline_T(p0, p1, p2, p3) -> Cubic_T | The uniform cubic B-spline segment of four control points |
alpha spaces Catmull-Rom’s knots: curve.UNIFORM (0), curve.CENTRIPETAL
(0.5) or curve.CHORDAL (1). Centripetal is the one to use when points
are unevenly spaced: it never makes a loop or a cusp inside a segment.
Uniform is the classic Catmull-Rom (the tangent at a point is half the
chord between its neighbours). Points on top of each other are fine.
A B-spline is smoother than Catmull-Rom but passes near its points, not through them.
let h = curve.hermite_f32(0.0, 3.0, 1.0, 3.0); // leaves and arrives with slope 3
let seg = curve.catmull_rom_Vec2(a, b, c, d, curve.CENTRIPETAL); // b to cSplines through points
The spline functions take the points as a slice, []const T:
| Function | Gives |
|---|---|
catmull_rom_segment_T(pts, i, alpha) -> Cubic_T | Segment i, from pts[i] to pts[i + 1] (i from 0 to n - 2) |
catmull_rom_at_T(pts, alpha, u) -> T | The spline at u, 0 to n - 1: point k at u = k |
catmull_rom_arc_T(pts, alpha) -> ArcTable | Its arc-length table, parameter 0 to n - 1 |
bspline_segment_T(pts, i) -> Cubic_T | Segment i of the B-spline over pts (i from 0 to n - 4) |
bspline_at_T(pts, u) -> T | The B-spline at u, 0 to n - 3 |
bspline_arc_T(pts) -> ArcTable | Its arc-length table, parameter 0 to n - 3 |
var path: [4]math3d.Vec2 = [math3d.vec2(0.0, 0.0), math3d.vec2(10.0, 5.0),
math3d.vec2(20.0, 0.0), math3d.vec2(30.0, 5.0)];
let at_second = curve.catmull_rom_at_Vec2(path[..], curve.CENTRIPETAL, 1.0); // path[1]
let heading = curve.catmull_rom_segment_Vec2(path[..], 1, curve.CENTRIPETAL).tangent(0.5);A Catmull-Rom spline goes through every point. Its first and last segment
have no outer neighbour, so the next point is mirrored to stand in for it.
A B-spline over n points starts near pts[1] and ends near pts[n - 2];
write a point three times to start or end exactly on it. A u outside the
range is clamped. Too few points (fewer than 2 for Catmull-Rom, 4 for a
B-spline) or a segment index out of range stops the program with a trap.
Arc length
A curve’s parameter is not distance: a point at t = 0.5 is not half way
along, and stepping t evenly moves at an uneven speed. An ArcTable
measures the curve (the lengths of 64 short chords along its whole
parameter range) and maps back:
| Method | Gives |
|---|---|
arc.length() -> f32 | The curve’s whole length |
arc.param(d) -> f32 | The parameter where the curve is d along (d clamped to 0..length) |
arc.distance(u) -> f32 | How far along the curve parameter u is |
let arc = c.arc();
var s: f32 = 0.0;
while (s < arc.length()) {
let p = c.at(arc.param(s)); // equal steps of s: an even speed
s = s + 0.5;
}The table is a plain value of 65 numbers: build it once, when the curve is
made, and keep it beside the curve. It is a little under the true length
(a quarter circle comes out 0.0025% short). For a spline of many segments,
64 chords spread thin: measure each segment’s arc() instead.
Keyframe tracks
A track is a value over time from keys: at each key a time (milliseconds,
f32), a value, and how the value moves on to the next key. The keys are
yours, as many as you like, in an array, a Vec or a loaded file; the
track reads them where they are.
let keys = [curve.key_f32(0.0, 0.0, curve.Interp.Cubic),
curve.key_f32(250.0, 40.0, curve.Interp.Cubic),
curve.key_eased_f32(500.0, 10.0, easing.Ease.BounceOut),
curve.key_f32(800.0, 0.0, curve.Interp.Step)];
let y = curve.track_f32(keys[..], curve.Wrap.Loop);
let now = y.at(clock_ms); // any time, in any order| Function or method | Does |
|---|---|
key_T(time, v, how) -> Key_T | A key; how is a curve.Interp |
key_eased_T(time, v, ease) -> Key_T | A key moving on along an easing.Ease curve |
track_T(keys, wrap) -> Track_T | A track over keys (a []const Key_T) |
t.at(time) -> T | The value at time |
t.duration() -> f32 | From the first key to the last, in ms |
t.len() -> i64 | How many keys it has |
t.wrap | Its curve.Wrap, which you can change |
curve.Interp is how a key moves on to the next one:
Interp | From this key to the next |
|---|---|
Step | Holds this key’s value, then jumps |
Linear | A straight mix |
Eased | A mix shaped by the key’s easing curve (key_eased) |
Cubic | A smooth curve through the keys (Catmull-Rom over time) |
curve.Wrap is what a time before the first key or after the last gives:
Wrap | Outside the keys |
|---|---|
Clamp | The first key’s value before, the last one’s after |
Loop | Starts again from the first key; the last key’s time shows the first value |
PingPong | Runs back to the first key, then forward again |
w.fold(t, span) is the same folding on its own: a time t counted from
the start, into 0..span (curve.Wrap.PingPong.fold(130.0, 100.0) is
70), for a clock of your own.
Keys go in time order. Two keys at the same time make a jump. A key
earlier than the one before it, or at on a track with no keys, stops the
program with a trap. The track keeps a view of your keys, not a copy: keep
them alive as long as the track, and copying a track copies no keys.
A track is not a tween: a tween is played (you step it, it remembers where it is, it can repeat and retarget), while a track just answers “what is the value at this time” for whatever clock you keep. Many objects can share one track, each at its own time.
Sprite-sheet frames
A frame track says which frame of a sprite sheet to draw:
var walk = curve.frame_strip(8, 6, 80.0, curve.Wrap.Loop); // frames 8..13, 80 ms each
let frame = walk.at(clock_ms);
let frames = [curve.frame(0, 2000.0), // open for 2 s
curve.frame(1, 100.0), // closed briefly
curve.frame(0, 100.0)];
let blink = curve.frame_track(frames[..], curve.Wrap.Clamp);| Function or method | Does |
|---|---|
frame_strip(first, count, ms, wrap) -> FrameTrack | Frames first to first + count - 1, each ms long (needs no storage) |
frame(index, ms) -> Frame | A frame of the sheet, shown ms long |
frame_track(frames, wrap) -> FrameTrack | A frame track over frames (a []const Frame, kept as a view) |
f.at(time) -> i64 | The frame to draw at time ms |
f.done(time) -> bool | A Clamp track at or past its end (Loop and PingPong never are) |
f.duration() -> f32, f.len() -> i64 | One play forward in ms; how many frames |
Clamp holds the last frame, Loop starts again, and PingPong runs
back and forth without showing the end frames twice (4 5 6 7 6 5 4 5 …).
at with no frames stops the program with a trap.
Your own types
gen_curve makes every function above for a type of your own: a struct
with + and - of two values, * and / by an f32, and length() -> f32.
pub struct Pt { x: f32; y: f32; }
pub fn Pt.op_add(self, o: Pt) -> Pt { return Pt { x: self.x + o.x; y: self.y + o.y; }; }
pub fn Pt.op_sub(self, o: Pt) -> Pt { return Pt { x: self.x - o.x; y: self.y - o.y; }; }
pub fn Pt.op_mul(self, s: f32) -> Pt { return Pt { x: self.x * s; y: self.y * s; }; }
pub fn Pt.op_div(self, s: f32) -> Pt { return Pt { x: self.x / s; y: self.y / s; }; }
pub fn Pt.length(self) -> f32 { return @sqrt(self.x * self.x + self.y * self.y); }
@tape { curve.gen_curve(Pt); } // cubic_Pt, Track_Pt, catmull_rom_at_Pt, ...The struct must be pub, as for any generator.
Last modified: