blob: d55b2100c3cc90cca82e2a05dfd5123c52e0f240 [file] [log] [blame]
/*
* 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;
}
};