yes
This commit is contained in:
parent
e5430032d6
commit
d544244fdf
5 changed files with 220 additions and 200 deletions
|
@ -13,17 +13,6 @@
|
|||
<body>
|
||||
<div id="loading">loading...</div>
|
||||
<canvas id="canvas"></canvas>
|
||||
<div class="bottom">
|
||||
<div class="picker">
|
||||
<button type="button" id="#F45B69"></button>
|
||||
<button type="button" id="#FE7F2D"></button>
|
||||
<button type="button" id="#FCCA46"></button>
|
||||
<button type="button" id="#87FF65"></button>
|
||||
<button type="button" id="#00A5CF"></button>
|
||||
<button type="button" id="#7D5BA6"></button>
|
||||
<button type="button" id="#312F2F"></button>
|
||||
<button type="button" id="white"></button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="bottom"><div id="picker"></div></div>
|
||||
</body>
|
||||
</html>
|
43
index.js
43
index.js
|
@ -8,12 +8,20 @@ const rooms = new Map();
|
|||
|
||||
app.use(express.static(__dirname + "/public"));
|
||||
|
||||
app.get("/", (req, res) => {
|
||||
res.sendFile(__dirname + "/views/index.html");
|
||||
app.get("/", (_, res) => {
|
||||
const consonants = "bcdfghjklmnpqrstvwxyz";
|
||||
const vowels = "aeiou";
|
||||
const rnd = (arr) => arr[Math.floor(Math.random() * arr.length)];
|
||||
let word = "";
|
||||
for (let i = 0; i < Math.random() * 2 + 2; i++) {
|
||||
word += rnd(consonants);
|
||||
word += rnd(vowels);
|
||||
}
|
||||
res.redirect("/" + word);
|
||||
});
|
||||
|
||||
app.get("/*", (req, res) => {
|
||||
res.sendFile(__dirname + "/views/board.html");
|
||||
app.get("/*", (_, res) => {
|
||||
res.sendFile(__dirname + "/board.html");
|
||||
});
|
||||
|
||||
const server = app.listen(3000);
|
||||
|
@ -24,26 +32,13 @@ io.on("connection", (socket) => {
|
|||
const { id } = socket;
|
||||
socket.on("join", (msg) => {
|
||||
socket.join(msg);
|
||||
if(rooms.has(msg)) {
|
||||
for(let ev of rooms.get(msg)) socket.emit(...ev);
|
||||
} else {
|
||||
rooms.set(msg, []);
|
||||
}
|
||||
socket.emit("sync");
|
||||
if(!rooms.has(msg)) rooms.set(msg, []);
|
||||
socket.emit("sync", rooms.get(msg));
|
||||
});
|
||||
socket.on("sync", () => {
|
||||
if(rooms.has(room())) {
|
||||
for(let ev of rooms.get(room())) socket.emit(...ev);
|
||||
}
|
||||
socket.emit("sync");
|
||||
});
|
||||
socket.on("disconnect", (msg) => emit("gc", id));
|
||||
socket.on("drawmove", (msg) => emit("drawmove", { ...msg, id }));
|
||||
socket.on("drawstart", (msg) => emit("drawstart", { ...msg, id }));
|
||||
socket.on("drawend", (msg) => emit("drawend", { ...msg, id }));
|
||||
|
||||
function emit(...args) {
|
||||
io.to(room()).emit(...args);
|
||||
if(rooms.has(room())) rooms.get(room()).push(args);
|
||||
}
|
||||
socket.on("draw", (msg) => {
|
||||
msg = { ...msg, id };
|
||||
io.to(room()).emit("draw", msg);
|
||||
if(rooms.has(room())) rooms.get(room()).push(msg);
|
||||
});
|
||||
});
|
||||
|
|
292
public/client.js
292
public/client.js
|
@ -1,29 +1,197 @@
|
|||
const $ = e => document.getElementById(e);
|
||||
const socket = io();
|
||||
|
||||
class Drawer {
|
||||
constructor() {
|
||||
this.canvas = new OffscreenCanvas(1e4, 1e4);
|
||||
this.ctx = this.canvas.getContext("2d");
|
||||
this.history = [];
|
||||
this.pens = new Map();
|
||||
|
||||
this.ctx.lineCap = "round";
|
||||
this.ctx.lineJoin = "round";
|
||||
}
|
||||
|
||||
chain(event) {
|
||||
const { pens } = this;
|
||||
if(event.type === "drawstart") {
|
||||
const path = new Path2D();
|
||||
path.moveTo(event.x, event.y);
|
||||
pens.set(event.id, { ...event, path });
|
||||
} else if(event.type === "drawmove") {
|
||||
if(!pens.has(event.id)) return;
|
||||
const pen = pens.get(event.id);
|
||||
pen.path.lineTo(event.x, event.y);
|
||||
this.ctx.strokeStyle = pen.color;
|
||||
this.ctx.lineWidth = pen.stroke;
|
||||
this.ctx.stroke(pen.path);
|
||||
} else if(event.type === "drawend") {
|
||||
if(!pens.has(event.id)) return;
|
||||
const pen = pens.get(event.id);
|
||||
pen.path.lineTo(event.x, event.y);
|
||||
this.ctx.strokeStyle = pen.color;
|
||||
this.ctx.lineWidth = pen.stroke;
|
||||
this.ctx.stroke(pen.path);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
render() {
|
||||
this.ctx.clearRect(0, 0, 1e9, 1e9);
|
||||
for(let event of this.history) this.chain(event);
|
||||
}
|
||||
|
||||
add(event) {
|
||||
this.history.push(event);
|
||||
this.chain(event);
|
||||
}
|
||||
}
|
||||
|
||||
class Renderer {
|
||||
constructor(canvas) {
|
||||
this.drawer = new Drawer();
|
||||
this.canvas = canvas;
|
||||
this.ctx = canvas.getContext("2d");
|
||||
this.pan = [0, 0];
|
||||
this.resize();
|
||||
}
|
||||
|
||||
resize() {
|
||||
this.canvas.height = window.innerHeight;
|
||||
this.canvas.width = window.innerWidth;
|
||||
this.ctx.height = window.innerHeight;
|
||||
this.ctx.width = window.innerWidth;
|
||||
this.ctx.clearRect(0, 0, 99999, 99999);
|
||||
this.render();
|
||||
}
|
||||
|
||||
add(event) {
|
||||
this.drawer.add(event);
|
||||
this.render();
|
||||
}
|
||||
|
||||
addAll(events) {
|
||||
this.drawer.history.push(...events);
|
||||
this.drawer.render();
|
||||
this.render();
|
||||
}
|
||||
|
||||
render() {
|
||||
this.ctx.clearRect(0, 0, 99999, 99999);
|
||||
this.ctx.drawImage(this.drawer.canvas, this.pan[0], this.pan[1]);
|
||||
}
|
||||
}
|
||||
|
||||
class ColorSelection {
|
||||
constructor(parent) {
|
||||
this.parent = parent;
|
||||
this.color = "black";
|
||||
this.current = null;
|
||||
this.elements = [];
|
||||
}
|
||||
|
||||
add(color) {
|
||||
const button = document.createElement("button");
|
||||
button.style.background = color;
|
||||
button.addEventListener("click", () => {
|
||||
if(this.current) this.current.classList.remove("selected");
|
||||
button.classList.add("selected");
|
||||
this.color = color;
|
||||
this.current = button;
|
||||
});
|
||||
this.elements.push(button);
|
||||
this.parent.append(button);
|
||||
}
|
||||
|
||||
addAll(colors) {
|
||||
for(let color of colors) this.add(color);
|
||||
}
|
||||
|
||||
random() {
|
||||
const el = this.elements[Math.floor(Math.random() * this.elements.length)];
|
||||
el.click();
|
||||
}
|
||||
}
|
||||
|
||||
const renderer = new Renderer($("canvas"));
|
||||
const colors = new ColorSelection($("picker"));
|
||||
colors.addAll([
|
||||
"#F45B69",
|
||||
"#FE7F2D",
|
||||
"#FCCA46",
|
||||
"#87FF65",
|
||||
"#00A5CF",
|
||||
"#7D5BA6",
|
||||
"#312F2F",
|
||||
]);
|
||||
colors.random();
|
||||
colors.add("white");
|
||||
|
||||
let pressed = false;
|
||||
function mousedown(e) {
|
||||
e = e.clientX ? e : e.touches?.[0];
|
||||
if(!e) return;
|
||||
pressed = true;
|
||||
if(e.which !== 1) return;
|
||||
const event = {
|
||||
type: "drawstart",
|
||||
x: e.clientX - renderer.pan[0],
|
||||
y: e.clientY - renderer.pan[1],
|
||||
color: colors.color,
|
||||
stroke: colors.color === "white" ? 30 : 5,
|
||||
};
|
||||
renderer.add(event);
|
||||
socket.emit("draw", event);
|
||||
}
|
||||
|
||||
function mousemove(e) {
|
||||
if(!pressed) return;
|
||||
e = e.clientX ? e : e.touches?.[0];
|
||||
if(!e) return;
|
||||
if(e.which !== 1) {
|
||||
renderer.pan[0] += e.movementX;
|
||||
renderer.pan[1] += e.movementY;
|
||||
renderer.render();
|
||||
return;
|
||||
}
|
||||
const event = {
|
||||
type: "drawmove",
|
||||
x: e.clientX - renderer.pan[0],
|
||||
y: e.clientY - renderer.pan[1],
|
||||
};
|
||||
renderer.add(event);
|
||||
socket.emit("draw", event);
|
||||
}
|
||||
|
||||
function mouseup(e) {
|
||||
e = e.clientX ? e : e.touches?.[0];
|
||||
if(!e) return;
|
||||
pressed = false;
|
||||
if(e.which !== 1) return;
|
||||
const event = {
|
||||
type: "drawend",
|
||||
x: e.clientX - renderer.pan[0],
|
||||
y: e.clientY - renderer.pan[1],
|
||||
};
|
||||
renderer.add(event);
|
||||
socket.emit("draw", event);
|
||||
}
|
||||
|
||||
socket.on("draw", (event) => {
|
||||
if(socket.id === event.id) return;
|
||||
renderer.add(event);
|
||||
});
|
||||
|
||||
socket.on("sync", (data) => {
|
||||
renderer.addAll(data);
|
||||
if($("loading")) $("loading").remove();
|
||||
});
|
||||
|
||||
socket.on("connect", () => {
|
||||
socket.emit("join", location.pathname);
|
||||
});
|
||||
|
||||
const canvas = $("canvas");
|
||||
const ctx = canvas.getContext("2d");
|
||||
const peers = new Map();
|
||||
let penColor = "black";
|
||||
|
||||
const colors = document.querySelectorAll(".picker button");
|
||||
let colorel = colors[Math.floor(Math.random() * colors.length)];
|
||||
select(colorel);
|
||||
|
||||
for (let el of colors) {
|
||||
el.addEventListener("click", () => select(el));
|
||||
el.style.background = el.id;
|
||||
}
|
||||
|
||||
function select(el) {
|
||||
penColor = el.id;
|
||||
if (colorel) colorel.classList.remove("selected");
|
||||
el.classList.add("selected");
|
||||
colorel = el;
|
||||
}
|
||||
|
||||
ctx.lineCap = "round";
|
||||
ctx.lineJoin = "round";
|
||||
|
||||
canvas.addEventListener("mousedown", mousedown);
|
||||
canvas.addEventListener("mouseup", mouseup);
|
||||
canvas.addEventListener("mousemove", mousemove);
|
||||
|
@ -31,81 +199,5 @@ canvas.addEventListener("touchstart", mousedown);
|
|||
canvas.addEventListener("touchmove", mousemove);
|
||||
canvas.addEventListener("touchend", mouseup);
|
||||
canvas.addEventListener("touchcancel", mouseup);
|
||||
window.addEventListener("resize", resize);
|
||||
document.addEventListener("contextmenu", e => e.preventDefault());
|
||||
window.addEventListener("resize", renderer.resize);
|
||||
|
||||
resize();
|
||||
|
||||
function resize() {
|
||||
canvas.height = window.innerHeight;
|
||||
canvas.width = window.innerWidth;
|
||||
ctx.height = window.innerHeight;
|
||||
ctx.width = window.innerWidth;
|
||||
ctx.clearRect(0, 0, 99999, 99999);
|
||||
socket.emit("sync");
|
||||
}
|
||||
|
||||
function mousedown(e) {
|
||||
e = e.clientX ? e : e.touches[0];
|
||||
socket.emit("drawstart", {
|
||||
x: e.clientX,
|
||||
y: e.clientY,
|
||||
color: penColor,
|
||||
stroke: penColor === "white" ? 30 : 5,
|
||||
});
|
||||
}
|
||||
|
||||
function mousemove(e) {
|
||||
e = e.clientX ? e : e.touches[0];
|
||||
socket.emit("drawmove", {
|
||||
x: e.clientX,
|
||||
y: e.clientY,
|
||||
});
|
||||
}
|
||||
|
||||
function mouseup(e) {
|
||||
e = e.clientX ? e : e.touches[0];
|
||||
socket.emit("drawend", {
|
||||
x: e.clientX,
|
||||
y: e.clientY,
|
||||
});
|
||||
}
|
||||
|
||||
socket.on("drawstart", (msg) => {
|
||||
const path = new Path2D();
|
||||
path.moveTo(msg.x, msg.y);
|
||||
peers.set(msg.id, {
|
||||
color: msg.color,
|
||||
stroke: msg.stroke,
|
||||
path,
|
||||
});
|
||||
});
|
||||
|
||||
socket.on("drawmove", (msg) => {
|
||||
if(!peers.has(msg.id)) return;
|
||||
const peer = peers.get(msg.id);
|
||||
peer.path.lineTo(msg.x, msg.y);
|
||||
ctx.strokeStyle = peer.color;
|
||||
ctx.lineWidth = peer.stroke;
|
||||
ctx.stroke(peer.path);
|
||||
});
|
||||
|
||||
socket.on("drawend", (msg) => {
|
||||
if(!peers.has(msg.id)) return;
|
||||
const peer = peers.get(msg.id);
|
||||
peer.path.lineTo(msg.x, msg.y);
|
||||
ctx.strokeStyle = peer.color;
|
||||
ctx.lineWidth = peer.stroke;
|
||||
ctx.stroke(peer.path);
|
||||
peers.delete(msg.id);
|
||||
});
|
||||
|
||||
socket.on("gc", (id) => {
|
||||
peers.delete(id);
|
||||
});
|
||||
|
||||
socket.on("sync", (id) => {
|
||||
if($("loading")) $("loading").remove();
|
||||
});
|
||||
|
||||
socket.emit("join", location.pathname);
|
||||
|
|
|
@ -8,24 +8,17 @@ body {
|
|||
font-family: "Trebuchet MS", Arial, sans-serif;
|
||||
}
|
||||
|
||||
.bottom {
|
||||
#bottom {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
left: 0;
|
||||
width: 100%;
|
||||
border-top: solid #cccccc 2px;
|
||||
background: #f5f5f5;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.picker {
|
||||
display: flex;
|
||||
justify-content: space-evenly;
|
||||
padding: 4px;
|
||||
border: solid #cccccc 2px;
|
||||
border-bottom: none;
|
||||
border-radius: 5px 5px 0 0;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.picker button {
|
||||
#picker button {
|
||||
height: 2em;
|
||||
width: 2em;
|
||||
border-radius: 50%;
|
||||
|
@ -36,19 +29,10 @@ body {
|
|||
transition: all 0.1s;
|
||||
}
|
||||
|
||||
.picker button.selected {
|
||||
#picker button.selected {
|
||||
transform: scale(0.8);
|
||||
}
|
||||
|
||||
#stroke {
|
||||
width: 100%;
|
||||
display: inline;
|
||||
}
|
||||
|
||||
#stroke-display {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
#loading {
|
||||
z-index: 9999;
|
||||
position: fixed;
|
||||
|
|
|
@ -1,40 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>bored</title>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
|
||||
<link rel="stylesheet" href="/style.css" />
|
||||
<style>
|
||||
body {
|
||||
padding: 2em;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<p>public whiteboard service</p>
|
||||
<ol>
|
||||
<li>go to any path on this domain</li>
|
||||
<li>share the same link with a friend</li>
|
||||
<li>draw stuff</li>
|
||||
<li>have a nice day</li>
|
||||
</ol>
|
||||
<script>
|
||||
location.pathname = generate();
|
||||
|
||||
function generate() {
|
||||
const consonants = "bcdfghjklmnpqrstvwxyz";
|
||||
const vowels = "aeiou";
|
||||
const rnd = (arr) => arr[Math.floor(Math.random() * arr.length)];
|
||||
let word = "";
|
||||
for (let i = 0; i < Math.random() * 2 + 2; i++) {
|
||||
word += rnd(consonants);
|
||||
word += rnd(vowels);
|
||||
}
|
||||
return word;
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
Loading…
Reference in a new issue