1573 lines
		
	
	
		
			46 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			1573 lines
		
	
	
		
			46 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
/* eslint-disable no-unused-vars */
 | 
						|
/* eslint-disable no-console */
 | 
						|
/* globals debugCanvas */
 | 
						|
import Interface from './interface.js'
 | 
						|
import { Points, Polygon, Angle, Elements, LowPassFilter } from './utils.js'
 | 
						|
import Events from './events.js'
 | 
						|
import { InteractionMapper } from './interaction.js'
 | 
						|
import { Capabilities } from './capabilities.js'
 | 
						|
 | 
						|
/** This interface allows scatters to delegate tap events to other objects. */
 | 
						|
export class ITapDelegate extends Interface {
 | 
						|
    /** This method must be defined by the delegate. It handles the tap event. */
 | 
						|
    tap(event) {}
 | 
						|
 | 
						|
    /** Tells the delegate that it should handle standard click events. */
 | 
						|
    handleClicks() {}
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * A base class for scatter specific events.
 | 
						|
 *
 | 
						|
 * @constructor
 | 
						|
 * @param {name} String - The name of the event
 | 
						|
 * @param {target} Object - The target of the event
 | 
						|
 */
 | 
						|
export class BaseEvent {
 | 
						|
    constructor(name, target) {
 | 
						|
        this.name = name
 | 
						|
        this.target = target
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
// Event types
 | 
						|
const START = 'onStart'
 | 
						|
const UPDATE = 'onUpdate'
 | 
						|
const END = 'onEnd'
 | 
						|
 | 
						|
/**
 | 
						|
 * A scatter event that describes how the scatter has changed.
 | 
						|
 *
 | 
						|
 * @constructor
 | 
						|
 * @param {target} Object - The target scatter of the event
 | 
						|
 * @param {optional} Object - Optional parameter
 | 
						|
 */
 | 
						|
export class ScatterEvent extends BaseEvent {
 | 
						|
    constructor(
 | 
						|
        target,
 | 
						|
        { translate = { x: 0, y: 0 }, scale = null, rotate = 0, about = null, fast = false, type = null } = {}
 | 
						|
    ) {
 | 
						|
        super('scatterTransformed', { target: target })
 | 
						|
        this.translate = translate
 | 
						|
        this.scale = scale
 | 
						|
        this.rotate = rotate
 | 
						|
        this.about = about
 | 
						|
        this.fast = fast
 | 
						|
        this.type = type
 | 
						|
    }
 | 
						|
 | 
						|
    toString() {
 | 
						|
        return (
 | 
						|
            "Event('scatterTransformed', scale: " + this.scale + ' about: ' + this.about.x + ', ' + this.about.y + ')'
 | 
						|
        )
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * A scatter resize event that describes how the scatter has changed.
 | 
						|
 *
 | 
						|
 * @constructor
 | 
						|
 * @param {target} Object - The target scatter of the event
 | 
						|
 * @param {optional} Object - Optional parameter
 | 
						|
 */
 | 
						|
export class ResizeEvent extends BaseEvent {
 | 
						|
    constructor(target, { width = 0, height = 0 } = {}) {
 | 
						|
        super('scatterResized', { width: width, height: height })
 | 
						|
        this.width = width
 | 
						|
        this.height = height
 | 
						|
    }
 | 
						|
 | 
						|
    toString() {
 | 
						|
        return 'Event(scatterResized width: ' + this.width + 'height: ' + this.height + ')'
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * A abstract base class that implements the throwable behavior of a scatter
 | 
						|
 * object.
 | 
						|
 *
 | 
						|
 * @constructor
 | 
						|
 */
 | 
						|
 | 
						|
 | 
						|
class RequestFrameThrower {
 | 
						|
    /** Implemenents the standard throw behavior. */
 | 
						|
    animateThrow(throwable) {
 | 
						|
        /*** Calls the animateThrow method in sync with the display link. */
 | 
						|
        requestAnimationFrame(throwable.animateThrow.bind(throwable))
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
class TimeoutThrower {
 | 
						|
 | 
						|
    constructor(delay=20) {
 | 
						|
        this.throwables = new Set()
 | 
						|
        this.delay = delay
 | 
						|
        setTimeout(this.animateStep.bind(this), this.delay)
 | 
						|
    }
 | 
						|
 | 
						|
    animateThrow(throwable) {
 | 
						|
        this.throwables.add(throwable)
 | 
						|
    }
 | 
						|
 | 
						|
    animateStep() {
 | 
						|
        let active = [...this.throwables]
 | 
						|
        this.throwables.clear()
 | 
						|
        for(let throwable of active) {
 | 
						|
            throwable.animateThrow()
 | 
						|
        }
 | 
						|
        setTimeout(this.animateStep.bind(this), this.delay)
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
let thrower = new RequestFrameThrower()
 | 
						|
 | 
						|
class Throwable {
 | 
						|
    constructor({
 | 
						|
        movableX = true,
 | 
						|
        movableY = true,
 | 
						|
        throwVisibility = 44,
 | 
						|
        throwDamping = 0.95,
 | 
						|
        autoThrow = true,
 | 
						|
        onThrowFinished = null
 | 
						|
    } = {}) {
 | 
						|
        this.movableX = movableX
 | 
						|
        this.movableY = movableY
 | 
						|
        this.throwVisibility = throwVisibility
 | 
						|
        this.throwDamping = throwDamping
 | 
						|
        this.autoThrow = autoThrow
 | 
						|
        this.velocities = []
 | 
						|
        this.velocity = null
 | 
						|
        this.lastframe = null
 | 
						|
        this.onThrowFinished = onThrowFinished
 | 
						|
    }
 | 
						|
 | 
						|
    static defaultThrow() {
 | 
						|
        thrower = new RequestFrameThrower()
 | 
						|
    }
 | 
						|
 | 
						|
    static timeoutThrow() {
 | 
						|
        thrower = new TimeoutThrower()
 | 
						|
    }
 | 
						|
 | 
						|
    observeVelocity() {
 | 
						|
        this.lastframe = performance.now()
 | 
						|
    }
 | 
						|
 | 
						|
    addVelocity(delta, buffer = 5) {
 | 
						|
        let t = performance.now()
 | 
						|
        let dt = t - this.lastframe
 | 
						|
        this.lastframe = t
 | 
						|
 | 
						|
        // When number is not set or is zero. Use one instead.
 | 
						|
        let number = delta.number ? delta.number : 1
 | 
						|
        if (dt > 0) {
 | 
						|
            // Avoid division by zero errors later on
 | 
						|
            // and consider the number of involved pointers sind addVelocity will be called by the
 | 
						|
            // onMove events
 | 
						|
            let velocity = {
 | 
						|
                t: t,
 | 
						|
                dt: dt,
 | 
						|
                dx: delta.x / number,
 | 
						|
                dy: delta.y / number
 | 
						|
            }
 | 
						|
            this.velocities.push(velocity)
 | 
						|
            while (this.velocities.length > buffer) {
 | 
						|
                this.velocities.shift()
 | 
						|
            }
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    addTestVelocity(delta, dt=20, buffer = 5) {
 | 
						|
        let t = performance.now()
 | 
						|
        this.lastframe = t
 | 
						|
        let velocity = {
 | 
						|
            t: t,
 | 
						|
            dt: dt,
 | 
						|
            dx: delta.x ,
 | 
						|
            dy: delta.y 
 | 
						|
        }
 | 
						|
        for(let i=0; i<buffer; i++) {
 | 
						|
            velocity.t += dt
 | 
						|
            this.velocities.push(velocity)
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    meanVelocity(milliseconds = 30) {
 | 
						|
     //   this.addVelocity({ x: 0, y: 0, number: 1 })
 | 
						|
        let sum = { x: 0, y: 0 }
 | 
						|
        let count = 0
 | 
						|
        let t = 0
 | 
						|
        for (let i = this.velocities.length - 1; i > 0; i--) {
 | 
						|
            let v = this.velocities[i]
 | 
						|
            t += v.dt
 | 
						|
            let nv = { x: v.dx / v.dt, y: v.dy / v.dt }
 | 
						|
            sum = Points.add(sum, nv)
 | 
						|
            count += 1
 | 
						|
            if (t > milliseconds) {
 | 
						|
                break
 | 
						|
            }
 | 
						|
        }
 | 
						|
        if (count === 0) return sum // empty vector
 | 
						|
        return Points.multiplyScalar(sum, 1 / count)
 | 
						|
    }
 | 
						|
 | 
						|
    killAnimation() {
 | 
						|
        this.velocity = null
 | 
						|
        this.velocities = []
 | 
						|
        this.lastframe = null
 | 
						|
    }
 | 
						|
 | 
						|
    startThrow() {
 | 
						|
        this.velocity = this.meanVelocity()
 | 
						|
        if (this.velocity != null) {
 | 
						|
            // Call next velocity to ansure that specializations
 | 
						|
            // that use keepOnStage are called
 | 
						|
            this.velocity = this.nextVelocity(this.velocity)
 | 
						|
            if (this.autoThrow) this.animateThrow(performance.now())
 | 
						|
        } else {
 | 
						|
            this.onDragComplete()
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    _throwDeltaTime() {
 | 
						|
        let t = performance.now()
 | 
						|
        let dt = t - this.lastframe
 | 
						|
        this.lastframe = t
 | 
						|
        return dt
 | 
						|
    }
 | 
						|
 | 
						|
    recurseAnimateThrow() {
 | 
						|
        ThrowableObjects.add(this)
 | 
						|
    }
 | 
						|
 | 
						|
    animateThrow(time) {
 | 
						|
        if (this.velocity != null) {
 | 
						|
            let dt = this._throwDeltaTime()
 | 
						|
            // console.log("animateThrow", dt)
 | 
						|
            let next = this.nextVelocity(this.velocity)
 | 
						|
            let prevLength = Points.length(this.velocity)
 | 
						|
            let nextLength = Points.length(next)
 | 
						|
            if (nextLength > prevLength) {
 | 
						|
                let factor = nextLength / prevLength
 | 
						|
                next = Points.multiplyScalar(next, 1 / factor)
 | 
						|
                // console.log('Prevent acceleration', factor, this.velocity, next)
 | 
						|
            }
 | 
						|
            this.velocity = next
 | 
						|
            let d = Points.multiplyScalar(this.velocity, dt)
 | 
						|
            this._move(d)
 | 
						|
 | 
						|
            this.onDragUpdate(d)
 | 
						|
            if (dt == 0 || this.needsAnimation()) {
 | 
						|
                thrower.animateThrow(this)
 | 
						|
                return
 | 
						|
            } else {
 | 
						|
                if (this.isOutside()) {
 | 
						|
                    thrower.animateThrow(this)
 | 
						|
                    return
 | 
						|
                }
 | 
						|
            }
 | 
						|
        }
 | 
						|
        this.onDragComplete()
 | 
						|
        if (this.onThrowFinished != null) {
 | 
						|
            this.onThrowFinished()
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    needsAnimation() {
 | 
						|
        if (this.velocity == null) {
 | 
						|
            return false
 | 
						|
        }
 | 
						|
        return Points.length(this.velocity) > 0.01
 | 
						|
    }
 | 
						|
 | 
						|
    nextVelocity(velocity) {
 | 
						|
        // Must be overwritten: computes the changed velocity. Implement
 | 
						|
        // damping, collison detection, etc. here
 | 
						|
        let next = Points.multiplyScalar(velocity, this.throwDamping)
 | 
						|
        return {
 | 
						|
            x: this.movableX ? next.x : 0,
 | 
						|
            y: this.movableY ? next.y : 0
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    _move(delta) {
 | 
						|
        // Overwrite if necessary
 | 
						|
    }
 | 
						|
 | 
						|
    onDragComplete() {
 | 
						|
        // Overwrite if necessary
 | 
						|
    }
 | 
						|
 | 
						|
    onDragUpdate(delta) {
 | 
						|
        // Overwrite if necessary
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
export class AbstractScatter extends Throwable {
 | 
						|
    constructor({
 | 
						|
        minScale = 0.1,
 | 
						|
        maxScale = 1.0,
 | 
						|
        startScale = 1.0,
 | 
						|
        autoBringToFront = true,
 | 
						|
        autoThrow = true,
 | 
						|
        translatable = true,
 | 
						|
        scalable = true,
 | 
						|
        rotatable = true,
 | 
						|
        resizable = false,
 | 
						|
        movableX = true,
 | 
						|
        movableY = true,
 | 
						|
        throwVisibility = 44,
 | 
						|
        throwDamping = 0.95,
 | 
						|
        overdoScaling = 1,
 | 
						|
        mouseZoomFactor = 1.1,
 | 
						|
        rotationDegrees = null,
 | 
						|
        rotation = null,
 | 
						|
        onTransform = null,
 | 
						|
        interactive = true,
 | 
						|
        onClose = null,
 | 
						|
        onThrowFinished = null,
 | 
						|
        scaleAutoClose = false,
 | 
						|
        scaleCloseThreshold = 0.1,
 | 
						|
        scaleCloseBuffer = 0.05,
 | 
						|
        maxRotation = Angle.degree2radian(5),
 | 
						|
        minInteractionDistance = 0,
 | 
						|
        useLowPassFilter = false
 | 
						|
    } = {}) {
 | 
						|
        if (rotationDegrees != null && rotation != null) {
 | 
						|
            throw new Error('Use rotationDegrees or rotation but not both')
 | 
						|
        } else if (rotation != null) {
 | 
						|
            rotationDegrees = Angle.radian2degree(rotation)
 | 
						|
        } else if (rotationDegrees == null) {
 | 
						|
            rotationDegrees = 0
 | 
						|
        }
 | 
						|
        super({
 | 
						|
            movableX,
 | 
						|
            movableY,
 | 
						|
            throwVisibility,
 | 
						|
            throwDamping,
 | 
						|
            autoThrow,
 | 
						|
            onThrowFinished
 | 
						|
        })
 | 
						|
 | 
						|
        /**
 | 
						|
         * Closes the card when the minScale is reached and the
 | 
						|
         * card is released. Card can be saved by scaling it up again.
 | 
						|
         */
 | 
						|
        this.scaleAutoClose = scaleAutoClose
 | 
						|
        this.scaleCloseThreshold = scaleCloseThreshold
 | 
						|
        this.scaleCloseBuffer = scaleCloseBuffer
 | 
						|
        this.scaleAutoCloseTimeout = null
 | 
						|
 | 
						|
        this.interactive = interactive
 | 
						|
        this.startRotationDegrees = rotationDegrees
 | 
						|
        this.startScale = startScale // Needed to reset object
 | 
						|
        this.minScale = minScale
 | 
						|
        this.maxScale = maxScale
 | 
						|
        this.maxRotation = maxRotation
 | 
						|
        this.overdoScaling = overdoScaling
 | 
						|
        this.translatable = translatable
 | 
						|
        if (!translatable) {
 | 
						|
            this.movableX = false
 | 
						|
            this.movableY = false
 | 
						|
        }
 | 
						|
        this.scalable = scalable
 | 
						|
        this.rotatable = rotatable
 | 
						|
        this.resizable = resizable
 | 
						|
        this.mouseZoomFactor = mouseZoomFactor
 | 
						|
        this.autoBringToFront = autoBringToFront
 | 
						|
        this.useLowPassFilter = useLowPassFilter
 | 
						|
        this.minInteractionDistance = minInteractionDistance
 | 
						|
        if (useLowPassFilter) {
 | 
						|
            this.rotateLPF = new LowPassFilter()
 | 
						|
            this.zoomLPF = new LowPassFilter()
 | 
						|
            this.zoomLPF.setup([1, 1, 1, 1, 1, 1, 1, 1, 1, 1])
 | 
						|
        }
 | 
						|
        this.dragging = false
 | 
						|
        this.onTransform = onTransform != null ? [onTransform] : null
 | 
						|
        this.onClose = onClose != null ? [onClose] : null
 | 
						|
    }
 | 
						|
 | 
						|
    addCloseEventCallback(callback) {
 | 
						|
        if (this.onClose == null) {
 | 
						|
            this.onClose = []
 | 
						|
        }
 | 
						|
        this.onClose.push(callback)
 | 
						|
    }
 | 
						|
 | 
						|
    addTransformEventCallback(callback) {
 | 
						|
        if (this.onTransform == null) {
 | 
						|
            this.onTransform = []
 | 
						|
        }
 | 
						|
        this.onTransform.push(callback)
 | 
						|
    }
 | 
						|
 | 
						|
    startGesture(interaction) {
 | 
						|
        this.bringToFront()
 | 
						|
        this.killAnimation()
 | 
						|
        this.observeVelocity()
 | 
						|
        if (this.useLowPassFilter) {
 | 
						|
            this.rotateLPF.clear()
 | 
						|
            this.zoomLPF.clear()
 | 
						|
            this.zoomLPF.setup([1, 1, 1, 1, 1, 1, 1, 1, 1, 1])
 | 
						|
        }
 | 
						|
        return true
 | 
						|
    }
 | 
						|
 | 
						|
    close() {
 | 
						|
        this._callCloseCallbacks()
 | 
						|
        this._removeCallbacks()
 | 
						|
        this._removeSelfFromScatterContainer()
 | 
						|
    }
 | 
						|
 | 
						|
    _callCloseCallbacks() {
 | 
						|
        if (this.onClose) {
 | 
						|
            this.onClose.forEach(callback => callback(this))
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    _removeCallbacks() {
 | 
						|
        this.onClose = []
 | 
						|
        this.onTransform = []
 | 
						|
    }
 | 
						|
 | 
						|
    _removeSelfFromScatterContainer() {
 | 
						|
        // Removes self from container when it's closed.
 | 
						|
        if (this.container) {
 | 
						|
            this.container.remove(this)
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    gesture(interaction) {
 | 
						|
        let delta = interaction.delta()
 | 
						|
 | 
						|
        if (delta != null) {
 | 
						|
            /* uo: Is this the best place to add velocity? It works with scrollable text in card drawers but
 | 
						|
            has to be tested */
 | 
						|
            this.addVelocity(delta)
 | 
						|
            let rotate = delta.rotate
 | 
						|
            let zoom = delta.zoom
 | 
						|
            if (this.maxRotation != null) {
 | 
						|
                if (Math.abs(rotate) > this.maxRotation) {
 | 
						|
                    rotate = 0
 | 
						|
                }
 | 
						|
            }
 | 
						|
            if (this.useLowPassFilter) {
 | 
						|
                rotate = this.rotateLPF.next(rotate)
 | 
						|
                zoom = this.zoomLPF.next(zoom)
 | 
						|
            }
 | 
						|
            if (delta.distance < this.minInteractionDistance) {
 | 
						|
                let ratio = delta.distance / this.minInteractionDistance
 | 
						|
                rotate *= ratio
 | 
						|
                let zoomDelta = zoom - 1
 | 
						|
                zoomDelta *= ratio
 | 
						|
                zoom = 1 + zoomDelta
 | 
						|
            }
 | 
						|
            this.transform(delta, zoom, rotate, delta.about)
 | 
						|
            /* uo: This is too late an dangerous. transform sometimes modifies delta
 | 
						|
            this.addVelocity(delta) // uo: reverted commit fa0256d782dd498c6d3e51321260ca375ca9f855
 | 
						|
            */
 | 
						|
 | 
						|
            if (zoom != 1) this.interactionAnchor = delta.about
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    get polygon() {
 | 
						|
        let w2 = (this.width * this.scale) / 2
 | 
						|
        let h2 = (this.height * this.scale) / 2
 | 
						|
        let center = this.center
 | 
						|
        let polygon = new Polygon(center)
 | 
						|
        polygon.addPoint({ x: -w2, y: -h2 })
 | 
						|
        polygon.addPoint({ x: w2, y: -h2 })
 | 
						|
        polygon.addPoint({ x: w2, y: h2 })
 | 
						|
        polygon.addPoint({ x: -w2, y: h2 })
 | 
						|
        polygon.rotate(this.rotation)
 | 
						|
        return polygon
 | 
						|
    }
 | 
						|
 | 
						|
    isOutside() {
 | 
						|
        let stagePolygon = this.containerPolygon
 | 
						|
        if (stagePolygon == null) return false
 | 
						|
        let polygon = this.polygon
 | 
						|
        if (polygon == null) return false
 | 
						|
        let result = stagePolygon.intersectsWith(polygon)
 | 
						|
        return result === false || result.overlap < this.throwVisibility
 | 
						|
    }
 | 
						|
 | 
						|
    recenter() {
 | 
						|
        // Return a small vector that guarantees that the scatter is moving
 | 
						|
        // towards the center of the stage
 | 
						|
        let center = this.center
 | 
						|
        let target = this.container.center
 | 
						|
        let delta = Points.subtract(target, center)
 | 
						|
        return Points.normalize(delta)
 | 
						|
    }
 | 
						|
 | 
						|
    nextVelocity(velocity) {
 | 
						|
        return this.keepOnStage(velocity)
 | 
						|
    }
 | 
						|
 | 
						|
    bouncing() {
 | 
						|
        // Implements the bouncing behavior of the scatter. Moves the scatter
 | 
						|
        // to the center of the stage if the scatter is outside the stage or
 | 
						|
        // not within the limits of the throwVisibility.
 | 
						|
 | 
						|
        let stagePolygon = this.containerPolygon
 | 
						|
        let polygon = this.polygon
 | 
						|
        let result = stagePolygon.intersectsWith(polygon)
 | 
						|
        if (result === false || result.overlap < this.throwVisibility) {
 | 
						|
            let cv = this.recenter()
 | 
						|
            let recentered = false
 | 
						|
            while (result === false || result.overlap < this.throwVisibility) {
 | 
						|
                polygon.center.x += cv.x
 | 
						|
                polygon.center.y += cv.y
 | 
						|
                this._move(cv)
 | 
						|
                result = stagePolygon.intersectsWith(polygon)
 | 
						|
                recentered = true
 | 
						|
            }
 | 
						|
            return recentered
 | 
						|
        }
 | 
						|
        return false
 | 
						|
    }
 | 
						|
 | 
						|
    keepOnStage(velocity, collision = 0.5) {
 | 
						|
        let stagePolygon = this.containerPolygon
 | 
						|
        // UO: since keepOnStage is called in nextVelocity we need to
 | 
						|
        // ensure a return value
 | 
						|
        if (!stagePolygon) return { x: 0, y: 0 }
 | 
						|
        let polygon = this.polygon
 | 
						|
        let bounced = this.bouncing()
 | 
						|
        if (bounced) {
 | 
						|
            let stage = this.containerBounds
 | 
						|
            let x = this.center.x
 | 
						|
            let y = this.center.y
 | 
						|
            let dx = this.movableX ? velocity.x : 0
 | 
						|
            let dy = this.movableY ? velocity.y : 0
 | 
						|
            let factor = this.throwDamping
 | 
						|
            // if (recentered) {
 | 
						|
            if (x < 0) {
 | 
						|
                dx = -dx
 | 
						|
                factor = collision
 | 
						|
            }
 | 
						|
            if (x > stage.width) {
 | 
						|
                dx = -dx
 | 
						|
                factor = collision
 | 
						|
            }
 | 
						|
            if (y < 0) {
 | 
						|
                dy = -dy
 | 
						|
                factor = collision
 | 
						|
            }
 | 
						|
            if (y > stage.height) {
 | 
						|
                dy = -dy
 | 
						|
                factor = collision
 | 
						|
            }
 | 
						|
            // }
 | 
						|
            return Points.multiplyScalar({ x: dx, y: dy }, factor)
 | 
						|
        }
 | 
						|
        return super.nextVelocity(velocity)
 | 
						|
    }
 | 
						|
 | 
						|
    endGesture(interaction) {
 | 
						|
        this.startThrow()
 | 
						|
        this._checkAutoClose()
 | 
						|
    }
 | 
						|
 | 
						|
    _checkAutoClose() {
 | 
						|
        if (this.scaleAutoClose)
 | 
						|
            if (this.scale < this.minScale + this.scaleCloseThreshold - this.scaleCloseBuffer) {
 | 
						|
                this.zoom(this.minScale, {
 | 
						|
                    animate: 0.2,
 | 
						|
                    onComplete: this.close.bind(this)
 | 
						|
                })
 | 
						|
            } else if (this.scale < this.minScale + this.scaleCloseThreshold) {
 | 
						|
                this.zoom(this.minScale + this.scaleCloseThreshold, {
 | 
						|
                    animate: 0.4
 | 
						|
                })
 | 
						|
            }
 | 
						|
    }
 | 
						|
 | 
						|
    rotateDegrees(degrees, anchor) {
 | 
						|
        let rad = Angle.degree2radian(degrees)
 | 
						|
        this.rotate(rad, anchor)
 | 
						|
    }
 | 
						|
 | 
						|
    rotate(rad, anchor) {
 | 
						|
        this.transform({ x: 0, y: 0 }, 1.0, rad, anchor)
 | 
						|
    }
 | 
						|
 | 
						|
    move(d, { animate = 0 } = {}) {
 | 
						|
        if (this.translatable) {
 | 
						|
            if (animate > 0) {
 | 
						|
                let startPos = this.position
 | 
						|
                TweenLite.to(this, animate, {
 | 
						|
                    x: '+=' + d.x,
 | 
						|
                    y: '+=' + d.y,
 | 
						|
                    /* scale: scale, uo: not defined, why was this here? */
 | 
						|
                    onUpdate: e => {
 | 
						|
                        let p = this.position
 | 
						|
                        let dx = p.x - startPos.x
 | 
						|
                        let dy = p.x - startPos.y
 | 
						|
                        this.onMoved(dx, dy)
 | 
						|
                    }
 | 
						|
                })
 | 
						|
            } else {
 | 
						|
                this._move(d)
 | 
						|
                this.onMoved(d.x, d.y)
 | 
						|
            }
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    moveTo(p, { animate = 0 } = {}) {
 | 
						|
        let c = this.origin
 | 
						|
        let delta = Points.subtract(p, c)
 | 
						|
        this.move(delta, { animate: animate })
 | 
						|
    }
 | 
						|
 | 
						|
    centerAt(p, { animate = 0 } = {}) {
 | 
						|
        let c = this.center
 | 
						|
        let delta = Points.subtract(p, c)
 | 
						|
        this.move(delta, { animate: animate })
 | 
						|
    }
 | 
						|
 | 
						|
    zoom(scale, { animate = 0, about = null, delay = 0, x = null, y = null, onComplete = null } = {}) {
 | 
						|
        let anchor = about || this.center
 | 
						|
        if (scale != this.scale) {
 | 
						|
            if (animate > 0) {
 | 
						|
                TweenLite.to(this, animate, {
 | 
						|
                    scale: scale,
 | 
						|
                    delay: delay,
 | 
						|
                    onComplete: onComplete,
 | 
						|
                    onUpdate: this.onZoomed.bind(this)
 | 
						|
                })
 | 
						|
            } else {
 | 
						|
                this.scale = scale
 | 
						|
                this.onZoomed(anchor)
 | 
						|
            }
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    _move(delta) {
 | 
						|
        // this.addVelocity(delta) uo: reverted commit fa0256d782dd498c6d3e51321260ca375ca9f855
 | 
						|
        this.x += this.movableX ? delta.x : 0
 | 
						|
        this.y += this.movableX ? delta.y : 0
 | 
						|
    }
 | 
						|
 | 
						|
    transform(translate, zoom, rotate, anchor) {
 | 
						|
        let delta = {
 | 
						|
            x: this.movableX ? translate.x : 0,
 | 
						|
            y: this.movableY ? translate.y : 0
 | 
						|
        }
 | 
						|
        if (this.resizable) var vzoom = zoom
 | 
						|
        if (!this.translatable) delta = { x: 0, y: 0 }
 | 
						|
        if (!this.rotatable) rotate = 0
 | 
						|
        if (!this.scalable) zoom = 1.0
 | 
						|
        if (zoom == 1.0 && rotate == 0) {
 | 
						|
            this._move(delta)
 | 
						|
            if (this.onTransform != null) {
 | 
						|
                let event = new ScatterEvent(this, {
 | 
						|
                    translate: delta,
 | 
						|
                    scale: this.scale,
 | 
						|
                    rotate: 0,
 | 
						|
                    about: anchor,
 | 
						|
                    fast: false,
 | 
						|
                    type: UPDATE
 | 
						|
                })
 | 
						|
                this.onTransform.forEach(function(f) {
 | 
						|
                    f(event)
 | 
						|
                })
 | 
						|
            }
 | 
						|
            return
 | 
						|
        }
 | 
						|
        let origin = this.rotationOrigin
 | 
						|
        let beta = Points.angle(origin, anchor)
 | 
						|
        let distance = Points.distance(origin, anchor)
 | 
						|
        let { scale: newScale, zoom: thresholdedZoom } = this.calculateScale(zoom)
 | 
						|
 | 
						|
        let newOrigin = Points.arc(anchor, beta + rotate, distance * thresholdedZoom)
 | 
						|
        let extra = Points.subtract(newOrigin, origin)
 | 
						|
        const anchorOffset = Points.subtract(anchor, origin)
 | 
						|
        // this._move(offset)
 | 
						|
        this.scale = newScale
 | 
						|
        this.rotation += rotate
 | 
						|
        let offset = Points.negate(anchorOffset)
 | 
						|
        offset = Points.add(offset, extra)
 | 
						|
        offset = Points.add(offset, translate)
 | 
						|
        this._move(Points.add(anchorOffset, offset))
 | 
						|
 | 
						|
        delta.x += extra.x
 | 
						|
        delta.y += extra.y
 | 
						|
        if (this.onTransform != null) {
 | 
						|
            let event = new ScatterEvent(this, {
 | 
						|
                translate: delta,
 | 
						|
                scale: newScale,
 | 
						|
                rotate: rotate,
 | 
						|
                about: anchor
 | 
						|
            })
 | 
						|
            this.onTransform.forEach(function(f) {
 | 
						|
                f(event)
 | 
						|
            })
 | 
						|
        }
 | 
						|
        if (this.resizable) {
 | 
						|
            this.resizeAfterTransform(vzoom)
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * For a given zoom, a new scale is calculated, taking
 | 
						|
     * min and max scale into account.
 | 
						|
     *
 | 
						|
     * @param {number} zoom - The zoom factor, to scale the object with.
 | 
						|
     * @returns {object} - Returns an object containing the a value for a valid scale and the corrected zoom factor.
 | 
						|
     */
 | 
						|
    calculateScale(zoom) {
 | 
						|
        let scale = this.scale * zoom
 | 
						|
 | 
						|
        let minScale = this.minScale / this.overdoScaling
 | 
						|
        let maxScale = this.maxScale * this.overdoScaling
 | 
						|
        if (scale < minScale) {
 | 
						|
            scale = minScale
 | 
						|
            zoom = scale / this.scale
 | 
						|
        }
 | 
						|
        if (scale > maxScale) {
 | 
						|
            scale = maxScale
 | 
						|
            zoom = scale / this.scale
 | 
						|
        }
 | 
						|
 | 
						|
        if (this.scaleAutoClose) this._updateTransparency()
 | 
						|
 | 
						|
        return { zoom, scale }
 | 
						|
    }
 | 
						|
 | 
						|
    _updateTransparency() {
 | 
						|
        if (this.scale < this.minScale + this.scaleCloseThreshold) {
 | 
						|
            let transparency = this.calculateScaleTransparency()
 | 
						|
            this.element.style.opacity = transparency
 | 
						|
        } else this.element.style.opacity = 1
 | 
						|
    }
 | 
						|
 | 
						|
    calculateScaleTransparency() {
 | 
						|
        let transparency = (this.scale - this.minScale) / this.scaleCloseThreshold
 | 
						|
        transparency = transparency > 1 ? 1 : transparency < 0 ? 0 : transparency
 | 
						|
        return transparency
 | 
						|
    }
 | 
						|
 | 
						|
    resizeAfterTransform(zoom) {
 | 
						|
        // Overwrite this in subclasses.
 | 
						|
    }
 | 
						|
 | 
						|
    validScale(scale) {
 | 
						|
        scale = Math.max(scale, this.minScale)
 | 
						|
        scale = Math.min(scale, this.maxScale)
 | 
						|
        return scale
 | 
						|
    }
 | 
						|
 | 
						|
    animateZoomBounce(dt = 1) {
 | 
						|
        if (this.zoomAnchor != null) {
 | 
						|
            let zoom = 1
 | 
						|
            let amount = Math.min(0.01, (0.3 * dt) / 100000.0)
 | 
						|
            if (this.scale < this.minScale) zoom = 1 + amount
 | 
						|
            if (this.scale > this.maxScale) zoom = 1 - amount
 | 
						|
            if (zoom != 1) {
 | 
						|
                this.transform({ x: 0, y: 0 }, zoom, 0, this.zoomAnchor)
 | 
						|
                requestAnimationFrame(dt => {
 | 
						|
                    this.animateZoomBounce(dt)
 | 
						|
                })
 | 
						|
                return
 | 
						|
            }
 | 
						|
            this.zoomAnchor = null
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    checkScaling(about, delay = 0) {
 | 
						|
        this.zoomAnchor = about
 | 
						|
        clearTimeout(this.animateZoomBounce.bind(this))
 | 
						|
        setTimeout(this.animateZoomBounce.bind(this), delay)
 | 
						|
    }
 | 
						|
 | 
						|
    onMouseWheel(event) {
 | 
						|
        if (event.claimedByScatter) {
 | 
						|
            if (event.claimedByScatter != this) return
 | 
						|
        }
 | 
						|
        this.killAnimation()
 | 
						|
        this.targetScale = null
 | 
						|
        let direction = event.detail < 0 || event.wheelDelta > 0
 | 
						|
        let globalPoint = { x: event.clientX, y: event.clientY }
 | 
						|
        let centerPoint = this.mapPositionToContainerPoint(globalPoint)
 | 
						|
        if (event.shiftKey) {
 | 
						|
            let degrees = direction ? 5 : -5
 | 
						|
            let rad = Angle.degree2radian(degrees)
 | 
						|
            return this.transform({ x: 0, y: 0 }, 1.0, rad, centerPoint)
 | 
						|
        }
 | 
						|
        const zoomFactor = this.mouseZoomFactor
 | 
						|
        let zoom = direction ? zoomFactor : 1 / zoomFactor
 | 
						|
        this.transform({ x: 0, y: 0 }, zoom, 0, centerPoint)
 | 
						|
        this.checkScaling(centerPoint, 200)
 | 
						|
 | 
						|
        if (this.scaleAutoClose) {
 | 
						|
            if (this.scale <= this.minScale + this.scaleCloseThreshold) {
 | 
						|
                if (this.scaleAutoCloseTimeout) clearTimeout(this.scaleAutoCloseTimeout)
 | 
						|
                this.scaleAutoCloseTimeout = setTimeout(() => {
 | 
						|
                    this._checkAutoClose()
 | 
						|
                }, 600)
 | 
						|
            }
 | 
						|
            this._updateTransparency()
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    onStart(event, interaction) {
 | 
						|
        if (this.startGesture(interaction)) {
 | 
						|
            this.dragging = true
 | 
						|
            this.interactionAnchor = null
 | 
						|
        }
 | 
						|
        if (this.onTransform != null) {
 | 
						|
            let event = new ScatterEvent(this, {
 | 
						|
                translate: { x: 0, y: 0 },
 | 
						|
                scale: this.scale,
 | 
						|
                rotate: 0,
 | 
						|
                about: null,
 | 
						|
                fast: false,
 | 
						|
                type: START
 | 
						|
            })
 | 
						|
            this.onTransform.forEach(function(f) {
 | 
						|
                f(event)
 | 
						|
            })
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    onMove(event, interaction) {
 | 
						|
        /** As long as mouseout && mouseleave interrupt we cannot be sure that
 | 
						|
         * dragging remains correct.
 | 
						|
         */
 | 
						|
        if (this.dragging) {
 | 
						|
            this.gesture(interaction)
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    onEnd(event, interaction) {
 | 
						|
        //console.log('Scatter.onEnd', this.dragging)
 | 
						|
        if (interaction.isFinished()) {
 | 
						|
            this.endGesture(interaction)
 | 
						|
            this.dragging = false
 | 
						|
            for (let key of interaction.ended.keys()) {
 | 
						|
                if (interaction.isTap(key)) {
 | 
						|
                    //console.log('Scatter.isTap')
 | 
						|
                    let point = interaction.ended.get(key)
 | 
						|
                    this.onTap(event, interaction, point)
 | 
						|
                }
 | 
						|
            }
 | 
						|
            if (this.onTransform != null) {
 | 
						|
                let event = new ScatterEvent(this, {
 | 
						|
                    translate: { x: 0, y: 0 },
 | 
						|
                    scale: this.scale,
 | 
						|
                    rotate: 0,
 | 
						|
                    about: null,
 | 
						|
                    fast: false,
 | 
						|
                    type: END
 | 
						|
                })
 | 
						|
                this.onTransform.forEach(function(f) {
 | 
						|
                    f(event)
 | 
						|
                })
 | 
						|
            }
 | 
						|
        }
 | 
						|
        let about = this.interactionAnchor
 | 
						|
        if (about != null) {
 | 
						|
            this.checkScaling(about, 100)
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    //onTap(event, interaction, point) {}
 | 
						|
 | 
						|
    onTap(event, interaction, point) {
 | 
						|
        //console.log('AbstractScatter.onTap', this.tapDelegate, interaction)
 | 
						|
        if (this.tapDelegate) {
 | 
						|
            Events.stop(event)
 | 
						|
            this.tapDelegate.tap(event, 'scatter')
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    onDragUpdate(delta) {
 | 
						|
        if (this.onTransform != null) {
 | 
						|
            let event = new ScatterEvent(this, {
 | 
						|
                fast: true,
 | 
						|
                translate: delta,
 | 
						|
                scale: this.scale,
 | 
						|
                about: this.currentAbout,
 | 
						|
                type: null
 | 
						|
            })
 | 
						|
            this.onTransform.forEach(function(f) {
 | 
						|
                f(event)
 | 
						|
            })
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    onDragComplete() {
 | 
						|
        if (this.onTransform) {
 | 
						|
            let event = new ScatterEvent(this, {
 | 
						|
                scale: this.scale,
 | 
						|
                about: this.currentAbout,
 | 
						|
                fast: false,
 | 
						|
                type: null
 | 
						|
            })
 | 
						|
            this.onTransform.forEach(function(f) {
 | 
						|
                f(event)
 | 
						|
            })
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    onMoved(dx, dy, about) {
 | 
						|
        if (this.onTransform != null) {
 | 
						|
            let event = new ScatterEvent(this, {
 | 
						|
                translate: { x: dx, y: dy },
 | 
						|
                about: about,
 | 
						|
                fast: true,
 | 
						|
                type: null
 | 
						|
            })
 | 
						|
            this.onTransform.forEach(function(f) {
 | 
						|
                f(event)
 | 
						|
            })
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    onResizing() {
 | 
						|
        if (this.onTransform != null) {
 | 
						|
            let event = new ScatterEvent(this, {
 | 
						|
                scale: this.scale,
 | 
						|
                fast: false,
 | 
						|
                type: null
 | 
						|
            })
 | 
						|
            this.onTransform.forEach(function(f) {
 | 
						|
                f(event)
 | 
						|
            })
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    onZoomed(about) {
 | 
						|
        if (this.scaleAutoClose) this._updateTransparency()
 | 
						|
 | 
						|
        if (this.onTransform != null) {
 | 
						|
            let event = new ScatterEvent(this, {
 | 
						|
                scale: this.scale,
 | 
						|
                about: about,
 | 
						|
                fast: false,
 | 
						|
                type: null
 | 
						|
            })
 | 
						|
            this.onTransform.forEach(function(f) {
 | 
						|
                f(event)
 | 
						|
            })
 | 
						|
        }
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
/** A container for scatter objects, which uses a single InteractionMapper
 | 
						|
 * for all children. This reduces the number of registered event handlers
 | 
						|
 * and covers the common use case that multiple objects are scattered
 | 
						|
 * on the same level.
 | 
						|
 */
 | 
						|
export class DOMScatterContainer {
 | 
						|
    /**
 | 
						|
     * @constructor
 | 
						|
     * @param {DOM node} element - DOM element that receives events
 | 
						|
     * @param {Bool} stopEvents -  Whether events should be stopped or propagated
 | 
						|
     * @param {Bool} claimEvents - Whether events should be marked as claimed
 | 
						|
     *                             if findTarget return as non-null value.
 | 
						|
     * @param {String} [touchAction=none] - CSS to set touch action style, needed to prevent
 | 
						|
     *                              pointer cancel events. Use null if the
 | 
						|
     *                              the touch action should not be set.
 | 
						|
     * @param {DOM node} debugCanvas - Shows debug infos about touches if not null
 | 
						|
     */
 | 
						|
    constructor(
 | 
						|
        element,
 | 
						|
        {
 | 
						|
            stopEvents = 'auto',
 | 
						|
            claimEvents = true,
 | 
						|
            useCapture = true,
 | 
						|
            capturePointerEvents = true,
 | 
						|
            touchAction = 'none',
 | 
						|
            debugCanvas = null
 | 
						|
        } = {}
 | 
						|
    ) {
 | 
						|
        this.onCapture = null
 | 
						|
        this.element = element
 | 
						|
        if (stopEvents === 'auto') {
 | 
						|
            /*
 | 
						|
            The events have to be stopped in Safari, otherwise the whole page will be zoomed with
 | 
						|
            a pinch gesture (preventDefault in method preventPinch). In order to enable the
 | 
						|
            movement of scatter objects, the touchmove event has to be bound again.
 | 
						|
            */
 | 
						|
            if (Capabilities.isSafari) {
 | 
						|
                document.addEventListener('touchmove', event => this.preventPinch(event), false)
 | 
						|
                stopEvents = false
 | 
						|
            } else {
 | 
						|
                stopEvents = true
 | 
						|
            }
 | 
						|
        }
 | 
						|
        this.stopEvents = stopEvents
 | 
						|
        this.claimEvents = claimEvents
 | 
						|
        if (touchAction !== null) {
 | 
						|
            Elements.setStyle(element, { touchAction })
 | 
						|
        }
 | 
						|
        this.scatter = new Map()
 | 
						|
        this.delegate = new InteractionMapper(element, this, {
 | 
						|
            useCapture,
 | 
						|
            capturePointerEvents,
 | 
						|
            mouseWheelElement: window
 | 
						|
        })
 | 
						|
 | 
						|
        if (debugCanvas !== null) {
 | 
						|
            requestAnimationFrame(dt => {
 | 
						|
                this.showTouches(dt, debugCanvas)
 | 
						|
            })
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    showTouches(dt, canvas) {
 | 
						|
        let resolution = window.devicePixelRatio
 | 
						|
        let current = this.delegate.interaction.current
 | 
						|
        let context = canvas.getContext('2d')
 | 
						|
        let radius = 20 * resolution
 | 
						|
        context.clearRect(0, 0, canvas.width, canvas.height)
 | 
						|
        context.fillStyle = 'rgba(0, 0, 0, 0.3)'
 | 
						|
        context.lineWidth = 2
 | 
						|
        context.strokeStyle = '#003300'
 | 
						|
        for (let [key, point] of current.entries()) {
 | 
						|
            let local = point
 | 
						|
            context.beginPath()
 | 
						|
            context.arc(local.x * resolution, local.y * resolution, radius, 0, 2 * Math.PI, false)
 | 
						|
            context.fill()
 | 
						|
            context.stroke()
 | 
						|
        }
 | 
						|
        requestAnimationFrame(dt => {
 | 
						|
            this.showTouches(dt, canvas)
 | 
						|
        })
 | 
						|
    }
 | 
						|
 | 
						|
    preventPinch(event) {
 | 
						|
        event = event.originalEvent || event
 | 
						|
        if (event.scale !== 1) {
 | 
						|
            event.preventDefault()
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Removes an element from the scatter.
 | 
						|
     *
 | 
						|
     * @param {Scatter} scatter - Element to remove.
 | 
						|
     * @memberof DOMScatterContainer
 | 
						|
     */
 | 
						|
    remove(scatter) {
 | 
						|
        this.scatter.delete(scatter.element)
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Adds an element to the ScatterContainer.
 | 
						|
     *
 | 
						|
     * @param {Scatter} scatter - Element to add to the ScatterContainer.
 | 
						|
     * @memberof DOMScatterContainer
 | 
						|
     */
 | 
						|
    add(scatter) {
 | 
						|
        this.scatter.set(scatter.element, scatter)
 | 
						|
    }
 | 
						|
 | 
						|
    capture(event) {
 | 
						|
        if (this.onCapture) {
 | 
						|
            return this.onCapture(event)
 | 
						|
        }
 | 
						|
        if (event.target == this.element && this.stopEvents) {
 | 
						|
            Events.stop(event)
 | 
						|
        }
 | 
						|
        return true
 | 
						|
    }
 | 
						|
 | 
						|
    mapPositionToPoint(point) {
 | 
						|
        return Points.fromPageToNode(this.element, point)
 | 
						|
    }
 | 
						|
 | 
						|
    isDescendant(parent, child, clickable = false) {
 | 
						|
        if (parent == child) return true
 | 
						|
        let node = child.parentNode
 | 
						|
        while (node != null) {
 | 
						|
            if (!clickable && node.onclick) {
 | 
						|
                return false
 | 
						|
            }
 | 
						|
            if (node == parent) {
 | 
						|
                return true
 | 
						|
            }
 | 
						|
            node = node.parentNode
 | 
						|
        }
 | 
						|
        return false
 | 
						|
    }
 | 
						|
 | 
						|
    findTarget(event, local, global) {
 | 
						|
        /*** Note that elementFromPoint works with clientX, clientY, not pageX, pageY
 | 
						|
        The important point is that event should not be used, since the TouchEvent
 | 
						|
        points are hidden in sub objects.
 | 
						|
        ***/
 | 
						|
        let found = document.elementFromPoint(global.x, global.y)
 | 
						|
        for (let target of this.scatter.values()) {
 | 
						|
            if (target.interactive && this.isDescendant(target.element, found)) {
 | 
						|
                if (this.stopEvents) Events.stop(event)
 | 
						|
                if (this.claimEvents) event.claimedByScatter = target
 | 
						|
                return target
 | 
						|
            }
 | 
						|
        }
 | 
						|
        return null
 | 
						|
    }
 | 
						|
 | 
						|
    get center() {
 | 
						|
        let r = this.bounds
 | 
						|
        let w2 = r.width / 2
 | 
						|
        let h2 = r.height / 2
 | 
						|
        return { x: w2, y: h2 }
 | 
						|
    }
 | 
						|
 | 
						|
    get bounds() {
 | 
						|
        return this.element.getBoundingClientRect()
 | 
						|
    }
 | 
						|
 | 
						|
    get polygon() {
 | 
						|
        let r = this.bounds
 | 
						|
        let w2 = r.width / 2
 | 
						|
        let h2 = r.height / 2
 | 
						|
        let center = { x: w2, y: h2 }
 | 
						|
        let polygon = new Polygon(center)
 | 
						|
        polygon.addPoint({ x: -w2, y: -h2 })
 | 
						|
        polygon.addPoint({ x: w2, y: -h2 })
 | 
						|
        polygon.addPoint({ x: w2, y: h2 })
 | 
						|
        polygon.addPoint({ x: -w2, y: h2 })
 | 
						|
        return polygon
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
export class DOMScatter extends AbstractScatter {
 | 
						|
    constructor(
 | 
						|
        element,
 | 
						|
        container,
 | 
						|
        {
 | 
						|
            startScale = 1.0,
 | 
						|
            minScale = 0.1,
 | 
						|
            maxScale = 1.0,
 | 
						|
            overdoScaling = 1.5,
 | 
						|
            autoBringToFront = true,
 | 
						|
            translatable = true,
 | 
						|
            scalable = true,
 | 
						|
            rotatable = true,
 | 
						|
            movableX = true,
 | 
						|
            movableY = true,
 | 
						|
            rotationDegrees = null,
 | 
						|
            rotation = null,
 | 
						|
            onTransform = null,
 | 
						|
            transformOrigin = 'center center',
 | 
						|
            // extras which are in part needed
 | 
						|
            x = 0,
 | 
						|
            y = 0,
 | 
						|
            width = null, // required
 | 
						|
            height = null, // required
 | 
						|
            resizable = false,
 | 
						|
            tapDelegate = null,
 | 
						|
            triggerSVGClicks = false,
 | 
						|
            allowClickDistance = 44,
 | 
						|
            verbose = true,
 | 
						|
            onResize = null,
 | 
						|
            touchAction = 'none',
 | 
						|
            throwVisibility = 44,
 | 
						|
            throwDamping = 0.95,
 | 
						|
            autoThrow = true,
 | 
						|
            scaleAutoClose = false,
 | 
						|
            onClose = null,
 | 
						|
            scaleCloseThreshold = 0.1,
 | 
						|
            scaleCloseBuffer = 0.05,
 | 
						|
            useLowPassFilter = false,
 | 
						|
            maxRotation = Angle.degree2radian(15),
 | 
						|
            minInteractionDistance = 200
 | 
						|
        } = {}
 | 
						|
    ) {
 | 
						|
        super({
 | 
						|
            minScale,
 | 
						|
            maxScale,
 | 
						|
            startScale,
 | 
						|
            overdoScaling,
 | 
						|
            autoBringToFront,
 | 
						|
            translatable,
 | 
						|
            scalable,
 | 
						|
            rotatable,
 | 
						|
            movableX,
 | 
						|
            movableY,
 | 
						|
            resizable,
 | 
						|
            rotationDegrees,
 | 
						|
            rotation,
 | 
						|
            onTransform,
 | 
						|
            throwVisibility,
 | 
						|
            throwDamping,
 | 
						|
            autoThrow,
 | 
						|
            scaleAutoClose,
 | 
						|
            scaleCloseThreshold,
 | 
						|
            scaleCloseBuffer,
 | 
						|
            onClose,
 | 
						|
            useLowPassFilter,
 | 
						|
            maxRotation,
 | 
						|
            minInteractionDistance
 | 
						|
        })
 | 
						|
        if (container == null || width == null || height == null) {
 | 
						|
            throw new Error('Invalid value: null')
 | 
						|
        }
 | 
						|
        element.scatter = this
 | 
						|
        this.element = element
 | 
						|
        this.x = x
 | 
						|
        this.y = y
 | 
						|
        this.oldX = 0
 | 
						|
        this.oldY = 0
 | 
						|
        this.meanX = x
 | 
						|
        this.meanY = y
 | 
						|
        this.width = width
 | 
						|
        this.height = height
 | 
						|
        this.throwVisibility = Math.min(width, height, throwVisibility)
 | 
						|
        this.container = container
 | 
						|
        this.tapDelegate = tapDelegate
 | 
						|
        this.scale = startScale
 | 
						|
        this.rotationDegrees = this.startRotationDegrees
 | 
						|
        this.transformOrigin = transformOrigin
 | 
						|
        this.initialValues = {
 | 
						|
            x: x,
 | 
						|
            y: y,
 | 
						|
            width: width,
 | 
						|
            height: height,
 | 
						|
            scale: startScale,
 | 
						|
            rotation: this.startRotationDegrees,
 | 
						|
            transformOrigin: transformOrigin
 | 
						|
        }
 | 
						|
        this.tapNodes = new Map()
 | 
						|
 | 
						|
        // For tweenlite we need initial values in _gsTransform
 | 
						|
        TweenLite.set(element, this.initialValues)
 | 
						|
        this.onResize = onResize
 | 
						|
        this.verbose = verbose
 | 
						|
        if (touchAction !== null) {
 | 
						|
            Elements.setStyle(element, { touchAction })
 | 
						|
        }
 | 
						|
        this.resizeButton = null
 | 
						|
        if (resizable) {
 | 
						|
            let button = document.createElement('div')
 | 
						|
            button.style.position = 'absolute'
 | 
						|
            button.style.right = '0px'
 | 
						|
            button.style.bottom = '0px'
 | 
						|
            button.style.width = '50px'
 | 
						|
            button.style.height = '50px'
 | 
						|
            button.className = 'interactiveElement'
 | 
						|
            this.element.appendChild(button)
 | 
						|
 | 
						|
            button.addEventListener('pointerdown', e => {
 | 
						|
                this.startResize(e)
 | 
						|
            })
 | 
						|
 | 
						|
            button.addEventListener('pointermove', e => {
 | 
						|
                this.resize(e)
 | 
						|
            })
 | 
						|
 | 
						|
            button.addEventListener('pointerup', e => {
 | 
						|
                this.stopResize(e)
 | 
						|
            })
 | 
						|
            this.resizeButton = button
 | 
						|
        }
 | 
						|
        if (tapDelegate) {
 | 
						|
            tapDelegate.handleClicks()
 | 
						|
        }
 | 
						|
        container.add(this)
 | 
						|
    }
 | 
						|
 | 
						|
    /** Returns geometry data as object. **/
 | 
						|
    getState() {
 | 
						|
        return {
 | 
						|
            scale: this.scale,
 | 
						|
            x: this.x,
 | 
						|
            y: this.y,
 | 
						|
            rotation: this.rotation
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    close() {
 | 
						|
        super.close()
 | 
						|
        let parent = this.element.parentNode
 | 
						|
        if (parent) parent.removeChild(this.element)
 | 
						|
    }
 | 
						|
 | 
						|
    get rotationOrigin() {
 | 
						|
        return this.center
 | 
						|
    }
 | 
						|
 | 
						|
    get x() {
 | 
						|
        return this._x
 | 
						|
    }
 | 
						|
 | 
						|
    get y() {
 | 
						|
        return this._y
 | 
						|
    }
 | 
						|
 | 
						|
    set x(value) {
 | 
						|
        this._x = value
 | 
						|
        TweenLite.set(this.element, { x: value })
 | 
						|
    }
 | 
						|
 | 
						|
    set y(value) {
 | 
						|
        this._y = value
 | 
						|
        TweenLite.set(this.element, { y: value })
 | 
						|
    }
 | 
						|
 | 
						|
    get position() {
 | 
						|
        let transform = this.element._gsTransform
 | 
						|
        let x = transform.x
 | 
						|
        let y = transform.y
 | 
						|
        return { x, y }
 | 
						|
    }
 | 
						|
 | 
						|
    get origin() {
 | 
						|
        let p = this.fromNodeToPage(0, 0)
 | 
						|
        return Points.fromPageToNode(this.container.element, p)
 | 
						|
    }
 | 
						|
 | 
						|
    get bounds() {
 | 
						|
        let stage = this.container.element.getBoundingClientRect()
 | 
						|
        let rect = this.element.getBoundingClientRect()
 | 
						|
        return {
 | 
						|
            top: rect.top - stage.top,
 | 
						|
            left: rect.left - stage.left,
 | 
						|
            width: rect.width,
 | 
						|
            height: rect.height
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    get center() {
 | 
						|
        let r = this.bounds
 | 
						|
        let w2 = r.width / 2
 | 
						|
        let h2 = r.height / 2
 | 
						|
        //   if (this.resizable) {
 | 
						|
        //             w2 *= this.scale
 | 
						|
        //             h2 *= this.scale
 | 
						|
        //         }
 | 
						|
        var x = r.left + w2
 | 
						|
        var y = r.top + h2
 | 
						|
        return { x, y }
 | 
						|
    }
 | 
						|
 | 
						|
    set rotation(radians) {
 | 
						|
        let rad = radians // Angle.normalize(radians)
 | 
						|
        let degrees = Angle.radian2degree(rad)
 | 
						|
        TweenLite.set(this.element, { rotation: degrees })
 | 
						|
        this._rotation = rad
 | 
						|
    }
 | 
						|
 | 
						|
    set rotationDegrees(degrees) {
 | 
						|
        let deg = degrees // Angle.normalizeDegree(degrees)
 | 
						|
        TweenLite.set(this.element, { rotation: deg })
 | 
						|
        this._rotation = Angle.degree2radian(deg)
 | 
						|
    }
 | 
						|
 | 
						|
    get rotation() {
 | 
						|
        return this._rotation
 | 
						|
    }
 | 
						|
 | 
						|
    get rotationDegrees() {
 | 
						|
        return this._rotation
 | 
						|
    }
 | 
						|
 | 
						|
    set scale(scale) {
 | 
						|
        TweenLite.set(this.element, {
 | 
						|
            scale: scale,
 | 
						|
            transformOrigin: this.transformOrigin
 | 
						|
        })
 | 
						|
        this._scale = scale
 | 
						|
    }
 | 
						|
 | 
						|
    get scale() {
 | 
						|
        return this._scale
 | 
						|
    }
 | 
						|
 | 
						|
    get containerBounds() {
 | 
						|
        return this.container.bounds
 | 
						|
    }
 | 
						|
 | 
						|
    get containerPolygon() {
 | 
						|
        return this.container.polygon
 | 
						|
    }
 | 
						|
 | 
						|
    mapPositionToContainerPoint(point) {
 | 
						|
        return this.container.mapPositionToPoint(point)
 | 
						|
    }
 | 
						|
 | 
						|
    capture(event) {
 | 
						|
        return true
 | 
						|
    }
 | 
						|
 | 
						|
    reset() {
 | 
						|
        TweenLite.set(this.element, this.initialValues)
 | 
						|
    }
 | 
						|
 | 
						|
    hide() {
 | 
						|
        TweenLite.to(this.element, 0.1, {
 | 
						|
            display: 'none',
 | 
						|
            onComplete: e => {
 | 
						|
                this.element.parentNode.removeChild(this.element)
 | 
						|
            }
 | 
						|
        })
 | 
						|
    }
 | 
						|
 | 
						|
    show() {
 | 
						|
        TweenLite.set(this.element, { display: 'block' })
 | 
						|
    }
 | 
						|
 | 
						|
    showAt(p, rotationDegrees) {
 | 
						|
        TweenLite.set(this.element, {
 | 
						|
            display: 'block',
 | 
						|
            x: p.x,
 | 
						|
            y: p.y,
 | 
						|
            rotation: rotationDegrees,
 | 
						|
            transformOrigin: this.transformOrigin
 | 
						|
        })
 | 
						|
    }
 | 
						|
 | 
						|
    bringToFront() {
 | 
						|
        // this.element.parentNode.appendChild(this.element)
 | 
						|
        // uo: On Chome and Electon appendChild leads to flicker
 | 
						|
        TweenLite.set(this.element, { zIndex: DOMScatter.zIndex++ })
 | 
						|
    }
 | 
						|
 | 
						|
    isDescendant(parent, child) {
 | 
						|
        let node = child.parentNode
 | 
						|
        while (node != null) {
 | 
						|
            if (node == parent) {
 | 
						|
                return true
 | 
						|
            }
 | 
						|
            node = node.parentNode
 | 
						|
        }
 | 
						|
        return false
 | 
						|
    }
 | 
						|
 | 
						|
    fromPageToNode(x, y) {
 | 
						|
        return Points.fromPageToNode(this.element, { x, y })
 | 
						|
    }
 | 
						|
 | 
						|
    fromNodeToPage(x, y) {
 | 
						|
        return Points.fromNodeToPage(this.element, { x, y })
 | 
						|
    }
 | 
						|
 | 
						|
    _move(delta) {
 | 
						|
        // UO: We need to keep TweenLite's _gsTransform and the private
 | 
						|
        // _x and _y attributes aligned
 | 
						|
        let x = this.element._gsTransform.x
 | 
						|
        let y = this.element._gsTransform.y
 | 
						|
        if (this.movableX) {
 | 
						|
            x += delta.x
 | 
						|
        }
 | 
						|
        if (this.movableY) {
 | 
						|
            y += delta.y
 | 
						|
        }
 | 
						|
        this._x = x
 | 
						|
        this._y = y
 | 
						|
        // this.addVelocity({ x: delta.x, y: delta.y }) uo: reverted commit fa0256d782dd498c6d3e51321260ca375ca9f855
 | 
						|
        TweenLite.set(this.element, { x, y })
 | 
						|
    }
 | 
						|
 | 
						|
    resizeAfterTransform(zoom) {
 | 
						|
        if (this.onResize) {
 | 
						|
            let w = this.width * this.scale
 | 
						|
            let h = this.height * this.scale
 | 
						|
            let event = new ResizeEvent(this, { width: w, height: h })
 | 
						|
            this.onResize(event)
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    startResize(e) {
 | 
						|
        e.preventDefault()
 | 
						|
        let event = new CustomEvent('resizeStarted')
 | 
						|
 | 
						|
        let oldPostition = {
 | 
						|
            x: this.element.getBoundingClientRect().left,
 | 
						|
            y: this.element.getBoundingClientRect().top
 | 
						|
        }
 | 
						|
        this.bringToFront()
 | 
						|
 | 
						|
        this.element.style.transformOrigin = '0% 0%'
 | 
						|
 | 
						|
        let newPostition = {
 | 
						|
            x: this.element.getBoundingClientRect().left,
 | 
						|
            y: this.element.getBoundingClientRect().top
 | 
						|
        }
 | 
						|
 | 
						|
        let offset = Points.subtract(oldPostition, newPostition)
 | 
						|
 | 
						|
        this.oldX = e.clientX
 | 
						|
        this.oldY = e.clientY
 | 
						|
 | 
						|
        e.target.setAttribute('resizing', 'true')
 | 
						|
        e.target.setPointerCapture(e.pointerId)
 | 
						|
 | 
						|
        TweenLite.to(this.element, 0, { css: { left: '+=' + offset.x + 'px' } })
 | 
						|
        TweenLite.to(this.element, 0, { css: { top: '+=' + offset.y + 'px' } })
 | 
						|
 | 
						|
        this.element.dispatchEvent(event)
 | 
						|
    }
 | 
						|
 | 
						|
    resize(e) {
 | 
						|
        e.preventDefault()
 | 
						|
 | 
						|
        let rotation = Angle.radian2degree(this.rotation)
 | 
						|
        rotation = (rotation + 360) % 360
 | 
						|
        let event = new CustomEvent('resized')
 | 
						|
        if (e.target.getAttribute('resizing') == 'true') {
 | 
						|
            let deltaX = e.clientX - this.oldX
 | 
						|
            let deltaY = e.clientY - this.oldY
 | 
						|
 | 
						|
            let r = Math.sqrt(Math.pow(deltaX, 2) + Math.pow(deltaY, 2))
 | 
						|
            let phi = Angle.radian2degree(Math.atan2(deltaX, deltaY))
 | 
						|
 | 
						|
            phi = (phi + 630) % 360
 | 
						|
            let rot = (rotation + 90 + 630) % 360
 | 
						|
 | 
						|
            let diffAngle = (0 + rot + 360) % 360
 | 
						|
            let phiCorrected = (phi + diffAngle + 360) % 360
 | 
						|
 | 
						|
            let resizeW = r * Math.cos(Angle.degree2radian(phiCorrected))
 | 
						|
            let resizeH = -r * Math.sin(Angle.degree2radian(phiCorrected))
 | 
						|
 | 
						|
            if (
 | 
						|
                (this.element.offsetWidth + resizeW) / this.scale > (this.width * 0.5) / this.scale &&
 | 
						|
                (this.element.offsetHeight + resizeH) / this.scale > (this.height * 0.3) / this.scale
 | 
						|
            )
 | 
						|
                TweenLite.to(this.element, 0, {
 | 
						|
                    width: this.element.offsetWidth + resizeW / this.scale,
 | 
						|
                    height: this.element.offsetHeight + resizeH / this.scale
 | 
						|
                })
 | 
						|
 | 
						|
            this.oldX = e.clientX
 | 
						|
            this.oldY = e.clientY
 | 
						|
            this.onResizing()
 | 
						|
 | 
						|
            this.element.dispatchEvent(event)
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    stopResize(e) {
 | 
						|
        e.preventDefault()
 | 
						|
 | 
						|
        let event = new CustomEvent('resizeEnded')
 | 
						|
        let oldPostition = {
 | 
						|
            x: this.element.getBoundingClientRect().left,
 | 
						|
            y: this.element.getBoundingClientRect().top
 | 
						|
        }
 | 
						|
        this.element.style.transformOrigin = '50% 50%'
 | 
						|
        let newPostition = {
 | 
						|
            x: this.element.getBoundingClientRect().left,
 | 
						|
            y: this.element.getBoundingClientRect().top
 | 
						|
        }
 | 
						|
        let offset = Points.subtract(oldPostition, newPostition)
 | 
						|
 | 
						|
        TweenLite.to(this.element, 0, { css: { left: '+=' + offset.x + 'px' } })
 | 
						|
        TweenLite.to(this.element, 0, { css: { top: '+=' + offset.y + 'px' } })
 | 
						|
 | 
						|
        e.target.setAttribute('resizing', 'false')
 | 
						|
 | 
						|
        this.element.dispatchEvent(event)
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
DOMScatter.zIndex = 1000
 |