lazer pointer wao
at main 19 kB view raw
1#![feature(if_let_guard)] 2 3use quanta::{Clock, Instant}; 4use std::collections::HashMap; 5use std::ops::DerefMut; 6use std::sync::Arc; 7use std::time::Duration; 8use tiny_skia::{Color, *}; 9use tokio::sync::{OnceCell, mpsc}; 10use winit::{ 11 application::ApplicationHandler, 12 event::{ElementState, MouseButton, TouchPhase, WindowEvent}, 13 event_loop::{ActiveEventLoop, ControlFlow, EventLoop}, 14 window::{Window, WindowAttributes, WindowId}, 15}; 16 17use crate::ws::LaserMessage; 18use crate::{ 19 renderer::{Renderer, skia_rgba_to_bgra_u32}, 20 ws::WsMessage, 21}; 22 23mod renderer; 24mod utils; 25mod ws; 26 27type BoxedError = Box<dyn std::error::Error + 'static>; 28type AppResult<T> = Result<T, BoxedError>; 29 30const BASE_MAX_AGE: Duration = Duration::from_millis(200); 31const MIN_AGE: Duration = Duration::from_millis(50); 32const FAST_DECAY_THRESHOLD: usize = 30; 33#[cfg(target_arch = "wasm32")] 34const TARGET_FPS: u32 = 120; 35#[cfg(not(target_arch = "wasm32"))] 36const TARGET_FPS: u32 = 60; 37const FRAME_TIME_MS: u64 = 1000 / TARGET_FPS as u64; // ~16.67ms 38 39#[derive(Clone)] 40struct LaserPoint { 41 x: f32, 42 y: f32, 43 color: [u8; 3], 44 created_at: Instant, 45} 46 47impl LaserPoint { 48 fn new(msg: LaserMessage, now: Instant) -> Self { 49 Self { 50 x: msg.x as f32, 51 y: msg.y as f32, 52 color: utils::id_to_color(msg.id), 53 created_at: now, 54 } 55 } 56} 57 58pub type WindowHandle = Arc<OnceCell<Arc<Window>>>; 59 60pub struct Graphics { 61 window: Arc<Window>, 62 renderer: Renderer, 63 pixmap: Pixmap, 64} 65 66impl Graphics { 67 fn resize(&mut self, width: u32, height: u32) -> AppResult<()> { 68 self.renderer.resize(width, height)?; 69 self.pixmap = Pixmap::new(width, height).unwrap(); 70 Ok(()) 71 } 72} 73 74pub struct LaserOverlay { 75 gfx: Option<Graphics>, 76 window: WindowHandle, 77 server_mouse_pos: (f32, f32), 78 laser_points: HashMap<(u64, u8), Vec<LaserPoint>, ahash::RandomState>, 79 in_chan: (mpsc::Sender<WsMessage>, mpsc::Receiver<WsMessage>), 80 out_tx: mpsc::Sender<WsMessage>, 81 last_render: Instant, 82 last_cleanup: Instant, 83 clock: Clock, 84 client_id: u64, 85 mouse_pressed: bool, 86 mouse_pos: (f32, f32), 87 current_line_id: u8, 88 next_line_id: u8, 89 needs_redraw: bool, 90 has_any_points: bool, 91} 92 93impl LaserOverlay { 94 pub fn new() -> (mpsc::Sender<WsMessage>, mpsc::Receiver<WsMessage>, Self) { 95 let in_chan = mpsc::channel(1024); 96 let (out_tx, out_rx) = mpsc::channel(512); 97 98 let clock = Clock::new(); 99 let now = clock.now(); 100 101 let this = Self { 102 gfx: None, 103 window: WindowHandle::default(), 104 server_mouse_pos: (0.0, 0.0), 105 laser_points: Default::default(), 106 in_chan, 107 out_tx, 108 last_render: now, 109 last_cleanup: now, 110 clock, 111 client_id: fastrand::u64(..), 112 mouse_pressed: false, 113 mouse_pos: (0.0, 0.0), 114 current_line_id: 0, 115 next_line_id: 1, 116 needs_redraw: false, 117 has_any_points: false, 118 }; 119 120 (this.in_chan.0.clone(), out_rx, this) 121 } 122 123 #[cfg(feature = "server")] 124 pub fn start_mouse_listener(send_tx: tokio::sync::broadcast::Sender<(u64, WsMessage)>) { 125 std::thread::spawn({ 126 use enigo::{Enigo, Mouse, Settings}; 127 128 move || { 129 let enigo = Enigo::new(&Settings::default()).unwrap(); 130 loop { 131 let res = enigo.location(); 132 let Ok(pos) = res else { 133 eprintln!("failed to get mouse position: {res:?}"); 134 continue; 135 }; 136 let msg = WsMessage::Mouse(ws::MouseMessage { 137 x: pos.0 as u32, 138 y: pos.1 as u32, 139 }); 140 let _ = send_tx.send((0, msg)); 141 std::thread::sleep(Duration::from_millis(1000 / 30)); 142 } 143 } 144 }); 145 } 146 147 pub fn window_handle(&self) -> WindowHandle { 148 self.window.clone() 149 } 150 151 pub fn init_graphics(&mut self, window: Arc<Window>) -> AppResult<()> { 152 #[cfg(target_arch = "wasm32")] 153 let size = { 154 use winit::platform::web::WindowExtWebSys; 155 let canvas = window.canvas().unwrap(); 156 let (w, h) = (canvas.client_width(), canvas.client_height()); 157 canvas.set_width(w.try_into().unwrap()); 158 canvas.set_height(h.try_into().unwrap()); 159 winit::dpi::PhysicalSize::new(w as u32, h as u32) 160 }; 161 #[cfg(not(target_arch = "wasm32"))] 162 let size = window.inner_size(); 163 164 let _ = self.window.set(window.clone()); 165 self.gfx = Some(Graphics { 166 renderer: Renderer::new(window.clone(), size.width, size.height)?, 167 pixmap: Pixmap::new(size.width, size.height).unwrap(), 168 window, 169 }); 170 171 self.needs_redraw = true; 172 173 Ok(()) 174 } 175 176 fn handle_mouse_press(&mut self, position: (f32, f32)) { 177 self.mouse_pressed = true; 178 self.mouse_pos = position; 179 self.current_line_id = self.next_line_id; 180 self.next_line_id = self.next_line_id.wrapping_add(1); 181 self.needs_redraw = true; 182 183 let msg = WsMessage::Laser(LaserMessage { 184 x: position.0 as u32, 185 y: position.1 as u32, 186 id: self.client_id, 187 line_id: self.current_line_id, 188 }); 189 let _ = self.in_chan.0.try_send(msg); 190 let _ = self.out_tx.try_send(msg); 191 } 192 193 fn handle_mouse_move(&mut self, position: (f32, f32)) { 194 if !self.mouse_pressed { 195 return; 196 } 197 198 let dx = position.0 - self.mouse_pos.0; 199 let dy = position.1 - self.mouse_pos.1; 200 let distance = (dx * dx + dy * dy).sqrt(); 201 202 if distance < 1.0 { 203 return; 204 } 205 206 self.mouse_pos = position; 207 self.needs_redraw = true; 208 let msg = WsMessage::Laser(LaserMessage { 209 x: position.0 as u32, 210 y: position.1 as u32, 211 id: self.client_id, 212 line_id: self.current_line_id, 213 }); 214 let _ = self.in_chan.0.try_send(msg); 215 let _ = self.out_tx.try_send(msg); 216 } 217 218 fn handle_mouse_release(&mut self) { 219 self.mouse_pressed = false; 220 } 221 222 fn calculate_point_max_age(point_index: usize, total_points: usize) -> Duration { 223 if total_points <= FAST_DECAY_THRESHOLD { 224 return BASE_MAX_AGE; 225 } 226 227 if point_index < FAST_DECAY_THRESHOLD { 228 let progress = point_index as f32 / FAST_DECAY_THRESHOLD as f32; 229 let age_range = BASE_MAX_AGE.as_millis() - MIN_AGE.as_millis(); 230 let calculated_age = MIN_AGE.as_millis() + (age_range as f32 * progress) as u128; 231 Duration::from_millis(calculated_age as u64) 232 } else { 233 BASE_MAX_AGE 234 } 235 } 236 237 #[inline(always)] 238 fn ingest_points(&mut self) { 239 let mut has_any_points = false; 240 241 let should_cleanup = self.last_cleanup.elapsed().as_millis() >= FRAME_TIME_MS as u128; 242 243 if should_cleanup { 244 self.last_cleanup = self.clock.now(); 245 246 let mut ids_to_remove = Vec::new(); 247 for (id, points) in self.laser_points.iter_mut() { 248 let points_len = points.len(); 249 if points_len == 0 { 250 continue; 251 } 252 253 let mut new_points = Vec::with_capacity(points_len); 254 for (index, point) in points.iter().enumerate() { 255 let max_age = Self::calculate_point_max_age(index, points_len); 256 if point.created_at.elapsed() < max_age { 257 new_points.push(point.clone()); 258 } 259 } 260 261 if new_points.len() != points_len { 262 self.needs_redraw = true; 263 } 264 *points = new_points; 265 266 if points.is_empty() { 267 ids_to_remove.push(*id); 268 } else { 269 has_any_points = true; 270 } 271 } 272 273 for id in ids_to_remove { 274 self.laser_points.remove(&id); 275 } 276 } else { 277 has_any_points = self.laser_points.values().any(|points| !points.is_empty()); 278 } 279 280 while let Ok(msg) = self.in_chan.1.try_recv() { 281 match msg { 282 WsMessage::Laser(msg) => { 283 self.laser_points 284 .entry((msg.id, msg.line_id)) 285 .or_default() 286 .push(LaserPoint::new(msg, self.clock.now())); 287 has_any_points = true; 288 } 289 #[cfg(feature = "client")] 290 WsMessage::Mouse(msg) => { 291 self.server_mouse_pos = (msg.x as f32, msg.y as f32); 292 self.needs_redraw = true; 293 } 294 #[cfg(not(feature = "client"))] 295 WsMessage::Mouse(_) => {} 296 } 297 } 298 299 self.has_any_points = has_any_points; 300 } 301 302 #[cfg(feature = "client")] 303 #[inline(always)] 304 fn draw_server_mouse(mut pixmap: PixmapMut, mouse_pos: (f32, f32)) { 305 let (x, y) = mouse_pos; 306 let radius = 10.0; 307 let color = Color::WHITE; 308 309 let mut pb = PathBuilder::new(); 310 pb.push_circle(x, y, radius); 311 312 if let Some(path) = pb.finish() { 313 let mut paint = Paint::default(); 314 paint.set_color(color); 315 paint.anti_alias = true; 316 paint.blend_mode = BlendMode::Source; 317 318 let mut stroke = Stroke::default(); 319 stroke.width = radius * 2.0; 320 stroke.line_cap = LineCap::Round; 321 stroke.line_join = LineJoin::Round; 322 323 pixmap.stroke_path(&path, &paint, &stroke, Transform::identity(), None); 324 } 325 } 326 327 #[inline(always)] 328 fn draw_tapering_laser_line(mut pixmap: PixmapMut, points: &[LaserPoint], now: Instant) { 329 if points.len() < 2 { 330 return; 331 } 332 333 let max_width = 8.0; 334 let min_width = 0.5; 335 let points_len = points.len(); 336 337 let data = points 338 .windows(2) 339 .enumerate() 340 .map(|(i, chunk)| { 341 let current = &chunk[0]; 342 let next = &chunk[1]; 343 344 let progress = i as f32 / (points_len - 2) as f32; 345 let width = min_width + (max_width - min_width) * progress; 346 347 let point_index = i; 348 let max_age = Self::calculate_point_max_age(point_index, points_len); 349 let age = now.duration_since(current.created_at); 350 let age_progress = age.as_millis() as f32 / max_age.as_millis() as f32; 351 let alpha = (255.0 * (1.0 - age_progress.clamp(0.0, 1.0))) as u8; 352 353 let mut pb = PathBuilder::new(); 354 pb.move_to(current.x, current.y); 355 pb.line_to(next.x, next.y); 356 357 (pb.finish(), width, next.color, alpha) 358 }) 359 .collect::<Vec<_>>(); 360 361 // draw glow first so we can draw the actual line on top 362 // otherwise the glow would cover the line 363 for (path, width, color, alpha) in data.iter().filter(|p| p.1 > 2.0) { 364 let Some(path) = path else { 365 continue; 366 }; 367 368 let mut glow_paint = Paint::default(); 369 glow_paint.set_color_rgba8(color[0], color[1], color[2], alpha / 5); 370 glow_paint.anti_alias = true; 371 // replace the existing alpha 372 glow_paint.blend_mode = BlendMode::Source; 373 374 let mut glow_stroke = Stroke::default(); 375 glow_stroke.width = width * 1.8; 376 glow_stroke.line_cap = LineCap::Round; 377 glow_stroke.line_join = LineJoin::Round; 378 379 pixmap.stroke_path( 380 &path, 381 &glow_paint, 382 &glow_stroke, 383 Transform::identity(), 384 None, 385 ); 386 } 387 388 for (path, width, color, alpha) in data { 389 let Some(path) = path else { 390 continue; 391 }; 392 393 let mut paint = Paint::default(); 394 paint.set_color_rgba8(color[0], color[1], color[2], alpha.max(10)); 395 paint.anti_alias = true; 396 // replace the existing alpha (so it doesn't blend with existing pixels, looks bad) 397 paint.blend_mode = BlendMode::Source; 398 399 let mut stroke = Stroke::default(); 400 stroke.width = width; 401 stroke.line_cap = LineCap::Round; 402 stroke.line_join = LineJoin::Round; 403 404 pixmap.stroke_path(&path, &paint, &stroke, Transform::identity(), None); 405 } 406 } 407 408 #[inline(always)] 409 pub fn render(&mut self) -> AppResult<()> { 410 let Some(gfx) = self.gfx.as_mut() else { 411 return Ok(()); 412 }; 413 414 let mut frame = gfx.renderer.frame_mut()?; 415 416 gfx.pixmap.fill(Color::TRANSPARENT); 417 for points in self.laser_points.values() { 418 Self::draw_tapering_laser_line(gfx.pixmap.as_mut(), points, self.clock.now()); 419 } 420 #[cfg(feature = "client")] 421 Self::draw_server_mouse(gfx.pixmap.as_mut(), self.server_mouse_pos); 422 skia_rgba_to_bgra_u32(gfx.pixmap.data(), frame.deref_mut()); 423 424 gfx.window.pre_present_notify(); 425 gfx.renderer.present()?; 426 427 self.last_render = self.clock.now(); 428 self.needs_redraw = false; 429 430 Ok(()) 431 } 432 433 #[inline(always)] 434 pub fn should_render(&self) -> bool { 435 self.has_any_points || self.needs_redraw 436 } 437} 438 439impl ApplicationHandler for LaserOverlay { 440 fn resumed(&mut self, event_loop: &ActiveEventLoop) { 441 let attrs = WindowAttributes::default() 442 .with_title("laser overlay") 443 .with_transparent(true); 444 445 #[cfg(feature = "server")] 446 let attrs = attrs 447 .with_fullscreen(Some(winit::window::Fullscreen::Borderless(None))) 448 .with_window_level(winit::window::WindowLevel::AlwaysOnTop) 449 .with_decorations(false); 450 451 #[cfg(target_arch = "wasm32")] 452 let attrs = winit::platform::web::WindowAttributesExtWebSys::with_append(attrs, true); 453 454 let window = event_loop.create_window(attrs).unwrap(); 455 #[cfg(feature = "server")] 456 let _ = window.set_cursor_hittest(false); 457 458 self.init_graphics(window.into()).unwrap(); 459 460 event_loop.set_control_flow(ControlFlow::wait_duration(Duration::from_millis( 461 FRAME_TIME_MS, 462 ))); 463 } 464 465 fn window_event( 466 &mut self, 467 event_loop: &ActiveEventLoop, 468 _window_id: WindowId, 469 event: WindowEvent, 470 ) { 471 match event { 472 WindowEvent::CloseRequested => { 473 event_loop.exit(); 474 return; 475 } 476 WindowEvent::Touch(touch) => { 477 let pos = (touch.location.x as f32, touch.location.y as f32); 478 match touch.phase { 479 TouchPhase::Started => { 480 self.handle_mouse_press(pos); 481 } 482 TouchPhase::Moved => { 483 self.handle_mouse_move(pos); 484 } 485 TouchPhase::Ended | TouchPhase::Cancelled => { 486 self.handle_mouse_release(); 487 } 488 } 489 } 490 WindowEvent::MouseInput { state, button, .. } => { 491 if button == MouseButton::Left { 492 match state { 493 ElementState::Pressed => { 494 self.handle_mouse_press(self.mouse_pos); 495 } 496 ElementState::Released => { 497 self.handle_mouse_release(); 498 } 499 } 500 } 501 } 502 WindowEvent::CursorMoved { position, .. } => { 503 let pos = (position.x as f32, position.y as f32); 504 self.handle_mouse_move(pos); 505 self.mouse_pos = pos; 506 } 507 WindowEvent::Resized(size) if let Some(gfx) = self.gfx.as_mut() => { 508 gfx.resize(size.width, size.height).unwrap(); 509 } 510 WindowEvent::RedrawRequested => { 511 self.ingest_points(); 512 self.render().unwrap(); 513 } 514 _ => {} 515 } 516 } 517 518 fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) { 519 if self.should_render() { 520 if let Some(gfx) = self.gfx.as_ref() { 521 if self.last_render.elapsed().as_millis() >= FRAME_TIME_MS as u128 { 522 gfx.window.request_redraw(); 523 } 524 } 525 event_loop.set_control_flow(ControlFlow::wait_duration(Duration::from_millis( 526 FRAME_TIME_MS, 527 ))); 528 } else { 529 event_loop.set_control_flow(ControlFlow::Wait); 530 } 531 } 532} 533 534#[allow(unused_mut)] 535fn run_app(event_loop: EventLoop<()>, mut app: LaserOverlay) -> AppResult<()> { 536 #[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))] 537 event_loop.run_app(&mut app)?; 538 #[cfg(any(target_arch = "wasm32", target_arch = "wasm64"))] 539 { 540 console_error_panic_hook::set_once(); 541 winit::platform::web::EventLoopExtWebSys::spawn_app(event_loop, app); 542 } 543 Ok(()) 544} 545 546fn run() -> AppResult<()> { 547 let event_loop = EventLoop::new()?; 548 let (_tx, _rx, app) = LaserOverlay::new(); 549 550 #[cfg(any(feature = "server", feature = "client"))] 551 let window = app.window_handle(); 552 553 #[cfg(feature = "server")] 554 tokio::spawn({ 555 let window = window.clone(); 556 let tx = _tx.clone(); 557 async move { 558 let (server, send_tx) = ws::server::listen(3111, window, tx).await.unwrap(); 559 LaserOverlay::start_mouse_listener(send_tx); 560 server.await; 561 } 562 }); 563 #[cfg(feature = "client")] 564 { 565 #[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))] 566 let _ = tokio_rustls::rustls::crypto::ring::default_provider().install_default(); 567 let client_id = app.client_id; 568 let fut = async move { 569 ws::client::connect(env!("SERVER_URL"), window, _rx, _tx, client_id) 570 .await 571 .unwrap() 572 }; 573 #[cfg(not(target_arch = "wasm32"))] 574 tokio::spawn(fut); 575 #[cfg(target_arch = "wasm32")] 576 wasm_bindgen_futures::spawn_local(fut); 577 } 578 579 run_app(event_loop, app)?; 580 581 Ok(()) 582} 583 584#[cfg(not(target_arch = "wasm32"))] 585#[tokio::main] 586async fn main() -> AppResult<()> { 587 run() 588} 589 590#[cfg(target_arch = "wasm32")] 591fn main() -> AppResult<()> { 592 run() 593}