返回 CodeWhale
props.rs
1 //! Port of `props.js`: flat physical props and particles, placed at rig
2 //! anchors so a held prop follows the whale under any transform.
3
4 use super::data::{Pose, p};
5 use super::math::{D2R, clamp};
6 use super::rig::{Cmd, Parts, Path, Point, Role, Shape, ellipse, parse};
7 use std::sync::OnceLock;
8
9 fn poly(pts: &[[f64; 2]]) -> Path {
10 let mut a = vec![Cmd::M(pts[0])];
11 for i in 1..=pts.len() {
12 let p = pts[(i - 1) % pts.len()];
13 let q = pts[i % pts.len()];
14 a.push(Cmd::C([p[0], p[1], q[0], q[1], q[0], q[1]]));
15 }
16 a.push(Cmd::Z);
17 a
18 }
19
20 pub fn transform(path: &Path, x: f64, y: f64, a: f64, s: f64) -> Path {
21 let (c, sn) = (a.cos(), a.sin());
22 path.iter()
23 .map(|cmd| {
24 let src = cmd.coords();
25 let mut out = [0.; 6];
26 let mut i = 0;
27 while i < src.len() {
28 out[i] = x + (src[i] * c - src[i + 1] * sn) * s;
29 out[i + 1] = y + (src[i] * sn + src[i + 1] * c) * s;
30 i += 2;
31 }
32 match cmd {
33 Cmd::M(_) => Cmd::M([out[0], out[1]]),
34 Cmd::C(_) => Cmd::C(out),
35 Cmd::Z => Cmd::Z,
36 }
37 })
38 .collect()
39 }
40
41 struct Library {
42 page: Path,
43 fold: Path,
44 rules: [Path; 2],
45 pad: Path,
46 binding: Path,
47 pad_lines: [Path; 4],
48 pencil: Path,
49 pencil_tip: Path,
50 lens: Path,
51 lens_handle: Path,
52 wrench: Path,
53 spyglass: Path,
54 spyglass_end: Path,
55 cursor: Path,
56 links: [Path; 2],
57 jet: Path,
58 drop: Path,
59 water_back: Path,
60 water_front: Path,
61 splashes: [Path; 2],
62 particle_drop: Path,
63 cloud: Path,
64 bubble: Path,
65 }
66
67 fn library() -> &'static Library {
68 static LIBRARY: OnceLock<Library> = OnceLock::new();
69 LIBRARY.get_or_init(|| {
70 let splash = parse("M0 0 C-2 -8 -8 -12 -11 -15 C-5 -15 2 -8 3 0 C2 2 1 2 0 0 Z");
71 let mirror = |side: f64| -> Path {
72 splash
73 .iter()
74 .map(|c| c.map(|i, v| if i % 2 == 1 { v } else { -side * v }))
75 .collect()
76 };
77 let link = |side: f64| -> Path {
78 let mut path = ellipse(side * 2.3, side * 5.5, 4.5, 8.);
79 path.extend(ellipse(side * 2.3, side * 5.5, 2.1, 5.3));
80 path
81 };
82 let mut lens = ellipse(0., 0., 9., 9.);
83 lens.extend(ellipse(0., 0., 5.8, 5.8));
84 Library {
85 page: parse("M-9 -13 C-5 -14 0 -14 4 -13 C6 -11 7 -9 9 -8 C9 -1 9 6 8 13 C3 12 -3 13 -9 14 C-8 5 -8 -4 -9 -13 Z"),
86 fold: poly(&[[4., -13.], [4., -8.], [9., -8.]]),
87 rules: [0., 1.].map(|i| {
88 poly(&[
89 [-5., -4. + i * 6.],
90 [5., -4. + i * 6.],
91 [5., -2. + i * 6.],
92 [-5., -2. + i * 6.],
93 ])
94 }),
95 pad: poly(&[[-11., -5.], [11., -5.], [11., 5.], [-11., 5.]]),
96 binding: poly(&[[-8.5, -3.], [8.5, -3.], [8.5, -1.5], [-8.5, -1.5]]),
97 pad_lines: [0., 1., 2., 3.].map(|i| {
98 poly(&[
99 [-8. + i * 4., 0.],
100 [-6. + i * 4., 0.],
101 [-6. + i * 4., 1.2],
102 [-8. + i * 4., 1.2],
103 ])
104 }),
105 pencil: poly(&[[-2., -5.], [2., -5.], [2., 6.], [0., 10.], [-2., 6.]]),
106 pencil_tip: poly(&[[-1.3, 6.5], [1.3, 6.5], [0., 10.]]),
107 lens,
108 lens_handle: poly(&[[-1.7, 7.], [1.7, 7.], [1.7, 17.], [-1.7, 17.]]),
109 wrench: poly(&[
110 [-2., 9.],
111 [-2., -8.],
112 [-6., -11.],
113 [-6., -18.],
114 [-3., -21.],
115 [-3., -14.],
116 [3., -14.],
117 [3., -21.],
118 [6., -18.],
119 [6., -11.],
120 [2., -8.],
121 [2., 9.],
122 [5., 12.],
123 [5., 17.],
124 [2., 20.],
125 [2., 14.],
126 [-2., 14.],
127 [-2., 20.],
128 [-5., 17.],
129 [-5., 12.],
130 ]),
131 spyglass: poly(&[
132 [0., -2.5],
133 [6., -2.5],
134 [6., -3.5],
135 [13., -3.5],
136 [13., -4.5],
137 [19., -4.5],
138 [19., 4.5],
139 [13., 4.5],
140 [13., 3.5],
141 [6., 3.5],
142 [6., 2.5],
143 [0., 2.5],
144 ]),
145 spyglass_end: poly(&[[17., -4.5], [20., -4.5], [20., 4.5], [17., 4.5]]),
146 cursor: poly(&[[-8., -12.], [10., 1.6], [1.6, 2.8], [-2.2, 10.4]]),
147 links: [link(-1.), link(1.)],
148 jet: parse("M-2 0 C-3 -12 -9 -28 -17 -29 C-22 -29 -23 -24 -19 -21 C-25 -21 -27 -28 -23 -32 C-16 -39 -5 -32 0 -16 C5 -33 16 -38 23 -31 C27 -26 23 -20 19 -21 C23 -25 20 -29 16 -28 C8 -26 4 -12 2 0 C1 1 -1 1 -2 0 Z"),
149 drop: parse("M0 -3 C3 0 3 3 0 3 C-3 3 -3 0 0 -3 Z"),
150 water_back: parse("M-43 0 C-28 -7 -12 -4 0 -1 C14 2 28 -7 43 -2 C29 -1 15 7 0 3 C-15 -1 -28 -3 -43 0 Z"),
151 water_front: parse("M-29 0 C-11 -2 8 4 28 0 C13 8 -11 4 -29 0 Z"),
152 splashes: [mirror(-1.), mirror(1.)],
153 particle_drop: parse("M0 -2 C1 -1 2 1 0 2 C-2 1 -1 -1 0 -2 Z"),
154 cloud: parse("M-5 1 C-8 0 -7 -4 -4 -4 C-3 -8 2 -8 4 -5 C8 -5 9 0 5 2 C4 6 -3 6 -5 1 Z"),
155 bubble: ellipse(0., 0., 1., 1.),
156 }
157 })
158 }
159
160 fn visible(v: f64) -> f64 {
161 clamp(if v.is_nan() { 0. } else { v }, 0., 1.)
162 }
163
164 /// `shapes()`: every prop, in the reference's paint order.
165 pub fn shapes(q: &Pose, parts: &Parts, small_view: bool) -> Vec<Shape> {
166 let lib = library();
167 let boost = if small_view { 1.15 } else { 1. } * parts.unit_scale;
168 let mut out = Vec::with_capacity(32);
169 let mut add = |id: String, path: &Path, role: Role, x: f64, y: f64, a: f64, s: f64, v: f64| {
170 out.push(Shape {
171 id,
172 path: transform(path, x, y, a, s),
173 role,
174 opacity: visible(v),
175 });
176 };
177 let hand = parts.anchors.fin_tip;
178 let angle = parts.anchors.fin_ang;
179 let offset = |anchor: Point, x: f64, y: f64, a: f64| Point {
180 x: anchor.x + (x * a.cos() - y * a.sin()) * boost,
181 y: anchor.y + (x * a.sin() + y * a.cos()) * boost,
182 };
183 let pc = offset(hand, 22. + q[p::pageDX], -31. + q[p::pageDY], angle);
184 let pa = angle - 0.08 + q[p::pageRot] * D2R;
185 add(
186 "prop-page".into(),
187 &lib.page,
188 Role::Tool,
189 pc.x,
190 pc.y,
191 pa,
192 boost,
193 q[p::page],
194 );
195 add(
196 "prop-page-fold".into(),
197 &lib.fold,
198 Role::Cutout,
199 pc.x,
200 pc.y,
201 pa,
202 boost,
203 q[p::page],
204 );
205 for (i, rule) in lib.rules.iter().enumerate() {
206 add(
207 format!("prop-page-rule-{i}"),
208 rule,
209 Role::Cutout,
210 pc.x,
211 pc.y,
212 pa,
213 boost,
214 q[p::page],
215 );
216 }
217 add(
218 "prop-page-turn".into(),
219 &lib.page,
220 Role::Tool,
221 pc.x + 2. * boost,
222 pc.y,
223 pa,
224 boost * (q[p::pageFlip] * std::f64::consts::PI / 2.).sin() * 0.75,
225 q[p::page] * q[p::pageFlip],
226 );
227 let pad = offset(hand, 21., -16., angle);
228 add(
229 "prop-pad".into(),
230 &lib.pad,
231 Role::Tool,
232 pad.x,
233 pad.y,
234 angle - 0.05,
235 boost,
236 q[p::pad],
237 );
238 add(
239 "prop-pad-binding".into(),
240 &lib.binding,
241 Role::Cutout,
242 pad.x,
243 pad.y,
244 angle - 0.05,
245 boost,
246 q[p::pad],
247 );
248 for (i, line) in lib.pad_lines.iter().enumerate() {
249 add(
250 format!("prop-pad-line-{i}"),
251 line,
252 Role::Cutout,
253 pad.x,
254 pad.y,
255 angle - 0.05,
256 boost,
257 q[p::pad] * clamp(q[p::padLines] - i as f64, 0., 1.),
258 );
259 }
260 let pen = offset(
261 hand,
262 16.4 + q[p::scribX] * 0.35,
263 -23.3 + q[p::scribY] * 0.35,
264 angle,
265 );
266 add(
267 "prop-pencil".into(),
268 &lib.pencil,
269 Role::Accent,
270 pen.x,
271 pen.y,
272 angle - 0.48,
273 boost,
274 q[p::pencil],
275 );
276 add(
277 "prop-pencil-tip".into(),
278 &lib.pencil_tip,
279 Role::Tool,
280 pen.x,
281 pen.y,
282 angle - 0.48,
283 boost,
284 q[p::pencil],
285 );
286 let la = angle + 0.55;
287 let lc = offset(hand, 3., -42., la);
288 add(
289 "prop-lens".into(),
290 &lib.lens,
291 Role::Tool,
292 lc.x,
293 lc.y,
294 la,
295 boost,
296 q[p::lens],
297 );
298 add(
299 "prop-lens-handle".into(),
300 &lib.lens_handle,
301 Role::Tool,
302 lc.x,
303 lc.y,
304 la,
305 boost,
306 q[p::lens],
307 );
308 let wc = offset(parts.anchors.tail, 40., -5. + q[p::wrenchY], angle);
309 add(
310 "prop-wrench".into(),
311 &lib.wrench,
312 Role::Tool,
313 wc.x,
314 wc.y,
315 parts.anchors.tail_ang + 1.1 + q[p::wrenchSpin] * D2R,
316 if small_view { 0.78 } else { 0.7 } * parts.unit_scale,
317 q[p::wrench],
318 );
319 let ga = angle - 0.22;
320 let gs = boost * (0.95 + 0.3 * clamp(q[p::glassExt], 0., 1.));
321 let gc = offset(hand, 12., -33., ga);
322 add(
323 "prop-spyglass".into(),
324 &lib.spyglass,
325 Role::Accent,
326 gc.x,
327 gc.y,
328 ga,
329 gs,
330 q[p::glass],
331 );
332 add(
333 "prop-spyglass-end".into(),
334 &lib.spyglass_end,
335 Role::Tool,
336 gc.x,
337 gc.y,
338 ga,
339 gs,
340 q[p::glass],
341 );
342 let cursor = offset(
343 parts.anchors.spout,
344 11. + q[p::cursorX],
345 -31. + q[p::cursorY],
346 angle,
347 );
348 add(
349 "prop-cursor".into(),
350 &lib.cursor,
351 Role::Pointer,
352 cursor.x,
353 cursor.y,
354 angle,
355 boost,
356 q[p::cursor],
357 );
358 let link = offset(parts.anchors.spout, 16., -34., angle);
359 let link_angle = angle + (0.55 + q[p::linkTilt] * D2R);
360 for (index, side) in [-1, 1].iter().enumerate() {
361 add(
362 format!("prop-link-{side}"),
363 &lib.links[index],
364 Role::Pointer,
365 link.x,
366 link.y,
367 link_angle,
368 boost,
369 q[p::link],
370 );
371 }
372 let spout = clamp(q[p::spout], 0., 1.);
373 let surface = clamp(q[p::splash], 0., 1.);
374 let origin = parts.anchors.spout;
375 let unit = parts.unit_scale;
376 add(
377 "prop-spout".into(),
378 &lib.jet,
379 Role::Water,
380 origin.x,
381 origin.y,
382 0.,
383 (0.35 + 0.65 * spout) * unit,
384 spout,
385 );
386 for side in [-1., 1.] {
387 add(
388 format!("prop-spout-drop-{}", side as i32),
389 &lib.drop,
390 Role::Water,
391 origin.x + side * 25. * unit,
392 origin.y - 35. * unit,
393 -side * 0.35,
394 unit * spout,
395 spout,
396 );
397 }
398 add(
399 "prop-water-back".into(),
400 &lib.water_back,
401 Role::Water,
402 0.,
403 43.,
404 0.,
405 0.8 + 0.2 * surface,
406 surface,
407 );
408 add(
409 "prop-water-front".into(),
410 &lib.water_front,
411 Role::Water,
412 0.,
413 50.,
414 0.,
415 0.8 + 0.2 * surface,
416 surface,
417 );
418 for (index, side) in [-1., 1.].iter().enumerate() {
419 add(
420 format!("prop-water-splash-{}", *side as i32),
421 &lib.splashes[index],
422 Role::Water,
423 side * 35.,
424 39.,
425 0.,
426 0.5 + 0.5 * surface,
427 surface,
428 );
429 }
430 out
431 }
432
433 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
434 pub enum ParticleKind {
435 Drop,
436 Mist,
437 Thought,
438 Bubble,
439 }
440
441 #[derive(Clone, Debug)]
442 pub struct Particle {
443 pub kind: ParticleKind,
444 pub x: f64,
445 pub y: f64,
446 pub vx: f64,
447 pub vy: f64,
448 pub g: f64,
449 pub drag: f64,
450 pub r: f64,
451 pub age: f64,
452 pub life: f64,
453 pub cloud: bool,
454 /// The acting state that emitted it; a particle never outlives it.
455 pub owner: super::data::Act,
456 }
457
458 /// `particles()`: drops and bubbles as filled water shapes.
459 pub fn particles(list: &[Particle], small_view: bool) -> Vec<Shape> {
460 let lib = library();
461 let mut out = Vec::new();
462 for (i, particle) in list.iter().enumerate() {
463 if particle.age < 0.
464 || particle.kind == ParticleKind::Mist
465 || (small_view && particle.kind == ParticleKind::Bubble)
466 {
467 continue;
468 }
469 let fade = clamp(
470 (particle.life - particle.age) / (particle.life * 0.2),
471 0.,
472 1.,
473 );
474 let r = particle.r * if small_view { 1.65 } else { 1. };
475 let (mut x, mut y) = (particle.x, particle.y);
476 if small_view && particle.kind == ParticleKind::Thought {
477 x += 4.;
478 y -= 6.;
479 }
480 if particle.kind == ParticleKind::Drop {
481 out.push(Shape {
482 id: format!("spout-{i}"),
483 path: transform(&lib.particle_drop, x, y, 0., r),
484 role: Role::Water,
485 opacity: fade,
486 });
487 } else {
488 let (path, scale) = if particle.cloud {
489 (&lib.cloud, r / 6.)
490 } else {
491 (&lib.bubble, r)
492 };
493 out.push(Shape {
494 id: format!("bubble-{i}"),
495 path: transform(path, x, y, 0., scale),
496 role: Role::Water,
497 opacity: fade,
498 });
499 }
500 }
501 out
502 }
503
503 lines RUST