@omer-x/svg-viewport 0.1.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/.eslintignore +1 -0
- package/.eslintrc +12 -0
- package/.github/workflows/npm-publish.yml +20 -0
- package/dist/build/components/SvgViewport.d.ts +18 -0
- package/dist/build/core/matrix.d.ts +2 -0
- package/dist/build/types/point.d.ts +4 -0
- package/dist/build/types/viewport.d.ts +4 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/package.json +56 -0
- package/rollup.config.js +41 -0
- package/src/components/SvgViewport.tsx +122 -0
- package/src/core/matrix.ts +14 -0
- package/src/types/point.ts +4 -0
- package/src/types/viewport.ts +4 -0
- package/tsconfig.json +32 -0
package/.eslintignore
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
rollup.config.js
|
package/.eslintrc
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
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 }}
|
|
@@ -0,0 +1,18 @@
|
|
|
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.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import React, { Dispatch, SetStateAction, ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
type ViewportTransform = {
|
|
4
|
+
zoom: number,
|
|
5
|
+
matrix: DOMMatrix,
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
type SvgViewportProps = {
|
|
9
|
+
width: number;
|
|
10
|
+
height: number;
|
|
11
|
+
pannable: boolean;
|
|
12
|
+
zoomable: boolean;
|
|
13
|
+
minZoom: number;
|
|
14
|
+
maxZoom: number;
|
|
15
|
+
panning: boolean;
|
|
16
|
+
setPanning: (status: boolean) => void;
|
|
17
|
+
transformation: ViewportTransform | null;
|
|
18
|
+
setTransformation: Dispatch<SetStateAction<ViewportTransform | null>>;
|
|
19
|
+
className?: string;
|
|
20
|
+
children: ReactNode;
|
|
21
|
+
};
|
|
22
|
+
declare const SvgViewport: ({ width, height, pannable, zoomable, minZoom, maxZoom, panning, setPanning, transformation, setTransformation, className, children, }: SvgViewportProps) => React.JSX.Element;
|
|
23
|
+
|
|
24
|
+
export { type SvgViewportProps, SvgViewport as default };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use strict";var e=require("react");function t({a:e,b:t,c:n,d:o,e:r,f:a}){return`matrix(${e}, ${t}, ${n}, ${o}, ${r}, ${a})`}function n(e,t,n,o,r){const a=n.getBoundingClientRect(),i=new DOMPoint(o-a.left,r-a.top).matrixTransform(e.inverse()),s=(new DOMMatrix).translate(i.x,i.y).scale(t).translate(-i.x,-i.y);return e.multiply(s)}module.exports=({width:o,height:r,pannable:a,zoomable:i,minZoom:s,maxZoom:c,panning:m,setPanning:u,transformation:l,setTransformation:d,className:v,children:x})=>{const[g,b]=e.useState(!1),f=e.useRef({x:0,y:0}),p=()=>{b(!1)},h=e.useCallback((e=>{if(m&&l){const t=(e.clientX-f.current.x)/l.zoom,n=(e.clientY-f.current.y)/l.zoom;f.current={x:e.clientX,y:e.clientY},d((e=>e?Object.assign(Object.assign({},e),{matrix:e.matrix.translate(t,n)}):e))}}),[m,l]),y=e.useCallback((()=>{u(!1)}),[]);e.useEffect((()=>(m&&(document.addEventListener("mousemove",h),document.addEventListener("mouseup",y)),()=>{document.removeEventListener("mousemove",h),document.removeEventListener("mouseup",y)})),[m]);const z=a?g||m?"grabbing":"grab":"auto";return e.createElement("svg",{width:o,height:r,onMouseDown:a?e=>{0===e.button&&(f.current={x:e.clientX,y:e.clientY},u(!0)),b(!0)}:void 0,onMouseUp:a?p:void 0,onMouseLeave:a?p:void 0,onWheel:i?e=>{const t=e.deltaY<0?1.25:.8,o=e.currentTarget,r=e.clientX,a=e.clientY;d((e=>e&&e.zoom*t>s&&e.zoom*t<c?Object.assign(Object.assign({},e),{zoom:e.zoom*t,matrix:n(e.matrix,t,o,r,a)}):e))}:void 0,onContextMenu:e=>e.preventDefault(),className:v,style:{cursor:z}},e.createElement("g",{transform:l?t(l.matrix):void 0},l&&x))};
|
|
2
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
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/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@omer-x/svg-viewport",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Provides a simple React component for displaying SVG content with zooming and panning capabilities",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"react",
|
|
7
|
+
"svg",
|
|
8
|
+
"component",
|
|
9
|
+
"zooming",
|
|
10
|
+
"panning",
|
|
11
|
+
"viewport",
|
|
12
|
+
"scalable-vector-graphics"
|
|
13
|
+
],
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/omermecitoglu/svg-viewport.git"
|
|
17
|
+
},
|
|
18
|
+
"bugs": {
|
|
19
|
+
"url": "https://github.com/omermecitoglu/svg-viewport/issues"
|
|
20
|
+
},
|
|
21
|
+
"homepage": "https://github.com/omermecitoglu/svg-viewport#readme",
|
|
22
|
+
"private": false,
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "public"
|
|
25
|
+
},
|
|
26
|
+
"author": {
|
|
27
|
+
"name": "Omer Mecitoglu",
|
|
28
|
+
"email": "omer.mecitoglu@gmail.com",
|
|
29
|
+
"url": "https://omermecitoglu.github.io"
|
|
30
|
+
},
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"type": "module",
|
|
33
|
+
"main": "./dist/index.js",
|
|
34
|
+
"types": "./dist/index.d.ts",
|
|
35
|
+
"scripts": {
|
|
36
|
+
"check-unused-exports": "ts-unused-exports tsconfig.json --excludePathsFromReport='SvgViewport'",
|
|
37
|
+
"prebuild": "npm run check-unused-exports && tsc",
|
|
38
|
+
"build": "rollup --config"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"react": "^18.2.0"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@omer-x/eslint-config": "^1.0.5",
|
|
45
|
+
"@rollup/plugin-alias": "^5.1.0",
|
|
46
|
+
"@rollup/plugin-terser": "^0.4.4",
|
|
47
|
+
"@rollup/plugin-typescript": "^11.1.6",
|
|
48
|
+
"@types/react": "^18.2.55",
|
|
49
|
+
"eslint": "^8.56.0",
|
|
50
|
+
"rollup": "^4.12.0",
|
|
51
|
+
"rollup-plugin-dts": "^6.1.0",
|
|
52
|
+
"ts-unused-exports": "^10.0.1",
|
|
53
|
+
"tslib": "^2.6.2",
|
|
54
|
+
"typescript": "^5.3.3"
|
|
55
|
+
}
|
|
56
|
+
}
|
package/rollup.config.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
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;
|
|
@@ -0,0 +1,122 @@
|
|
|
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;
|
|
@@ -0,0 +1,14 @@
|
|
|
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/tsconfig.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
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
|
+
}
|