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).
An affine map of the grid: x' = a x + c y + e, y' = b x + d y + f.
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 }
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.
Every rectangle of nodes at t seconds with the eyes looking gaze (each axis -1..1).
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}