From 2fd8fa8e6f546bad0cb93eee4e41399faafaaf1a Mon Sep 17 00:00:00 2001 From: zhe-he Date: Tue, 11 Nov 2025 14:17:36 +0800 Subject: [PATCH 1/2] feat(): Add scrollbars to extensions --- CHANGELOG.md | 1 + e2e/tests/scrollbars/index.spec.ts | 28 ++ .../index.spec.ts-snapshots/pan100.png | Bin 0 -> 935 bytes .../index.spec.ts-snapshots/zoom-double.png | Bin 0 -> 887 bytes .../index.spec.ts-snapshots/zoom-half.png | Bin 0 -> 809 bytes e2e/tests/scrollbars/index.ts | 37 +++ extensions/index.ts | 3 + extensions/scrollbars/README.md | 36 +++ extensions/scrollbars/index.ts | 274 ++++++++++++++++++ extensions/scrollbars/typedefs.ts | 30 ++ 10 files changed, 409 insertions(+) create mode 100644 e2e/tests/scrollbars/index.spec.ts create mode 100644 e2e/tests/scrollbars/index.spec.ts-snapshots/pan100.png create mode 100644 e2e/tests/scrollbars/index.spec.ts-snapshots/zoom-double.png create mode 100644 e2e/tests/scrollbars/index.spec.ts-snapshots/zoom-half.png create mode 100644 e2e/tests/scrollbars/index.ts create mode 100644 extensions/scrollbars/README.md create mode 100644 extensions/scrollbars/index.ts create mode 100644 extensions/scrollbars/typedefs.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index a75350be853..5e66073b8d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## [next] +- feat(): Add scrollbars to extensions [#10371](https://github.com/fabricjs/fabric.js/discussions/10371) - fix(): BREAKING Fix text positioning [#10803](https://github.com/fabricjs/fabric.js/pull/10803) - fix(AligningGuidelines): Guidelines features updates [#10120] (https://github.com/fabricjs/fabric.js/pull/10120) - chore(deps-dev): bump inquirer from 12.9.6 to 12.10.0 [#10789](https://github.com/fabricjs/fabric.js/pull/10789) diff --git a/e2e/tests/scrollbars/index.spec.ts b/e2e/tests/scrollbars/index.spec.ts new file mode 100644 index 00000000000..553af7c236d --- /dev/null +++ b/e2e/tests/scrollbars/index.spec.ts @@ -0,0 +1,28 @@ +import { expect, test } from '../../fixtures/base'; + +test('Scrollbars', async ({ page, canvasUtil }) => { + await test.step('zoom canvas', async () => { + await canvasUtil.executeInBrowser((canvas) => { + canvas.setZoom(2); + }); + expect(await canvasUtil.screenshot()).toMatchSnapshot({ + name: 'zoom-double.png', + }); + await canvasUtil.executeInBrowser((canvas) => { + canvas.setZoom(0.5); + }); + expect(await canvasUtil.screenshot()).toMatchSnapshot({ + name: 'zoom-half.png', + }); + }); + + await test.step('pan canvas', async () => { + await canvasUtil.executeInBrowser((canvas) => { + canvas.setZoom(1); + canvas.absolutePan(new window.fabric.Point(100, 100)); + }); + expect(await canvasUtil.screenshot()).toMatchSnapshot({ + name: 'pan100.png', + }); + }); +}); diff --git a/e2e/tests/scrollbars/index.spec.ts-snapshots/pan100.png b/e2e/tests/scrollbars/index.spec.ts-snapshots/pan100.png new file mode 100644 index 0000000000000000000000000000000000000000..ab497d16b81f81dc0f32913d59fd4cb6c841485d GIT binary patch literal 935 zcmeAS@N?(olHy`uVBq!ia0y~yV4MJCPvc+$lJ%cGjTji1V?13PLn`LHxn-L*+gqaT z;q_xj1J(b_KXzLYwovwn^$DR(BJ7pOje{(Sb|lP6Dpo5%@_wy$5mK79BvX`_X$?cQCxZrNE?m=^vmJ^i%E zZ1&x|cSSd(>*?vW9Zp<-z4|r#S>K4sCsVBEifyQ$H-G+a!7LN0y=SS%FMBSBr z_PLVr_p6T|Kl1DTl3df=-2ChIF6)x`wXAPeKYsk!e#?rT{)gn>=koLOv%Z;}w_(?w zJvCoX{qrjBf5(vjaXT9<$$yk(VEF%kkL}gx!0g5b%4f_BMN@Nk%uEe81Bx(sy85}S Ib4q9e00{7D6aWAK literal 0 HcmV?d00001 diff --git a/e2e/tests/scrollbars/index.spec.ts-snapshots/zoom-double.png b/e2e/tests/scrollbars/index.spec.ts-snapshots/zoom-double.png new file mode 100644 index 0000000000000000000000000000000000000000..c981b849c7708b9d8fadb2ecca831031624ddcff GIT binary patch literal 887 zcmeAS@N?(olHy`uVBq!ia0y~yV4MJCPvc+$lJ%cGjTji1bv#`hLn`LHxwY3zIZ)#G z!^w%W8+!ZXd0qx(O!mHA9q~=>TiqVMR)HW^)5{$b&fQ$ONlQU%veF&l;<_@A^TaMLT2GhCTl9 zqbuP%N6TX!#SyFOUK=g*((>OS9}3O1!fF~Wi8AX5^Dn1J(!28kxt zBXA37FLfnVqOJUSU@${gQCZox`Tl->adI~|H0(N)ws~3RuYLKqx4qsq-F>pTw7A38 zoB?zvX<>3r-KWlk?pfe+*(0&DztR>bM2MO%x4|6PCkY}@*Gaus3DsD?Q+H1 z+i$lq+`oGG@ZlScK^yB2&hk;qy)E`&x0RipoOOWKw^tJj%x0VV7T4FuSDa6MzSO$C zyrd*#->n@Hu`w|vvEi@GR@N}DIdygZYNqzG&!3llFs#e|(Q^Bqcli%5NXk1F56m_H k|Fg5(`T-eipnSv3zzopr03UTj)c^nh literal 0 HcmV?d00001 diff --git a/e2e/tests/scrollbars/index.spec.ts-snapshots/zoom-half.png b/e2e/tests/scrollbars/index.spec.ts-snapshots/zoom-half.png new file mode 100644 index 0000000000000000000000000000000000000000..8c34f00400efc526782f12c7e17bdb23daeff42b GIT binary patch literal 809 zcmeAS@N?(olHy`uVBq!ia0y~yV4MJCPvc+$lJ%cGjTji1?s>X6hE&XXb62r1#8JfI z;`J1TEsTs}Ing3w+YYpO+~Aod`yepljRp_r;;IHuJ9{6dd4K$V`JD??^}ZVt8dNEM zdJ~Vs!J`Z=9AXm`BOKZi7$uunQ^@BWOFOu0-_Ct|cG=vMc|PZj{KQ#y?=jvJH1`M*pJVj?|s~UYa3f0u*Bx8QLE3#+n=9lx!qa*!~H#p6uN?9Z{RRs zW8{W!;=d11K0Ie;srTz+Y&(R@PH#FR*0;ZJdieC?vmeiyyfy5+y#4+A*L8Mqn{kUB zSXp_P$%uvfgn*8M^M(eVgIG9gqUF=<&gOg){B-ur>6z`!Ilde6)^4q`FE*97II~AU z$Ab0XuF^ljtKV;KlsEo2`LHMXk%Q)|yH%gl=RN4Zd0_R-zYOXdqQ#uQgk3kbH=m`x z { + canvas.setDimensions({ width: 400, height: 150 }); + const rect1 = new Rect({ + originX: 'center', + originY: 'center', + left: 100, + top: 100, + width: 100, + height: 100, + fill: 'green', + }); + + const rect2 = new Rect({ + originX: 'center', + originY: 'center', + left: 200, + top: 200, + width: 50, + height: 50, + fill: 'yellow', + }); + + new Scrollbars(canvas); + + canvas.add(rect1, rect2); + + return { rect1, rect2 }; +}); diff --git a/extensions/index.ts b/extensions/index.ts index f921d017778..038b4a2a4db 100644 --- a/extensions/index.ts +++ b/extensions/index.ts @@ -1,6 +1,9 @@ export { AligningGuidelines } from './aligning_guidelines'; export type * from './aligning_guidelines/typedefs'; +export { Scrollbars } from './scrollbars'; +export type * from './scrollbars/typedefs'; + export { originUpdaterWrapper, installOriginWrapperUpdater, diff --git a/extensions/scrollbars/README.md b/extensions/scrollbars/README.md new file mode 100644 index 00000000000..2e6736e8b8c --- /dev/null +++ b/extensions/scrollbars/README.md @@ -0,0 +1,36 @@ +# Scrollbars + +Add scrollbars to the infinite canvas. The scrollbars will be hidden when all graphics fit within the visible area. + +## How to use it + +```ts +import { Scrollbars } from 'fabric/extensions'; + +const config = { + /** Scrollbar fill color */ + fill = 'rgba(0,0,0,.3)'; + /** Scrollbar stroke color */ + stroke = 'rgba(255,255,255,.3)'; + /** Scrollbar line width */ + lineWidth = 1; + /** Hide horizontal scrollbar */ + hideX = false; + /** Hide vertical scrollbar */ + hideY = false; + /** Scrollbar minimum width */ + scrollbarMinWidth = 40; + /** Scrollbar size */ + scrollbarSize = 5; + /** Scrollbar distance from the boundary */ + scrollSpace = 4; + /** Scrollbar expansion size, the distance from which the user can effectively slide the scrollbar */ + padding = 4; +}; + +const scrollbars = new Scrollbars(myCanvas, options); + +// in order to disable scrollbars later: + +scrollbars.dispose(); +``` diff --git a/extensions/scrollbars/index.ts b/extensions/scrollbars/index.ts new file mode 100644 index 00000000000..113b3ab0b89 --- /dev/null +++ b/extensions/scrollbars/index.ts @@ -0,0 +1,274 @@ +import type { TMat2D, TPointerEvent } from 'fabric'; +import { Canvas, util } from 'fabric'; +import type { + ScrollbarProps, + ScrollbarsProps, + ScrollbarXProps, + ScrollbarYProps, +} from './typedefs'; + +export class Scrollbars { + canvas: Canvas; + /** Scrollbar fill color */ + fill = 'rgba(0,0,0,.3)'; + /** Scrollbar stroke color */ + stroke = 'rgba(255,255,255,.3)'; + /** Scrollbar line width */ + lineWidth = 1; + /** Hide horizontal scrollbar */ + hideX = false; + /** Hide vertical scrollbar */ + hideY = false; + /** Scrollbar minimum width */ + scrollbarMinWidth = 40; + /** Scrollbar size */ + scrollbarSize = 5; + /** Scrollbar distance from the boundary */ + scrollSpace = 4; + /** Scrollbar expansion size, the distance from which the user can effectively slide the scrollbar */ + padding = 4; + + /** The scrollbar currently hit */ + private _bar?: { type: string; start: number; vpt: TMat2D }; + /** The current area that can hit the scrollbar */ + private _barViewport = { + left: 1, + right: -1, + top: 1, + bottom: -1, + sx: 1, + sy: 1, + }; + + constructor(canvas: Canvas, props: ScrollbarsProps = {}) { + this.canvas = canvas; + Object.assign(this, props); + + this.canvas.__onMouseDown = this.mouseDownHandler.bind(this); + this.canvas.__onMouseMove = this.mouseMoveHandler.bind(this); + this.canvas.__onMouseUp = this.mouseUpHandler.bind(this); + this.beforeRenderHandler = this.beforeRenderHandler.bind(this); + this.afterRenderHandler = this.afterRenderHandler.bind(this); + + this.initBehavior(); + } + initBehavior() { + this.canvas.on('before:render', this.beforeRenderHandler); + this.canvas.on('after:render', this.afterRenderHandler); + } + getScrollbar(e: TPointerEvent) { + const p = this.canvas.getViewportPoint(e); + const vpt = this.canvas.viewportTransform.slice(0) as TMat2D; + if (!this.hideX) { + const b = + p.x > this._barViewport.left && + p.x < this._barViewport.right && + p.y > + this.canvas.height - + this.scrollbarSize - + this.scrollSpace - + this.padding && + p.y < this.canvas.height - this.scrollSpace + this.padding; + + if (b) return { type: 'x', start: p.x, vpt }; + } + if (!this.hideY) { + const b = + p.y > this._barViewport.top && + p.y < this._barViewport.bottom && + p.x > + this.canvas.width - + this.scrollbarSize - + this.scrollSpace - + this.padding && + p.x < this.canvas.width - this.scrollSpace + this.padding; + + if (b) return { type: 'y', start: p.y, vpt }; + } + } + mouseDownHandler(e: TPointerEvent) { + this._bar = this.getScrollbar(e); + if (!this._bar) return Canvas.prototype.__onMouseDown.call(this.canvas, e); + } + mouseMoveHandler(e: TPointerEvent) { + // When the mouse is not pressed and the mouse is in the scrollbar area, it will trigger the object's mouse:over/mouse:out, but it cannot select the object (because pressing the mouse will select the scrollbar). + // For the simplicity of the code, this situation is not judged, it does not affect the use + if (!this._bar) return Canvas.prototype.__onMouseMove.call(this.canvas, e); + const p = this.canvas.getViewportPoint(e); + const s = + this._bar.type == 'x' ? this._barViewport.sx : this._barViewport.sy; + const n = this._bar.type == 'x' ? 4 : 5; + const end = this._bar.type == 'x' ? p.x : p.y; + const vpt = this._bar.vpt.slice(0) as TMat2D; + vpt[n] -= (end - this._bar.start) * s; + + this.canvas.setViewportTransform(vpt); + this.canvas.requestRenderAll(); + } + mouseUpHandler(e: TPointerEvent) { + if (!this._bar) Canvas.prototype.__onMouseUp.call(this.canvas, e); + delete this._bar; + } + beforeRenderHandler() { + const ctx = this.canvas.contextTop; + // Clear horizontal scrollbar + if (!this.hideX) { + ctx.clearRect( + this.scrollSpace - this.lineWidth / 2, + this.canvas.height - + this.scrollbarSize - + this.scrollSpace - + this.lineWidth / 2, + this.canvas.width - this.scrollSpace * 2 + this.lineWidth, + this.scrollbarSize + this.lineWidth, + ); + } + + // Clear vertical scrollbar + if (!this.hideY) { + ctx.clearRect( + this.canvas.width - + this.scrollbarSize - + this.scrollSpace - + this.lineWidth / 2, + this.scrollSpace - this.lineWidth / 2, + this.scrollbarSize + this.lineWidth, + this.canvas.height - this.scrollSpace * 2 + this.lineWidth, + ); + } + } + afterRenderHandler() { + const { tl, br } = this.canvas.vptCoords; + /** Visible area */ + const mapRect = { left: tl.x, top: tl.y, right: br.x, bottom: br.y }; + /** The area where all shapes are located */ + const objectRect = this.getObjectsBoundingRect(); + if (objectRect.left > mapRect.left) objectRect.left = mapRect.left; + if (objectRect.top > mapRect.top) objectRect.top = mapRect.top; + if (objectRect.bottom < mapRect.bottom) objectRect.bottom = mapRect.bottom; + if (objectRect.right < mapRect.right) objectRect.right = mapRect.right; + + this.render(this.canvas.contextTop, mapRect, objectRect); + } + render( + ctx: CanvasRenderingContext2D, + mapRect: ScrollbarProps, + objectRect: ScrollbarProps, + ) { + ctx.save(); + ctx.fillStyle = this.fill; + ctx.strokeStyle = this.stroke; + ctx.lineWidth = this.lineWidth; + + // Draw horizontal scrollbar + if (!this.hideX) this.drawScrollbarX(ctx, mapRect, objectRect); + // Draw vertical scrollbar + if (!this.hideY) this.drawScrollbarY(ctx, mapRect, objectRect); + + ctx.restore(); + } + drawScrollbarX( + ctx: CanvasRenderingContext2D, + mapRect: ScrollbarXProps, + objectRect: ScrollbarXProps, + ) { + const mapWidth = mapRect.right - mapRect.left; + const objectWidth = objectRect.right - objectRect.left; + if (mapWidth == objectWidth) { + this._barViewport.left = 1; + this._barViewport.right = -1; + this._barViewport.sx = 1; + return; + } + + const scaleX = Math.min(mapWidth / objectWidth, 1); + const w = this.canvas.width - this.scrollbarSize - this.scrollSpace * 2; + const width = Math.max((w * scaleX) | 0, this.scrollbarMinWidth); + const left = + ((mapRect.left - objectRect.left) / (objectWidth - mapWidth)) * + (w - width); + + const x = this.scrollSpace + left; + const y = this.canvas.height - this.scrollbarSize - this.scrollSpace; + this._barViewport.left = x; + this._barViewport.right = x + width; + this._barViewport.sx = objectWidth / mapWidth; + + this.drawRect(ctx, { + x, + y, + w: width, + h: this.scrollbarSize, + }); + } + drawScrollbarY( + ctx: CanvasRenderingContext2D, + mapRect: ScrollbarYProps, + objectRect: ScrollbarYProps, + ) { + const mapHeight = mapRect.bottom - mapRect.top; + const objectHeight = objectRect.bottom - objectRect.top; + if (mapHeight == objectHeight) { + this._barViewport.top = 1; + this._barViewport.bottom = -1; + this._barViewport.sy = 1; + } + + const scaleY = Math.min(mapHeight / objectHeight, 1); + const h = this.canvas.height - this.scrollbarSize - this.scrollSpace * 2; + const height = Math.max((h * scaleY) | 0, this.scrollbarMinWidth); + const top = + ((mapRect.top - objectRect.top) / (objectHeight - mapHeight)) * + (h - height); + + const x = this.canvas.width - this.scrollbarSize - this.scrollSpace; + const y = this.scrollSpace + top; + this._barViewport.top = y; + this._barViewport.bottom = y + height; + this._barViewport.sy = objectHeight / mapHeight; + this.drawRect(ctx, { + x, + y, + w: this.scrollbarSize, + h: height, + }); + } + drawRect( + ctx: CanvasRenderingContext2D, + props: { x: number; y: number; w: number; h: number }, + ) { + const { x, y, w, h } = props; + const r = Math.min(w, h) / 2; + ctx.beginPath(); + ctx.moveTo(x + r, y); + ctx.lineTo(x + w - r, y); + ctx.arcTo(x + w, y, x + w, y + r, r); + ctx.lineTo(x + w, y + h - r); + ctx.arcTo(x + w, y + h, x + w - r, y + h, r); + ctx.lineTo(x + r, y + h); + ctx.arcTo(x, y + h, x, y + h - r, r); + ctx.lineTo(x, y + r); + ctx.arcTo(x, y, x + r, y, r); + ctx.closePath(); + ctx.fill(); + ctx.stroke(); + } + getObjectsBoundingRect() { + const objects = this.canvas.getObjects(); + const { left, top, width, height } = util.makeBoundingBoxFromPoints( + objects.map((x) => x.getCoords()).flat(1), + ); + return { left, top, right: left + width, bottom: top + height }; + } + + dispose() { + // @ts-expect-error: In the initialization, __onMouseDown was overridden, here it is restored + delete this.canvas.__onMouseDown; + // @ts-expect-error: In the initialization, __onMouseMove was overridden, here it is restored + delete this.canvas.__onMouseMove; + // @ts-expect-error: In the initialization, __onMouseUp was overridden, here it is restored + delete this.canvas.__onMouseUp; + this.canvas.off('before:render', this.beforeRenderHandler); + this.canvas.off('after:render', this.afterRenderHandler); + } +} diff --git a/extensions/scrollbars/typedefs.ts b/extensions/scrollbars/typedefs.ts new file mode 100644 index 00000000000..2eead15d82f --- /dev/null +++ b/extensions/scrollbars/typedefs.ts @@ -0,0 +1,30 @@ +export type ScrollbarsProps = { + /** Scrollbar fill color */ + fill?: string; + /** Scrollbar stroke color */ + stroke?: string; + /** Scrollbar line width */ + lineWidth?: number; + /** Hide horizontal scrollbar */ + hideX?: boolean; + /** Hide vertical scrollbar */ + hideY?: boolean; + /** Scrollbar minimum width */ + scrollbarMinWidth?: number; + /** Scrollbar size */ + scrollbarSize?: number; + /** Scrollbar distance from the boundary */ + scrollSpace?: number; + /** Scrollbar expansion size, the distance from which the user can effectively slide the scrollbar */ + padding?: number; +}; + +export type ScrollbarProps = { + left: number; + top: number; + right: number; + bottom: number; +}; +export type ScrollbarXProps = Pick; + +export type ScrollbarYProps = Pick; From 3dc10126cb0c2e4cf61c1249a22c3d4cacfac1ea Mon Sep 17 00:00:00 2001 From: zhe-he Date: Tue, 11 Nov 2025 15:08:08 +0800 Subject: [PATCH 2/2] feat(extensions/scrollbars): add makeMouseWheel --- extensions/index.ts | 1 + extensions/scrollbars/README.md | 8 ++++++- extensions/scrollbars/typedefs.ts | 13 +++++++++++ extensions/scrollbars/util.ts | 38 +++++++++++++++++++++++++++++++ 4 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 extensions/scrollbars/util.ts diff --git a/extensions/index.ts b/extensions/index.ts index 038b4a2a4db..d29e4ceb669 100644 --- a/extensions/index.ts +++ b/extensions/index.ts @@ -2,6 +2,7 @@ export { AligningGuidelines } from './aligning_guidelines'; export type * from './aligning_guidelines/typedefs'; export { Scrollbars } from './scrollbars'; +export { makeMouseWheel } from './scrollbars/util'; export type * from './scrollbars/typedefs'; export { diff --git a/extensions/scrollbars/README.md b/extensions/scrollbars/README.md index 2e6736e8b8c..942e9c33f12 100644 --- a/extensions/scrollbars/README.md +++ b/extensions/scrollbars/README.md @@ -5,7 +5,7 @@ Add scrollbars to the infinite canvas. The scrollbars will be hidden when all gr ## How to use it ```ts -import { Scrollbars } from 'fabric/extensions'; +import { Scrollbars, makeMouseWheel } from 'fabric/extensions'; const config = { /** Scrollbar fill color */ @@ -28,9 +28,15 @@ const config = { padding = 4; }; +// You have the option to implement custom canvas zooming or use the plugin's built-in solution. +const mousewheel = makeMouseWheel(canvas); +canvas.on("mouse:wheel", mousewheel); +// canvas.off("mouse:wheel", mousewheel); + const scrollbars = new Scrollbars(myCanvas, options); // in order to disable scrollbars later: scrollbars.dispose(); + ``` diff --git a/extensions/scrollbars/typedefs.ts b/extensions/scrollbars/typedefs.ts index 2eead15d82f..3f48ab347f6 100644 --- a/extensions/scrollbars/typedefs.ts +++ b/extensions/scrollbars/typedefs.ts @@ -28,3 +28,16 @@ export type ScrollbarProps = { export type ScrollbarXProps = Pick; export type ScrollbarYProps = Pick; + +export type MakeMouseWheelProps = { + /** Minimum zoom size */ + min: number; + /** Maximum zoom size */ + max: number; + /** Touchpad Zoom Speed default to 0.99 */ + tSpeed: number; + /** Mouse Wheel Zoom Speed default to 0.998 */ + mSpeed: number; + /** Canvas panning speed defaults to 1 */ + pSpeed: number; +}; diff --git a/extensions/scrollbars/util.ts b/extensions/scrollbars/util.ts new file mode 100644 index 00000000000..e494e72072b --- /dev/null +++ b/extensions/scrollbars/util.ts @@ -0,0 +1,38 @@ +import type { TPointerEventInfo, Canvas, TMat2D } from 'fabric'; +import type { MakeMouseWheelProps } from './typedefs'; + +export const makeMouseWheel = + (canvas: Canvas, props: Partial = {}) => + (options: TPointerEventInfo) => { + const e = options.e; + if (e.target == canvas.upperCanvasEl) e.preventDefault(); + const { min, max, tSpeed = 0.99, mSpeed = 0.998, pSpeed = 1 } = props; + + /** Is it touchpad zoom? */ + const isTouchScale = Math.floor(e.deltaY) != Math.ceil(e.deltaY); + // Touchpad zooming, Mac's touchpad automatically sets e.ctrlKey to true + if (e.ctrlKey || e.metaKey) { + // Mouse and touchpad speed separate + const speed = isTouchScale ? tSpeed : mSpeed; + let zoom = canvas.getZoom(); + zoom *= speed ** e.deltaY; + if (max != undefined && zoom > max) zoom = max; + if (min != undefined && zoom < min) zoom = min; + + canvas.zoomToPoint(options.viewportPoint, zoom); + canvas.requestRenderAll(); + return; + } + + const vpt = canvas.viewportTransform.slice(0) as TMat2D; + let { deltaX, deltaY } = e; + // Hold down Shift and scroll the mouse to indicate horizontal scrolling + if (e.shiftKey && deltaX == 0) { + deltaX = deltaY; + deltaY = 0; + } + vpt[4] -= deltaX * pSpeed; + vpt[5] -= deltaY * pSpeed; + canvas.setViewportTransform(vpt); + canvas.requestRenderAll(); + };