import Events from '../events.js' import { Angle } from '../utils.js' class StylusCommand extends Object { constructor() { super() } do(stylus) { stylus.commandStack.push(this) } undo(stylus) { stylus.undoCommandStack.push(this) } redo(stylus) { this.do(stylus) } } class StrokeCommand extends StylusCommand { constructor(stroke) { super() this.stroke = stroke } do(stylus) { if (this.stroke.length > 0) { super.do(stylus) stylus.stroke = [] stylus.strokes.push(this.stroke) stylus.redraw() stylus.changed() } } undo(stylus) { if (this.stroke.length > 0) { super.undo(stylus) stylus.strokes.pop() stylus.redraw() stylus.changed() } } } class ClearCommand extends StylusCommand { do(stylus) { // Clears the command stack stylus.commandStack = [] super.do(stylus) this.strokes = stylus.strokes stylus.stroke = [] stylus.strokes = [] stylus.redraw() stylus.changed() } undo(stylus) { //super.undo(stylus) // Clear all is not redoable stylus.stroke = [] stylus.strokes = this.strokes stylus.redraw() stylus.changed() } } export default class Stylus extends PIXI.Graphics { constructor({ width = window.innerWidth, height = window.innerHeight, interactive = true, color = 0x000000, tiltX = 0, tiltY = 0, backgroundAlpha = 1, backgroundFill = 0xFFFFFF, colorAlpha = 1, captureEvents = true, acceptMouseEvents = true } = {}) { super() this.wantedWidth = width this.wantedHeight = height this.backgroundAlpha = backgroundAlpha this.backgroundFill = backgroundFill this.colorAlpha = colorAlpha this.color = color this.interactive = interactive this.debug = false this.tiltX = tiltX // degrees -90 ... 90 this.tiltY = tiltY // degrees -90 ... 90 this.captureEvents = captureEvents this.commandStack = [] this.undoCommandStack = [] this.strokes = [] this.stroke = [] if (captureEvents) this.registerEventHandler(acceptMouseEvents) this.drawBackground() } drawBackground() { this.clear() this.beginFill(this.backgroundFill, this.backgroundAlpha) this.drawRect(0, 0, this.wantedWidth, this.wantedHeight) this.endFill() } touchToPoint(t) { return { x: t.clientX, y: t.clientY } } isStylusPointer(event) { let identifier = event.data.identifier if (typeof (event.data.originalEvent.changedTouches) !== 'undefined') { for (let touch of event.data.originalEvent.changedTouches) { if (touch.identifier === identifier && touch.touchType === 'stylus') { this.tiltX = Angle.radian2degree(touch.azimuthAngle) this.tiltY = 90.0 - Angle.radian2degree(touch.altitudeAngle) return true } } } // UO: Not tested since the Sprot delivered "mouse" events to Chrome if (event.data.originalEvent.pointerType === 'pen') { this.tiltX = event.data.originalEvent.tiltX this.tiltY = event.data.originalEvent.tiltY return true } return false } isStylusTouch(event) { let identifier = event.data.identifier if (typeof (event.data.originalEvent.changedTouches) !== 'undefined') { for (let touch of event.data.originalEvent.changedTouches) { if (touch.identifier === identifier && touch.pointerType === 'touch') { return true } } } return false } getPointerID(event) { let identifier = event.data.identifier for (let touch of event.data.originalEvent.changedTouches) { if (touch.identifier === identifier) { return touch.pointerId } } } registerEventHandler() { window.addEventListener('keydown', (e) => { switch (e.keyCode) { case 38: // up arrow this.tiltX += 5 break case 40: // down arrow this.tiltX -= 5 break case 37: // left arrow this.tiltY -= 5 break case 39: // right arrow this.tiltY += 5 break } if (this.debug) console.log("keydown", e.keyCode, this.tiltX, this.tiltY) }) this.on('pointerdown', (e) => { if (this.debug) console.log("pointerdown", e) if (this.eventInside(e)) this.startStroke(this.toStroke(e)) }) this.on('pointermove', (e) => { if (Events.isPointerDown(e.data.originalEvent) || this.isStylusPointer(e) || this.isStylusTouch(e)) { if (this.debug) console.log("pointermove", e, this.eventInside(e)) if (this.eventInside(e)) this.moveStroke(this.toStroke(e)) } }) this.on('pointerup', (e) => { if (this.debug) console.log("pointerup", e) if (this.eventInside(e)) this.endStroke(this.toStroke(e)) }) this.on('pointerleave', (e) => { this.endStroke(this.toStroke(e)) }) this.on('pointercancel', (e) => { this.endStroke(this.toStroke(e)) }) } undoable() { return this.commandStack.length > 0 } redoable() { return this.undoCommandStack.length > 0 } undo() { if (this.undoable()) { let cmd = this.commandStack.pop() cmd.undo(this) } } redo() { if (this.redoable()) { let cmd = this.undoCommandStack.pop() cmd.redo(this) } } eventInside(event) { let local = this.toLocal(event.data.global) for (let child of this.children) { let r = child.getBounds() if (r.contains(local.x, local.y)) { console.log("Child touched") return false } } if (local.x < 0 || local.x > this.wantedWidth) return false if (local.y < 0 || local.y > this.wantedHeight) return false event.stopPropagation() if (this.debug) console.log("stopPropagation", event) if (event.data.originalEvent.claimedByScatter) { return false } return true } toLocalPoint(event) { return this.toLocal(event.data.global) } toStroke(event) { let local = this.toLocalPoint(event) let x = Math.max(0, Math.min(local.x, this.wantedWidth)) let y = Math.max(0, Math.min(local.y, this.wantedHeight)) let desc = { x, y, pressure: event.pressure || null, tiltX: this.tiltX, tiltY: this.tiltY, color: this.color } return desc } startStroke(info) { this.stroke = [info] this.redraw() } moveStroke(info) { this.stroke.push(info) this.redraw() } endStroke(info) { if (this.stroke.length > 1) { let cmd = new StrokeCommand(this.stroke) cmd.do(this) } } tiltToLineWidth(value) { return Math.round(Math.abs(value / 10) + 1) } drawStroke(stroke) { if (stroke.length) { let start = stroke[0] this.beginFill(0, 0) this.moveTo(start.x, start.y) for (let i = 1; i < stroke.length; i++) { let info = stroke[i] this.lineStyle(this.tiltToLineWidth(info.tiltY), info.color, this.colorAlpha) this.lineTo(info.x, info.y) } this.endFill() } } drawTouch(point) { this.beginFill(0, 0) this.drawCircle(point.x, point.y, 22) this.endFill() } drawStrokes() { this.drawBackground() this.lineStyle(1.0, 0xFF0000, 1) for (let stroke of this.iterStrokes()) { this.drawStroke(stroke) } } redraw() { this.drawStrokes() } // Can be overwritten if different levels of strokes are necessary *iterStrokes() { for (let stroke of this.strokes) { yield stroke } yield this.stroke } changed() { // Can be overwritten } clearAll() { let cmd = new ClearCommand() cmd.do(this) } normalizeInfo(info) { let { x, y, pressure, tiltX, tiltY, color } = info x /= this.wantedWidth y /= this.wantedHeight return { x, y, pressure, tiltX, tiltY, color } } denormalizeInfo(info) { let { x, y, pressure, tiltX, tiltY, color } = info x = x * this.wantedWidth y = y * this.wantedHeight return { x, y, pressure, tiltX, tiltY, color } } // Convert strokes into an object that can be stored in an Indexed DB. // Returns normalized strokes toObject() { let result = [] for (let stroke of this.strokes) { let normalized = [] for (let info of stroke) { normalized.push(this.normalizeInfo(info)) } result.push(normalized) } return result } // Read normalized strokes from an object from an Indexed DB. fromObject(normalizedStrokes) { this.strokes = [] for (let stroke of normalizedStrokes) { let denormalized = [] for (let info of stroke) { denormalized.push(this.denormalizeInfo(info)) } this.strokes.push(denormalized) } } // Convert strokes into a JSON object that can be stored in an Indexed DB toJSON() { return JSON.stringify(this.toObject()) } // Convert strokes from a JSON fromJSON(json) { this.fromObject(JSON.parse(json)) } // Returns a set of used colors usedColors() { let used = new Set() for (let info of this.stroke) { used.add(info.color) } for (let stroke of this.strokes) { for (let info of stroke) { used.add(info.color) } } return used.values() } }