返回 CodeWhale
habitat.rs
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
305 lines RUST