UNPKG

@mui/x-internal-gestures

Version:

The core engine of GestureEvents, a modern and robust multi-pointer gesture detection library for JavaScript.

259 lines (232 loc) 8.29 kB
import _extends from "@babel/runtime/helpers/esm/extends"; /** * RotateGesture - Detects rotation movements between two or more pointers * * This gesture tracks when multiple pointers rotate around a common center point, firing events when: * - Two or more pointers begin a rotation motion (start) * - The pointers continue rotating (ongoing) * - One or more pointers are released or lifted (end) * * This gesture is commonly used for rotation controls in drawing or image manipulation interfaces. */ import { PointerGesture } from "../PointerGesture.js"; import { calculateCentroid, calculateRotationAngle, createEventName } from "../utils/index.js"; /** * Configuration options for the RotateGesture * Uses the same options as the base PointerGesture */ /** * Event data specific to rotate gesture events * Contains information about rotation angle, delta, and velocity */ /** * Type definition for the CustomEvent created by RotateGesture */ /** * State tracking for the RotateGesture */ /** * RotateGesture class for handling rotation interactions * * This gesture detects when users rotate multiple pointers around a central point, * and dispatches rotation-related events with angle and angular velocity information. */ export class RotateGesture extends PointerGesture { // eslint-disable-next-line @typescript-eslint/no-useless-constructor constructor(options) { super(options); this.state = { startAngle: 0, lastAngle: 0, totalRotation: 0, lastTime: 0, velocity: 0, lastDelta: 0 }; this.isSinglePhase = void 0; this.eventType = void 0; this.optionsType = void 0; this.mutableOptionsType = void 0; this.mutableStateType = void 0; } clone(overrides) { return new RotateGesture(_extends({ name: this.name, preventDefault: this.preventDefault, stopPropagation: this.stopPropagation, 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 = _extends({}, this.state, { startAngle: 0, lastAngle: 0, lastTime: 0, velocity: 0, lastDelta: 0 }); } /** * Handle pointer events for the rotate 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.emitRotateEvent(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); // Check if we have enough pointers for a rotation (at least 2) if (relevantPointers.length < this.minPointers || relevantPointers.length > this.maxPointers) { if (this.isActive) { // End the gesture if it was active this.emitRotateEvent(targetElement, 'end', relevantPointers, event); this.resetState(); } return; } switch (event.type) { case 'pointerdown': if (relevantPointers.length >= 2 && !this.isActive) { // Calculate and store the starting angle const initialAngle = calculateRotationAngle(relevantPointers); this.state.startAngle = initialAngle; this.state.lastAngle = initialAngle; this.state.lastTime = event.timeStamp; // Store the original target element this.originalTarget = targetElement; } break; case 'pointermove': if (relevantPointers.length >= 2) { // Calculate current rotation angle const currentAngle = calculateRotationAngle(relevantPointers); // Calculate rotation delta (change in angle) let delta = currentAngle - this.state.lastAngle; // Adjust for angle wrapping (event.g., from 359° to 0°) if (delta > 180) { delta -= 360; } if (delta < -180) { delta += 360; } // Store the delta for use in emitRotateEvent this.state.lastDelta = delta; // Update rotation value (cumulative) this.state.totalRotation += delta; // Calculate angular velocity (degrees per second) const deltaTime = (event.timeStamp - this.state.lastTime) / 1000; // convert to seconds if (deltaTime > 0) { this.state.velocity = delta / deltaTime; } // Update state this.state.lastAngle = currentAngle; this.state.lastTime = event.timeStamp; // Emit ongoing event if there's an actual rotation // We don't want to emit events for tiny movements that might be just noise if (Math.abs(delta) <= 0.1) { return; } if (!this.isActive) { this.isActive = true; // Emit start event this.emitRotateEvent(targetElement, 'start', relevantPointers, event); this.emitRotateEvent(targetElement, 'ongoing', relevantPointers, event); } else { this.emitRotateEvent(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.emitRotateEvent(targetElement, 'cancel', relevantPointers, event); } this.emitRotateEvent(targetElement, 'end', relevantPointers, event); // Reset state this.resetState(); } else if (remainingPointers.length >= 2) { // If we still have enough pointers, update the start angle // to prevent jumping when a finger is lifted const newAngle = calculateRotationAngle(remainingPointers); this.state.startAngle = newAngle - this.state.totalRotation; this.state.lastAngle = newAngle; } } break; default: break; } } /** * Emit rotate-specific events with additional data */ emitRotateEvent(element, phase, pointers, event) { // Calculate current centroid const centroid = calculateCentroid(pointers); // Create custom event data const rotation = this.state.totalRotation; // 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, rotation, delta: this.state.lastDelta, totalRotation: this.state.totalRotation, velocity: this.state.velocity, activeGestures, customData: this.customData }; // Handle default event behavior if (this.preventDefault) { event.preventDefault(); } if (this.stopPropagation) { event.stopPropagation(); } // Event names to trigger const eventName = 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); } }