diff --git a/.changeset/hip-clocks-nail.md b/.changeset/hip-clocks-nail.md new file mode 100644 index 00000000..619fa329 --- /dev/null +++ b/.changeset/hip-clocks-nail.md @@ -0,0 +1,5 @@ +--- +"@vue-flow/core": minor +--- + +Add `ease` option to viewport altering functions. diff --git a/packages/core/package.json b/packages/core/package.json index d665e837..90c8a6d4 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -80,9 +80,10 @@ "@rollup/plugin-replace": "^5.0.3", "@tooling/eslint-config": "workspace:*", "@tooling/tsconfig": "workspace:*", - "@types/d3-drag": "^3.0.4", - "@types/d3-selection": "^3.0.7", - "@types/d3-zoom": "^3.0.5", + "@types/d3-drag": "^3.0.7", + "@types/d3-selection": "^3.0.11", + "@types/d3-transition": "^3.0.9", + "@types/d3-zoom": "^3.0.8", "@vitejs/plugin-vue": "^4.4.0", "autoprefixer": "^10.4.16", "postcss": "^8.4.31", diff --git a/packages/core/src/composables/useViewportHelper.ts b/packages/core/src/composables/useViewportHelper.ts index 58698417..7f242d15 100644 --- a/packages/core/src/composables/useViewportHelper.ts +++ b/packages/core/src/composables/useViewportHelper.ts @@ -1,6 +1,6 @@ import { zoomIdentity } from 'd3-zoom' import { computed } from 'vue' -import type { D3Selection, GraphNode, Project, State, ViewportFunctions } from '../types' +import type { D3Selection, GraphNode, Project, State, TransitionOptions, ViewportFunctions } from '../types' import { clampPosition, getRectOfNodes, getTransformForBounds, pointToRendererPoint, rendererPointToPoint, warn } from '../utils' export interface ViewportHelper extends ViewportFunctions { @@ -11,6 +11,10 @@ export interface ViewportHelper extends ViewportFunctions { const DEFAULT_PADDING = 0.1 +// taken from d3-ease: https://github.com/d3/d3-ease/blob/main/src/cubic.js +// eslint-disable-next-line no-cond-assign +const defaultEase = (t: number) => ((t *= 2) <= 1 ? t * t * t : (t -= 2) * t * t + 2) / 2 + function noop() { warn('Viewport not initialized yet.') @@ -41,11 +45,11 @@ const initialViewportHelper: ViewportHelper = { * @param state */ export function useViewportHelper(state: State) { - function zoom(scale: number, duration?: number) { + function zoom(scale: number, transitionOptions?: TransitionOptions) { return new Promise((resolve) => { if (state.d3Selection && state.d3Zoom) { state.d3Zoom.scaleBy( - transition(state.d3Selection, duration, () => { + getD3Transition(state.d3Selection, transitionOptions?.duration, transitionOptions?.ease, () => { resolve(true) }), scale, @@ -56,7 +60,7 @@ export function useViewportHelper(state: State) { }) } - function transformViewport(x: number, y: number, zoom: number, duration?: number) { + function transformViewport(x: number, y: number, zoom: number, transitionOptions?: TransitionOptions) { return new Promise((resolve) => { // enforce translate extent const { x: clampedX, y: clampedY } = clampPosition({ x: -x, y: -y }, state.translateExtent) @@ -65,7 +69,7 @@ export function useViewportHelper(state: State) { if (state.d3Selection && state.d3Zoom) { state.d3Zoom.transform( - transition(state.d3Selection, duration, () => { + getD3Transition(state.d3Selection, transitionOptions?.duration, transitionOptions?.ease, () => { resolve(true) }), nextTransform, @@ -87,16 +91,16 @@ export function useViewportHelper(state: State) { viewportInitialized: true, // todo: allow passing scale as option zoomIn: (options) => { - return zoom(1.2, options?.duration) + return zoom(1.2, options) }, zoomOut: (options) => { - return zoom(1 / 1.2, options?.duration) + return zoom(1 / 1.2, options) }, zoomTo: (zoomLevel, options) => { return new Promise((resolve) => { if (state.d3Selection && state.d3Zoom) { state.d3Zoom.scaleTo( - transition(state.d3Selection, options?.duration, () => { + getD3Transition(state.d3Selection, options?.duration, options?.ease, () => { resolve(true) }), zoomLevel, @@ -107,10 +111,10 @@ export function useViewportHelper(state: State) { }) }, setViewport: (transform, options) => { - return transformViewport(transform.x, transform.y, transform.zoom, options?.duration) + return transformViewport(transform.x, transform.y, transform.zoom, options) }, setTransform: (transform, options) => { - return transformViewport(transform.x, transform.y, transform.zoom, options?.duration) + return transformViewport(transform.x, transform.y, transform.zoom, options) }, getViewport: () => ({ x: state.viewport.x, @@ -158,14 +162,14 @@ export function useViewportHelper(state: State) { options.offset, ) - return transformViewport(x, y, zoom, options?.duration) + return transformViewport(x, y, zoom, options) }, setCenter: (x, y, options) => { const nextZoom = typeof options?.zoom !== 'undefined' ? options.zoom : state.maxZoom const centerX = state.dimensions.width / 2 - x * nextZoom const centerY = state.dimensions.height / 2 - y * nextZoom - return transformViewport(centerX, centerY, nextZoom, options?.duration) + return transformViewport(centerX, centerY, nextZoom, options) }, fitBounds: (bounds, options = { padding: DEFAULT_PADDING }) => { const { x, y, zoom } = getTransformForBounds( @@ -177,7 +181,7 @@ export function useViewportHelper(state: State) { options.padding, ) - return transformViewport(x, y, zoom, options?.duration) + return transformViewport(x, y, zoom, options) }, project: (position) => pointToRendererPoint(position, state.viewport, state.snapToGrid, state.snapGrid), screenToFlowCoordinate: (position) => { @@ -212,6 +216,12 @@ export function useViewportHelper(state: State) { }) } -function transition(selection: D3Selection, ms = 0, onEnd: () => void) { - return (selection as any).transition().duration(ms).on('end', onEnd) +export function getD3Transition(selection: D3Selection, duration = 0, ease = defaultEase, onEnd = () => {}) { + const hasDuration = typeof duration === 'number' && duration > 0 + + if (!hasDuration) { + onEnd() + } + + return hasDuration ? selection.transition().duration(duration).ease(ease).on('end', onEnd) : selection } diff --git a/packages/core/src/types/zoom.ts b/packages/core/src/types/zoom.ts index 198baa1e..07b225b6 100644 --- a/packages/core/src/types/zoom.ts +++ b/packages/core/src/types/zoom.ts @@ -1,9 +1,13 @@ import type { Selection } from 'd3-selection' + +// eslint-disable-next-line unused-imports/no-unused-imports,@typescript-eslint/no-unused-vars -- this is needed for the Selection type to include the transition function :/ +import type { Transition } from 'd3-transition' import type { ZoomBehavior } from 'd3-zoom' + import type { Rect, XYPosition } from './flow' export type D3Zoom = ZoomBehavior -export type D3Selection = Selection +export type D3Selection = Selection export type D3ZoomHandler = (this: HTMLDivElement, event: any, d: unknown) => void export enum PanOnScrollMode { @@ -14,6 +18,7 @@ export enum PanOnScrollMode { export interface TransitionOptions { duration?: number + ease?: (t: number) => number } export type FitViewParams = { diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json index 85005ecc..6c8b689e 100644 --- a/packages/core/tsconfig.json +++ b/packages/core/tsconfig.json @@ -4,14 +4,14 @@ "baseUrl": ".", "declarationDir": "./dist", "resolveJsonModule": true, - "types": [ - "vite/client", - "vue/macros" - ] }, "include": [ "./src" ], + "exclude": [ + "node_modules", + "dist", + ], "references": [ { "path": "./tsconfig.node.json" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 365e3851..9c429fd8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -95,13 +95,13 @@ importers: version: 6.0.3(typedoc@0.26.11(typescript@5.4.5)) unplugin-auto-import: specifier: ^0.18.3 - version: 0.18.3(@nuxt/kit@3.13.2(magicast@0.3.5)(rollup@4.21.0)(webpack-sources@3.2.3))(@vueuse/core@11.2.0(vue@3.5.12(typescript@5.4.5)))(rollup@4.21.0)(webpack-sources@3.2.3) + version: 0.18.3(@nuxt/kit@3.13.2(rollup@4.21.0)(webpack-sources@3.2.3))(@vueuse/core@11.2.0(vue@3.5.12(typescript@5.4.5)))(rollup@4.21.0)(webpack-sources@3.2.3) unplugin-icons: specifier: ^0.20.0 version: 0.20.0(@vue/compiler-sfc@3.5.12)(vue-template-compiler@2.7.14)(webpack-sources@3.2.3) unplugin-vue-components: specifier: ^0.27.4 - version: 0.27.4(@babel/parser@7.25.7)(@nuxt/kit@3.13.2(magicast@0.3.5)(rollup@4.21.0)(webpack-sources@3.2.3))(rollup@4.21.0)(vue@3.5.12(typescript@5.4.5)) + version: 0.27.4(@babel/parser@7.25.7)(@nuxt/kit@3.13.2(rollup@4.21.0)(webpack-sources@3.2.3))(rollup@4.21.0)(vue@3.5.12(typescript@5.4.5)) vite-plugin-windicss: specifier: ^1.9.3 version: 1.9.3(vite@5.4.10(@types/node@20.14.2)(sass@1.79.4)(terser@5.21.0)) @@ -221,7 +221,7 @@ importers: version: 5.1.4(vite@5.4.8(@types/node@20.14.2)(sass@1.79.4)(terser@5.21.0))(vue@3.5.11(typescript@5.4.5)) unplugin-auto-import: specifier: ^0.18.3 - version: 0.18.3(@nuxt/kit@3.13.2(magicast@0.3.5)(rollup@4.21.0)(webpack-sources@3.2.3))(@vueuse/core@11.2.0(vue@3.5.11(typescript@5.4.5)))(rollup@4.21.0)(webpack-sources@3.2.3) + version: 0.18.3(@nuxt/kit@3.13.2(rollup@4.21.0)(webpack-sources@3.2.3))(@vueuse/core@11.2.0(vue@3.5.11(typescript@5.4.5)))(rollup@4.21.0)(webpack-sources@3.2.3) vite: specifier: ^5.4.8 version: 5.4.8(@types/node@20.14.2)(sass@1.79.4)(terser@5.21.0) @@ -313,14 +313,17 @@ importers: specifier: workspace:* version: link:../../tooling/tsconfig '@types/d3-drag': - specifier: ^3.0.4 - version: 3.0.4 - '@types/d3-selection': specifier: ^3.0.7 version: 3.0.7 + '@types/d3-selection': + specifier: ^3.0.11 + version: 3.0.11 + '@types/d3-transition': + specifier: ^3.0.9 + version: 3.0.9 '@types/d3-zoom': - specifier: ^3.0.5 - version: 3.0.5 + specifier: ^3.0.8 + version: 3.0.8 '@vitejs/plugin-vue': specifier: ^4.4.0 version: 4.4.0(vite@4.4.11(@types/node@20.14.2)(sass@1.79.4)(terser@5.21.0))(vue@3.3.4) @@ -2200,15 +2203,27 @@ packages: '@types/d3-drag@3.0.4': resolution: {integrity: sha512-/t53K1erTuUbP7WIX9SE0hlmytpTYRbIthlhbGkBHzCV5vPO++7yrk8OlisWPyIJO5TGowTmqCtGH2tokY5T/g==} + '@types/d3-drag@3.0.7': + resolution: {integrity: sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==} + '@types/d3-interpolate@3.0.2': resolution: {integrity: sha512-zAbCj9lTqW9J9PlF4FwnvEjXZUy75NQqPm7DMHZXuxCFTpuTrdK2NMYGQekf4hlasL78fCYOLu4EE3/tXElwow==} + '@types/d3-selection@3.0.11': + resolution: {integrity: sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==} + '@types/d3-selection@3.0.7': resolution: {integrity: sha512-qoj2O7KjfqCobmtFOth8FMvjwMVPUAAmn6xiUbLl1ld7vQCPgffvyV5BBcEFfqWdilAUm+3zciU/3P3vZrUMlg==} + '@types/d3-transition@3.0.9': + resolution: {integrity: sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==} + '@types/d3-zoom@3.0.5': resolution: {integrity: sha512-mIefdTLtxuWUWTbBupCUXPAXVPmi8/Uwrq41gQpRh0rD25GMU1ku+oTELqNY2NuuiI0F3wXC5e1liBQi7YS7XQ==} + '@types/d3-zoom@3.0.8': + resolution: {integrity: sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==} + '@types/estree@1.0.2': resolution: {integrity: sha512-VeiPZ9MMwXjO32/Xu7+OwflfmeoRwkE/qzndw42gGtgJwZopBnzy2gD//NN1+go1mADzkDcqf/KnFRSjTJ8xJA==} @@ -4073,6 +4088,7 @@ packages: eslint-plugin-i@2.28.1: resolution: {integrity: sha512-a4oVt0j3ixNhGhvV4XF6NS7OWRFK2rrJ0Q5C4S2dSRb8FxZi31J0uUd5WJLL58wnVJ/OiQ1BxiXnFA4dWQO1Cg==} engines: {node: '>=12'} + deprecated: Please migrate to the brand new `eslint-plugin-import-x` instead peerDependencies: eslint: ^7.2.0 || ^8 @@ -9541,18 +9557,33 @@ snapshots: '@types/d3-drag@3.0.4': dependencies: - '@types/d3-selection': 3.0.7 + '@types/d3-selection': 3.0.11 + + '@types/d3-drag@3.0.7': + dependencies: + '@types/d3-selection': 3.0.11 '@types/d3-interpolate@3.0.2': dependencies: '@types/d3-color': 3.1.1 + '@types/d3-selection@3.0.11': {} + '@types/d3-selection@3.0.7': {} + '@types/d3-transition@3.0.9': + dependencies: + '@types/d3-selection': 3.0.11 + '@types/d3-zoom@3.0.5': dependencies: '@types/d3-interpolate': 3.0.2 - '@types/d3-selection': 3.0.7 + '@types/d3-selection': 3.0.11 + + '@types/d3-zoom@3.0.8': + dependencies: + '@types/d3-interpolate': 3.0.2 + '@types/d3-selection': 3.0.11 '@types/estree@1.0.2': {} @@ -15073,7 +15104,7 @@ snapshots: unpipe@1.0.0: {} - unplugin-auto-import@0.18.3(@nuxt/kit@3.13.2(magicast@0.3.5)(rollup@4.21.0)(webpack-sources@3.2.3))(@vueuse/core@11.2.0(vue@3.5.11(typescript@5.4.5)))(rollup@4.21.0)(webpack-sources@3.2.3): + unplugin-auto-import@0.18.3(@nuxt/kit@3.13.2(rollup@4.21.0)(webpack-sources@3.2.3))(@vueuse/core@11.2.0(vue@3.5.11(typescript@5.4.5)))(rollup@4.21.0)(webpack-sources@3.2.3): dependencies: '@antfu/utils': 0.7.10 '@rollup/pluginutils': 5.1.0(rollup@4.21.0) @@ -15090,7 +15121,7 @@ snapshots: - rollup - webpack-sources - unplugin-auto-import@0.18.3(@nuxt/kit@3.13.2(magicast@0.3.5)(rollup@4.21.0)(webpack-sources@3.2.3))(@vueuse/core@11.2.0(vue@3.5.12(typescript@5.4.5)))(rollup@4.21.0)(webpack-sources@3.2.3): + unplugin-auto-import@0.18.3(@nuxt/kit@3.13.2(rollup@4.21.0)(webpack-sources@3.2.3))(@vueuse/core@11.2.0(vue@3.5.12(typescript@5.4.5)))(rollup@4.21.0)(webpack-sources@3.2.3): dependencies: '@antfu/utils': 0.7.10 '@rollup/pluginutils': 5.1.0(rollup@4.21.0) @@ -15123,7 +15154,7 @@ snapshots: - supports-color - webpack-sources - unplugin-vue-components@0.27.4(@babel/parser@7.25.7)(@nuxt/kit@3.13.2(magicast@0.3.5)(rollup@4.21.0)(webpack-sources@3.2.3))(rollup@4.21.0)(vue@3.5.12(typescript@5.4.5)): + unplugin-vue-components@0.27.4(@babel/parser@7.25.7)(@nuxt/kit@3.13.2(rollup@4.21.0)(webpack-sources@3.2.3))(rollup@4.21.0)(vue@3.5.12(typescript@5.4.5)): dependencies: '@antfu/utils': 0.7.10 '@rollup/pluginutils': 5.1.0(rollup@4.21.0)