/* * Copyright (C) 2021 Apple Inc. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF * THE POSSIBILITY OF SUCH DAMAGE. */ WI.GestureController = class GestureController { constructor(target, delegate, {container, supportsScale, supportsTranslate}) { console.assert(target instanceof Node, target); console.assert(!container || container instanceof Node, container); console.assert(!supportsScale || typeof delegate.gestureControllerDidScale === "function", delegate.gestureControllerDidScale); console.assert(!supportsTranslate || typeof delegate.gestureControllerDidTranslate === "function", delegate.gestureControllerDidTranslate); console.assert(supportsScale || supportsTranslate, "expects at least one gesture"); this._target = target; this._delegate = delegate; this._scale = 1; this._translate = {x: 0, y: 0}; this._mouseWheelDelta = 0; container ||= target; this._supportsScale = supportsScale || false; if (this._supportsScale) { container.addEventListener("wheel", this._handleWheel.bind(this)); container.addEventListener("gesturestart", this._handleGestureStart.bind(this)); container.addEventListener("gesturechange", this._handleGestureChange.bind(this)); container.addEventListener("gestureend", this._handleGestureEnd.bind(this)); } this._supportsTranslate = supportsTranslate || false; if (this._supportsTranslate) { console.assert(!container.draggable, "cannot have both a translate gesture and dragging"); container.addEventListener("mousedown", this._handleMouseDown.bind(this)); } } // Public get scale() { return this._scale; } set scale(scale) { console.assert(this._supportsScale); scale = Number.constrain(scale, 0.01, 100); if (scale === this._scale) return; this._scale = scale; this._delegate.gestureControllerDidScale(this); } get translate() { return this._translate; } set translate(translate) { console.assert(this._supportsTranslate); if (translate.x === this._translate.x && translate.y === this._translate.y) return; this._translate = translate; this._delegate.gestureControllerDidTranslate(this); } reset() { this.scale = 1; this.translate = {x: 0, y: 0}; this._mouseWheelDelta = 0; } // Private _startScaleInteraction(event) { this._scaleInteractionStartScale = this._scale; if (this._supportsTranslate) this._scaleInteractionStartTranslate = this._translate; if (event.target === this._target) { let elementBounds = this._target.getBoundingClientRect(); this._scaleInteractionStartPosition = { x: (event.pageX - elementBounds.left - (elementBounds.width / 2)) / this._scaleInteractionStartScale, y: (event.pageY - elementBounds.top - (elementBounds.height / 2)) / this._scaleInteractionStartScale, }; } else this._scaleInteractionStartPosition = {x: 0, y: 0}; } _updateScaleInteraction(scale) { this.scale = this._scaleInteractionStartScale * scale; if (this._supportsTranslate) { this.translate = { x: this._scaleInteractionStartTranslate.x - (this._scaleInteractionStartPosition.x * (this._scale - this._scaleInteractionStartScale)), y: this._scaleInteractionStartTranslate.y - (this._scaleInteractionStartPosition.y * (this._scale - this._scaleInteractionStartScale)), }; } } _endScaleInteraction() { this._scaleInteractionStartScale = NaN; if (this._supportsTranslate) this._scaleInteractionStartTranslate = null; this._scaleInteractionStartPosition = null; } _handleWheel(event) { // Ignore wheel events while handing gestures. if (this._handlingGesture) return; // Require twice the vertical delta to overcome horizontal scrolling. // This prevents most cases of inadvertent zooming for slightly diagonal scrolls. if (Math.abs(event.deltaX) >= Math.abs(event.deltaY) * 0.5) return; let deviceDirection = event.webkitDirectionInvertedFromDevice ? -1 : 1; let delta = (event.deltaZ || event.deltaY || event.deltaX) * deviceDirection / 1000; // Reset accumulated wheel delta when direction changes. if (delta < 0 && this._mouseWheelDelta >= 0 || delta >= 0 && this._mouseWheelDelta < 0) this._mouseWheelDelta = 0; this._mouseWheelDelta += delta; this._startScaleInteraction(event); this._updateScaleInteraction(1 - this._mouseWheelDelta); this._endScaleInteraction(); event.preventDefault(); event.stopPropagation(); } _handleGestureStart(event) { console.assert(!this._handlingGesture); this._handlingGesture = true; this._startScaleInteraction(event); event.preventDefault(); event.stopPropagation(); } _handleGestureChange(event) { console.assert(this._handlingGesture); this._updateScaleInteraction(event.scale); event.preventDefault(); event.stopPropagation(); } _handleGestureEnd(event) { console.assert(this._handlingGesture); this._handlingGesture = false; this._endScaleInteraction(); } _handleMouseDown(event) { if (event.target.draggable) return; if (event.button !== 0) return; this._translateInteractionStartTranslate = this._translate; this._translateInteractionStartPosition = { x: event.pageX, y: event.pageY, }; console.assert(!this._boundHandleMouseMove); this._boundHandleMouseMove = this._handleMouseMove.bind(this); window.addEventListener("mousemove", this._boundHandleMouseMove, {capture: true}); console.assert(!this._boundHandleMouseUp); this._boundHandleMouseUp = this._handleMouseUp.bind(this); window.addEventListener("mouseup", this._boundHandleMouseUp, {capture: true}); } _handleMouseMove(event) { this.translate = { x: this._translateInteractionStartTranslate.x + (event.pageX - this._translateInteractionStartPosition.x), y: this._translateInteractionStartTranslate.y + (event.pageY - this._translateInteractionStartPosition.y), }; } _handleMouseUp(event) { window.removeEventListener("mousemove", this._boundHandleMouseMove, {capture: true}); this._boundHandleMouseMove = null; window.removeEventListener("mouseup", this._boundHandleMouseUp, {capture: true}); this._boundHandleMouseUp = null; this._translateInteractionStartTranslate = null; this._translateInteractionStartPosition = null; } };