diff --git a/pub/index.html b/pub/index.html
new file mode 100644
index 0000000..8446511
--- /dev/null
+++ b/pub/index.html
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/pub/main.js b/pub/main.js
new file mode 100644
index 0000000..c055ffc
--- /dev/null
+++ b/pub/main.js
@@ -0,0 +1,130 @@
+const colors = [
+ "#000000",
+ "#1D2B53",
+ "#7E2553",
+ "#008751",
+ "#AB5236",
+ "#5F574F",
+ "#C2C3C7",
+ "#FFF1E8",
+ "#FF004D",
+ "#FFA300",
+ "#FFEC27",
+ "#00E436",
+ "#29ADFF",
+ "#83769C",
+ "#FF77A8",
+ "#FFCCAA"
+];
+
+const initUI = (colorsDiv, selectedColor) => {
+ for (const color of colors) {
+ (color => {
+ const div = document.createElement("div");
+ div.setAttribute("class", "color");
+ div.setAttribute("style", "background: "+color+";");
+ div.onclick = evt => { selectedColor.value = color; };
+ colorsDiv.appendChild(div);
+ })(color);
+ }
+};
+
+//Get Mouse Position
+const getMousePoint = (canvas, evt, color) => {
+ var rect = canvas.getBoundingClientRect();
+ const x = Math.floor(canvas.width * (evt.clientX - rect.left) / rect.width);
+ const y = Math.floor(canvas.height * (evt.clientY - rect.top) / rect.width);
+ return { x, y, color };
+};
+
+const updateLine = (ctx, line) => {
+ ctx.strokeStyle = line[0].color;
+ ctx.beginPath();
+ ctx.moveTo(line[0].x, line[0].y);
+ for(let i=1; i < line.length; ++i) {
+ ctx.lineTo(line[i].x, line[i].y);
+ }
+ ctx.stroke();
+};
+
+const updateCanvas = (canvas, ctx, lines) => {
+ ctx.fillStyle = "black";
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
+ for(let line of lines) {
+ updateLine(ctx, line);
+ }
+};
+
+const initDrawing = (canvas, clearButton, selectedColor, ws) => {
+ const ctx = canvas.getContext("2d");
+ ctx.lineWidth = 2;
+ let lines = [];
+ let currentLine = null;
+
+ const resetCanvas = () => {
+ ctx.fillStyle = "black";
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
+ };
+ resetCanvas();
+
+ clearButton.onclick = evt => {
+ lines = [];
+ resetCanvas();
+ ws.send(JSON.stringify({ t: "clear" }));
+ };
+
+ canvas.addEventListener("mousedown", evt => {
+ const pt = getMousePoint(canvas, evt, selectedColor.value);
+ currentLine = [ pt ];
+ ws.send(JSON.stringify({ ...pt, t: "moveTo" }));
+ });
+
+ canvas.addEventListener("mousemove", evt => {
+ if(evt.buttons === 1) {
+ const pt = getMousePoint(canvas, evt, selectedColor.value);
+ currentLine.push(pt);
+ updateLine(ctx, currentLine);
+ ws.send(JSON.stringify({ ...pt, t: "lineTo" }));
+ }
+ });
+
+ canvas.addEventListener("mouseup", evt => {
+ console.log(currentLine);
+ currentLine = null;
+ ws.send(JSON.stringify({ t: "stroke"}));
+ });
+
+ ws.addEventListener('message', function (event) {
+ let j = JSON.parse(event.data);
+ console.log(j);
+ if (j.t === "line") {
+ let line = j.line.map(a => ({ x: a[0], y: a[1], color: a[2] }));
+ lines.push(line);
+ updateCanvas(canvas, ctx, lines);
+ } else if (j.t == "clear") {
+ lines = [];
+ resetCanvas();
+ }
+ });
+};
+
+
+const initWs = () => {
+ const socket = new WebSocket('ws://localhost:3000/ws');
+
+ socket.addEventListener('open', function (event) {
+ });
+
+ return socket;
+};
+
+window.onload = () => {
+ const colorsDiv = document.querySelector("#colors");
+ const selectedColor = document.querySelector("#selectedColor");
+ const clearButton = document.querySelector("#clearButton");
+ const canvas = document.querySelector("#canvas");
+ initUI(colorsDiv, selectedColor);
+ let ws = initWs();
+ initDrawing(canvas, clearButton, selectedColor, ws);
+
+};
diff --git a/pub/style.css b/pub/style.css
new file mode 100644
index 0000000..0ad422d
--- /dev/null
+++ b/pub/style.css
@@ -0,0 +1,66 @@
+body {
+ margin: 0px;
+ padding: 0px;
+ background: #222;
+ color: #ddd;
+ font-family: monospace;
+}
+
+#main {
+ 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;
+}
+
+#canvas {
+ margin: 0 auto;
+ display: block;
+ height: 100%;
+ aspect-ratio: 1/1;
+}
+
+#canvas:hover {
+ cursor: crosshair;
+}
+
+#toolbox {
+ display: inline-block;
+ margin: 0 auto;
+ width: 128px;
+ padding: 1em;
+}
+
+.button2x {
+ width: 128px !important;
+ height: 128px !important;
+}
+
+#toolbox > input {
+ display: inline-block;
+ width: 64px;
+ height: 64px;
+ font-size: 64px;
+}
+
+#colors {
+}
+
+.color {
+ display: inline-block;
+ width: 64px;
+ height: 64px;
+}
+
+.footer {
+ display: block;
+ text-align: center;
+}
+
diff --git a/src/gen_server.rs b/src/gen_server.rs
index ff72696..38e336d 100644
--- a/src/gen_server.rs
+++ b/src/gen_server.rs
@@ -1,25 +1,25 @@
use std::collections::HashMap;
use std::net::SocketAddr;
-use tokio::sync::mpsc:: {Sender, Receiver};
-use crate::ws_client::Line;
+use crate::line::Line;
+use tokio::sync::mpsc::{self, Sender, Receiver};
#[derive(Debug, Clone)]
pub enum GSMsg {
NewClient((SocketAddr, Sender
)),
- NewLine(Line),
DeleteClient(SocketAddr),
+ NewLine(Line),
Clear
}
-pub struct State {
- pub gs_tx: Sender
+pub fn spawn() -> Sender {
+ let (tx, rx) = mpsc::channel(32);
+ tokio::spawn(gen_server(rx));
+ tx
}
-pub async fn gen_server(mut rx: Receiver) {
- let mut clients : HashMap> =
- HashMap::new();
-
- let mut lines : Vec = vec![];
+async fn gen_server(mut rx: Receiver) {
+ let mut clients: HashMap> = HashMap::new();
+ let mut lines: Vec = vec![];
while let Some(msg) = rx.recv().await {
match msg {
@@ -36,8 +36,8 @@ pub async fn gen_server(mut rx: Receiver) {
lines.push(line);
},
GSMsg::DeleteClient(addr) => {
- tracing::info!("Client {addr} removed");
clients.remove(&addr);
+ tracing::info!("Client {addr} removed");
},
GSMsg::Clear => {
send_all(&mut clients, &GSMsg::Clear).await;
@@ -52,8 +52,8 @@ async fn send_all(
msg: &GSMsg
) {
let mut to_remove : Vec = vec![];
-
- for (addr, ref mut tx) in &mut *clients {
+
+ for (addr, ref mut tx) in clients.iter() {
let ret = tx
.send(msg.clone())
.await;
diff --git a/src/line.rs b/src/line.rs
new file mode 100644
index 0000000..1698d47
--- /dev/null
+++ b/src/line.rs
@@ -0,0 +1,19 @@
+use geo::Simplify;
+
+pub type Line = Vec<(f32,f32,u32)>;
+
+pub fn simplify_line(line: &Line) -> Line {
+ if line.len() < 2 {
+ return line.to_vec();
+ }
+ let color = line[0].2;
+ let linestring : geo::LineString =
+ line.iter()
+ .map(| (x, y, _) | (*x as f64, *y as f64 ))
+ .collect();
+ let linestring = linestring.simplify(&4.0);
+ linestring.0.iter()
+ .map(| c | (c.x as f32, c.y as f32, color))
+ .collect()
+}
+
diff --git a/src/main.rs b/src/main.rs
index 068f1c7..3dbbbcc 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,5 +1,6 @@
mod gen_server;
mod ws_client;
+mod line;
use axum::{
extract::{
@@ -11,20 +12,15 @@ use axum::{
Router,
Extension
};
+use axum::extract::connect_info::ConnectInfo;
use std::{net::SocketAddr, path::PathBuf};
use tower_http::{
services::ServeDir,
trace::{DefaultMakeSpan, TraceLayer},
};
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
-use axum::extract::connect_info::ConnectInfo;
-
-use std::sync::Arc;
-use tokio::sync::{
- mpsc:: { self, Sender, Receiver },
- Mutex
-};
-use gen_server::{State,GSMsg,gen_server};
+use tokio::sync::mpsc::Sender;
+use gen_server::GSMsg;
const LISTEN_ON : &str = "0.0.0.0:3000";
@@ -38,25 +34,19 @@ async fn main() {
)
.with(tracing_subscriber::fmt::layer())
.init();
-
- let assets_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("assets");
- let (gs_tx, gs_rx) : (Sender, Receiver) = mpsc::channel(32);
-
- let state = Arc::new(Mutex::new(State { gs_tx }));
-
- tokio::spawn(gen_server(gs_rx));
+ let gs_tx = gen_server::spawn();
+
+ let assets_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("pub");
let app = Router::new()
.fallback_service(ServeDir::new(assets_dir)
.append_index_html_on_directories(true))
.route("/ws", get(ws_handler))
- .layer(Extension(state))
- .layer(
- TraceLayer::new_for_http()
- .make_span_with(DefaultMakeSpan::default()
- .include_headers(false)),
- );
+ .layer(Extension(gs_tx))
+ .layer(TraceLayer::new_for_http()
+ .make_span_with(DefaultMakeSpan::default()
+ .include_headers(false)));
let addr : SocketAddr = LISTEN_ON.parse().unwrap();
@@ -69,7 +59,7 @@ async fn main() {
async fn ws_handler(
ws: WebSocketUpgrade,
- Extension(state): Extension>>,
+ Extension(gs_tx): Extension>,
user_agent: Option>,
ConnectInfo(addr): ConnectInfo,
) -> impl IntoResponse {
@@ -78,9 +68,6 @@ async fn ws_handler(
} else {
String::from("Unknown browser")
};
- tracing::info!("`{user_agent}` at {addr} connected.");
- // finalize the upgrade process by returning upgrade callback.
- // we can customize the callback by sending additional info such as address.
- ws.on_upgrade(move |socket| ws_client::handle_socket(socket, addr, state))
+ tracing::info!("{addr} connected [{user_agent}].");
+ ws.on_upgrade(move |socket| ws_client::handle_socket(socket, addr, gs_tx))
}
-
diff --git a/src/ws_client.rs b/src/ws_client.rs
index fb663fb..93a677a 100644
--- a/src/ws_client.rs
+++ b/src/ws_client.rs
@@ -1,20 +1,14 @@
-use crate::gen_server::{State,GSMsg};
-
use axum::extract::ws::{ Message, Message::Text, Message::Close, WebSocket };
use std::net::SocketAddr;
-use std::sync::Arc;
-use tokio::sync::{
- mpsc:: { self, Sender, Receiver },
- Mutex
-};
+use tokio::sync::mpsc::{self, Sender};
use serde::{Serialize,Deserialize};
-use geo::Simplify;
-
use core::ops::ControlFlow;
-
+use crate::gen_server::GSMsg;
+use crate::line::{Line,simplify_line};
+
#[derive(Serialize, Deserialize, Debug)]
#[serde(tag = "t")]
-pub enum JMsg {
+enum JMsg {
#[serde(rename = "clear")]
Clear,
#[serde(rename = "moveTo")]
@@ -27,126 +21,118 @@ pub enum JMsg {
Line { line: Vec<(f32,f32,String)> }
}
-pub type Line = Vec<(f32,f32,u32)>;
-
pub async fn handle_socket(
mut socket: WebSocket,
who: SocketAddr,
- state: Arc>
+ gs_tx: Sender
) {
-
- let (c_tx, mut c_rx) : (Sender, Receiver) = mpsc::channel(32);
-
- {
- state.lock()
- .await
- .gs_tx.send(GSMsg::NewClient((who, c_tx)))
- .await.unwrap();
- }
+ let (c_tx, mut c_rx) = mpsc::channel(32);
+ gs_tx.send(GSMsg::NewClient((who, c_tx))).await.unwrap();
let mut line : Line = vec![];
loop {
tokio::select! {
Some(msg) = socket.recv() => {
- match process_ws_msg(&state, &who, &mut line, msg).await {
- ControlFlow::Break(()) => { return; },
+ let Ok(msg) = msg else {
+ tracing::warn!("{who}: Error receiving packet: {msg:?}");
+ continue;
+ };
+ match process_ws_msg(&gs_tx, &who, &mut line, msg).await {
+ ControlFlow::Break(()) => break,
ControlFlow::Continue(()) => {}
}
},
Some(msg) = c_rx.recv() => {
- match msg {
- GSMsg::NewLine(line) => {
- socket.send(Message::Text(line_to_json(&line)))
- .await.unwrap();
- },
- GSMsg::Clear => {
- let msg = serde_json::to_string(&JMsg::Clear).unwrap();
- socket.send(Message::Text(msg))
- .await.unwrap();
- },
- msg => {
- tracing::info!("{who} should not get this: {:?}", msg)
- }
- }
+ process_gs_msg(&mut socket, &who, msg).await
},
else => {
tracing::warn!("{who}: Connection lost unexpectedly.");
- return;
+ break;
}
}
}
}
+async fn process_gs_msg(socket: &mut WebSocket, who: &SocketAddr, msg: GSMsg) {
+ match msg {
+ GSMsg::NewLine(line) => {
+ socket.send(Message::Text(line_to_json(&line))).await.unwrap();
+ },
+ GSMsg::Clear => {
+ let msg = serde_json::to_string(&JMsg::Clear).unwrap();
+ socket.send(Message::Text(msg)).await.unwrap();
+ },
+ msg => {
+ tracing::info!("{who} should not get this: {:?}", msg);
+ }
+ }
+}
+
async fn process_ws_msg(
- state: &Arc>,
+ gs_tx: &Sender,
who: &SocketAddr,
line: &mut Line,
- msg: Result
+ msg: Message
) -> ControlFlow<(),()> {
match msg {
- Ok(Text(msg)) => {
- let Ok(msg) : Result = serde_json::from_str(&msg) else {
- tracing::warn!("{who}: Can't parse JSON: {:?}", msg);
- return ControlFlow::Continue(());
- };
- tracing::debug!("{who}: '{:?}'", msg);
- match msg {
- JMsg::Clear => {
- state.lock()
- .await
- .gs_tx.send(GSMsg::Clear)
- .await.unwrap();
- line.clear();
- },
- JMsg::MoveTo { x, y, color } => {
- *line = vec![ (x, y, parse_color(color)) ];
- },
- JMsg::LineTo { x, y, color } => {
- line.push( (x, y, parse_color(color)) );
- },
- JMsg::Stroke => {
- if line.len() > 1 {
- state.lock()
- .await
- .gs_tx.send(GSMsg::NewLine(simplify_line(line)))
- .await.unwrap();
+ Text(text) => {
+ match serde_json::from_str(&text) {
+ Ok(json) => {
+ tracing::debug!("{who}: '{:?}'", json);
+ match handle_ws_msg(line, json) {
+ Ok(Some(req)) => gs_tx.send(req).await.unwrap(),
+ Ok(None) => {},
+ Err(err) => {
+ tracing::warn!("{who}: message error: {err}");
+ }
}
- *line = vec![];
},
- JMsg::Line{..} => { panic!("recieved a line message :/"); }
+ Err(err) => {
+ tracing::warn!("{who}: can't parse JSON: {err}");
+ }
}
},
- Ok(Close(close)) => {
+ Close(close) => {
tracing::info!("{who}: closing: {:?}", close);
- state.lock()
- .await
- .gs_tx.send(GSMsg::DeleteClient(*who))
- .await.unwrap();
+ gs_tx.send(GSMsg::DeleteClient(*who)).await.unwrap();
return ControlFlow::Break(());
},
_ => {
- tracing::warn!("{who}: Can't handle message: {:?}", msg);
+ tracing::warn!("{who}: can't handle message: {:?}", msg);
}
}
ControlFlow::Continue(())
}
-fn simplify_line(line: &Line) -> Line {
- if line.len() < 2 {
- return line.to_vec();
- }
- let color = line[0].2;
- let linestring : geo::LineString =
- line.iter()
- .map(| (x, y, _) | (*x as f64, *y as f64 ))
- .collect();
- let linestring = linestring.simplify(&4.0);
- linestring.0.iter()
- .map(| c | (c.x as f32, c.y as f32, color))
- .collect()
+fn handle_ws_msg(
+ line: &mut Line,
+ msg: JMsg
+) -> Result