| 1 | //! Port of `habitat.js`: the companion's little cove for large surfaces. |
| 2 | //! |
| 3 | //! The cove is scenery on the Director's own clock (`f`); it never advances |
| 4 | //! a clock of its own and never changes presence or the acting state. While |
| 5 | //! the whale rests, its gaze may follow the pointer over the water; a tap |
| 6 | //! leaves a decorative ripple. Reduced motion paints the still cove and no |
| 7 | //! gaze. Compact icons never carry it. |
| 8 | |
| 9 | use super::acting::Director; |
| 10 | use super::data::{Act, p}; |
| 11 | use super::math::{clamp, lerp}; |
| 12 | use super::rig::{Path, ellipse, parse}; |
| 13 | use std::collections::VecDeque; |
| 14 | |
| 15 | /// One filled cove layer: its paths are filled together with the even-odd |
| 16 | /// rule, in the cove's own palette. |
| 17 | #[derive(Clone, Debug, PartialEq)] |
| 18 | pub struct Layer { |
| 19 | pub paths: Vec<Path>, |
| 20 | pub color: u32, |
| 21 | pub alpha: f64, |
| 22 | } |
| 23 | |
| 24 | #[derive(Clone, Copy, Debug, PartialEq)] |
| 25 | struct Ripple { |
| 26 | x: f64, |
| 27 | y: f64, |
| 28 | at: f64, |
| 29 | } |
| 30 | |
| 31 | /// A ripple lasts this many frames. |
| 32 | const RIPPLE_LIFE: f64 = 78.; |
| 33 | |
| 34 | #[derive(Clone, Debug, Default)] |
| 35 | pub struct Cove { |
| 36 | target: (f64, f64), |
| 37 | gaze: (f64, f64), |
| 38 | last_frame: f64, |
| 39 | ripples: VecDeque<Ripple>, |
| 40 | } |
| 41 | |
| 42 | struct Palette { |
| 43 | air: u32, |
| 44 | shore: u32, |
| 45 | water: u32, |
| 46 | deep: u32, |
| 47 | reed: u32, |
| 48 | glint: u32, |
| 49 | } |
| 50 | |
| 51 | fn palette(dark: bool) -> Palette { |
| 52 | if dark { |
| 53 | Palette { |
| 54 | air: 0x243c4b, |
| 55 | shore: 0x426878, |
| 56 | water: 0x254e64, |
| 57 | deep: 0x1d4055, |
| 58 | reed: 0x4c8490, |
| 59 | glint: 0x699cab, |
| 60 | } |
| 61 | } else { |
| 62 | Palette { |
| 63 | air: 0xedf5f5, |
| 64 | shore: 0xc4d8d7, |
| 65 | water: 0xd6eaed, |
| 66 | deep: 0xb9dce3, |
| 67 | reed: 0x86b5b6, |
| 68 | glint: 0xf7fcfb, |
| 69 | } |
| 70 | } |
| 71 | } |
| 72 | |
| 73 | fn layer(d: String, color: u32, alpha: f64) -> Layer { |
| 74 | Layer { |
| 75 | paths: vec![parse(&d)], |
| 76 | color, |
| 77 | alpha, |
| 78 | } |
| 79 | } |
| 80 | |
| 81 | impl Cove { |
| 82 | /// The pointer is over the water at design coordinates `x`, `y`. |
| 83 | pub fn observe(&mut self, x: f64, y: f64) { |
| 84 | if !x.is_finite() || !y.is_finite() { |
| 85 | return; |
| 86 | } |
| 87 | self.target = (clamp(x / 62., -1., 1.), clamp(y / 62., -1., 1.)); |
| 88 | } |
| 89 | |
| 90 | /// The pointer left the cove. |
| 91 | pub fn leave(&mut self) { |
| 92 | self.target = (0., 0.); |
| 93 | } |
| 94 | |
| 95 | /// A decorative tap on the water; it cannot change Engine state. |
| 96 | pub fn tap(&mut self, x: f64, y: f64, director: &Director) { |
| 97 | if !x.is_finite() || !y.is_finite() || !director.f.is_finite() { |
| 98 | return; |
| 99 | } |
| 100 | let point = Ripple { |
| 101 | x: clamp(x, -42., 42.), |
| 102 | y: clamp(y, 25., 48.), |
| 103 | at: director.f, |
| 104 | }; |
| 105 | self.ripples.push_back(point); |
| 106 | if self.ripples.len() > 3 { |
| 107 | self.ripples.pop_front(); |
| 108 | } |
| 109 | self.observe(point.x, point.y); |
| 110 | } |
| 111 | |
| 112 | /// Forget every ripple and gaze: a new session starts in a still cove. |
| 113 | pub fn reset(&mut self) { |
| 114 | *self = Self::default(); |
| 115 | } |
| 116 | |
| 117 | pub fn ripples(&self) -> usize { |
| 118 | self.ripples.len() |
| 119 | } |
| 120 | |
| 121 | /// `pose(d)`: absolute overrides for the whale's pose while it rests, |
| 122 | /// easing back to the Director's own pose in every other state. |
| 123 | /// Idempotent within a frame: two surfaces painting the same frame see |
| 124 | /// the same gaze. |
| 125 | pub fn pose(&mut self, d: &Director) -> Vec<(usize, f64)> { |
| 126 | let dt = clamp((d.f - self.last_frame) / 30., 0., 0.25); |
| 127 | self.last_frame = d.f; |
| 128 | if d.reduced { |
| 129 | self.gaze = (0., 0.); |
| 130 | return Vec::new(); |
| 131 | } |
| 132 | let active = d.acting == Act::Rest; |
| 133 | let w = 1. - (-dt * 4.).exp(); |
| 134 | let (tx, ty) = if active { self.target } else { (0., 0.) }; |
| 135 | self.gaze = (lerp(self.gaze.0, tx, w), lerp(self.gaze.1, ty, w)); |
| 136 | let pose = d.pose(); |
| 137 | vec![ |
| 138 | (p::lookX, pose[p::lookX] + self.gaze.0 * 0.85), |
| 139 | (p::lookY, pose[p::lookY] + self.gaze.1 * 0.65), |
| 140 | (p::head, pose[p::head] - self.gaze.1 * 5.), |
| 141 | (p::rot, pose[p::rot] + self.gaze.0 * 2.), |
| 142 | ] |
| 143 | } |
| 144 | |
| 145 | /// `behind()`: sky, shore, water, reeds, glints and rising bubbles. |
| 146 | pub fn behind(&self, d: &Director, dark: bool) -> Vec<Layer> { |
| 147 | let c = palette(dark); |
| 148 | let t = if d.reduced { 0. } else { d.f / 30. }; |
| 149 | let w = (t * 0.55).sin() * 1.4; |
| 150 | let mut out = vec![ |
| 151 | layer( |
| 152 | "M-59 7 C-63 -29 -37 -58 0 -59 C35 -62 60 -41 61 -6 C66 31 48 57 13 60 C-22 67 -54 48 -59 7 Z".into(), |
| 153 | c.air, |
| 154 | 1., |
| 155 | ), |
| 156 | layer( |
| 157 | "M-61 29 C-61 13 -56 5 -51 11 C-46 16 -48 22 -43 25 C-39 27 -37 30 -35 34 C-45 37 -55 36 -61 29 Z".into(), |
| 158 | c.shore, |
| 159 | 1., |
| 160 | ), |
| 161 | layer( |
| 162 | format!( |
| 163 | "M-60 {} C-37 {} -14 {} 6 21 C26 {} 43 {} 61 18 C61 42 42 59 12 61 C-19 65 -51 47 -60 {} Z", |
| 164 | 21. + w, |
| 165 | 15. + w, |
| 166 | 23. - w, |
| 167 | 18. + w, |
| 168 | 15. - w, |
| 169 | 21. + w |
| 170 | ), |
| 171 | c.water, |
| 172 | 1., |
| 173 | ), |
| 174 | layer( |
| 175 | "M-55 40 C-29 49 -9 42 12 45 C32 49 44 39 56 36 C44 56 21 63 -3 61 C-25 59 -44 49 -55 40 Z".into(), |
| 176 | c.deep, |
| 177 | 0.7, |
| 178 | ), |
| 179 | ]; |
| 180 | for (x, h, phase) in [ |
| 181 | (-54., 29., 0.), |
| 182 | (-49., 22., 1.), |
| 183 | (53., 19., 2.), |
| 184 | (57., 26., 3.), |
| 185 | ] { |
| 186 | let sway = (t * 0.65 + phase).sin() * 2.; |
| 187 | out.push(layer( |
| 188 | format!( |
| 189 | "M{x} 52 C{} 42 {} {} {} {} C{} {} {} 40 {} 52 Z", |
| 190 | x - 2., |
| 191 | x + sway - 5., |
| 192 | 52. - h + 5., |
| 193 | x + sway, |
| 194 | 52. - h, |
| 195 | x + sway - 1., |
| 196 | 52. - h + 9., |
| 197 | x + 3., |
| 198 | x + 1. |
| 199 | ), |
| 200 | c.reed, |
| 201 | 0.8, |
| 202 | )); |
| 203 | } |
| 204 | out.push(layer( |
| 205 | format!( |
| 206 | "M-53 33 C-45 {} -40 {} -34 33 C-42 32 -47 34 -53 33 Z", |
| 207 | 30. + w, |
| 208 | 30. + w |
| 209 | ), |
| 210 | c.glint, |
| 211 | 0.7, |
| 212 | )); |
| 213 | out.push(layer( |
| 214 | format!( |
| 215 | "M35 30 C43 {} 48 {} 54 30 C47 30 42 32 35 30 Z", |
| 216 | 28. - w, |
| 217 | 28. - w |
| 218 | ), |
| 219 | c.glint, |
| 220 | 0.65, |
| 221 | )); |
| 222 | // Water bubbles live low in the cove, away from the thinking cloud. |
| 223 | for i in 0..3 { |
| 224 | let fi = i as f64; |
| 225 | let phase = if d.reduced { |
| 226 | 0.35 |
| 227 | } else { |
| 228 | (t / (7. + fi * 2.) + fi * 0.31) % 1. |
| 229 | }; |
| 230 | let x = [-47., 47., 39.][i] + (t * 0.6 + fi).sin() * 1.2; |
| 231 | let y = 55. - phase * 24.; |
| 232 | let r = 0.55 + phase * 0.5; |
| 233 | out.push(Layer { |
| 234 | paths: vec![ellipse(x, y, r, r)], |
| 235 | color: c.glint, |
| 236 | alpha: (phase * std::f64::consts::PI).sin() * 0.65, |
| 237 | }); |
| 238 | } |
| 239 | out |
| 240 | } |
| 241 | |
| 242 | /// `front()`: the near current below the face, and tap ripples. |
| 243 | pub fn front(&self, d: &Director, dark: bool) -> Vec<Layer> { |
| 244 | let c = palette(dark); |
| 245 | let t = if d.reduced { 0. } else { d.f / 30. }; |
| 246 | let w = (t * 0.45).sin() * 2.; |
| 247 | let mut out = vec![ |
| 248 | layer( |
| 249 | format!( |
| 250 | "M-40 51 C-23 {} -9 {} 8 51 C21 {} 30 49 40 47 C26 54 16 54 5 54 C-11 57 -26 50 -40 51 Z", |
| 251 | 47. + w, |
| 252 | 54. - w, |
| 253 | 48. + w |
| 254 | ), |
| 255 | c.glint, |
| 256 | 0.6, |
| 257 | ), |
| 258 | layer( |
| 259 | format!( |
| 260 | "M-23 58 C-8 {} 8 60 21 57 C9 62 -8 60 -23 58 Z", |
| 261 | 56. - w * 0.3 |
| 262 | ), |
| 263 | c.glint, |
| 264 | 0.55, |
| 265 | ), |
| 266 | ]; |
| 267 | for ripple in &self.ripples { |
| 268 | let age = if d.reduced { 12. } else { d.f - ripple.at }; |
| 269 | if !(0. ..=RIPPLE_LIFE).contains(&age) { |
| 270 | continue; |
| 271 | } |
| 272 | for i in 0..2 { |
| 273 | let a = age - i as f64 * 9.; |
| 274 | if a < 0. { |
| 275 | continue; |
| 276 | } |
| 277 | let radius = 2. + a * 0.19; |
| 278 | out.push(Layer { |
| 279 | paths: vec![ |
| 280 | ellipse(ripple.x, ripple.y, radius, radius * 0.25), |
| 281 | ellipse( |
| 282 | ripple.x, |
| 283 | ripple.y, |
| 284 | (radius - 0.7).max(0.), |
| 285 | (radius * 0.25 - 0.55).max(0.), |
| 286 | ), |
| 287 | ], |
| 288 | color: c.glint, |
| 289 | alpha: (1. - a / RIPPLE_LIFE) * 0.72, |
| 290 | }); |
| 291 | } |
| 292 | } |
| 293 | out |
| 294 | } |
| 295 | |
| 296 | /// Whether a ripple is still spreading at the Director's frame. |
| 297 | pub fn rippling(&self, d: &Director) -> bool { |
| 298 | !d.reduced |
| 299 | && self |
| 300 | .ripples |
| 301 | .iter() |
| 302 | .any(|r| (0. ..=RIPPLE_LIFE).contains(&(d.f - r.at))) |
| 303 | } |
| 304 | } |
| 305 |