Skip to main content

fastanim_core/
axes.rs

1//! manim's `Axes`: a coordinate system mapped onto the scene, for plotting in graph units.
2
3use std::ops::Range;
4
5use kurbo::{Affine, Point, Rect, Vec2};
6
7use crate::color::WHITE;
8use crate::geom::{SubPath, VPath};
9use crate::mobject::VState;
10use crate::position::Position;
11
12/// Half the length of a tick mark, in scene units (manim's `tick_size`).
13const TICK: f64 = 0.1;
14
15/// x and y axes over `[min, max, step]` ranges, placed in the scene by an affine map from
16/// graph coordinates. Centered on the origin when made; move it with [`Position`] or
17/// [`shift`](Axes::shift), and [`c2p`](Axes::c2p) follows.
18///
19/// The axes cross at graph `(0, 0)`, or at the nearest end of a range that excludes 0.
20// ponytail: no arrow tips; add with `Arrow`.
21#[derive(Debug, Clone, Copy, PartialEq)]
22pub struct Axes {
23    /// `[min, max, step]` along x, in graph units.
24    pub x_range: [f64; 3],
25    /// `[min, max, step]` along y, in graph units.
26    pub y_range: [f64; 3],
27    /// Graph coordinates to scene coordinates.
28    pub to_scene: Affine,
29}
30
31impl Axes {
32    /// manim's default size: 12 units wide, 6 tall.
33    pub fn new(x_range: [f64; 3], y_range: [f64; 3]) -> Self {
34        Self::sized(x_range, y_range, 12.0, 6.0)
35    }
36
37    /// Axes `x_length` by `y_length` scene units, centered on the origin.
38    pub fn sized(x_range: [f64; 3], y_range: [f64; 3], x_length: f64, y_length: f64) -> Self {
39        let [x0, x1, _] = x_range;
40        let [y0, y1, _] = y_range;
41        let to_scene = Affine::scale_non_uniform(x_length / (x1 - x0), y_length / (y1 - y0))
42            * Affine::translate((-(x0 + x1) / 2.0, -(y0 + y1) / 2.0));
43        Self {
44            x_range,
45            y_range,
46            to_scene,
47        }
48    }
49
50    /// Graph coordinates to a scene point, manim's `c2p`.
51    pub fn c2p(&self, x: f64, y: f64) -> Point {
52        self.to_scene * Point::new(x, y)
53    }
54
55    /// A scene point to graph coordinates, manim's `p2c`.
56    pub fn p2c(&self, p: Point) -> Point {
57        self.to_scene.inverse() * p
58    }
59
60    /// Where the axes cross, in graph coordinates.
61    pub fn crossing(&self) -> Point {
62        let cross = |[lo, hi, _]: [f64; 3]| 0.0f64.clamp(lo, hi);
63        Point::new(cross(self.x_range), cross(self.y_range))
64    }
65
66    /// Tick values along x: `min`, `min + step`, ... up to `max`.
67    pub fn x_ticks(&self) -> Vec<f64> {
68        ticks(self.x_range)
69    }
70
71    /// Tick values along y.
72    pub fn y_ticks(&self) -> Vec<f64> {
73        ticks(self.y_range)
74    }
75
76    /// The axis lines and tick marks, in manim's default style.
77    pub fn shape(&self) -> VState {
78        let o = self.crossing();
79        let [x0, x1, _] = self.x_range;
80        let [y0, y1, _] = self.y_range;
81        let line = |a: Point, b: Point| SubPath::polyline(&[a, b], false);
82        // Ticks stay `2 * TICK` long however the axes are scaled.
83        let tick = |p: Point, along: Vec2| {
84            let n = Vec2::new(-along.y, along.x).normalize() * TICK;
85            line(p - n, p + n)
86        };
87        let (ex, ey) = (
88            self.c2p(1.0, 0.0) - self.c2p(0.0, 0.0),
89            self.c2p(0.0, 1.0) - self.c2p(0.0, 0.0),
90        );
91        let mut subpaths = vec![
92            line(self.c2p(x0, o.y), self.c2p(x1, o.y)),
93            line(self.c2p(o.x, y0), self.c2p(o.x, y1)),
94        ];
95        subpaths.extend(
96            self.x_ticks()
97                .into_iter()
98                .map(|x| tick(self.c2p(x, o.y), ex)),
99        );
100        subpaths.extend(
101            self.y_ticks()
102                .into_iter()
103                .map(|y| tick(self.c2p(o.x, y), ey)),
104        );
105        VState::new(VPath { subpaths }).stroke(WHITE, 0.03)
106    }
107
108    /// Graph of `y = f(x)` over `x_range` in graph units, manim's `plot`.
109    // ponytail: fixed 64 segments; take a count if a plot shows its corners.
110    pub fn plot(&self, f: impl Fn(f64) -> f64, x_range: Range<f64>) -> VState {
111        VState::function_graph(f, x_range, 64).transform(self.to_scene)
112    }
113
114    /// Line from scene point `p` straight to the x-axis, manim's `get_vertical_line`
115    /// (solid, not dashed).
116    pub fn vertical_line(&self, p: Point) -> VState {
117        let c = self.p2c(p);
118        VState::line(p, self.c2p(c.x, self.crossing().y))
119    }
120
121    /// Translates by `v`.
122    pub fn shift(self, v: Vec2) -> Self {
123        Position::transform(self, Affine::translate(v))
124    }
125
126    /// Moves so the bounding-box center is at `p`.
127    pub fn move_to(self, p: Point) -> Self {
128        let c = self.bbox().map_or(p, |b| b.center());
129        self.shift(p - c)
130    }
131
132    /// Scales about the bounding-box center.
133    pub fn scale(self, factor: f64) -> Self {
134        let c = self.bbox().map_or(Point::ORIGIN, |b| b.center());
135        Position::transform(self, Affine::scale_about(factor, c))
136    }
137}
138
139fn ticks([lo, hi, step]: [f64; 3]) -> Vec<f64> {
140    let n = ((hi - lo) / step + 1e-9).floor() as usize;
141    (0..=n).map(|i| lo + step * i as f64).collect()
142}
143
144impl Position for Axes {
145    /// The span of the axis lines, ticks excluded.
146    fn bbox(&self) -> Option<Rect> {
147        let [x0, x1, _] = self.x_range;
148        let [y0, y1, _] = self.y_range;
149        let (a, b) = (self.c2p(x0, y0), self.c2p(x1, y1));
150        Some(
151            Rect::from_points(a, b)
152                .union_pt(self.c2p(x0, y1))
153                .union_pt(self.c2p(x1, y0)),
154        )
155    }
156    fn transform(self, a: Affine) -> Self {
157        Self {
158            to_scene: a * self.to_scene,
159            ..self
160        }
161    }
162}