From bf2272e70b969923a2a9af2fc7c549b7e83c8029 Mon Sep 17 00:00:00 2001 From: Marc Planard Date: Sun, 8 Oct 2023 13:25:48 +0200 Subject: [PATCH] initial commit --- .gitignore | 1 + Cargo.toml | 16 ++++++ index.html | 8 +++ src/canvas.rs | 136 ++++++++++++++++++++++++++++++++++++++++++++++++ src/colors.rs | 60 +++++++++++++++++++++ src/draw_app.rs | 44 ++++++++++++++++ src/main.rs | 21 ++++++++ src/toolbox.rs | 51 ++++++++++++++++++ style.css | 82 +++++++++++++++++++++++++++++ 9 files changed, 419 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 index.html create mode 100644 src/canvas.rs create mode 100644 src/colors.rs create mode 100644 src/draw_app.rs create mode 100644 src/main.rs create mode 100644 src/toolbox.rs create mode 100644 style.css diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..6110382 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "lj_sketch_front" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +js-sys = "0.3.64" +log = "0.4.19" +wasm-bindgen = "0.2.87" +wasm-logger = "0.2.0" +web-sys = { version = "0.3.64", features = ["HtmlCanvasElement", + "CanvasRenderingContext2d", + "MouseEvent","DomRect"] } +yew = { version = "0.20.0", features = ["csr"] } diff --git a/index.html b/index.html new file mode 100644 index 0000000..2f2f6b4 --- /dev/null +++ b/index.html @@ -0,0 +1,8 @@ + + + + LJ Sketch + + + + diff --git a/src/canvas.rs b/src/canvas.rs new file mode 100644 index 0000000..1778fb3 --- /dev/null +++ b/src/canvas.rs @@ -0,0 +1,136 @@ +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) +} diff --git a/src/colors.rs b/src/colors.rs new file mode 100644 index 0000000..b7015a7 --- /dev/null +++ b/src/colors.rs @@ -0,0 +1,60 @@ +pub const COLORS : [&str; 16] = [ + "#000000", "#1D2B53", "#7E2553", "#008751", + "#AB5236", "#5F574F", "#C2C3C7", "#FFF1E8", + "#FF004D", "#FFA300", "#FFEC27", "#00E436", + "#29ADFF", "#83769C", "#FF77A8", "#FFCCAA" +]; + +#[derive(Clone,Copy,PartialEq,Debug)] +pub struct Color([u8 ; 3]); + +impl From for String { + fn from(color: Color) -> String { + format!("#{:02x}{:02x}{:02x}", color.0[0], color.0[1], color.0[2]) + } +} + +impl TryFrom for Color { + type Error = &'static str; + + fn try_from(s: String) -> Result { + let ("#", rest) = s[..].split_at(1) else { + return Err("Badly formated color: should start with '#'"); + }; + if rest.len() != 6 { + return Err("Badly formated color"); + } + let Ok(r) = u8::from_str_radix(&rest[0..2], 16) else { + return Err("Badly formated color"); + }; + let Ok(g) = u8::from_str_radix(&rest[2..4], 16) else { + return Err("Badly formated color"); + }; + let Ok(b) = u8::from_str_radix(&rest[4..6], 16) else { + return Err("Badly formated color"); + }; + + Ok(Color([r,g,b])) + } +} + +#[test] +fn test_string_to_color() { + let c : Result = "#ff0042".to_string().try_into(); + assert_eq!(c, Ok(Color([0xff, 0x00, 0x42]))); + + let c : Result = "ff0042".to_string().try_into(); + assert_eq!(c, Err("Badly formated color: should start with '#'")); + + let c : Result = "#ff004242".to_string().try_into(); + assert_eq!(c, Err("Badly formated color")); + + let c : Result = "#ffg042".to_string().try_into(); + assert_eq!(c, Err("Badly formated color")); +} + +#[test] +fn test_color_to_string() { + let s : String = Color([0xff, 0x00, 0x42]).into(); + assert_eq!(s, "#ff0042"); +} diff --git a/src/draw_app.rs b/src/draw_app.rs new file mode 100644 index 0000000..8d8a64d --- /dev/null +++ b/src/draw_app.rs @@ -0,0 +1,44 @@ +use yew::prelude::*; + +use crate::canvas::{CanvasComp,Line,Sketch}; +use crate::toolbox::ToolboxComp; + +#[function_component] +pub fn DrawApp() -> Html { + let sketch_handle = use_state(|| Sketch{ lines: vec![] }); + let sketch = (*sketch_handle).clone(); + + let color_handle = use_state(|| String::from("#ffffff")); + let color = (*color_handle).clone(); + + let on_changecolor = Callback::from(move | color: String | { + color_handle.set(color); + }); + + let on_clearcanvas = { + let sketch_handle = sketch_handle.clone(); + Callback::from(move | _: () | { + sketch_handle.set(Sketch { lines: vec![] }); + }) + }; + + let on_newline = Callback::from(move | line: Line | { + let mut s = (*sketch_handle).clone(); + s.lines.push(line); + sketch_handle.set(s); + }); + + html! { +
+ + +
+ + {"LJ Sketch"} +
+
+ } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..c65c3a7 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,21 @@ +mod canvas; +mod toolbox; +mod draw_app; +mod colors; + +use yew::prelude::*; +use draw_app::DrawApp; + +#[function_component] +pub fn App() -> Html { + html! { +
+ +
+ } +} + +fn main() { + wasm_logger::init(wasm_logger::Config::default()); + yew::Renderer::::new().render(); +} diff --git a/src/toolbox.rs b/src/toolbox.rs new file mode 100644 index 0000000..5709d64 --- /dev/null +++ b/src/toolbox.rs @@ -0,0 +1,51 @@ +use yew::prelude::*; +use web_sys::{HtmlInputElement}; +use crate::colors::COLORS; + +#[derive(PartialEq, Properties, Clone)] +pub struct ToolboxProps { + pub color: String, + pub on_changecolor: Callback, + pub on_clearcanvas: Callback<()> +} + +#[function_component] +pub fn ToolboxComp(props: &ToolboxProps) -> Html { + let onchange = { + let parent_onchange = props.on_changecolor.clone(); + Callback::from(move | event: Event | { + if let Some(input) = event.target_dyn_into::() { + parent_onchange.emit(input.value()); + } + }) + }; + + let onclearcanvas = { + let parent_onclearcanvas = props.on_clearcanvas.clone(); + Callback::from(move | _event: MouseEvent | { + parent_onclearcanvas.emit(()); + }) + }; + + html! { +
+ { for COLORS.iter().map(| &color | { + let style = format!("background: {};", color); + let onclick = { + let parent_onchange = props.on_changecolor.clone(); + Callback::from(move | _event | { + parent_onchange.emit(color.to_string()); + }) + }; + html! {
} + }) } + + + + + +
+ } +} diff --git a/style.css b/style.css new file mode 100644 index 0000000..180e509 --- /dev/null +++ b/style.css @@ -0,0 +1,82 @@ +body { + margin: 0px; + padding: 0px; + background: #222; + color: #ddd; + font-family: monospace; +} + +.drawApp { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 10px; + grid-auto-rows: minmax(100px, auto); + + position: absolute; + top: 0px; + right: 0px; + bottom: 0px; + left: 0px; + margin: 2em; +} + +.drawApp > * > canvas { + margin: 0 auto; + display: block; + height: 100%; + aspect-ratio: 1/1; +} + +.drawApp > * > canvas:hover { + cursor: crosshair; +} + +.drawApp > .toolbox { + display: inline-block; + margin: 0 auto; + width: 128px; + padding: 1em; +} + + +.drawApp > .toolbox > input { + display: inline-block; + width: 64px; + height: 64px; + font-size: 64px; +} + +.drawApp > .toolbox > .button2x { + width: 128px !important; + height: 128px !important; +} + +.drawApp > .toolbox > .color { + display: inline-block; + width: 64px; + height: 64px; +} + +.drawApp > * > .footer { + display: block; + text-align: center; +} + +#errorBox { + border: thick solid #990000; + background: #dd0000; + position: fixed; + bottom: 0px; + right: 0px; + text-align: center; + padding: 1em; + font-size: x-large; +} + +.visible { + display: block; +} + +.invisible { + display: none; +}