返回 CodeWhale
data.rs
1 //! The authored performance tables, read from the vendored handoff export
2 //! (`vendor/whale-character-v2/handoff/animation.json`) so the Rust Director
3 //! and the JavaScript oracle share one copy of every pose, clip, spring and
4 //! rate. The three small clips the export does not carry (the in-work hmm
5 //! beat, the resting puff and the blink) are transcribed from `acting.js`.
6
7 use serde::Deserialize;
8 use serde_json::Value;
9 use std::collections::HashMap;
10 use std::sync::OnceLock;
11
12 /// Every animated parameter, in the export's order. The pose is a fixed
13 /// array indexed by this list.
14 pub const PARAMS: [&str; 48] = [
15 "x",
16 "y",
17 "rot",
18 "scale",
19 "squash",
20 "tilt",
21 "yaw",
22 "curl",
23 "arch",
24 "head",
25 "fluke",
26 "flukeSpread",
27 "fin",
28 "finFar",
29 "lookX",
30 "lookY",
31 "lid",
32 "lidLow",
33 "eyeScale",
34 "brow",
35 "browTilt",
36 "mouth",
37 "smile",
38 "mouthSide",
39 "page",
40 "pageFlip",
41 "pageRot",
42 "pageDX",
43 "pageDY",
44 "pad",
45 "padLines",
46 "pencil",
47 "scribX",
48 "scribY",
49 "lens",
50 "wrench",
51 "wrenchSpin",
52 "wrenchY",
53 "glass",
54 "glassExt",
55 "pod",
56 "spout",
57 "splash",
58 "cursor",
59 "cursorX",
60 "cursorY",
61 "link",
62 "linkTilt",
63 ];
64 pub const N: usize = PARAMS.len();
65 pub type Pose = [f64; N];
66
67 /// Named indices into a [`Pose`].
68 #[allow(non_upper_case_globals, dead_code)]
69 pub mod p {
70 pub const x: usize = 0;
71 pub const y: usize = 1;
72 pub const rot: usize = 2;
73 pub const scale: usize = 3;
74 pub const squash: usize = 4;
75 pub const tilt: usize = 5;
76 pub const yaw: usize = 6;
77 pub const curl: usize = 7;
78 pub const arch: usize = 8;
79 pub const head: usize = 9;
80 pub const fluke: usize = 10;
81 pub const flukeSpread: usize = 11;
82 pub const fin: usize = 12;
83 pub const finFar: usize = 13;
84 pub const lookX: usize = 14;
85 pub const lookY: usize = 15;
86 pub const lid: usize = 16;
87 pub const lidLow: usize = 17;
88 pub const eyeScale: usize = 18;
89 pub const brow: usize = 19;
90 pub const browTilt: usize = 20;
91 pub const mouth: usize = 21;
92 pub const smile: usize = 22;
93 pub const mouthSide: usize = 23;
94 pub const page: usize = 24;
95 pub const pageFlip: usize = 25;
96 pub const pageRot: usize = 26;
97 pub const pageDX: usize = 27;
98 pub const pageDY: usize = 28;
99 pub const pad: usize = 29;
100 pub const padLines: usize = 30;
101 pub const pencil: usize = 31;
102 pub const scribX: usize = 32;
103 pub const scribY: usize = 33;
104 pub const lens: usize = 34;
105 pub const wrench: usize = 35;
106 pub const wrenchSpin: usize = 36;
107 pub const wrenchY: usize = 37;
108 pub const glass: usize = 38;
109 pub const glassExt: usize = 39;
110 pub const pod: usize = 40;
111 pub const spout: usize = 41;
112 pub const splash: usize = 42;
113 pub const cursor: usize = 43;
114 pub const cursorX: usize = 44;
115 pub const cursorY: usize = 45;
116 pub const link: usize = 46;
117 pub const linkTilt: usize = 47;
118 }
119
120 pub fn param_index(name: &str) -> Option<usize> {
121 PARAMS.iter().position(|p| *p == name)
122 }
123
124 /// The 17 acting states. One-to-one with the export's `acts`.
125 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
126 pub enum Act {
127 Rest,
128 Listen,
129 Think,
130 Busy,
131 Read,
132 Search,
133 Write,
134 Run,
135 Browse,
136 Talk,
137 Pod,
138 Needs,
139 Done,
140 Hmm,
141 Computer,
142 Connect,
143 Sleep,
144 }
145
146 impl Act {
147 pub const ALL: [Act; 17] = [
148 Act::Rest,
149 Act::Listen,
150 Act::Think,
151 Act::Busy,
152 Act::Read,
153 Act::Search,
154 Act::Write,
155 Act::Run,
156 Act::Browse,
157 Act::Talk,
158 Act::Pod,
159 Act::Needs,
160 Act::Done,
161 Act::Hmm,
162 Act::Computer,
163 Act::Connect,
164 Act::Sleep,
165 ];
166 pub fn id(self) -> &'static str {
167 match self {
168 Act::Rest => "rest",
169 Act::Listen => "listen",
170 Act::Think => "think",
171 Act::Busy => "busy",
172 Act::Read => "read",
173 Act::Search => "search",
174 Act::Write => "write",
175 Act::Run => "run",
176 Act::Browse => "browse",
177 Act::Talk => "talk",
178 Act::Pod => "pod",
179 Act::Needs => "needs",
180 Act::Done => "done",
181 Act::Hmm => "hmm",
182 Act::Computer => "computer",
183 Act::Connect => "connect",
184 Act::Sleep => "sleep",
185 }
186 }
187 pub fn from_id(id: &str) -> Option<Act> {
188 Act::ALL.into_iter().find(|act| act.id() == id)
189 }
190 fn index(self) -> usize {
191 Act::ALL.iter().position(|a| *a == self).unwrap()
192 }
193 }
194
195 #[derive(Clone, Copy, Debug, PartialEq)]
196 pub enum Ease {
197 Linear,
198 In,
199 Out,
200 InOut,
201 Back,
202 Step,
203 }
204
205 impl Ease {
206 fn parse(value: Option<&str>) -> Ease {
207 match value {
208 Some("l") => Ease::Linear,
209 Some("i") => Ease::In,
210 Some("o") => Ease::Out,
211 Some("b") => Ease::Back,
212 Some("s") => Ease::Step,
213 _ => Ease::InOut,
214 }
215 }
216 pub fn apply(self, t: f64) -> f64 {
217 match self {
218 Ease::Linear => t,
219 Ease::In => t * t * t,
220 Ease::Out => 1. - (1. - t).powf(3.),
221 Ease::InOut => {
222 if t < 0.5 {
223 4. * t * t * t
224 } else {
225 1. - (-2. * t + 2.).powf(3.) / 2.
226 }
227 }
228 Ease::Back => {
229 let c1 = 1.70158;
230 let c3 = c1 + 1.;
231 1. + c3 * (t - 1.).powf(3.) + c1 * (t - 1.).powf(2.)
232 }
233 Ease::Step => {
234 if t < 1. {
235 0.
236 } else {
237 1.
238 }
239 }
240 }
241 }
242 }
243
244 #[derive(Clone, Copy, Debug)]
245 pub struct Key {
246 pub frame: f64,
247 pub value: f64,
248 pub ease: Ease,
249 }
250
251 /// A particle emission authored on a clip frame.
252 #[derive(Clone, Copy, Debug, PartialEq)]
253 pub enum Emit {
254 Spout { n: u32, big: bool },
255 Thought,
256 Speech,
257 Blub,
258 Sleep,
259 }
260
261 #[derive(Clone, Debug)]
262 pub struct Clip {
263 pub dur: f64,
264 /// Parameter index and its keys, in authored order.
265 pub tracks: Vec<(usize, Vec<Key>)>,
266 pub events: Vec<(f64, Emit)>,
267 }
268
269 impl Clip {
270 /// `track()` from acting.js: keys are additive offsets, omitted easing is
271 /// cubic in/out.
272 pub fn track(keys: &[Key], f: f64) -> f64 {
273 if keys.is_empty() || !f.is_finite() {
274 return 0.;
275 }
276 if f <= keys[0].frame {
277 return keys[0].value;
278 }
279 for i in 1..keys.len() {
280 let k1 = keys[i];
281 if f <= k1.frame {
282 let k0 = keys[i - 1];
283 let t = if k1.frame == k0.frame {
284 1.
285 } else {
286 (f - k0.frame) / (k1.frame - k0.frame)
287 };
288 return k0.value + (k1.value - k0.value) * k1.ease.apply(t);
289 }
290 }
291 keys[keys.len() - 1].value
292 }
293 pub fn eval(&self, f: f64, out: &mut Pose, weight: f64) {
294 if !f.is_finite() || !weight.is_finite() {
295 return;
296 }
297 for (param, keys) in &self.tracks {
298 if let Some(value) = out.get_mut(*param) {
299 *value += Self::track(keys, f) * weight;
300 }
301 }
302 }
303 }
304
305 #[derive(Clone, Copy, Debug, Default)]
306 pub struct Ambient {
307 pub breath: f64,
308 pub bob: f64,
309 pub blink: f64,
310 pub dart: f64,
311 pub puff: f64,
312 pub sleepy: f64,
313 pub period: f64,
314 }
315
316 #[derive(Clone, Copy, Debug, PartialEq)]
317 pub enum PosterParticle {
318 ThoughtCloud,
319 Speech,
320 }
321
322 #[derive(Clone, Debug)]
323 pub struct ActData {
324 /// Absolute pose: defaults, then the act's base.
325 pub base: Vec<(usize, f64)>,
326 pub enter: Clip,
327 pub loop_: Clip,
328 pub beat: Option<Clip>,
329 pub exit: Option<Clip>,
330 pub ambient: Ambient,
331 pub poster: Vec<(usize, f64)>,
332 pub poster_particles: Vec<PosterParticle>,
333 }
334
335 pub struct Tables {
336 pub defaults: Pose,
337 acts: Vec<ActData>,
338 pub springs: [(f64, f64); N],
339 pub rates: [f64; N],
340 pub wake: Clip,
341 pub hmm_beat: Clip,
342 pub puff: Clip,
343 pub blink: Clip,
344 kind_to_act: HashMap<String, Act>,
345 /// The owner's canonical kinds the export maps, for coverage checks.
346 pub owner_kinds: Vec<String>,
347 }
348
349 impl Tables {
350 pub fn act(&self, act: Act) -> &ActData {
351 &self.acts[act.index()]
352 }
353 /// `KIND_TO_ACT`: the owner's canonical kind to its action. Unknown or
354 /// future kinds have no action and fall back to plain work.
355 pub fn act_for_kind(&self, kind: &str) -> Option<Act> {
356 self.kind_to_act.get(kind).copied()
357 }
358 }
359
360 #[derive(Deserialize)]
361 #[serde(rename_all = "camelCase")]
362 struct Export {
363 fps: f64,
364 parameters: Vec<String>,
365 defaults: HashMap<String, f64>,
366 acts: HashMap<String, Value>,
367 springs: HashMap<String, (f64, f64)>,
368 rate_limits: HashMap<String, f64>,
369 wake: Value,
370 owner_kind_to_action: HashMap<String, String>,
371 }
372
373 fn clip(value: &Value) -> Clip {
374 let dur = value["dur"].as_f64().expect("clip duration");
375 let mut tracks = Vec::new();
376 if let Some(keys) = value["keys"].as_object() {
377 for (name, list) in keys {
378 let param = param_index(name).unwrap_or_else(|| panic!("unknown parameter {name}"));
379 let keys = list
380 .as_array()
381 .expect("keys")
382 .iter()
383 .map(|key| Key {
384 frame: key[0].as_f64().expect("key frame"),
385 value: key[1].as_f64().expect("key value"),
386 ease: Ease::parse(key.get(2).and_then(Value::as_str)),
387 })
388 .collect();
389 tracks.push((param, keys));
390 }
391 }
392 let events = value["events"]
393 .as_array()
394 .map(|events| {
395 events
396 .iter()
397 .map(|event| {
398 let frame = event[0].as_f64().expect("event frame");
399 let kind = match event[1].as_str().expect("event kind") {
400 "spout" => Emit::Spout {
401 n: event[2]["n"].as_u64().unwrap_or(4) as u32,
402 big: event[2]["big"].as_bool().unwrap_or(false),
403 },
404 "thought" => Emit::Thought,
405 "speech" => Emit::Speech,
406 "blub" => Emit::Blub,
407 "sleep" => Emit::Sleep,
408 other => panic!("unknown event {other}"),
409 };
410 (frame, kind)
411 })
412 .collect()
413 })
414 .unwrap_or_default();
415 Clip {
416 dur,
417 tracks,
418 events,
419 }
420 }
421
422 fn pairs(value: &Value) -> Vec<(usize, f64)> {
423 value
424 .as_object()
425 .map(|object| {
426 object
427 .iter()
428 .filter_map(|(name, value)| Some((param_index(name)?, value.as_f64()?)))
429 .collect()
430 })
431 .unwrap_or_default()
432 }
433
434 /// One authored key: `[frame, value, ease?]`.
435 type Authored<'a> = (f64, f64, Option<&'a str>);
436
437 /// A clip written the way acting.js writes it.
438 fn authored(dur: f64, tracks: &[(&str, &[Authored])], events: Vec<(f64, Emit)>) -> Clip {
439 Clip {
440 dur,
441 tracks: tracks
442 .iter()
443 .map(|(name, keys)| {
444 (
445 param_index(name).unwrap(),
446 keys.iter()
447 .map(|(frame, value, ease)| Key {
448 frame: *frame,
449 value: *value,
450 ease: Ease::parse(*ease),
451 })
452 .collect(),
453 )
454 })
455 .collect(),
456 events,
457 }
458 }
459
460 const EXPORT: &str = include_str!("../../assets/whale-motion/animation.json");
461
462 pub fn tables() -> &'static Tables {
463 static TABLES: OnceLock<Tables> = OnceLock::new();
464 TABLES.get_or_init(|| {
465 let export: Export = serde_json::from_str(EXPORT).expect("whale animation export");
466 assert_eq!(export.fps, 30.);
467 assert_eq!(export.parameters, PARAMS.map(str::to_string));
468 let mut defaults = [0.; N];
469 for (index, name) in PARAMS.iter().enumerate() {
470 defaults[index] = export.defaults[*name];
471 }
472 let acts = Act::ALL
473 .iter()
474 .map(|act| {
475 let value = &export.acts[act.id()];
476 let ambient = &value["ambient"];
477 let number = |key: &str| ambient[key].as_f64().unwrap_or(0.);
478 let poster_particles = value["poster"]["particles"]
479 .as_array()
480 .map(|list| {
481 list.iter()
482 .filter_map(|entry| match entry[0].as_str()? {
483 "thought-cloud" => Some(PosterParticle::ThoughtCloud),
484 "speech" => Some(PosterParticle::Speech),
485 _ => None,
486 })
487 .collect()
488 })
489 .unwrap_or_default();
490 ActData {
491 base: pairs(&value["base"]),
492 enter: clip(&value["enter"]),
493 loop_: clip(&value["loop"]),
494 beat: value.get("beat").map(clip),
495 exit: value.get("exit").map(clip),
496 ambient: Ambient {
497 breath: number("breath"),
498 bob: number("bob"),
499 blink: number("blink"),
500 dart: number("dart"),
501 puff: number("puff"),
502 sleepy: number("sleepy"),
503 period: number("period"),
504 },
505 poster: pairs(&value["poster"]),
506 poster_particles,
507 }
508 })
509 .collect();
510 let fallback = export.springs["_"];
511 let springs = PARAMS.map(|name| export.springs.get(name).copied().unwrap_or(fallback));
512 let rate = export.rate_limits["_"];
513 let rates = PARAMS.map(|name| export.rate_limits.get(name).copied().unwrap_or(rate));
514 let kind_to_act = export
515 .owner_kind_to_action
516 .iter()
517 .map(|(kind, act)| (kind.clone(), Act::from_id(act).expect("mapped action")))
518 .collect();
519 let mut owner_kinds: Vec<String> = export.owner_kind_to_action.keys().cloned().collect();
520 owner_kinds.sort();
521 // acting.js HMM_BEAT, PUFF and BLINK.
522 let hmm_beat = authored(
523 12.,
524 &[
525 (
526 "head",
527 &[
528 (0., 0., None),
529 (4., -4., Some("o")),
530 (8., -4., None),
531 (12., 0., None),
532 ],
533 ),
534 (
535 "tilt",
536 &[
537 (0., 0., None),
538 (5., -6., None),
539 (8., -6., None),
540 (12., 0., None),
541 ],
542 ),
543 (
544 "mouthSide",
545 &[
546 (0., 0., None),
547 (5., 1., None),
548 (8., 1., None),
549 (12., 0., None),
550 ],
551 ),
552 ],
553 Vec::new(),
554 );
555 let puff = authored(
556 34.,
557 &[
558 (
559 "y",
560 &[
561 (0., 0., None),
562 (6., 0.7, None),
563 (10., -0.6, Some("o")),
564 (34., 0., Some("io")),
565 ],
566 ),
567 (
568 "squash",
569 &[
570 (0., 0., None),
571 (6., 0.03, None),
572 (10., -0.02, None),
573 (20., 0., None),
574 ],
575 ),
576 ],
577 vec![(9., Emit::Spout { n: 3, big: false })],
578 );
579 let blink = authored(
580 7.,
581 &[(
582 "lid",
583 &[
584 (0., 0., None),
585 (2., 1., Some("i")),
586 (3., 1., None),
587 (7., 0., Some("o")),
588 ],
589 )],
590 Vec::new(),
591 );
592 Tables {
593 defaults,
594 acts,
595 springs,
596 rates,
597 wake: clip(&export.wake),
598 hmm_beat,
599 puff,
600 blink,
601 kind_to_act,
602 owner_kinds,
603 }
604 })
605 }
606
606 lines RUST