426 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			JavaScript
		
	
	
		
			Executable File
		
	
	
	
	
			
		
		
	
	
			426 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			JavaScript
		
	
	
		
			Executable File
		
	
	
	
	
/* eslint-disable no-undef */
 | 
						|
/* eslint-disable no-console */
 | 
						|
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,
 | 
						|
        lineWidth = 16,
 | 
						|
        minStrokeLength = 4,
 | 
						|
    } = {}) {
 | 
						|
        super()
 | 
						|
        this.activePointers = 0
 | 
						|
        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 = []
 | 
						|
        this.lineWidth = lineWidth
 | 
						|
        this.minStrokeLength = minStrokeLength
 | 
						|
        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
 | 
						|
            }
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    singlePointer() {
 | 
						|
        return this.activePointers == 1
 | 
						|
    }
 | 
						|
 | 
						|
    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.activePointers += 1
 | 
						|
                if (this.singlePointer()) {
 | 
						|
                    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.singlePointer()) this.moveStroke(this.toStroke(e))
 | 
						|
            }
 | 
						|
        })
 | 
						|
        this.on('pointerup', (e) => {
 | 
						|
            if (this.eventInside(e)) {
 | 
						|
                if (this.activePointers > 0) {
 | 
						|
                    this.activePointers -= 1
 | 
						|
                    this.endStroke(this.toStroke(e))
 | 
						|
                }
 | 
						|
            }
 | 
						|
            if (this.debug) console.log('pointerup', this.activePointers)
 | 
						|
        })
 | 
						|
        this.on('pointerleave', (e) => {
 | 
						|
            if (this.activePointers > 0) {
 | 
						|
                this.activePointers -= 1
 | 
						|
            }
 | 
						|
            this.endStroke(this.toStroke(e))
 | 
						|
        })
 | 
						|
        this.on('pointercancel', (e) => {
 | 
						|
            if (this.activePointers > 0) {
 | 
						|
                this.activePointers -= 1
 | 
						|
            }
 | 
						|
            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,
 | 
						|
            lineWidth: this.tiltToLineWidth(this.tiltY),
 | 
						|
        }
 | 
						|
        return desc
 | 
						|
    }
 | 
						|
 | 
						|
    startStroke(info) {
 | 
						|
        this.stroke = [info]
 | 
						|
        this.redraw()
 | 
						|
    }
 | 
						|
 | 
						|
    moveStroke(info) {
 | 
						|
        this.stroke.push(info)
 | 
						|
        this.redraw()
 | 
						|
    }
 | 
						|
 | 
						|
    // eslint-disable-next-line no-unused-vars
 | 
						|
    endStroke(info) {
 | 
						|
        if (this.stroke.length >= this.minStrokeLength) {
 | 
						|
            let cmd = new StrokeCommand(this.stroke)
 | 
						|
            cmd.do(this)
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    tiltToLineWidth(value) {
 | 
						|
        return this.lineWidth * 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)
 | 
						|
            let lineWidth = start.lineWidth
 | 
						|
            this.lineStyle(lineWidth, start.color, this.colorAlpha)
 | 
						|
            for (let i = 1; i < stroke.length; i++) {
 | 
						|
                let info = stroke[i]
 | 
						|
                let lw = info.lineWidth
 | 
						|
                if (lw != lineWidth) {
 | 
						|
                    lineWidth = lw
 | 
						|
                    this.lineStyle(lineWidth, 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(this.lineWidth, 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, lineWidth } = info
 | 
						|
        x /= this.wantedWidth
 | 
						|
        y /= this.wantedHeight
 | 
						|
        return { x, y, pressure, tiltX, tiltY, color, lineWidth }
 | 
						|
    }
 | 
						|
 | 
						|
    denormalizeInfo(info) {
 | 
						|
        let { x, y, pressure, tiltX, tiltY, color, lineWidth } = info
 | 
						|
        x = x * this.wantedWidth
 | 
						|
        y = y * this.wantedHeight
 | 
						|
        return { x, y, pressure, tiltX, tiltY, color, lineWidth }
 | 
						|
    }
 | 
						|
 | 
						|
    // 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()
 | 
						|
    }
 | 
						|
}
 |