@omer-x/svg-viewport 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/build/components/SvgViewport.d.ts +11 -8
- package/dist/build/core/matrix.d.ts +3 -1
- package/dist/build/hooks/polyfill-state.d.ts +2 -0
- package/dist/index.d.ts +21 -9
- package/dist/index.js +101 -2
- package/package.json +5 -2
- package/.eslintignore +0 -1
- package/.eslintrc +0 -12
- package/.github/workflows/npm-publish.yml +0 -20
- package/dist/index.js.map +0 -1
- package/rollup.config.js +0 -41
- package/src/components/SvgViewport.tsx +0 -122
- package/src/core/matrix.ts +0 -14
- package/src/types/point.ts +0 -4
- package/src/types/viewport.ts +0 -4
- package/tsconfig.json +0 -32
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
import React, { type Dispatch, type ReactNode, type SetStateAction } from "react";
|
|
2
|
+
import { focusTo } from "~/core/matrix";
|
|
2
3
|
import type { ViewportTransform } from "~/types/viewport";
|
|
4
|
+
export type { ViewportTransform };
|
|
5
|
+
export { focusTo };
|
|
3
6
|
export type SvgViewportProps = {
|
|
4
7
|
width: number;
|
|
5
8
|
height: number;
|
|
6
|
-
pannable
|
|
7
|
-
zoomable
|
|
8
|
-
minZoom
|
|
9
|
-
maxZoom
|
|
10
|
-
panning
|
|
11
|
-
setPanning
|
|
12
|
-
transformation
|
|
13
|
-
setTransformation
|
|
9
|
+
pannable?: boolean;
|
|
10
|
+
zoomable?: boolean;
|
|
11
|
+
minZoom?: number;
|
|
12
|
+
maxZoom?: number;
|
|
13
|
+
panning?: boolean;
|
|
14
|
+
setPanning?: Dispatch<SetStateAction<boolean>>;
|
|
15
|
+
transformation?: ViewportTransform | null;
|
|
16
|
+
setTransformation?: Dispatch<SetStateAction<ViewportTransform | null>>;
|
|
14
17
|
className?: string;
|
|
15
18
|
children: ReactNode;
|
|
16
19
|
};
|
|
@@ -1,2 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
import type { Point } from "~/types/point";
|
|
2
|
+
export declare function transform(matrix?: DOMMatrix): string | undefined;
|
|
3
|
+
export declare function focusTo(matrix: DOMMatrix, { x, y }: Point, width: number, height: number, zoom: number): DOMMatrix;
|
|
2
4
|
export declare function adjustWithZoom(matrix: DOMMatrix, scale: number, svgElement: SVGSVGElement, eX: number, eY: number): DOMMatrix;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
import React, { Dispatch, SetStateAction, ReactNode } from 'react';
|
|
2
2
|
|
|
3
|
+
type Point = {
|
|
4
|
+
x: number,
|
|
5
|
+
y: number,
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
declare function focusTo(matrix: DOMMatrix, { x, y }: Point, width: number, height: number, zoom: number) {
|
|
9
|
+
return matrix.translate(
|
|
10
|
+
((width / 2) / zoom) - x - (matrix.m41 / zoom),
|
|
11
|
+
((height / 2) / zoom) - y - (matrix.m42 / zoom),
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
3
15
|
type ViewportTransform = {
|
|
4
16
|
zoom: number,
|
|
5
17
|
matrix: DOMMatrix,
|
|
@@ -8,17 +20,17 @@ type ViewportTransform = {
|
|
|
8
20
|
type SvgViewportProps = {
|
|
9
21
|
width: number;
|
|
10
22
|
height: number;
|
|
11
|
-
pannable
|
|
12
|
-
zoomable
|
|
13
|
-
minZoom
|
|
14
|
-
maxZoom
|
|
15
|
-
panning
|
|
16
|
-
setPanning
|
|
17
|
-
transformation
|
|
18
|
-
setTransformation
|
|
23
|
+
pannable?: boolean;
|
|
24
|
+
zoomable?: boolean;
|
|
25
|
+
minZoom?: number;
|
|
26
|
+
maxZoom?: number;
|
|
27
|
+
panning?: boolean;
|
|
28
|
+
setPanning?: Dispatch<SetStateAction<boolean>>;
|
|
29
|
+
transformation?: ViewportTransform | null;
|
|
30
|
+
setTransformation?: Dispatch<SetStateAction<ViewportTransform | null>>;
|
|
19
31
|
className?: string;
|
|
20
32
|
children: ReactNode;
|
|
21
33
|
};
|
|
22
34
|
declare const SvgViewport: ({ width, height, pannable, zoomable, minZoom, maxZoom, panning, setPanning, transformation, setTransformation, className, children, }: SvgViewportProps) => React.JSX.Element;
|
|
23
35
|
|
|
24
|
-
export { type SvgViewportProps, SvgViewport as default };
|
|
36
|
+
export { type SvgViewportProps, type ViewportTransform, SvgViewport as default, focusTo };
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,101 @@
|
|
|
1
|
-
"use
|
|
2
|
-
|
|
1
|
+
"use client";
|
|
2
|
+
import React, { useState, useRef, useEffect, useCallback } from 'react';
|
|
3
|
+
|
|
4
|
+
function transform(matrix) {
|
|
5
|
+
if (!matrix)
|
|
6
|
+
return undefined;
|
|
7
|
+
const { a, b, c, d, e, f } = matrix;
|
|
8
|
+
return `matrix(${a}, ${b}, ${c}, ${d}, ${e}, ${f})`;
|
|
9
|
+
}
|
|
10
|
+
function focusTo(matrix, { x, y }, width, height, zoom) {
|
|
11
|
+
return matrix.translate(((width / 2) / zoom) - x - (matrix.m41 / zoom), ((height / 2) / zoom) - y - (matrix.m42 / zoom));
|
|
12
|
+
}
|
|
13
|
+
function adjustWithZoom(matrix, scale, svgElement, eX, eY) {
|
|
14
|
+
const rect = svgElement.getBoundingClientRect();
|
|
15
|
+
const focusPoint = new DOMPoint(eX - rect.left, eY - rect.top);
|
|
16
|
+
const relativePoint = focusPoint.matrixTransform(matrix.inverse());
|
|
17
|
+
const modifier = new DOMMatrix()
|
|
18
|
+
.translate(relativePoint.x, relativePoint.y)
|
|
19
|
+
.scale(scale)
|
|
20
|
+
.translate(-relativePoint.x, -relativePoint.y);
|
|
21
|
+
return matrix.multiply(modifier);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function usePolyfillState(state, dispatch) {
|
|
25
|
+
const [polyfill, setPolyfill] = useState(state);
|
|
26
|
+
if (dispatch) {
|
|
27
|
+
return [state, dispatch];
|
|
28
|
+
}
|
|
29
|
+
return [polyfill, setPolyfill];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const SvgViewport = ({ width, height, pannable = false, zoomable = false, minZoom = 0.5, maxZoom = 2, panning = false, setPanning, transformation = null, setTransformation, className, children, }) => {
|
|
33
|
+
const pointer = useRef({ x: 0, y: 0 });
|
|
34
|
+
const [grabbing, setGrabbing] = useState(false);
|
|
35
|
+
const [activeTransformation, activeSetTransformation] = usePolyfillState(transformation, setTransformation);
|
|
36
|
+
const [activePanning, setActivePanning] = usePolyfillState(panning, setPanning);
|
|
37
|
+
const stopGrabbing = () => {
|
|
38
|
+
setGrabbing(false);
|
|
39
|
+
};
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
if (setTransformation)
|
|
42
|
+
return;
|
|
43
|
+
activeSetTransformation({
|
|
44
|
+
zoom: 1,
|
|
45
|
+
matrix: focusTo(new DOMMatrix(), { x: 0, y: 0 }, width, height, 1),
|
|
46
|
+
});
|
|
47
|
+
}, [setTransformation]);
|
|
48
|
+
// panning
|
|
49
|
+
const down = (e) => {
|
|
50
|
+
if (e.button === 0) {
|
|
51
|
+
pointer.current = {
|
|
52
|
+
x: e.clientX,
|
|
53
|
+
y: e.clientY,
|
|
54
|
+
};
|
|
55
|
+
setActivePanning(true);
|
|
56
|
+
}
|
|
57
|
+
setGrabbing(true);
|
|
58
|
+
};
|
|
59
|
+
const move = useCallback((e) => {
|
|
60
|
+
if (activePanning && activeTransformation) {
|
|
61
|
+
const x = (e.clientX - pointer.current.x) / activeTransformation.zoom;
|
|
62
|
+
const y = (e.clientY - pointer.current.y) / activeTransformation.zoom;
|
|
63
|
+
pointer.current = {
|
|
64
|
+
x: e.clientX,
|
|
65
|
+
y: e.clientY,
|
|
66
|
+
};
|
|
67
|
+
activeSetTransformation(t => (t ? Object.assign(Object.assign({}, t), { matrix: t.matrix.translate(x, y) }) : t));
|
|
68
|
+
}
|
|
69
|
+
}, [activePanning, activeTransformation]);
|
|
70
|
+
const up = useCallback(() => {
|
|
71
|
+
setActivePanning(false);
|
|
72
|
+
}, []);
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
if (activePanning) {
|
|
75
|
+
document.addEventListener("mousemove", move);
|
|
76
|
+
document.addEventListener("mouseup", up);
|
|
77
|
+
}
|
|
78
|
+
return () => {
|
|
79
|
+
document.removeEventListener("mousemove", move);
|
|
80
|
+
document.removeEventListener("mouseup", up);
|
|
81
|
+
};
|
|
82
|
+
}, [activePanning]);
|
|
83
|
+
// zooming
|
|
84
|
+
const adjustZoom = (e) => {
|
|
85
|
+
const scale = e.deltaY < 0 ? 1.25 : 0.8;
|
|
86
|
+
const eventTarget = e.currentTarget;
|
|
87
|
+
const eventClientX = e.clientX;
|
|
88
|
+
const eventClientY = e.clientY;
|
|
89
|
+
activeSetTransformation(t => {
|
|
90
|
+
if (t && t.zoom * scale > minZoom && t.zoom * scale < maxZoom) {
|
|
91
|
+
return Object.assign(Object.assign({}, t), { zoom: t.zoom * scale, matrix: adjustWithZoom(t.matrix, scale, eventTarget, eventClientX, eventClientY) });
|
|
92
|
+
}
|
|
93
|
+
return t;
|
|
94
|
+
});
|
|
95
|
+
};
|
|
96
|
+
const cursor = pannable ? ((grabbing || panning) ? "grabbing" : "grab") : "auto";
|
|
97
|
+
return (React.createElement("svg", { width: width, height: height, onMouseDown: pannable ? down : undefined, onMouseUp: pannable ? stopGrabbing : undefined, onMouseLeave: pannable ? stopGrabbing : undefined, onWheel: zoomable ? adjustZoom : undefined, onContextMenu: e => e.preventDefault(), className: className, style: { cursor } },
|
|
98
|
+
React.createElement("g", { transform: transform(activeTransformation === null || activeTransformation === void 0 ? void 0 : activeTransformation.matrix) }, activeTransformation && children)));
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
export { SvgViewport as default, focusTo };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@omer-x/svg-viewport",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Provides a simple React component for displaying SVG content with zooming and panning capabilities",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|
|
@@ -23,6 +23,9 @@
|
|
|
23
23
|
"publishConfig": {
|
|
24
24
|
"access": "public"
|
|
25
25
|
},
|
|
26
|
+
"files": [
|
|
27
|
+
"dist/"
|
|
28
|
+
],
|
|
26
29
|
"author": {
|
|
27
30
|
"name": "Omer Mecitoglu",
|
|
28
31
|
"email": "omer.mecitoglu@gmail.com",
|
|
@@ -43,11 +46,11 @@
|
|
|
43
46
|
"devDependencies": {
|
|
44
47
|
"@omer-x/eslint-config": "^1.0.5",
|
|
45
48
|
"@rollup/plugin-alias": "^5.1.0",
|
|
46
|
-
"@rollup/plugin-terser": "^0.4.4",
|
|
47
49
|
"@rollup/plugin-typescript": "^11.1.6",
|
|
48
50
|
"@types/react": "^18.2.55",
|
|
49
51
|
"eslint": "^8.56.0",
|
|
50
52
|
"rollup": "^4.12.0",
|
|
53
|
+
"rollup-plugin-banner2": "^1.2.2",
|
|
51
54
|
"rollup-plugin-dts": "^6.1.0",
|
|
52
55
|
"ts-unused-exports": "^10.0.1",
|
|
53
56
|
"tslib": "^2.6.2",
|
package/.eslintignore
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
rollup.config.js
|
package/.eslintrc
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
name: Publish Package to npm
|
|
2
|
-
on:
|
|
3
|
-
release:
|
|
4
|
-
types: [published]
|
|
5
|
-
jobs:
|
|
6
|
-
build:
|
|
7
|
-
runs-on: ubuntu-latest
|
|
8
|
-
steps:
|
|
9
|
-
- uses: actions/checkout@v4
|
|
10
|
-
# Setup .npmrc file to publish to npm
|
|
11
|
-
- uses: actions/setup-node@v4
|
|
12
|
-
with:
|
|
13
|
-
node-version: '20.x'
|
|
14
|
-
registry-url: 'https://registry.npmjs.org'
|
|
15
|
-
- run: npm ci
|
|
16
|
-
# - run: npm test
|
|
17
|
-
- run: npm run build
|
|
18
|
-
- run: npm publish
|
|
19
|
-
env:
|
|
20
|
-
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
package/dist/index.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":["../build/core/matrix.js","../build/components/SvgViewport.js"],"sourcesContent":["export function transform({ a, b, c, d, e, f }) {\n return `matrix(${a}, ${b}, ${c}, ${d}, ${e}, ${f})`;\n}\nexport function adjustWithZoom(matrix, scale, svgElement, eX, eY) {\n const rect = svgElement.getBoundingClientRect();\n const focusPoint = new DOMPoint(eX - rect.left, eY - rect.top);\n const relativePoint = focusPoint.matrixTransform(matrix.inverse());\n const modifier = new DOMMatrix()\n .translate(relativePoint.x, relativePoint.y)\n .scale(scale)\n .translate(-relativePoint.x, -relativePoint.y);\n return matrix.multiply(modifier);\n}\n//# sourceMappingURL=matrix.js.map","import React, { useCallback, useEffect, useRef, useState } from \"react\";\nimport { adjustWithZoom, transform } from \"../core/matrix\";\nconst SvgViewport = ({ width, height, pannable, zoomable, minZoom, maxZoom, panning, setPanning, transformation, setTransformation, className, children, }) => {\n const [grabbing, setGrabbing] = useState(false);\n const pointer = useRef({ x: 0, y: 0 });\n const stopGrabbing = () => {\n setGrabbing(false);\n };\n // panning\n const down = (e) => {\n if (e.button === 0) {\n pointer.current = {\n x: e.clientX,\n y: e.clientY,\n };\n setPanning(true);\n }\n setGrabbing(true);\n };\n const move = useCallback((e) => {\n if (panning && transformation) {\n const x = (e.clientX - pointer.current.x) / transformation.zoom;\n const y = (e.clientY - pointer.current.y) / transformation.zoom;\n pointer.current = {\n x: e.clientX,\n y: e.clientY,\n };\n setTransformation(t => (t ? Object.assign(Object.assign({}, t), { matrix: t.matrix.translate(x, y) }) : t));\n }\n }, [panning, transformation]);\n const up = useCallback(() => {\n setPanning(false);\n }, []);\n useEffect(() => {\n if (panning) {\n document.addEventListener(\"mousemove\", move);\n document.addEventListener(\"mouseup\", up);\n }\n return () => {\n document.removeEventListener(\"mousemove\", move);\n document.removeEventListener(\"mouseup\", up);\n };\n }, [panning]);\n // zooming\n const adjustZoom = (e) => {\n const scale = e.deltaY < 0 ? 1.25 : 0.8;\n const eventTarget = e.currentTarget;\n const eventClientX = e.clientX;\n const eventClientY = e.clientY;\n setTransformation(t => {\n if (t && t.zoom * scale > minZoom && t.zoom * scale < maxZoom) {\n return Object.assign(Object.assign({}, t), { zoom: t.zoom * scale, matrix: adjustWithZoom(t.matrix, scale, eventTarget, eventClientX, eventClientY) });\n }\n return t;\n });\n };\n const cursor = pannable ? ((grabbing || panning) ? \"grabbing\" : \"grab\") : \"auto\";\n return (React.createElement(\"svg\", { width: width, height: height, onMouseDown: pannable ? down : undefined, onMouseUp: pannable ? stopGrabbing : undefined, onMouseLeave: pannable ? stopGrabbing : undefined, onWheel: zoomable ? adjustZoom : undefined, onContextMenu: e => e.preventDefault(), className: className, style: { cursor } },\n React.createElement(\"g\", { transform: transformation ? transform(transformation.matrix) : undefined }, transformation && children)));\n};\nexport default SvgViewport;\n//# sourceMappingURL=SvgViewport.js.map"],"names":["transform","a","b","c","d","e","f","adjustWithZoom","matrix","scale","svgElement","eX","eY","rect","getBoundingClientRect","relativePoint","DOMPoint","left","top","matrixTransform","inverse","modifier","DOMMatrix","translate","x","y","multiply","width","height","pannable","zoomable","minZoom","maxZoom","panning","setPanning","transformation","setTransformation","className","children","grabbing","setGrabbing","useState","pointer","useRef","stopGrabbing","move","useCallback","clientX","current","zoom","clientY","t","Object","assign","up","useEffect","document","addEventListener","removeEventListener","cursor","React","createElement","onMouseDown","button","undefined","onMouseUp","onMouseLeave","onWheel","deltaY","eventTarget","currentTarget","eventClientX","eventClientY","onContextMenu","preventDefault","style"],"mappings":"oCAAO,SAASA,GAAUC,EAAEA,EAACC,EAAEA,EAACC,EAAEA,EAACC,EAAEA,EAACC,EAAEA,EAACC,EAAEA,IACvC,MAAO,UAAUL,MAAMC,MAAMC,MAAMC,MAAMC,MAAMC,IACnD,CACO,SAASC,EAAeC,EAAQC,EAAOC,EAAYC,EAAIC,GAC1D,MAAMC,EAAOH,EAAWI,wBAElBC,EADa,IAAIC,SAASL,EAAKE,EAAKI,KAAML,EAAKC,EAAKK,KACzBC,gBAAgBX,EAAOY,WAClDC,GAAW,IAAIC,WAChBC,UAAUR,EAAcS,EAAGT,EAAcU,GACzChB,MAAMA,GACNc,WAAWR,EAAcS,GAAIT,EAAcU,GAChD,OAAOjB,EAAOkB,SAASL,EAC3B,gBCVoB,EAAGM,QAAOC,SAAQC,WAAUC,WAAUC,UAASC,UAASC,UAASC,aAAYC,iBAAgBC,oBAAmBC,YAAWC,eAC3I,MAAOC,EAAUC,GAAeC,EAAQA,UAAC,GACnCC,EAAUC,EAAAA,OAAO,CAAEnB,EAAG,EAAGC,EAAG,IAC5BmB,EAAe,KACjBJ,GAAY,EAAM,EAahBK,EAAOC,eAAazC,IACtB,GAAI4B,GAAWE,EAAgB,CAC3B,MAAMX,GAAKnB,EAAE0C,QAAUL,EAAQM,QAAQxB,GAAKW,EAAec,KACrDxB,GAAKpB,EAAE6C,QAAUR,EAAQM,QAAQvB,GAAKU,EAAec,KAC3DP,EAAQM,QAAU,CACdxB,EAAGnB,EAAE0C,QACLtB,EAAGpB,EAAE6C,SAETd,GAAkBe,GAAMA,EAAIC,OAAOC,OAAOD,OAAOC,OAAO,CAAE,EAAEF,GAAI,CAAE3C,OAAQ2C,EAAE3C,OAAOe,UAAUC,EAAGC,KAAQ0B,GAC3G,IACF,CAAClB,EAASE,IACPmB,EAAKR,EAAAA,aAAY,KACnBZ,GAAW,EAAM,GAClB,IACHqB,EAAAA,WAAU,KACFtB,IACAuB,SAASC,iBAAiB,YAAaZ,GACvCW,SAASC,iBAAiB,UAAWH,IAElC,KACHE,SAASE,oBAAoB,YAAab,GAC1CW,SAASE,oBAAoB,UAAWJ,EAAG,IAEhD,CAACrB,IAEJ,MAYM0B,EAAS9B,EAAaU,GAAYN,EAAW,WAAa,OAAU,OAC1E,OAAQ2B,EAAMC,cAAc,MAAO,CAAElC,MAAOA,EAAOC,OAAQA,EAAQkC,YAAajC,EAhDlExB,IACO,IAAbA,EAAE0D,SACFrB,EAAQM,QAAU,CACdxB,EAAGnB,EAAE0C,QACLtB,EAAGpB,EAAE6C,SAEThB,GAAW,IAEfM,GAAY,EAAK,OAwC6EwB,EAAWC,UAAWpC,EAAWe,OAAeoB,EAAWE,aAAcrC,EAAWe,OAAeoB,EAAWG,QAASrC,EAbrMzB,IAChB,MAAMI,EAAQJ,EAAE+D,OAAS,EAAI,KAAO,GAC9BC,EAAchE,EAAEiE,cAChBC,EAAelE,EAAE0C,QACjByB,EAAenE,EAAE6C,QACvBd,GAAkBe,GACVA,GAAKA,EAAEF,KAAOxC,EAAQsB,GAAWoB,EAAEF,KAAOxC,EAAQuB,EAC3CoB,OAAOC,OAAOD,OAAOC,OAAO,CAAE,EAAEF,GAAI,CAAEF,KAAME,EAAEF,KAAOxC,EAAOD,OAAQD,EAAe4C,EAAE3C,OAAQC,EAAO4D,EAAaE,EAAcC,KAEnIrB,GACT,OAG2Oa,EAAWS,cAAepE,GAAKA,EAAEqE,iBAAkBrC,UAAWA,EAAWsC,MAAO,CAAEhB,WAC/TC,EAAMC,cAAc,IAAK,CAAE7D,UAAWmC,EAAiBnC,EAAUmC,EAAe3B,aAAUwD,GAAa7B,GAAkBG,GAAY"}
|
package/rollup.config.js
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import path from "path";
|
|
2
|
-
import alias from "@rollup/plugin-alias";
|
|
3
|
-
import terser from "@rollup/plugin-terser";
|
|
4
|
-
import typescript from "@rollup/plugin-typescript";
|
|
5
|
-
import dts from "rollup-plugin-dts";
|
|
6
|
-
|
|
7
|
-
const config = [
|
|
8
|
-
{
|
|
9
|
-
input: "build/components/SvgViewport.js",
|
|
10
|
-
output: {
|
|
11
|
-
file: "dist/index.js",
|
|
12
|
-
format: "cjs",
|
|
13
|
-
sourcemap: true,
|
|
14
|
-
},
|
|
15
|
-
external: ["react"],
|
|
16
|
-
plugins: [
|
|
17
|
-
typescript(),
|
|
18
|
-
alias({
|
|
19
|
-
entries: [
|
|
20
|
-
{ find: /^~/, replacement: path.resolve(process.cwd() + "/src") },
|
|
21
|
-
],
|
|
22
|
-
}),
|
|
23
|
-
terser(),
|
|
24
|
-
],
|
|
25
|
-
}, {
|
|
26
|
-
input: "build/components/SvgViewport.d.ts",
|
|
27
|
-
output: {
|
|
28
|
-
file: "dist/index.d.ts",
|
|
29
|
-
format: "es",
|
|
30
|
-
},
|
|
31
|
-
plugins: [
|
|
32
|
-
dts(),
|
|
33
|
-
alias({
|
|
34
|
-
entries: [
|
|
35
|
-
{ find: /^~/, replacement: path.resolve(process.cwd() + "/src") },
|
|
36
|
-
],
|
|
37
|
-
}),
|
|
38
|
-
],
|
|
39
|
-
},
|
|
40
|
-
];
|
|
41
|
-
export default config;
|
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
import React, { type Dispatch, type ReactNode, type SetStateAction, useCallback, useEffect, useRef, useState } from "react";
|
|
2
|
-
import type { Point } from "~/types/point";
|
|
3
|
-
import type { ViewportTransform } from "~/types/viewport";
|
|
4
|
-
import { adjustWithZoom, transform } from "../core/matrix";
|
|
5
|
-
|
|
6
|
-
export type SvgViewportProps = {
|
|
7
|
-
width: number,
|
|
8
|
-
height: number,
|
|
9
|
-
pannable: boolean,
|
|
10
|
-
zoomable: boolean,
|
|
11
|
-
minZoom: number,
|
|
12
|
-
maxZoom: number,
|
|
13
|
-
panning: boolean,
|
|
14
|
-
setPanning: (status: boolean) => void,
|
|
15
|
-
transformation: ViewportTransform | null,
|
|
16
|
-
setTransformation: Dispatch<SetStateAction<ViewportTransform | null>>,
|
|
17
|
-
className?: string,
|
|
18
|
-
children: ReactNode,
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
const SvgViewport = ({
|
|
22
|
-
width,
|
|
23
|
-
height,
|
|
24
|
-
pannable,
|
|
25
|
-
zoomable,
|
|
26
|
-
minZoom,
|
|
27
|
-
maxZoom,
|
|
28
|
-
panning,
|
|
29
|
-
setPanning,
|
|
30
|
-
transformation,
|
|
31
|
-
setTransformation,
|
|
32
|
-
className,
|
|
33
|
-
children,
|
|
34
|
-
}: SvgViewportProps) => {
|
|
35
|
-
const [grabbing, setGrabbing] = useState(false);
|
|
36
|
-
const pointer = useRef<Point>({ x: 0, y: 0 });
|
|
37
|
-
|
|
38
|
-
const stopGrabbing = () => {
|
|
39
|
-
setGrabbing(false);
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
// panning
|
|
43
|
-
|
|
44
|
-
const down = (e: React.MouseEvent<SVGSVGElement>) => {
|
|
45
|
-
if (e.button === 0) {
|
|
46
|
-
pointer.current = {
|
|
47
|
-
x: e.clientX,
|
|
48
|
-
y: e.clientY,
|
|
49
|
-
};
|
|
50
|
-
setPanning(true);
|
|
51
|
-
}
|
|
52
|
-
setGrabbing(true);
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
const move = useCallback((e: MouseEvent) => {
|
|
56
|
-
if (panning && transformation) {
|
|
57
|
-
const x = (e.clientX - pointer.current.x) / transformation.zoom;
|
|
58
|
-
const y = (e.clientY - pointer.current.y) / transformation.zoom;
|
|
59
|
-
pointer.current = {
|
|
60
|
-
x: e.clientX,
|
|
61
|
-
y: e.clientY,
|
|
62
|
-
};
|
|
63
|
-
setTransformation(t => (t ? { ...t, matrix: t.matrix.translate(x, y) } : t));
|
|
64
|
-
}
|
|
65
|
-
}, [panning, transformation]);
|
|
66
|
-
|
|
67
|
-
const up = useCallback(() => {
|
|
68
|
-
setPanning(false);
|
|
69
|
-
}, []);
|
|
70
|
-
|
|
71
|
-
useEffect(() => {
|
|
72
|
-
if (panning) {
|
|
73
|
-
document.addEventListener("mousemove", move);
|
|
74
|
-
document.addEventListener("mouseup", up);
|
|
75
|
-
}
|
|
76
|
-
return () => {
|
|
77
|
-
document.removeEventListener("mousemove", move);
|
|
78
|
-
document.removeEventListener("mouseup", up);
|
|
79
|
-
};
|
|
80
|
-
}, [panning]);
|
|
81
|
-
|
|
82
|
-
// zooming
|
|
83
|
-
|
|
84
|
-
const adjustZoom = (e: React.WheelEvent<SVGSVGElement>) => {
|
|
85
|
-
const scale = e.deltaY < 0 ? 1.25 : 0.8;
|
|
86
|
-
const eventTarget = e.currentTarget;
|
|
87
|
-
const eventClientX = e.clientX;
|
|
88
|
-
const eventClientY = e.clientY;
|
|
89
|
-
setTransformation(t => {
|
|
90
|
-
if (t && t.zoom * scale > minZoom && t.zoom * scale < maxZoom) {
|
|
91
|
-
return {
|
|
92
|
-
...t,
|
|
93
|
-
zoom: t.zoom * scale,
|
|
94
|
-
matrix: adjustWithZoom(t.matrix, scale, eventTarget, eventClientX, eventClientY),
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
return t;
|
|
98
|
-
});
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
const cursor = pannable ? ((grabbing || panning) ? "grabbing" : "grab") : "auto";
|
|
102
|
-
|
|
103
|
-
return (
|
|
104
|
-
<svg
|
|
105
|
-
width={width}
|
|
106
|
-
height={height}
|
|
107
|
-
onMouseDown={pannable ? down : undefined}
|
|
108
|
-
onMouseUp={pannable ? stopGrabbing : undefined}
|
|
109
|
-
onMouseLeave={pannable ? stopGrabbing : undefined}
|
|
110
|
-
onWheel={zoomable ? adjustZoom : undefined}
|
|
111
|
-
onContextMenu={e => e.preventDefault()}
|
|
112
|
-
className={className}
|
|
113
|
-
style={{ cursor }}
|
|
114
|
-
>
|
|
115
|
-
<g transform={transformation ? transform(transformation.matrix) : undefined}>
|
|
116
|
-
{transformation && children}
|
|
117
|
-
</g>
|
|
118
|
-
</svg>
|
|
119
|
-
);
|
|
120
|
-
};
|
|
121
|
-
|
|
122
|
-
export default SvgViewport;
|
package/src/core/matrix.ts
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
export function transform({ a, b, c, d, e, f }: DOMMatrix) {
|
|
2
|
-
return `matrix(${a}, ${b}, ${c}, ${d}, ${e}, ${f})`;
|
|
3
|
-
}
|
|
4
|
-
|
|
5
|
-
export function adjustWithZoom(matrix: DOMMatrix, scale: number, svgElement: SVGSVGElement, eX: number, eY: number) {
|
|
6
|
-
const rect = svgElement.getBoundingClientRect();
|
|
7
|
-
const focusPoint = new DOMPoint(eX - rect.left, eY - rect.top);
|
|
8
|
-
const relativePoint = focusPoint.matrixTransform(matrix.inverse());
|
|
9
|
-
const modifier = new DOMMatrix()
|
|
10
|
-
.translate(relativePoint.x, relativePoint.y)
|
|
11
|
-
.scale(scale)
|
|
12
|
-
.translate(-relativePoint.x, -relativePoint.y);
|
|
13
|
-
return matrix.multiply(modifier);
|
|
14
|
-
}
|
package/src/types/point.ts
DELETED
package/src/types/viewport.ts
DELETED
package/tsconfig.json
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"outDir": "build",
|
|
4
|
-
"target": "es6",
|
|
5
|
-
"lib": ["dom", "ESNext"],
|
|
6
|
-
"allowJs": true,
|
|
7
|
-
"skipLibCheck": true,
|
|
8
|
-
"strict": true,
|
|
9
|
-
"noEmit": false,
|
|
10
|
-
"esModuleInterop": true,
|
|
11
|
-
"module": "ESNext",
|
|
12
|
-
"moduleResolution": "node",
|
|
13
|
-
"resolveJsonModule": true,
|
|
14
|
-
"isolatedModules": true,
|
|
15
|
-
"declaration": true,
|
|
16
|
-
"sourceMap": true,
|
|
17
|
-
"jsx": "react",
|
|
18
|
-
"incremental": false,
|
|
19
|
-
"plugins": [
|
|
20
|
-
],
|
|
21
|
-
"paths": {
|
|
22
|
-
"~/*": ["./src/*"]
|
|
23
|
-
},
|
|
24
|
-
},
|
|
25
|
-
"include": [
|
|
26
|
-
"src/**/*.ts",
|
|
27
|
-
"src/**/*.tsx",
|
|
28
|
-
],
|
|
29
|
-
"exclude": [
|
|
30
|
-
"node_modules",
|
|
31
|
-
],
|
|
32
|
-
}
|