|
| 1 | +use excaildraw_core::{Element, ElementType}; |
| 2 | +use std::rc::Rc; |
| 3 | +use wasm_bindgen::JsCast; |
| 4 | +use web_sys::{CanvasRenderingContext2d, HtmlCanvasElement, MouseEvent, WheelEvent}; |
| 5 | +use yew::prelude::*; |
| 6 | + |
| 7 | +use crate::canvas::render_scene; |
| 8 | +use crate::viewport::Viewport; |
| 9 | + |
| 10 | +#[derive(Debug, Clone, Copy, PartialEq, Eq)] |
| 11 | +pub enum Tool { |
| 12 | + Select, |
| 13 | + Rectangle, |
| 14 | + Ellipse, |
| 15 | + Freedraw, |
| 16 | +} |
| 17 | + |
| 18 | +#[function_component(App)] |
| 19 | +pub fn app() -> Html { |
| 20 | + let canvas_ref = use_node_ref(); |
| 21 | + let viewport = use_mut_ref(Viewport::default); |
| 22 | + let elements = use_mut_ref(Vec::<Element>::new); |
| 23 | + let tool = use_state(|| Tool::Rectangle); |
| 24 | + let drawing = use_mut_ref(|| None::<(f64, f64)>); |
| 25 | + let preview = use_mut_ref(|| None::<Element>); |
| 26 | + let panning = use_mut_ref(|| None::<(f64, f64)>); |
| 27 | + let freedraw_points = use_mut_ref(Vec::<f64>::new); |
| 28 | + |
| 29 | + let frame = use_state(|| 0u32); |
| 30 | + |
| 31 | + { |
| 32 | + let canvas_ref = canvas_ref.clone(); |
| 33 | + let viewport = viewport.clone(); |
| 34 | + let elements = elements.clone(); |
| 35 | + let preview = preview.clone(); |
| 36 | + let frame_id = *frame; |
| 37 | + use_effect_with(frame_id, move |_| { |
| 38 | + if let Some(canvas) = canvas_ref.cast::<HtmlCanvasElement>() { |
| 39 | + let rect = canvas.get_bounding_client_rect(); |
| 40 | + let width = rect.width(); |
| 41 | + let height = rect.height(); |
| 42 | + canvas.set_width(width as u32); |
| 43 | + canvas.set_height(height as u32); |
| 44 | + if let Ok(Some(ctx)) = canvas |
| 45 | + .get_context("2d") |
| 46 | + .map(|c| c.map(|v| v.dyn_into::<CanvasRenderingContext2d>().ok())) |
| 47 | + .map(|o| o.flatten()) |
| 48 | + { |
| 49 | + let vp = viewport.borrow(); |
| 50 | + let els = elements.borrow(); |
| 51 | + let prev = preview.borrow(); |
| 52 | + render_scene( |
| 53 | + &ctx, |
| 54 | + &vp, |
| 55 | + width, |
| 56 | + height, |
| 57 | + &els, |
| 58 | + prev.as_ref(), |
| 59 | + ); |
| 60 | + } |
| 61 | + } |
| 62 | + || () |
| 63 | + }); |
| 64 | + } |
| 65 | + |
| 66 | + let bump_frame = { |
| 67 | + let frame = frame.clone(); |
| 68 | + Rc::new(move || frame.set(*frame + 1)) |
| 69 | + }; |
| 70 | + |
| 71 | + let on_wheel = { |
| 72 | + let viewport = viewport.clone(); |
| 73 | + let canvas_ref = canvas_ref.clone(); |
| 74 | + let bump_frame = bump_frame.clone(); |
| 75 | + Callback::from(move |e: WheelEvent| { |
| 76 | + e.prevent_default(); |
| 77 | + let rect = canvas_ref |
| 78 | + .cast::<HtmlCanvasElement>() |
| 79 | + .map(|c| c.get_bounding_client_rect()); |
| 80 | + if let Some(rect) = rect { |
| 81 | + let sx = e.client_x() as f64 - rect.left(); |
| 82 | + let sy = e.client_y() as f64 - rect.top(); |
| 83 | + viewport.borrow_mut().zoom_at(sx, sy, -e.delta_y()); |
| 84 | + bump_frame(); |
| 85 | + } |
| 86 | + }) |
| 87 | + }; |
| 88 | + |
| 89 | + let on_mouse_down = { |
| 90 | + let viewport = viewport.clone(); |
| 91 | + let canvas_ref = canvas_ref.clone(); |
| 92 | + let tool = tool.clone(); |
| 93 | + let drawing = drawing.clone(); |
| 94 | + let preview = preview.clone(); |
| 95 | + let panning = panning.clone(); |
| 96 | + let freedraw_points = freedraw_points.clone(); |
| 97 | + let bump_frame = bump_frame.clone(); |
| 98 | + Callback::from(move |e: MouseEvent| { |
| 99 | + let rect = canvas_ref |
| 100 | + .cast::<HtmlCanvasElement>() |
| 101 | + .map(|c| c.get_bounding_client_rect()); |
| 102 | + let Some(rect) = rect else { return }; |
| 103 | + |
| 104 | + let sx = e.client_x() as f64 - rect.left(); |
| 105 | + let sy = e.client_y() as f64 - rect.top(); |
| 106 | + |
| 107 | + if e.button() == 1 || e.shift_key() { |
| 108 | + *panning.borrow_mut() = Some((sx, sy)); |
| 109 | + return; |
| 110 | + } |
| 111 | + |
| 112 | + let (wx, wy) = viewport.borrow().screen_to_world(sx, sy); |
| 113 | + match *tool { |
| 114 | + Tool::Select => {} |
| 115 | + Tool::Rectangle => { |
| 116 | + *drawing.borrow_mut() = Some((wx, wy)); |
| 117 | + *preview.borrow_mut() = |
| 118 | + Some(Element::new(ElementType::Rectangle, wx, wy, 0.0, 0.0)); |
| 119 | + } |
| 120 | + Tool::Ellipse => { |
| 121 | + *drawing.borrow_mut() = Some((wx, wy)); |
| 122 | + *preview.borrow_mut() = |
| 123 | + Some(Element::new(ElementType::Ellipse, wx, wy, 0.0, 0.0)); |
| 124 | + } |
| 125 | + Tool::Freedraw => { |
| 126 | + freedraw_points.borrow_mut().clear(); |
| 127 | + freedraw_points.borrow_mut().extend([wx, wy]); |
| 128 | + let mut el = Element::new(ElementType::Freedraw, wx, wy, 0.0, 0.0); |
| 129 | + el.points = Some(freedraw_points.borrow().clone()); |
| 130 | + *preview.borrow_mut() = Some(el); |
| 131 | + *drawing.borrow_mut() = Some((wx, wy)); |
| 132 | + } |
| 133 | + } |
| 134 | + bump_frame(); |
| 135 | + }) |
| 136 | + }; |
| 137 | + |
| 138 | + let on_mouse_move = { |
| 139 | + let viewport = viewport.clone(); |
| 140 | + let canvas_ref = canvas_ref.clone(); |
| 141 | + let drawing = drawing.clone(); |
| 142 | + let preview = preview.clone(); |
| 143 | + let panning = panning.clone(); |
| 144 | + let freedraw_points = freedraw_points.clone(); |
| 145 | + let tool = tool.clone(); |
| 146 | + let bump_frame = bump_frame.clone(); |
| 147 | + Callback::from(move |e: MouseEvent| { |
| 148 | + let rect = canvas_ref |
| 149 | + .cast::<HtmlCanvasElement>() |
| 150 | + .map(|c| c.get_bounding_client_rect()); |
| 151 | + let Some(rect) = rect else { return }; |
| 152 | + |
| 153 | + let sx = e.client_x() as f64 - rect.left(); |
| 154 | + let sy = e.client_y() as f64 - rect.top(); |
| 155 | + |
| 156 | + if let Some((px, py)) = *panning.borrow() { |
| 157 | + viewport.borrow_mut().pan(sx - px, sy - py); |
| 158 | + *panning.borrow_mut() = Some((sx, sy)); |
| 159 | + return; |
| 160 | + } |
| 161 | + |
| 162 | + if drawing.borrow().is_none() { |
| 163 | + return; |
| 164 | + } |
| 165 | + |
| 166 | + let (wx, wy) = viewport.borrow().screen_to_world(sx, sy); |
| 167 | + match *tool { |
| 168 | + Tool::Rectangle | Tool::Ellipse => { |
| 169 | + if let Some((sx0, sy0)) = *drawing.borrow() { |
| 170 | + if let Some(p) = preview.borrow_mut().as_mut() { |
| 171 | + p.x = sx0.min(wx); |
| 172 | + p.y = sy0.min(wy); |
| 173 | + p.width = (wx - sx0).abs(); |
| 174 | + p.height = (wy - sy0).abs(); |
| 175 | + } |
| 176 | + } |
| 177 | + } |
| 178 | + Tool::Freedraw => { |
| 179 | + freedraw_points.borrow_mut().extend([wx, wy]); |
| 180 | + if let Some(p) = preview.borrow_mut().as_mut() { |
| 181 | + p.points = Some(freedraw_points.borrow().clone()); |
| 182 | + } |
| 183 | + } |
| 184 | + Tool::Select => {} |
| 185 | + } |
| 186 | + bump_frame(); |
| 187 | + }) |
| 188 | + }; |
| 189 | + |
| 190 | + let on_mouse_up = { |
| 191 | + let drawing = drawing.clone(); |
| 192 | + let preview = preview.clone(); |
| 193 | + let elements = elements.clone(); |
| 194 | + let panning = panning.clone(); |
| 195 | + let bump_frame = bump_frame.clone(); |
| 196 | + Callback::from(move |_: MouseEvent| { |
| 197 | + if panning.borrow_mut().take().is_some() { |
| 198 | + return; |
| 199 | + } |
| 200 | + if let Some(mut el) = preview.borrow_mut().take() { |
| 201 | + if el.width.abs() > 1.0 || el.height.abs() > 1.0 || el.points.is_some() { |
| 202 | + el.bump_version(); |
| 203 | + elements.borrow_mut().push(el); |
| 204 | + } |
| 205 | + } |
| 206 | + drawing.borrow_mut().take(); |
| 207 | + bump_frame(); |
| 208 | + }) |
| 209 | + }; |
| 210 | + |
| 211 | + let set_tool = |t: Tool| { |
| 212 | + let tool = tool.clone(); |
| 213 | + Callback::from(move |_| tool.set(t)) |
| 214 | + }; |
| 215 | + |
| 216 | + html! { |
| 217 | + <div class="app"> |
| 218 | + <header class="toolbar"> |
| 219 | + <span class="brand">{"excaildraw"}</span> |
| 220 | + <button class={classes!("tool-btn", (*tool == Tool::Select).then_some("active"))} |
| 221 | + onclick={set_tool(Tool::Select)}>{"Select"}</button> |
| 222 | + <button class={classes!("tool-btn", (*tool == Tool::Rectangle).then_some("active"))} |
| 223 | + onclick={set_tool(Tool::Rectangle)}>{"Rectangle"}</button> |
| 224 | + <button class={classes!("tool-btn", (*tool == Tool::Ellipse).then_some("active"))} |
| 225 | + onclick={set_tool(Tool::Ellipse)}>{"Ellipse"}</button> |
| 226 | + <button class={classes!("tool-btn", (*tool == Tool::Freedraw).then_some("active"))} |
| 227 | + onclick={set_tool(Tool::Freedraw)}>{"Draw"}</button> |
| 228 | + <span class="hint">{"Scroll to zoom · Shift+drag to pan"}</span> |
| 229 | + </header> |
| 230 | + <canvas |
| 231 | + ref={canvas_ref} |
| 232 | + class="canvas" |
| 233 | + onwheel={on_wheel} |
| 234 | + onmousedown={on_mouse_down} |
| 235 | + onmousemove={on_mouse_move} |
| 236 | + onmouseup={on_mouse_up.clone()} |
| 237 | + onmouseleave={on_mouse_up} |
| 238 | + /> |
| 239 | + </div> |
| 240 | + } |
| 241 | +} |
0 commit comments