137 lines
3.6 KiB
Rust
137 lines
3.6 KiB
Rust
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<Point>;
|
|
|
|
#[derive(Clone,Debug,Properties,PartialEq)]
|
|
pub struct Sketch {
|
|
pub lines: Vec<Line>
|
|
}
|
|
|
|
#[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<Line>
|
|
}
|
|
|
|
impl Component for CanvasComp {
|
|
type Message = Option<CanvasMsg>;
|
|
type Properties = CanvasProps;
|
|
|
|
fn create(_ctx: &Context<Self>) -> Self {
|
|
Self {
|
|
node_ref: NodeRef::default(),
|
|
line: vec![],
|
|
}
|
|
}
|
|
|
|
fn view(&self, ctx: &Context<Self>) -> 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! {
|
|
<canvas {width} {height} {onmousedown} {onmousemove} {onmouseup}
|
|
ref={self.node_ref.clone()} />
|
|
}
|
|
}
|
|
|
|
fn update(&mut self, ctx: &Context<Self>, 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<Self>, _first_render: bool) {
|
|
let canvas = self.node_ref.cast::<HtmlCanvasElement>().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::<Element>().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)
|
|
}
|