1use crate::motion::{Cascade, Rest, STEP, Stepper};
2
3pub const DEG: f32 = std::f32::consts::PI / 180.0;
4
5#[derive(Clone, Copy, Debug, PartialEq, Eq)]
6pub enum Level {
7 Full,
8 Subtle,
9 Off,
10}
11
12impl Level {
13 pub fn amount(self) -> f32 {
14 match self {
15 Level::Full => 1.0,
16 Level::Subtle => 0.5,
17 Level::Off => 0.0,
18 }
19 }
20
21 pub fn parse(value: &str) -> Option<Self> {
22 match value {
23 "off" => Some(Level::Off),
24 "subtle" => Some(Level::Subtle),
25 "full" => Some(Level::Full),
26 _ => None,
27 }
28 }
29}
30
31#[derive(Clone, Copy, Debug, PartialEq)]
32pub struct Harmonic {
33 pub weight: f32,
34 pub rate: f32,
35 pub phase: f32,
36}
37
38#[derive(Clone, Copy, Debug, PartialEq)]
39pub struct Drift {
40 pub yaw: [Harmonic; 3],
41 pub yaw_gain: f32,
42 pub pitch: [Harmonic; 3],
43 pub pitch_gain: f32,
44 pub roll: Harmonic,
45 pub roll_gain: f32,
46 pub offset: Harmonic,
47 pub offset_gain: f32,
48 pub calmed: bool,
49 pub quiet_roll: bool,
50}
51
52impl Default for Drift {
53 fn default() -> Self {
54 Self {
55 yaw: [
56 Harmonic {
57 weight: 0.55,
58 rate: 0.043,
59 phase: 0.0,
60 },
61 Harmonic {
62 weight: 0.3,
63 rate: 0.071,
64 phase: 1.3,
65 },
66 Harmonic {
67 weight: 0.15,
68 rate: 0.113,
69 phase: 2.1,
70 },
71 ],
72 yaw_gain: 0.25 * DEG,
73 pitch: [
74 Harmonic {
75 weight: 0.5,
76 rate: 0.037,
77 phase: 0.7,
78 },
79 Harmonic {
80 weight: 0.35,
81 rate: 0.083,
82 phase: 2.4,
83 },
84 Harmonic {
85 weight: 0.15,
86 rate: 0.127,
87 phase: 0.0,
88 },
89 ],
90 pitch_gain: 0.16 * DEG,
91 roll: Harmonic {
92 weight: 1.0,
93 rate: 0.029,
94 phase: 0.4,
95 },
96 roll_gain: 0.04 * DEG,
97 offset: Harmonic {
98 weight: 1.0,
99 rate: 0.061,
100 phase: 0.0,
101 },
102 offset_gain: 0.002,
103 calmed: true,
104 quiet_roll: true,
105 }
106 }
107}
108
109#[derive(Clone, Copy, Debug, PartialEq)]
110pub struct Orbit {
111 pub stiffness: f32,
112 pub yaw: f32,
113 pub pitch: f32,
114 pub hold: f32,
115}
116
117#[derive(Clone, Copy, Debug, PartialEq)]
118pub struct Look {
119 pub stiffness: f32,
120 pub lean: f32,
121 pub yaw: f32,
122 pub pitch: f32,
123 pub sign: f32,
124}
125
126#[derive(Clone, Copy, Debug, PartialEq)]
127pub struct Zoom {
128 pub stiffness: f32,
129 pub hold: f32,
130 pub max: f32,
131 pub rate: f32,
132}
133
134#[derive(Clone, Copy, Debug, PartialEq)]
135pub struct Wander {
136 pub idle: f32,
137 pub every: f32,
138 pub hold: f32,
139}
140
141#[derive(Clone, Copy, Debug, PartialEq)]
142pub struct Hush {
143 pub floor: f32,
144 pub stiffness: f32,
145 pub blocks_cursor: bool,
146}
147
148#[derive(Clone, Copy, Debug, PartialEq)]
149pub struct Depth {
150 pub focus: f32,
151 pub blur: f32,
152 pub floor: f32,
153}
154
155#[derive(Clone, Copy, Debug, PartialEq)]
156pub struct Nudge {
157 pub stiffness: f32,
158 pub pitch: f32,
159 pub hold: f32,
160 pub floor: f32,
161}
162
163#[derive(Clone, Copy, Debug, PartialEq)]
164pub enum Redraw {
165 Always,
166 Exact,
167 Threshold {
168 angle: f32,
169 offset: f32,
170 zoom: f32,
171 focus: f32,
172 blur: f32,
173 },
174}
175
176#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
177pub enum Stepping {
178 #[default]
179 Shared,
180 PerFrame,
181}
182
183#[derive(Clone, Copy, Debug, PartialEq)]
184pub struct Preset {
185 pub gain: f32,
186 pub orbit: Orbit,
187 pub look: Look,
188 pub zoom: Zoom,
189 pub wander: Wander,
190 pub hush: Option<Hush>,
191 pub depth: Option<Depth>,
192 pub nudge: Nudge,
193 pub drift: Drift,
194 pub redraw: Redraw,
195 pub rest: Rest,
196}
197
198impl Default for Preset {
199 fn default() -> Self {
200 Self {
201 gain: 1.0,
202 orbit: Orbit {
203 stiffness: 4.0,
204 yaw: 0.5 * DEG,
205 pitch: 0.25 * DEG,
206 hold: 2.0,
207 },
208 look: Look {
209 stiffness: 3.0,
210 lean: 0.2,
211 yaw: 0.6 * DEG,
212 pitch: 0.4 * DEG,
213 sign: 1.0,
214 },
215 zoom: Zoom {
216 stiffness: 3.0,
217 hold: 2.0,
218 max: 2.0,
219 rate: 1.1,
220 },
221 wander: Wander {
222 idle: 8.0,
223 every: 12.0,
224 hold: 3.0,
225 },
226 hush: None,
227 depth: None,
228 nudge: Nudge {
229 stiffness: 30.0,
230 pitch: 0.1 * DEG,
231 hold: 0.2,
232 floor: 0.0,
233 },
234 drift: Drift::default(),
235 redraw: Redraw::Exact,
236 rest: Rest::COARSE,
237 }
238 }
239}
240
241#[derive(Clone, Copy, Debug, PartialEq)]
242pub struct Aim {
243 pub yaw: f32,
244 pub pitch: f32,
245 pub zoom: f32,
246 pub focus: f32,
247 pub blur: f32,
248}
249
250#[derive(Clone, Copy, Debug, PartialEq)]
251pub struct Pose {
252 pub yaw: f32,
253 pub pitch: f32,
254 pub roll: f32,
255 pub offset: f32,
256 pub zoom: f32,
257 pub focus: f32,
258 pub blur: f32,
259 pub look: [f32; 2],
260 pub moved: bool,
261}
262
263impl Pose {
264 pub fn rest(focus: f32, blur: f32) -> Self {
265 Self {
266 yaw: 0.0,
267 pitch: 0.0,
268 roll: 0.0,
269 offset: 0.0,
270 zoom: 1.0,
271 focus,
272 blur,
273 look: [0.0, 0.0],
274 moved: false,
275 }
276 }
277
278 pub fn from_spec(spec: &str) -> Self {
279 let mut pose = Self::rest(0.0, 0.0);
280 for pair in spec.split(',') {
281 let Some((key, value)) = pair.split_once('=') else {
282 continue;
283 };
284 let Ok(parsed) = value.trim().parse::<f32>() else {
285 continue;
286 };
287 match key.trim() {
288 "yaw" => pose.yaw = parsed * DEG,
289 "pitch" => pose.pitch = parsed * DEG,
290 "roll" => pose.roll = parsed * DEG,
291 "dolly" => pose.offset = parsed,
292 "zoom" => pose.zoom = parsed.max(1.0),
293 "focus" => pose.focus = parsed,
294 "blur" => pose.blur = parsed,
295 "lookx" => pose.look[0] = parsed * DEG,
296 "looky" => pose.look[1] = parsed * DEG,
297 _ => {}
298 }
299 }
300 pose
301 }
302}
303
304#[derive(Clone, Copy, Debug)]
305pub struct Director {
306 level: Level,
307 preset: Preset,
308 stepping: Stepping,
309 frame_time: f32,
310 clock: Stepper,
311 ticks: u64,
312 orbit_yaw: Cascade<f32>,
313 orbit_pitch: Cascade<f32>,
314 look_yaw: Cascade<f32>,
315 look_pitch: Cascade<f32>,
316 zoom: Cascade<f32>,
317 focus: Cascade<f32>,
318 blur: Cascade<f32>,
319 nudge: Cascade<f32>,
320 calm: Cascade<f32>,
321 nudge_until: f64,
322 mouse: [f32; 2],
323 mouse_at: f64,
324 busy: bool,
325 hushed: bool,
326 base: Option<Aim>,
327 glance: Option<(Aim, f64)>,
328 rest_focus: f32,
329 rest_blur: f32,
330 user_zoom: f32,
331 zoom_at: f64,
332 input_at: f64,
333 wander: Option<Aim>,
334 wandered_at: f64,
335}
336
337fn eased(base: f32, gain: f32, value: f32, rest: Rest) -> Cascade<f32> {
338 Cascade::new(base * gain, value).with_rest(rest)
339}
340
341fn wave(term: Harmonic, t: f32) -> f32 {
342 term.weight * (std::f32::consts::TAU * term.rate * t + term.phase).sin()
343}
344
345impl Director {
346 pub fn new(level: Level, preset: Preset, rest_focus: f32, rest_blur: f32) -> Self {
347 let gain = preset.gain;
348 let focus_k = preset.depth.map(|depth| depth.focus).unwrap_or(1.0);
349 let blur_k = preset.depth.map(|depth| depth.blur).unwrap_or(1.0);
350 let calm_k = preset.hush.map(|hush| hush.stiffness).unwrap_or(1.0);
351 let focus0 = if preset.depth.is_some() {
352 rest_focus
353 } else {
354 0.0
355 };
356 let blur0 = if preset.depth.is_some() {
357 rest_blur
358 } else {
359 0.0
360 };
361 Self {
362 level,
363 preset,
364 stepping: Stepping::Shared,
365 frame_time: 0.0,
366 clock: Stepper::new(),
367 ticks: 0,
368 orbit_yaw: eased(preset.orbit.stiffness, gain, 0.0, preset.rest),
369 orbit_pitch: eased(preset.orbit.stiffness, gain, 0.0, preset.rest),
370 look_yaw: eased(preset.look.stiffness, gain, 0.0, preset.rest),
371 look_pitch: eased(preset.look.stiffness, gain, 0.0, preset.rest),
372 zoom: eased(preset.zoom.stiffness, gain, 1.0, preset.rest),
373 focus: eased(focus_k, gain, focus0, preset.rest),
374 blur: eased(blur_k, gain, blur0, preset.rest),
375 nudge: eased(preset.nudge.stiffness, gain, 0.0, preset.rest),
376 calm: eased(calm_k, gain, 1.0, preset.rest),
377 nudge_until: f64::NEG_INFINITY,
378 mouse: [0.0; 2],
379 mouse_at: f64::NEG_INFINITY,
380 busy: false,
381 hushed: false,
382 base: None,
383 glance: None,
384 rest_focus,
385 rest_blur,
386 user_zoom: 1.0,
387 zoom_at: f64::NEG_INFINITY,
388 input_at: 0.0,
389 wander: None,
390 wandered_at: 0.0,
391 }
392 }
393
394 pub fn level(&self) -> Level {
395 self.level
396 }
397
398 pub fn stepping(&self) -> Stepping {
399 self.stepping
400 }
401
402 pub fn set_stepping(&mut self, stepping: Stepping) {
403 if self.stepping == stepping {
404 return;
405 }
406 match stepping {
407 Stepping::Shared => {
408 self.ticks += u64::from(self.clock.advance_to(self.frame_time));
409 }
410 Stepping::PerFrame => {
411 self.frame_time = self.clock.time() as f32;
412 }
413 }
414 self.stepping = stepping;
415 }
416
417 pub fn with_stepping(mut self, stepping: Stepping) -> Self {
418 self.set_stepping(stepping);
419 self
420 }
421
422 pub fn set_level(&mut self, level: Level) {
423 self.level = level;
424 }
425
426 pub fn set_rest_focus(&mut self, focus: f32) {
427 self.rest_focus = focus;
428 }
429
430 pub fn set_rest_blur(&mut self, blur: f32) {
431 self.rest_blur = blur;
432 }
433
434 pub fn set_wander(&mut self, target: Option<Aim>) {
435 self.wander = target;
436 }
437
438 pub fn hush(&mut self, reading: bool) {
439 self.hushed = reading;
440 }
441
442 pub fn cursor(&mut self, x: f32, y: f32, busy: bool) {
443 self.mouse = [x.clamp(-1.0, 1.0), y.clamp(-1.0, 1.0)];
444 self.mouse_at = self.now();
445 self.busy = busy;
446 self.touch();
447 }
448
449 pub fn attend(&mut self, target: Aim) {
450 self.base = Some(target);
451 self.touch();
452 }
453
454 pub fn release(&mut self) {
455 self.base = None;
456 self.touch();
457 }
458
459 pub fn glance(&mut self, target: Aim, hold: f32) {
460 self.glance = Some((target, self.deadline(hold)));
461 self.touch();
462 }
463
464 pub fn nudge(&mut self) {
465 let amount = self.level.amount().max(self.preset.nudge.floor);
466 self.nudge.lead.target = -self.preset.nudge.pitch * amount;
467 self.nudge_until = self.deadline(self.preset.nudge.hold);
468 self.touch();
469 }
470
471 pub fn zoom_by(&mut self, steps: f32) {
472 let next = self.user_zoom * self.preset.zoom.rate.powf(steps);
473 self.user_zoom = next.clamp(1.0, self.preset.zoom.max);
474 self.zoom_at = self.now();
475 self.touch();
476 }
477
478 pub fn step(&mut self, dt: f32) -> Pose {
479 match self.stepping {
480 Stepping::Shared => {
481 let ticks = self.clock.advance(dt);
482 self.advance(ticks)
483 }
484 Stepping::PerFrame => {
485 self.frame_time += dt;
486 self.integrate_per_frame(dt);
487 self.pose()
488 }
489 }
490 }
491
492 pub fn advance_to(&mut self, to: f32) -> Pose {
493 match self.stepping {
494 Stepping::Shared => {
495 let ticks = self.clock.advance_to(to);
496 self.advance(ticks)
497 }
498 Stepping::PerFrame => {
499 if to.is_finite() && to >= self.frame_time {
500 self.step(to - self.frame_time)
501 } else {
502 self.pose()
503 }
504 }
505 }
506 }
507
508 pub fn settled(&self) -> bool {
509 self.level == Level::Off
510 && self.base.is_none()
511 && self.glance.is_none()
512 && self.user_zoom == 1.0
513 && self.orbit_yaw.settled()
514 && self.orbit_pitch.settled()
515 && self.look_yaw.settled()
516 && self.look_pitch.settled()
517 && self.zoom.settled()
518 && self.focus.settled()
519 && self.blur.settled()
520 && self.nudge.settled()
521 && self.calm.settled()
522 }
523
524 fn touch(&mut self) {
525 self.input_at = self.now();
526 }
527
528 fn now(&self) -> f64 {
529 match self.stepping {
530 Stepping::Shared => self.ticks as f64 * f64::from(STEP),
531 Stepping::PerFrame => f64::from(self.frame_time),
532 }
533 }
534
535 fn deadline(&self, hold: f32) -> f64 {
536 match self.stepping {
537 Stepping::Shared => self.now() + f64::from(hold),
538 Stepping::PerFrame => f64::from(self.frame_time + hold),
539 }
540 }
541
542 fn advance(&mut self, ticks: u32) -> Pose {
543 for _ in 0..ticks {
544 self.ticks += 1;
545 self.integrate(self.now());
546 }
547 self.pose()
548 }
549
550 fn integrate(&mut self, time: f64) {
551 let preset = self.preset;
552 let amount = self.level.amount();
553 let hushed = self.hushed;
554 self.calm.lead.target = match preset.hush {
555 Some(hush) if hushed => hush.floor,
556 _ => 1.0,
557 };
558 let blocked = preset.hush.is_some_and(|hush| hush.blocks_cursor) && hushed;
559 let mousing = time - self.mouse_at < f64::from(preset.orbit.hold) && !self.busy && !blocked;
560 let (mx, my) = if mousing {
561 (self.mouse[0], self.mouse[1])
562 } else {
563 (0.0, 0.0)
564 };
565 self.orbit_yaw.lead.target = -mx * preset.orbit.yaw * amount;
566 self.orbit_pitch.lead.target = my * preset.orbit.pitch * amount;
567 let aim = self.intent(time);
568 if time - self.zoom_at > f64::from(preset.zoom.hold) {
569 self.user_zoom = 1.0;
570 }
571 let (look_yaw, look_pitch, zoom, focus, blur) = self.aimed(aim, amount);
572 self.look_yaw.lead.target = look_yaw;
573 self.look_pitch.lead.target = look_pitch;
574 self.zoom.lead.target = zoom;
575 self.focus.lead.target = focus;
576 self.blur.lead.target = blur;
577 if time > self.nudge_until {
578 self.nudge.lead.target = 0.0;
579 }
580 self.orbit_yaw.step(STEP);
581 self.orbit_pitch.step(STEP);
582 self.look_yaw.step(STEP);
583 self.look_pitch.step(STEP);
584 self.zoom.step(STEP);
585 self.focus.step(STEP);
586 self.blur.step(STEP);
587 self.nudge.step(STEP);
588 self.calm.step(STEP);
589 }
590
591 fn integrate_per_frame(&mut self, dt: f32) {
592 let time = self.frame_time;
593 let preset = self.preset;
594 let amount = self.level.amount();
595 self.calm.lead.target = if self.hushed {
596 preset.hush.map(|hush| hush.floor).unwrap_or(1.0)
597 } else {
598 1.0
599 };
600 let blocked = preset.hush.is_some_and(|hush| hush.blocks_cursor) && self.hushed;
601 let mousing = time - (self.mouse_at as f32) < preset.orbit.hold && !self.busy && !blocked;
602 let (mx, my) = if mousing {
603 (self.mouse[0], self.mouse[1])
604 } else {
605 (0.0, 0.0)
606 };
607 self.orbit_yaw.lead.target = -mx * (preset.orbit.yaw / DEG) * DEG * amount;
608 self.orbit_pitch.lead.target = my * (preset.orbit.pitch / DEG) * DEG * amount;
609 let aim = self.intent_per_frame();
610 if time - (self.zoom_at as f32) > preset.zoom.hold {
611 self.user_zoom = 1.0;
612 }
613 let (look_yaw, look_pitch, zoom, focus, blur) = self.aimed(aim, amount);
614 self.look_yaw.lead.target = look_yaw;
615 self.look_pitch.lead.target = look_pitch;
616 self.zoom.lead.target = zoom;
617 self.focus.lead.target = focus;
618 self.blur.lead.target = blur;
619 if time > self.nudge_until as f32 {
620 self.nudge.lead.target = 0.0;
621 }
622 self.orbit_yaw.step(dt);
623 self.orbit_pitch.step(dt);
624 self.look_yaw.step(dt);
625 self.look_pitch.step(dt);
626 self.zoom.step(dt);
627 self.focus.step(dt);
628 self.blur.step(dt);
629 self.nudge.step(dt);
630 self.calm.step(dt);
631 }
632
633 fn aimed(&self, aim: Option<Aim>, amount: f32) -> (f32, f32, f32, f32, f32) {
634 let preset = self.preset;
635 let zoom = match aim {
636 Some(aim) => self.user_zoom.max(1.0 + (aim.zoom - 1.0) * amount),
637 None => self.user_zoom,
638 };
639 let (look_yaw, look_pitch) = match aim {
640 Some(aim) => {
641 let yaw = (preset.look.sign * aim.yaw * preset.look.lean)
642 .clamp(-preset.look.yaw, preset.look.yaw)
643 * amount;
644 let pitch = (aim.pitch * preset.look.lean)
645 .clamp(-preset.look.pitch, preset.look.pitch)
646 * amount;
647 (yaw, pitch)
648 }
649 None => (0.0, 0.0),
650 };
651 let (focus, blur) = if let Some(depth) = preset.depth {
652 let scale = amount.max(depth.floor);
653 match aim {
654 Some(aim) => (aim.focus, aim.blur * scale),
655 None => (self.rest_focus, self.rest_blur * scale),
656 }
657 } else {
658 (0.0, 0.0)
659 };
660 (look_yaw, look_pitch, zoom, focus, blur)
661 }
662
663 fn intent(&mut self, time: f64) -> Option<Aim> {
664 if let Some((target, until)) = self.glance {
665 if time < until {
666 return Some(target);
667 }
668 self.glance = None;
669 }
670 if let Some(target) = self.base {
671 return Some(target);
672 }
673 let wander = self.preset.wander;
674 let idle = f64::from(wander.idle);
675 if self.level == Level::Full && time - self.input_at > idle {
676 if time - self.wandered_at > f64::from(wander.every) {
677 self.wandered_at = time;
678 }
679 if time - self.wandered_at < f64::from(wander.hold) {
680 return self.wander;
681 }
682 } else if time - self.input_at <= idle {
683 self.wandered_at = time;
684 }
685 None
686 }
687
688 fn intent_per_frame(&mut self) -> Option<Aim> {
689 let time = self.frame_time;
690 if let Some((target, until)) = self.glance {
691 if time < until as f32 {
692 return Some(target);
693 }
694 self.glance = None;
695 }
696 if self.base.is_some() {
697 return self.base;
698 }
699 let wander = self.preset.wander;
700 if self.level == Level::Full && time - (self.input_at as f32) > wander.idle {
701 if time - (self.wandered_at as f32) > wander.every {
702 self.wandered_at = f64::from(time);
703 }
704 if time - (self.wandered_at as f32) < wander.hold {
705 return self.wander;
706 }
707 } else if time - (self.input_at as f32) <= wander.idle {
708 self.wandered_at = f64::from(time);
709 }
710 None
711 }
712
713 fn pose(&self) -> Pose {
714 let calm = self.calm.value();
715 let scale = self.level.amount() * if self.preset.drift.calmed { calm } else { 1.0 };
716 let hush = self.preset.hush.map(|hush| hush.floor).unwrap_or(0.0);
717 let quiet = if self.preset.drift.quiet_roll {
718 let span = 1.0 - hush;
719 if span > 0.0 {
720 ((calm - hush) / span).clamp(0.0, 1.0)
721 } else {
722 0.0
723 }
724 } else {
725 1.0
726 };
727 let t = self.now() as f32;
728 let drift = &self.preset.drift;
729 let yaw_sum = wave(drift.yaw[0], t) + wave(drift.yaw[1], t) + wave(drift.yaw[2], t);
730 let pitch_sum = wave(drift.pitch[0], t) + wave(drift.pitch[1], t) + wave(drift.pitch[2], t);
731 let (yaw, pitch, roll) = if self.stepping == Stepping::PerFrame {
732 (
733 yaw_sum * (drift.yaw_gain / DEG) * DEG * scale,
734 pitch_sum * (drift.pitch_gain / DEG) * DEG * scale,
735 wave(drift.roll, t) * (drift.roll_gain / DEG) * DEG * scale * quiet,
736 )
737 } else {
738 (
739 yaw_sum * drift.yaw_gain * scale,
740 pitch_sum * drift.pitch_gain * scale,
741 wave(drift.roll, t) * drift.roll_gain * scale * quiet,
742 )
743 };
744 let offset = wave(drift.offset, t) * drift.offset_gain * scale;
745 let pose = Pose {
746 yaw: self.orbit_yaw.value() + yaw,
747 pitch: self.orbit_pitch.value() + pitch + self.nudge.value(),
748 roll,
749 offset,
750 zoom: self.zoom.value(),
751 focus: self.focus.value(),
752 blur: self.blur.value(),
753 look: [self.look_yaw.value(), self.look_pitch.value()],
754 moved: false,
755 };
756 Pose {
757 moved: self.moved(&pose),
758 ..pose
759 }
760 }
761
762 fn moved(&self, pose: &Pose) -> bool {
763 match self.preset.redraw {
764 Redraw::Always => true,
765 Redraw::Exact => {
766 pose.yaw != 0.0
767 || pose.pitch != 0.0
768 || pose.roll != 0.0
769 || pose.offset != 0.0
770 || (pose.zoom - 1.0).abs() >= 1e-6
771 || pose.look != [0.0, 0.0]
772 }
773 Redraw::Threshold {
774 angle,
775 offset,
776 zoom,
777 focus,
778 blur,
779 } => {
780 let angles = pose.yaw.abs()
781 + pose.pitch.abs()
782 + pose.roll.abs()
783 + pose.look[0].abs()
784 + pose.look[1].abs();
785 angles > angle
786 || pose.offset.abs() > offset
787 || (pose.zoom - 1.0).abs() > zoom
788 || (pose.focus - self.rest_focus).abs() > focus
789 || pose.blur.abs() > blur
790 }
791 }
792 }
793}
794
795#[cfg(test)]
796mod tests {
797 use super::*;
798
799 struct FrameSpring {
800 lead: (f32, f32),
801 main: (f32, f32),
802 stiffness: f32,
803 }
804
805 impl FrameSpring {
806 fn new(stiffness: f32, gain: f32, value: f32) -> Self {
807 Self {
808 lead: (value, 0.0),
809 main: (value, 0.0),
810 stiffness: stiffness * gain,
811 }
812 }
813
814 fn step(&mut self, target: f32, dt: f32) -> f32 {
815 let spring = |state: &mut (f32, f32), target: f32| {
816 let steps = (dt / (1.0 / 240.0)).ceil().max(1.0) as u32;
817 let h = dt / steps as f32;
818 let damping = 2.0 * self.stiffness.sqrt();
819 for _ in 0..steps {
820 let force = -self.stiffness * (state.0 - target) - damping * state.1;
821 state.1 += force * h;
822 state.0 += state.1 * h;
823 }
824 if (state.0 - target).abs() < 1e-4 && state.1.abs() < 1e-3 {
825 state.0 = target;
826 state.1 = 0.0;
827 }
828 };
829 spring(&mut self.lead, target);
830 spring(&mut self.main, self.lead.0);
831 self.main.0
832 }
833 }
834
835 fn aim(yaw: f32, pitch: f32, zoom: f32, focus: f32, blur: f32) -> Aim {
836 Aim {
837 yaw,
838 pitch,
839 zoom,
840 focus,
841 blur,
842 }
843 }
844
845 fn depth() -> Preset {
846 Preset {
847 depth: Some(Depth {
848 focus: 5.0,
849 blur: 2.0,
850 floor: 0.0,
851 }),
852 hush: Some(Hush {
853 floor: 0.25,
854 stiffness: 2.0,
855 blocks_cursor: true,
856 }),
857 redraw: Redraw::Always,
858 ..Preset::default()
859 }
860 }
861
862 fn open() -> Director {
863 Director::new(Level::Full, depth(), 1.2, 4.0)
864 }
865
866 fn hold(director: &mut Director, seconds: f32) -> Pose {
867 let mut pose = Pose::rest(0.0, 0.0);
868 let frames = (seconds * 60.0).round() as usize;
869 for _ in 0..frames {
870 pose = director.step(1.0 / 60.0);
871 }
872 pose
873 }
874
875 fn until_settled(director: &mut Director) -> Pose {
876 let mut pose = director.step(1.0 / 60.0);
877 for _ in 0..60 * 30 {
878 if director.settled() {
879 return pose;
880 }
881 pose = director.step(1.0 / 60.0);
882 }
883 panic!("camera did not settle, zoom {}", pose.zoom);
884 }
885
886 fn still(pose: &Pose) {
887 assert_eq!(pose.yaw, 0.0);
888 assert_eq!(pose.pitch, 0.0);
889 assert_eq!(pose.roll, 0.0);
890 assert_eq!(pose.offset, 0.0);
891 assert_eq!(pose.look, [0.0, 0.0]);
892 }
893
894 fn per_frame() -> Preset {
895 let mut preset = depth();
896 preset.gain = 1.5;
897 preset.orbit.stiffness = 4.0;
898 preset.orbit.yaw = 0.4 * DEG;
899 preset.orbit.pitch = 0.2 * DEG;
900 preset.zoom.stiffness = 2.0;
901 preset.zoom.rate = 1.2;
902 preset.zoom.max = 1.8;
903 preset.zoom.hold = 10.0;
904 preset.depth = Some(Depth {
905 focus: 5.0,
906 blur: 2.0,
907 floor: 1.0,
908 });
909 preset.nudge = Nudge {
910 stiffness: 20.0,
911 pitch: 0.05 * DEG,
912 hold: 0.15,
913 floor: 0.4,
914 };
915 preset
916 }
917
918 #[test]
919 fn per_frame_steps_once_from_the_preset() {
920 let preset = per_frame();
921 let mut director =
922 Director::new(Level::Off, preset, 1.2, 4.0).with_stepping(Stepping::PerFrame);
923 assert_eq!(director.stepping(), Stepping::PerFrame);
924 director.attend(aim(0.2, -0.1, 1.1, 2.4, 8.0));
925 director.zoom_by(3.0);
926 director.nudge();
927 let mut zoom = FrameSpring::new(preset.zoom.stiffness, preset.gain, 1.0);
928 let mut focus = FrameSpring::new(preset.depth.unwrap().focus, preset.gain, 1.2);
929 let mut blur = FrameSpring::new(preset.depth.unwrap().blur, preset.gain, 4.0);
930 let mut nudge = FrameSpring::new(preset.nudge.stiffness, preset.gain, 0.0);
931 let mut time = 0.0f32;
932 let zoom_target = preset.zoom.rate.powf(3.0).min(preset.zoom.max);
933 for dt in [0.007, 0.013, 1.0 / 60.0, 0.031, 0.004, 0.052, 0.2] {
934 time += dt;
935 let expected_zoom = zoom.step(zoom_target, dt);
936 let expected_focus = focus.step(2.4, dt);
937 let expected_blur = blur.step(8.0, dt);
938 let nudge_target = if time > preset.nudge.hold {
939 0.0
940 } else {
941 -preset.nudge.pitch * preset.nudge.floor
942 };
943 let expected_nudge = nudge.step(nudge_target, dt);
944 let pose = director.step(dt);
945 assert_eq!(pose.zoom.to_bits(), expected_zoom.to_bits());
946 assert_eq!(pose.focus.to_bits(), expected_focus.to_bits());
947 assert_eq!(pose.blur.to_bits(), expected_blur.to_bits());
948 assert_eq!(pose.pitch.to_bits(), expected_nudge.to_bits());
949 assert!(pose.moved);
950 }
951 let mut shared = Director::new(Level::Off, preset, 1.2, 4.0);
952 shared.zoom_by(3.0);
953 assert_ne!(shared.step(0.007).zoom.to_bits(), zoom.main.0.to_bits());
954 }
955
956 #[test]
957 fn per_frame_orbit_and_drift_read_the_preset() {
958 let preset = per_frame();
959 let mut director =
960 Director::new(Level::Full, preset, 1.2, 4.0).with_stepping(Stepping::PerFrame);
961 director.cursor(0.73, -0.41, false);
962 let mut orbit_yaw = FrameSpring::new(preset.orbit.stiffness, preset.gain, 0.0);
963 let mut orbit_pitch = FrameSpring::new(preset.orbit.stiffness, preset.gain, 0.0);
964 let mut time = 0.0f32;
965 for dt in [0.007, 0.019, 1.0 / 60.0, 0.031, 0.11, 0.02] {
966 time += dt;
967 let yaw = orbit_yaw.step(-0.73 * (preset.orbit.yaw / DEG) * DEG, dt);
968 let pitch = orbit_pitch.step(-0.41 * (preset.orbit.pitch / DEG) * DEG, dt);
969 let drift = &preset.drift;
970 let idle_yaw =
971 (wave(drift.yaw[0], time) + wave(drift.yaw[1], time) + wave(drift.yaw[2], time))
972 * (drift.yaw_gain / DEG)
973 * DEG;
974 let idle_pitch = (wave(drift.pitch[0], time)
975 + wave(drift.pitch[1], time)
976 + wave(drift.pitch[2], time))
977 * (drift.pitch_gain / DEG)
978 * DEG;
979 let pose = director.step(dt);
980 assert_eq!(pose.yaw.to_bits(), (yaw + idle_yaw).to_bits());
981 assert_eq!(pose.pitch.to_bits(), (pitch + idle_pitch).to_bits());
982 }
983 }
984
985 #[test]
986 fn a_default_preset_is_neutral() {
987 let preset = Preset::default();
988 assert_eq!(preset.gain, 1.0);
989 assert_eq!(preset.look.sign, 1.0);
990 assert!(preset.hush.is_none());
991 assert!(preset.depth.is_none());
992 assert!(matches!(preset.redraw, Redraw::Exact));
993 assert_eq!(preset.rest, Rest::COARSE);
994 assert!(preset.drift.calmed && preset.drift.quiet_roll);
995 let mut fine = preset;
996 fine.rest = Rest::FINE;
997 let director = Director::new(Level::Full, fine, 0.0, 0.0);
998 assert_eq!(director.zoom.follow.rest, Rest::FINE);
999 assert_eq!(director.orbit_yaw.lead.rest, Rest::FINE);
1000 }
1001
1002 #[test]
1003 fn a_director_settles_to_rest() {
1004 let mut director = Director::new(Level::Off, depth(), 1.2, 4.0);
1005 let pose = until_settled(&mut director);
1006 still(&pose);
1007 assert_eq!(pose.zoom, 1.0);
1008 assert_eq!(pose.focus, 1.2);
1009 assert_eq!(pose.blur, 0.0);
1010 assert!(director.settled());
1011 director.zoom_by(4.0);
1012 assert!(!director.settled());
1013 let back = until_settled(&mut director);
1014 still(&back);
1015 assert!((back.zoom - 1.0).abs() < 1e-4);
1016
1017 let mut plain = Director::new(Level::Off, Preset::default(), 0.8, 3.0);
1018 let pose = until_settled(&mut plain);
1019 still(&pose);
1020 assert_eq!(pose.focus, 0.0);
1021 assert_eq!(pose.blur, 0.0);
1022 assert!(!pose.moved);
1023 }
1024
1025 #[test]
1026 fn a_lean_follows_the_cursor_and_returns() {
1027 let mut director = open();
1028 for _ in 0..180 {
1029 director.cursor(1.0, -1.0, false);
1030 let _ = director.step(1.0 / 60.0);
1031 }
1032 let leaning = director.step(1.0 / 60.0);
1033 assert!(leaning.yaw < -0.2 * DEG, "yaw {}", leaning.yaw);
1034 assert!(leaning.pitch < -0.1 * DEG, "pitch {}", leaning.pitch);
1035 let back = hold(&mut director, 6.0);
1036 assert!(back.yaw.abs() < leaning.yaw.abs());
1037 assert!(back.pitch.abs() < leaning.pitch.abs());
1038 }
1039
1040 #[test]
1041 fn zoom_stays_within_the_preset_cap() {
1042 let mut preset = depth();
1043 preset.zoom.stiffness = 12.0;
1044 preset.zoom.hold = 8.0;
1045 let mut director = Director::new(Level::Full, preset, 1.2, 4.0);
1046 let max = preset.zoom.max;
1047 for _ in 0..40 {
1048 director.zoom_by(1.0);
1049 }
1050 director.zoom_by(-80.0);
1051 director.zoom_by(8.0);
1052 let mut peak = 1.0f32;
1053 for _ in 0..240 {
1054 let zoom = director.step(1.0 / 60.0).zoom;
1055 assert!(zoom <= max + 1e-4, "zoom {zoom}");
1056 assert!(zoom >= 1.0 - 1e-4, "zoom {zoom}");
1057 peak = peak.max(zoom);
1058 }
1059 assert!((peak - max).abs() < 1e-2, "peak {peak}");
1060 let back = hold(&mut director, 12.0);
1061 assert!(back.zoom < 1.01);
1062 }
1063
1064 #[test]
1065 fn frame_patterns_meet_at_the_same_time() {
1066 let make = open();
1067 let script = |director: &mut Director| {
1068 director.cursor(0.6, -0.25, false);
1069 director.zoom_by(2.5);
1070 director.attend(aim(0.15, -0.05, 1.04, 1.7, 6.0));
1071 director.glance(aim(-0.35, 0.12, 1.02, 1.2, 3.0), 0.4);
1072 director.nudge();
1073 };
1074 let mut timed = make;
1075 script(&mut timed);
1076 let pose = timed.advance_to(1.0);
1077 for frames in [
1078 vec![1.0 / 60.0; 60],
1079 vec![1.0 / 240.0; 240],
1080 vec![1.0 / 120.0; 120],
1081 vec![1.0 / 30.0; 30],
1082 ] {
1083 let mut director = make;
1084 script(&mut director);
1085 let mut got = Pose::rest(0.0, 0.0);
1086 for dt in frames {
1087 got = director.step(dt);
1088 }
1089 assert_eq!(got, pose);
1090 }
1091 }
1092
1093 #[test]
1094 fn level_off_holds_the_camera_still() {
1095 let mut always = Director::new(Level::Off, depth(), 1.2, 4.0);
1096 let mut exact = Director::new(Level::Off, Preset::default(), 0.0, 0.0);
1097 for _ in 0..180 {
1098 let posed = always.step(1.0 / 60.0);
1099 let quiet = exact.step(1.0 / 60.0);
1100 still(&posed);
1101 still(&quiet);
1102 assert_eq!(posed.zoom, 1.0);
1103 assert!(posed.moved);
1104 assert!(!quiet.moved);
1105 }
1106 always.cursor(1.0, 1.0, false);
1107 always.attend(aim(0.4, -0.3, 1.2, 2.0, 8.0));
1108 exact.cursor(-1.0, 1.0, true);
1109 exact.attend(aim(0.2, 0.2, 1.05, 4.0, 8.0));
1110 for _ in 0..120 {
1111 still(&always.step(1.0 / 60.0));
1112 let pose = exact.step(1.0 / 60.0);
1113 still(&pose);
1114 assert_eq!(pose.focus, 0.0);
1115 assert_eq!(pose.blur, 0.0);
1116 }
1117 }
1118
1119 #[test]
1120 fn off_ignores_motion_events_but_keeps_explicit_zoom() {
1121 for stepping in [Stepping::Shared, Stepping::PerFrame] {
1122 let mut director =
1123 Director::new(Level::Off, Preset::default(), 0.8, 0.0).with_stepping(stepping);
1124 director.cursor(1.0, -1.0, false);
1125 director.attend(aim(0.4, -0.3, 1.2, 2.0, 6.0));
1126 director.glance(aim(-0.5, 0.2, 1.1, 1.0, 4.0), 0.5);
1127 director.set_wander(Some(aim(0.3, 0.4, 1.1, 1.0, 4.0)));
1128 director.nudge();
1129 for _ in 0..120 {
1130 still(&director.step(1.0 / 60.0));
1131 }
1132 director.zoom_by(3.0);
1133 let mut peak = 1.0f32;
1134 for _ in 0..720 {
1135 let pose = director.step(1.0 / 60.0);
1136 still(&pose);
1137 peak = peak.max(pose.zoom);
1138 }
1139 assert!(peak > 1.1);
1140 }
1141 }
1142
1143 #[test]
1144 fn a_glance_returns_to_the_base() {
1145 let mut director = open();
1146 let player = aim(0.05, 0.02, 1.04, 1.4, 6.0);
1147 let cards = aim(-0.2, 0.0, 1.02, 1.2, 5.0);
1148 director.attend(player);
1149 let on_player = hold(&mut director, 4.0);
1150 director.glance(cards, 1.0);
1151 let on_cards = hold(&mut director, 0.9);
1152 let back = hold(&mut director, 5.0);
1153 assert!(on_player.zoom > 1.03);
1154 assert!(on_cards.look[0] < back.look[0]);
1155 assert!((back.look[0] - on_player.look[0]).abs() < 1e-3);
1156 assert!((back.focus - player.focus).abs() < 1e-2);
1157 director.release();
1158 let home = hold(&mut director, 4.0);
1159 assert!(home.look[0].abs() < 1e-3);
1160 }
1161
1162 #[test]
1163 fn look_sign_follows_or_opposes_the_aim() {
1164 let far = aim(-0.3, 0.2, 1.03, 0.7, 4.0);
1165 let mut positive = depth();
1166 positive.look.sign = 1.0;
1167 let mut director = Director::new(Level::Full, positive, 1.2, 4.0);
1168 director.attend(far);
1169 let posed = hold(&mut director, 5.0);
1170 assert!((posed.look[0] + positive.look.yaw).abs() < 1e-3);
1171 assert!((posed.look[1] - positive.look.pitch).abs() < 1e-3);
1172 let mut negative = positive;
1173 negative.look.sign = -1.0;
1174 let mut director = Director::new(Level::Full, negative, 1.2, 4.0);
1175 director.attend(far);
1176 let posed = hold(&mut director, 5.0);
1177 assert!((posed.look[0] - negative.look.yaw).abs() < 1e-3);
1178 assert!((posed.look[1] - negative.look.pitch).abs() < 1e-3);
1179 }
1180
1181 #[test]
1182 fn reading_quiets_the_drift_and_stops_the_roll() {
1183 let mut preset = depth();
1184 preset.hush = Some(Hush {
1185 floor: 0.25,
1186 stiffness: 12.0,
1187 blocks_cursor: true,
1188 });
1189 let mut director = Director::new(Level::Full, preset, 1.2, 4.0);
1190 director.hush(true);
1191 let _ = hold(&mut director, 5.0);
1192 for _ in 0..60 {
1193 let pose = director.step(1.0 / 60.0);
1194 assert!(pose.yaw.abs() < 0.1 * DEG);
1195 assert!(pose.roll.abs() < 1e-5);
1196 }
1197 let mut loose = Preset::default();
1198 loose.drift.calmed = false;
1199 loose.drift.quiet_roll = false;
1200 loose.redraw = Redraw::Always;
1201 let mut director = Director::new(Level::Full, loose, 0.0, 0.0);
1202 director.hush(true);
1203 let _ = hold(&mut director, 5.0);
1204 let mut rolled = false;
1205 for _ in 0..240 {
1206 if director.step(1.0 / 60.0).roll.abs() > 1e-5 {
1207 rolled = true;
1208 }
1209 }
1210 assert!(rolled);
1211 }
1212
1213 #[test]
1214 fn wander_leans_for_a_full_level_only() {
1215 let target = aim(0.5, -0.4, 1.02, 1.4, 3.0);
1216 let mut director = open();
1217 director.set_wander(Some(target));
1218 let _ = hold(&mut director, 21.0);
1219 let wandering = hold(&mut director, 1.0);
1220 assert!(wandering.look[0] > 0.3 * DEG);
1221 assert!(wandering.look[1] < -0.2 * DEG);
1222 let mut subtle = open();
1223 subtle.set_level(Level::Subtle);
1224 subtle.set_wander(Some(target));
1225 let pose = hold(&mut subtle, 22.0);
1226 assert!(pose.look[0].abs() < 1e-3);
1227 assert!(pose.look[1].abs() < 1e-3);
1228 }
1229
1230 #[test]
1231 fn level_scales_a_clamped_look() {
1232 let far = aim(1.0, 1.0, 1.0, 1.2, 4.0);
1233 let preset = depth();
1234 let mut full = Director::new(Level::Full, preset, 1.2, 4.0);
1235 let mut subtle = Director::new(Level::Subtle, preset, 1.2, 4.0);
1236 full.attend(far);
1237 subtle.attend(far);
1238 let full = hold(&mut full, 5.0);
1239 let subtle = hold(&mut subtle, 5.0);
1240 assert!((full.look[0] - preset.look.yaw).abs() < 1e-3);
1241 assert!((subtle.look[0] - 0.5 * preset.look.yaw).abs() < 1e-3);
1242 assert!((full.look[1] - preset.look.pitch).abs() < 1e-3);
1243 assert!((subtle.look[1] - 0.5 * preset.look.pitch).abs() < 1e-3);
1244 }
1245
1246 #[test]
1247 fn a_busy_cursor_does_not_lean() {
1248 let mut director = open();
1249 for _ in 0..90 {
1250 director.cursor(1.0, 1.0, true);
1251 let pose = director.step(1.0 / 60.0);
1252 assert!(pose.yaw.abs() < 0.3 * DEG);
1253 }
1254 }
1255
1256 #[test]
1257 fn a_nudge_dips_and_returns() {
1258 let mut preset = depth();
1259 preset.nudge.floor = 0.5;
1260 let mut director = Director::new(Level::Off, preset, 1.2, 4.0);
1261 director.nudge();
1262 let mut low = 0.0f32;
1263 for _ in 0..20 {
1264 low = low.min(director.step(1.0 / 60.0).pitch);
1265 }
1266 assert!(low < 0.0);
1267 let pose = hold(&mut director, 2.0);
1268 assert!(pose.pitch.abs() < 1e-4);
1269 }
1270
1271 #[test]
1272 fn idle_drift_stays_under_a_third_of_a_degree() {
1273 let mut director = open();
1274 for _ in 0..(30 * 60) {
1275 let pose = director.step(1.0 / 60.0);
1276 assert!(pose.yaw.abs() < 0.3 * DEG);
1277 assert!(pose.pitch.abs() < 0.2 * DEG);
1278 assert!(pose.roll.abs() < 0.05 * DEG);
1279 }
1280 }
1281
1282 #[test]
1283 fn level_parse_reads_the_three_names() {
1284 assert_eq!(Level::parse("off"), Some(Level::Off));
1285 assert_eq!(Level::parse("subtle"), Some(Level::Subtle));
1286 assert_eq!(Level::parse("full"), Some(Level::Full));
1287 assert_eq!(Level::parse("0"), None);
1288 assert_eq!(Level::parse("nope"), None);
1289 }
1290
1291 #[test]
1292 fn a_spec_parses_a_pose() {
1293 let pose = Pose::from_spec(
1294 "yaw=10, pitch=-2, roll=0.5, dolly=0.02, zoom=0.4, focus=1.6, blur=3, lookx=4, looky=-1, nope=1",
1295 );
1296 assert!((pose.yaw - 10.0 * DEG).abs() < 1e-6);
1297 assert!((pose.pitch + 2.0 * DEG).abs() < 1e-6);
1298 assert!((pose.roll - 0.5 * DEG).abs() < 1e-6);
1299 assert_eq!(pose.offset, 0.02);
1300 assert_eq!(pose.zoom, 1.0);
1301 assert_eq!(pose.focus, 1.6);
1302 assert_eq!(pose.blur, 3.0);
1303 assert!((pose.look[0] - 4.0 * DEG).abs() < 1e-6);
1304 assert!((pose.look[1] + DEG).abs() < 1e-6);
1305 assert!(!pose.moved);
1306 let blank = Pose::from_spec("broken, zoom=two");
1307 assert_eq!(blank.zoom, 1.0);
1308 assert_eq!(blank.focus, 0.0);
1309 }
1310
1311 #[test]
1312 fn a_preset_without_depth_ignores_focus_and_blur() {
1313 let mut director = Director::new(Level::Full, Preset::default(), 1.0, 8.0);
1314 director.attend(aim(0.0, 0.0, 1.0, 3.0, 40.0));
1315 let pose = hold(&mut director, 3.0);
1316 assert_eq!(pose.focus, 0.0);
1317 assert_eq!(pose.blur, 0.0);
1318 }
1319
1320 #[test]
1321 fn set_level_off_brings_the_rig_back_to_rest() {
1322 let mut director = open();
1323 director.cursor(1.0, 0.5, false);
1324 let _ = hold(&mut director, 1.0);
1325 director.set_level(Level::Off);
1326 let pose = hold(&mut director, 6.0);
1327 still(&pose);
1328 assert_eq!(pose.zoom, 1.0);
1329 }
1330}