返回 CodeWhale
1 //! Port of `rig.js`: the mark is the rig. Profile contours are partitioned
2 //! from the official mark's cubic anchors; every pose keeps each named
3 //! part's command topology. Only move, cubic and close commands exist.
4
5 use super::data::{Pose, p, tables};
6 use super::math::{D2R, clamp, hypot, hypot2, lerp, round, smooth};
7 use std::collections::HashMap;
8 use std::sync::{Arc, Mutex, OnceLock};
9
10 #[derive(Clone, Copy, Debug, PartialEq)]
11 pub enum Cmd {
12 M([f64; 2]),
13 C([f64; 6]),
14 Z,
15 }
16
17 impl Cmd {
18 pub fn coords(&self) -> &[f64] {
19 match self {
20 Cmd::M(c) => c,
21 Cmd::C(c) => c,
22 Cmd::Z => &[],
23 }
24 }
25 pub fn coords_mut(&mut self) -> &mut [f64] {
26 match self {
27 Cmd::M(c) => c,
28 Cmd::C(c) => c,
29 Cmd::Z => &mut [],
30 }
31 }
32 /// `c.slice(-2)`: the command's end point.
33 pub fn end(&self) -> [f64; 2] {
34 let c = self.coords();
35 if c.is_empty() {
36 return [0., 0.];
37 }
38 [c[c.len() - 2], c[c.len() - 1]]
39 }
40 /// The same command with every coordinate mapped; `i` is the index
41 /// within the command's coordinates, so `i % 2` picks x or y.
42 pub fn map(&self, mut f: impl FnMut(usize, f64) -> f64) -> Cmd {
43 let mut out = *self;
44 for (i, v) in out.coords_mut().iter_mut().enumerate() {
45 *v = f(i, *v);
46 }
47 out
48 }
49 /// Rebuild a command of this kind from coordinates.
50 fn with(&self, coords: &[f64]) -> Cmd {
51 match self {
52 Cmd::M(_) => Cmd::M([coords[0], coords[1]]),
53 Cmd::C(_) => Cmd::C([
54 coords[0], coords[1], coords[2], coords[3], coords[4], coords[5],
55 ]),
56 Cmd::Z => Cmd::Z,
57 }
58 }
59 }
60
61 pub type Path = Vec<Cmd>;
62
63 /// `parse()`: `M`, `C` and `Z` with plain decimal numbers.
64 pub fn parse(d: &str) -> Path {
65 let bytes = d.as_bytes();
66 let mut tokens: Vec<Result<u8, f64>> = Vec::new();
67 let mut i = 0;
68 while i < bytes.len() {
69 let b = bytes[i];
70 if matches!(b, b'M' | b'C' | b'Z') {
71 tokens.push(Ok(b));
72 i += 1;
73 } else if b == b'-' || b == b'.' || b.is_ascii_digit() {
74 let start = i;
75 i += 1;
76 while i < bytes.len() && (bytes[i].is_ascii_digit() || bytes[i] == b'.') {
77 i += 1;
78 }
79 let Ok(number) = d[start..i].parse::<f64>() else {
80 return Vec::new();
81 };
82 if !number.is_finite() {
83 return Vec::new();
84 }
85 tokens.push(Err(number));
86 } else {
87 i += 1;
88 }
89 }
90 let mut path = Vec::new();
91 let mut i = 0;
92 while i < tokens.len() {
93 let Ok(op) = tokens[i] else {
94 return Vec::new();
95 };
96 i += 1;
97 let n = match op {
98 b'M' => 2,
99 b'C' => 6,
100 _ => 0,
101 };
102 let Some(coords) = tokens.get(i..i + n) else {
103 return Vec::new();
104 };
105 let Some(nums) = coords.iter().map(|t| t.err()).collect::<Option<Vec<_>>>() else {
106 return Vec::new();
107 };
108 i += n;
109 path.push(match op {
110 b'M' => Cmd::M([nums[0], nums[1]]),
111 b'C' => Cmd::C([nums[0], nums[1], nums[2], nums[3], nums[4], nums[5]]),
112 _ => Cmd::Z,
113 });
114 }
115 path
116 }
117
118 pub fn ellipse(x: f64, y: f64, rx: f64, ry: f64) -> Path {
119 let k = 0.55228475;
120 vec![
121 Cmd::M([x + rx, y]),
122 Cmd::C([x + rx, y + ry * k, x + rx * k, y + ry, x, y + ry]),
123 Cmd::C([x - rx * k, y + ry, x - rx, y + ry * k, x - rx, y]),
124 Cmd::C([x - rx, y - ry * k, x - rx * k, y - ry, x, y - ry]),
125 Cmd::C([x + rx * k, y - ry, x + rx, y - ry * k, x + rx, y]),
126 Cmd::Z,
127 ]
128 }
129
130 fn bezier(a: [f64; 2], c: &[f64; 6], t: f64) -> [f64; 2] {
131 let q = 1. - t;
132 [
133 q * q * q * a[0] + 3. * q * q * t * c[0] + 3. * q * t * t * c[2] + t * t * t * c[4],
134 q * q * q * a[1] + 3. * q * q * t * c[1] + 3. * q * t * t * c[3] + t * t * t * c[5],
135 ]
136 }
137
138 fn points(path: &Path) -> Vec<[f64; 2]> {
139 let mut out = Vec::new();
140 let mut last = [0., 0.];
141 for c in path {
142 match c {
143 Cmd::M(m) => {
144 last = *m;
145 out.push(last);
146 }
147 Cmd::C(c) => {
148 for j in 1..=10 {
149 out.push(bezier(last, c, j as f64 / 10.));
150 }
151 last = [c[4], c[5]];
152 }
153 Cmd::Z => {}
154 }
155 }
156 out
157 }
158
159 pub fn signed_area(path: &Path) -> f64 {
160 let p = points(path);
161 let mut sum = 0.;
162 for (i, a) in p.iter().enumerate() {
163 let b = p[(i + 1) % p.len()];
164 sum += a[0] * b[1] - b[0] * a[1];
165 }
166 sum / 2.
167 }
168
169 struct Sample {
170 l: f64,
171 p: [f64; 2],
172 d: [f64; 2],
173 }
174 struct Knot {
175 l0: f64,
176 l1: f64,
177 h0: f64,
178 h1: f64,
179 }
180 struct Measure {
181 length: f64,
182 knots: Vec<Knot>,
183 samples: Vec<Sample>,
184 }
185
186 fn or_one(v: f64) -> f64 {
187 if v == 0. || v.is_nan() { 1. } else { v }
188 }
189
190 fn measure(path: &Path) -> Measure {
191 let (mut last, mut length) = ([0., 0.], 0.);
192 let (mut knots, mut samples) = (Vec::new(), Vec::new());
193 for c in path {
194 match c {
195 Cmd::M(m) => {
196 last = *m;
197 samples.push(Sample {
198 l: 0.,
199 p: last,
200 d: [1., 0.],
201 });
202 }
203 Cmd::C(c) => {
204 let a = last;
205 let l0 = length;
206 let mut prev = a;
207 for j in 1..=30 {
208 let t = j as f64 / 30.;
209 let q = 1. - t;
210 let p = bezier(a, c, t);
211 let d = [
212 3. * q * q * (c[0] - a[0])
213 + 6. * q * t * (c[2] - c[0])
214 + 3. * t * t * (c[4] - c[2]),
215 3. * q * q * (c[1] - a[1])
216 + 6. * q * t * (c[3] - c[1])
217 + 3. * t * t * (c[5] - c[3]),
218 ];
219 length += hypot2(p[0] - prev[0], p[1] - prev[1]);
220 samples.push(Sample { l: length, p, d });
221 prev = p;
222 }
223 let seg = length - l0;
224 knots.push(Knot {
225 l0,
226 l1: length,
227 h0: hypot2(c[0] - a[0], c[1] - a[1]) / or_one(seg),
228 h1: hypot2(c[2] - c[4], c[3] - c[5]) / or_one(seg),
229 });
230 last = [c[4], c[5]];
231 }
232 Cmd::Z => {}
233 }
234 }
235 samples[0].d = samples[1].d;
236 Measure {
237 length,
238 knots,
239 samples,
240 }
241 }
242
243 /// `match()`: resample `target` onto `source`'s command topology.
244 fn fit(source: &Path, target: &Path) -> Path {
245 let src = measure(source);
246 let dst = measure(target);
247 let at = |u: f64| {
248 let l = clamp(u, 0., 1.) * dst.length;
249 let mut i = 1;
250 while i < dst.samples.len() - 1 && dst.samples[i].l < l {
251 i += 1;
252 }
253 let (a, b) = (&dst.samples[i - 1], &dst.samples[i]);
254 let t = (l - a.l) / or_one(b.l - a.l);
255 let d = [lerp(a.d[0], b.d[0], t), lerp(a.d[1], b.d[1], t)];
256 let n = or_one(hypot(&d));
257 (
258 [lerp(a.p[0], b.p[0], t), lerp(a.p[1], b.p[1], t)],
259 [d[0] / n, d[1] / n],
260 )
261 };
262 let mut out = vec![Cmd::M(at(0.).0)];
263 for k in &src.knots {
264 let (ap, ad) = at(k.l0 / src.length);
265 let (bp, bd) = at(k.l1 / src.length);
266 let span = (k.l1 - k.l0) / src.length * dst.length;
267 let h0 = span * k.h0.min(0.6);
268 let h1 = span * k.h1.min(0.6);
269 out.push(Cmd::C([
270 ap[0] + ad[0] * h0,
271 ap[1] + ad[1] * h0,
272 bp[0] - bd[0] * h1,
273 bp[1] - bd[1] * h1,
274 bp[0],
275 bp[1],
276 ]));
277 }
278 out.push(Cmd::Z);
279 out
280 }
281
282 fn orient_like(source: &Path, path: &Path) -> Path {
283 if signed_area(source) * signed_area(path) >= 0. {
284 return path.clone();
285 }
286 let start = path[0].end();
287 let mut at = start;
288 let mut segments: Vec<([f64; 2], [f64; 6])> = Vec::new();
289 for c in path {
290 if let Cmd::C(c) = c {
291 segments.push((at, *c));
292 at = [c[4], c[5]];
293 }
294 }
295 if hypot2(at[0] - start[0], at[1] - start[1]) > 0.01 {
296 segments.push((at, [at[0], at[1], start[0], start[1], start[0], start[1]]));
297 }
298 let mut out = vec![Cmd::M(start)];
299 for (at, c) in segments.iter().rev() {
300 out.push(Cmd::C([c[2], c[3], c[0], c[1], at[0], at[1]]));
301 }
302 out.push(Cmd::Z);
303 out
304 }
305
306 /// Match anatomical landmarks section by section.
307 fn pieces(source: &Path, stops: &[usize], targets: &[Path]) -> Path {
308 let mut begin = 0;
309 let mut result: Path = Vec::new();
310 for (i, &last) in stops.iter().enumerate() {
311 let section: Path = if begin == 0 {
312 source[..=last].to_vec()
313 } else {
314 let mut section = vec![Cmd::M(source[begin].end())];
315 section.extend_from_slice(&source[begin + 1..=last]);
316 section
317 };
318 let fitted = fit(&section, &targets[i]);
319 let from = if i > 0 { 1 } else { 0 };
320 result.extend_from_slice(&fitted[from..fitted.len() - 1]);
321 begin = last;
322 }
323 result.push(Cmd::Z);
324 result
325 }
326
327 pub const PART_IDS: [&str; 10] = [
328 "body", "pouch", "pleat-1", "pleat-2", "jaw-band", "eye", "eyelid", "flipper", "flukes",
329 "eye-far",
330 ];
331
332 pub struct Rig {
333 pub src: HashMap<&'static str, Path>,
334 pub front: HashMap<&'static str, Path>,
335 small_grid: HashMap<&'static str, Path>,
336 small: Mutex<HashMap<u64, Arc<HashMap<&'static str, Path>>>>,
337 }
338
339 const MARK: &str = include_str!("../../assets/whale-motion/mark-data.js");
340
341 fn mark_contours() -> Vec<Path> {
342 let start = MARK.find("root.WhaleMark=").expect("mark data") + "root.WhaleMark=".len();
343 let end = MARK.rfind(";})").expect("mark data end");
344 let value: serde_json::Value = serde_json::from_str(&MARK[start..end]).expect("mark json");
345 value["contours"]
346 .as_array()
347 .unwrap()
348 .iter()
349 .map(|contour| {
350 contour
351 .as_array()
352 .unwrap()
353 .iter()
354 .map(|c| {
355 let nums: Vec<f64> = c.as_array().unwrap()[1..]
356 .iter()
357 .map(|n| n.as_f64().unwrap())
358 .collect();
359 match c[0].as_str().unwrap() {
360 "M" => Cmd::M([nums[0], nums[1]]),
361 "C" => Cmd::C([nums[0], nums[1], nums[2], nums[3], nums[4], nums[5]]),
362 _ => Cmd::Z,
363 }
364 })
365 .collect()
366 })
367 .collect()
368 }
369
370 fn index_at(path: &Path, x: f64, y: f64) -> usize {
371 path.iter()
372 .position(|c| !matches!(c, Cmd::Z) && c.end() == [x, y])
373 .expect("landmark")
374 }
375
376 pub fn rig() -> &'static Rig {
377 static RIG: OnceLock<Rig> = OnceLock::new();
378 RIG.get_or_init(|| {
379 let contours = mark_contours();
380 let (outer, mut pouch, eye, pleat1, pleat2) = (
381 contours[0].clone(),
382 contours[1].clone(),
383 contours[2].clone(),
384 contours[3].clone(),
385 contours[4].clone(),
386 );
387 let mut body = outer[..125].to_vec();
388 body.push(Cmd::C([215., 92., 218., 59., 215.77, 40.96]));
389 body.push(Cmd::Z);
390 let mut flipper = vec![Cmd::M([228.68, 388.82])];
391 flipper.extend_from_slice(&outer[48..54]);
392 flipper.push(Cmd::C([273., 445., 250., 405., 228.68, 388.82]));
393 flipper.push(Cmd::Z);
394 let mut flukes = vec![Cmd::M([203.32, 110.65])];
395 flukes.extend_from_slice(&outer[125..outer.len() - 1]);
396 flukes.push(Cmd::Z);
397 let ji = index_at(&outer, 141.12, 447.47);
398 let je = index_at(&outer, 496., 236.55);
399 let pe = index_at(&pouch, 238.49, 455.19);
400 let mut jaw = vec![Cmd::M([141.12, 447.47])];
401 jaw.extend_from_slice(&outer[ji + 1..=je]);
402 jaw.push(Cmd::C([497., 249., 486., 265., 483.42, 270.06]));
403 jaw.extend_from_slice(&pouch[3..=pe]);
404 jaw.push(Cmd::C([209., 463., 169., 458., 141.12, 447.47]));
405 jaw.push(Cmd::Z);
406 let front_body = parse("M244 70 C147 65 78 114 76 228 C75 279 76 303 94 342 C112 370 127 380 151 378 C172 450 250 475 286 465 C384 453 436 399 442 341 C442 320 440 310 431 310 C353 269 270 274 215 288 C176 300 138 314 121 294 C101 269 112 222 127 192 C153 145 203 127 244 128 C244 108 244 87 244 70 Z");
407 let front_pouch = parse("M416 353 C405 398 362 431 288 443 C234 448 184 410 155 352 C204 368 241 374 281 374 C328 374 375 365 416 353 Z");
408 let front_flukes = parse("M244 128 C270 130 274 154 302 151 C324 149 343 126 346 108 C321 107 314 94 302 100 C279 95 263 97 250 96 C265 86 282 67 285 52 C267 49 249 64 244 70 C244 96 244 116 244 128 Z");
409 let front_fin = parse("M165 360 C147 376 139 397 124 404 C147 408 163 402 180 390 C183 378 174 367 165 360 Z");
410 let front_jaw = parse("M155 352 C184 410 234 448 288 443 C362 431 405 398 416 353 C424 354 435 350 442 341 C436 399 384 453 286 465 C230 470 180 435 151 378 C150 368 152 359 155 352 Z");
411 let front_pleat1 = parse("M330 384 C329 405 315 424 294 434 C314 430 336 405 330 384 Z");
412 let front_pleat2 = parse("M247 386 C247 404 261 423 277 433 C257 426 242 403 247 386 Z");
413 // SRC in the JavaScript key order; pouch is the mark contour itself.
414 let mut src_list: Vec<(&'static str, Path)> = Vec::new();
415 src_list.push(("body", body));
416 // Jaw was built from the pouch before the seam pass mutates it.
417 let jaw_band = jaw;
418 pouch = pouch.clone();
419 src_list.push(("pouch", pouch));
420 src_list.push(("pleat-1", pleat1));
421 src_list.push(("pleat-2", pleat2));
422 src_list.push(("jaw-band", jaw_band));
423 src_list.push(("eye", eye.clone()));
424 src_list.push(("eyelid", ellipse(294., 337., 18., 15.)));
425 src_list.push(("flipper", flipper));
426 src_list.push(("flukes", flukes));
427 src_list.push(("eye-far", eye));
428 let target: HashMap<&str, Path> = HashMap::from([
429 ("body", front_body),
430 ("pouch", front_pouch),
431 ("pleat-1", front_pleat1),
432 ("pleat-2", front_pleat2),
433 ("jaw-band", front_jaw),
434 ("eye", ellipse(200., 330., 13., 8.)),
435 ("eyelid", ellipse(200., 330., 18., 15.)),
436 ("flipper", front_fin),
437 ("flukes", front_flukes.clone()),
438 ("eye-far", ellipse(365., 330., 13., 8.)),
439 ]);
440 // Close any open seam with a straight cubic.
441 for (_, path) in src_list.iter_mut() {
442 let first = path[0].end();
443 let last = path[path.len() - 2].end();
444 if hypot2(first[0] - last[0], first[1] - last[1]) > 0.01 {
445 let at = path.len() - 1;
446 path.insert(
447 at,
448 Cmd::C([
449 lerp(last[0], first[0], 1. / 3.),
450 lerp(last[1], first[1], 1. / 3.),
451 lerp(last[0], first[0], 2. / 3.),
452 lerp(last[1], first[1], 2. / 3.),
453 first[0],
454 first[1],
455 ]),
456 );
457 }
458 }
459 let mut src: HashMap<&'static str, Path> = src_list.into_iter().collect();
460 let mut front: HashMap<&'static str, Path> = HashMap::new();
461 for id in PART_IDS {
462 front.insert(id, fit(&src[id], &orient_like(&src[id], &target[id])));
463 }
464 front.insert(
465 "body",
466 pieces(
467 &src["body"],
468 &[47, 53, 80, 124, 125],
469 &[
470 parse("M244 70 C147 65 78 114 76 228 C75 279 98 325 128 335 C142 347 144 363 151 378"),
471 parse("M151 378 C164 410 187 434 210 449 C231 462 261 468 286 465"),
472 parse("M286 465 C384 453 436 399 442 341 C442 320 440 310 431 310"),
473 parse("M431 310 C353 269 270 274 215 288 C176 300 138 314 121 294 C101 269 112 222 127 192 C153 145 203 127 244 128"),
474 parse("M244 128 C244 108 244 87 244 70"),
475 ],
476 ),
477 );
478 // Keep the tapered throat and parallel pleats in the attentive turn.
479 for id in ["pouch", "pleat-1", "pleat-2"] {
480 let scaled = src[id]
481 .iter()
482 .map(|c| {
483 c.map(|i, v| {
484 if i % 2 == 1 {
485 355. + (v - 355.) * 0.65
486 } else {
487 300. + (v - 300.) * 0.9
488 }
489 })
490 })
491 .collect();
492 front.insert(id, scaled);
493 }
494 front.insert(
495 "flipper",
496 pieces(
497 &src["flipper"],
498 &[4, 7],
499 &[
500 parse("M165 360 C147 376 139 397 124 404"),
501 parse("M124 404 C147 408 163 402 180 390 C183 378 174 367 165 360"),
502 ],
503 ),
504 );
505 let flukes_src = &src["flukes"];
506 let mut open_src = flukes_src[..flukes_src.len() - 2].to_vec();
507 open_src.push(Cmd::Z);
508 let mut open_target = front_flukes[..front_flukes.len() - 2].to_vec();
509 open_target.push(Cmd::Z);
510 let tail_open = fit(&open_src, &open_target);
511 let mut front_flukes_fit = tail_open[..tail_open.len() - 1].to_vec();
512 front_flukes_fit.extend_from_slice(&front_flukes[front_flukes.len() - 2..]);
513 front.insert("flukes", front_flukes_fit);
514 // Overlap only the internal closing seam.
515 for path in [src.get_mut("flukes").unwrap(), front.get_mut("flukes").unwrap()] {
516 let at = path.len() - 2;
517 // `path.at(-2)[1]` and `[3]` in `['C', x1, y1, x2, y2, x, y]`:
518 // the two control-point x coordinates.
519 let c = path[at].coords_mut();
520 c[0] -= 2.;
521 c[2] -= 2.;
522 }
523 let small_grid = HashMap::from([
524 ("body", parse("M10.5 3.5 C6 3.5 3 7 3 12 C3 16.5 5 18.5 8 18.5 C9.5 18.5 10.5 18 11 17 C10.5 18.5 9 19.5 8 19.5 C9 20.5 11 20.5 12.5 20 C17 20 21 16 21.5 11.5 C22 8.5 15 10.5 12 12 C6.5 15 5.5 13 6 10 C6 8 8 6.5 10 6.5 C10.5 5.5 10.5 4.5 10.5 3.5 Z")),
525 ("flukes", parse("M10 6.5 C11.5 6.5 11.5 8 13.5 8 C15 8 16.5 7 17 6.5 C14 6 13 5 12 6 C13 5.5 14 4.5 14.5 4 C13 3.5 12 3.5 10.5 3.5 C10.5 4.5 10.5 5.5 10 6.5 Z")),
526 ("pouch", parse("M20.5 12.5 C20 17 16 20 11.5 19.5 C15 18.5 15.5 13 20.5 12.5 Z")),
527 ("flipper", parse("M11 17 C10.5 18.5 9 19.5 8 19.5 C9 20.5 11 20.5 12.5 20 C13 19.5 12 18 11 17 Z")),
528 ("jaw-band", parse("M11 20 C17 20 21 16 22 12 C21 17 17 21 11 21 C10 21 10 20 11 20 Z")),
529 ("eye", ellipse(14., 15., 0.65, 0.46)),
530 ("eye-far", ellipse(14., 15., 0.65, 0.46)),
531 ("eyelid", ellipse(14., 15., 1., 0.7)),
532 ]);
533 Rig {
534 src,
535 front,
536 small_grid,
537 small: Mutex::new(HashMap::new()),
538 }
539 })
540 }
541
542 impl Rig {
543 /// Optical-fit sources for a 16/24/32-cell icon; authored on a 24-cell
544 /// grid, not downsampled from the hero.
545 fn small_source(&self, size: f64) -> Arc<HashMap<&'static str, Path>> {
546 let mut cache = self.small.lock().unwrap();
547 if let Some(found) = cache.get(&size.to_bits()) {
548 return found.clone();
549 }
550 let mut out = HashMap::new();
551 for id in PART_IDS {
552 let Some(grid) = self.small_grid.get(id) else {
553 out.insert(id, self.src[id].clone());
554 continue;
555 };
556 let fitted: Path = grid
557 .iter()
558 .map(|c| {
559 c.map(|_, v| {
560 let cell = round(v / 24. * size * 2.) / 2.;
561 (cell / size - 0.5) * 634.88 + 256.
562 })
563 })
564 .collect();
565 out.insert(id, fit(&self.src[id], &orient_like(&self.src[id], &fitted)));
566 }
567 let out = Arc::new(out);
568 // Arbitrary public optical sizes must not create an unbounded cache.
569 // Eviction changes no evaluated geometry; existing readers own Arcs.
570 if cache.len() >= 64 {
571 cache.clear();
572 }
573 cache.insert(size.to_bits(), out.clone());
574 out
575 }
576 }
577
578 fn rotate(x: f64, y: f64, cx: f64, cy: f64, a: f64) -> [f64; 2] {
579 let (c, s) = (a.cos(), a.sin());
580 [
581 cx + (x - cx) * c - (y - cy) * s,
582 cy + (x - cx) * s + (y - cy) * c,
583 ]
584 }
585
586 /// Morph a blade in its shoulder-to-tip frame.
587 fn morph_blade(a: &Path, b: &Path, t: f64) -> Path {
588 let aa = a[0].end();
589 let bb = b[0].end();
590 let at = a[4].end();
591 let bt = b[4].end();
592 let mut angle = (bt[1] - bb[1]).atan2(bt[0] - bb[0]) - (at[1] - aa[1]).atan2(at[0] - aa[0]);
593 angle = angle.sin().atan2(angle.cos());
594 a.iter()
595 .enumerate()
596 .map(|(i, c)| {
597 let src = c.coords();
598 let dst = b[i].coords();
599 let mut out = Vec::with_capacity(src.len());
600 let mut j = 0;
601 while j < src.len() {
602 let target = rotate(dst[j] - bb[0], dst[j + 1] - bb[1], 0., 0., -angle);
603 let p = rotate(
604 lerp(src[j] - aa[0], target[0], t),
605 lerp(src[j + 1] - aa[1], target[1], t),
606 0.,
607 0.,
608 angle * t,
609 );
610 out.push(p[0] + lerp(aa[0], bb[0], t));
611 out.push(p[1] + lerp(aa[1], bb[1], t));
612 j += 2;
613 }
614 c.with(&out)
615 })
616 .collect()
617 }
618
619 #[derive(Clone, Copy, Debug, PartialEq)]
620 pub struct Direction {
621 pub uncurl: f64,
622 pub front_width: f64,
623 }
624
625 /// `01 / Emblem`, the product direction.
626 pub const MARK_DIRECTION: Direction = Direction {
627 uncurl: 0.,
628 front_width: 1.,
629 };
630 /// `02 / Cruise` and `03 / Open C`, the reference's alternate studies.
631 pub const CRUISE_DIRECTION: Direction = Direction {
632 uncurl: 0.56,
633 front_width: 1.08,
634 };
635 pub const OPEN_DIRECTION: Direction = Direction {
636 uncurl: 0.25,
637 front_width: 0.94,
638 };
639
640 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
641 pub enum Role {
642 Body,
643 Hole,
644 Cutout,
645 Tool,
646 Accent,
647 Pointer,
648 Water,
649 }
650
651 #[derive(Clone, Debug)]
652 pub struct Shape {
653 pub id: String,
654 pub path: Path,
655 pub role: Role,
656 pub opacity: f64,
657 }
658
659 #[derive(Clone, Copy, Debug, Default, PartialEq)]
660 pub struct Point {
661 pub x: f64,
662 pub y: f64,
663 }
664
665 #[derive(Clone, Copy, Debug, Default)]
666 pub struct Anchors {
667 pub blowhole: Point,
668 pub spout: Point,
669 pub mouth: Point,
670 pub chin: Point,
671 pub eye: Point,
672 pub fin_tip: Point,
673 pub tail: Point,
674 pub tail_ang: f64,
675 pub fin_ang: f64,
676 pub head_ang: f64,
677 }
678
679 #[derive(Clone, Debug, Default)]
680 pub struct Parts {
681 pub shapes: Vec<Shape>,
682 pub anchors: Anchors,
683 pub small: bool,
684 pub unit_scale: f64,
685 }
686
687 /// The rig's own defaults (`POSE`) with props at zero — what a partial pose
688 /// such as a calf's resolves to.
689 pub fn rig_pose() -> Pose {
690 tables().defaults
691 }
692
693 /// `build()`: the named contour parts and anchors for one pose.
694 pub fn build(dir: Direction, pose: &Pose, lod: u8, size: f64, dpr: f64) -> Parts {
695 if !size.is_finite()
696 || size <= 0.
697 || !dpr.is_finite()
698 || dpr <= 0.
699 || size > 16_384.
700 || dpr > 16.
701 || !dir.uncurl.is_finite()
702 || !dir.front_width.is_finite()
703 || dir.front_width <= 0.
704 || pose.iter().any(|v| !v.is_finite())
705 {
706 return Parts::default();
707 }
708 let rig = rig();
709 let q = pose;
710 let yaw = clamp(q[p::yaw], 0., 1.);
711 let head_yaw = smooth(0., 0.82, yaw) * 0.18;
712 let tail_yaw = smooth(0.24, 1., yaw) * 0.12;
713 let uncurl = dir.uncurl * (1. - clamp(q[p::curl], 0., 1.)) + (-q[p::curl]).max(0.) * 0.9
714 - q[p::curl].max(0.) * 0.08;
715 let small = lod >= 2;
716 let tool_weight = clamp(q[p::page] + q[p::pencil] + q[p::lens] + q[p::glass], 0., 1.);
717 let targets = [
718 (p::page, 392., 222., 132.),
719 (p::pencil, 394., 218., 118.),
720 (p::lens, 380., 240., 168.),
721 (p::glass, 401., 255., 122.),
722 ];
723 let mut tx = 335. * (1. - tool_weight);
724 let mut ty = 386. * (1. - tool_weight);
725 let mut total = 0.;
726 for (key, x, y, neutral) in targets {
727 let weight = q[key];
728 total += weight;
729 tx += (x + (q[p::fin] - neutral) * 0.38) * weight;
730 ty += (y + (q[p::fin] - neutral) * 0.18) * weight;
731 }
732 if total > 1. {
733 tx /= total;
734 ty /= total;
735 }
736 let lift = smooth(30., 98., q[p::fin]);
737 let attention = smooth(0.65, 1., yaw) * (1. - tool_weight);
738 let tw = tool_weight;
739 let held_fin = pieces(
740 &rig.src["flipper"],
741 &[4, 7],
742 &[
743 vec![
744 Cmd::M([230., 398.]),
745 Cmd::C([
746 238.,
747 408.,
748 lerp(252., 250., tw),
749 lerp(427., 425., tw),
750 lerp(262., 255., tw),
751 lerp(438., 435., tw),
752 ]),
753 Cmd::C([
754 lerp(290., 240., tw),
755 lerp(416., 350., tw),
756 tx - lerp(10., 35., tw),
757 ty + lerp(18., 5., tw),
758 tx,
759 ty,
760 ]),
761 ],
762 vec![
763 Cmd::M([tx, ty]),
764 Cmd::C([tx - 66., ty + 20., 230., 357., 230., 398.]),
765 ],
766 ],
767 );
768 let small_src = small.then(|| rig.small_source(size));
769 let source = |id: &str| -> &Path {
770 match &small_src {
771 Some(s) => &s[id],
772 None => &rig.src[id],
773 }
774 };
775 let profile_fin = morph_blade(source("flipper"), &held_fin, lift);
776 let posed_fin = morph_blade(&profile_fin, &rig.front["flipper"], head_yaw);
777 let transform = |id: &str, x: f64, y: f64| -> [f64; 2] {
778 let (mut x, mut y) = (x, y);
779 let w = (1. - smooth(165., 295., y)) * smooth(315., 135., x);
780 x -= uncurl * 48. * w;
781 y += uncurl * 60. * w;
782 if id == "flukes" {
783 let hinge = lerp(220., 246., tail_yaw);
784 let weight = smooth(hinge, hinge + 75., x);
785 [x, y] = rotate(x, y, hinge, 110., (q[p::fluke] - 6.) * 0.48 * D2R * weight);
786 }
787 if y > 285. {
788 let jaw_weight = smooth(320., 470., y);
789 y += q[p::mouth] * 22. * jaw_weight;
790 if id == "pouch" {
791 y += (q[p::smile] - 0.4) * 7. * ((x - 230.) / 260. * std::f64::consts::PI).sin();
792 y += q[p::mouthSide] * 10. * smooth(280., 480., x) * (1. - smooth(400., 460., y));
793 }
794 }
795 let head_weight = smooth(185., 470., x) * smooth(150., 300., y);
796 [x, y] = rotate(x, y, 290., 305., -q[p::head] * 0.34 * D2R * head_weight);
797 x = (x - 256.) / 5.12;
798 y = (y - 256.) / 5.12;
799 x *= q[p::scale] * (1. + q[p::squash] * 0.5);
800 y *= q[p::scale] * (1. - q[p::squash]);
801 [x, y] = rotate(x, y, 0., 0., (q[p::rot] + q[p::tilt] * 0.28) * D2R);
802 if small {
803 x *= 0.9;
804 y *= 0.9;
805 }
806 [x + q[p::x], y + q[p::y]]
807 };
808 let mut shapes: Vec<Shape> = Vec::with_capacity(12);
809 for id in [
810 "flukes", "body", "jaw-band", "pouch", "pleat-1", "pleat-2", "flipper", "eye", "eye-far",
811 "eyelid",
812 ] {
813 let a = source(id);
814 let b = &rig.front[id];
815 let t = if id == "flukes" { tail_yaw } else { head_yaw };
816 let mut opacity = 1.;
817 if id == "flipper" {
818 opacity = lift * (1. - attention);
819 }
820 if id == "jaw-band" {
821 opacity = 0.;
822 }
823 if id.starts_with("pleat") && small {
824 opacity = 0.;
825 }
826 if id.starts_with("eye") && small && 26. / 512. * size * dpr < 2. {
827 opacity = 0.;
828 }
829 if id == "eyelid" || id == "eye-far" {
830 opacity = 0.;
831 }
832 let path: Path = a
833 .iter()
834 .enumerate()
835 .map(|(i, c)| {
836 if matches!(c, Cmd::Z) {
837 return Cmd::Z;
838 }
839 let src = c.coords();
840 let dst = b[i].coords();
841 let mut out = Vec::with_capacity(src.len());
842 let mut j = 0;
843 while j < src.len() {
844 let blend = if id == "body" {
845 lerp(head_yaw, tail_yaw, 1. - smooth(140., 260., src[j + 1]))
846 } else {
847 t
848 };
849 let mut x = lerp(src[j], dst[j], blend);
850 let mut y = lerp(src[j + 1], dst[j + 1], blend);
851 if id == "body" {
852 x -= 8.
853 * (yaw * std::f64::consts::PI).sin()
854 * smooth(90., 0., hypot2(src[j] - 160., src[j + 1] - 442.));
855 }
856 if id == "flipper" {
857 let fin = posed_fin[i].coords();
858 x = fin[j];
859 y = fin[j + 1];
860 }
861 if yaw > 0. {
862 x = 256. + (x - 256.) * lerp(1., dir.front_width, head_yaw);
863 }
864 if id == "eye" || id == "eye-far" {
865 let cx = if id == "eye" {
866 lerp(294., 200., head_yaw)
867 } else {
868 lerp(294., 365., head_yaw)
869 };
870 let cy = lerp(337., 330., head_yaw);
871 let visible = if id == "eye-far" { 0. } else { 1. };
872 x = cx + (x - cx) * q[p::eyeScale] * visible;
873 y = cy
874 + (y - cy)
875 * q[p::eyeScale]
876 * (1. - q[p::lid] * 0.97 - q[p::lidLow] * 0.64)
877 * visible;
878 y += q[p::lidLow] * 3. * (((x - cx) / 13.).powf(2.) - 1.);
879 x += q[p::lookX] * 2.;
880 y += q[p::lookY] * 2.;
881 }
882 let xy = transform(id, x, y);
883 let grid = 124. / (size * dpr);
884 if small {
885 out.push(round(xy[0] / grid * 2.) / 2. * grid);
886 out.push(round(xy[1] / grid * 2.) / 2. * grid);
887 } else {
888 out.extend_from_slice(&xy);
889 }
890 j += 2;
891 }
892 c.with(&out)
893 })
894 .collect();
895 let role = if matches!(id, "pouch" | "eye" | "eye-far") {
896 Role::Hole
897 } else {
898 Role::Body
899 };
900 shapes.push(Shape {
901 id: id.to_string(),
902 path,
903 role,
904 opacity,
905 });
906 }
907 // The legacy named lid geometry; closing the eye narrows its aperture.
908 let cy = lerp(337., 328., head_yaw);
909 let lid_path: Path = rig.src["eyelid"]
910 .iter()
911 .enumerate()
912 .map(|(i, c)| {
913 if matches!(c, Cmd::Z) {
914 return Cmd::Z;
915 }
916 let src = c.coords();
917 let dst = rig.front["eyelid"][i].coords();
918 let mut out = Vec::new();
919 let mut j = 0;
920 while j < src.len() {
921 let x = lerp(src[j], dst[j], head_yaw);
922 let y = lerp(src[j + 1], dst[j + 1], head_yaw);
923 out.extend_from_slice(&transform(
924 "eye",
925 x,
926 cy - 13. + (y - cy + 15.) * q[p::lid] * 0.40,
927 ));
928 j += 2;
929 }
930 c.with(&out)
931 })
932 .collect();
933 let lid = shapes.iter_mut().find(|s| s.id == "eyelid").unwrap();
934 lid.opacity = 0.;
935 lid.path = lid_path;
936 let anchor = |x: f64, y: f64| {
937 let z = transform("body", x, y);
938 Point { x: z[0], y: z[1] }
939 };
940 let tip = shapes.iter().find(|s| s.id == "flipper").unwrap().path[4].end();
941 // The mark's own flipper separation, carried up with the arm.
942 let left = [[230., 398.], [230., 357.], [tx - 66., ty + 20.], [tx, ty]];
943 let mix = |a: [f64; 2], b: [f64; 2], t: f64| [lerp(a[0], b[0], t), lerp(a[1], b[1], t)];
944 let q1 = mix(left[0], left[1], 0.42);
945 let mid = mix(left[1], left[2], 0.42);
946 let q2 = mix(q1, mid, 0.42);
947 let q3 = mix(q2, mix(mid, mix(left[2], left[3], 0.42), 0.42), 0.42);
948 let point = |pt: [f64; 2], dx: f64, dy: f64| transform("body", pt[0] + dx, pt[1] + dy);
949 let cat = |a: [f64; 2], b: [f64; 2], c: [f64; 2]| [a[0], a[1], b[0], b[1], c[0], c[1]];
950 let start = point(left[0], -6., -4.);
951 let gap = Shape {
952 id: "flipper-gap".into(),
953 path: vec![
954 Cmd::M(start),
955 Cmd::C(cat(
956 point(q1, -7., -2.),
957 point(q2, -5., -1.),
958 point(q3, 0., 0.),
959 )),
960 Cmd::C(cat(
961 point(q2, 0., 0.),
962 point(q1, 0., 0.),
963 point(left[0], -6., -4.),
964 )),
965 Cmd::Z,
966 ],
967 role: Role::Hole,
968 opacity: lift * (1. - head_yaw) * (1. - attention),
969 };
970 let at = shapes.iter().position(|s| s.id == "flipper").unwrap();
971 shapes.insert(at, gap);
972 let tail = transform(
973 "flukes",
974 lerp(289., 296., tail_yaw),
975 lerp(113., 121., tail_yaw),
976 );
977 let anchors = Anchors {
978 blowhole: anchor(lerp(325., 278., head_yaw), lerp(234., 278., head_yaw)),
979 spout: anchor(410., 222.),
980 mouth: anchor(lerp(480., 426., head_yaw), lerp(300., 353., head_yaw)),
981 chin: anchor(lerp(397., 285., head_yaw), lerp(376., 425., head_yaw)),
982 eye: anchor(lerp(294., 200., head_yaw), lerp(337., 330., head_yaw)),
983 fin_tip: Point {
984 x: tip[0],
985 y: tip[1],
986 },
987 tail: Point {
988 x: tail[0],
989 y: tail[1],
990 },
991 tail_ang: (q[p::rot] + q[p::tilt] * 0.28 + (q[p::fluke] - 6.) * 0.35) * D2R,
992 fin_ang: (q[p::rot] + q[p::tilt] * 0.28) * D2R,
993 head_ang: (q[p::rot] + q[p::tilt] * 0.28 - q[p::head] * 0.34) * D2R,
994 };
995 Parts {
996 shapes,
997 anchors,
998 small,
999 unit_scale: q[p::scale] * if small { 0.9 } else { 1. },
1000 }
1001 }
1002
1003 /// `holesFor()`: the compound holes painted with a shape. In one ink the
1004 /// body subtracts its apertures; designated prop details are always holes.
1005 pub fn holes_for(id: &str, shapes: &[Shape], mono: bool) -> Vec<Path> {
1006 let prefix = id.strip_suffix("body").unwrap_or("");
1007 shapes
1008 .iter()
1009 .filter(|h| {
1010 (mono
1011 && id.ends_with("body")
1012 && h.role == Role::Hole
1013 && [
1014 format!("{prefix}pouch"),
1015 format!("{prefix}eye"),
1016 format!("{prefix}eye-far"),
1017 ]
1018 .contains(&h.id))
1019 || (mono && h.id == "flipper-gap" && id == "body")
1020 || (h.role == Role::Cutout
1021 && (id == "prop-page" || id == "prop-pad")
1022 && h.id.starts_with(&format!("{id}-")))
1023 })
1024 .filter_map(|h| {
1025 if !h.opacity.is_finite() {
1026 return None;
1027 }
1028 let origin = h.path.first()?.end();
1029 let w = h.opacity;
1030 Some(
1031 h.path
1032 .iter()
1033 .map(|c| c.map(|i, v| lerp(origin[i % 2], v, w)))
1034 .collect(),
1035 )
1036 })
1037 .collect()
1038 }
1039
1040 /// `pathString()`: the geometry receipt's exact number format.
1041 pub fn path_string(path: &Path) -> String {
1042 path.iter()
1043 .map(|c| {
1044 let op = match c {
1045 Cmd::M(_) => "M",
1046 Cmd::C(_) => "C",
1047 Cmd::Z => "Z",
1048 };
1049 let nums: Vec<String> = c.coords().iter().map(|v| super::math::fixed3(*v)).collect();
1050 format!("{op}{}", nums.join(" "))
1051 })
1052 .collect::<Vec<_>>()
1053 .join(" ")
1054 }
1055
1055 lines RUST