Created
June 4, 2022 01:07
-
-
Save shawnco/ee1e9ec3457926d99c3c7e75a897fde4 to your computer and use it in GitHub Desktop.
Collaborative Drawing App files as of article 8
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); | |
}); | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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