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