@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 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: 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>>;
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 { type SvgViewportProps, SvgViewport as default };
27
+ export { SvgViewport as default };
package/dist/index.js CHANGED
@@ -1,2 +1,110 @@
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
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.0",
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='SvgViewport'",
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,12 +0,0 @@
1
- {
2
- "parser": "@typescript-eslint/parser",
3
- "parserOptions": {
4
- "project": ["./tsconfig.json"]
5
- },
6
- "extends": [
7
- "@omer-x/eslint-config",
8
- "@omer-x/eslint-config/typescript"
9
- ],
10
- "rules": {
11
- }
12
- }
@@ -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;
@@ -1,2 +0,0 @@
1
- export declare function transform({ a, b, c, d, e, f }: DOMMatrix): string;
2
- export declare function adjustWithZoom(matrix: DOMMatrix, scale: number, svgElement: SVGSVGElement, eX: number, eY: number): DOMMatrix;
@@ -1,4 +0,0 @@
1
- export type Point = {
2
- x: number;
3
- y: number;
4
- };
@@ -1,4 +0,0 @@
1
- export type ViewportTransform = {
2
- zoom: number;
3
- matrix: DOMMatrix;
4
- };
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;
@@ -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
- }
@@ -1,4 +0,0 @@
1
- export type Point = {
2
- x: number,
3
- y: number,
4
- };
@@ -1,4 +0,0 @@
1
- export type ViewportTransform = {
2
- zoom: number,
3
- matrix: DOMMatrix,
4
- };
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
- }