// vim: set ts=2 sw=2 et tw=80: class App { static get BRUSHES() { return { "PenBrush": new PenBrush(), "DiscBrush": new DiscBrush(), "StarBrush": new StarBrush(), }; } constructor(conf) { if (!(typeof conf === 'object' && conf)) { throw new Error('Argument conf different from specification'); } this.canvas = document.getElementById(conf.canvas); if (!this.canvas || this.canvas.tagName !== 'CANVAS') { throw new Error(`canvas is not a canvas`); } this.ctx = this.canvas.getContext('2d'); this.favourites = document.querySelector('div#favourites'); if (typeof conf.buttons === 'object' && conf.buttons) { this.buttons = {} for (const b of ['clear', 'undo', 'camera']) this.buttons[b] = document.getElementById(conf.buttons[b]); if (this.buttons.clear) { this.buttons.clear.addEventListener('click', () => { this.erase(); this.background = null; history.clear(); }); } if (this.buttons.undo) { this.buttons.undo.addEventListener('click', () => { history.pop(); this.redrawAll(); }); } if (this.buttons.camera) { this.buttons.camera.addEventListener('click', () => { const base64 = this.canvas.toDataURL(); const img = document.createElement('img'); img.src = base64; const span = document.createElement('span'); span.contentEditable = true; span.innerText = new Date().toString(); this.favourites.appendChild(img); this.favourites.appendChild(span); }); } const player = document.createElement('video'); player.addEventListener('loadeddata', () => { player.play(); setTimeout(() => { const imgCanvas = document.createElement('canvas'); imgCanvas.width = this.canvas.width; imgCanvas.height = this.canvas.height; const imgCtx = imgCanvas.getContext('2d'); imgCtx.drawImage(player, 0, 0, canvas.width, canvas.height); const imgData = imgCtx.getImageData(0, 0, imgCanvas.width, imgCanvas.height); for (let i = 0; i < data.length; i += 4) { const pixel = (data[i] + 2 * data[i+1] + data[i+2]) / 4; data[i] = data[i+1] = data[i+2] = pixel; } imgCtx.putImageData(imgData, 0, 0); const img = document.createElement('img'); img.src = imgCanvas.toDataURL(); img.addEventListener('load', () => { this.background = img; this.redrawAll(); }); player.srcObject.getVideoTracks().forEach(track => track.stop()); }, 100); }); const button = document.createElement('button'); button.type = 'button'; button.innerHTML = 'Photo'; button.addEventListener('click', () => { navigator.mediaDevices.getUserMedia({ video: true }) .then((stream) => { player.srcObject = stream; }); }); document.getElementById('left-toolbar').appendChild(button); } this.ctx.lineWidth = 1; this.strokeStyle = this.constructor.defaultStrokeStyle; this.brush = "PenBrush"; const brushToolbar = document.querySelector('#brush-toolbar'); if (brushToolbar) { for (const name in App.BRUSHES) { const b = document.createElement('button'); b.innerText = name; b.addEventListener('click', () => this.brush = name); brushToolbar.appendChild(b); } const label = document.createElement('label'); label.setAttribute('for', 'color'); const color = document.createElement('input'); color.type = 'color'; color.name = 'color'; color.value = this.constructor.defaultStrokeStyle; color.addEventListener('change', () => this.strokeStyle = color.value); brushToolbar.appendChild(label); brushToolbar.appendChild(color); } const toMouse = (e, func) => { if (e && e.touches && e.touches[0]) { return func.bind(this)({ offsetX: e.touches[0].pageX - this.canvas.offsetLeft, offsetY: e.touches[0].pageY - this.canvas.offsetTop }); } }; this.canvas.addEventListener('touchstart', e => toMouse(e, this.startPath)); this.canvas.addEventListener('mousedown', this.startPath.bind(this)); this.canvas.addEventListener('touchmove', e => toMouse(e, this.draw)); this.canvas.addEventListener('mousemove', this.draw.bind(this)); this.canvas.addEventListener('touchcancel', e => toMouse(e, this.endPath)); this.canvas.addEventListener('mouseup', this.endPath.bind(this)); this.canvas.addEventListener('mouseout', this.endPath.bind(this)); } static get defaultStrokeStyle() { return 'black'; } get strokeStyle() { if (this.ctx.strokeStyle == '#000000') { return 'black'; } return this.ctx.strokeStyle; } set strokeStyle(style) { if (typeof style !== 'string') { throw new Error('style is not a string'); } this.ctx.strokeStyle = style; } get brush() { return this._brush.name; } set brush(brushName) { this._brush = App.BRUSHES[brushName]; } erase() { this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); } startPath(e, record = true) { this.ctx.beginPath(); this.ctx.moveTo(e.offsetX, e.offsetY); if (record) { history.initializeNewPath(); history.push(new Stroke(this.brush, this.strokeStyle, e.offsetX, e.offsetY)); } if (e instanceof MouseEvent) { this.mousedown = true; } } draw(e, beginNew = true, record = true) { if (this.mousedown || !(e instanceof MouseEvent)) { this._brush.draw(this.ctx, this.strokeStyle, e.offsetX, e.offsetY); if (record) { history.push(new Stroke(this.brush, this.strokeStyle, e.offsetX, e.offsetY)); } if (beginNew) { this.ctx.beginPath(); this.ctx.moveTo(e.offsetX, e.offsetY); } else if (e instanceof MouseEvent) { this.mousedown = false; } } } endPath(e, record = true) { this.draw(e, false, record); } drawPath(path) { const last = path.length - 1; const lastBrush = this.brush; const lastStyle = this.strokeStyle; for (let i = 0; i <= last; i++) { this.brush = path[i].brushName; this.strokeStyle = path[i].strokeStyle; switch(i) { case 0: this.startPath(path[i], false); break; case last: this.endPath(path[i], false); break; default: this.draw(path[i], true, false); } } this.brush = lastBrush; this.strokeStyle = lastStyle; } redrawAll() { this.erase(); if (this.background) { this.ctx.drawImage(this.background, 0, 0, this.canvas.width, this.canvas.height); } for (const path of history.paths) { this.drawPath(path); } } }