Skip to main content

alpha2_geometry/
spatial.rs

1//! Motion along spatial curves. A position keyframed with tangents travels along the cubic
2//! between keyframes by arc length, at the speed its timing curve sets.
3
4use crate::arclen;
5use kurbo::{CubicBez, PathSeg, Point, Vec2};
6
7/// The point `amount` of the arc length along the cubic from `from` (control `from +
8/// out_tangent`) to `to` (control `to + in_tangent`), in a space scaled to screen proportions.
9///
10/// An `amount` outside `0..=1` continues straight along the end tangent, or along the chord when
11/// that tangent is zero.
12#[must_use]
13pub fn point_along(
14    from: Point,
15    out_tangent: Vec2,
16    in_tangent: Vec2,
17    to: Point,
18    amount: f64,
19) -> Point {
20    let curve = CubicBez::new(from, from + out_tangent, to + in_tangent, to);
21    let segment = PathSeg::Cubic(curve);
22    let total = arclen::length(&segment);
23    if total <= 0.0 {
24        return from;
25    }
26    if amount < 0.0 {
27        let direction = unit(from - (from + out_tangent), from - to);
28        return from + direction * (-amount * total);
29    }
30    if amount > 1.0 {
31        let direction = unit(to - (to + in_tangent), to - from);
32        return to + direction * ((amount - 1.0) * total);
33    }
34    let t = arclen::parameter_at(&segment, amount * total, total);
35    arclen::point_at(&segment, t)
36}
37
38/// `primary` normalized, else `fallback` normalized, else zero.
39fn unit(primary: Vec2, fallback: Vec2) -> Vec2 {
40    for vector in [primary, fallback] {
41        let length = crate::affine::length(vector);
42        if length > 0.0 {
43            return vector / length;
44        }
45    }
46    Vec2::ZERO
47}
48
49/// The motion path between keyframe values as a polyline with `samples` points per segment.
50#[must_use]
51pub fn motion_polyline(keys: &[(Point, Vec2, Vec2)], samples: u32) -> Vec<Point> {
52    let mut points = Vec::new();
53    let Some(first) = keys.first() else {
54        return points;
55    };
56    points.push(first.0);
57    let samples = samples.max(1);
58    for pair in keys.windows(2) {
59        let (from, out_tangent, _) = pair[0];
60        let (to, _, in_tangent) = pair[1];
61        let curve = CubicBez::new(from, from + out_tangent, to + in_tangent, to);
62        for step in 1..=samples {
63            points.push(kurbo::ParamCurve::eval(
64                &curve,
65                f64::from(step) / f64::from(samples),
66            ));
67        }
68    }
69    points
70}