use yew::prelude::*; use web_sys::{HtmlCanvasElement, CanvasRenderingContext2d, MouseEvent, EventTarget, Element, DomRect}; use wasm_bindgen::{JsValue, JsCast}; pub type Point = (f32, f32, String); pub type Line = Vec; #[derive(Clone,Debug,Properties,PartialEq)] pub struct Sketch { pub lines: Vec } #[derive(Debug)] pub struct CanvasComp { node_ref: NodeRef, line: Line, } #[derive(Debug)] pub enum CanvasMsg { MoveTo(Point), LineTo(Point), Stroke, } #[derive(Clone, PartialEq, Properties, Debug)] pub struct CanvasProps { pub width: u32, pub height: u32, pub color: String, pub sketch: Sketch, pub on_newline: Callback } impl Component for CanvasComp { type Message = Option; type Properties = CanvasProps; fn create(_ctx: &Context) -> Self { Self { node_ref: NodeRef::default(), line: vec![], } } fn view(&self, ctx: &Context) -> Html { let props = ctx.props(); let size = (props.width as f32 , props.height as f32); let width : AttrValue = format!("{}", props.width).into(); let height : AttrValue = format!("{}", props.height).into(); let onmousedown = { let color = props.color.clone(); ctx.link().callback(move |e: MouseEvent| { let (x, y) = get_position(e, size); Some(CanvasMsg::MoveTo((x, y, color.clone()))) }) }; let onmousemove = { let color = props.color.clone(); ctx.link().callback(move |e: MouseEvent| { match e.buttons() { 0 => None, _ => { let (x, y) = get_position(e, size); Some(CanvasMsg::LineTo((x, y, color.clone()))) } } }) }; let onmouseup = ctx.link().callback(move |_| Some(CanvasMsg::Stroke)); html! { } } fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool { let Some(msg) = msg else { return false; }; match msg { CanvasMsg::MoveTo(point) => self.line = vec![ point ], CanvasMsg::LineTo(point) => self.line.push(point), CanvasMsg::Stroke => { ctx.props().on_newline.emit(self.line.clone()); self.line.clear(); } }; true } fn rendered(&mut self, ctx: &Context, _first_render: bool) { let canvas = self.node_ref.cast::().unwrap(); let ctx2d: CanvasRenderingContext2d = canvas .get_context("2d").unwrap().unwrap() .dyn_into().unwrap(); let (width, height) = (canvas.width(), canvas.height()); ctx2d.set_line_width(2.0); ctx2d.set_fill_style(&JsValue::from_str("#000000")); ctx2d.fill_rect(0.0, 0.0, width.into(), height.into()); for line in &ctx.props().sketch.lines { draw_line(&ctx2d, line); } if !self.line.is_empty() { draw_line(&ctx2d, &self.line); } } } fn draw_line(ctx: &CanvasRenderingContext2d, line: &Line) { ctx.set_stroke_style(&JsValue::from_str(&line[0].2)); ctx.begin_path(); ctx.move_to(line[0].0.into(), line[0].1.into()); for (x, y, _) in &line[1..] { ctx.line_to((*x).into(), (*y).into()); } ctx.stroke(); } fn get_position(e: MouseEvent, (w, h) : (f32, f32)) -> (f32, f32) { let target = e.target().and_then(|event_target: EventTarget| { event_target.dyn_into::().ok() }).unwrap(); let rect: DomRect = target.get_bounding_client_rect(); let (width, height) = (rect.width(), rect.height()); let mouse_x = w * e.offset_x() as f32 / width as f32; let mouse_y = h * e.offset_y() as f32 / height as f32; (mouse_x, mouse_y) }