Update implementation to account for WebKit bug
getScreenCTM() on WebKit does not reflect transformations applied to an ancestor (see bug https://bugs.webkit.org/show_bug.cgi?id=209220), so instead of transforming the root <svg> element, we can only transform a child element
This commit is contained in:
parent
e0fbba8fee
commit
b449601a6b
@ -1,6 +1,7 @@
|
|||||||
# Pan-Zoom
|
# Pan-Zoom
|
||||||
|
|
||||||
Pan/zoom library for web browsers. Hold and drag an element to pan. Use the mouse wheel to zoom. See `src/app.js` for a usage example.
|
Pan/zoom library for SVG elements. Hold and drag to pan. Use the mouse wheel to
|
||||||
|
zoom. See `src/app.js` for a usage example.
|
||||||
|
|
||||||
## To view the demo using Docker
|
## To view the demo using Docker
|
||||||
|
|
||||||
|
357
package-lock.json
generated
357
package-lock.json
generated
@ -1,270 +1,17 @@
|
|||||||
{
|
{
|
||||||
"name": "app",
|
"name": "pan-zoom",
|
||||||
|
"version": "0.2.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
|
"name": "pan-zoom",
|
||||||
|
"version": "0.2.0",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"esbuild": "^0.20.2",
|
"esbuild": "^0.20.2",
|
||||||
"esbuild-server": "^0.3.0"
|
"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": {
|
"node_modules/@esbuild/linux-x64": {
|
||||||
"version": "0.20.2",
|
"version": "0.20.2",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz",
|
||||||
@ -281,102 +28,6 @@
|
|||||||
"node": ">=12"
|
"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": {
|
"node_modules/esbuild": {
|
||||||
"version": "0.20.2",
|
"version": "0.20.2",
|
||||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz",
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz",
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "pan-zoom",
|
"name": "pan-zoom",
|
||||||
"version": "0.1.0",
|
"version": "0.2.0",
|
||||||
"browser": "index.js",
|
"browser": "index.js",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"esbuild": "^0.20.2",
|
"esbuild": "^0.20.2",
|
||||||
|
@ -1,40 +1,17 @@
|
|||||||
body {
|
body {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
max-width: 100vw;
|
max-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
object {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
max-width: 586.033px;
|
margin: 5px;
|
||||||
max-height: 586.033px;
|
|
||||||
margin: 0 auto;
|
|
||||||
overflow: hidden;
|
|
||||||
border: 1px solid steelblue;
|
border: 1px solid steelblue;
|
||||||
background-color: gray;
|
background-color: gray;
|
||||||
}
|
|
||||||
|
|
||||||
img, object {
|
|
||||||
touch-action: none;
|
touch-action: none;
|
||||||
}
|
|
||||||
|
|
||||||
img {
|
|
||||||
max-width: 100%;
|
|
||||||
border: 1px solid silver;
|
|
||||||
transform: scale(0.9);
|
|
||||||
}
|
|
||||||
|
|
||||||
.container object, .container.switch img {
|
|
||||||
display: block;
|
display: block;
|
||||||
}
|
min-height: 0;
|
||||||
|
|
||||||
.container img, .container.switch object {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
button .button-text.raster, button.switch .button-text.svg {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
button.switch .button-text.raster {
|
|
||||||
display: inline;
|
|
||||||
}
|
}
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 571 KiB |
@ -1,22 +1,20 @@
|
|||||||
<?xml version="1.0" standalone="no"?>
|
<?xml version="1.0" standalone="yes"?>
|
||||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
|
||||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
|
||||||
<svg viewBox="-200 -150 400 300" version="1.1" xmlns="http://www.w3.org/2000/svg">
|
<svg viewBox="-200 -150 400 300" version="1.1" xmlns="http://www.w3.org/2000/svg">
|
||||||
<style>
|
<style>
|
||||||
svg {
|
|
||||||
overflow: hidden;
|
|
||||||
border: 1px solid silver;
|
|
||||||
transform: scale(0.9);
|
|
||||||
}
|
|
||||||
|
|
||||||
circle, rect {
|
circle, rect {
|
||||||
fill-opacity: 0.9;
|
fill-opacity: 0.9;
|
||||||
filter: drop-shadow(5px 5px 2px rgba(0, 0, 0, .5));
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<g>
|
||||||
|
<circle id="pointer" cx="0" cy="0" r="5" fill="red" stroke="maroon"/>
|
||||||
|
</g>
|
||||||
|
|
||||||
<script type="text/javascript">//<![CDATA[
|
<script type="text/javascript">//<![CDATA[
|
||||||
const svgns = 'http://www.w3.org/2000/svg',
|
const svgns = 'http://www.w3.org/2000/svg',
|
||||||
svg = document.querySelector('svg'),
|
svg = document.querySelector('svg'),
|
||||||
|
group = svg.querySelector('g'),
|
||||||
|
pointerEl = svg.querySelector('#pointer'),
|
||||||
{ x: vbX, y: vbY, width: vbWidth, height: vbHeight } = svg.viewBox.baseVal,
|
{ x: vbX, y: vbY, width: vbWidth, height: vbHeight } = svg.viewBox.baseVal,
|
||||||
|
|
||||||
shapeCount = 100,
|
shapeCount = 100,
|
||||||
@ -103,6 +101,6 @@
|
|||||||
|
|
||||||
[...Array(shapeCount)]
|
[...Array(shapeCount)]
|
||||||
.map(() => getRandomFillAndStrokeVals())
|
.map(() => getRandomFillAndStrokeVals())
|
||||||
.forEach(fillAndStrokeVal => svg.appendChild(getRandomShape(fillAndStrokeVal)));
|
.forEach(fillAndStrokeVal => pointerEl.before(getRandomShape(fillAndStrokeVal)));
|
||||||
//]]></script>
|
//]]></script>
|
||||||
</svg>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.5 KiB |
@ -3,28 +3,17 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8"/>
|
<meta charset="utf-8"/>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||||
<title>JavaScript/CSS Pan & Zoom Demo</title>
|
<title>SVG Element Pan & Zoom Demo</title>
|
||||||
<link rel="stylesheet" href="assets/css/style.css">
|
<link rel="stylesheet" href="assets/css/style.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>Pan & Zoom an Element with CSS/JavaScript</h1>
|
<h1>Pan & Zoom SVG Element with CSS/JavaScript</h1>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
Click and drag on the image to pan. Use the mouse wheel
|
Click and drag the image to pan. Use the mouse wheel to zoom in and out.
|
||||||
to zoom in and out.
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
|
||||||
<button>
|
|
||||||
<span class="button-text svg">Raster image</span>
|
|
||||||
<span class="button-text raster">SVG image</span>
|
|
||||||
</button>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div class="container">
|
|
||||||
<object type="image/svg+xml" data="assets/images/image.svg"></object>
|
<object type="image/svg+xml" data="assets/images/image.svg"></object>
|
||||||
<img src="assets/images/41156165560-4438592e93-o.webp"/>
|
|
||||||
</div>
|
|
||||||
<script src="app.js"></script>
|
<script src="app.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
28
src/app.js
28
src/app.js
@ -2,14 +2,7 @@ import zoom from './modules/zoom.js';
|
|||||||
import pan from './modules/pan.js';
|
import pan from './modules/pan.js';
|
||||||
|
|
||||||
const optionalZoomFactor = 0.1,
|
const optionalZoomFactor = 0.1,
|
||||||
container = document.querySelector('.container'),
|
object = document.querySelector('object');
|
||||||
object = document.querySelector('object'),
|
|
||||||
img = document.querySelector('img'),
|
|
||||||
button = document.querySelector('button');
|
|
||||||
|
|
||||||
function reset(elements) {
|
|
||||||
elements.forEach(el => el.removeAttribute('style'));
|
|
||||||
}
|
|
||||||
|
|
||||||
// If embedding an SVG using an <object> tag, it's necessary to wait until the
|
// If embedding an SVG using an <object> tag, it's necessary to wait until the
|
||||||
// page has loaded before querying its `contentDocument`, otherwise it will be
|
// page has loaded before querying its `contentDocument`, otherwise it will be
|
||||||
@ -17,15 +10,18 @@ function reset(elements) {
|
|||||||
|
|
||||||
window.addEventListener('load', function () {
|
window.addEventListener('load', function () {
|
||||||
const svg = object.contentDocument.querySelector('svg'),
|
const svg = object.contentDocument.querySelector('svg'),
|
||||||
pannableAndZoomableElements = [img, svg];
|
targetEl = svg.querySelector('g'),
|
||||||
|
pointer = svg.querySelector('#pointer'),
|
||||||
|
options = { passive: false };
|
||||||
|
|
||||||
button.addEventListener('click', () => {
|
svg.addEventListener('wheel', e => zoom(targetEl, e, optionalZoomFactor), options);
|
||||||
[button, container].forEach(el => el.classList.toggle('switch'));
|
svg.addEventListener('pointerdown', e => pan(svg, targetEl, e), options);
|
||||||
reset(pannableAndZoomableElements);
|
|
||||||
});
|
|
||||||
|
|
||||||
pannableAndZoomableElements.forEach(el => {
|
svg.addEventListener('pointermove', e => {
|
||||||
el.addEventListener('wheel', e => zoom(el, e, optionalZoomFactor), { passive: false });
|
const pt = new DOMPoint(e.clientX, e.clientY),
|
||||||
el.addEventListener('pointerdown', e => pan(el, e), { passive: false });
|
svgP = pt.matrixTransform(targetEl.getScreenCTM().inverse());
|
||||||
|
|
||||||
|
pointer.setAttributeNS(null, 'cx', svgP.x);
|
||||||
|
pointer.setAttributeNS(null, 'cy', svgP.y);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -2,13 +2,6 @@ import getComputedTransformMatrix from './utils.js';
|
|||||||
|
|
||||||
const minDistanceThreshold = 5;
|
const minDistanceThreshold = 5;
|
||||||
|
|
||||||
function setToCurrentPointerCoords(point, e) {
|
|
||||||
point.x = e.clientX;
|
|
||||||
point.y = e.clientY;
|
|
||||||
|
|
||||||
return point;
|
|
||||||
}
|
|
||||||
|
|
||||||
function distanceBetween({ x: x1, y: y1 }, { x: x2, y: y2 }) {
|
function distanceBetween({ x: x1, y: y1 }, { x: x2, y: y2 }) {
|
||||||
return Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2);
|
return Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2);
|
||||||
}
|
}
|
||||||
@ -27,35 +20,45 @@ function getTranslateMatrix(startPt, movePt) {
|
|||||||
return translateMatrix.translate(movePt.x - startPt.x, movePt.y - startPt.y);
|
return translateMatrix.translate(movePt.x - startPt.x, movePt.y - startPt.y);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function (el, e) {
|
export default function (svg, el, e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const mtx = getComputedTransformMatrix(el),
|
const mtx = getComputedTransformMatrix(el),
|
||||||
startPt = new DOMPoint(e.clientX, e.clientY),
|
inverseScreenCTM = el.getScreenCTM().inverse();
|
||||||
movePt = new DOMPoint();
|
|
||||||
|
|
||||||
let isPanning = false;
|
let startPt = new DOMPoint(e.clientX, e.clientY),
|
||||||
|
movePt = new DOMPoint(),
|
||||||
|
isPanning = false;
|
||||||
|
|
||||||
function pointerMove(e) {
|
function pointerMove(e) {
|
||||||
setToCurrentPointerCoords(movePt, e);
|
movePt.x = e.clientX;
|
||||||
|
movePt.y = e.clientY;
|
||||||
|
|
||||||
if (!isPanning && minDistanceThresholdIsMet(startPt, movePt)) {
|
if (!isPanning && minDistanceThresholdIsMet(startPt, movePt)) {
|
||||||
isPanning = true;
|
isPanning = true;
|
||||||
e.target.setPointerCapture(e.pointerId);
|
e.target.setPointerCapture(e.pointerId);
|
||||||
setToCurrentPointerCoords(startPt, e);
|
|
||||||
|
startPt.x = e.clientX;
|
||||||
|
startPt.y = e.clientY;
|
||||||
|
startPt = startPt.matrixTransform(inverseScreenCTM);
|
||||||
|
|
||||||
stopEventPropagationToChildren(el, 'click');
|
stopEventPropagationToChildren(el, 'click');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isPanning) {
|
if (isPanning) {
|
||||||
el.style.transform = getTranslateMatrix(startPt, movePt).multiply(mtx);
|
movePt.x = e.clientX;
|
||||||
|
movePt.y = e.clientY;
|
||||||
|
movePt = movePt.matrixTransform(inverseScreenCTM);
|
||||||
|
|
||||||
|
el.style.transform = mtx.multiply(getTranslateMatrix(startPt, movePt));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
el.addEventListener('pointermove', pointerMove);
|
svg.addEventListener('pointermove', pointerMove);
|
||||||
|
|
||||||
el.addEventListener(
|
svg.addEventListener(
|
||||||
'pointerup',
|
'pointerup',
|
||||||
() => el.removeEventListener('pointermove', pointerMove),
|
() => svg.removeEventListener('pointermove', pointerMove),
|
||||||
{ once: true }
|
{ once: true }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -8,35 +8,41 @@ function getScale(e, factor) {
|
|||||||
return zoomIn(e.deltaY) ? 1 + factor : 1 - factor;
|
return zoomIn(e.deltaY) ? 1 + factor : 1 - factor;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFocalPointBeforeTransform(el, e) {
|
function getFocalPointBeforeTransform(el, e, inverseScreenCTM) {
|
||||||
const { x, y, width, height } = el.getBoundingClientRect();
|
const { x, y, width, height } = el.getBoundingClientRect(),
|
||||||
|
pointer = (new DOMPoint(e.clientX, e.clientY)).matrixTransform(inverseScreenCTM),
|
||||||
|
origin = (new DOMPoint(x, y)).matrixTransform(inverseScreenCTM),
|
||||||
|
terminus = (new DOMPoint(x + width, y + height)).matrixTransform(inverseScreenCTM);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
x: e.clientX,
|
x: pointer.x,
|
||||||
y: e.clientY,
|
y: pointer.y,
|
||||||
relativeToImageSize: {
|
relativeToImageSize: {
|
||||||
x: (e.clientX - x) / width,
|
x: (pointer.x - origin.x) / (terminus.x - origin.x),
|
||||||
y: (e.clientY - y) / height
|
y: (pointer.y - origin.y) / (terminus.y - origin.y)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFocalPointAfterTransform(el, fpBeforeTrans) {
|
function getFocalPointAfterTransform(el, fpBeforeTrans, inverseScreenCTM) {
|
||||||
const { x, y, width, height } = el.getBoundingClientRect(),
|
const { x, y, width, height } = el.getBoundingClientRect(),
|
||||||
|
origin = (new DOMPoint(x, y)).matrixTransform(inverseScreenCTM),
|
||||||
|
terminus = (new DOMPoint(x + width, y + height)).matrixTransform(inverseScreenCTM),
|
||||||
relativeFocalPoint = fpBeforeTrans.relativeToImageSize;
|
relativeFocalPoint = fpBeforeTrans.relativeToImageSize;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
x: x + width * relativeFocalPoint.x,
|
x: origin.x + (terminus.x - origin.x) * relativeFocalPoint.x,
|
||||||
y: y + height * relativeFocalPoint.y
|
y: origin.y + (terminus.y - origin.y) * relativeFocalPoint.y
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTranslateMatrix(el, e, scaleMatrix) {
|
function getTranslateMatrix(el, e, scaleMatrix) {
|
||||||
const fpBeforeTrans = getFocalPointBeforeTransform(el, e);
|
const inverseScreenCTM = el.getScreenCTM().inverse(),
|
||||||
|
fpBeforeTrans = getFocalPointBeforeTransform(el, e, inverseScreenCTM);
|
||||||
|
|
||||||
el.style.transform = scaleMatrix;
|
el.style.transform = scaleMatrix;
|
||||||
|
|
||||||
const fpAfterTrans = getFocalPointAfterTransform(el, fpBeforeTrans),
|
const fpAfterTrans = getFocalPointAfterTransform(el, fpBeforeTrans, inverseScreenCTM),
|
||||||
translateMatrix = new DOMMatrix();
|
translateMatrix = new DOMMatrix();
|
||||||
|
|
||||||
return translateMatrix.translate(
|
return translateMatrix.translate(
|
||||||
@ -52,5 +58,5 @@ export default function (el, e, factor = 0.1) {
|
|||||||
scale = getScale(e, factor),
|
scale = getScale(e, factor),
|
||||||
transMtx = getTranslateMatrix(el, e, mtx.scale(scale));
|
transMtx = getTranslateMatrix(el, e, mtx.scale(scale));
|
||||||
|
|
||||||
el.style.transform = transMtx.multiply(mtx).scale(scale);
|
el.style.transform = mtx.multiply(transMtx).scale(scale);
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user