Skip to content

Instantly share code, notes, and snippets.

@shawnco
Last active June 4, 2022 01:32
Show Gist options
  • Save shawnco/61142005c23318ac7fcaab868e9443fd to your computer and use it in GitHub Desktop.
Save shawnco/61142005c23318ac7fcaab868e9443fd to your computer and use it in GitHub Desktop.
Collaborative Drawing App files as of article 6
<html>
<head>
<title>Collaborative Drawing App</title>
<style type="text/css">
canvas {
border: 1px solid black;
}
.palette {
border: 1px solid black;
display: inline-block;
margin: 2px;
height: 25px;
width: 25px;
}
</style>
</head>
<body>
<h1>Collaborative Drawing App</h1>
<div>
<canvas id="canvas" height="500px" width="500px"></canvas><br />
<b>Mode:</b> <span id="mode"></span><br />
<b>Message:</b> <span id="message"></span>
</div>
<div id="palette">
<div id="row-1">
<div class="palette"></div>
<div class="palette"></div>
<div class="palette"></div>
<div class="palette"></div>
<div class="palette"></div>
<div class="palette"></div>
<div class="palette"></div>
<div class="palette"></div>
<div class="palette"></div>
<div class="palette"></div>
</div>
<div id="row-2">
<div class="palette"></div>
<div class="palette"></div>
<div class="palette"></div>
<div class="palette"></div>
<div class="palette"></div>
<div class="palette"></div>
<div class="palette"></div>
<div class="palette"></div>
<div class="palette"></div>
<div class="palette"></div>
</div>
</div>
<div id="draw-methods">
<button onclick="canvas.setMode('Line')">Line</button>
<button onclick="canvas.setMode('Hollow Rectangle')">Hollow Rectangle</button>
<button onclick="canvas.setMode('Filled Rectangle')">Filled Rectangle</button>
<button onclick="canvas.setMode('Hollow Circle')">Hollow Circle</button>
<button onclick="canvas.setMode('Filled Circle')">Filled Circle</button>
</div>
<div>
<button onclick="canvas.clear()">Clear</button>
<button onclick="canvas.download()">Download</Button>
</div>
<script type="text/javascript" src="./script.js"></script>
<script type="text/javascript">
const room = new Room();
const socket = new Socket();
const canvas = new Canvas(socket, room);
const palette = new Palette();
palette.draw(canvas);
</script>
</body>
</html>
const express = require('express');
const bodyParser = require('body-parser');
const path = require('path');
const {WebSocketServer} = require('ws');
const PORT = 3000;
const app = express();
app.use(bodyParser.json());
app.use(express.static(path.join(__dirname + '/../client')));
app.get('/script.js', (req, res) => {
res.sendFile(path.join(__dirname + '/../client/script.js'));
});
app.get('/:name?', (req, res) => {
res.sendFile(path.join(__dirname + '/../client/index.html'));
});
app.get('/api/test', (req, res) => {
res.end('Test works!');
});
app.listen(PORT, () => {
console.log(`App is live on port ${PORT}`);
});
const wss = new WebSocketServer({ port: 8080 });
wss.on('connection', ws => {
console.log('A connection!');
ws.on('message', data => {
console.log('Data from frontend:', data.toString());
wss.clients.forEach(client => client.send(data));
});
});
class Canvas {
constructor(socket, room) {
this.canvas = document.querySelector('#canvas');
this.ctx = this.canvas.getContext('2d');
this.activeColor = '#000000';
this.startPoint = null;
this.endPoint = null;
this.pointMode = 'start';
this.mode = 'Line';
this.handleDraw = this.handleDraw.bind(this);
this.canvas.addEventListener('click', this.handleDraw);
this.shapeMessages = [
{ type: 'line', start: 'Select the starting point', end: 'Select the ending point' },
{ type: 'rectangle', start: 'Select the first corner', end: 'Select the second corner' },
{ type: 'circle', start: 'Select the middle of the circle', end: 'Select the edge of the circle' }
];
this.socket = socket;
this.room = room;
this.displayMode();
this.displayMessage();
}
setColor(color) {
this.activeColor = color;
this.ctx.strokeStyle = color;
this.ctx.fillStyle = color;
}
setMode(mode) {
this.mode = mode;
this.displayMode();
this.displayMessage();
}
handleDraw(e) {
const rect = this.canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
if (this.pointMode == 'start') {
this.startPoint = [x, y];
this.pointMode = 'end';
} else if (this.pointMode == 'end') {
this.pointMode = 'start';
this.endPoint = [x, y];
// do the drawing
if (this.mode == 'Line') {
this.drawLine(this.startPoint, this.endPoint);
} else if (this.mode == 'Hollow Rectangle') {
this.drawHollowRectangle(this.startPoint, this.endPoint);
} else if (this.mode == 'Filled Rectangle') {
this.drawFilledRectangle(this.startPoint, this.endPoint);
} else if (this.mode == 'Hollow Circle') {
this.drawHollowCircle(this.startPoint, this.endPoint);
} else if (this.mode == 'Filled Circle') {
this.drawFilledCircle(this.startPoint, this.endPoint);
}
this.socket.sendDraw(this.room, this.mode, this.activeColor, this.startPoint, this.endPoint);
this.startPoint = null;
this.endPoint = null;
}
this.displayMessage();
}
drawLine(startPoint, endPoint) {
this.ctx.beginPath();
this.ctx.moveTo(startPoint[0], startPoint[1]);
this.ctx.lineTo(endPoint[0], endPoint[1]);
this.ctx.stroke();
}
drawHollowRectangle(startPoint, endPoint) {
this.ctx.beginPath();
this.ctx.strokeRect(
startPoint[0],
startPoint[1],
endPoint[0] - startPoint[0],
endPoint[1] - startPoint[1]
);
}
drawFilledRectangle(startPoint, endPoint) {
this.ctx.beginPath();
this.ctx.fillRect(
startPoint[0],
startPoint[1],
endPoint[0] - startPoint[0],
endPoint[1] - startPoint[1]
);
}
drawHollowCircle(startPoint, endPoint) {
const x = startPoint[0] - endPoint[0];
const y = startPoint[1] - endPoint[1];
const radius = Math.sqrt(x * x + y * y);
this.ctx.beginPath();
this.ctx.arc(startPoint[0], startPoint[1], radius, 0, 2 * Math.PI, false);
this.ctx.stroke();
}
drawFilledCircle(startPoint, endPoint) {
const x = startPoint[0] - endPoint[0];
const y = startPoint[1] - endPoint[1];
const radius = Math.sqrt(x * x + y * y);
this.ctx.beginPath();
this.ctx.arc(startPoint[0], startPoint[1], radius, 0, 2 * Math.PI, false);
this.ctx.fill();
}
displayMode() {
const modeDiv = document.querySelector('#mode');
modeDiv.innerHTML = this.mode;
}
getModeType(mode) {
let type = '';
switch (mode) {
case 'Line':
type = 'line';
break;
case 'Hollow Rectangle':
case 'Filled Rectangle':
type = 'rectangle';
break;
case 'Hollow Circle':
case 'Filled Circle':
type = 'circle';
break;
}
return type;
}
displayMessage() {
const type = this.getModeType(this.mode);
const find = this.shapeMessages.find(m => m.type === type);
const msgDiv = document.querySelector('#message');
msgDiv.innerHTML = find[this.pointMode];
}
clear() {
this.ctx.clearRect(0, 0, 500, 500);
}
download() {
const image = this.canvas.toDataURL('image/png', 1.0);
image.replace('image/png', 'image/octet-stream');
const link = document.createElement('a');
link.download = 'canvas-image.png';
link.href = image;
link.click();
}
}
class Palette {
constructor() {
this.colors = [
['#000000', '#FFFFFF', '#7F7F7F', '#C3C3C3', '#880015', '#B97A57', '#ED1C24', '#FFAEC9', '#FF7F27', '#FFC90E'],
['#FFF200', '#EFE4B0', '#22B14C', '#B5E61D', '#00A2E8', '#99D9EA', '#3F48CC', '#7092BE', '#A349A4', '#C8BFE7']
];
}
draw(canvas) {
const row1 = document.querySelectorAll('#row-1 .palette');
const row2 = document.querySelectorAll('#row-2 .palette');
row1.forEach((div, idx) => {
div.style.backgroundColor = this.colors[0][idx];
div.onclick = e => canvas.setColor(this.colors[0][idx]);
});
row2.forEach((div, idx) => {
div.style.backgroundColor = this.colors[1][idx];
div.onclick = e => canvas.setColor(this.colors[1][idx]);
});
}
}
class Socket {
constructor() {
this.socket = new WebSocket('ws://localhost:8080');
this.socket.onmessage = msg => this.handleMessage(msg);
}
sendDraw(room, mode, color, startPoint, endPoint) {
this.socket.send(JSON.stringify({
type: 'draw',
roomId: room.id,
roomName: room.room,
mode,
color,
startPoint,
endPoint
}));
}
async handleMessage(msg) {
console.log(await msg.data.text());
}
}
class Room {
constructor() {
this.getRoom();
this.id = Math.round(Math.random() * 10000).toString();
}
getRoom() {
const {pathname} = location;
if (pathname !== '/') {
this.room = pathname.split('/')[1];
} else {
this.room = Math.round(Math.random() * 10000).toString();
history.pushState({}, '', '/' + this.room);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment