initial commit
This commit is contained in:
commit
bf2272e70b
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/target
|
16
Cargo.toml
Normal file
16
Cargo.toml
Normal file
@ -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"] }
|
8
index.html
Normal file
8
index.html
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>LJ Sketch</title>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<link data-trunk rel="css" href="style.css"/>
|
||||||
|
</head>
|
||||||
|
</html>
|
136
src/canvas.rs
Normal file
136
src/canvas.rs
Normal file
@ -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<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)
|
||||||
|
}
|
60
src/colors.rs
Normal file
60
src/colors.rs
Normal file
@ -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<Color> for String {
|
||||||
|
fn from(color: Color) -> String {
|
||||||
|
format!("#{:02x}{:02x}{:02x}", color.0[0], color.0[1], color.0[2])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<String> for Color {
|
||||||
|
type Error = &'static str;
|
||||||
|
|
||||||
|
fn try_from(s: String) -> Result<Color, Self::Error> {
|
||||||
|
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<Color, _> = "#ff0042".to_string().try_into();
|
||||||
|
assert_eq!(c, Ok(Color([0xff, 0x00, 0x42])));
|
||||||
|
|
||||||
|
let c : Result<Color, _> = "ff0042".to_string().try_into();
|
||||||
|
assert_eq!(c, Err("Badly formated color: should start with '#'"));
|
||||||
|
|
||||||
|
let c : Result<Color, _> = "#ff004242".to_string().try_into();
|
||||||
|
assert_eq!(c, Err("Badly formated color"));
|
||||||
|
|
||||||
|
let c : Result<Color, _> = "#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");
|
||||||
|
}
|
44
src/draw_app.rs
Normal file
44
src/draw_app.rs
Normal file
@ -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! {
|
||||||
|
<div class="drawApp">
|
||||||
|
<ToolboxComp {on_changecolor} {on_clearcanvas}
|
||||||
|
color={color.clone()} />
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<CanvasComp width=1024 height=1024
|
||||||
|
{sketch} {on_newline}
|
||||||
|
color={color.clone()} />
|
||||||
|
<span class="footer">{"LJ Sketch"}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
21
src/main.rs
Normal file
21
src/main.rs
Normal file
@ -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! {
|
||||||
|
<div>
|
||||||
|
<DrawApp />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
wasm_logger::init(wasm_logger::Config::default());
|
||||||
|
yew::Renderer::<App>::new().render();
|
||||||
|
}
|
51
src/toolbox.rs
Normal file
51
src/toolbox.rs
Normal file
@ -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<String>,
|
||||||
|
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::<HtmlInputElement>() {
|
||||||
|
parent_onchange.emit(input.value());
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
let onclearcanvas = {
|
||||||
|
let parent_onclearcanvas = props.on_clearcanvas.clone();
|
||||||
|
Callback::from(move | _event: MouseEvent | {
|
||||||
|
parent_onclearcanvas.emit(());
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
html! {
|
||||||
|
<div class="toolbox">
|
||||||
|
{ 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! { <div class="color" style={style} {onclick} /> }
|
||||||
|
}) }
|
||||||
|
|
||||||
|
<input type="color" class="button2x" {onchange}
|
||||||
|
value={props.color.clone()} />
|
||||||
|
|
||||||
|
<input type="button" class="button2x" onclick={onclearcanvas}
|
||||||
|
value="♻" />
|
||||||
|
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
82
style.css
Normal file
82
style.css
Normal file
@ -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;
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user