import EventEmitter from '@antv/event-emitter';
import { CommonEvent } from '../constants';
import { IPointerEvent } from '../types';
/**
* 表示指针位置的点坐标
*
* Represents the coordinates of a pointer position
*/
export interface PointerPoint {
x: number;
y: number;
pointerId: number;
}
/**
* 捏合事件参数
*
* Pinch event parameters
* @remarks
* 包含与捏合手势相关的参数,当前支持缩放比例,未来可扩展中心点坐标、旋转角度等参数
*
* Contains parameters related to pinch gestures, currently supports scale factor,
* can be extended with center coordinates, rotation angle etc. in the future
*/
export interface PinchEventOptions {
/**
* 缩放比例因子,>1 表示放大,<1 表示缩小
*
* Scaling factor, >1 indicates zoom in, <1 indicates zoom out
*/
scale: number;
}
/**
* 捏合手势阶段类型
* Pinch gesture phase type
* @remarks
* 包含三个手势阶段:
* - start: 手势开始
* - move: 手势移动中
* - end: 手势结束
*
* Contains three gesture phases:
* - pinchstart: Gesture started
* - pinchmove: Gesture in progress
* - pinchend: Gesture ended
*/
export type PinchEvent = 'pinchstart' | 'pinchmove' | 'pinchend';
/**
* 捏合手势回调函数类型
*
* Pinch gesture callback function type
* @param event - 原始指针事件对象 | Original pointer event object
* @param options - 捏合事件参数对象 | Pinch event parameters object
*/
export type PinchCallback = (event: IPointerEvent, options: PinchEventOptions) => void;
/**
* 捏合手势处理器
*
* Pinch gesture handler
* @remarks
* 处理双指触摸事件,计算缩放比例并触发回调。通过跟踪两个触摸点的位置变化,计算两点间距离变化率来确定缩放比例。
*
* Handles two-finger touch events, calculates zoom ratio and triggers callbacks. Tracks position changes of two touch points to determine zoom ratio based on distance variation.
*/
export class PinchHandler {
/**
* 是否处于 Pinch 阶段
*
* Whether it is in the Pinch stage
*/
public static isPinching: boolean = false;
/**
* 当前跟踪的触摸点集合
*
* Currently tracked touch points collection
*/
private pointerByTouch: PointerPoint[] = [];
/**
* 初始两点间距离
*
* Initial distance between two points
*/
private initialDistance: number | null = null;
private emitter: EventEmitter;
private static instance: PinchHandler | null = null;
private static callbacks: {
pinchstart: PinchCallback[];
pinchmove: PinchCallback[];
pinchend: PinchCallback[];
} = { pinchstart: [], pinchmove: [], pinchend: [] };
constructor(
emitter: EventEmitter,
private phase: PinchEvent,
callback: PinchCallback,
) {
this.emitter = emitter;
if (PinchHandler.instance) {
PinchHandler.callbacks[this.phase].push(callback);
return PinchHandler.instance;
}
this.onPointerDown = this.onPointerDown.bind(this);
this.onPointerMove = this.onPointerMove.bind(this);
this.onPointerUp = this.onPointerUp.bind(this);
this.bindEvents();
PinchHandler.instance = this;
PinchHandler.callbacks[this.phase].push(callback);
}
private bindEvents() {
const { emitter } = this;
emitter.on(CommonEvent.POINTER_DOWN, this.onPointerDown);
emitter.on(CommonEvent.POINTER_MOVE, this.onPointerMove);
emitter.on(CommonEvent.POINTER_UP, this.onPointerUp);
}
/**
* 更新指定指针的位置
*
* Update position of specified pointer
* @param pointerId - 指针唯一标识符 | Pointer unique identifier1
* @param x - 新的X坐标 | New X coordinate
* @param y - 新的Y坐标 | New Y coordinate
*/
private updatePointerPosition(pointerId: number, x: number, y: number) {
const index = this.pointerByTouch.findIndex((p) => p.pointerId === pointerId);
if (index >= 0) {
this.pointerByTouch[index] = { x, y, pointerId };
}
}
/**
* 处理指针按下事件
*
* Handle pointer down event
* @param event - 指针事件对象 | Pointer event object
* @remarks
* 当检测到两个触摸点时记录初始距离
*
* Record initial distance when detecting two touch points
*/
onPointerDown(event: IPointerEvent) {
const { x, y } = event.client || {};
if (x === undefined || y === undefined) return;
this.pointerByTouch.push({ x, y, pointerId: event.pointerId });
if (event.pointerType === 'touch' && this.pointerByTouch.length === 2) {
PinchHandler.isPinching = true;
const dx = this.pointerByTouch[0].x - this.pointerByTouch[1].x;
const dy = this.pointerByTouch[0].y - this.pointerByTouch[1].y;
this.initialDistance = Math.sqrt(dx * dx + dy * dy);
PinchHandler.callbacks.pinchstart.forEach((cb) => cb(event, { scale: 0 }));
}
}
/**
* 处理指针移动事件
*
* Handle pointer move event
* @param event - 指针事件对象 | Pointer event object
* @remarks
* 当存在两个有效触摸点时计算缩放比例
*
* Calculate zoom ratio when two valid touch points exist
*/
onPointerMove(event: IPointerEvent) {
if (this.pointerByTouch.length !== 2 || this.initialDistance === null) return;
const { x, y } = event.client || {};
if (x === undefined || y === undefined) return;
this.updatePointerPosition(event.pointerId, x, y);
const dx = this.pointerByTouch[0].x - this.pointerByTouch[1].x;
const dy = this.pointerByTouch[0].y - this.pointerByTouch[1].y;
const currentDistance = Math.sqrt(dx * dx + dy * dy);
const ratio = currentDistance / this.initialDistance;
PinchHandler.callbacks.pinchmove.forEach((cb) => cb(event, { scale: (ratio - 1) * 5 }));
}
/**
* 处理指针抬起事件
*
* Handle pointer up event
* @param event
* @remarks
* 重置触摸状态和初始距离
*
* Reset touch state and initial distance
*/
onPointerUp(event: IPointerEvent) {
PinchHandler.callbacks.pinchend.forEach((cb) => cb(event, { scale: 0 }));
PinchHandler.isPinching = false;
this.initialDistance = null;
this.pointerByTouch = [];
PinchHandler.instance?.tryDestroy();
}
/**
* 销毁捏合手势相关监听
*
* Destroy pinch gesture listeners
* @remarks
* 移除指针按下、移动、抬起事件的监听
*
* Remove listeners for pointer down, move, and up events
*/
public destroy() {
this.emitter.off(CommonEvent.POINTER_DOWN, this.onPointerDown);
this.emitter.off(CommonEvent.POINTER_MOVE, this.onPointerMove);
this.emitter.off(CommonEvent.POINTER_UP, this.onPointerUp);
PinchHandler.instance = null;
}
/**
* 解绑指定阶段的手势回调
* Unregister gesture callback for specific phase
* @param phase - 手势阶段:开始(pinchstart)/移动(pinchmove)/结束(pinchend) | Gesture phase: start/move/end
* @param callback - 要解绑的回调函数 | Callback function to unregister
* @remarks
* 从指定阶段的回调列表中移除特定回调,当所有回调都解绑后自动销毁事件监听
* Remove specific callback from the phase's callback list, auto-destroy event listeners when all callbacks are unregistered
*/
public off(phase: PinchEvent, callback: PinchCallback) {
const index = PinchHandler.callbacks[phase].indexOf(callback);
if (index > -1) PinchHandler.callbacks[phase].splice(index, 1);
this.tryDestroy();
}
/**
* 尝试销毁手势处理器
* Attempt to destroy the gesture handler
* @remarks
* 当所有阶段(开始/移动/结束)的回调列表都为空时,执行实际销毁操作
* Perform actual destruction when all phase (pinchstart/pinchmove/pinchend) callback lists are empty
* 自动解除事件监听并重置单例实例
* Automatically remove event listeners and reset singleton instance
*/
private tryDestroy() {
if (Object.values(PinchHandler.callbacks).every((arr) => arr.length === 0)) {
this.destroy();
}
}
}