返回 CodeWhale
acting.rs
1 //! Port of `acting.js`: presence → acting state → pose over time.
2 //!
3 //! ```text
4 //! pose(t) = spring(base pose of the state) state blends, follow-through
5 //! + Σ one-shot clips (enter, exit, beats) authored, exact frame timing
6 //! + loop clip of the state the ongoing performance
7 //! + ambient (breath, bob, blinks, puffs) alive, never dancing
8 //! + drag (flukes and fins trail the body) secondary motion
9 //! ```
10 //!
11 //! Nothing here invents activity: every acting state comes from the owner's
12 //! presence, every work action from the owner's activity kind, every beat
13 //! from an observed owner onset. The Director owns no status timer and no
14 //! telemetry; it is a pure function of its inputs and the frames it steps.
15
16 use super::data::{Act, Ambient, Clip, Emit, N, Pose, PosterParticle, p, tables};
17 use super::math::{Rng, TAU, clamp, lerp};
18 use super::props::{Particle, ParticleKind};
19 use super::rig::{Anchors, MARK_DIRECTION, build};
20 use std::collections::{HashSet, VecDeque};
21
22 pub const FPS: f64 = 30.;
23
24 /// The authoritative owner presence. The first seven are today's GPUI
25 /// states; `Stuck` is the handoff's **proposed** eighth state, accepted here
26 /// for conformance only — the app never emits it.
27 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
28 pub enum Presence {
29 Offline,
30 Idle,
31 Listening,
32 Thinking,
33 Working,
34 NeedsYou,
35 Done,
36 Stuck,
37 }
38
39 /// One observed foreground span onset reported by the owner.
40 #[derive(Clone, Debug, PartialEq)]
41 pub struct Span {
42 pub kind: String,
43 pub since_ms: f64,
44 }
45
46 /// The owner's activity, exactly as reported. `observed` is true only when
47 /// the owner strictly said so; `parallel` keeps its raw number so a
48 /// non-integer count is refused rather than rounded.
49 #[derive(Clone, Debug, Default, PartialEq)]
50 pub struct Activity {
51 pub kind: Option<String>,
52 pub observed: bool,
53 pub parallel: Option<f64>,
54 pub active: Vec<Span>,
55 }
56
57 #[derive(Clone, Debug, Default, PartialEq)]
58 pub struct Context {
59 /// `freshness === 'Live'`. Missing or stale freshness is plain work.
60 pub live: bool,
61 /// Stable turn identity; a completed one authorizes one Done flourish.
62 pub turn_id: Option<String>,
63 /// The explicit terminal status: completed, failed, error, canceled,
64 /// interrupted. Never inferred.
65 pub status: Option<String>,
66 pub now_ms: Option<f64>,
67 pub failed_at_ms: Option<f64>,
68 }
69
70 #[derive(Clone, Debug, PartialEq)]
71 pub enum Event {
72 Tool {
73 kind: String,
74 },
75 Failed {
76 kind: String,
77 status: Option<String>,
78 },
79 }
80
81 /// `actingFor()`.
82 pub fn acting_for(presence: Presence, activity: Option<&Activity>, context: &Context) -> Act {
83 match presence {
84 Presence::Offline => Act::Sleep,
85 Presence::Idle => Act::Rest,
86 Presence::Listening => Act::Listen,
87 Presence::Thinking => Act::Think,
88 Presence::NeedsYou => Act::Needs,
89 Presence::Done => Act::Done,
90 Presence::Stuck => {
91 let failed = matches!(context.status.as_deref(), Some("failed" | "error"));
92 let age = match (context.now_ms, context.failed_at_ms) {
93 (Some(now), Some(at)) => now - at,
94 _ => f64::NAN,
95 };
96 if failed && (0. ..3000.).contains(&age) {
97 Act::Hmm
98 } else {
99 Act::Rest
100 }
101 }
102 Presence::Working => match activity {
103 Some(activity) if activity.observed && context.live => activity
104 .kind
105 .as_deref()
106 .and_then(|kind| tables().act_for_kind(kind))
107 .unwrap_or(Act::Busy),
108 _ => Act::Busy,
109 },
110 }
111 }
112
113 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
114 pub enum ClipId {
115 Enter(Act),
116 Loop(Act),
117 Beat(Act),
118 Exit(Act),
119 Wake,
120 HmmBeat,
121 Puff,
122 Blink,
123 }
124
125 impl ClipId {
126 pub fn clip(self) -> &'static Clip {
127 let t = tables();
128 match self {
129 ClipId::Enter(act) => &t.act(act).enter,
130 ClipId::Loop(act) => &t.act(act).loop_,
131 ClipId::Beat(act) => t.act(act).beat.as_ref().expect("beat"),
132 ClipId::Exit(act) => t.act(act).exit.as_ref().expect("exit"),
133 ClipId::Wake => &t.wake,
134 ClipId::HmmBeat => &t.hmm_beat,
135 ClipId::Puff => &t.puff,
136 ClipId::Blink => &t.blink,
137 }
138 }
139 }
140
141 #[derive(Clone, Copy, Debug)]
142 pub struct Shot {
143 pub clip: ClipId,
144 pub at: f64,
145 pub owner: Act,
146 pub exit: bool,
147 pub wake: bool,
148 pub beat: bool,
149 }
150
151 #[derive(Clone, Copy, Debug, Default)]
152 pub struct Calf {
153 pub value: f64,
154 pub velocity: f64,
155 pub target: f64,
156 }
157
158 #[derive(Clone, Copy, Debug, PartialEq)]
159 pub struct Emission {
160 pub kind: Emit,
161 pub owner: Act,
162 pub f: f64,
163 }
164
165 #[derive(Clone, Copy, Debug)]
166 struct AmbientClock {
167 blink_at: f64,
168 dart_at: f64,
169 puff_at: f64,
170 sleep_at: f64,
171 look_at: f64,
172 look: (f64, f64),
173 cur: (f64, f64),
174 }
175
176 #[derive(Clone, Copy, Debug)]
177 struct Departure {
178 at: f64,
179 offset: Pose,
180 }
181
182 #[derive(Clone, Debug)]
183 pub struct Options {
184 pub seed: u32,
185 pub reduced: bool,
186 pub ambient: bool,
187 }
188
189 impl Default for Options {
190 fn default() -> Self {
191 Self {
192 seed: 7,
193 reduced: false,
194 ambient: true,
195 }
196 }
197 }
198
199 /// One session's performance. Drop it when the foreground session changes:
200 /// no pending clip, calf count or completed-turn identity may carry over.
201 #[derive(Clone, Debug)]
202 pub struct Director {
203 rng: Rng,
204 /// Frame time at 30 fps, floating.
205 pub f: f64,
206 pub presence: Presence,
207 pub activity: Option<Activity>,
208 pub context: Context,
209 pub acting: Act,
210 pub reduced: bool,
211 ambient_on: bool,
212 x: Pose,
213 v: Pose,
214 pub shots: Vec<Shot>,
215 loops: Vec<Shot>,
216 pub particles: Vec<Particle>,
217 pub lines: f64,
218 pub events: VecDeque<(Event, f64)>,
219 pub emissions: VecDeque<Emission>,
220 amb: AmbientClock,
221 pose_out: Pose,
222 departure: Option<Departure>,
223 changed_at: f64,
224 from_target: Pose,
225 delays: Pose,
226 done_turns: HashSet<String>,
227 spans: VecDeque<String>,
228 span_set: HashSet<String>,
229 pub calves: [Calf; 3],
230 anchors: Option<Anchors>,
231 }
232
233 const HELD_PROPS: [usize; 9] = [
234 p::page,
235 p::pad,
236 p::pencil,
237 p::lens,
238 p::wrench,
239 p::glass,
240 p::pod,
241 p::cursor,
242 p::link,
243 ];
244
245 impl Director {
246 pub fn new(options: Options) -> Self {
247 let seed = if options.seed == 0 { 7 } else { options.seed };
248 let mut director = Self {
249 rng: Rng::new(seed),
250 f: 0.,
251 presence: Presence::Idle,
252 activity: None,
253 context: Context::default(),
254 acting: Act::Rest,
255 reduced: options.reduced,
256 ambient_on: options.ambient,
257 x: [0.; N],
258 v: [0.; N],
259 shots: Vec::new(),
260 loops: vec![Shot {
261 clip: ClipId::Loop(Act::Rest),
262 at: 0.,
263 owner: Act::Rest,
264 exit: false,
265 wake: false,
266 beat: false,
267 }],
268 particles: Vec::new(),
269 lines: 1.,
270 events: VecDeque::new(),
271 emissions: VecDeque::new(),
272 amb: AmbientClock {
273 blink_at: 40.,
274 dart_at: 120.,
275 puff_at: 360.,
276 sleep_at: 0.,
277 look_at: 0.,
278 look: (0., 0.),
279 cur: (0., 0.),
280 },
281 pose_out: [0.; N],
282 departure: None,
283 changed_at: 0.,
284 from_target: [0.; N],
285 delays: [0.; N],
286 done_turns: HashSet::new(),
287 spans: VecDeque::new(),
288 span_set: HashSet::new(),
289 calves: [Calf::default(); 3],
290 anchors: None,
291 };
292 director.x = director.target_for(Act::Rest);
293 director.pose_out = director.x;
294 director
295 }
296
297 pub fn target_for(&self, act: Act) -> Pose {
298 let mut pose = tables().defaults;
299 for (param, value) in &tables().act(act).base {
300 pose[*param] = *value;
301 }
302 pose[p::padLines] = if self.lines == 0. { 1. } else { self.lines };
303 pose
304 }
305
306 /// `set(presence, activity, context)`: the only way owner truth enters.
307 pub fn set(&mut self, presence: Presence, activity: Option<Activity>, context: Context) {
308 self.presence = presence;
309 self.activity = activity;
310 self.context = context;
311 let next = acting_for(presence, self.activity.as_ref(), &self.context);
312 let done_key = (presence == Presence::Done
313 && self.context.status.as_deref() == Some("completed"))
314 .then(|| self.context.turn_id.clone())
315 .flatten()
316 .filter(|id| !id.is_empty());
317 let celebrate = done_key.is_some_and(|key| self.done_turns.insert(key));
318 self.go(next, celebrate, celebrate && next == Act::Done);
319 let n = match (next, self.activity.as_ref().and_then(|a| a.parallel)) {
320 (Act::Pod, Some(parallel)) if parallel.fract() == 0. && parallel > 0. => {
321 parallel.min(3.) as usize
322 }
323 _ => 0,
324 };
325 let reduced = self.reduced;
326 for (i, calf) in self.calves.iter_mut().enumerate() {
327 calf.target = if i < n { 1. } else { 0. };
328 if reduced {
329 calf.value = calf.target;
330 calf.velocity = 0.;
331 }
332 }
333 // Observe every onset, including those hidden by owner priority:
334 // never queue, never replay.
335 let spans = match &self.activity {
336 Some(activity) if activity.observed && self.context.live => {
337 activity.active.iter().take(4).cloned().collect()
338 }
339 _ => Vec::new(),
340 };
341 for span in spans {
342 if !span.since_ms.is_finite() {
343 continue;
344 }
345 let since = if span.since_ms == 0. {
346 0.
347 } else {
348 span.since_ms
349 };
350 let key = format!(
351 "{}|{}|{}",
352 self.context.turn_id.as_deref().unwrap_or(""),
353 span.kind,
354 since
355 );
356 if self.span_set.insert(key.clone()) {
357 self.spans.push_back(key);
358 self.event(Event::Tool { kind: span.kind });
359 }
360 }
361 if self.span_set.len() > 512 {
362 while self.spans.len() > 256 {
363 let old = self.spans.pop_front().unwrap();
364 self.span_set.remove(&old);
365 }
366 }
367 if self.reduced {
368 self.pose_out = self.poster();
369 }
370 }
371
372 fn go(&mut self, next: Act, celebrate: bool, force: bool) {
373 if next == self.acting && !force {
374 return;
375 }
376 let prev = self.acting;
377 let current = self.pose();
378 // Freeze the visible offset, not the old clip's future movement or events.
379 let mut offset = [0.; N];
380 for i in 0..N {
381 offset[i] = current[i] - self.x[i];
382 }
383 self.departure = Some(Departure { at: self.f, offset });
384 let wake_dur = tables().wake.dur;
385 let f = self.f;
386 self.shots
387 .retain(|s| s.wake && f - s.at < wake_dur && next != Act::Sleep);
388 self.loops.clear();
389 self.particles.clear();
390 self.acting = next;
391 self.changed_at = self.f;
392 self.from_target = self.x;
393 self.delays = [0.; N];
394 self.delays[p::fin] = 3.;
395 self.delays[p::finFar] = 4.;
396 self.delays[p::fluke] = 4.;
397 self.delays[p::curl] = if prev == Act::Sleep { 2. } else { 3. };
398 let think = if next == Act::Think { 6. } else { 0. };
399 self.delays[p::lookX] = think;
400 self.delays[p::lookY] = think;
401 self.delays[p::lid] = if next == Act::Sleep { 6. } else { 0. };
402 self.delays[p::glassExt] = if next == Act::Browse { 8. } else { 0. };
403 if next == Act::Write {
404 self.lines = 1.;
405 }
406 if self.reduced {
407 self.x = self.target_for(next);
408 self.v = [0.; N];
409 self.shots.clear();
410 self.departure = None;
411 self.pose_out = self.poster();
412 return;
413 }
414 let shot = |clip, owner| Shot {
415 clip,
416 at: f,
417 owner,
418 exit: false,
419 wake: false,
420 beat: false,
421 };
422 if tables().act(prev).exit.is_some() {
423 self.shots.push(Shot {
424 exit: true,
425 ..shot(ClipId::Exit(prev), prev)
426 });
427 }
428 if prev == Act::Sleep && next != Act::Sleep {
429 self.shots.push(Shot {
430 wake: true,
431 ..shot(ClipId::Wake, next)
432 });
433 }
434 if next != Act::Done || celebrate {
435 self.shots.push(shot(ClipId::Enter(next), next));
436 }
437 let enter = tables().act(next).enter.dur;
438 self.loops = vec![Shot {
439 at: f + (enter * 0.6).min(10.),
440 ..shot(ClipId::Loop(next), next)
441 }];
442 }
443
444 pub fn set_reduced(&mut self, value: bool) {
445 if self.reduced == value {
446 return;
447 }
448 self.reduced = value;
449 self.shots.clear();
450 self.loops.clear();
451 self.particles.clear();
452 self.departure = None;
453 self.acting = acting_for(self.presence, self.activity.as_ref(), &self.context);
454 self.x = self.target_for(self.acting);
455 self.v = [0.; N];
456 for calf in &mut self.calves {
457 calf.value = calf.target;
458 calf.velocity = 0.;
459 }
460 self.pose_out = self.poster();
461 if !self.reduced {
462 self.loops = vec![Shot {
463 clip: ClipId::Loop(self.acting),
464 at: self.f,
465 owner: self.acting,
466 exit: false,
467 wake: false,
468 beat: false,
469 }];
470 }
471 }
472
473 /// An owner event. A beat plays only for matching, fresh, observed work.
474 pub fn event(&mut self, event: Event) -> bool {
475 self.events.push_back((event.clone(), self.f));
476 if self.events.len() > 40 {
477 self.events.pop_front();
478 }
479 let observed = self.activity.as_ref().is_some_and(|a| a.observed);
480 if self.presence != Presence::Working || !self.context.live || !observed {
481 return false;
482 }
483 let kind = match &event {
484 Event::Tool { kind } | Event::Failed { kind, .. } => kind,
485 };
486 if tables().act_for_kind(kind) != Some(self.acting) {
487 return false;
488 }
489 let clip = match &event {
490 Event::Tool { .. } => tables()
491 .act(self.acting)
492 .beat
493 .is_some()
494 .then_some(ClipId::Beat(self.acting)),
495 Event::Failed { status, .. } => (matches!(status.as_deref(), Some("failed" | "error"))
496 && !matches!(
497 self.acting,
498 Act::Rest
499 | Act::Sleep
500 | Act::Needs
501 | Act::Done
502 | Act::Hmm
503 | Act::Listen
504 | Act::Think
505 ))
506 .then_some(ClipId::HmmBeat),
507 };
508 let Some(clip) = clip else {
509 return false;
510 };
511 if matches!(event, Event::Tool { .. }) && self.acting == Act::Write {
512 self.lines = (self.lines + 1.).min(4.);
513 }
514 if self.reduced {
515 self.pose_out = self.poster();
516 return true;
517 }
518 self.shots.retain(|s| s.clip != clip);
519 self.shots.push(Shot {
520 clip,
521 at: self.f,
522 owner: self.acting,
523 exit: false,
524 wake: false,
525 beat: true,
526 });
527 true
528 }
529
530 /// Advance one step of at most a tenth of a second.
531 pub fn step(&mut self, dt: f64) {
532 // Invalid host time must not poison every spring and contour.
533 if !dt.is_finite() {
534 return;
535 }
536 if self.reduced {
537 self.pose_out = self.poster();
538 return;
539 }
540 let t = tables();
541 let dt = clamp(dt, 0., 1. / 10.);
542 let df = dt * FPS;
543 let f0 = self.f;
544 self.f += df;
545 let target = self.target_for(self.acting);
546 let sub = (dt * 120.).ceil().max(1.) as usize;
547 let h = dt / sub as f64;
548 for _ in 0..sub {
549 for (i, &target) in target.iter().enumerate() {
550 let (w, z) = t.springs[i];
551 let goal = if self.f - self.changed_at < self.delays[i] {
552 self.from_target[i]
553 } else {
554 target
555 };
556 self.v[i] += (w * w * (goal - self.x[i]) - 2. * z * w * self.v[i]) * h;
557 self.x[i] += self.v[i] * h;
558 }
559 for calf in &mut self.calves {
560 calf.velocity += (144. * (calf.target - calf.value) - 24. * calf.velocity) * h;
561 calf.value = clamp(calf.value + calf.velocity * h, 0., 1.);
562 }
563 }
564 let mut out = [0.; N];
565 if let Some(departure) = self.departure {
566 let w = 1. - clamp((self.f - departure.at) / 8., 0., 1.);
567 for (out, offset) in out.iter_mut().zip(departure.offset) {
568 *out = offset * w;
569 }
570 if w == 0. {
571 self.departure = None;
572 }
573 }
574 self.anchors = Some(build(MARK_DIRECTION, &self.pose_out, 0, 24., 1.).anchors);
575 let f = self.f;
576 self.shots.retain(|s| f - s.at <= s.clip.clip().dur);
577 let shots = self.shots.clone();
578 for s in &shots {
579 if f < s.at {
580 continue;
581 }
582 let clip = s.clip.clip();
583 clip.eval(f - s.at, &mut out, 1.);
584 if !s.exit && (s.wake || s.owner == self.acting) {
585 self.fire(clip, f0 - s.at, f - s.at, s.owner);
586 }
587 }
588 let loops = self.loops.clone();
589 for l in &loops {
590 if f < l.at {
591 continue;
592 }
593 let clip = l.clip.clip();
594 let lf = (f - l.at) % clip.dur;
595 let w = clamp((f - l.at) / 8., 0., 1.);
596 clip.eval(lf, &mut out, w);
597 let pf = (f0 - l.at) % clip.dur;
598 self.fire(clip, if pf <= lf { pf } else { -1. }, lf, l.owner);
599 }
600 self.ambient(&mut out);
601 out[p::fluke] += clamp(-self.v[p::y] * 2.2 - self.v[p::rot] * 0.6, -22., 22.);
602 out[p::fin] += clamp(-self.v[p::y] * 1.2, -12., 12.);
603 let mut pose = [0.; N];
604 for i in 0..N {
605 let desired = self.x[i] + out[i];
606 let prior = self.pose_out[i];
607 let delta = if i == p::wrenchSpin {
608 angle_delta(desired, prior)
609 } else {
610 desired - prior
611 };
612 let rate = t.rates[i];
613 pose[i] = prior + clamp(delta, -rate * df, rate * df);
614 }
615 for i in [p::lid, p::lidLow, p::mouth, p::yaw, p::pageFlip] {
616 pose[i] = clamp(pose[i], 0., 1.);
617 }
618 for i in HELD_PROPS {
619 pose[i] = clamp(pose[i], 0., 1.);
620 }
621 self.pose_out = pose;
622 for particle in &mut self.particles {
623 particle.age += df;
624 particle.vy += particle.g * df;
625 particle.vx *= particle.drag.powf(df);
626 particle.vy *= particle.drag.powf(df);
627 particle.x += particle.vx * df;
628 particle.y += particle.vy * df;
629 }
630 let acting = self.acting;
631 self.particles
632 .retain(|particle| particle.age < particle.life && particle.owner == acting);
633 }
634
635 fn fire(&mut self, clip: &Clip, a: f64, b: f64, owner: Act) {
636 if owner != self.acting {
637 return;
638 }
639 for (frame, kind) in &clip.events {
640 if *frame > a && *frame <= b {
641 self.emit(*kind);
642 }
643 }
644 }
645
646 fn ambient(&mut self, out: &mut Pose) {
647 if !self.ambient_on {
648 return;
649 }
650 let a: Ambient = tables().act(self.acting).ambient;
651 let f = self.f;
652 // Breath: a slow inhale, a short hold, a longer exhale.
653 let ph = (f % a.period) / a.period;
654 let half_pi = std::f64::consts::PI / 2.;
655 let b = if ph < 0.4 {
656 (ph / 0.4 * half_pi).sin()
657 } else if ph < 0.5 {
658 1.
659 } else {
660 ((ph - 0.5) / 0.5 * half_pi).cos()
661 };
662 let breath = a.breath;
663 out[p::squash] -= 0.016 * b * breath;
664 out[p::y] = out[p::y] - 0.55 * b * breath + (f / 210. * TAU).sin() * 1.1 * a.bob;
665 out[p::fluke] += 3.5 * ((f - 10.) / a.period * TAU).sin() * breath;
666 out[p::fin] += 2.2 * ((f - 6.) / a.period * TAU).sin() * breath;
667 if self.acting == Act::Rest {
668 out[p::rot] += 2.3 * (f / 240. * TAU).sin();
669 out[p::x] += 1.8 * (f / 330. * TAU).sin();
670 out[p::y] += ((f + 33.) / 210. * TAU).sin();
671 out[p::fluke] += 8. * ((f - 24.) / 180. * TAU).sin();
672 out[p::head] += 1.5 * ((f - 12.) / 250. * TAU).sin();
673 }
674 let blink = |at: f64| Shot {
675 clip: ClipId::Blink,
676 at,
677 owner: self.acting,
678 exit: false,
679 wake: false,
680 beat: false,
681 };
682 if a.blink > 0. && f >= self.amb.blink_at {
683 self.shots.push(blink(f));
684 if self.rng.draw() < 0.2 {
685 self.shots.push(blink(f + 9.));
686 }
687 self.amb.blink_at = f + (90. + self.rng.draw() * 150.) / a.blink;
688 }
689 if a.dart > 0. {
690 if f >= self.amb.dart_at {
691 let x = (self.rng.draw() - 0.5) * 0.9;
692 let y = (self.rng.draw() - 0.5) * 0.5;
693 self.amb.look = (x, y);
694 self.amb.dart_at = f + 150. + self.rng.draw() * 240.;
695 self.amb.look_at = f;
696 }
697 let t = clamp((f - self.amb.look_at) / 4., 0., 1.);
698 self.amb.cur.0 = lerp(self.amb.cur.0, self.amb.look.0, t);
699 self.amb.cur.1 = lerp(self.amb.cur.1, self.amb.look.1, t);
700 out[p::lookX] += self.amb.cur.0;
701 out[p::lookY] += self.amb.cur.1;
702 }
703 if a.puff > 0. && f >= self.amb.puff_at {
704 self.shots.push(Shot {
705 clip: ClipId::Puff,
706 ..blink(f)
707 });
708 self.amb.puff_at = f + 600. + self.rng.draw() * 540.;
709 }
710 if a.sleepy > 0. && f >= self.amb.sleep_at {
711 self.emit(Emit::Sleep);
712 self.amb.sleep_at = f + 170. + self.rng.draw() * 140.;
713 }
714 }
715
716 /// Particles are born at rig anchors, in world design units.
717 fn emit(&mut self, kind: Emit) {
718 let Some(a) = self.anchors else {
719 return;
720 };
721 let owner = self.acting;
722 self.emissions.push_back(Emission {
723 kind,
724 owner,
725 f: self.f,
726 });
727 if self.emissions.len() > 100 {
728 self.emissions.pop_front();
729 }
730 let r = &mut self.rng;
731 let _seed = r.draw();
732 let particle = |kind, x, y, vx, vy, radius, life| Particle {
733 kind,
734 x,
735 y,
736 vx,
737 vy,
738 g: 0.,
739 drag: 0.98,
740 r: radius,
741 age: 0.,
742 life,
743 cloud: false,
744 owner,
745 };
746 match kind {
747 Emit::Spout { n, big } => {
748 let source = if big { a.spout } else { a.blowhole };
749 for _ in 0..n {
750 let ang =
751 -std::f64::consts::PI / 2. + (r.draw() - 0.5) * if big { 1.3 } else { 0.8 };
752 let sp = if big { 1.55 } else { 0.9 } * (0.75 + r.draw() * 0.5);
753 let x = source.x + if big { (r.draw() - 0.5) * 26. } else { 0. };
754 let y = source.y - if big { 24. + r.draw() * 3. } else { 1. };
755 let vx = ang.cos() * if big { 0.3 } else { sp };
756 let vy = if big { -0.35 } else { ang.sin() * sp };
757 self.particles.push(Particle {
758 g: if big { 0.055 } else { 0.075 },
759 drag: 0.985,
760 ..particle(
761 ParticleKind::Drop,
762 x,
763 y,
764 vx,
765 vy,
766 if big { 1.6 } else { 1.3 },
767 if big { 35. } else { 26. },
768 )
769 });
770 }
771 for _ in 0..if big { 3 } else { 2 } {
772 let x = a.blowhole.x + (r.draw() - 0.5) * 3.;
773 let y = a.blowhole.y - 5. - r.draw() * 4.;
774 let vx = (r.draw() - 0.5) * 0.1;
775 self.particles.push(Particle {
776 drag: 0.95,
777 ..particle(
778 ParticleKind::Mist,
779 x,
780 y,
781 vx,
782 -0.12,
783 if big { 4.5 } else { 3.2 },
784 24.,
785 )
786 });
787 }
788 }
789 Emit::Thought => {
790 let bx = a.blowhole.x + 2.;
791 let by = a.blowhole.y - 3.;
792 for (i, (delay, radius, cloud)) in
793 [(0., 1.3, false), (5., 2.1, false), (10., 5.2, true)]
794 .into_iter()
795 .enumerate()
796 {
797 self.particles.push(Particle {
798 age: -delay,
799 drag: 0.99,
800 cloud,
801 ..particle(
802 ParticleKind::Thought,
803 bx + [0., 6., 14.][i],
804 by - i as f64 * 5.5,
805 0.06,
806 -0.05,
807 radius,
808 64. - delay,
809 )
810 });
811 }
812 }
813 Emit::Speech => {
814 let m = a.mouth;
815 for i in 0..3 {
816 let x = m.x + 4. + r.draw() * 2.;
817 let vx = 0.26 + r.draw() * 0.15;
818 let vy = -0.3 - r.draw() * 0.2;
819 let radius = 1.7 + r.draw() * 1.6;
820 let _seed = r.draw();
821 self.particles.push(Particle {
822 age: -(i as f64) * 6.,
823 drag: 0.985,
824 ..particle(ParticleKind::Bubble, x, m.y - 1., vx, vy, radius, 50.)
825 });
826 }
827 }
828 Emit::Blub => {
829 self.particles.push(Particle {
830 drag: 0.995,
831 ..particle(
832 ParticleKind::Bubble,
833 a.blowhole.x,
834 a.blowhole.y - 2.,
835 0.02,
836 -0.16,
837 2.2,
838 64.,
839 )
840 });
841 }
842 Emit::Sleep => {
843 self.particles.push(Particle {
844 drag: 0.997,
845 ..particle(
846 ParticleKind::Bubble,
847 a.blowhole.x,
848 a.blowhole.y - 1.5,
849 0.03,
850 -0.1,
851 1.6,
852 90.,
853 )
854 });
855 }
856 }
857 }
858
859 /// Reduced motion: the state's poster pose — no clock, no ambient, no
860 /// transitions.
861 pub fn poster(&self) -> Pose {
862 let mut pose = self.target_for(self.acting);
863 for (param, value) in &tables().act(self.acting).poster {
864 pose[*param] = *value;
865 }
866 pose[p::padLines] = self.lines;
867 pose
868 }
869
870 pub fn poster_particles(&self, anchors: &Anchors) -> Vec<Particle> {
871 let owner = self.acting;
872 let still = |kind, x, y, r, cloud| Particle {
873 kind,
874 x,
875 y,
876 vx: 0.,
877 vy: 0.,
878 g: 0.,
879 drag: 0.98,
880 r,
881 age: 10.,
882 life: 100.,
883 cloud,
884 owner,
885 };
886 let mut out = Vec::new();
887 for kind in &tables().act(self.acting).poster_particles {
888 match kind {
889 PosterParticle::ThoughtCloud => {
890 let bx = anchors.blowhole.x + 2.;
891 let by = anchors.blowhole.y - 3.;
892 out.push(still(ParticleKind::Thought, bx, by, 1.3, false));
893 out.push(still(ParticleKind::Thought, bx + 6., by - 5.5, 2.1, false));
894 out.push(still(ParticleKind::Thought, bx + 14., by - 13., 5.2, true));
895 }
896 PosterParticle::Speech => {
897 let m = anchors.mouth;
898 out.push(still(ParticleKind::Bubble, m.x + 6., m.y - 5., 1.9, false));
899 out.push(still(ParticleKind::Bubble, m.x + 9., m.y - 10., 1.3, false));
900 }
901 }
902 }
903 out
904 }
905
906 pub fn pose(&self) -> Pose {
907 self.pose_out
908 }
909
910 /// Whether anything is still moving beyond the settled loop: a clip, a
911 /// departure fade, a particle or a calf in transit. Surfaces use it to
912 /// choose their active or resting paint cap.
913 pub fn in_transition(&self) -> bool {
914 self.departure.is_some()
915 || self.shots.iter().any(|s| s.clip != ClipId::Blink)
916 || !self.particles.is_empty()
917 || self
918 .calves
919 .iter()
920 .any(|c| (c.value - c.target).abs() > 0.002 || c.velocity.abs() > 0.002)
921 }
922 }
923
924 pub fn angle_delta(a: f64, b: f64) -> f64 {
925 ((a - b + 180.) % 360. + 360.) % 360. - 180.
926 }
927
927 lines RUST