Skip to content

Instantly share code, notes, and snippets.

@shawnco
Created June 4, 2022 01:07
Show Gist options
  • Save shawnco/ee1e9ec3457926d99c3c7e75a897fde4 to your computer and use it in GitHub Desktop.
Save shawnco/ee1e9ec3457926d99c3c7e75a897fde4 to your computer and use it in GitHub Desktop.
Collaborative Drawing App files as of article 8
<html>
<head>
<title>Basic 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>Basic 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.save()">Save</button>
<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 session = new Session();
const socket = new Socket(session);
const canvas = new Canvas(socket, session);
canvas.getImage();
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 {Sequelize} = require('sequelize');
const db = new Sequelize({
dialect: 'sqlite',
storage: __dirname + '/collab.sqlite'
});
const Room = db.define('room', {
id: {
type: Sequelize.INTEGER,
primaryKey: true,
autoIncrement: true
},
name: Sequelize.STRING,
image: Sequelize.STRING
}, {
freezeTableName: true,
timestamps: false
});
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.get('/api/room/:name', async (req, res) => {
const {name} = req.params;
const result = await Room.findOne({where: {name}});
res.send({result});
});
app.post('/api/room/:name', async (req, res) => {
const {name} = req.params;
const {image} = req.body;
const result = await Room.findOne({where: {name}});
if (result) {
// update
result.image = image;
await result.save();
res.send({result});
} else {
// insert
const newImage = await Room.create({name, image});
res.send({result: newImage});
}
});
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();
}
drawMessage(msg) {
const {mode, color, startPoint, endPoint} = msg;
this.ctx.strokeStyle = color;
this.ctx.fillStyle = color;
if (mode === 'Line') {
this.drawLine(startPoint, endPoint);
} else if (mode === 'Hollow Rectangle') {
this.drawHollowRectangle(startPoint, endPoint);
} else if (mode === 'Filled Rectangle') {
this.drawFilledRectangle(startPoint, endPoint);
} else if (mode === 'Hollow Circle') {
this.drawHollowCircle(startPoint, endPoint);
} else if (mode === 'Filled Circle') {
this.drawFilledCircle(startPoint, endPoint);
}
this.ctx.strokeStyle = this.activeColor;
this.ctx.fillStyle = this.activeColor;
}
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);
this.socket.sendClear();
}
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();
}
clearMessage(msg) {
this.ctx.clearRect(0, 0, 500, 500);
}
async getImage() {
const result = await fetch(`http://localhost:3000/api/room/${this.room.room}`);
const image = await result.json();
if (image.result) {
const img = new Image();
img.onload = e => {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.ctx.drawImage(img, 0, 0, img.width, img.height);
}
img.setAttribute('src', image.result.image);
}
}
async save() {
const image = this.canvas.toDataURL();
await fetch(`http://localhost:3000/api/room/${this.room.room}`, {
method: 'POST',
body: JSON.stringify({image}),
headers: {
'Content-Type': 'application/json'
}
});
}
}
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(room) {
this.socket = new WebSocket('ws://localhost:8080');
this.socket.onmessage = msg => this.handleMessage(msg);
this.room = room;
}
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) {
const msgDecoded = JSON.parse(await msg.data.text());
if (msgDecoded.roomName == this.room.room && msgDecoded.roomId !== this.room.id) {
if (msgDecoded.type === 'draw') {
this.canvas.drawMessage(msgDecoded);
} else if (msgDecoded.type === 'clear') {
this.canvas.clearMessage(msgDecoded);
}
}
}
setCanvas(canvas) {
this.canvas = canvas;
}
sendClear() {
this.socket.send(JSON.stringify({
type: 'clear',
roomId: this.room.id,
roomName: this.room.room
}));
}
}
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