1use 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
12const TICK: f64 = 0.1;
14
15#[derive(Debug, Clone, Copy, PartialEq)]
22pub struct Axes {
23 pub x_range: [f64; 3],
25 pub y_range: [f64; 3],
27 pub to_scene: Affine,
29}
30
31impl Axes {
32 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 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 pub fn c2p(&self, x: f64, y: f64) -> Point {
52 self.to_scene * Point::new(x, y)
53 }
54
55 pub fn p2c(&self, p: Point) -> Point {
57 self.to_scene.inverse() * p
58 }
59
60 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 pub fn x_ticks(&self) -> Vec<f64> {
68 ticks(self.x_range)
69 }
70
71 pub fn y_ticks(&self) -> Vec<f64> {
73 ticks(self.y_range)
74 }
75
76 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 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 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 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 pub fn shift(self, v: Vec2) -> Self {
123 Position::transform(self, Affine::translate(v))
124 }
125
126 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 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 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}