284 lines
8.4 KiB
Rust
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(())
|
|
}
|