@omer-x/svg-viewport 0.1.0 → 0.3.1
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/index.d.ts +13 -10
- package/dist/index.js +110 -2
- package/package.json +7 -3
- package/.eslintignore +0 -1
- package/.eslintrc +0 -12
- package/.github/workflows/npm-publish.yml +0 -20
- package/dist/build/components/SvgViewport.d.ts +0 -18
- package/dist/build/core/matrix.d.ts +0 -2
- package/dist/build/types/point.d.ts +0 -4
- package/dist/build/types/viewport.d.ts +0 -4
- 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
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import React, { Dispatch, SetStateAction, ReactNode } from 'react';
|
|
2
2
|
|
|
3
|
+
type FocusPoint = "center" | "top-left";
|
|
4
|
+
|
|
3
5
|
type ViewportTransform = {
|
|
4
6
|
zoom: number,
|
|
5
7
|
matrix: DOMMatrix,
|
|
@@ -8,17 +10,18 @@ type ViewportTransform = {
|
|
|
8
10
|
type SvgViewportProps = {
|
|
9
11
|
width: number;
|
|
10
12
|
height: number;
|
|
11
|
-
pannable
|
|
12
|
-
zoomable
|
|
13
|
-
minZoom
|
|
14
|
-
maxZoom
|
|
15
|
-
panning
|
|
16
|
-
setPanning
|
|
17
|
-
transformation
|
|
18
|
-
setTransformation
|
|
13
|
+
pannable?: boolean;
|
|
14
|
+
zoomable?: boolean;
|
|
15
|
+
minZoom?: number;
|
|
16
|
+
maxZoom?: number;
|
|
17
|
+
panning?: boolean;
|
|
18
|
+
setPanning?: Dispatch<SetStateAction<boolean>>;
|
|
19
|
+
transformation?: ViewportTransform | null;
|
|
20
|
+
setTransformation?: Dispatch<SetStateAction<ViewportTransform | null>>;
|
|
21
|
+
initialFocusPoint?: FocusPoint;
|
|
19
22
|
className?: string;
|
|
20
23
|
children: ReactNode;
|
|
21
24
|
};
|
|
22
|
-
declare const SvgViewport: ({ width, height, pannable, zoomable, minZoom, maxZoom, panning, setPanning, transformation, setTransformation, className, children, }: SvgViewportProps) => React.JSX.Element;
|
|
25
|
+
declare const SvgViewport: ({ width, height, pannable, zoomable, minZoom, maxZoom, panning, setPanning, transformation, setTransformation, initialFocusPoint, className, children, }: SvgViewportProps) => React.JSX.Element;
|
|
23
26
|
|
|
24
|
-
export {
|
|
27
|
+
export { SvgViewport as default };
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,110 @@
|
|
|
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 getFocusedMatrix(focusPoint, width, height) {
|
|
25
|
+
switch (focusPoint) {
|
|
26
|
+
case "center":
|
|
27
|
+
return focusTo(new DOMMatrix(), { x: 0, y: 0 }, width, height, 1);
|
|
28
|
+
case "top-left":
|
|
29
|
+
return focusTo(new DOMMatrix(), { x: 0, y: 0 }, 0, 0, 1);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function usePolyfillState(state, dispatch) {
|
|
34
|
+
const [polyfill, setPolyfill] = useState(state);
|
|
35
|
+
if (dispatch) {
|
|
36
|
+
return [state, dispatch];
|
|
37
|
+
}
|
|
38
|
+
return [polyfill, setPolyfill];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const SvgViewport = ({ width, height, pannable = false, zoomable = false, minZoom = 0.5, maxZoom = 2, panning = false, setPanning, transformation = null, setTransformation, initialFocusPoint = "center", className, children, }) => {
|
|
42
|
+
const pointer = useRef({ x: 0, y: 0 });
|
|
43
|
+
const [grabbing, setGrabbing] = useState(false);
|
|
44
|
+
const [activeTransformation, activeSetTransformation] = usePolyfillState(transformation, setTransformation);
|
|
45
|
+
const [activePanning, setActivePanning] = usePolyfillState(panning, setPanning);
|
|
46
|
+
const stopGrabbing = () => {
|
|
47
|
+
setGrabbing(false);
|
|
48
|
+
};
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
if (setTransformation)
|
|
51
|
+
return;
|
|
52
|
+
activeSetTransformation({
|
|
53
|
+
zoom: 1,
|
|
54
|
+
matrix: getFocusedMatrix(initialFocusPoint, width, height),
|
|
55
|
+
});
|
|
56
|
+
}, [setTransformation]);
|
|
57
|
+
// panning
|
|
58
|
+
const down = (e) => {
|
|
59
|
+
if (e.button === 0) {
|
|
60
|
+
pointer.current = {
|
|
61
|
+
x: e.clientX,
|
|
62
|
+
y: e.clientY,
|
|
63
|
+
};
|
|
64
|
+
setActivePanning(true);
|
|
65
|
+
}
|
|
66
|
+
setGrabbing(true);
|
|
67
|
+
};
|
|
68
|
+
const move = useCallback((e) => {
|
|
69
|
+
if (activePanning && activeTransformation) {
|
|
70
|
+
const x = (e.clientX - pointer.current.x) / activeTransformation.zoom;
|
|
71
|
+
const y = (e.clientY - pointer.current.y) / activeTransformation.zoom;
|
|
72
|
+
pointer.current = {
|
|
73
|
+
x: e.clientX,
|
|
74
|
+
y: e.clientY,
|
|
75
|
+
};
|
|
76
|
+
activeSetTransformation(t => (t ? Object.assign(Object.assign({}, t), { matrix: t.matrix.translate(x, y) }) : t));
|
|
77
|
+
}
|
|
78
|
+
}, [activePanning, activeTransformation]);
|
|
79
|
+
const up = useCallback(() => {
|
|
80
|
+
setActivePanning(false);
|
|
81
|
+
}, []);
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
if (activePanning) {
|
|
84
|
+
document.addEventListener("mousemove", move);
|
|
85
|
+
document.addEventListener("mouseup", up);
|
|
86
|
+
}
|
|
87
|
+
return () => {
|
|
88
|
+
document.removeEventListener("mousemove", move);
|
|
89
|
+
document.removeEventListener("mouseup", up);
|
|
90
|
+
};
|
|
91
|
+
}, [activePanning]);
|
|
92
|
+
// zooming
|
|
93
|
+
const adjustZoom = (e) => {
|
|
94
|
+
const scale = e.deltaY < 0 ? 1.25 : 0.8;
|
|
95
|
+
const eventTarget = e.currentTarget;
|
|
96
|
+
const eventClientX = e.clientX;
|
|
97
|
+
const eventClientY = e.clientY;
|
|
98
|
+
activeSetTransformation(t => {
|
|
99
|
+
if (t && t.zoom * scale > minZoom && t.zoom * scale < maxZoom) {
|
|
100
|
+
return Object.assign(Object.assign({}, t), { zoom: t.zoom * scale, matrix: adjustWithZoom(t.matrix, scale, eventTarget, eventClientX, eventClientY) });
|
|
101
|
+
}
|
|
102
|
+
return t;
|
|
103
|
+
});
|
|
104
|
+
};
|
|
105
|
+
const cursor = pannable ? ((grabbing || panning) ? "grabbing" : "grab") : "auto";
|
|
106
|
+
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 } },
|
|
107
|
+
React.createElement("g", { transform: transform(activeTransformation === null || activeTransformation === void 0 ? void 0 : activeTransformation.matrix) }, activeTransformation && children)));
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
export { SvgViewport as default };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@omer-x/svg-viewport",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.3.1",
|
|
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",
|
|
@@ -33,8 +36,9 @@
|
|
|
33
36
|
"main": "./dist/index.js",
|
|
34
37
|
"types": "./dist/index.d.ts",
|
|
35
38
|
"scripts": {
|
|
36
|
-
"check-unused-exports": "ts-unused-exports tsconfig.json --excludePathsFromReport='
|
|
39
|
+
"check-unused-exports": "ts-unused-exports tsconfig.json --excludePathsFromReport='src/index'",
|
|
37
40
|
"prebuild": "npm run check-unused-exports && tsc",
|
|
41
|
+
"postbuild": "rimraf dist/build",
|
|
38
42
|
"build": "rollup --config"
|
|
39
43
|
},
|
|
40
44
|
"dependencies": {
|
|
@@ -43,11 +47,11 @@
|
|
|
43
47
|
"devDependencies": {
|
|
44
48
|
"@omer-x/eslint-config": "^1.0.5",
|
|
45
49
|
"@rollup/plugin-alias": "^5.1.0",
|
|
46
|
-
"@rollup/plugin-terser": "^0.4.4",
|
|
47
50
|
"@rollup/plugin-typescript": "^11.1.6",
|
|
48
51
|
"@types/react": "^18.2.55",
|
|
49
52
|
"eslint": "^8.56.0",
|
|
50
53
|
"rollup": "^4.12.0",
|
|
54
|
+
"rollup-plugin-banner2": "^1.2.2",
|
|
51
55
|
"rollup-plugin-dts": "^6.1.0",
|
|
52
56
|
"ts-unused-exports": "^10.0.1",
|
|
53
57
|
"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 }}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import React, { type Dispatch, type ReactNode, type SetStateAction } from "react";
|
|
2
|
-
import type { ViewportTransform } from "~/types/viewport";
|
|
3
|
-
export type SvgViewportProps = {
|
|
4
|
-
width: number;
|
|
5
|
-
height: number;
|
|
6
|
-
pannable: boolean;
|
|
7
|
-
zoomable: boolean;
|
|
8
|
-
minZoom: number;
|
|
9
|
-
maxZoom: number;
|
|
10
|
-
panning: boolean;
|
|
11
|
-
setPanning: (status: boolean) => void;
|
|
12
|
-
transformation: ViewportTransform | null;
|
|
13
|
-
setTransformation: Dispatch<SetStateAction<ViewportTransform | null>>;
|
|
14
|
-
className?: string;
|
|
15
|
-
children: ReactNode;
|
|
16
|
-
};
|
|
17
|
-
declare const SvgViewport: ({ width, height, pannable, zoomable, minZoom, maxZoom, panning, setPanning, transformation, setTransformation, className, children, }: SvgViewportProps) => React.JSX.Element;
|
|
18
|
-
export default SvgViewport;
|
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
|
-
}
|