comfy_core/
camera.rs

1use std::sync::atomic::{AtomicU32, Ordering};
2
3use crate::*;
4
5pub static MAIN_CAMERA: Lazy<AtomicRefCell<MainCamera>> =
6    Lazy::new(|| AtomicRefCell::new(MainCamera::default()));
7
8pub const LINE_W: f32 = 2.0;
9
10pub static CAMERA_BOUNDS: AtomicCell<Rect> = AtomicCell::new(Rect {
11    center: Vec2::ZERO,
12    size: Vec2 { x: 30.0, y: 30.0 },
13});
14
15static PX: AtomicU32 =
16    AtomicU32::new(unsafe { std::mem::transmute(0.0347f32) });
17
18pub fn set_px(value: f32) {
19    PX.store(value.to_bits(), Ordering::SeqCst);
20}
21
22pub fn px() -> f32 {
23    f32::from_bits(PX.load(Ordering::SeqCst))
24}
25
26pub fn mouse_screen() -> Vec2 {
27    let pos = GLOBAL_STATE.borrow().mouse_position;
28    Vec2::new(pos.x, pos.y)
29}
30
31pub fn mouse_world() -> Vec2 {
32    GLOBAL_STATE.borrow().mouse_world
33}
34
35pub fn screen_width() -> f32 {
36    GLOBAL_STATE.borrow().screen_size.x
37}
38
39pub fn aspect_ratio() -> f32 {
40    MAIN_CAMERA.borrow().aspect_ratio
41}
42
43pub fn screen_height() -> f32 {
44    GLOBAL_STATE.borrow().screen_size.y
45}
46
47/// Returns true if there was any mouse input during the current frame.
48pub fn mouse_input_this_frame() -> bool {
49    GLOBAL_STATE.borrow().mouse_input_this_frame
50}
51
52/// Returns true if the mouse was moved during the current frame.
53pub fn mouse_moved_this_frame() -> bool {
54    GLOBAL_STATE.borrow().mouse_moved_this_frame
55}
56
57pub fn screenshake(timer: f32, amount: f32) {
58    let mut camera = main_camera_mut();
59
60    camera.shake_timer = timer;
61    camera.shake_amount = amount;
62}
63
64pub fn main_camera() -> AtomicRef<'static, MainCamera> {
65    MAIN_CAMERA.borrow()
66}
67
68pub fn main_camera_mut() -> AtomicRefMut<'static, MainCamera> {
69    MAIN_CAMERA.borrow_mut()
70}
71
72pub fn set_main_camera_zoom(zoom: f32) {
73    MAIN_CAMERA.borrow_mut().zoom = zoom;
74}
75
76pub fn egui_scale_factor() -> f32 {
77    GLOBAL_STATE.borrow().egui_scale_factor
78}
79
80pub fn world_to_gl_screen(position: Vec2) -> Vec2 {
81    let mut screen = world_to_screen(position);
82    screen.y = screen_height() - screen.y;
83    screen
84}
85
86pub fn world_to_screen(position: Vec2) -> Vec2 {
87    main_camera().world_to_screen(position)
88}
89
90pub fn screen_to_world(position: Vec2) -> Vec2 {
91    main_camera().screen_to_world(position)
92}
93
94// TODO: use this for zoom
95pub struct DampedSpring {
96    pub value: f32,
97    pub target: f32,
98    pub velocity: f32,
99    pub damping: f32,
100}
101
102impl DampedSpring {
103    pub fn new(target: f32, damping: f32) -> DampedSpring {
104        DampedSpring { value: target, target, velocity: 0.0, damping }
105    }
106
107    pub fn update(&mut self) {
108        let diff = self.target - self.value;
109        self.velocity += diff * self.damping;
110        self.value += self.velocity;
111        self.velocity *= 0.9; // This is to ensure the value will eventually settle
112    }
113}
114
115#[derive(Copy, Clone)]
116struct HistoryStackValue {
117    pub center: Vec2,
118    pub desired_zoom: f32,
119    pub zoom: f32,
120}
121
122pub type CameraMatrixFn =
123    Box<dyn (Fn(&MainCamera, Vec2) -> Mat4) + Send + Sync + 'static>;
124
125pub struct MainCamera {
126    /// Screenshake time remaining.
127    pub shake_timer: f32,
128    /// Amount of screenshake to apply.
129    pub shake_amount: f32,
130
131    pub recoil: f32,
132
133    /// Center of the camera. This updates the camera immediately for the current frame without any
134    /// smoothing. If you need something set `target` instead.
135    pub center: Vec2,
136    /// Smoothing target for the camera. By default this also uses a deadzone as defined by
137    /// `deadzone_width` and `deadzone_height`. If you don't want a deadzone, set those to zero.
138    pub target: Option<Vec2>,
139
140    /// Smoothing speed when `target` is set.
141    pub smoothing_speed: f32,
142    /// Width of the camera deadzone in world space.
143    pub deadzone_width: f32,
144    /// Height of the camera deadzone in world space.
145    pub deadzone_height: f32,
146
147    pub aspect_ratio: f32,
148
149    pub zoom: f32,
150    pub desired_zoom: f32,
151
152    /// Optional camera matrix function that allows the user to create their own projection matrix.
153    ///
154    /// See the implementation of `build_view_projection_matrix` for what is the default with
155    /// `Mat4::orthographic_rh`. Note that this doesn't have to return an orthographic perspective
156    /// matrix, it can be anything (perspective projection, etc.).
157    pub matrix_fn: Option<CameraMatrixFn>,
158    /// Override config allowing to disable matrix_fn even when one is provided.
159    /// Useful for debugging.
160    pub use_matrix_fn: bool,
161
162    history_stack: Vec<HistoryStackValue>,
163}
164
165impl Default for MainCamera {
166    fn default() -> Self {
167        Self::new(Vec2::new(0.0, 0.0), 30.0)
168    }
169}
170
171impl MainCamera {
172    pub fn new(center: Vec2, zoom: f32) -> Self {
173        Self {
174            shake_timer: 0.0,
175            shake_amount: 0.0,
176
177            recoil: 0.0,
178
179            center,
180            target: None,
181
182            deadzone_width: 3.0,
183            deadzone_height: 2.0,
184
185            smoothing_speed: 4.0,
186
187            aspect_ratio: 1.0,
188
189            zoom,
190            desired_zoom: zoom,
191
192            history_stack: Vec::new(),
193
194            matrix_fn: None,
195            use_matrix_fn: true,
196        }
197    }
198
199    pub fn update(&mut self, delta: f32) {
200        self.shake_timer -= delta;
201        self.shake_timer = self.shake_timer.max(0.0);
202        self.recoil = (self.recoil - delta).max(0.0);
203
204        set_px(self.zoom / screen_width());
205
206        if let Some(player_position) = self.target {
207            let deadzone_hw = self.deadzone_width / 2.0;
208            let deadzone_hh = self.deadzone_height / 2.0;
209
210            let ox = player_position.x - self.center.x;
211            let oy = player_position.y - self.center.y;
212
213            let dx = ox.abs() - deadzone_hw;
214            let dy = oy.abs() - deadzone_hh;
215
216            let mut offset = Vec2::ZERO;
217
218            if dx > 0.0 {
219                offset.x = dx * ox.signum();
220            }
221
222            if dy > 0.0 {
223                offset.y = dy * oy.signum();
224            }
225
226            self.center += offset * delta * self.smoothing_speed;
227            self.center = player_position;
228        }
229
230        CAMERA_BOUNDS
231            .store(Rect { center: self.center, size: self.world_viewport() });
232    }
233
234    pub fn push_center(&mut self, new_center: Vec2, new_zoom: f32) {
235        self.history_stack.push(HistoryStackValue {
236            center: self.center,
237            desired_zoom: self.desired_zoom,
238            zoom: self.zoom,
239        });
240
241        self.center = new_center;
242
243        self.desired_zoom = new_zoom;
244        self.zoom = new_zoom;
245    }
246
247    pub fn pop_center(&mut self) {
248        if let Some(item) = self.history_stack.pop() {
249            self.center = item.center;
250            self.zoom = item.zoom;
251            self.desired_zoom = item.desired_zoom;
252        }
253    }
254
255    pub fn bump_recoil(&mut self, amount: f32) {
256        self.recoil = (self.recoil + amount).clamp(0.0, 1.0);
257    }
258
259    pub fn world_viewport(&self) -> Vec2 {
260        vec2(self.zoom, self.zoom / self.aspect_ratio)
261    }
262
263    pub fn screen_top_left(&self) -> Vec2 {
264        let world_viewport = self.world_viewport();
265        self.center + vec2(-world_viewport.x, world_viewport.y) / 2.0
266    }
267
268    pub fn screen_top_right(&self) -> Vec2 {
269        let world_viewport = self.world_viewport();
270        self.center + vec2(world_viewport.x, world_viewport.y) / 2.0
271    }
272
273    pub fn shake(&mut self, amount: f32, time: f32) {
274        self.shake_amount = amount;
275        self.shake_timer = time;
276    }
277
278    pub fn current_shake(&self) -> f32 {
279        self.shake_amount * self.shake_timer.clamp(0.0, 1.0)
280    }
281
282    pub fn build_view_projection_matrix(&self) -> Mat4 {
283        let hx = self.zoom / 2.0;
284        let hy = self.zoom / 2.0 / self.aspect_ratio;
285
286        let range = 1000.0;
287
288        const SHAKE: f32 = 0.2;
289
290        let (sx, sy) = if self.shake_timer > 0.0 {
291            let off = random_around(Vec2::ZERO, 0.0, SHAKE * self.shake_amount);
292            (off.x, off.y)
293        } else {
294            (0.0, 0.0)
295        };
296
297        let center = self.center + vec2(sx, sy);
298
299        let ortho_camera = Mat4::orthographic_rh(
300            center.x - hx,
301            center.x + hx,
302            center.y - hy,
303            center.y + hy,
304            -range,
305            range,
306        );
307
308        if let Some(matrix_fn) = self.matrix_fn.as_ref() {
309            if self.use_matrix_fn {
310                matrix_fn(self, center)
311            } else {
312                ortho_camera
313            }
314        } else {
315            ortho_camera
316        }
317    }
318
319    pub fn screen_to_world(&self, position: Vec2) -> Vec2 {
320        let viewport = self.world_viewport();
321        let camera_center = self.center;
322
323        let state = GLOBAL_STATE.borrow();
324
325        let normalized = position / state.screen_size;
326        let normalized = vec2(normalized.x, 1.0 - normalized.y);
327
328        let world_unoffset = (normalized - 0.5) * viewport;
329
330        world_unoffset + camera_center
331    }
332
333    pub fn world_to_screen(&self, position: Vec2) -> Vec2 {
334        let viewport = self.world_viewport();
335
336        let position = position - self.center;
337
338        let state = GLOBAL_STATE.borrow();
339
340        let moved = position + viewport / 2.0;
341        let normalized = moved / viewport;
342        let normalized = vec2(normalized.x, 1.0 - normalized.y);
343
344        normalized * state.screen_size
345        // vec2(
346        //     normalized.x * state.screen_size.x / state.egui_scale_factor,
347        //     normalized.y * state.screen_size.y / state.egui_scale_factor,
348        // )
349    }
350
351    pub fn world_to_render_px(
352        &self,
353        position: Vec2,
354        render_scale: f32,
355    ) -> IVec2 {
356        let px = self.world_to_screen(position).as_ivec2();
357        (px.as_vec2() * render_scale).as_ivec2()
358    }
359}
360
361#[derive(Copy, Clone, Debug)]
362pub enum Position {
363    World { x: f32, y: f32 },
364    Screen { x: ScreenVal, y: ScreenVal },
365}
366
367impl Position {
368    pub fn world(x: f32, y: f32) -> Self {
369        Self::World { x, y }
370    }
371
372    pub fn screen(x: ScreenVal, y: ScreenVal) -> Self {
373        Self::Screen { x, y }
374    }
375
376    pub fn screen_px(x: f32, y: f32) -> Self {
377        Self::Screen { x: ScreenVal::Px(x), y: ScreenVal::Px(y) }
378    }
379
380    // TODO: rename to screen & percent
381    pub fn screen_percent(x: f32, y: f32) -> Self {
382        Self::Screen { x: ScreenVal::Percent(x), y: ScreenVal::Percent(y) }
383    }
384
385    pub fn vec2(self) -> Vec2 {
386        match self {
387            Position::World { x, y } => vec2(x, y),
388            Position::Screen { x, y } => vec2(x.to_f32(), y.to_f32()),
389        }
390    }
391
392    pub fn to_world(self) -> Vec2 {
393        match self {
394            Position::World { x, y } => vec2(x, y),
395            Position::Screen { x, y } => {
396                screen_to_world(vec2(
397                    x.to_px(screen_width()),
398                    y.to_px(screen_height()),
399                ))
400            }
401        }
402    }
403
404    pub fn to_screen(self) -> Vec2 {
405        match self {
406            Position::World { x, y } => world_to_screen(vec2(x, y)),
407            Position::Screen { x, y } => {
408                vec2(x.to_px(screen_width()), y.to_px(screen_height()))
409            }
410        }
411    }
412}
413
414#[derive(Copy, Clone, Debug)]
415pub enum ScreenVal {
416    Px(f32),
417    Percent(f32),
418}
419
420impl ScreenVal {
421    pub fn to_f32(self) -> f32 {
422        match self {
423            ScreenVal::Px(val) => val,
424            ScreenVal::Percent(val) => val,
425        }
426    }
427
428    pub fn to_px(self, screen_dim: f32) -> f32 {
429        match self {
430            ScreenVal::Px(val) => val,
431            ScreenVal::Percent(val) => screen_dim * val,
432        }
433    }
434}
435
436// TODO: get rid of this
437#[derive(Copy, Clone, Debug)]
438pub enum Value {
439    World(f32),
440    Px(f32),
441    Percent(f32),
442}
443
444#[derive(Copy, Clone, Debug)]
445pub enum Axis {
446    X,
447    Y,
448}
449
450impl Value {
451    pub fn to_world(self, axis: Axis) -> f32 {
452        let viewport = main_camera().world_viewport();
453
454        match self {
455            Value::World(val) => val,
456            Value::Px(val) => {
457                match axis {
458                    Axis::X => val / screen_width() * viewport.x,
459                    Axis::Y => val / screen_height() * viewport.y,
460                }
461            }
462            // TODO: simplify
463            Value::Percent(percent) => {
464                match axis {
465                    Axis::X => percent * viewport.x,
466                    Axis::Y => percent * viewport.y,
467                }
468            }
469        }
470    }
471}
472
473#[derive(Copy, Clone, Debug)]
474pub struct Size {
475    pub width: Value,
476    pub height: Value,
477}
478
479impl Size {
480    pub fn world(width: f32, height: f32) -> Self {
481        Size { width: Value::World(width), height: Value::World(height) }
482    }
483
484    pub fn screen(width: f32, height: f32) -> Self {
485        Size { width: Value::Px(width), height: Value::Px(height) }
486    }
487
488    pub fn percent(width: f32, height: f32) -> Self {
489        Size { width: Value::Percent(width), height: Value::Percent(height) }
490    }
491
492    pub fn to_world(self) -> Vec2 {
493        vec2(self.width.to_world(Axis::X), self.height.to_world(Axis::Y))
494    }
495}