Skip to main content

alpha2_geometry/
spatial.rs

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