Skip to content

Commit

Permalink
feat: support the canvas zoom event on mobile devices (#6768)
Browse files Browse the repository at this point in the history
* feat: support the canvas zoom event on mobile devices

* chore: remove code of mobile, unify the canvas zooming logic

* chore: delete redundant SVG files for zoom canvas

* chore: update variable name of pointer event

* fix: unique zoom canvas interface of g6 and g6-extension-3d

* refactor(behavior): refactor code of zoom canvas for pointer event

* refactor(behavior): defined pinch event and refactor shortcut code

* chore: optimize code naming and parameter definition

* chore: optimize bind code of shortcut
  • Loading branch information
zhongyunWan authored Feb 14, 2025
1 parent 340b4ea commit c354b72
Show file tree
Hide file tree
Showing 8 changed files with 1,372 additions and 11 deletions.
10 changes: 8 additions & 2 deletions packages/g6-extension-3d/src/behaviors/zoom-canvas-3d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import type { IKeyboardEvent, IWheelEvent, ViewportAnimationEffectTiming, ZoomCanvasOptions } from '@antv/g6';
import type {
IKeyboardEvent,
IPointerEvent,
IWheelEvent,
ViewportAnimationEffectTiming,
ZoomCanvasOptions,
} from '@antv/g6';
import { ZoomCanvas } from '@antv/g6';
import { clamp } from '@antv/util';

Expand All @@ -17,7 +23,7 @@ export interface ZoomCanvas3DOptions extends ZoomCanvasOptions {}
export class ZoomCanvas3D extends ZoomCanvas {
protected zoom = async (
value: number,
event: IWheelEvent | IKeyboardEvent,
event: IWheelEvent | IKeyboardEvent | IPointerEvent,
animation: ViewportAnimationEffectTiming | undefined,
) => {
if (!this.validate(event)) return;
Expand Down
538 changes: 538 additions & 0 deletions packages/g6/__tests__/snapshots/behaviors/zoom-canvas/mobile-final.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
72 changes: 72 additions & 0 deletions packages/g6/__tests__/unit/behaviors/zoom-canvas.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,78 @@ describe('behavior zoom canvas', () => {
expect(graph.getZoom()).toBeCloseTo(currentZoom * 0.95 ** 2);
});

it('mobile zoom', async () => {
const initZoom = graph.getZoom();
const canvas = graph.getCanvas();
const container = canvas.getContainer();
if (!container) return;

const initialBehaviors = graph.getBehaviors();
graph.setBehaviors([{ type: 'zoom-canvas' }, { type: 'zoom-canvas', trigger: ['pinch'] }]);

const pointerdownListener = jest.fn();
const pointermoveListener = jest.fn();

const pointerByTouch = [
{
client: {
x: 100,
y: 100,
},
pointerId: 1,
pointerType: 'touch',
},
{
client: {
x: 200,
y: 200,
},
pointerId: 2,
pointerType: 'touch',
},
];

const dxForInitial = pointerByTouch[0].client.x - pointerByTouch[1].client.x;
const dyForInitial = pointerByTouch[0].client.y - pointerByTouch[1].client.y;
const initialDistance = Math.sqrt(dxForInitial * dxForInitial + dyForInitial * dyForInitial);

await expect(graph).toMatchSnapshot(__filename, 'mobile-initial');

graph.once('canvas:pointerdown', pointerdownListener);
canvas.document.emit(CommonEvent.POINTER_DOWN, { client: { x: 100, y: 100 } });
expect(pointerdownListener).toHaveBeenCalledTimes(1);

graph.once('canvas:pointermove', pointermoveListener);
canvas.document.emit(CommonEvent.POINTER_MOVE, { client: { x: 200, y: 200 } });
expect(pointermoveListener).toHaveBeenCalledTimes(1);

pointerByTouch[1] = {
client: {
x: 250,
y: 250,
},
pointerId: 2,
pointerType: 'touch',
};

const dxForMove = pointerByTouch[0].client.x - pointerByTouch[1].client.x;
const dyForMove = pointerByTouch[0].client.y - pointerByTouch[1].client.y;
const currentDistance = Math.sqrt(dxForMove * dxForMove + dyForMove * dyForMove);
const ratio = currentDistance / initialDistance;
const value = (ratio - 1) * 5;

await graph.zoomTo(initZoom * value, false, undefined);
expect(graph.getZoom()).not.toBe(initZoom);

await expect(graph).toMatchSnapshot(__filename, 'mobile-final');

await graph.zoomTo(initZoom, false, undefined);
expect(graph.getZoom()).toBe(initZoom);

graph.setBehaviors(initialBehaviors);
expect(graph.getBehaviors()).toEqual([{ type: 'zoom-canvas' }]);
});

const shortcutZoomCanvasOptions: ZoomCanvasOptions = {
key: 'shortcut-zoom-canvas',
type: 'zoom-canvas',
Expand Down
32 changes: 23 additions & 9 deletions packages/g6/src/behaviors/zoom-canvas.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import { clamp, isFunction } from '@antv/util';
import { CommonEvent } from '../constants';
import type { RuntimeContext } from '../runtime/types';
import type { IKeyboardEvent, IWheelEvent, Point, PointObject, ViewportAnimationEffectTiming } from '../types';
import type {
IKeyboardEvent,
IPointerEvent,
IWheelEvent,
Point,
PointObject,
ViewportAnimationEffectTiming,
} from '../types';
import { parsePoint } from '../utils/point';
import type { ShortcutKey } from '../utils/shortcut';
import { Shortcut } from '../utils/shortcut';
Expand All @@ -27,7 +34,7 @@ export interface ZoomCanvasOptions extends BaseBehaviorOptions {
* <en/> Whether to enable the function of zooming the canvas
* @defaultValue true
*/
enable?: boolean | ((event: IWheelEvent | IKeyboardEvent) => boolean);
enable?: boolean | ((event: IWheelEvent | IKeyboardEvent | IPointerEvent) => boolean);
/**
* <zh/> 触发缩放的方式
* - ShortcutKey:组合快捷键,**默认使用滚轮缩放**,['Control'] 表示按住 Control 键滚动鼠标滚轮时触发缩放
Expand Down Expand Up @@ -107,11 +114,18 @@ export class ZoomCanvas extends BaseBehavior<ZoomCanvasOptions> {
this.shortcut.unbindAll();

if (Array.isArray(trigger)) {
this.context.canvas.getContainer()?.addEventListener(CommonEvent.WHEEL, this.preventDefault);
this.shortcut.bind([...trigger, CommonEvent.WHEEL], (event) => {
const { deltaX, deltaY } = event;
this.zoom(-(deltaY ?? deltaX), event, false);
});
if (trigger.includes(CommonEvent.PINCH)) {
this.shortcut.bind([CommonEvent.PINCH], (event) => {
this.zoom(event.scale, event, false);
});
} else {
const container = this.context.canvas.getContainer();
container?.addEventListener(CommonEvent.WHEEL, this.preventDefault);
this.shortcut.bind([...trigger, CommonEvent.WHEEL], (event) => {
const { deltaX, deltaY } = event;
this.zoom(-(deltaY ?? deltaX), event, false);
});
}
}

if (typeof trigger === 'object') {
Expand Down Expand Up @@ -140,7 +154,7 @@ export class ZoomCanvas extends BaseBehavior<ZoomCanvasOptions> {
*/
protected zoom = async (
value: number,
event: IWheelEvent | IKeyboardEvent,
event: IWheelEvent | IKeyboardEvent | IPointerEvent,
animation: ZoomCanvasOptions['animation'],
) => {
if (!this.validate(event)) return;
Expand Down Expand Up @@ -171,7 +185,7 @@ export class ZoomCanvas extends BaseBehavior<ZoomCanvasOptions> {
* @returns <zh/> 是否可以缩放 | <en/> Whether it can be zoomed
* @internal
*/
protected validate(event: IWheelEvent | IKeyboardEvent) {
protected validate(event: IWheelEvent | IKeyboardEvent | IPointerEvent) {
if (this.destroyed) return false;
const { enable } = this.options;
if (isFunction(enable)) return enable(event);
Expand Down
6 changes: 6 additions & 0 deletions packages/g6/src/constants/events/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,4 +119,10 @@ export enum CommonEvent {
* <en/> Triggered when scrolling
*/
WHEEL = 'wheel',
/**
* <zh/> 双指捏拢或张开时触发
*
* <en/> Triggered when pinch in and pinch out
*/
PINCH = 'pinch',
}
176 changes: 176 additions & 0 deletions packages/g6/src/utils/pinch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import EventEmitter from '@antv/event-emitter';
import { CommonEvent } from '../constants';
import { IPointerEvent } from '../types';

/**
* <zh/> 表示指针位置的点坐标
*
* <en/> Represents the coordinates of a pointer position
*/
export interface PointerPoint {
x: number;
y: number;
pointerId: number;
}

/**
* <zh/> 捏合事件参数
*
* <en/> Pinch event parameters
* @remarks
* <zh/> 包含与捏合手势相关的参数,当前支持缩放比例,未来可扩展中心点坐标、旋转角度等参数
*
* <en/> 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 {
/**
* <zh/> 缩放比例因子,>1 表示放大,<1 表示缩小
*
* <en/> Scaling factor, >1 indicates zoom in, <1 indicates zoom out
*/
scale: number;
}

/**
* <zh/> 捏合手势回调函数类型
*
* <en/> Pinch gesture callback function type
* @param event - <zh/> 原始指针事件对象 | <en/> Original pointer event object
* @param options - <zh/> 捏合事件参数对象 | <en/> Pinch event parameters object
*/
export type PinchCallback = (event: IPointerEvent, options: PinchEventOptions) => void;

/**
* <zh/> 捏合手势处理器
*
* <en/> Pinch gesture handler
* @remarks
* <zh/> 处理双指触摸事件,计算缩放比例并触发回调。通过跟踪两个触摸点的位置变化,计算两点间距离变化率来确定缩放比例。
*
* <en/> 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 {
/**
* <zh/> 当前跟踪的触摸点集合
*
* <en/> Currently tracked touch points collection
*/
private pointerByTouch: PointerPoint[] = [];

/**
* <zh/> 初始两点间距离
*
* <en/> Initial distance between two points
*/
private initialDistance: number | null = null;

private emitter: EventEmitter;

constructor(
emitter: EventEmitter,
private callback: PinchCallback,
) {
this.emitter = emitter;
this.onPointerDown = this.onPointerDown.bind(this);
this.onPointerMove = this.onPointerMove.bind(this);
this.onPointerUp = this.onPointerUp.bind(this);
this.bindEvents();
}

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);
}

/**
* <zh/> 更新指定指针的位置
*
* <en/> Update position of specified pointer
* @param pointerId - <zh/> 指针唯一标识符 | <en/> Pointer unique identifier<sup>1</sup>
* @param x - <zh/> 新的X坐标 | <en/> New X coordinate
* @param y - <zh/> 新的Y坐标 | <en/> 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 };
}
}

/**
* <zh/> 处理指针按下事件
*
* <en/> Handle pointer down event
* @param event - <zh/> 指针事件对象 | <en/> Pointer event object
* @remarks
* <zh/> 当检测到两个触摸点时记录初始距离
*
* <en/> 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) {
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);
}
}

/**
* <zh/> 处理指针移动事件
*
* <en/> Handle pointer move event
* @param event - <zh/> 指针事件对象 | <en/> Pointer event object
* @remarks
* <zh/> 当存在两个有效触摸点时计算缩放比例
*
* <en/> 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;

this.callback(event, { scale: (ratio - 1) * 5 });
}

/**
* <zh/> 处理指针抬起事件
*
* <en/> Handle pointer up event
* @remarks
* <zh/> 重置触摸状态和初始距离
*
* <en/> Reset touch state and initial distance
*/
onPointerUp() {
this.initialDistance = null;
this.pointerByTouch = [];
}

/**
* <zh/> 销毁捏合手势相关监听
*
* <en/> Destroy pinch gesture listeners
* @remarks
* <zh/> 移除指针按下、移动、抬起事件的监听
*
* <en/> Remove listeners for pointer down, move, and up events
*/
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);
}
}
Loading

0 comments on commit c354b72

Please sign in to comment.