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}