Skip to main content

fastanim_core/
position.rs

1//! Positioning helpers (SPEC ยง8.3): `next_to`, `align_to`, `to_edge` and [`arrange`], for
2//! anything with a bounding box that can be transformed.
3
4use kurbo::{Affine, Point, Rect, Vec2};
5
6use crate::{FRAME_HEIGHT, FRAME_WIDTH};
7
8/// manim's default gap between mobjects placed with [`Position::next_to`] or [`arrange`].
9pub const DEFAULT_BUFF: f64 = 0.25;
10/// manim's default gap to the frame edge for [`Position::to_edge`].
11pub const EDGE_BUFF: f64 = 0.5;
12
13/// The bounding-box point in direction `dir`: the center moved to the edge (or corner) along
14/// each axis where `dir` is nonzero, e.g. the top-center for `UP`.
15pub fn critical_point(b: Rect, dir: Vec2) -> Point {
16    // Not `total_cmp`: `-dir` turns 0 into -0, which must still pick the middle.
17    let pick = |d: f64, lo: f64, mid: f64, hi: f64| {
18        if d > 0.0 {
19            hi
20        } else if d < 0.0 {
21            lo
22        } else {
23            mid
24        }
25    };
26    let c = b.center();
27    Point::new(pick(dir.x, b.x0, c.x, b.x1), pick(dir.y, b.y0, c.y, b.y1))
28}
29
30/// Something that can be placed relative to other things by its bounding box. Empty things
31/// (no bounding box) are left where they are.
32pub trait Position: Sized {
33    /// Tight bounding box, or `None` if empty.
34    fn bbox(&self) -> Option<Rect>;
35
36    /// Applies an affine transform to the geometry.
37    fn transform(self, a: Affine) -> Self;
38
39    /// Places this `buff` units from `other` in direction `dir`, centered on it along the other
40    /// axis, manim's `next_to` (e.g. `label.next_to(&dot, UP, DEFAULT_BUFF)`). A diagonal
41    /// `dir` places it off a corner.
42    fn next_to(self, other: &impl Position, dir: Vec2, buff: f64) -> Self {
43        let (Some(me), Some(them)) = (self.bbox(), other.bbox()) else {
44            return self;
45        };
46        let target = critical_point(them, dir) + dir * buff;
47        let v = target - critical_point(me, -dir);
48        self.transform(Affine::translate(v))
49    }
50
51    /// Lines up this edge in direction `dir` with the same edge of `other`, moving only along
52    /// the axes where `dir` is nonzero, manim's `align_to` (e.g. `LEFT` aligns left edges).
53    fn align_to(self, other: &impl Position, dir: Vec2) -> Self {
54        let (Some(me), Some(them)) = (self.bbox(), other.bbox()) else {
55            return self;
56        };
57        let mut v = critical_point(them, dir) - critical_point(me, dir);
58        if dir.x == 0.0 {
59            v.x = 0.0;
60        }
61        if dir.y == 0.0 {
62            v.y = 0.0;
63        }
64        self.transform(Affine::translate(v))
65    }
66
67    /// Moves against the frame edge in direction `dir` (e.g. `UP`, or `UP + LEFT` for a
68    /// corner), leaving manim's 0.5-unit gap.
69    fn to_edge(self, dir: Vec2) -> Self {
70        let frame = Rect::new(
71            -FRAME_WIDTH / 2.0 + EDGE_BUFF,
72            -FRAME_HEIGHT / 2.0 + EDGE_BUFF,
73            FRAME_WIDTH / 2.0 - EDGE_BUFF,
74            FRAME_HEIGHT / 2.0 - EDGE_BUFF,
75        );
76        self.align_to(&frame, dir)
77    }
78}
79
80impl Position for Rect {
81    fn bbox(&self) -> Option<Rect> {
82        Some(*self)
83    }
84    fn transform(self, a: Affine) -> Self {
85        a.transform_rect_bbox(self)
86    }
87}
88
89impl Position for Point {
90    fn bbox(&self) -> Option<Rect> {
91        Some(Rect::from_points(*self, *self))
92    }
93    fn transform(self, a: Affine) -> Self {
94        a * self
95    }
96}
97
98/// Lays `items` out in a row in direction `dir`, `buff` apart and centered on each other along
99/// the other axis, keeping the group's center where it was, manim's `arrange`.
100pub fn arrange<T: Position>(items: Vec<T>, dir: Vec2, buff: f64) -> Vec<T> {
101    let before = union(&items);
102    let mut out: Vec<T> = Vec::with_capacity(items.len());
103    for item in items {
104        let placed = match out.iter().rev().find(|p| p.bbox().is_some()) {
105            Some(prev) => item.next_to(prev, dir, buff),
106            None => item,
107        };
108        out.push(placed);
109    }
110    let (Some(before), Some(after)) = (before, union(&out)) else {
111        return out;
112    };
113    let v = before.center() - after.center();
114    out.into_iter()
115        .map(|m| m.transform(Affine::translate(v)))
116        .collect()
117}
118
119fn union<T: Position>(items: &[T]) -> Option<Rect> {
120    items
121        .iter()
122        .filter_map(Position::bbox)
123        .reduce(|a, b| a.union(b))
124}