318 lines
10 KiB
JavaScript
318 lines
10 KiB
JavaScript
/**
|
|
* Magic Chalkboard demo app
|
|
*
|
|
* A simple app to share an editable canvas between bus users.
|
|
* Demonstrates :
|
|
* - subscription to a custom channel
|
|
* - burst event send/receive of custom event types
|
|
* - lightspeed response time of events broadcasting
|
|
* - accuracy and reliability of event queuing
|
|
* - implementation of app design with bus connected/disconnected.
|
|
*
|
|
* @author Michael Fallise
|
|
* @version 1.0
|
|
*/
|
|
class ChalkboardView extends EICDomContent {
|
|
|
|
options = {
|
|
background: 'rgba(0,0,0,1)',
|
|
busChannel: 'collaborative:chalkboard'
|
|
}
|
|
|
|
DOMContentLoaded() {
|
|
|
|
let components = ui.eicfy(this.el);
|
|
|
|
this.useBus = false;
|
|
this.busUsers = [];
|
|
|
|
this.toolbox = new Form(this.find('.toolbox'));
|
|
|
|
let colors = this.findAll('.color-sample');
|
|
for(let color of colors) color.addEventListener('click', this.onColorChange.bind(this));
|
|
|
|
let pens = this.findAll('.pen-sample');
|
|
for(let pen of pens) pen.addEventListener('click', this.onPenChange.bind(this));
|
|
|
|
let dl = components.find(item => item.el.classList.contains('dl-board'));
|
|
dl.addEventListener('click', this.onBoardDownload.bind(this));
|
|
|
|
let clear = components.find(item => item.el.classList.contains('clear-board'));
|
|
clear.addEventListener('click', this.onBoardClear.bind(this));
|
|
|
|
this.board = this.find('canvas');
|
|
this.board.style.background = this.options.background;
|
|
this.ctx = this.board.getContext("2d");
|
|
this.ctx.lineCap = 'round';
|
|
|
|
this.drawing = this.eraser = false;
|
|
this.mouseCurrent = { x: 0, y: 0 };
|
|
this.mousePrev = { x: 0, y: 0 };
|
|
|
|
// referencing computed event handlers
|
|
this.mouseDownHandler = this.onChalkStart.bind(this);
|
|
this.mouseMoveHandler = this.onChalkMove.bind(this);
|
|
this.mouseUpHandler = this.onChalkEnd.bind(this);
|
|
this.busDrawHandler = this.onBusDraw.bind(this);
|
|
this.busClearHandler = this.onBusClear.bind(this);
|
|
this.busPingHandler = this.onBusPing.bind(this);
|
|
this.busJoinHandler = this.onBusJoin.bind(this);
|
|
this.busLeaveHandler = this.onBusLeave.bind(this);
|
|
|
|
this.setBrush();
|
|
this.setBoardSize();
|
|
this.clear();
|
|
this.initListeners();
|
|
}
|
|
|
|
DOMContentRemoved() { this.closeListeners(); }
|
|
|
|
DOMContentResized() {
|
|
let data = this.board.toDataURL('image/png');
|
|
this.setBoardSize();
|
|
let img = new Image;
|
|
let self = this
|
|
img.onload = function() { self.ctx.drawImage(img, 0, 0); };
|
|
img.src = data;
|
|
|
|
}
|
|
|
|
setBoardSize() {
|
|
let rect = this.board.getBoundingClientRect()
|
|
this.board.width = rect.width;
|
|
this.board.height = rect.height;
|
|
}
|
|
|
|
/* Nike to Mike : As planned, made higher-level addBusListener & removeBusListener
|
|
* Changes : Includes channel filtering (important to avoid chan injections by script-kiddies)
|
|
*
|
|
* You can replace old-style :
|
|
* app.events.addEvent('MessageBus.XXX', handler, scope)
|
|
* app.events.removeEvent('MessageBus.XXX', handler, scope)
|
|
* => handler receives pure JS event, and probably uses e.detail.payload
|
|
*
|
|
* by new-style:
|
|
* app.MessageBus.addBusListener('XXX', [this.busChannel], handler, scope)
|
|
* app.MessageBus.removeBusListener('XXX', handler, scope)
|
|
* => handler receives (realChan, payload, sender)
|
|
*
|
|
*/
|
|
|
|
|
|
initListeners() {
|
|
this.board.addEventListener('mousedown', this.mouseDownHandler);
|
|
this.board.addEventListener('mousemove', this.mouseMoveHandler);
|
|
this.board.addEventListener('mouseup', this.mouseUpHandler);
|
|
// disabling context menu on canvas in order to use right button for eraser
|
|
this.board.oncontextmenu = function() { return false;}
|
|
|
|
if(app.MessageBus.connected) {
|
|
console.log("USING BUS")
|
|
app.MessageBus.subscribe([this.options.busChannel])
|
|
.then( payload => {
|
|
app.MessageBus.addBusListener('board_draw', ['collaborative:chalkboard'], this.busDrawHandler, 'chalkboard');
|
|
app.MessageBus.addBusListener('board_clear', ['collaborative:chalkboard'], this.busClearHandler, 'chalkboard');
|
|
app.MessageBus.addBusListener('board_ping', ['collaborative:chalkboard'], this.busPingHandler, 'chalkboard');
|
|
app.MessageBus.addBusListener('board_join', ['collaborative:chalkboard'], this.busJoinHandler, 'chalkboard');
|
|
app.MessageBus.addBusListener('board_leave', ['collaborative:chalkboard'], this.busLeaveHandler, 'chalkboard');
|
|
this.useBus = true;
|
|
this.pingUsers();
|
|
} );
|
|
}
|
|
}
|
|
|
|
closeListeners() {
|
|
if(this.useBus) {
|
|
app.MessageBus.removeBusListener('board_draw', this.busDrawHandler, 'chalkboard');
|
|
app.MessageBus.removeBusListener('board_clear', this.busClearHandler, 'chalkboard');
|
|
app.MessageBus.removeBusListener('board_join', this.busJoinHandler, 'chalkboard');
|
|
app.MessageBus.removeBusListener('board_ping', this.busPingHandler, 'chalkboard');
|
|
app.MessageBus.removeBusListener('board_leave', this.busLeaveHandler, 'chalkboard');
|
|
this.leaveApp();
|
|
this.useBus = false;
|
|
}
|
|
}
|
|
|
|
setBrush() { this.brush = this.toolbox.value; }
|
|
|
|
draw(brush, from, to) {
|
|
this.ctx.fillStyle = brush.color;
|
|
this.ctx.strokeStyle = brush.color;
|
|
this.ctx.lineWidth = brush.width;
|
|
this.ctx.beginPath();
|
|
this.ctx.moveTo(from.x, from.y);
|
|
this.ctx.lineTo(to.x, to.y);
|
|
this.ctx.stroke();
|
|
}
|
|
|
|
clear() {
|
|
this.ctx.fillStyle = this.options.background;
|
|
this.ctx.fillRect(0, 0, this.board.width, this.board.height);
|
|
this.ctx.fillStyle = this.brush.color;
|
|
}
|
|
|
|
onChalkStart(event) {
|
|
this.drawing = true;
|
|
this.eraser = event.button == 2,
|
|
|
|
this.mousePrev.x = this.mouseCurrent.x;
|
|
this.mousePrev.y = this.mouseCurrent.y;
|
|
|
|
this.setBrush();
|
|
}
|
|
|
|
onChalkMove(event) {
|
|
|
|
let rect = event.target.getBoundingClientRect();
|
|
|
|
this.mouseCurrent.x = event.pageX - rect.left;
|
|
this.mouseCurrent.y = event.pageY - rect.top;
|
|
|
|
if(this.drawing) {
|
|
|
|
this.board.classList.add('drawing');
|
|
|
|
let brush = this.eraser ? { color: this.options.background, width: 20 }: this.brush;
|
|
|
|
if(this.useBus) {
|
|
app.MessageBus.sendEvent(this.options.busChannel,'board_draw', {
|
|
uid: app.User.identity.uuid,
|
|
brush: brush,
|
|
from: this.mousePrev,
|
|
to: this.mouseCurrent,
|
|
});
|
|
} else {
|
|
this.draw(brush, this.mousePrev, this.mouseCurrent);
|
|
}
|
|
|
|
this.mousePrev.x = this.mouseCurrent.x;
|
|
this.mousePrev.y = this.mouseCurrent.y;
|
|
}
|
|
}
|
|
|
|
onChalkEnd(event) {
|
|
this.drawing = this.eraser = false;
|
|
this.board.classList.remove('drawing');
|
|
}
|
|
|
|
onColorChange(event) {
|
|
event.stopPropagation();
|
|
event.preventDefault();
|
|
|
|
this.find('.color-sample.selected').classList.remove('selected');
|
|
|
|
let color = event.currentTarget;
|
|
color.classList.add('selected');
|
|
this.find('input[name="color"]').value = color.dataset.value;
|
|
}
|
|
|
|
onPenChange(event) {
|
|
event.stopPropagation();
|
|
event.preventDefault();
|
|
|
|
this.find('.pen-sample.selected').classList.remove('selected');
|
|
|
|
let pen = event.currentTarget;
|
|
pen.classList.add('selected');
|
|
this.find('input[name="width"]').value = pen.dataset.value;
|
|
}
|
|
|
|
onBusPing(event) {
|
|
let payload = {
|
|
uid: app.User.identity.uuid,
|
|
name: app.User.identity.firstname + ' ' + app.User.identity.lastname
|
|
}
|
|
|
|
app.MessageBus.sendEvent(this.options.busChannel,'board_join', payload);
|
|
}
|
|
|
|
joinApp() {
|
|
let payload = {
|
|
uid: app.User.identity.uuid,
|
|
name: app.User.identity.firstname + ' ' + app.User.identity.lastname
|
|
}
|
|
|
|
app.MessageBus.sendEvent(this.options.busChannel,'board_join', payload);
|
|
}
|
|
|
|
leaveApp() { app.MessageBus.sendEvent(this.options.busChannel,'board_leave'); }
|
|
|
|
pingUsers() {
|
|
this.busUsers = [];
|
|
|
|
let container = this.find('.users');
|
|
container.innerHTML = '';
|
|
|
|
app.MessageBus.sendEvent(this.options.busChannel,'board_ping');
|
|
}
|
|
|
|
refreshUsers() {
|
|
let container = this.find('.users');
|
|
console.log('', this.busUsers)
|
|
for(let user of this.busUsers) {
|
|
if(!user.tag) {
|
|
let tag = new Chip(null, {
|
|
label: user.name,
|
|
icon: 'icon-edit',
|
|
size: 'small',
|
|
severity: 'secondary'
|
|
});
|
|
container.append(tag.el)
|
|
user.tag = tag;
|
|
}
|
|
}
|
|
}
|
|
|
|
onBusDraw(chan, payload, sender) {
|
|
|
|
/*
|
|
for(let item of this.busUsers) {
|
|
item.tag.severity = 'secondary';
|
|
}
|
|
|
|
let user = this.busUsers.find(item => item.uid == payload.uid);
|
|
user.tag.severity = 'primary';
|
|
*/
|
|
this.draw(payload.brush, payload.from, payload.to);
|
|
}
|
|
|
|
onBusClear(chan, payload, sender) { this.clear(); }
|
|
|
|
onBusJoin(chan, payload, sender) {
|
|
let user = payload;
|
|
let index = this.busUsers.findIndex(item => item.uid == user.uid);
|
|
console.log('index', index, this.busUsers)
|
|
console.log('user', user)
|
|
if(index == -1) {
|
|
this.busUsers.push(user);
|
|
this.refreshUsers();
|
|
}
|
|
}
|
|
|
|
onBusLeave(chan, payload, sender) { this.pingUsers(); }
|
|
|
|
onBoardDownload(event) {
|
|
event.stopPropagation();
|
|
event.preventDefault();
|
|
|
|
let data = this.board.toDataURL('image/png');
|
|
|
|
let link = document.createElement('a');
|
|
link.download = 'chalkboard.png';
|
|
link.href = data;
|
|
link.click();
|
|
}
|
|
|
|
onBoardClear(event) {
|
|
event.stopPropagation();
|
|
event.preventDefault();
|
|
|
|
if(this.useBus) {
|
|
app.MessageBus.sendEvent(this.options.busChannel,'board_clear', {});
|
|
} else {
|
|
this.clear();
|
|
}
|
|
}
|
|
}
|
|
|
|
app.registerClass('ChalkboardView',ChalkboardView); |