whiskers.git / crates / whiskers-art / src / flatten.rs
1//! A scene at an instant, as the quads it covers: each rectangle carried through the groups'
2//! motions into the grid, with the opacity of the groups multiplied in. The site draws SVG with
3//! the same transforms and the app does the same sum in Kotlin; this is the reference both are
4//! held to (`fixtures/frames.txt`).
5
6use crate::motion::Xf;
7use crate::node::{Color, Group, Look, Node, Role};
8use crate::scene::gaze_offset;
9
10/// An affine map of the grid: x' = a x + c y + e, y' = b x + d y + f.
11#[derive(Clone, Copy, Debug, PartialEq)]
12pub struct Affine {
13    pub a: f32,
14    pub b: f32,
15    pub c: f32,
16    pub d: f32,
17    pub e: f32,
18    pub f: f32,
19}
20
21impl Affine {
22    pub const IDENTITY: Affine = Affine { a: 1.0, b: 0.0, c: 0.0, d: 1.0, e: 0.0, f: 0.0 };
23
24    pub fn translate(tx: f32, ty: f32) -> Affine {
25        Affine { e: tx, f: ty, ..Affine::IDENTITY }
26    }
27
28    /// `self` applied after `inner` (so `inner` acts on a point first).
29    pub fn after(self, inner: Affine) -> Affine {
30        Affine {
31            a: self.a * inner.a + self.c * inner.b,
32            b: self.b * inner.a + self.d * inner.b,
33            c: self.a * inner.c + self.c * inner.d,
34            d: self.b * inner.c + self.d * inner.d,
35            e: self.a * inner.e + self.c * inner.f + self.e,
36            f: self.b * inner.e + self.d * inner.f + self.f,
37        }
38    }
39
40    pub fn apply(self, (x, y): (f32, f32)) -> (f32, f32) {
41        (self.a * x + self.c * y + self.e, self.b * x + self.d * y + self.f)
42    }
43
44    /// A group's motion about its origin, as the SVG `transform` says it:
45    /// translate(origin) translate(t) rotate scale translate(-origin).
46    pub fn of_motion(xf: &Xf, origin: (f32, f32)) -> Affine {
47        let (sin, cos) = xf.rotate.to_radians().sin_cos();
48        let rotate = Affine { a: cos, b: sin, c: -sin, d: cos, e: 0.0, f: 0.0 };
49        let scale = Affine { a: xf.sx, d: xf.sy, ..Affine::IDENTITY };
50        Affine::translate(origin.0 + xf.tx, origin.1 + xf.ty).after(rotate).after(scale).after(Affine::translate(-origin.0, -origin.1))
51    }
52}
53
54/// A rectangle after its transforms: corners in the order top left, top right, bottom right,
55/// bottom left.
56#[derive(Clone, Copy, Debug, PartialEq)]
57pub struct Quad {
58    pub corners: [(f32, f32); 4],
59    pub color: Color,
60    pub alpha: f32,
61}
62
63/// Every rectangle of `nodes` at `t` seconds with the eyes looking `gaze` (each axis -1..1).
64pub fn flatten(nodes: &[Node], t: f32, gaze: (f32, f32)) -> Vec<Quad> {
65    let mut out = Vec::new();
66    walk(nodes, Affine::IDENTITY, 1.0, t, gaze, &mut out);
67    out
68}
69
70fn walk(nodes: &[Node], map: Affine, alpha: f32, t: f32, gaze: (f32, f32), out: &mut Vec<Quad>) {
71    for node in nodes {
72        match node {
73            Node::Rect(rect) => {
74                let (x, y, w, h) = (rect.x, rect.y, rect.w, rect.h);
75                out.push(Quad {
76                    corners: [map.apply((x, y)), map.apply((x + w, y)), map.apply((x + w, y + h)), map.apply((x, y + h))],
77                    color: rect.color,
78                    alpha: rect.alpha * alpha,
79                });
80            }
81            Node::Group(group) => group_into(group, map, alpha, t, gaze, out),
82        }
83    }
84}
85
86fn group_into(group: &Group, map: Affine, alpha: f32, t: f32, gaze: (f32, f32), out: &mut Vec<Quad>) {
87    match group.role {
88        Role::Eyes(look) => {
89            let (ex, ey) = match look {
90                Look::Fixed(gx, gy) => gaze_offset((gx, gy)),
91                Look::Follow => gaze_offset(gaze),
92            };
93            walk(&group.children, map.after(Affine::translate(ex, ey)), alpha, t, gaze, out);
94        }
95        Role::Plain => {
96            let xf = group.motion.at(t);
97            walk(&group.children, map.after(Affine::of_motion(&xf, group.origin)), alpha * xf.opacity, t, gaze, out);
98        }
99    }
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105    use crate::node::{moving, r};
106    use crate::motion::Motion;
107
108    #[test]
109    fn a_rectangle_in_a_still_world_keeps_its_corners() {
110        let quads = flatten(&[r(1.0, 2.0, 3.0, 4.0, Color::hex(0))], 0.0, (0.0, 0.0));
111        assert_eq!(quads[0].corners, [(1.0, 2.0), (4.0, 2.0), (4.0, 6.0), (1.0, 6.0)]);
112    }
113
114    #[test]
115    fn breathing_scales_about_the_origin() {
116        let nodes = [moving(Motion::Breathe, (0.0, 10.0), vec![r(0.0, 0.0, 1.0, 10.0, Color::hex(0))])];
117        let t = std::f32::consts::FRAC_PI_4;
118        let q = &flatten(&nodes, t, (0.0, 0.0))[0];
119        let sy = 1.015;
120        assert!((q.corners[0].1 - (10.0 - 10.0 * sy)).abs() < 1e-3);
121        assert!((q.corners[2].1 - 10.0).abs() < 1e-3);
122    }
123
124    #[test]
125    fn opacity_multiplies_down_the_groups() {
126        let nodes = [moving(Motion::Pulse(0), (0.0, 0.0), vec![crate::node::ra(0.0, 0.0, 1.0, 1.0, Color::hex(0), 0.5)])];
127        let q = &flatten(&nodes, 0.0, (0.0, 0.0))[0];
128        assert!((q.alpha - 0.5 * Motion::Pulse(0).at(0.0).opacity).abs() < 1e-4);
129    }
130
131    #[test]
132    fn a_rotation_turns_the_corners_about_the_origin() {
133        let xf = Xf { rotate: 90.0, ..Xf::IDENTITY };
134        let m = Affine::of_motion(&xf, (1.0, 1.0));
135        let (x, y) = m.apply((2.0, 1.0));
136        assert!((x - 1.0).abs() < 1e-5 && (y - 2.0).abs() < 1e-5);
137    }
138}