Skip to main content

algebraeon_drawing/canvas2d/
mod.rs

1use crate::canvas::*;
2use std::sync::Arc;
3use wgpu::{BindGroup, BindGroupLayout, Color, CommandEncoder, TextureView, util::DeviceExt};
4use winit::{
5    dpi::{PhysicalPosition, PhysicalSize},
6    event::WindowEvent,
7    event_loop::ActiveEventLoop,
8    window::{Window, WindowId},
9};
10
11pub mod complex_polynomial;
12pub mod plottable;
13pub mod shapes;
14pub mod test_pentagon;
15
16#[repr(C)]
17#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
18pub struct CameraUniform {
19    pub matrix: [[f32; 2]; 2],
20    pub matrix_inv: [[f32; 2]; 2],
21    pub shift: [f32; 2],
22}
23
24fn mat2x2inv([[a, b], [c, d]]: [[f32; 2]; 2]) -> [[f32; 2]; 2] {
25    let det = a * d - b * c;
26    [[d / det, -c / det], [-b / det, a / det]]
27}
28
29pub trait Camera {
30    fn get_uniform(&self, display_size: PhysicalSize<u32>) -> CameraUniform;
31
32    fn window_event(
33        &mut self,
34        display_size: PhysicalSize<u32>,
35        mouse_pos: PhysicalPosition<f64>,
36        event_loop: &ActiveEventLoop,
37        id: WindowId,
38        event: &WindowEvent,
39    );
40
41    fn pixel_to_wgpu(
42        &self,
43        display_size: PhysicalSize<u32>,
44        pixels: PhysicalPosition<f64>,
45    ) -> (f64, f64) {
46        (
47            2.0 * pixels.x / display_size.width as f64 - 1.0,
48            -2.0 * pixels.y / display_size.height as f64 + 1.0,
49        )
50    }
51
52    fn wgpu_to_pixel(
53        &self,
54        display_size: PhysicalSize<u32>,
55        wgpu: (f64, f64),
56    ) -> PhysicalPosition<f64> {
57        PhysicalPosition::new(
58            display_size.width as f64 * (wgpu.0 + 1.0) / 2.0,
59            display_size.height as f64 * (-wgpu.1 + 1.0) / 2.0,
60        )
61    }
62
63    fn wgpu_to_coord(&self, display_size: PhysicalSize<u32>, wgpu: (f64, f64)) -> (f64, f64) {
64        let uniform = self.get_uniform(display_size);
65        let [[a, b], [c, d]] = mat2x2inv(uniform.matrix);
66        let v = (
67            wgpu.0 - uniform.shift[0] as f64,
68            wgpu.1 - uniform.shift[1] as f64,
69        );
70        (
71            a as f64 * v.0 + b as f64 * v.1,
72            c as f64 * v.0 + d as f64 * v.1,
73        )
74    }
75
76    fn coord_to_wgpu(&self, display_size: PhysicalSize<u32>, coord: (f64, f64)) -> (f64, f64) {
77        let uniform = self.get_uniform(display_size);
78        let [[a, b], [c, d]] = uniform.matrix;
79        let [x, y] = uniform.shift;
80        let v = coord;
81        (
82            a as f64 * v.0 + b as f64 * v.1 + x as f64,
83            c as f64 * v.0 + d as f64 * v.1 + y as f64,
84        )
85    }
86
87    fn pixel_to_coord(
88        &self,
89        display_size: PhysicalSize<u32>,
90        pixels: PhysicalPosition<f64>,
91    ) -> (f64, f64) {
92        self.wgpu_to_coord(display_size, self.pixel_to_wgpu(display_size, pixels))
93    }
94
95    fn coord_to_pixel(
96        &self,
97        display_size: PhysicalSize<u32>,
98        coord: (f64, f64),
99    ) -> PhysicalPosition<f64> {
100        self.wgpu_to_pixel(display_size, self.coord_to_wgpu(display_size, coord))
101    }
102}
103
104pub struct MouseWheelZoomCamera {
105    // the x coordinate at the centre of the screen
106    mid_x: f64,
107    // the y coordinate at the centre of the screen
108    mid_y: f64,
109    // the square root of the visible area
110    sqrt_area: f64,
111}
112
113#[allow(clippy::new_without_default)]
114impl MouseWheelZoomCamera {
115    pub fn new() -> Self {
116        Self {
117            mid_x: 0.0,
118            mid_y: 0.0,
119            sqrt_area: 6.0,
120        }
121    }
122}
123
124impl Camera for MouseWheelZoomCamera {
125    fn get_uniform(&self, display_size: PhysicalSize<u32>) -> CameraUniform {
126        let display_size = (display_size.width as f32, display_size.height as f32);
127        let avg_side = (display_size.0 * display_size.1).sqrt();
128        let x_mult = 2.0 * avg_side / (display_size.0 * self.sqrt_area as f32);
129        let y_mult = 2.0 * avg_side / (display_size.1 * self.sqrt_area as f32);
130        let matrix = [[x_mult, 0.0], [0.0, y_mult]];
131        let matrix_inv = mat2x2inv(matrix);
132        CameraUniform {
133            matrix,
134            matrix_inv,
135            shift: [-self.mid_x as f32 * x_mult, -self.mid_y as f32 * y_mult],
136        }
137    }
138
139    fn window_event(
140        &mut self,
141        display_size: PhysicalSize<u32>,
142        mouse_pos: PhysicalPosition<f64>,
143        _event_loop: &ActiveEventLoop,
144        _id: WindowId,
145        event: &WindowEvent,
146    ) {
147        #[allow(clippy::single_match)]
148        match event {
149            WindowEvent::MouseWheel { delta, .. } => {
150                let dy = match delta {
151                    winit::event::MouseScrollDelta::PixelDelta(pos) => pos.y,
152                    winit::event::MouseScrollDelta::LineDelta(_x, y) => *y as f64,
153                };
154                self.zoom_event(display_size, mouse_pos, 0.8f64.powf(dy));
155            }
156            _ => {}
157        }
158    }
159}
160
161impl MouseWheelZoomCamera {
162    fn zoom_event(
163        &mut self,
164        display_size: PhysicalSize<u32>,
165        center: PhysicalPosition<f64>,
166        mult: f64,
167    ) {
168        // println!("{:?} {:?}", center, mult);
169        let center_before = self.pixel_to_coord(display_size, center);
170        self.sqrt_area *= mult;
171        let center_after = self.pixel_to_coord(display_size, center);
172        self.mid_x += center_before.0 - center_after.0;
173        self.mid_y += center_before.1 - center_after.1;
174    }
175}
176
177pub struct Canvas2D {
178    mouse_pos: PhysicalPosition<f64>,
179    items: Vec<Box<dyn Canvas2DItem>>,
180    camera: Box<dyn Camera>,
181}
182
183pub trait Canvas2DItemWgpu {
184    fn render(
185        &mut self,
186        encoder: &mut CommandEncoder,
187        view: &TextureView,
188        camera_bind_group: &BindGroup,
189    ) -> Result<(), wgpu::SurfaceError>;
190}
191
192#[allow(clippy::new_without_default)]
193pub trait Canvas2DItem {
194    fn new_wgpu(
195        &self,
196        wgpu_state: &WgpuState,
197        camera_bind_group_layout: &BindGroupLayout,
198    ) -> Box<dyn Canvas2DItemWgpu>;
199}
200
201pub struct Canvas2DWindowState {
202    wgpu_state: WgpuState,
203
204    camera_uniform: CameraUniform,
205    camera_buffer: wgpu::Buffer,
206    camera_bind_group_layout: wgpu::BindGroupLayout,
207    camera_bind_group: wgpu::BindGroup,
208
209    items: Vec<Box<dyn Canvas2DItemWgpu>>,
210}
211
212impl Canvas2DWindowState {
213    fn new(window: Arc<Window>) -> Self {
214        let wgpu_state = WgpuState::new(window);
215
216        let camera_uniform = CameraUniform {
217            matrix: [[1.0, 0.0], [0.0, 1.0]],
218            matrix_inv: [[1.0, 0.0], [0.0, 1.0]],
219            shift: [0.0, 0.0],
220        };
221
222        let camera_buffer =
223            wgpu_state
224                .device
225                .create_buffer_init(&wgpu::util::BufferInitDescriptor {
226                    label: Some("Camera Buffer"),
227                    contents: bytemuck::cast_slice(&[camera_uniform]),
228                    usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
229                });
230
231        let camera_bind_group_layout =
232            wgpu_state
233                .device
234                .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
235                    entries: &[wgpu::BindGroupLayoutEntry {
236                        binding: 0,
237                        visibility: wgpu::ShaderStages::VERTEX,
238                        ty: wgpu::BindingType::Buffer {
239                            ty: wgpu::BufferBindingType::Uniform,
240                            has_dynamic_offset: false,
241                            min_binding_size: None,
242                        },
243                        count: None,
244                    }],
245                    label: Some("camera_bind_group_layout"),
246                });
247
248        let camera_bind_group = wgpu_state
249            .device
250            .create_bind_group(&wgpu::BindGroupDescriptor {
251                layout: &camera_bind_group_layout,
252                entries: &[wgpu::BindGroupEntry {
253                    binding: 0,
254                    resource: camera_buffer.as_entire_binding(),
255                }],
256                label: Some("camera_bind_group"),
257            });
258
259        Self {
260            items: vec![],
261            wgpu_state,
262            camera_uniform,
263            camera_bind_group,
264            camera_buffer,
265            camera_bind_group_layout,
266        }
267    }
268
269    fn render(&mut self) -> Result<(), wgpu::SurfaceError> {
270        self.wgpu_state.queue.write_buffer(
271            &self.camera_buffer,
272            0,
273            bytemuck::cast_slice(&[self.camera_uniform]),
274        );
275
276        let output = self.wgpu_state.surface.get_current_texture()?;
277        let view = output
278            .texture
279            .create_view(&wgpu::TextureViewDescriptor::default());
280        let mut encoder =
281            self.wgpu_state
282                .device
283                .create_command_encoder(&wgpu::CommandEncoderDescriptor {
284                    label: Some("Render Encoder"),
285                });
286
287        encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
288            label: Some("clear-pass"),
289            color_attachments: &[Some(wgpu::RenderPassColorAttachment {
290                view: &view,
291                resolve_target: None,
292                ops: wgpu::Operations {
293                    load: wgpu::LoadOp::Clear(Color::WHITE),
294                    store: wgpu::StoreOp::Store,
295                },
296            })],
297            depth_stencil_attachment: None,
298            timestamp_writes: None,
299            occlusion_query_set: None,
300        });
301
302        for item in &mut self.items {
303            item.render(&mut encoder, &view, &self.camera_bind_group)?;
304        }
305
306        // submit will accept anything that implements IntoIter
307        self.wgpu_state
308            .queue
309            .submit(std::iter::once(encoder.finish()));
310        output.present();
311
312        Ok(())
313    }
314}
315
316impl Canvas for Canvas2D {
317    type WindowState = Canvas2DWindowState;
318
319    fn new_window_state(&self, window: Arc<Window>) -> Self::WindowState {
320        let mut state = Canvas2DWindowState::new(window);
321        for item in &self.items {
322            state
323                .items
324                .push(item.new_wgpu(&state.wgpu_state, &state.camera_bind_group_layout));
325        }
326        state
327    }
328
329    fn window_event(
330        &mut self,
331        window_state: &mut Self::WindowState,
332        event_loop: &ActiveEventLoop,
333        id: WindowId,
334        event: WindowEvent,
335    ) {
336        self.camera.window_event(
337            window_state.wgpu_state.size,
338            self.mouse_pos,
339            event_loop,
340            id,
341            &event,
342        );
343
344        #[allow(clippy::single_match)]
345        match event {
346            WindowEvent::CloseRequested => {
347                event_loop.exit();
348            }
349            WindowEvent::Resized(physical_size) => {
350                window_state.wgpu_state.resize(physical_size);
351            }
352            WindowEvent::RedrawRequested => {
353                window_state.camera_uniform = self.camera.get_uniform(window_state.wgpu_state.size);
354                match window_state.render() {
355                    Ok(()) => {}
356                    // Reconfigure the surface if it's lost or outdated
357                    Err(wgpu::SurfaceError::Lost | wgpu::SurfaceError::Outdated) => {
358                        window_state.wgpu_state.reconfigure();
359                    }
360                    // The system is out of memory, we should probably quit
361                    Err(wgpu::SurfaceError::OutOfMemory | wgpu::SurfaceError::Other) => {
362                        log::error!("OutOfMemory");
363                        event_loop.exit();
364                    }
365
366                    // This happens when the a frame takes too long to present
367                    Err(wgpu::SurfaceError::Timeout) => {
368                        log::warn!("Surface timeout")
369                    }
370                }
371                window_state.wgpu_state.window.request_redraw();
372            }
373            WindowEvent::CursorMoved { position, .. } => {
374                self.mouse_pos = position;
375            }
376            WindowEvent::MouseInput { state, button, .. } => match (button, state) {
377                (winit::event::MouseButton::Left, winit::event::ElementState::Pressed) => {
378                    println!(
379                        "{:?} -> {:?} -> {:?}",
380                        self.mouse_pos,
381                        self.camera
382                            .pixel_to_coord(window_state.wgpu_state.size, self.mouse_pos),
383                        self.camera.coord_to_pixel(
384                            window_state.wgpu_state.size,
385                            self.camera
386                                .pixel_to_coord(window_state.wgpu_state.size, self.mouse_pos)
387                        )
388                    );
389                }
390                _ => {}
391            },
392            _ => (),
393        }
394    }
395}
396
397impl Canvas2D {
398    pub fn new(camera: Box<dyn Camera>) -> Self {
399        Self {
400            mouse_pos: PhysicalPosition::new(0.0, 0.0),
401            items: vec![],
402            camera,
403        }
404    }
405
406    pub fn add_item(&mut self, item: impl Canvas2DItem + 'static) {
407        self.items.push(Box::new(item));
408    }
409}