From 9691d2c070f28407c49e7faeb6613085be8b18a6 Mon Sep 17 00:00:00 2001 From: moklick Date: Mon, 19 May 2025 13:14:27 +0200 Subject: [PATCH 1/4] feat(viewport-options): add ease #3189 --- .../react/src/examples/UseReactFlow/index.tsx | 12 +++++- .../routes/examples/usesvelteflow/Flow.svelte | 2 +- .../examples/usesvelteflow/Sidebar.svelte | 3 +- packages/react/src/hooks/useViewportHelper.ts | 6 +-- packages/react/src/types/general.ts | 5 +++ .../src/lib/hooks/useSvelteFlow.svelte.ts | 9 +++-- packages/system/src/types/general.ts | 2 + packages/system/src/types/panzoom.ts | 1 + packages/system/src/utils/graph.ts | 2 +- packages/system/src/xypanzoom/XYPanZoom.ts | 37 ++++++++++--------- packages/system/src/xypanzoom/utils.ts | 10 ++++- 11 files changed, 58 insertions(+), 31 deletions(-) diff --git a/examples/react/src/examples/UseReactFlow/index.tsx b/examples/react/src/examples/UseReactFlow/index.tsx index cf6ae59f..3e1d8f92 100644 --- a/examples/react/src/examples/UseReactFlow/index.tsx +++ b/examples/react/src/examples/UseReactFlow/index.tsx @@ -170,7 +170,6 @@ const UseZoomPanHelperFlow = () => { onConnect={onConnect} onPaneClick={onPaneClick} fitView - fitViewOptions={{ duration: 1200, padding: 0.2 }} maxZoom={Infinity} > @@ -183,7 +182,16 @@ const UseZoomPanHelperFlow = () => { console.log('fit view success'); }} > - fitView + fitView default + + diff --git a/examples/svelte/src/routes/examples/usesvelteflow/Flow.svelte b/examples/svelte/src/routes/examples/usesvelteflow/Flow.svelte index aa37a49a..cab0bdde 100644 --- a/examples/svelte/src/routes/examples/usesvelteflow/Flow.svelte +++ b/examples/svelte/src/routes/examples/usesvelteflow/Flow.svelte @@ -44,7 +44,7 @@
- + diff --git a/examples/svelte/src/routes/examples/usesvelteflow/Sidebar.svelte b/examples/svelte/src/routes/examples/usesvelteflow/Sidebar.svelte index c64131f0..6c56e10a 100644 --- a/examples/svelte/src/routes/examples/usesvelteflow/Sidebar.svelte +++ b/examples/svelte/src/routes/examples/usesvelteflow/Sidebar.svelte @@ -30,7 +30,8 @@ - + + diff --git a/packages/react/src/hooks/useViewportHelper.ts b/packages/react/src/hooks/useViewportHelper.ts index a88bd3fc..3f115152 100644 --- a/packages/react/src/hooks/useViewportHelper.ts +++ b/packages/react/src/hooks/useViewportHelper.ts @@ -53,7 +53,7 @@ const useViewportHelper = (): ViewportHelperFunctions => { y: viewport.y ?? tY, zoom: viewport.zoom ?? tZoom, }, - { duration: options?.duration } + options ); return Promise.resolve(true); @@ -78,7 +78,7 @@ const useViewportHelper = (): ViewportHelperFunctions => { y: centerY, zoom: nextZoom, }, - { duration: options?.duration } + { duration: options?.duration, ease: options?.ease } ); return Promise.resolve(true); @@ -91,7 +91,7 @@ const useViewportHelper = (): ViewportHelperFunctions => { return Promise.resolve(false); } - await panZoom.setViewport(viewport, { duration: options?.duration }); + await panZoom.setViewport(viewport, { duration: options?.duration, ease: options?.ease }); return Promise.resolve(true); }, diff --git a/packages/react/src/types/general.ts b/packages/react/src/types/general.ts index 98302a86..80acc45c 100644 --- a/packages/react/src/types/general.ts +++ b/packages/react/src/types/general.ts @@ -149,6 +149,7 @@ export type ViewportHelperFunctions = { * * @param viewport - the viewport to set * @param options.duration - optional duration. If set, a transition will be applied + * @param options.ease - optional ease function. */ setViewport: SetViewport; /** @@ -164,6 +165,8 @@ export type ViewportHelperFunctions = { * @param x - x position * @param y - y position * @param options.zoom - optional zoom + * @param options.duration - optional duration. If set, a transition will be applied + * @param options.ease - optional ease function. */ setCenter: SetCenter; /** @@ -173,6 +176,8 @@ export type ViewportHelperFunctions = { * * @param bounds - the bounds ({ x: number, y: number, width: number, height: number }) to fit the view to * @param options.padding - optional padding + * @param options.duration - optional duration. If set, a transition will be applied + * @param options.ease - optional ease function. */ fitBounds: FitBounds; /** diff --git a/packages/svelte/src/lib/hooks/useSvelteFlow.svelte.ts b/packages/svelte/src/lib/hooks/useSvelteFlow.svelte.ts index e3dcf1f4..d56b3992 100644 --- a/packages/svelte/src/lib/hooks/useSvelteFlow.svelte.ts +++ b/packages/svelte/src/lib/hooks/useSvelteFlow.svelte.ts @@ -360,7 +360,7 @@ export function useSvelteFlow = { minZoom?: number; maxZoom?: number; duration?: number; + ease?: (t: number) => number; nodes?: (NodeType | { id: string })[]; }; @@ -173,6 +174,7 @@ export enum PanOnScrollMode { */ export type ViewportHelperFunctionOptions = { duration?: number; + ease?: (t: number) => number; }; export type SetCenterOptions = ViewportHelperFunctionOptions & { diff --git a/packages/system/src/types/panzoom.ts b/packages/system/src/types/panzoom.ts index a2577505..1738bc50 100644 --- a/packages/system/src/types/panzoom.ts +++ b/packages/system/src/types/panzoom.ts @@ -20,6 +20,7 @@ export type PanZoomParams = { export type PanZoomTransformOptions = { duration?: number; + ease?: (t: number) => number; }; export type OnPanZoom = (event: MouseEvent | TouchEvent | null, viewport: Viewport) => void; diff --git a/packages/system/src/utils/graph.ts b/packages/system/src/utils/graph.ts index 9bd7beff..386eac79 100644 --- a/packages/system/src/utils/graph.ts +++ b/packages/system/src/utils/graph.ts @@ -378,7 +378,7 @@ export async function fitViewport< options?.padding ?? 0.1 ); - await panZoom.setViewport(viewport, { duration: options?.duration }); + await panZoom.setViewport(viewport, { duration: options?.duration, ease: options?.ease }); return Promise.resolve(true); } diff --git a/packages/system/src/xypanzoom/XYPanZoom.ts b/packages/system/src/xypanzoom/XYPanZoom.ts index 93bc79f8..b30ec8f5 100644 --- a/packages/system/src/xypanzoom/XYPanZoom.ts +++ b/packages/system/src/xypanzoom/XYPanZoom.ts @@ -19,6 +19,7 @@ import { createZoomOnScrollHandler, } from './eventhandler'; import { createFilter } from './filter'; +import { transition } from 'd3-transition'; export type ZoomPanValues = { isZoomingOrPanning: boolean; @@ -79,7 +80,7 @@ export function XYPanZoom({ if (d3Selection) { return new Promise((resolve) => { d3ZoomInstance?.transform( - getD3Transition(d3Selection, options?.duration, () => resolve(true)), + getD3Transition(d3Selection, options?.duration, options?.ease, () => resolve(true)), transform ); }); @@ -114,22 +115,22 @@ export function XYPanZoom({ const wheelHandler = isPanOnScroll ? createPanOnScrollHandler({ - zoomPanValues, - noWheelClassName, - d3Selection, - d3Zoom: d3ZoomInstance, - panOnScrollMode, - panOnScrollSpeed, - zoomOnPinch, - onPanZoomStart, - onPanZoom, - onPanZoomEnd, - }) + zoomPanValues, + noWheelClassName, + d3Selection, + d3Zoom: d3ZoomInstance, + panOnScrollMode, + panOnScrollSpeed, + zoomOnPinch, + onPanZoomStart, + onPanZoom, + onPanZoomEnd, + }) : createZoomOnScrollHandler({ - noWheelClassName, - preventScrolling, - d3ZoomHandler, - }); + noWheelClassName, + preventScrolling, + d3ZoomHandler, + }); d3Selection.on('wheel.zoom', wheelHandler, { passive: false }); @@ -243,7 +244,7 @@ export function XYPanZoom({ if (d3Selection) { return new Promise((resolve) => { d3ZoomInstance?.scaleTo( - getD3Transition(d3Selection, options?.duration, () => resolve(true)), + getD3Transition(d3Selection, options?.duration, options?.ease, () => resolve(true)), zoom ); }); @@ -256,7 +257,7 @@ export function XYPanZoom({ if (d3Selection) { return new Promise((resolve) => { d3ZoomInstance?.scaleBy( - getD3Transition(d3Selection, options?.duration, () => resolve(true)), + getD3Transition(d3Selection, options?.duration, options?.ease, () => resolve(true)), factor ); }); diff --git a/packages/system/src/xypanzoom/utils.ts b/packages/system/src/xypanzoom/utils.ts index fbd0324d..b6fd71f3 100644 --- a/packages/system/src/xypanzoom/utils.ts +++ b/packages/system/src/xypanzoom/utils.ts @@ -1,5 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { type ZoomTransform, zoomIdentity } from 'd3-zoom'; +import { transition } from 'd3-transition'; import { type D3SelectionInstance, type Viewport } from '../types'; import { isMacOs } from '../utils'; @@ -21,14 +22,19 @@ export const isWrappedWithClass = (event: any, className: string | undefined) => export const isRightClickPan = (panOnDrag: boolean | number[], usedButton: number) => usedButton === 2 && Array.isArray(panOnDrag) && panOnDrag.includes(2); -export const getD3Transition = (selection: D3SelectionInstance, duration = 0, onEnd = () => {}) => { +// taken from d3-ease: https://github.com/d3/d3-ease/blob/main/src/cubic.js +const defaultEase = (t: number) => ((t *= 2) <= 1 ? t * t * t : (t -= 2) * t * t + 2) / 2; + +export const getD3Transition = (selection: D3SelectionInstance, duration = 0, ease = defaultEase, onEnd = () => {}) => { const hasDuration = typeof duration === 'number' && duration > 0; if (!hasDuration) { onEnd(); } - return hasDuration ? selection.transition().duration(duration).on('end', onEnd) : selection; + console.log(ease); + + return hasDuration ? selection.transition().duration(duration).ease(ease).on('end', onEnd) : selection; }; export const wheelDelta = (event: any) => { From 6ce44a05c829068ff5a8416ce3fa4ee6e0eced48 Mon Sep 17 00:00:00 2001 From: moklick Date: Mon, 19 May 2025 13:15:39 +0200 Subject: [PATCH 2/4] chore(changeset): add --- .changeset/late-taxis-press.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changeset/late-taxis-press.md diff --git a/.changeset/late-taxis-press.md b/.changeset/late-taxis-press.md new file mode 100644 index 00000000..d263bb08 --- /dev/null +++ b/.changeset/late-taxis-press.md @@ -0,0 +1,7 @@ +--- +'@xyflow/react': minor +'@xyflow/svelte': minor +'@xyflow/system': minor +--- + +Add an ease option to all function that alter the viewport From ce9814ddcef753c8cf4a23e70c6fe7a4b10f6cf2 Mon Sep 17 00:00:00 2001 From: moklick Date: Mon, 19 May 2025 15:19:11 +0200 Subject: [PATCH 3/4] feat(viewport-options): add interpolate --- .../src/routes/examples/usesvelteflow/Sidebar.svelte | 4 +++- packages/react/src/hooks/useViewportHelper.ts | 8 ++++++-- packages/svelte/src/lib/hooks/useSvelteFlow.svelte.ts | 5 +++-- packages/system/package.json | 2 ++ packages/system/src/types/general.ts | 2 ++ packages/system/src/types/panzoom.ts | 1 + packages/system/src/utils/graph.ts | 6 +++++- packages/system/src/xypanzoom/XYPanZoom.ts | 7 ++++--- packages/system/src/xypanzoom/utils.ts | 2 -- pnpm-lock.yaml | 6 ++++++ 10 files changed, 32 insertions(+), 11 deletions(-) diff --git a/examples/svelte/src/routes/examples/usesvelteflow/Sidebar.svelte b/examples/svelte/src/routes/examples/usesvelteflow/Sidebar.svelte index 6c56e10a..3b687eb3 100644 --- a/examples/svelte/src/routes/examples/usesvelteflow/Sidebar.svelte +++ b/examples/svelte/src/routes/examples/usesvelteflow/Sidebar.svelte @@ -31,7 +31,9 @@ - + diff --git a/packages/react/src/hooks/useViewportHelper.ts b/packages/react/src/hooks/useViewportHelper.ts index 3f115152..fc01a150 100644 --- a/packages/react/src/hooks/useViewportHelper.ts +++ b/packages/react/src/hooks/useViewportHelper.ts @@ -78,7 +78,7 @@ const useViewportHelper = (): ViewportHelperFunctions => { y: centerY, zoom: nextZoom, }, - { duration: options?.duration, ease: options?.ease } + { duration: options?.duration, ease: options?.ease, interpolate: options?.interpolate } ); return Promise.resolve(true); @@ -91,7 +91,11 @@ const useViewportHelper = (): ViewportHelperFunctions => { return Promise.resolve(false); } - await panZoom.setViewport(viewport, { duration: options?.duration, ease: options?.ease }); + await panZoom.setViewport(viewport, { + duration: options?.duration, + ease: options?.ease, + interpolate: options?.interpolate, + }); return Promise.resolve(true); }, diff --git a/packages/svelte/src/lib/hooks/useSvelteFlow.svelte.ts b/packages/svelte/src/lib/hooks/useSvelteFlow.svelte.ts index d56b3992..82c66064 100644 --- a/packages/svelte/src/lib/hooks/useSvelteFlow.svelte.ts +++ b/packages/svelte/src/lib/hooks/useSvelteFlow.svelte.ts @@ -380,7 +380,7 @@ export function useSvelteFlow = { maxZoom?: number; duration?: number; ease?: (t: number) => number; + interpolate?: 'smooth' | 'linear'; nodes?: (NodeType | { id: string })[]; }; @@ -175,6 +176,7 @@ export enum PanOnScrollMode { export type ViewportHelperFunctionOptions = { duration?: number; ease?: (t: number) => number; + interpolate?: 'smooth' | 'linear'; }; export type SetCenterOptions = ViewportHelperFunctionOptions & { diff --git a/packages/system/src/types/panzoom.ts b/packages/system/src/types/panzoom.ts index 1738bc50..ae968fc8 100644 --- a/packages/system/src/types/panzoom.ts +++ b/packages/system/src/types/panzoom.ts @@ -21,6 +21,7 @@ export type PanZoomParams = { export type PanZoomTransformOptions = { duration?: number; ease?: (t: number) => number; + interpolate?: 'smooth' | 'linear'; }; export type OnPanZoom = (event: MouseEvent | TouchEvent | null, viewport: Viewport) => void; diff --git a/packages/system/src/utils/graph.ts b/packages/system/src/utils/graph.ts index 386eac79..402067d2 100644 --- a/packages/system/src/utils/graph.ts +++ b/packages/system/src/utils/graph.ts @@ -378,7 +378,11 @@ export async function fitViewport< options?.padding ?? 0.1 ); - await panZoom.setViewport(viewport, { duration: options?.duration, ease: options?.ease }); + await panZoom.setViewport(viewport, { + duration: options?.duration, + ease: options?.ease, + interpolate: options?.interpolate, + }); return Promise.resolve(true); } diff --git a/packages/system/src/xypanzoom/XYPanZoom.ts b/packages/system/src/xypanzoom/XYPanZoom.ts index b30ec8f5..0c1a8e12 100644 --- a/packages/system/src/xypanzoom/XYPanZoom.ts +++ b/packages/system/src/xypanzoom/XYPanZoom.ts @@ -1,5 +1,6 @@ import { type ZoomTransform, zoom, zoomTransform } from 'd3-zoom'; import { select } from 'd3-selection'; +import { interpolateZoom, interpolate } from 'd3-interpolate'; import { type CoordinateExtent, @@ -79,7 +80,7 @@ export function XYPanZoom({ function setTransform(transform: ZoomTransform, options?: PanZoomTransformOptions) { if (d3Selection) { return new Promise((resolve) => { - d3ZoomInstance?.transform( + d3ZoomInstance?.interpolate(options?.interpolate === 'linear' ? interpolate : interpolateZoom).transform( getD3Transition(d3Selection, options?.duration, options?.ease, () => resolve(true)), transform ); @@ -243,7 +244,7 @@ export function XYPanZoom({ function scaleTo(zoom: number, options?: PanZoomTransformOptions) { if (d3Selection) { return new Promise((resolve) => { - d3ZoomInstance?.scaleTo( + d3ZoomInstance?.interpolate(options?.interpolate === 'linear' ? interpolate : interpolateZoom).scaleTo( getD3Transition(d3Selection, options?.duration, options?.ease, () => resolve(true)), zoom ); @@ -256,7 +257,7 @@ export function XYPanZoom({ function scaleBy(factor: number, options?: PanZoomTransformOptions) { if (d3Selection) { return new Promise((resolve) => { - d3ZoomInstance?.scaleBy( + d3ZoomInstance?.interpolate(options?.interpolate === 'linear' ? interpolate : interpolateZoom).scaleBy( getD3Transition(d3Selection, options?.duration, options?.ease, () => resolve(true)), factor ); diff --git a/packages/system/src/xypanzoom/utils.ts b/packages/system/src/xypanzoom/utils.ts index b6fd71f3..22382bd5 100644 --- a/packages/system/src/xypanzoom/utils.ts +++ b/packages/system/src/xypanzoom/utils.ts @@ -32,8 +32,6 @@ export const getD3Transition = (selection: D3SelectionInstance, duration = 0, ea onEnd(); } - console.log(ease); - return hasDuration ? selection.transition().duration(duration).ease(ease).on('end', onEnd) : selection; }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c962a91e..d47c4673 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -372,6 +372,9 @@ importers: '@types/d3-drag': specifier: ^3.0.7 version: 3.0.7 + '@types/d3-interpolate': + specifier: ^3.0.4 + version: 3.0.4 '@types/d3-selection': specifier: ^3.0.10 version: 3.0.10 @@ -384,6 +387,9 @@ importers: d3-drag: specifier: ^3.0.0 version: 3.0.0 + d3-interpolate: + specifier: ^3.0.1 + version: 3.0.1 d3-selection: specifier: ^3.0.0 version: 3.0.0 From 5025b8206ba1112cf8d834034ed47bc93bd0b9b6 Mon Sep 17 00:00:00 2001 From: moklick Date: Mon, 19 May 2025 15:23:00 +0200 Subject: [PATCH 4/4] chore(changeset): update --- .changeset/late-taxis-press.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/late-taxis-press.md b/.changeset/late-taxis-press.md index d263bb08..e48f797b 100644 --- a/.changeset/late-taxis-press.md +++ b/.changeset/late-taxis-press.md @@ -4,4 +4,4 @@ '@xyflow/system': minor --- -Add an ease option to all function that alter the viewport +Add an `ease` and `interpolate` option to all function that alter the viewport