Skip to main content

fastanim_core/
mobject.rs

1//! Mobject state (SPEC ยง4.2): a plain value type that can be cloned, interpolated and snapshotted.
2
3use std::collections::BTreeMap;
4use std::f64::consts::TAU;
5use std::ops::Range;
6
7use kurbo::{Affine, Point, Vec2};
8
9use crate::Interpolate;
10use crate::color::{Color, WHITE};
11use crate::geom::VPath;
12
13/// Identifies a mobject within a [`Scene`](crate::Scene).
14#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
15pub struct MobjectId(pub u32);
16
17/// Every mobject's state at one instant, in id order.
18pub type SceneState = BTreeMap<MobjectId, VState>;
19
20/// Outline style.
21#[derive(Debug, Clone, Copy, PartialEq)]
22pub struct Stroke {
23    /// Stroke color.
24    pub color: Color,
25    /// Width in scene units.
26    pub width: f64,
27}
28
29/// The animatable state of a shape.
30// ponytail: solid fills only and no stored transform (animations bake affines into `path`);
31// gradients / decomposed transforms when something needs them.
32#[derive(Debug, Clone, PartialEq)]
33pub struct VState {
34    /// The geometry, in scene units (y up).
35    pub path: VPath,
36    /// Fill color; transparent by default.
37    pub fill: Color,
38    /// Outline.
39    pub stroke: Stroke,
40    /// Overall opacity multiplier.
41    pub opacity: f32,
42    /// Draw order; higher is on top, ties broken by id.
43    pub z_index: i32,
44    /// Fraction `0..1` of arc length that is drawn; driven by `create`.
45    pub draw_range: Range<f32>,
46}
47
48impl VState {
49    /// A shape with manim's default style: white 0.04-unit outline, no fill.
50    pub fn new(path: VPath) -> Self {
51        Self {
52            path,
53            fill: Color::TRANSPARENT,
54            stroke: Stroke {
55                color: WHITE,
56                width: 0.04,
57            },
58            opacity: 1.0,
59            z_index: 0,
60            draw_range: 0.0..1.0,
61        }
62    }
63
64    /// Circle centered on the origin.
65    pub fn circle(radius: f64) -> Self {
66        Self::new(VPath::arc(radius, 0.0, TAU))
67    }
68
69    /// Arc around the origin, `sweep` radians counter-clockwise from `start`.
70    pub fn arc(radius: f64, start: f64, sweep: f64) -> Self {
71        Self::new(VPath::arc(radius, start, sweep))
72    }
73
74    /// Axis-aligned rectangle centered on the origin.
75    pub fn rectangle(width: f64, height: f64) -> Self {
76        let (w, h) = (width / 2.0, height / 2.0);
77        let pts = [(w, h), (-w, h), (-w, -h), (w, -h)].map(Point::from);
78        Self::new(VPath::polyline(&pts, true))
79    }
80
81    /// Square centered on the origin.
82    pub fn square(side: f64) -> Self {
83        Self::rectangle(side, side)
84    }
85
86    /// Closed polygon through `points`.
87    pub fn polygon(points: &[Point]) -> Self {
88        Self::new(VPath::polyline(points, true))
89    }
90
91    /// Line segment.
92    pub fn line(a: Point, b: Point) -> Self {
93        Self::new(VPath::polyline(&[a, b], false))
94    }
95
96    /// Graph of `y = f(x)` over `x_range` (scene units), as `segments` smooth cubics
97    /// (Hermite, slopes by central difference).
98    pub fn function_graph(f: impl Fn(f64) -> f64, x_range: Range<f64>, segments: usize) -> Self {
99        let n = segments.max(1);
100        let h = (x_range.end - x_range.start) / n as f64;
101        let slope = |x: f64| (f(x + h * 1e-3) - f(x - h * 1e-3)) / (h * 2e-3);
102        let segs = (0..n)
103            .map(|i| {
104                let (x0, x1) = (
105                    x_range.start + h * i as f64,
106                    x_range.start + h * (i + 1) as f64,
107                );
108                let (p0, p3) = (Point::new(x0, f(x0)), Point::new(x1, f(x1)));
109                let (d0, d1) = (Vec2::new(h, h * slope(x0)), Vec2::new(h, h * slope(x1)));
110                kurbo::CubicBez::new(p0, p0 + d0 / 3.0, p3 - d1 / 3.0, p3)
111            })
112            .collect();
113        Self::new(VPath {
114            subpaths: vec![crate::geom::SubPath {
115                segments: segs,
116                closed: false,
117            }],
118        })
119    }
120
121    /// Small filled white dot at `p`.
122    pub fn dot(p: Point) -> Self {
123        Self::circle(0.08).fill(WHITE).shift(p.to_vec2())
124    }
125
126    /// Sets the fill color.
127    pub fn fill(self, fill: Color) -> Self {
128        Self { fill, ..self }
129    }
130
131    /// Sets the outline.
132    pub fn stroke(self, color: Color, width: f64) -> Self {
133        Self {
134            stroke: Stroke { color, width },
135            ..self
136        }
137    }
138
139    /// Sets the draw order.
140    pub fn z_index(self, z_index: i32) -> Self {
141        Self { z_index, ..self }
142    }
143
144    /// Applies an affine transform to the geometry.
145    pub fn transform(self, a: Affine) -> Self {
146        Self {
147            path: self.path.transform(a),
148            ..self
149        }
150    }
151
152    /// Translates by `v`.
153    pub fn shift(self, v: Vec2) -> Self {
154        self.transform(Affine::translate(v))
155    }
156
157    /// Moves so the bounding-box center is at `p`.
158    pub fn move_to(self, p: Point) -> Self {
159        let c = self.path.center();
160        self.shift(p - c)
161    }
162
163    /// Scales about the bounding-box center.
164    pub fn scale(self, factor: f64) -> Self {
165        let c = self.path.center();
166        self.transform(Affine::scale_about(factor, c))
167    }
168
169    /// Rotates counter-clockwise by `angle` radians about the bounding-box center.
170    pub fn rotate(self, angle: f64) -> Self {
171        let c = self.path.center();
172        self.transform(Affine::rotate_about(angle, c))
173    }
174}
175
176impl crate::position::Position for VState {
177    fn bbox(&self) -> Option<kurbo::Rect> {
178        self.path.bbox()
179    }
180    fn transform(self, a: Affine) -> Self {
181        VState::transform(self, a)
182    }
183}
184
185impl Interpolate for f32 {
186    fn lerp(a: &Self, b: &Self, t: f32) -> Self {
187        a + (b - a) * t
188    }
189}
190
191impl Interpolate for VPath {
192    /// Lerps control points. Both paths must already be [aligned](crate::geom::align).
193    fn lerp(a: &Self, b: &Self, t: f32) -> Self {
194        assert_eq!(
195            a.subpaths.len(),
196            b.subpaths.len(),
197            "lerp of unaligned paths"
198        );
199        let t64 = f64::from(t);
200        let subpaths = a
201            .subpaths
202            .iter()
203            .zip(&b.subpaths)
204            .map(|(sa, sb)| {
205                assert_eq!(
206                    sa.segments.len(),
207                    sb.segments.len(),
208                    "lerp of unaligned paths"
209                );
210                let segments = sa
211                    .segments
212                    .iter()
213                    .zip(&sb.segments)
214                    .map(|(x, y)| {
215                        kurbo::CubicBez::new(
216                            x.p0.lerp(y.p0, t64),
217                            x.p1.lerp(y.p1, t64),
218                            x.p2.lerp(y.p2, t64),
219                            x.p3.lerp(y.p3, t64),
220                        )
221                    })
222                    .collect();
223                // Only fully closed at an endpoint that is closed.
224                let closed = if t <= 0.0 {
225                    sa.closed
226                } else if t >= 1.0 {
227                    sb.closed
228                } else {
229                    sa.closed && sb.closed
230                };
231                crate::geom::SubPath { segments, closed }
232            })
233            .collect();
234        VPath { subpaths }
235    }
236}
237
238impl Interpolate for VState {
239    /// Lerps every field. Paths must already be [aligned](crate::geom::align).
240    fn lerp(a: &Self, b: &Self, t: f32) -> Self {
241        Self {
242            path: VPath::lerp(&a.path, &b.path, t),
243            fill: Color::lerp(&a.fill, &b.fill, t),
244            stroke: Stroke {
245                color: Color::lerp(&a.stroke.color, &b.stroke.color, t),
246                width: a.stroke.width + (b.stroke.width - a.stroke.width) * f64::from(t),
247            },
248            opacity: f32::lerp(&a.opacity, &b.opacity, t),
249            z_index: if t < 1.0 { a.z_index } else { b.z_index },
250            draw_range: f32::lerp(&a.draw_range.start, &b.draw_range.start, t)
251                ..f32::lerp(&a.draw_range.end, &b.draw_range.end, t),
252        }
253    }
254}