From 4f35bfd51fd45f5db2419bb6470b03b0ed393118 Mon Sep 17 00:00:00 2001 From: Uwe Oestermeier Date: Fri, 5 Jul 2019 09:17:52 +0200 Subject: [PATCH] Fixed eslint problems and cleaned DOMScatter.onTap code --- dist/iwmlib.js | 4417 +++++++++++++++++++++---------------------- dist/iwmlib.pixi.js | 202 +- lib/interaction.js | 87 +- lib/scatter.js | 138 +- 4 files changed, 2359 insertions(+), 2485 deletions(-) diff --git a/dist/iwmlib.js b/dist/iwmlib.js index db06854..50a3b26 100644 --- a/dist/iwmlib.js +++ b/dist/iwmlib.js @@ -1614,7 +1614,7 @@ } } - /* globals Hammer, propagating */ + /* eslint-disable no-unused-vars */ /** Interaction patterns @@ -1622,6 +1622,7 @@ */ class IInteractionTarget extends Interface { + capture(event) { return typeof true } @@ -1825,10 +1826,10 @@ let d1 = Points.subtract(c1, p1); let d2 = Points.subtract(c2, p2); let cm = Points.mean(c1, c2); - + // Using the mean leads to jumps between time slices with 3 and 2 fingers // We use the mean of deltas instead - let delta = Points.mean(d1, d2); + let delta = Points.mean(d1, d2); let zoom = 1.0; let distance1 = Points.distance(p1, p2); let distance2 = Points.distance(c1, c2); @@ -2032,7 +2033,6 @@ } let result = false; if (this.isTap(key)) { - this.registerTap(key, ended); result = this.tapCounts.get(key) == 2; } @@ -2146,7 +2146,9 @@ if (this.capturePointerEvents) { try { element.setPointerCapture(e.pointerId); - } catch (e) { } + } catch (e) { + console.warn('Cannot setPointerCapture'); + } } this.onStart(e); } @@ -2178,7 +2180,9 @@ if (this.capturePointerEvents) { try { element.releasePointerCapture(e.pointerId); - } catch (e) { } + } catch (e) { + console.warn('Cannot release pointer'); + } } }, useCapture @@ -2222,7 +2226,7 @@ e => { if (this.debug) console.log('pointerout', e.pointerId, e.pointerType, e.target); if (e.target == element) { - this.onEnd(e); + this.onEnd(e); } }, useCapture); @@ -2325,9 +2329,8 @@ e => { if (e.target == element) { this.onEnd(e); - console.warn("Shouldn't happen: mouseout ends interaction"); + console.warn('Shouldn\'t happen: mouseout ends interaction'); } - }, useCapture ); @@ -2427,39 +2430,42 @@ // 'targetTouches' let result = {}; switch (event.constructor.name) { - case 'MouseEvent': - let buttons = event.buttons || event.which; - if (buttons) result['mouse'] = this.getPosition(event); - break - case 'PointerEvent': - result[event.pointerId.toString()] = this.getPosition(event); - break - case 'Touch': - let id = + case 'MouseEvent': { + let buttons = event.buttons || event.which; + if (buttons) result['mouse'] = this.getPosition(event); + break + } + case 'PointerEvent': { + result[event.pointerId.toString()] = this.getPosition(event); + break + } + case 'Touch': { + let id = event.touchType === 'stylus' ? 'stylus' : event.identifier.toString(); - result[id] = this.getPosition(event); - break - // case 'TouchEvent': - // // Needs to be observed: Perhaps changedTouches are all we need. If so - // // we can remove the touchEventKey default parameter - // if (touchEventKey == 'all') { - // for(let t of event.targetTouches) { - // result[t.identifier.toString()] = this.getPosition(t) - // } - // for(let t of event.changedTouches) { - // result[t.identifier.toString()] = this.getPosition(t) - // } - // } - // else { - // for(let t of event.changedTouches) { - // result[t.identifier.toString()] = this.getPosition(t) - // } - // } - // break - default: - break + result[id] = this.getPosition(event); + break + } + // case 'TouchEvent': + // // Needs to be observed: Perhaps changedTouches are all we need. If so + // // we can remove the touchEventKey default parameter + // if (touchEventKey == 'all') { + // for(let t of event.targetTouches) { + // result[t.identifier.toString()] = this.getPosition(t) + // } + // for(let t of event.changedTouches) { + // result[t.identifier.toString()] = this.getPosition(t) + // } + // } + // else { + // for(let t of event.changedTouches) { + // result[t.identifier.toString()] = this.getPosition(t) + // } + // } + // break + default: + break } return result } @@ -2487,7 +2493,7 @@ let point = extracted[key]; let updated = this.interaction.update(key, point); if (updated) { - console.warn("new pointer in updateInteraction shouldn't happen", key); + console.warn('new pointer in updateInteraction shouldn\'t happen', key); this.interactionStarted(event, key, point); } } @@ -2872,2193 +2878,6 @@ window.Capabilities = Capabilities; window.CapabilitiesTests = CapabilitiesTests; - /** - * A base class for scatter specific events. - * - * @constructor - * @param {name} String - The name of the event - * @param {target} Object - The target of the event - */ - 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 - */ - 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 - */ - 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 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.timestamp = null; - this.onThrowFinished = onThrowFinished; - //console.log("onThrowFinished", onThrowFinished) - } - - observeVelocity() { - this.lastframe = performance.now(); - } - - addVelocity(delta, buffer = 5) { - let t = performance.now(); - let dt = t - this.lastframe; - this.lastframe = t; - 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 / delta.number, dy: delta.y / delta.number}; - this.velocities.push(velocity); - while (this.velocities.length > buffer) { - this.velocities.shift(); - } - } - } - - 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 = []; - } - - 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 - } - - 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()) { - requestAnimationFrame(this.animateThrow.bind(this)); - return - } else { - if (this.isOutside()) { - requestAnimationFrame(this.animateThrow.bind(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 - } - } - - 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.10, - scaleCloseBuffer = 0.05, - maxRotation = Angle.degree2radian(5) - } = {}) { - 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.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(); - return true - } - - close() { - if (this.onClose) { - this.onClose.forEach(callback => callback(this)); - } - } - - gesture(interaction) { - let delta = interaction.delta(); - if (delta != null) { - this.addVelocity(delta); - let alpha = delta.rotate; - if (this.maxRotation != null) { - if (Math.abs(alpha) > this.maxRotation) { - console.log("limited rotation"); - alpha = 0; - } - } - this.transform(delta, delta.zoom, alpha, delta.about); - if (delta.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.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); - let offset = Points.subtract(anchor, origin); - this._move(offset); - this.scale = newScale; - this.rotation += rotate; - offset = Points.negate(offset); - offset = Points.add(offset, extra); - offset = Points.add(offset, translate); - this._move(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(); - } - // - // if (this.onTransform != null) { - // let event = new ScatterEvent(this, { - // translate: {x: 0, y: 0}, - // scale: this.scale, - // rotate: 0, - // about: null, - // fast: false, - // type: ZOOM - // }) - // this.onTransform.forEach(function(f) { - // f(event) - // }) - // } - } - - 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)) { - 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) { } - - 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. - */ - 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. - */ - constructor( - element, - { stopEvents = 'auto', claimEvents = true, useCapture = true, touchAction = 'none' } = {} - ) { - this.onCapture = null; - this.element = element; - if (stopEvents === 'auto') { - 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$1.setStyle(element, { touchAction }); - } - this.scatter = new Map(); - this.delegate = new InteractionMapper$1(element, this, { - useCapture, - mouseWheelElement: window - }); - - if (typeof debugCanvas !== 'undefined') { - requestAnimationFrame(dt => { - this.showTouches(dt); - }); - } - } - - showTouches(dt) { - let resolution = window.devicePixelRatio; - let canvas = debugCanvas; - 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); - }); - } - - preventPinch(event) { - event = event.originalEvent || event; - if (event.scale !== 1) { - event.preventDefault(); - } - } - - 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 - } - } - - - 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, - simulateClick = false, - verbose = true, - onResize = null, - touchAction = 'none', - throwVisibility = 44, - throwDamping = 0.95, - autoThrow = true, - scaleAutoClose = false, - onClose = null, - scaleCloseThreshold = 0.10, - scaleCloseBuffer = 0.05 - } = {} - ) { - super({ - minScale, - maxScale, - startScale, - overdoScaling, - autoBringToFront, - translatable, - scalable, - rotatable, - movableX, - movableY, - resizable, - rotationDegrees, - rotation, - onTransform, - throwVisibility, - throwDamping, - autoThrow, - scaleAutoClose, - scaleCloseThreshold, - scaleCloseBuffer, - onClose - }); - 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.simulateClick = simulateClick; - 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 - }; - - - // For tweenlite we need initial values in _gsTransform - TweenLite.set(element, this.initialValues); - this.onResize = onResize; - this.verbose = verbose; - if (touchAction !== null) { - Elements$1.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.style.borderRadius = "100% 0px 0px 0px"; - // button.style.background = this.element.style.backgroundColor - 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; - } - 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++ }); - } - - toggleVideo(element) { - if (element.paused) { - element.play(); - } else { - element.pause(); - } - } - - onTap(event, interaction, point) { - if (this.simulateClick) { - let p = Points.fromPageToNode(this.element, point); - let iframe = this.element.querySelector('iframe'); - if (iframe) { - let doc = iframe.contentWindow.document; - let element = doc.elementFromPoint(p.x, p.y); - if (element == null) { - return - } - switch (element.tagName) { - case 'VIDEO': - console.log(element.currentSrc); - if (PopupMenu) { - PopupMenu.open( - { - Fullscreen: () => - window.open(element.currentSrc), - Play: () => element.play() - }, - { x, y } - ); - } else { - this.toggleVideo(element); - } - break - default: - element.click(); - } - } - } - } - - 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; - TweenLite.set(this.element, { x: x, y: y }); - } - - resizeAfterTransform(zoom) { - // let w = this.width * this.scale - // let h = this.height * this.scale - // TweenLite.set(this.element, { width: w, height: h }) - if (this.onResize) { - let event = new ResizeEvent(this, { width: w, height: h }); - this.onResize(event); - } - if (this.resizeButton != null) ; - } - - 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; - - class CardLoader { - constructor( - src, - { - x = 0, - y = 0, - width = 1000, - height = 800, - maxWidth = null, - maxHeight = null, - scale = 1, - minScale = 0.5, - maxScale = 1.5, - rotation = 0 - } = {} - ) { - this.src = src; - this.x = x; - this.y = y; - this.scale = scale; - this.rotation = 0; - this.maxScale = maxScale; - this.minScale = minScale; - this.wantedWidth = width; - this.wantedHeight = height; - this.maxWidth = maxWidth != null ? maxWidth : window.innerWidth; - this.maxHeight = maxHeight != null ? maxHeight : window.innerHeight; - this.addedNode = null; - console.log({ - - width, - height, - maxWidth, - maxHeight, - - }); - } - - unload() { - if (this.addedNode) { - this.addedNode.remove(); - this.addedNode = null; - } - } - } - - class PDFLoader extends CardLoader { - constructor(src, { width = 1640, height = 800, scale = 1 } = {}) { - super(src, { width, height, scale }); - if (typeof PDFJS == 'undefined') { - alert('PDF.js needed'); - } - } - - load(domNode) { - return new Promise((resolve, reject) => { - PDFJS.getDocument(this.src).then(pdf => { - pdf.getPage(1).then(page => { - let scale = this.scale * app.renderer.resolution; - let invScale = 1 / scale; - let viewport = page.getViewport(scale); - - // Prepare canvas using PDF page dimensions. - let canvas = document.createElement('canvas'); - let context = canvas.getContext('2d'); - canvas.height = viewport.height; - canvas.width = viewport.width; - - // Render PDF page into canvas context. - let renderContext = { - canvasContext: context, - viewport: viewport - }; - page.render(renderContext); - domNode.appendChild(canvas); - this.wantedWidth = canvas.width; - this.wantedHeight = canvas.height; - this.scale = invScale; - this.addedNode = canvas; - resolve(this); - }); - }); - }) - } - } - - class ImageLoader extends CardLoader { - load(domNode) { - return new Promise((resolve, reject) => { - let isImage = domNode instanceof HTMLImageElement; - let image = isImage ? domNode : document.createElement('img'); - image.onload = e => { - if (!isImage) { - domNode.appendChild(image); - this.addedNode = image; - } - this.wantedWidth = image.naturalWidth; - this.wantedHeight = image.naturalHeight; - - let scaleW = this.maxWidth / image.naturalWidth; - let scaleH = this.maxHeight / image.naturalHeight; - this.scale = Math.min(this.maxScale, Math.min(scaleW, scaleH)); - image.setAttribute('draggable', false); - image.width = image.naturalWidth; - image.height = image.naturalHeight; - resolve(this); - }; - image.onerror = e => { - reject(this); - }; - image.src = this.src; - }) - } - } - - class FrameLoader extends CardLoader { - load(domNode) { - return new Promise((resolve, reject) => { - let isFrame = domNode instanceof HTMLIFrameElement; - let iframe = isFrame ? domNode : document.createElement('iframe'); - console.log('FrameLoader.load', isFrame, iframe, this.src); - iframe.frameBorder = 0; - iframe.style.scrolling = false; - iframe.width = this.wantedWidth; - iframe.height = this.wantedHeight; - if (!isFrame) { - // Unlike img onload is only triggered if the iframe is part of the DOM tree - domNode.appendChild(iframe); - this.addedNode = iframe; - } - iframe.onload = e => { - resolve(this); - }; - iframe.onerror = e => { - reject(this); - }; - iframe.src = this.src; - }) - } - } - - class HTMLLoader extends CardLoader { - load(domNode) { - return new Promise((resolve, reject) => { - let xhr = new XMLHttpRequest(); - xhr.open('GET', this.src, false); - xhr.onload = e => { - domNode.innerHTML = xhr.response; - this.addedNode = domNode.firstElementChild; - let { width, height } = this.size(this.addedNode); - console.log("HTMLLoader.load", { added: this.addedNode, width, height }); - if (width) - this.wantedWidth = width || this.wantedWidth; - if (height) - this.wantedHeight = height || this.wantedHeight; - resolve(this); - }; - xhr.onerror = e => { - reject(this); - }; - xhr.send(); - }) - } - - /** - * Tries to determine the size of the addedNode. - * Checks for explicit width and height style attributes. - * - * Overwrite this method if you want to extract values from other infos. - * - * @returns { width: int, height: int } - * @memberof HTMLLoader - */ - size(node) { - let width = parseInt(node.style.width) || null; - let height = parseInt(node.style.height) || null; - return { width, height } - } - } - - class DOMFlip { - constructor( - domScatterContainer, - flipTemplate, - frontLoader, - backLoader, - { - closeOnMinScale = false, - flipDuration = 1, - fadeDuration = 0.2, - overdoScaling = 1, - autoLoad = false, - center = null, - preloadBack = false, - translatable = true, - scalable = true, - rotatable = true, - onFront = null, - onBack = null, - onClose = null, - onUpdate = null, - onRemoved = null, - onLoaded = null - } = {} - ) { - this.domScatterContainer = domScatterContainer; - this.id = getId$1(); - this.flipDuration = flipDuration; - this.fadeDuration = fadeDuration; - this.closeOnMinScale = closeOnMinScale; - this.flipTemplate = flipTemplate; - this.frontLoader = frontLoader; - this.backLoader = backLoader; - this.translatable = translatable; - this.scalable = scalable; - this.rotatable = rotatable; - this.onFrontFlipped = onFront; - this.onBackFlipped = onBack; - this.onClose = onClose; - this.onRemoved = onRemoved; - this.onUpdate = onUpdate; - this.onLoaded = onLoaded; - this.center = center; - this.preloadBack = preloadBack; - this.overdoScaling = overdoScaling; - if (autoLoad) { - this.load(); - } - } - - load() { - return new Promise((resolve, reject) => { - let t = this.flipTemplate; - let dom = this.domScatterContainer.element; - let wrapper = t.content.querySelector('.flipWrapper'); - wrapper.id = this.id; - let clone = document.importNode(t.content, true); - dom.appendChild(clone); - // We cannot use the document fragment itself because it - // is not part of the main dom tree. After the appendChild - // call we can access the new dom element by id - this.cardWrapper = dom.querySelector('#' + this.id); - let front = this.cardWrapper.querySelector('.front'); - this.frontLoader.load(front).then(loader => { - this.frontLoaded(loader).then((obj) => { - if (this.onLoaded) this.onLoaded(); - resolve(this); - }); - }); - }) - } - - frontLoaded(loader) { - return new Promise((resolve, reject) => { - let scatter = new DOMScatter( - this.cardWrapper, - this.domScatterContainer, - { - x: loader.x, - y: loader.y, - startScale: loader.scale, - scale: loader.scale, - maxScale: loader.maxScale, - minScale: loader.minScale, - width: loader.wantedWidth, - height: loader.wantedHeight, - rotation: loader.rotation, - translatable: this.translatable, - scalable: this.scalable, - rotatable: this.rotatable, - overdoScaling: this.overdoScaling - } - ); - - if (this.center) { - scatter.centerAt(this.center); - } - - if (this.closeOnMinScale) { - - const removeOnMinScale = function () { - if (scatter.scale <= scatter.minScale) { - this.flippable.close(); - - // 'Disable' overdoscaling to avoid weird jumps on close. - scatter.minScale /= scatter.overdoScaling; - scatter.overdoScaling = 1; - - //Remove callback - if (scatter.onTransform) { - let callbackIdx = scatter.onTransform.indexOf(removeOnMinScale); - scatter.onTransform.splice(callbackIdx, 1); - } - } - - }.bind(this); - - - - scatter.addTransformEventCallback(removeOnMinScale); - } - - let flippable = new DOMFlippable(this.cardWrapper, scatter, this); - let back = this.cardWrapper.querySelector('.back'); - - if (this.preloadBack) { - this.backLoader.load(back).then(loader => { - this.setupFlippable(flippable, loader); - }); - } - this.flippable = flippable; - resolve(this); - }) - } - - centerAt(p) { - this.center = p; - this.flippable.centerAt(p); - } - - zoom(scale) { - this.flippable.zoom(scale); - } - - setupFlippable(flippable, loader) { - flippable.wantedWidth = loader.wantedWidth; - flippable.wantedHeight = loader.wantedHeight; - flippable.wantedScale = loader.scale; - flippable.minScale = loader.minScale; - flippable.maxScale = loader.maxScale; - flippable.scaleButtons(); - } - - start({ targetCenter = null } = {}) { - console.log('DOMFlip.start', targetCenter); - if (this.preloadBack) this.flippable.start({ duration: this.flipDuration, targetCenter }); - else { - let back = this.cardWrapper.querySelector('.back'); - let flippable = this.flippable; - this.backLoader.load(back).then(loader => { - this.setupFlippable(flippable, loader); - flippable.start({ duration: this.flipDuration, targetCenter }); - }); - } - } - - fadeOutAndRemove() { - TweenLite.to(this.cardWrapper, this.fadeDuration, { - opacity: 0, - onComplete: () => { - this.cardWrapper.remove(); - } - }); - } - - closed() { - this.unload(); - } - - unload() { - if (!this.preloadBack) { - this.backLoader.unload(); - } - } - } - - class DOMFlippable { - constructor(element, scatter, flip) { - // Set log to console.log or a custom log function - // define data structures to store our touchpoints in - - this.element = element; - this.flip = flip; - this.card = element.querySelector('.flipCard'); - this.front = element.querySelector('.front'); - this.back = element.querySelector('.back'); - this.flipped = false; - this.scatter = scatter; - this.onFrontFlipped = flip.onFrontFlipped; - this.onBackFlipped = flip.onBackFlipped; - this.onClose = flip.onClose; - this.onRemoved = flip.onRemoved; - this.onUpdate = flip.onUpdate; - - this.flipDuration = flip.flipDuration; - this.fadeDuration = flip.fadeDuration; - scatter.addTransformEventCallback(this.scatterTransformed.bind(this)); - console.log('lib.DOMFlippable', 5000); - TweenLite.set(this.element, { perspective: 5000 }); - TweenLite.set(this.card, { transformStyle: 'preserve-3d' }); - TweenLite.set(this.back, { rotationY: -180 }); - TweenLite.set([this.back, this.front], { - backfaceVisibility: 'hidden', - perspective: 5000 - }); - TweenLite.set(this.front, { visibility: 'visible' }); - this.infoBtn = element.querySelector('.infoBtn'); - this.backBtn = element.querySelector('.backBtn'); - this.closeBtn = element.querySelector('.closeBtn'); - /* Buttons are not guaranteed to exist. */ - if (this.infoBtn) { - InteractionMapper$1.on('tap', this.infoBtn, event => this.flip.start()); - - this.enable(this.infoBtn); - } - if (this.backBtn) { - InteractionMapper$1.on('tap', this.backBtn, event => this.start()); - } - if (this.closeBtn) { - InteractionMapper$1.on('tap', this.closeBtn, event => this.close()); - this.enable(this.closeBtn); - } - this.scaleButtons(); - this.bringToFront(); - } - - close() { - this.disable(this.infoBtn); - this.disable(this.closeBtn); - if (this.onClose) { - this.onClose(this); - this.flip.closed(); - } else { - this.scatter.zoom(0.1, { - animate: this.fadeDuration, - onComplete: () => { - this.element.remove(); - this.flip.closed(); - if (this.onRemoved) { - this.onRemoved.call(this); - } - } - }); - } - } - - showFront() { - TweenLite.set(this.front, { visibility: 'visible' }); - } - - centerAt(p) { - this.scatter.centerAt(p); - } - - zoom(scale) { - this.scatter.zoom(scale); - } - - get buttonScale() { - let iscale = 1.0; - - if (this.scatter != null) { - let scale = this.scatter.scale || 1; - iscale = 1.0 / scale; - } - return iscale - } - - scaleButtons() { - //This also works for svgs. - // if (this.infoBtn) - // this.infoBtn.style.transform = "scale(" + this.buttonScale + ")" - - // if (this.backBtn) - // this.backBtn.style.transform = "scale(" + this.buttonScale + ")" - - // if (this.closeBtn) - // this.closeBtn.style.transform = "scale(" + this.buttonScale + ")" - - console.log(this.buttonScale); - //// This did not work with svgs! - TweenLite.set([this.infoBtn, this.backBtn, this.closeBtn], { - scale: this.buttonScale - }); - } - - bringToFront() { - this.scatter.bringToFront(); - TweenLite.set(this.element, { zIndex: DOMScatter.zIndex++ }); - } - - clickInfo() { - this.bringToFront(); - this.infoBtn.click(); - } - - scatterTransformed(event) { - this.scaleButtons(); - } - - targetRotation(alpha) { - let ortho = 90; - let rest = alpha % ortho; - let delta = 0.0; - if (rest > ortho / 2.0) { - delta = ortho - rest; - } else { - delta = -rest; - } - return delta - } - - infoValues(info) { - let startX = this.element._gsTransform.x; - let startY = this.element._gsTransform.y; - let startAngle = this.element._gsTransform.rotation; - let startScale = this.element._gsTransform.scaleX; - let w = this.element.style.width; - let h = this.element.style.height; - console.log(info, startX, startY, startAngle, startScale, w, h); - } - - show(element, duration = 0, alpha = 1) { - if (element) { - TweenLite.to(element, duration, { autoAlpha: alpha }); // visibility: 'visible', display: 'initial'}) - } - } - - hide(element, duration = 0, alpha = 0) { - if (element) { - TweenLite.to(element, duration, { autoAlpha: alpha }); // {visibility: 'hidden', display: 'none'}) - } - } - - - - enable(button) { - this.show(button, this.fadeDuration); - if (button) { - TweenLite.set(button, { pointerEvents: 'auto' }); - } - } - - disable(button) { - this.hide(button, this.fadeDuration); - if (button) { - TweenLite.set(button, { pointerEvents: 'none' }); - } - } - - start({ targetCenter = null } = {}) { - this.bringToFront(); - if (!this.flipped) { - this.startX = this.element._gsTransform.x; - this.startY = this.element._gsTransform.y; - this.startAngle = this.element._gsTransform.rotation; - this.startScale = this.element._gsTransform.scaleX; - this.startWidth = this.element.style.width; - this.startHeight = this.element.style.height; - this.scatterStartWidth = this.scatter.width; - this.scatterStartHeight = this.scatter.height; - this.show(this.back); - this.disable(this.infoBtn); - this.disable(this.closeBtn); - } else { - this.show(this.front, this.fadeDuration); - this.disable(this.backBtn); - } - let { scalable, translatable, rotatable } = this.scatter; - this.saved = { scalable, translatable, rotatable }; - this.scatter.scalable = false; - this.scatter.translatable = false; - this.scatter.rotatable = false; - this.scatter.killAnimation(); - - this.flipped = !this.flipped; - let targetY = this.flipped ? 180 : 0; - let targetZ = this.flipped - ? this.startAngle + this.targetRotation(this.startAngle) - : this.startAngle; - let targetScale = this.flipped ? this.wantedScale : this.startScale; - let w = this.flipped ? this.wantedWidth : this.startWidth; - let h = this.flipped ? this.wantedHeight : this.startHeight; - let dw = this.wantedWidth - this.scatter.width; - let dh = this.wantedHeight - this.scatter.height; - let tc = targetCenter; - let xx = tc != null ? tc.x - w / 2 : this.startX - dw / 2; - let yy = tc != null ? tc.y - h / 2 : this.startY - dh / 2; - let x = this.flipped ? xx : this.startX; - let y = this.flipped ? yy : this.startY; - - console.log("DOMFlippable.start", this.flipped, targetCenter, x, y, this.saved); - let onUpdate = this.onUpdate !== null ? () => this.onUpdate(this) : null; - console.log(this.flipDuration); - TweenLite.to(this.card, this.flipDuration, { - rotationY: targetY, - ease: Power1.easeOut, - transformOrigin: '50% 50%', - onUpdate, - onComplete: e => { - if (this.flipped) { - //this.hide(this.front) - this.enable(this.backBtn); - this.show(this.backBtn); - - if (this.onFrontFlipped) { - this.onFrontFlipped(this); - } - } else { - - if (this.onBackFlipped == null) { - this.enable(this.infoBtn, this.fadeDuration); - this.enable(this.closeBtn, this.fadeDuration); - } else { - this.onBackFlipped(this); - } - this.flip.unload(); - } - this.scatter.scale = targetScale; - this.scaleButtons(); - this.scatter.rotationDegrees = targetZ; - this.scatter.width = this.flipped ? w : this.scatterStartWidth; - this.scatter.height = this.flipped ? h : this.scatterStartHeight; - - let { scalable, translatable, rotatable } = this.saved; - this.scatter.scalable = scalable; - this.scatter.translatable = translatable; - this.scatter.rotatable = rotatable; - }, - force3D: true - }); - - // See https://greensock.com/forums/topic/7997-rotate-the-shortest-way/ - TweenLite.to(this.element, this.flipDuration / 2, { - scale: targetScale, - ease: Power1.easeOut, - rotationZ: targetZ + '_short', - transformOrigin: '50% 50%', - width: w, - height: h, - x: x, - y: y, - onComplete: e => { - if (this.flipped) { - this.hide(this.front); - // this.hide(this.infoBtn) - } else { - this.hide(this.back); - // this.show(this.infoBtn) - } - } - }); - } - } - - class Index { - - constructor(template, pages, notfound='thumbnails/notfound.png') { - this.template = template; - this.pages = pages; - this.notfound = notfound; - } - - setup() { - for(let pair of this.pages) { - let [title, src] = pair; - let id = getId(); - pair.push(id); - let t = this.template; - let wrapper = t.content.querySelector('.wrapper'); - wrapper.id = id; - let clone = document.importNode(t.content, true); - container.appendChild(clone); - wrapper = container.querySelector('#'+id); - - let icon = wrapper.querySelector('.icon'); - - icon.onerror = (e) => { - if (this.notfound) - icon.src = this.notfound; - }; - let iconSrc = src.replace('.html', '.png'); - //console.log("iconSrc", iconSrc) - if (iconSrc.endsWith('index.png')) { - icon.src = iconSrc.replace('index.png', 'thumbnail.png'); - } - else { - icon.src = 'thumbnails/' + iconSrc; - } - - // icon.src = 'thumbnails/' + iconSrc - // console.log(iconSrc) - wrapper.href = src; - let titleDiv = wrapper.querySelector('.title'); - titleDiv.innerText = title; - } - } - - frames() { - if (this.pages.length == 0) - return - let [title, src, id] = this.pages.shift(); - let iframe = document.createElement('iframe'); - iframe.frameborder = 0; - let wrapper = document.getElementById(id); - let icon = wrapper.querySelector('.icon'); - - icon.parentNode.replaceChild(iframe, icon); - iframe.onload = (e) => { - this.frames(); - }; - iframe.src = src + window.location.search; - } - - load() { - this.setup(); - if (window.location.search.startsWith('?test')) - this.frames(); - } - - loadAndTest() { - this.setup(); - if (!Capabilities.isMobile) - this.frames(); - } - } - /** Basic class for poppable elements that need to be closed as soon as one poppable is * shown. */ @@ -6097,7 +3916,7 @@ /** A Popup Menu that shows text labels in a vertical row. */ - class PopupMenu$1 extends Popup { + class PopupMenu extends Popup { /** * The constructor. * @constructor @@ -6261,7 +4080,7 @@ } console.log("open", point); let notchPosition = (point.y < 50 && switchPos) ? 'topCenter' : 'bottomCenter'; - let popup = new PopupMenu$1({ + let popup = new PopupMenu({ parent, fontSize, padding, zIndex, spacing, switchPos, notchSize, notchPosition, maxWidth, backgroundColor, normalColor, @@ -6298,6 +4117,2144 @@ } } + /* eslint-disable no-unused-vars */ + /** + * A base class for scatter specific events. + * + * @constructor + * @param {name} String - The name of the event + * @param {target} Object - The target of the event + */ + 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 + */ + 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 + */ + 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 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.timestamp = null; + this.onThrowFinished = onThrowFinished; + //console.log("onThrowFinished", onThrowFinished) + } + + observeVelocity() { + this.lastframe = performance.now(); + } + + addVelocity(delta, buffer = 5) { + let t = performance.now(); + let dt = t - this.lastframe; + this.lastframe = t; + 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 / delta.number, dy: delta.y / delta.number }; + this.velocities.push(velocity); + while (this.velocities.length > buffer) { + this.velocities.shift(); + } + } + } + + 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 = []; + } + + 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 + } + + 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()) { + requestAnimationFrame(this.animateThrow.bind(this)); + return + } else { + if (this.isOutside()) { + requestAnimationFrame(this.animateThrow.bind(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 + } + } + + 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.10, + scaleCloseBuffer = 0.05, + maxRotation = Angle.degree2radian(5) + } = {}) { + 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.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(); + return true + } + + close() { + if (this.onClose) { + this.onClose.forEach(callback => callback(this)); + } + } + + gesture(interaction) { + let delta = interaction.delta(); + if (delta != null) { + this.addVelocity(delta); + let alpha = delta.rotate; + if (this.maxRotation != null) { + if (Math.abs(alpha) > this.maxRotation) { + alpha = 0; + } + } + this.transform(delta, delta.zoom, alpha, delta.about); + if (delta.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.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); + let offset = Points.subtract(anchor, origin); + this._move(offset); + this.scale = newScale; + this.rotation += rotate; + offset = Points.negate(offset); + offset = Points.add(offset, extra); + offset = Points.add(offset, translate); + this._move(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)) { + 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) { } + + 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. + */ + 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. + */ + constructor( + element, + { stopEvents = 'auto', claimEvents = true, useCapture = true, touchAction = 'none' } = {} + ) { + this.onCapture = null; + this.element = element; + if (stopEvents === 'auto') { + 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$1.setStyle(element, { touchAction }); + } + this.scatter = new Map(); + this.delegate = new InteractionMapper$1(element, this, { + useCapture, + mouseWheelElement: window + }); + + if (typeof debugCanvas !== 'undefined') { + 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); + }); + } + + preventPinch(event) { + event = event.originalEvent || event; + if (event.scale !== 1) { + event.preventDefault(); + } + } + + 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 + } + } + + + 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, + simulateClick = false, + verbose = true, + onResize = null, + touchAction = 'none', + throwVisibility = 44, + throwDamping = 0.95, + autoThrow = true, + scaleAutoClose = false, + onClose = null, + scaleCloseThreshold = 0.10, + scaleCloseBuffer = 0.05 + } = {} + ) { + super({ + minScale, + maxScale, + startScale, + overdoScaling, + autoBringToFront, + translatable, + scalable, + rotatable, + movableX, + movableY, + resizable, + rotationDegrees, + rotation, + onTransform, + throwVisibility, + throwDamping, + autoThrow, + scaleAutoClose, + scaleCloseThreshold, + scaleCloseBuffer, + onClose + }); + 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.simulateClick = simulateClick; + 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 + }; + + + // For tweenlite we need initial values in _gsTransform + TweenLite.set(element, this.initialValues); + this.onResize = onResize; + this.verbose = verbose; + if (touchAction !== null) { + Elements$1.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; + } + 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++ }); + } + + onTap(event, interaction, point) { + if (this.simulateClick) { + let p = Points.fromPageToNode(this.element, point); + let element = document.elementFromPoint(p.x, p.y); + if (element != null) { + console.log('tap simulates click'); + element.click(); + } + } + } + + 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; + TweenLite.set(this.element, { x: x, y: 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; + + class CardLoader { + constructor( + src, + { + x = 0, + y = 0, + width = 1000, + height = 800, + maxWidth = null, + maxHeight = null, + scale = 1, + minScale = 0.5, + maxScale = 1.5, + rotation = 0 + } = {} + ) { + this.src = src; + this.x = x; + this.y = y; + this.scale = scale; + this.rotation = 0; + this.maxScale = maxScale; + this.minScale = minScale; + this.wantedWidth = width; + this.wantedHeight = height; + this.maxWidth = maxWidth != null ? maxWidth : window.innerWidth; + this.maxHeight = maxHeight != null ? maxHeight : window.innerHeight; + this.addedNode = null; + console.log({ + + width, + height, + maxWidth, + maxHeight, + + }); + } + + unload() { + if (this.addedNode) { + this.addedNode.remove(); + this.addedNode = null; + } + } + } + + class PDFLoader extends CardLoader { + constructor(src, { width = 1640, height = 800, scale = 1 } = {}) { + super(src, { width, height, scale }); + if (typeof PDFJS == 'undefined') { + alert('PDF.js needed'); + } + } + + load(domNode) { + return new Promise((resolve, reject) => { + PDFJS.getDocument(this.src).then(pdf => { + pdf.getPage(1).then(page => { + let scale = this.scale * app.renderer.resolution; + let invScale = 1 / scale; + let viewport = page.getViewport(scale); + + // Prepare canvas using PDF page dimensions. + let canvas = document.createElement('canvas'); + let context = canvas.getContext('2d'); + canvas.height = viewport.height; + canvas.width = viewport.width; + + // Render PDF page into canvas context. + let renderContext = { + canvasContext: context, + viewport: viewport + }; + page.render(renderContext); + domNode.appendChild(canvas); + this.wantedWidth = canvas.width; + this.wantedHeight = canvas.height; + this.scale = invScale; + this.addedNode = canvas; + resolve(this); + }); + }); + }) + } + } + + class ImageLoader extends CardLoader { + load(domNode) { + return new Promise((resolve, reject) => { + let isImage = domNode instanceof HTMLImageElement; + let image = isImage ? domNode : document.createElement('img'); + image.onload = e => { + if (!isImage) { + domNode.appendChild(image); + this.addedNode = image; + } + this.wantedWidth = image.naturalWidth; + this.wantedHeight = image.naturalHeight; + + let scaleW = this.maxWidth / image.naturalWidth; + let scaleH = this.maxHeight / image.naturalHeight; + this.scale = Math.min(this.maxScale, Math.min(scaleW, scaleH)); + image.setAttribute('draggable', false); + image.width = image.naturalWidth; + image.height = image.naturalHeight; + resolve(this); + }; + image.onerror = e => { + reject(this); + }; + image.src = this.src; + }) + } + } + + class FrameLoader extends CardLoader { + load(domNode) { + return new Promise((resolve, reject) => { + let isFrame = domNode instanceof HTMLIFrameElement; + let iframe = isFrame ? domNode : document.createElement('iframe'); + console.log('FrameLoader.load', isFrame, iframe, this.src); + iframe.frameBorder = 0; + iframe.style.scrolling = false; + iframe.width = this.wantedWidth; + iframe.height = this.wantedHeight; + if (!isFrame) { + // Unlike img onload is only triggered if the iframe is part of the DOM tree + domNode.appendChild(iframe); + this.addedNode = iframe; + } + iframe.onload = e => { + resolve(this); + }; + iframe.onerror = e => { + reject(this); + }; + iframe.src = this.src; + }) + } + } + + class HTMLLoader extends CardLoader { + load(domNode) { + return new Promise((resolve, reject) => { + let xhr = new XMLHttpRequest(); + xhr.open('GET', this.src, false); + xhr.onload = e => { + domNode.innerHTML = xhr.response; + this.addedNode = domNode.firstElementChild; + let { width, height } = this.size(this.addedNode); + console.log("HTMLLoader.load", { added: this.addedNode, width, height }); + if (width) + this.wantedWidth = width || this.wantedWidth; + if (height) + this.wantedHeight = height || this.wantedHeight; + resolve(this); + }; + xhr.onerror = e => { + reject(this); + }; + xhr.send(); + }) + } + + /** + * Tries to determine the size of the addedNode. + * Checks for explicit width and height style attributes. + * + * Overwrite this method if you want to extract values from other infos. + * + * @returns { width: int, height: int } + * @memberof HTMLLoader + */ + size(node) { + let width = parseInt(node.style.width) || null; + let height = parseInt(node.style.height) || null; + return { width, height } + } + } + + class DOMFlip { + constructor( + domScatterContainer, + flipTemplate, + frontLoader, + backLoader, + { + closeOnMinScale = false, + flipDuration = 1, + fadeDuration = 0.2, + overdoScaling = 1, + autoLoad = false, + center = null, + preloadBack = false, + translatable = true, + scalable = true, + rotatable = true, + onFront = null, + onBack = null, + onClose = null, + onUpdate = null, + onRemoved = null, + onLoaded = null + } = {} + ) { + this.domScatterContainer = domScatterContainer; + this.id = getId$1(); + this.flipDuration = flipDuration; + this.fadeDuration = fadeDuration; + this.closeOnMinScale = closeOnMinScale; + this.flipTemplate = flipTemplate; + this.frontLoader = frontLoader; + this.backLoader = backLoader; + this.translatable = translatable; + this.scalable = scalable; + this.rotatable = rotatable; + this.onFrontFlipped = onFront; + this.onBackFlipped = onBack; + this.onClose = onClose; + this.onRemoved = onRemoved; + this.onUpdate = onUpdate; + this.onLoaded = onLoaded; + this.center = center; + this.preloadBack = preloadBack; + this.overdoScaling = overdoScaling; + if (autoLoad) { + this.load(); + } + } + + load() { + return new Promise((resolve, reject) => { + let t = this.flipTemplate; + let dom = this.domScatterContainer.element; + let wrapper = t.content.querySelector('.flipWrapper'); + wrapper.id = this.id; + let clone = document.importNode(t.content, true); + dom.appendChild(clone); + // We cannot use the document fragment itself because it + // is not part of the main dom tree. After the appendChild + // call we can access the new dom element by id + this.cardWrapper = dom.querySelector('#' + this.id); + let front = this.cardWrapper.querySelector('.front'); + this.frontLoader.load(front).then(loader => { + this.frontLoaded(loader).then((obj) => { + if (this.onLoaded) this.onLoaded(); + resolve(this); + }); + }); + }) + } + + frontLoaded(loader) { + return new Promise((resolve, reject) => { + let scatter = new DOMScatter( + this.cardWrapper, + this.domScatterContainer, + { + x: loader.x, + y: loader.y, + startScale: loader.scale, + scale: loader.scale, + maxScale: loader.maxScale, + minScale: loader.minScale, + width: loader.wantedWidth, + height: loader.wantedHeight, + rotation: loader.rotation, + translatable: this.translatable, + scalable: this.scalable, + rotatable: this.rotatable, + overdoScaling: this.overdoScaling + } + ); + + if (this.center) { + scatter.centerAt(this.center); + } + + if (this.closeOnMinScale) { + + const removeOnMinScale = function () { + if (scatter.scale <= scatter.minScale) { + this.flippable.close(); + + // 'Disable' overdoscaling to avoid weird jumps on close. + scatter.minScale /= scatter.overdoScaling; + scatter.overdoScaling = 1; + + //Remove callback + if (scatter.onTransform) { + let callbackIdx = scatter.onTransform.indexOf(removeOnMinScale); + scatter.onTransform.splice(callbackIdx, 1); + } + } + + }.bind(this); + + + + scatter.addTransformEventCallback(removeOnMinScale); + } + + let flippable = new DOMFlippable(this.cardWrapper, scatter, this); + let back = this.cardWrapper.querySelector('.back'); + + if (this.preloadBack) { + this.backLoader.load(back).then(loader => { + this.setupFlippable(flippable, loader); + }); + } + this.flippable = flippable; + resolve(this); + }) + } + + centerAt(p) { + this.center = p; + this.flippable.centerAt(p); + } + + zoom(scale) { + this.flippable.zoom(scale); + } + + setupFlippable(flippable, loader) { + flippable.wantedWidth = loader.wantedWidth; + flippable.wantedHeight = loader.wantedHeight; + flippable.wantedScale = loader.scale; + flippable.minScale = loader.minScale; + flippable.maxScale = loader.maxScale; + flippable.scaleButtons(); + } + + start({ targetCenter = null } = {}) { + console.log('DOMFlip.start', targetCenter); + if (this.preloadBack) this.flippable.start({ duration: this.flipDuration, targetCenter }); + else { + let back = this.cardWrapper.querySelector('.back'); + let flippable = this.flippable; + this.backLoader.load(back).then(loader => { + this.setupFlippable(flippable, loader); + flippable.start({ duration: this.flipDuration, targetCenter }); + }); + } + } + + fadeOutAndRemove() { + TweenLite.to(this.cardWrapper, this.fadeDuration, { + opacity: 0, + onComplete: () => { + this.cardWrapper.remove(); + } + }); + } + + closed() { + this.unload(); + } + + unload() { + if (!this.preloadBack) { + this.backLoader.unload(); + } + } + } + + class DOMFlippable { + constructor(element, scatter, flip) { + // Set log to console.log or a custom log function + // define data structures to store our touchpoints in + + this.element = element; + this.flip = flip; + this.card = element.querySelector('.flipCard'); + this.front = element.querySelector('.front'); + this.back = element.querySelector('.back'); + this.flipped = false; + this.scatter = scatter; + this.onFrontFlipped = flip.onFrontFlipped; + this.onBackFlipped = flip.onBackFlipped; + this.onClose = flip.onClose; + this.onRemoved = flip.onRemoved; + this.onUpdate = flip.onUpdate; + + this.flipDuration = flip.flipDuration; + this.fadeDuration = flip.fadeDuration; + scatter.addTransformEventCallback(this.scatterTransformed.bind(this)); + console.log('lib.DOMFlippable', 5000); + TweenLite.set(this.element, { perspective: 5000 }); + TweenLite.set(this.card, { transformStyle: 'preserve-3d' }); + TweenLite.set(this.back, { rotationY: -180 }); + TweenLite.set([this.back, this.front], { + backfaceVisibility: 'hidden', + perspective: 5000 + }); + TweenLite.set(this.front, { visibility: 'visible' }); + this.infoBtn = element.querySelector('.infoBtn'); + this.backBtn = element.querySelector('.backBtn'); + this.closeBtn = element.querySelector('.closeBtn'); + /* Buttons are not guaranteed to exist. */ + if (this.infoBtn) { + InteractionMapper$1.on('tap', this.infoBtn, event => this.flip.start()); + + this.enable(this.infoBtn); + } + if (this.backBtn) { + InteractionMapper$1.on('tap', this.backBtn, event => this.start()); + } + if (this.closeBtn) { + InteractionMapper$1.on('tap', this.closeBtn, event => this.close()); + this.enable(this.closeBtn); + } + this.scaleButtons(); + this.bringToFront(); + } + + close() { + this.disable(this.infoBtn); + this.disable(this.closeBtn); + if (this.onClose) { + this.onClose(this); + this.flip.closed(); + } else { + this.scatter.zoom(0.1, { + animate: this.fadeDuration, + onComplete: () => { + this.element.remove(); + this.flip.closed(); + if (this.onRemoved) { + this.onRemoved.call(this); + } + } + }); + } + } + + showFront() { + TweenLite.set(this.front, { visibility: 'visible' }); + } + + centerAt(p) { + this.scatter.centerAt(p); + } + + zoom(scale) { + this.scatter.zoom(scale); + } + + get buttonScale() { + let iscale = 1.0; + + if (this.scatter != null) { + let scale = this.scatter.scale || 1; + iscale = 1.0 / scale; + } + return iscale + } + + scaleButtons() { + //This also works for svgs. + // if (this.infoBtn) + // this.infoBtn.style.transform = "scale(" + this.buttonScale + ")" + + // if (this.backBtn) + // this.backBtn.style.transform = "scale(" + this.buttonScale + ")" + + // if (this.closeBtn) + // this.closeBtn.style.transform = "scale(" + this.buttonScale + ")" + + console.log(this.buttonScale); + //// This did not work with svgs! + TweenLite.set([this.infoBtn, this.backBtn, this.closeBtn], { + scale: this.buttonScale + }); + } + + bringToFront() { + this.scatter.bringToFront(); + TweenLite.set(this.element, { zIndex: DOMScatter.zIndex++ }); + } + + clickInfo() { + this.bringToFront(); + this.infoBtn.click(); + } + + scatterTransformed(event) { + this.scaleButtons(); + } + + targetRotation(alpha) { + let ortho = 90; + let rest = alpha % ortho; + let delta = 0.0; + if (rest > ortho / 2.0) { + delta = ortho - rest; + } else { + delta = -rest; + } + return delta + } + + infoValues(info) { + let startX = this.element._gsTransform.x; + let startY = this.element._gsTransform.y; + let startAngle = this.element._gsTransform.rotation; + let startScale = this.element._gsTransform.scaleX; + let w = this.element.style.width; + let h = this.element.style.height; + console.log(info, startX, startY, startAngle, startScale, w, h); + } + + show(element, duration = 0, alpha = 1) { + if (element) { + TweenLite.to(element, duration, { autoAlpha: alpha }); // visibility: 'visible', display: 'initial'}) + } + } + + hide(element, duration = 0, alpha = 0) { + if (element) { + TweenLite.to(element, duration, { autoAlpha: alpha }); // {visibility: 'hidden', display: 'none'}) + } + } + + + + enable(button) { + this.show(button, this.fadeDuration); + if (button) { + TweenLite.set(button, { pointerEvents: 'auto' }); + } + } + + disable(button) { + this.hide(button, this.fadeDuration); + if (button) { + TweenLite.set(button, { pointerEvents: 'none' }); + } + } + + start({ targetCenter = null } = {}) { + this.bringToFront(); + if (!this.flipped) { + this.startX = this.element._gsTransform.x; + this.startY = this.element._gsTransform.y; + this.startAngle = this.element._gsTransform.rotation; + this.startScale = this.element._gsTransform.scaleX; + this.startWidth = this.element.style.width; + this.startHeight = this.element.style.height; + this.scatterStartWidth = this.scatter.width; + this.scatterStartHeight = this.scatter.height; + this.show(this.back); + this.disable(this.infoBtn); + this.disable(this.closeBtn); + } else { + this.show(this.front, this.fadeDuration); + this.disable(this.backBtn); + } + let { scalable, translatable, rotatable } = this.scatter; + this.saved = { scalable, translatable, rotatable }; + this.scatter.scalable = false; + this.scatter.translatable = false; + this.scatter.rotatable = false; + this.scatter.killAnimation(); + + this.flipped = !this.flipped; + let targetY = this.flipped ? 180 : 0; + let targetZ = this.flipped + ? this.startAngle + this.targetRotation(this.startAngle) + : this.startAngle; + let targetScale = this.flipped ? this.wantedScale : this.startScale; + let w = this.flipped ? this.wantedWidth : this.startWidth; + let h = this.flipped ? this.wantedHeight : this.startHeight; + let dw = this.wantedWidth - this.scatter.width; + let dh = this.wantedHeight - this.scatter.height; + let tc = targetCenter; + let xx = tc != null ? tc.x - w / 2 : this.startX - dw / 2; + let yy = tc != null ? tc.y - h / 2 : this.startY - dh / 2; + let x = this.flipped ? xx : this.startX; + let y = this.flipped ? yy : this.startY; + + console.log("DOMFlippable.start", this.flipped, targetCenter, x, y, this.saved); + let onUpdate = this.onUpdate !== null ? () => this.onUpdate(this) : null; + console.log(this.flipDuration); + TweenLite.to(this.card, this.flipDuration, { + rotationY: targetY, + ease: Power1.easeOut, + transformOrigin: '50% 50%', + onUpdate, + onComplete: e => { + if (this.flipped) { + //this.hide(this.front) + this.enable(this.backBtn); + this.show(this.backBtn); + + if (this.onFrontFlipped) { + this.onFrontFlipped(this); + } + } else { + + if (this.onBackFlipped == null) { + this.enable(this.infoBtn, this.fadeDuration); + this.enable(this.closeBtn, this.fadeDuration); + } else { + this.onBackFlipped(this); + } + this.flip.unload(); + } + this.scatter.scale = targetScale; + this.scaleButtons(); + this.scatter.rotationDegrees = targetZ; + this.scatter.width = this.flipped ? w : this.scatterStartWidth; + this.scatter.height = this.flipped ? h : this.scatterStartHeight; + + let { scalable, translatable, rotatable } = this.saved; + this.scatter.scalable = scalable; + this.scatter.translatable = translatable; + this.scatter.rotatable = rotatable; + }, + force3D: true + }); + + // See https://greensock.com/forums/topic/7997-rotate-the-shortest-way/ + TweenLite.to(this.element, this.flipDuration / 2, { + scale: targetScale, + ease: Power1.easeOut, + rotationZ: targetZ + '_short', + transformOrigin: '50% 50%', + width: w, + height: h, + x: x, + y: y, + onComplete: e => { + if (this.flipped) { + this.hide(this.front); + // this.hide(this.infoBtn) + } else { + this.hide(this.back); + // this.show(this.infoBtn) + } + } + }); + } + } + + class Index { + + constructor(template, pages, notfound='thumbnails/notfound.png') { + this.template = template; + this.pages = pages; + this.notfound = notfound; + } + + setup() { + for(let pair of this.pages) { + let [title, src] = pair; + let id = getId(); + pair.push(id); + let t = this.template; + let wrapper = t.content.querySelector('.wrapper'); + wrapper.id = id; + let clone = document.importNode(t.content, true); + container.appendChild(clone); + wrapper = container.querySelector('#'+id); + + let icon = wrapper.querySelector('.icon'); + + icon.onerror = (e) => { + if (this.notfound) + icon.src = this.notfound; + }; + let iconSrc = src.replace('.html', '.png'); + //console.log("iconSrc", iconSrc) + if (iconSrc.endsWith('index.png')) { + icon.src = iconSrc.replace('index.png', 'thumbnail.png'); + } + else { + icon.src = 'thumbnails/' + iconSrc; + } + + // icon.src = 'thumbnails/' + iconSrc + // console.log(iconSrc) + wrapper.href = src; + let titleDiv = wrapper.querySelector('.title'); + titleDiv.innerText = title; + } + } + + frames() { + if (this.pages.length == 0) + return + let [title, src, id] = this.pages.shift(); + let iframe = document.createElement('iframe'); + iframe.frameborder = 0; + let wrapper = document.getElementById(id); + let icon = wrapper.querySelector('.icon'); + + icon.parentNode.replaceChild(iframe, icon); + iframe.onload = (e) => { + this.frames(); + }; + iframe.src = src + window.location.search; + } + + load() { + this.setup(); + if (window.location.search.startsWith('?test')) + this.frames(); + } + + loadAndTest() { + this.setup(); + if (!Capabilities.isMobile) + this.frames(); + } + } + class FrameContainer { constructor(element) { @@ -7485,7 +7442,7 @@ window.Polygon = Polygon; window.Poppable = Poppable; window.Popup = Popup; - window.PopupMenu = PopupMenu$1; + window.PopupMenu = PopupMenu; window.ResizeEvent = ResizeEvent; window.ScatterEvent = ScatterEvent; window.Sets = Sets; diff --git a/dist/iwmlib.pixi.js b/dist/iwmlib.pixi.js index aba397f..16d53ae 100644 --- a/dist/iwmlib.pixi.js +++ b/dist/iwmlib.pixi.js @@ -4781,7 +4781,7 @@ } } - /* globals Hammer, propagating */ + /* eslint-disable no-unused-vars */ /** Interaction patterns @@ -4789,6 +4789,7 @@ */ class IInteractionTarget extends Interface { + capture(event) { return typeof true } @@ -4992,10 +4993,10 @@ let d1 = Points.subtract(c1, p1); let d2 = Points.subtract(c2, p2); let cm = Points.mean(c1, c2); - + // Using the mean leads to jumps between time slices with 3 and 2 fingers // We use the mean of deltas instead - let delta = Points.mean(d1, d2); + let delta = Points.mean(d1, d2); let zoom = 1.0; let distance1 = Points.distance(p1, p2); let distance2 = Points.distance(c1, c2); @@ -5199,7 +5200,6 @@ } let result = false; if (this.isTap(key)) { - this.registerTap(key, ended); result = this.tapCounts.get(key) == 2; } @@ -5313,7 +5313,9 @@ if (this.capturePointerEvents) { try { element.setPointerCapture(e.pointerId); - } catch (e) { } + } catch (e) { + console.warn('Cannot setPointerCapture'); + } } this.onStart(e); } @@ -5345,7 +5347,9 @@ if (this.capturePointerEvents) { try { element.releasePointerCapture(e.pointerId); - } catch (e) { } + } catch (e) { + console.warn('Cannot release pointer'); + } } }, useCapture @@ -5389,7 +5393,7 @@ e => { if (this.debug) console.log('pointerout', e.pointerId, e.pointerType, e.target); if (e.target == element) { - this.onEnd(e); + this.onEnd(e); } }, useCapture); @@ -5492,9 +5496,8 @@ e => { if (e.target == element) { this.onEnd(e); - console.warn("Shouldn't happen: mouseout ends interaction"); + console.warn('Shouldn\'t happen: mouseout ends interaction'); } - }, useCapture ); @@ -5594,39 +5597,42 @@ // 'targetTouches' let result = {}; switch (event.constructor.name) { - case 'MouseEvent': - let buttons = event.buttons || event.which; - if (buttons) result['mouse'] = this.getPosition(event); - break - case 'PointerEvent': - result[event.pointerId.toString()] = this.getPosition(event); - break - case 'Touch': - let id = + case 'MouseEvent': { + let buttons = event.buttons || event.which; + if (buttons) result['mouse'] = this.getPosition(event); + break + } + case 'PointerEvent': { + result[event.pointerId.toString()] = this.getPosition(event); + break + } + case 'Touch': { + let id = event.touchType === 'stylus' ? 'stylus' : event.identifier.toString(); - result[id] = this.getPosition(event); - break - // case 'TouchEvent': - // // Needs to be observed: Perhaps changedTouches are all we need. If so - // // we can remove the touchEventKey default parameter - // if (touchEventKey == 'all') { - // for(let t of event.targetTouches) { - // result[t.identifier.toString()] = this.getPosition(t) - // } - // for(let t of event.changedTouches) { - // result[t.identifier.toString()] = this.getPosition(t) - // } - // } - // else { - // for(let t of event.changedTouches) { - // result[t.identifier.toString()] = this.getPosition(t) - // } - // } - // break - default: - break + result[id] = this.getPosition(event); + break + } + // case 'TouchEvent': + // // Needs to be observed: Perhaps changedTouches are all we need. If so + // // we can remove the touchEventKey default parameter + // if (touchEventKey == 'all') { + // for(let t of event.targetTouches) { + // result[t.identifier.toString()] = this.getPosition(t) + // } + // for(let t of event.changedTouches) { + // result[t.identifier.toString()] = this.getPosition(t) + // } + // } + // else { + // for(let t of event.changedTouches) { + // result[t.identifier.toString()] = this.getPosition(t) + // } + // } + // break + default: + break } return result } @@ -5654,7 +5660,7 @@ let point = extracted[key]; let updated = this.interaction.update(key, point); if (updated) { - console.warn("new pointer in updateInteraction shouldn't happen", key); + console.warn('new pointer in updateInteraction shouldn\'t happen', key); this.interactionStarted(event, key, point); } } @@ -6039,6 +6045,11 @@ window.Capabilities = Capabilities; window.CapabilitiesTests = CapabilitiesTests; + /** Basic class for poppable elements that need to be closed as soon as one poppable is + * shown. + */ + + /* eslint-disable no-unused-vars */ /** * A base class for scatter specific events. * @@ -6088,7 +6099,7 @@ toString() { return ( - "Event('scatterTransformed', scale: " + + 'Event(\'scatterTransformed\', scale: ' + this.scale + ' about: ' + this.about.x + @@ -6163,7 +6174,7 @@ // 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 / delta.number, dy: delta.y / delta.number}; + let velocity = { t: t, dt: dt, dx: delta.x / delta.number, dy: delta.y / delta.number }; this.velocities.push(velocity); while (this.velocities.length > buffer) { this.velocities.shift(); @@ -6347,7 +6358,7 @@ this.resizable = resizable; this.mouseZoomFactor = mouseZoomFactor; this.autoBringToFront = autoBringToFront; - + this.dragging = false; this.onTransform = onTransform != null ? [onTransform] : null; this.onClose = onClose != null ? [onClose] : null; @@ -6387,7 +6398,6 @@ let alpha = delta.rotate; if (this.maxRotation != null) { if (Math.abs(alpha) > this.maxRotation) { - console.log("limited rotation"); alpha = 0; } } @@ -6460,7 +6470,7 @@ 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} + if (!stagePolygon) return { x: 0, y: 0 } let polygon = this.polygon; let bounced = this.bouncing(); if (bounced) { @@ -6744,20 +6754,6 @@ } this._updateTransparency(); } - // - // if (this.onTransform != null) { - // let event = new ScatterEvent(this, { - // translate: {x: 0, y: 0}, - // scale: this.scale, - // rotate: 0, - // about: null, - // fast: false, - // type: ZOOM - // }) - // this.onTransform.forEach(function(f) { - // f(event) - // }) - // } } onStart(event, interaction) { @@ -6999,15 +6995,13 @@ } 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.style.borderRadius = "100% 0px 0px 0px"; - // button.style.background = this.element.style.backgroundColor - button.className = "interactiveElement"; + 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) => { @@ -7182,43 +7176,13 @@ TweenLite.set(this.element, { zIndex: DOMScatter.zIndex++ }); } - toggleVideo(element) { - if (element.paused) { - element.play(); - } else { - element.pause(); - } - } - onTap(event, interaction, point) { if (this.simulateClick) { let p = Points.fromPageToNode(this.element, point); - let iframe = this.element.querySelector('iframe'); - if (iframe) { - let doc = iframe.contentWindow.document; - let element = doc.elementFromPoint(p.x, p.y); - if (element == null) { - return - } - switch (element.tagName) { - case 'VIDEO': - console.log(element.currentSrc); - if (PopupMenu) { - PopupMenu.open( - { - Fullscreen: () => - window.open(element.currentSrc), - Play: () => element.play() - }, - { x, y } - ); - } else { - this.toggleVideo(element); - } - break - default: - element.click(); - } + let element = document.elementFromPoint(p.x, p.y); + if (element != null) { + console.log('tap simulates click'); + element.click(); } } } @@ -7259,37 +7223,35 @@ } resizeAfterTransform(zoom) { - // let w = this.width * this.scale - // let h = this.height * this.scale - // TweenLite.set(this.element, { width: w, height: h }) 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); } - if (this.resizeButton != null) ; } startResize(e) { e.preventDefault(); let event = new CustomEvent('resizeStarted'); - let oldPostition = {x: this.element.getBoundingClientRect().left, y: this.element.getBoundingClientRect().top}; + let oldPostition = { x: this.element.getBoundingClientRect().left, y: this.element.getBoundingClientRect().top }; this.bringToFront(); - this.element.style.transformOrigin = "0% 0%"; + this.element.style.transformOrigin = '0% 0%'; - let newPostition = {x: this.element.getBoundingClientRect().left, y: this.element.getBoundingClientRect().top}; + 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.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" } }); + TweenLite.to(this.element, 0, { css: { left: '+=' + offset.x + 'px' } }); + TweenLite.to(this.element, 0, { css: { top: '+=' + offset.y + 'px' } }); this.element.dispatchEvent(event); } @@ -7300,7 +7262,7 @@ let rotation = Angle.radian2degree(this.rotation); rotation = (rotation + 360) % 360; let event = new CustomEvent('resized'); - if (e.target.getAttribute('resizing') == "true") { + if (e.target.getAttribute('resizing') == 'true') { let deltaX = (e.clientX - this.oldX); let deltaY = (e.clientY - this.oldY); @@ -7317,7 +7279,7 @@ 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 }); + 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; @@ -7331,15 +7293,15 @@ 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 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" } }); + 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"); + e.target.setAttribute('resizing', 'false'); this.element.dispatchEvent(event); } diff --git a/lib/interaction.js b/lib/interaction.js index a63c6d9..2b86c89 100755 --- a/lib/interaction.js +++ b/lib/interaction.js @@ -1,8 +1,9 @@ +/* eslint-disable no-unused-vars */ /* globals Hammer, propagating */ /*eslint no-console: ["error", { allow: ["log", "warn", "info", "error"] }]*/ import Interface from './interface.js' -import { Points, Angle, MapProxy } from './utils.js' +import { Points, MapProxy } from './utils.js' import Events from './events.js' import Logging from './logging.js' @@ -12,6 +13,7 @@ import Logging from './logging.js' */ export class IInteractionTarget extends Interface { + capture(event) { return typeof true } @@ -215,10 +217,10 @@ export class InteractionPoints { let d1 = Points.subtract(c1, p1) let d2 = Points.subtract(c2, p2) let cm = Points.mean(c1, c2) - + // Using the mean leads to jumps between time slices with 3 and 2 fingers // We use the mean of deltas instead - let delta = Points.mean(d1, d2) + let delta = Points.mean(d1, d2) let zoom = 1.0 let distance1 = Points.distance(p1, p2) let distance2 = Points.distance(c1, c2) @@ -422,7 +424,6 @@ export class Interaction extends InteractionPoints { } let result = false if (this.isTap(key)) { - this.registerTap(key, ended) result = this.tapCounts.get(key) == 2 } @@ -536,7 +537,9 @@ export class InteractionDelegate { if (this.capturePointerEvents) { try { element.setPointerCapture(e.pointerId) - } catch (e) { } + } catch (e) { + console.warn('Cannot setPointerCapture') + } } this.onStart(e) } @@ -568,7 +571,9 @@ export class InteractionDelegate { if (this.capturePointerEvents) { try { element.releasePointerCapture(e.pointerId) - } catch (e) { } + } catch (e) { + console.warn('Cannot release pointer') + } } }, useCapture @@ -612,7 +617,7 @@ export class InteractionDelegate { e => { if (this.debug) console.log('pointerout', e.pointerId, e.pointerType, e.target) if (e.target == element) { - this.onEnd(e) + this.onEnd(e) } }, useCapture) @@ -715,9 +720,8 @@ export class InteractionDelegate { e => { if (e.target == element) { this.onEnd(e) - console.warn("Shouldn't happen: mouseout ends interaction") + console.warn('Shouldn\'t happen: mouseout ends interaction') } - }, useCapture ) @@ -819,39 +823,42 @@ export class InteractionDelegate { // 'targetTouches' let result = {} switch (event.constructor.name) { - case 'MouseEvent': - let buttons = event.buttons || event.which - if (buttons) result['mouse'] = this.getPosition(event) - break - case 'PointerEvent': - result[event.pointerId.toString()] = this.getPosition(event) - break - case 'Touch': - let id = + case 'MouseEvent': { + let buttons = event.buttons || event.which + if (buttons) result['mouse'] = this.getPosition(event) + break + } + case 'PointerEvent': { + result[event.pointerId.toString()] = this.getPosition(event) + break + } + case 'Touch': { + let id = event.touchType === 'stylus' ? 'stylus' : event.identifier.toString() - result[id] = this.getPosition(event) - break - // case 'TouchEvent': - // // Needs to be observed: Perhaps changedTouches are all we need. If so - // // we can remove the touchEventKey default parameter - // if (touchEventKey == 'all') { - // for(let t of event.targetTouches) { - // result[t.identifier.toString()] = this.getPosition(t) - // } - // for(let t of event.changedTouches) { - // result[t.identifier.toString()] = this.getPosition(t) - // } - // } - // else { - // for(let t of event.changedTouches) { - // result[t.identifier.toString()] = this.getPosition(t) - // } - // } - // break - default: - break + result[id] = this.getPosition(event) + break + } + // case 'TouchEvent': + // // Needs to be observed: Perhaps changedTouches are all we need. If so + // // we can remove the touchEventKey default parameter + // if (touchEventKey == 'all') { + // for(let t of event.targetTouches) { + // result[t.identifier.toString()] = this.getPosition(t) + // } + // for(let t of event.changedTouches) { + // result[t.identifier.toString()] = this.getPosition(t) + // } + // } + // else { + // for(let t of event.changedTouches) { + // result[t.identifier.toString()] = this.getPosition(t) + // } + // } + // break + default: + break } return result } @@ -879,7 +886,7 @@ export class InteractionDelegate { let point = extracted[key] let updated = this.interaction.update(key, point) if (updated) { - console.warn("new pointer in updateInteraction shouldn't happen", key) + console.warn('new pointer in updateInteraction shouldn\'t happen', key) this.interactionStarted(event, key, point) } } diff --git a/lib/scatter.js b/lib/scatter.js index ceb4c23..02a65ac 100644 --- a/lib/scatter.js +++ b/lib/scatter.js @@ -1,8 +1,11 @@ +/* eslint-disable no-unused-vars */ +/* eslint-disable no-console */ +/* globals TweenLite debugCanvas */ import { Points, Polygon, Angle, Elements } from './utils.js' import Events from './events.js' import { InteractionMapper } from './interaction.js' import { Capabilities } from './capabilities.js' - +import PopupMenu from './popupmenu.js' /** * A base class for scatter specific events. * @@ -21,8 +24,6 @@ export class BaseEvent { const START = 'onStart' const UPDATE = 'onUpdate' const END = 'onEnd' -const ZOOM = 'onZoom' -const MOVE = 'onMove' /** * A scatter event that describes how the scatter has changed. @@ -54,7 +55,7 @@ export class ScatterEvent extends BaseEvent { toString() { return ( - "Event('scatterTransformed', scale: " + + 'Event(\'scatterTransformed\', scale: ' + this.scale + ' about: ' + this.about.x + @@ -129,7 +130,7 @@ class Throwable { // 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 / delta.number, dy: delta.y / delta.number} + let velocity = { t: t, dt: dt, dx: delta.x / delta.number, dy: delta.y / delta.number } this.velocities.push(velocity) while (this.velocities.length > buffer) { this.velocities.shift() @@ -313,7 +314,7 @@ export class AbstractScatter extends Throwable { this.resizable = resizable this.mouseZoomFactor = mouseZoomFactor this.autoBringToFront = autoBringToFront - + this.dragging = false this.onTransform = onTransform != null ? [onTransform] : null this.onClose = onClose != null ? [onClose] : null @@ -353,7 +354,6 @@ export class AbstractScatter extends Throwable { let alpha = delta.rotate if (this.maxRotation != null) { if (Math.abs(alpha) > this.maxRotation) { - console.log("limited rotation") alpha = 0 } } @@ -426,7 +426,7 @@ export class AbstractScatter extends Throwable { 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} + if (!stagePolygon) return { x: 0, y: 0 } let polygon = this.polygon let bounced = this.bouncing() if (bounced) { @@ -710,20 +710,6 @@ export class AbstractScatter extends Throwable { } this._updateTransparency() } - // - // if (this.onTransform != null) { - // let event = new ScatterEvent(this, { - // translate: {x: 0, y: 0}, - // scale: this.scale, - // rotate: 0, - // about: null, - // fast: false, - // type: ZOOM - // }) - // this.onTransform.forEach(function(f) { - // f(event) - // }) - // } } onStart(event, interaction) { @@ -838,10 +824,10 @@ export class AbstractScatter extends Throwable { scale: this.scale, fast: false, type: null - }); + }) this.onTransform.forEach(function (f) { - f(event); - }); + f(event) + }) } } @@ -911,14 +897,13 @@ export class DOMScatterContainer { if (typeof debugCanvas !== 'undefined') { requestAnimationFrame(dt => { - this.showTouches(dt) + this.showTouches(dt, debugCanvas) }) } } - showTouches(dt) { + showTouches(dt, canvas) { let resolution = window.devicePixelRatio - let canvas = debugCanvas let current = this.delegate.interaction.current let context = canvas.getContext('2d') let radius = 20 * resolution @@ -1127,15 +1112,13 @@ export class DOMScatter extends AbstractScatter { } 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.style.borderRadius = "100% 0px 0px 0px"; - // button.style.background = this.element.style.backgroundColor - button.className = "interactiveElement" + 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) => { @@ -1310,43 +1293,13 @@ export class DOMScatter extends AbstractScatter { TweenLite.set(this.element, { zIndex: DOMScatter.zIndex++ }) } - toggleVideo(element) { - if (element.paused) { - element.play() - } else { - element.pause() - } - } - onTap(event, interaction, point) { if (this.simulateClick) { let p = Points.fromPageToNode(this.element, point) - let iframe = this.element.querySelector('iframe') - if (iframe) { - let doc = iframe.contentWindow.document - let element = doc.elementFromPoint(p.x, p.y) - if (element == null) { - return - } - switch (element.tagName) { - case 'VIDEO': - console.log(element.currentSrc) - if (PopupMenu) { - PopupMenu.open( - { - Fullscreen: () => - window.open(element.currentSrc), - Play: () => element.play() - }, - { x, y } - ) - } else { - this.toggleVideo(element) - } - break - default: - element.click() - } + let element = document.elementFromPoint(p.x, p.y) + if (element != null) { + console.log('tap simulates click') + element.click() } } } @@ -1387,42 +1340,37 @@ export class DOMScatter extends AbstractScatter { } resizeAfterTransform(zoom) { - // let w = this.width * this.scale - // let h = this.height * this.scale - // TweenLite.set(this.element, { width: w, height: h }) 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) } - if (this.resizeButton != null) { - // this.resizeButton.style.width = 50/this.scale+"px" - // this.resizeButton.style.height = 50/this.scale+"px" - } } startResize(e) { e.preventDefault() let event = new CustomEvent('resizeStarted') - let oldPostition = {x: this.element.getBoundingClientRect().left, y: this.element.getBoundingClientRect().top}; + let oldPostition = { x: this.element.getBoundingClientRect().left, y: this.element.getBoundingClientRect().top } this.bringToFront() - this.element.style.transformOrigin = "0% 0%" + this.element.style.transformOrigin = '0% 0%' - let newPostition = {x: this.element.getBoundingClientRect().left, y: this.element.getBoundingClientRect().top}; + 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.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" } }) + TweenLite.to(this.element, 0, { css: { left: '+=' + offset.x + 'px' } }) + TweenLite.to(this.element, 0, { css: { top: '+=' + offset.y + 'px' } }) - this.element.dispatchEvent(event); + this.element.dispatchEvent(event) } resize(e) { @@ -1431,7 +1379,7 @@ export class DOMScatter extends AbstractScatter { let rotation = Angle.radian2degree(this.rotation) rotation = (rotation + 360) % 360 let event = new CustomEvent('resized') - if (e.target.getAttribute('resizing') == "true") { + if (e.target.getAttribute('resizing') == 'true') { let deltaX = (e.clientX - this.oldX) let deltaY = (e.clientY - this.oldY) @@ -1448,13 +1396,13 @@ export class DOMScatter extends AbstractScatter { 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 }); + 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); + this.element.dispatchEvent(event) } } @@ -1462,17 +1410,17 @@ export class DOMScatter extends AbstractScatter { 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 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" } }) + 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") + e.target.setAttribute('resizing', 'false') - this.element.dispatchEvent(event); + this.element.dispatchEvent(event) } }