284 lines
8.4 KiB
Rust

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::<dyn FnMut(KeyboardEvent)>::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::<dyn FnMut(KeyboardEvent)>::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::<dyn FnMut(WheelEvent)>::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::<Closure<dyn FnMut()>>));
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<dyn FnMut()>));
window.request_animation_frame(
draw_handle
.borrow()
.as_ref()
.unwrap()
.as_ref()
.unchecked_ref(),
)?;
Ok(())
}