use std::cell::RefCell; use std::rc::Rc; use wasm_bindgen::closure::Closure; use wasm_bindgen::prelude::*; use wasm_bindgen::JsCast; use web_sys::{ window, CanvasRenderingContext2d, Document, HtmlCanvasElement, KeyboardEvent, WheelEvent, }; #[derive(Default, Clone, Copy)] struct InputState { w: bool, a: bool, s: bool, d: bool, } struct Camera { x: f64, y: f64, ppm: f64, } struct SceneState { camera: Camera, speed_pps: f64, triangles: Vec<(&'static str, [(f64, f64); 3])>, need_camera_setup: bool, } impl SceneState { fn new() -> Self { Self { camera: Camera { x: 0.0, y: 0.0, ppm: 10.0, }, speed_pps: 900.0, triangles: vec![ ("#ff6b6b", [(0.0, 0.0), (3.0, 0.0), (1.5, 2.5)]), ("#4dabf7", [(-4.0, -2.0), (-1.0, -3.5), (-2.5, 0.5)]), ("#ffd43b", [(2.0, -4.0), (5.0, -4.0), (4.0, -1.0)]), ], need_camera_setup: true, } } fn setup_camera_if_needed(&mut self, width: f64, height: f64) { if !self.need_camera_setup { return; } if width <= 0.0 || height <= 0.0 || self.camera.ppm <= 0.0 { return; } self.camera.x = -width / (2.0 * self.camera.ppm); self.camera.y = -height / (2.0 * self.camera.ppm); self.need_camera_setup = false; } } fn update_key(state: &mut InputState, code: &str, is_down: bool) -> bool { match code { "KeyW" => { state.w = is_down; true } "KeyA" => { state.a = is_down; true } "KeyS" => { state.s = is_down; true } "KeyD" => { state.d = is_down; true } _ => false, } } #[wasm_bindgen] pub fn init_index() -> Result<(), JsValue> { let document: Document = window().unwrap().document().unwrap(); let canvas: HtmlCanvasElement = document .get_element_by_id("canvas") .ok_or_else(|| JsValue::from_str("Missing #canvas element"))? .dyn_into()?; let context: CanvasRenderingContext2d = canvas .get_context("2d")? .ok_or_else(|| JsValue::from_str("Missing 2d context"))? .dyn_into()?; let window = window().unwrap(); let canvas = canvas.clone(); let context = context.clone(); let input = Rc::new(RefCell::new(InputState::default())); let scene = Rc::new(RefCell::new(SceneState::new())); let last_time = Rc::new(RefCell::new( window .performance() .ok_or_else(|| JsValue::from_str("Missing performance"))? .now(), )); let input_down = input.clone(); let keydown = Closure::::wrap(Box::new(move |event: KeyboardEvent| { let code = event.code(); let mut state = input_down.borrow_mut(); if update_key(&mut state, &code, true) { event.prevent_default(); } })); window.add_event_listener_with_callback("keydown", keydown.as_ref().unchecked_ref())?; keydown.forget(); let input_up = input.clone(); let keyup = Closure::::wrap(Box::new(move |event: KeyboardEvent| { let code = event.code(); let mut state = input_up.borrow_mut(); if update_key(&mut state, &code, false) { event.prevent_default(); } })); window.add_event_listener_with_callback("keyup", keyup.as_ref().unchecked_ref())?; keyup.forget(); let scene_zoom = scene.clone(); let canvas_zoom = canvas.clone(); let wheel = Closure::::wrap(Box::new(move |event: WheelEvent| { event.prevent_default(); let pointer_x = event.offset_x() as f64; let pointer_y = event.offset_y() as f64; let mut scene = scene_zoom.borrow_mut(); let old_ppm = scene.camera.ppm; let zoom_factor = (-event.delta_y() * 0.001).exp(); let mut new_ppm = old_ppm * zoom_factor; let min_ppm = 2.0; let max_ppm = 200.0; if new_ppm < min_ppm { new_ppm = min_ppm; } if new_ppm > max_ppm { new_ppm = max_ppm; } let world_x = scene.camera.x + pointer_x / old_ppm; let world_y = scene.camera.y + pointer_y / old_ppm; scene.camera.ppm = new_ppm; scene.camera.x = world_x - pointer_x / new_ppm; scene.camera.y = world_y - pointer_y / new_ppm; })); canvas_zoom.add_event_listener_with_callback("wheel", wheel.as_ref().unchecked_ref())?; wheel.forget(); let draw = Rc::new(RefCell::new(None::>)); let draw_handle = draw.clone(); let window_handle = window.clone(); let input_handle = input.clone(); let scene_handle = scene.clone(); let last_time_handle = last_time.clone(); *draw_handle.borrow_mut() = Some(Closure::wrap(Box::new(move || { let now = window_handle .performance() .map(|perf| perf.now()) .unwrap_or(0.0); let mut last = last_time_handle.borrow_mut(); let mut dt = (now - *last) / 1000.0; *last = now; if dt.is_nan() || dt.is_infinite() { dt = 0.0; } if dt > 0.05 { dt = 0.05; } let width = window_handle .inner_width() .ok() .and_then(|v| v.as_f64()) .unwrap_or(0.0); let height = window_handle .inner_height() .ok() .and_then(|v| v.as_f64()) .unwrap_or(0.0); canvas.set_width(width as u32); canvas.set_height(height as u32); let input = input_handle.borrow(); let mut scene = scene_handle.borrow_mut(); scene.setup_camera_if_needed(width, height); let step = (scene.speed_pps / scene.camera.ppm) * dt; if input.w { scene.camera.y -= step; } if input.s { scene.camera.y += step; } if input.a { scene.camera.x -= step; } if input.d { scene.camera.x += step; } context.set_fill_style(&JsValue::from_str("#0b0f17")); context.fill_rect(0.0, 0.0, width, height); context.set_line_width(1.0); context.set_stroke_style(&JsValue::from_str("#111827")); let to_screen = |(x, y): (f64, f64)| -> (f64, f64) { let sx = (x - scene.camera.x) * scene.camera.ppm; let sy = (y - scene.camera.y) * scene.camera.ppm; (sx, sy) }; for (color, points) in scene.triangles.iter() { let (x0, y0) = to_screen(points[0]); let (x1, y1) = to_screen(points[1]); let (x2, y2) = to_screen(points[2]); context.begin_path(); context.move_to(x0, y0); context.line_to(x1, y1); context.line_to(x2, y2); context.close_path(); context.set_fill_style(&JsValue::from_str(color)); context.fill(); context.stroke(); } let box_world_x = 15.0; let box_world_y = 3.0; let box_world_w = 6.0; let box_world_h = 2.0; let (box_x, box_y) = to_screen((box_world_x, box_world_y)); let (box_x2, box_y2) = to_screen((box_world_x + box_world_w, box_world_y + box_world_h)); let box_w = box_x2 - box_x; let box_h = box_y2 - box_y; context.set_fill_style(&JsValue::from_str("rgba(17, 24, 39, 0.85)")); context.fill_rect(box_x, box_y, box_w, box_h); context.set_stroke_style(&JsValue::from_str("#e5e7eb")); context.stroke_rect(box_x, box_y, box_w, box_h); let font_px = (0.8 * scene.camera.ppm).clamp(10.0, 64.0); context.set_font(&format!("{:.0}px sans-serif", font_px)); context.set_text_baseline("top"); context.set_fill_style(&JsValue::from_str("#e5e7eb")); let _ = context.fill_text("123\nllo-world hello-world hello-world \n ========== hello-world \n hello-world ", box_x + 6.0, box_y + 4.0); let _ = window_handle.request_animation_frame( draw.borrow().as_ref().unwrap().as_ref().unchecked_ref(), ); }) as Box)); window.request_animation_frame( draw_handle .borrow() .as_ref() .unwrap() .as_ref() .unchecked_ref(), )?; Ok(()) }