diff --git a/packages/g6/__tests__/demos/behavior-scroll-canvas.ts b/packages/g6/__tests__/demos/behavior-scroll-canvas.ts
new file mode 100644
index 00000000000..0a321d170d3
--- /dev/null
+++ b/packages/g6/__tests__/demos/behavior-scroll-canvas.ts
@@ -0,0 +1,35 @@
+import { Graph } from '@/src';
+import data from '@@/dataset/cluster.json';
+
+export const behaviorScrollCanvas: TestCase = async (context) => {
+ const graph = new Graph({
+ ...context,
+ data,
+ layout: {
+ type: 'd3force',
+ },
+ node: {
+ style: {
+ size: 20,
+ },
+ },
+ behaviors: [
+ {
+ type: 'scroll-canvas',
+ // direction: 'x',
+ // direction: 'y',
+ // sensitivity: 5,
+ // trigger: {
+ // up: ['ArrowUp'],
+ // down: ['ArrowDown'],
+ // right: ['ArrowRight'],
+ // left: ['ArrowLeft'],
+ // },
+ },
+ ],
+ });
+
+ await graph.render();
+
+ return graph;
+};
diff --git a/packages/g6/__tests__/demos/index.ts b/packages/g6/__tests__/demos/index.ts
index a16e14c2ae5..3c77bdb0e80 100644
--- a/packages/g6/__tests__/demos/index.ts
+++ b/packages/g6/__tests__/demos/index.ts
@@ -13,6 +13,7 @@ export * from './behavior-drag-element';
export * from './behavior-focus-element';
export * from './behavior-hover-element';
export * from './behavior-lasso-select';
+export * from './behavior-scroll-canvas';
export * from './behavior-zoom-canvas';
export * from './combo';
export * from './combo-expand-collapse';
diff --git a/packages/g6/__tests__/snapshots/behaviors/behavior-scroll-canvas/default.svg b/packages/g6/__tests__/snapshots/behaviors/behavior-scroll-canvas/default.svg
new file mode 100644
index 00000000000..eac7be699a1
--- /dev/null
+++ b/packages/g6/__tests__/snapshots/behaviors/behavior-scroll-canvas/default.svg
@@ -0,0 +1,2443 @@
+
\ No newline at end of file
diff --git a/packages/g6/__tests__/snapshots/behaviors/scroll-canvas/default.svg b/packages/g6/__tests__/snapshots/behaviors/scroll-canvas/default.svg
new file mode 100644
index 00000000000..15a75a4f98f
--- /dev/null
+++ b/packages/g6/__tests__/snapshots/behaviors/scroll-canvas/default.svg
@@ -0,0 +1,602 @@
+
\ No newline at end of file
diff --git a/packages/g6/__tests__/unit/behaviors/scroll-canvas.spec.ts b/packages/g6/__tests__/unit/behaviors/scroll-canvas.spec.ts
new file mode 100644
index 00000000000..834983d6da6
--- /dev/null
+++ b/packages/g6/__tests__/unit/behaviors/scroll-canvas.spec.ts
@@ -0,0 +1,104 @@
+import { behaviorScrollCanvas } from '@/__tests__/demos';
+import type { Graph } from '@/src';
+import { CommonEvent } from '@/src';
+import { createDemoGraph } from '@@/utils';
+import { isObject } from '@antv/util';
+import { ScrollCanvasOptions } from '../../../src/behaviors';
+
+describe('behavior scroll canvas', () => {
+ let graph: Graph;
+
+ beforeAll(async () => {
+ graph = await createDemoGraph(behaviorScrollCanvas, { animation: false });
+ });
+
+ function setBehavior(options?: ScrollCanvasOptions) {
+ graph.setBehaviors((behaviors) =>
+ behaviors.map((behavior) => {
+ if (isObject(behavior) && behavior.type === 'scroll-canvas') {
+ return { ...behavior, ...options };
+ }
+ return behavior;
+ }),
+ );
+ }
+
+ it('default status', () => {
+ expect(graph.getBehaviors()).toEqual([
+ {
+ type: 'scroll-canvas',
+ },
+ ]);
+ });
+
+ function emitWheelEvent(options?: { deltaX: number; deltaY: number }) {
+ const dom = graph.getCanvas().getContextService().getDomElement();
+ dom?.dispatchEvent(new WheelEvent(CommonEvent.WHEEL, options));
+ }
+
+ it('scroll', async () => {
+ const [x, y] = graph.getPosition();
+ emitWheelEvent({ deltaX: -10, deltaY: -10 });
+ expect(graph.getPosition()).toBeCloseTo([x + 10, y + 10]);
+
+ await expect(graph).toMatchSnapshot(__filename);
+ });
+
+ it('direction', async () => {
+ setBehavior({ direction: 'x' });
+ const [x, y] = graph.getPosition();
+ emitWheelEvent({ deltaX: -10, deltaY: -10 });
+ expect(graph.getPosition()).toBeCloseTo([x + 10, y]);
+
+ setBehavior({ direction: undefined });
+ });
+
+ it('sensitivity', () => {
+ const sensitivity = 5;
+ setBehavior({ sensitivity });
+ const [x, y] = graph.getPosition();
+ const deltaX = -10,
+ deltaY = -10;
+ emitWheelEvent({ deltaX, deltaY });
+ expect(graph.getPosition()).toBeCloseTo([x + Math.abs(deltaX * sensitivity), y + Math.abs(deltaY * sensitivity)]);
+ });
+
+ const shortcutScrollCanvasOptions: ScrollCanvasOptions = {
+ key: 'shortcut-scroll-canvas',
+ type: 'scroll-canvas',
+ trigger: {
+ up: ['ArrowUp'],
+ down: ['ArrowDown'],
+ right: ['ArrowRight'],
+ left: ['ArrowLeft'],
+ },
+ };
+
+ it('custom trigger', () => {
+ graph.setBehaviors((behavior) => [...behavior, shortcutScrollCanvasOptions]);
+
+ let [x, y] = graph.getPosition();
+ graph.emit(CommonEvent.KEY_DOWN, { key: 'ArrowUp' });
+ graph.emit(CommonEvent.KEY_UP, { key: 'ArrowUp' });
+ expect(graph.getPosition()).toBeCloseTo([x, y - 10]);
+
+ [x, y] = graph.getPosition();
+ graph.emit(CommonEvent.KEY_DOWN, { key: 'ArrowDown' });
+ graph.emit(CommonEvent.KEY_UP, { key: 'ArrowDown' });
+ expect(graph.getPosition()).toBeCloseTo([x, y + 10]);
+
+ [x, y] = graph.getPosition();
+ graph.emit(CommonEvent.KEY_DOWN, { key: 'ArrowLeft' });
+ graph.emit(CommonEvent.KEY_UP, { key: 'ArrowLeft' });
+ expect(graph.getPosition()).toBeCloseTo([x - 10, y]);
+
+ [x, y] = graph.getPosition();
+ graph.emit(CommonEvent.KEY_DOWN, { key: 'ArrowRight' });
+ graph.emit(CommonEvent.KEY_UP, { key: 'ArrowRight' });
+ expect(graph.getPosition()).toBeCloseTo([x + 10, y]);
+ });
+
+ it('destroy', () => {
+ graph.destroy();
+ });
+});
diff --git a/packages/g6/src/behaviors/index.ts b/packages/g6/src/behaviors/index.ts
index 370ef0c831a..4386011b9ba 100644
--- a/packages/g6/src/behaviors/index.ts
+++ b/packages/g6/src/behaviors/index.ts
@@ -9,6 +9,7 @@ export { DragElementForce } from './drag-element-force';
export { FocusElement } from './focus-element';
export { HoverElement } from './hover-element';
export { LassoSelect } from './lasso-select';
+export { ScrollCanvas } from './scroll-canvas';
export { ZoomCanvas } from './zoom-canvas';
export type { BaseBehaviorOptions } from './base-behavior';
@@ -22,4 +23,5 @@ export type { DragElementForceOptions } from './drag-element-force';
export type { FocusElementOptions } from './focus-element';
export type { HoverElementOptions } from './hover-element';
export type { LassoSelectOptions } from './lasso-select';
+export type { ScrollCanvasOptions } from './scroll-canvas';
export type { ZoomCanvasOptions } from './zoom-canvas';
diff --git a/packages/g6/src/behaviors/scroll-canvas.ts b/packages/g6/src/behaviors/scroll-canvas.ts
new file mode 100644
index 00000000000..5493c6c599c
--- /dev/null
+++ b/packages/g6/src/behaviors/scroll-canvas.ts
@@ -0,0 +1,141 @@
+import { isFunction, isObject } from '@antv/util';
+import { CanvasEvent } from '../constants';
+import type { RuntimeContext } from '../runtime/types';
+import type { IKeyboardEvent, Point } from '../types';
+import { Shortcut, ShortcutKey } from '../utils/shortcut';
+import type { BaseBehaviorOptions } from './base-behavior';
+import { BaseBehavior } from './base-behavior';
+
+export interface ScrollCanvasOptions extends BaseBehaviorOptions {
+ /**
+ * 是否启用滚动画布的功能
+ *
+ * Whether to enable the function of scrolling the canvas
+ */
+ enable?: boolean | ((event: WheelEvent | IKeyboardEvent) => boolean);
+ /**
+ * 触发滚动的方式,默认使用指针滚动
+ *
+ * The way to trigger scrolling, default to scrolling with the pointer pressed
+ */
+ trigger?: CombinationKey;
+ /**
+ * 允许的滚动方向。选项有:"x"、"y",默认情况下没有限制
+ *
+ * The allowed rolling direction. The options are "x" and "y", with no restrictions by default
+ */
+ direction?: 'x' | 'y';
+ /**
+ * 滚动灵敏度
+ *
+ * Scroll sensitivity
+ */
+ sensitivity?: number;
+ /**
+ * 完成滚动时的回调
+ *
+ * Callback when scrolling is completed
+ */
+ onfinish?: () => void;
+}
+
+type CombinationKey = {
+ up: ShortcutKey;
+ down: ShortcutKey;
+ left: ShortcutKey;
+ right: ShortcutKey;
+};
+
+type EnableOptions = {
+ node?: boolean;
+ edge?: boolean;
+ combo?: boolean;
+};
+
+export class ScrollCanvas extends BaseBehavior {
+ static defaultOptions: ScrollCanvasOptions = {
+ enable: true,
+ sensitivity: 1,
+ };
+
+ private shortcut: Shortcut;
+
+ constructor(context: RuntimeContext, options: ScrollCanvasOptions) {
+ super(context, Object.assign({}, ScrollCanvas.defaultOptions, options));
+
+ this.shortcut = new Shortcut(context.graph);
+
+ this.bindEvents();
+ }
+
+ private bindEvents() {
+ const { trigger } = this.options;
+ this.shortcut.unbindAll();
+ const { graph } = this.context;
+ if (isObject(trigger)) {
+ graph.off(CanvasEvent.WHEEL, this.onWheel);
+ const { up = [], down = [], left = [], right = [] } = trigger;
+
+ this.shortcut.bind(up, (event) => this.scroll([0, -10], event));
+ this.shortcut.bind(down, (event) => this.scroll([0, 10], event));
+ this.shortcut.bind(left, (event) => this.scroll([-10, 0], event));
+ this.shortcut.bind(right, (event) => this.scroll([10, 0], event));
+ } else {
+ /**
+ * 这里必需在原生canvas上绑定wheel事件,参考:
+ * https://g.antv.antgroup.com/api/event/faq#%E5%9C%A8-chrome-%E4%B8%AD%E7%A6%81%E6%AD%A2%E9%A1%B5%E9%9D%A2%E9%BB%98%E8%AE%A4%E6%BB%9A%E5%8A%A8%E8%A1%8C%E4%B8%BA
+ */
+ this.graphDom?.addEventListener(CanvasEvent.WHEEL, this.onWheel, { passive: false });
+ }
+ }
+
+ get graphDom() {
+ return this.context.graph.getCanvas().getContextService().getDomElement();
+ }
+
+ private onWheel = async (event: WheelEvent) => {
+ event.preventDefault();
+ const diffX = event.deltaX;
+ const diffY = event.deltaY;
+
+ await this.scroll([-diffX, -diffY], event);
+ };
+
+ private formatDisplacement([dx, dy]: Point) {
+ const { direction, sensitivity } = this.options;
+
+ dx = dx * sensitivity;
+ dy = dy * sensitivity;
+
+ if (direction === 'x') {
+ dy = 0;
+ } else if (direction === 'y') {
+ dx = 0;
+ }
+
+ return [dx, dy] as Point;
+ }
+
+ private async scroll(value: Point, event: WheelEvent | IKeyboardEvent) {
+ if (!this.validate(event)) return;
+ const { onfinish } = this.options;
+ const graph = this.context.graph;
+ const formattedValue = this.formatDisplacement(value);
+ await graph.translateBy(formattedValue, false);
+ onfinish?.();
+ }
+
+ private validate(event: WheelEvent | IKeyboardEvent) {
+ if (this.destroyed) return false;
+
+ const { enable } = this.options;
+ if (isFunction(enable)) return enable(event);
+ return !!enable;
+ }
+
+ public destroy(): void {
+ this.shortcut.destroy();
+ this.graphDom?.removeEventListener(CanvasEvent.WHEEL, this.onWheel);
+ super.destroy();
+ }
+}
diff --git a/packages/g6/src/behaviors/types.ts b/packages/g6/src/behaviors/types.ts
index b7927848956..3133d13d00b 100644
--- a/packages/g6/src/behaviors/types.ts
+++ b/packages/g6/src/behaviors/types.ts
@@ -1,8 +1,9 @@
import type { BaseBehavior } from './base-behavior';
import type { DragCanvasOptions } from './drag-canvas';
+import type { ScrollCanvasOptions } from './scroll-canvas';
import type { ZoomCanvasOptions } from './zoom-canvas';
export type { States } from './brush-select';
-export type BuiltInBehaviorOptions = DragCanvasOptions | ZoomCanvasOptions;
+export type BuiltInBehaviorOptions = DragCanvasOptions | ZoomCanvasOptions | ScrollCanvasOptions;
export type Behavior = BaseBehavior;
diff --git a/packages/g6/src/registry/build-in.ts b/packages/g6/src/registry/build-in.ts
index b8c1d3f96d2..a54000dc720 100644
--- a/packages/g6/src/registry/build-in.ts
+++ b/packages/g6/src/registry/build-in.ts
@@ -10,6 +10,7 @@ import {
FocusElement,
HoverElement,
LassoSelect,
+ ScrollCanvas,
ZoomCanvas,
} from '../behaviors';
import {
@@ -73,6 +74,7 @@ export const BUILT_IN_EXTENSIONS: ExtensionRegistry = {
'drag-canvas': DragCanvas,
'drag-element': DragElement,
'drag-element-force': DragElementForce,
+ 'scroll-canvas': ScrollCanvas,
'collapse-expand': CollapseExpand,
'click-element': ClickElement,
'hover-element': HoverElement,