@mui/x-internal-gestures
Version:
The core engine of GestureEvents, a modern and robust multi-pointer gesture detection library for JavaScript.
265 lines (241 loc) • 9.07 kB
JavaScript
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault").default;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.PinchGesture = void 0;
var _extends2 = _interopRequireDefault(require("@babel/runtime/helpers/extends"));
var _PointerGesture = require("../PointerGesture");
var _utils = require("../utils");
/**
* PinchGesture - Detects pinch (zoom) movements with two or more pointers
*
* This gesture tracks when multiple pointers move toward or away from each other, firing events when:
* - Two or more pointers begin moving (start)
* - The pointers continue changing distance (ongoing)
* - One or more pointers are released or lifted (end)
*
* This gesture is commonly used to implement zoom functionality in touch interfaces.
*/
/**
* Configuration options for the PinchGesture
* Uses the same options as the base PointerGesture
*/
/**
* Event data specific to pinch gesture events
* Contains information about scale, distance, and velocity
*/
/**
* Type definition for the CustomEvent created by PinchGesture
*/
/**
* State tracking for the PinchGesture
*/
/**
* PinchGesture class for handling pinch/zoom interactions
*
* This gesture detects when users move multiple pointers toward or away from each other,
* and dispatches scale-related events with distance and velocity information.
*/
class PinchGesture extends _PointerGesture.PointerGesture {
constructor(options) {
super((0, _extends2.default)({}, options, {
minPointers: options.minPointers ?? 2
}));
this.state = {
startDistance: 0,
lastDistance: 0,
lastScale: 1,
lastTime: 0,
velocity: 0,
totalScale: 1,
deltaScale: 0
};
this.isSinglePhase = void 0;
this.eventType = void 0;
this.optionsType = void 0;
this.mutableOptionsType = void 0;
this.mutableStateType = void 0;
/**
* Movement threshold in pixels that must be exceeded before the gesture activates.
* Higher values reduce false positive gesture detection for small movements.
*/
this.threshold = void 0;
this.threshold = options.threshold ?? 0;
}
clone(overrides) {
return new PinchGesture((0, _extends2.default)({
name: this.name,
preventDefault: this.preventDefault,
stopPropagation: this.stopPropagation,
threshold: this.threshold,
minPointers: this.minPointers,
maxPointers: this.maxPointers,
requiredKeys: [...this.requiredKeys],
pointerMode: [...this.pointerMode],
preventIf: [...this.preventIf]
}, overrides));
}
destroy() {
this.resetState();
super.destroy();
}
updateOptions(options) {
super.updateOptions(options);
}
resetState() {
this.isActive = false;
this.state = (0, _extends2.default)({}, this.state, {
startDistance: 0,
lastDistance: 0,
lastScale: 1,
lastTime: 0,
velocity: 0,
deltaScale: 0
});
}
/**
* Handle pointer events for the pinch gesture
*/
handlePointerEvent(pointers, event) {
const pointersArray = Array.from(pointers.values());
// Find which element (if any) is being targeted
const targetElement = this.getTargetElement(event);
if (!targetElement) {
return;
}
// Check if this gesture should be prevented by active gestures
if (this.shouldPreventGesture(targetElement)) {
if (this.isActive) {
// If the gesture was active but now should be prevented, end it gracefully
this.emitPinchEvent(targetElement, 'cancel', pointersArray, event);
this.resetState();
}
return;
}
// Filter pointers to only include those targeting our element or its children
const relevantPointers = this.getRelevantPointers(pointersArray, targetElement);
switch (event.type) {
case 'pointerdown':
if (relevantPointers.length >= 2 && !this.isActive) {
// Calculate and store the starting distance between pointers
const initialDistance = (0, _utils.calculateAverageDistance)(relevantPointers);
this.state.startDistance = initialDistance;
this.state.lastDistance = initialDistance;
this.state.lastTime = event.timeStamp;
// Store the original target element
this.originalTarget = targetElement;
}
break;
case 'pointermove':
if (this.state.startDistance && relevantPointers.length >= this.minPointers) {
// Calculate current distance between pointers
const currentDistance = (0, _utils.calculateAverageDistance)(relevantPointers);
// Calculate absolute distance change
const distanceChange = Math.abs(currentDistance - this.state.lastDistance);
// Only proceed if the distance between pointers has changed enough
if (distanceChange !== 0 && distanceChange >= this.threshold) {
// Calculate scale relative to starting distance
const scale = this.state.startDistance ? currentDistance / this.state.startDistance : 1;
// Calculate the relative scale change since last event
const scaleChange = scale / this.state.lastScale;
// Apply this change to the total accumulated scale
this.state.totalScale *= scaleChange;
// Calculate velocity (change in scale over time)
const deltaTime = (event.timeStamp - this.state.lastTime) / 1000; // convert to seconds
if (this.state.lastDistance) {
const deltaDistance = currentDistance - this.state.lastDistance;
const result = deltaDistance / deltaTime;
this.state.velocity = Number.isNaN(result) ? 0 : result;
}
// Update state
this.state.lastDistance = currentDistance;
this.state.deltaScale = scale - this.state.lastScale;
this.state.lastScale = scale;
this.state.lastTime = event.timeStamp;
if (!this.isActive) {
// Mark gesture as active
this.isActive = true;
// Emit start event
this.emitPinchEvent(targetElement, 'start', relevantPointers, event);
this.emitPinchEvent(targetElement, 'ongoing', relevantPointers, event);
} else {
// Emit ongoing event
this.emitPinchEvent(targetElement, 'ongoing', relevantPointers, event);
}
}
}
break;
case 'pointerup':
case 'pointercancel':
case 'forceCancel':
if (this.isActive) {
const remainingPointers = relevantPointers.filter(p => p.type !== 'pointerup' && p.type !== 'pointercancel');
// If we have less than the minimum required pointers, end the gesture
if (remainingPointers.length < this.minPointers) {
if (event.type === 'pointercancel') {
this.emitPinchEvent(targetElement, 'cancel', relevantPointers, event);
}
this.emitPinchEvent(targetElement, 'end', relevantPointers, event);
// Reset state
this.resetState();
} else if (remainingPointers.length >= 2) {
// If we still have enough pointers, update the start distance
// to prevent jumping when a finger is lifted
const newDistance = (0, _utils.calculateAverageDistance)(remainingPointers);
this.state.startDistance = newDistance / this.state.lastScale;
}
}
break;
default:
break;
}
}
/**
* Emit pinch-specific events with additional data
*/
emitPinchEvent(element, phase, pointers, event) {
// Calculate current centroid
const centroid = (0, _utils.calculateCentroid)(pointers);
// Create custom event data
const distance = this.state.lastDistance;
const scale = this.state.lastScale;
// Get list of active gestures
const activeGestures = this.gesturesRegistry.getActiveGestures(element);
const customEventData = {
gestureName: this.name,
centroid,
target: event.target,
srcEvent: event,
phase,
pointers,
timeStamp: event.timeStamp,
scale,
deltaScale: this.state.deltaScale,
totalScale: this.state.totalScale,
distance,
velocity: this.state.velocity,
activeGestures,
direction: (0, _utils.getPinchDirection)(this.state.velocity),
customData: this.customData
};
// Handle default event behavior
if (this.preventDefault) {
event.preventDefault();
}
if (this.stopPropagation) {
event.stopPropagation();
}
// Event names to trigger
const eventName = (0, _utils.createEventName)(this.name, phase);
// Dispatch custom events on the element
const domEvent = new CustomEvent(eventName, {
bubbles: true,
cancelable: true,
composed: true,
detail: customEventData
});
element.dispatchEvent(domEvent);
}
}
exports.PinchGesture = PinchGesture;