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