index : pan-zoom | |
SVG pan/zoom library. |
aboutsummaryrefslogtreecommitdiff |
diff options
-rw-r--r-- | .gitignore | 4 | ||||
-rw-r--r-- | README.md | 9 | ||||
-rw-r--r-- | dev-server.js | 11 | ||||
-rw-r--r-- | index.js | 2 | ||||
-rw-r--r-- | package-lock.json | 431 | ||||
-rw-r--r-- | package.json | 7 | ||||
-rw-r--r-- | public/image.svg | 82 | ||||
-rw-r--r-- | public/index.html | 33 | ||||
-rw-r--r-- | src/app.js | 18 | ||||
-rw-r--r-- | src/modules/pan.js | 77 | ||||
-rw-r--r-- | src/modules/zoom.js | 57 |
11 files changed, 731 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..44848b3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +*.log +*.tgz +dist +node_modules diff --git a/README.md b/README.md new file mode 100644 index 0000000..0ca567d --- /dev/null +++ b/README.md @@ -0,0 +1,9 @@ +## Install dev server packages + + docker run --rm -w /app -v $PWD:/app -u $(id -u):$(id -u) node npm install + +## Start the dev server + + docker run --rm --init -it -w /app -v $PWD:/app -p 8080:8080 node node dev-server.js + +Visit `localhost:8080` to view. diff --git a/dev-server.js b/dev-server.js new file mode 100644 index 0000000..4330d0d --- /dev/null +++ b/dev-server.js @@ -0,0 +1,11 @@ +require('esbuild-server') + .createServer( + { + bundle: true, + entryPoints: ['src/app.js'], + }, + { + static: 'public', + } + ) + .start(); diff --git a/index.js b/index.js new file mode 100644 index 0000000..b619e1b --- /dev/null +++ b/index.js @@ -0,0 +1,2 @@ +export { default as pan } from './src/modules/pan.js'; +export { default as zoom } from './src/modules/zoom.js'; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..d47231c --- /dev/null +++ b/package-lock.json @@ -0,0 +1,431 @@ +{ + "name": "app", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "devDependencies": { + "esbuild": "^0.20.2", + "esbuild-server": "^0.3.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", + "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz", + "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz", + "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz", + "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz", + "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz", + "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz", + "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz", + "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz", + "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz", + "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz", + "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz", + "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz", + "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz", + "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz", + "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz", + "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz", + "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz", + "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz", + "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz", + "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz", + "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz", + "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz", + "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", + "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.20.2", + "@esbuild/android-arm": "0.20.2", + "@esbuild/android-arm64": "0.20.2", + "@esbuild/android-x64": "0.20.2", + "@esbuild/darwin-arm64": "0.20.2", + "@esbuild/darwin-x64": "0.20.2", + "@esbuild/freebsd-arm64": "0.20.2", + "@esbuild/freebsd-x64": "0.20.2", + "@esbuild/linux-arm": "0.20.2", + "@esbuild/linux-arm64": "0.20.2", + "@esbuild/linux-ia32": "0.20.2", + "@esbuild/linux-loong64": "0.20.2", + "@esbuild/linux-mips64el": "0.20.2", + "@esbuild/linux-ppc64": "0.20.2", + "@esbuild/linux-riscv64": "0.20.2", + "@esbuild/linux-s390x": "0.20.2", + "@esbuild/linux-x64": "0.20.2", + "@esbuild/netbsd-x64": "0.20.2", + "@esbuild/openbsd-x64": "0.20.2", + "@esbuild/sunos-x64": "0.20.2", + "@esbuild/win32-arm64": "0.20.2", + "@esbuild/win32-ia32": "0.20.2", + "@esbuild/win32-x64": "0.20.2" + } + }, + "node_modules/esbuild-server": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/esbuild-server/-/esbuild-server-0.3.0.tgz", + "integrity": "sha512-8RuzIdM13gs7MyYwxn/c88nDdx086aREBvzWDk4G3cC7nudF8480OTrvAvanVmFZ9anDv9U4cRX/OKbladaRVA==", + "dev": true, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "esbuild": ">=0.17.0" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..fb86df3 --- /dev/null +++ b/package.json @@ -0,0 +1,7 @@ +{ + "name": "svg-pan-zoom", + "devDependencies": { + "esbuild": "^0.20.2", + "esbuild-server": "^0.3.0" + } +} diff --git a/public/image.svg b/public/image.svg new file mode 100644 index 0000000..5a5c3d9 --- /dev/null +++ b/public/image.svg @@ -0,0 +1,82 @@ +<?xml version="1.0" standalone="no"?> +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" + "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> +<svg viewBox="0 0 400 300" version="1.1" xmlns="http://www.w3.org/2000/svg"> + <style> + svg { + overflow: hidden; + } + + circle, rect { + fill-opacity: 0.9; + } + </style> + <script type="text/javascript">//<![CDATA[ + const svgns = 'http://www.w3.org/2000/svg', + svg = document.querySelector('svg'), + { width: vbWidth, height: vbHeight } = svg.viewBox.baseVal, + shapeCount = 100, + maxColorValue = 256, + circleRadius = { max: 45, min: 5 }, + rectSideLength = { max: 95, min: 5 }; + + function getRandomInt(max) { + return Math.floor(Math.random() * max); + } + + function getRandomPositiveInt(max) { + return getRandomInt(max) + 1; + } + + function getRandomFillAndStrokeVals() { + const shadeFactor = Math.random(), + fill = ['r', 'g', 'b'].map(() => getRandomInt(maxColorValue)), + stroke = fill.map(v => Math.floor(v * shadeFactor)); + + return { + fill: fill, + stroke: stroke + }; + } + + function getRandomCircle(fill, stroke) { + const el = document.createElementNS(svgns, 'circle'), + r = getRandomPositiveInt(circleRadius.max) + circleRadius.min; + + el.setAttributeNS(null, 'cx', getRandomInt(vbWidth)); + el.setAttributeNS(null, 'cy', getRandomInt(vbHeight)); + el.setAttributeNS(null, 'r', r); + el.setAttributeNS(null, 'fill', fill); + el.setAttributeNS(null, 'stroke', stroke); + + return el; + } + + function getRandomRect(fill, stroke) { + const el = document.createElementNS(svgns, 'rect'), + [width, height] = ['w', 'h'].map(() => + getRandomPositiveInt(rectSideLength.max) + rectSideLength.min + ); + + el.setAttributeNS(null, 'x', getRandomInt(vbWidth)); + el.setAttributeNS(null, 'y', getRandomInt(vbHeight)); + el.setAttributeNS(null, 'width', width); + el.setAttributeNS(null, 'height', height); + el.setAttributeNS(null, 'fill', fill); + el.setAttributeNS(null, 'stroke', stroke); + + return el; + } + + function getRandomShape({ fill: fillVals, stroke: strokeVals }) { + const shapes = [getRandomCircle, getRandomRect], + [fill, stroke] = [fillVals, strokeVals].map(v => `rgb(${v.join(', ')})`); + + return shapes[getRandomInt(shapes.length)](fill, stroke); + } + + [...Array(shapeCount)] + .map(() => getRandomFillAndStrokeVals()) + .forEach(fillAndStrokeVal => svg.appendChild(getRandomShape(fillAndStrokeVal))); + //]]></script> +</svg> diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..8040421 --- /dev/null +++ b/public/index.html @@ -0,0 +1,33 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8"/> + <meta name="viewport" content="width=device-width, initial-scale=1.0"/> + <title>SVG Pan & Zoom Example</title> + <style> + body { + text-align: center; + max-width: 100vw; + } + + object { + max-height: 400px; + max-width: 100%; + background-color: lightsteelblue; + border: 1px solid steelblue; + touch-action: none; + } + </style> + </head> + <body> + <h1>Pan & Zoom an SVG Image with JavaScript</h1> + + <p> + Click and drag on the image to pan. Use the mouse wheel + to zoom in and out. + </p> + + <object type="image/svg+xml" data="image.svg"></object> + <script src="app.js"></script> + </body> +</html> diff --git a/src/app.js b/src/app.js new file mode 100644 index 0000000..f5a4fa8 --- /dev/null +++ b/src/app.js @@ -0,0 +1,18 @@ +import zoom from './modules/zoom.js'; +import pan from './modules/pan.js'; + +window.addEventListener('load', function () { + const svg = document.querySelector('object').contentDocument.querySelector('svg'); + + svg.addEventListener('wheel', e => { + e.preventDefault(); + + svg.setAttributeNS(null, 'viewBox', zoom(svg, e)); + }, { passive: false }); + + svg.addEventListener('pointerdown', e => { + e.preventDefault(); + + pan(svg, e); + }, { passive: false }); +}); diff --git a/src/modules/pan.js b/src/modules/pan.js new file mode 100644 index 0000000..9faae0a --- /dev/null +++ b/src/modules/pan.js @@ -0,0 +1,77 @@ +const minDistanceThreshold = 5; + +function distanceBetween({ x: x1, y: y1 }, { x: x2, y: y2 }) { + return Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2); +} + +function minDistanceThresholdIsMet(startPt, endPt) { + return distanceBetween(startPt, endPt) >= minDistanceThreshold; +} + +function getPositionChangeInLocalCoords(svg, startPt, endPt) { + const matrix = svg.getScreenCTM().inverse(), + localStartPt = startPt.matrixTransform(matrix), + localEndPt = endPt.matrixTransform(matrix); + + return { + x: localStartPt.x - localEndPt.x, + y: localStartPt.y - localEndPt.y + }; +} + +function stopEventPropagationToChildren(svg, type) { + svg.addEventListener(type, e => e.stopPropagation(), { capture: true, once: true }); +} + +function setToCurrentPointerCoords(point, e) { + point.x = e.clientX; + point.y = e.clientY; + + return point; +} + +function getPanCoords(svg, startPt, movePt, initialPos) { + const posChange = getPositionChangeInLocalCoords(svg, startPt, movePt); + + return { + x: initialPos.x + posChange.x, + y: initialPos.y + posChange.y + }; +} + +function setViewBoxPosition(svg, { x, y }) { + const { width, height } = svg.viewBox.baseVal; + + svg.setAttributeNS(null, 'viewBox', `${x} ${y} ${width} ${height}`); +} + +export default function (svg, e) { + const { x, y } = svg.viewBox.baseVal, + startPt = setToCurrentPointerCoords(new DOMPoint(), e), + movePt = new DOMPoint(); + + let isPanning = false; + + function pointerMove(e) { + setToCurrentPointerCoords(movePt, e); + + if (!isPanning && minDistanceThresholdIsMet(startPt, movePt)) { + isPanning = true; + e.target.setPointerCapture(e.pointerId); + setToCurrentPointerCoords(startPt, e); + stopEventPropagationToChildren(svg, 'click'); + } + + if (isPanning) { + setViewBoxPosition(svg, getPanCoords(svg, startPt, movePt, { x, y })); + } + } + + svg.addEventListener('pointermove', pointerMove); + + svg.addEventListener( + 'pointerup', + () => svg.removeEventListener('pointermove', pointerMove), + { once: true } + ); +} diff --git a/src/modules/zoom.js b/src/modules/zoom.js new file mode 100644 index 0000000..24ddb79 --- /dev/null +++ b/src/modules/zoom.js @@ -0,0 +1,57 @@ +const zoomStepRatio = 0.25, + positive = 1, + negative = -1; + +function toLocalCoords(svg, x, y) { + const clientP = new DOMPoint(x, y); + + return clientP.matrixTransform(svg.getScreenCTM().inverse()); +} + +function zoomIn(deltaY) { + return deltaY < 0; +} + +function calcSizeChangeAmounts(width, height) { + return { + width: width * zoomStepRatio, + height: height * zoomStepRatio + }; +} + +function calcValChangeRatios(focusPoint, x, y, width, height) { + return { + x: (focusPoint.x - x) / width, + y: (focusPoint.y - y) / height, + width: (width + x - focusPoint.x) / width, + height: (height + y - focusPoint.y) / height + }; +} + +function calcValChangeAmounts(focusPoint, x, y, width, height) { + const changeAmount = calcSizeChangeAmounts(width, height), + valChangeRatio = calcValChangeRatios(focusPoint, x, y, width, height); + + return { + x: valChangeRatio.x * changeAmount.width, + y: valChangeRatio.y * changeAmount.height, + width: valChangeRatio.width * changeAmount.width, + height: valChangeRatio.height * changeAmount.height + }; +} + +export default function (svg, e) { + const pointerPosition = toLocalCoords(svg, e.clientX, e.clientY), + sign = zoomIn(e.deltaY) ? positive : negative, + { x, y, width, height } = svg.viewBox.baseVal, + changeAmount = calcValChangeAmounts(pointerPosition, x, y, width, height), + + attr = { + x: x + sign * changeAmount.x, + y: y + sign * changeAmount.y, + width: width + sign * (-changeAmount.x - changeAmount.width), + height: height + sign * (-changeAmount.y - changeAmount.height) + }; + + return `${attr.x} ${attr.y} ${attr.width} ${attr.height}`; +} |