| 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 |