Files
P42_UI/app/views/system/lab/ChalkboardView.js
T
2025-08-27 07:03:09 +00:00

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);