UNPKG

@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
"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;