whiskers.git / crates / whiskers-art / src / flatten.rs

A scene at an instant, as the quads it covers: each rectangle carried through the groups' motions into the grid, with the opacity of the groups multiplied in. The site draws SVG with the same transforms and the app does the same sum in Kotlin; this is the reference both are held to (fixtures/frames.txt).

6use crate::motion::Xf;
7use crate::node::{Color, Group, Look, Node, Role};
8use crate::scene::gaze_offset;

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}
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    }

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    }
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    }

A group's motion about its origin, as the SVG transform says it: 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}

A rectangle after its transforms: corners in the order top left, top right, bottom right, 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}

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}
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}