Skip to main content

pfx_core/
camera.rs

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}