Use CSS transformations instead of manipulating the viewBox
This commit is contained in:
parent
9f82c19ed4
commit
e0fbba8fee
12
README.md
12
README.md
@ -1,9 +1,15 @@
|
|||||||
## Install dev server packages
|
# 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.
|
||||||
|
|
||||||
|
## To view the demo using Docker
|
||||||
|
|
||||||
|
1. Install the development server packages.
|
||||||
|
|
||||||
docker run --rm -w /app -v $PWD:/app -u $(id -u):$(id -u) node npm install
|
docker run --rm -w /app -v $PWD:/app -u $(id -u):$(id -u) node npm install
|
||||||
|
|
||||||
## Start the dev server
|
2. Start the server.
|
||||||
|
|
||||||
docker run --rm --init -it -w /app -v $PWD:/app -p 8080:8080 node node dev-server.js
|
docker run --rm --init -it -w /app -v $PWD:/app -p 8080:8080 node node dev-server.js
|
||||||
|
|
||||||
Visit `localhost:8080` to view.
|
3. Visit `localhost:8080` to view.
|
||||||
|
@ -1,7 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "svg-pan-zoom",
|
"name": "pan-zoom",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"browser": "index.js",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"esbuild": "^0.20.2",
|
"esbuild": "^0.20.2",
|
||||||
"esbuild-server": "^0.3.0"
|
"esbuild-server": "^0.3.0"
|
||||||
}
|
},
|
||||||
|
"files": [
|
||||||
|
"./src/modules"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
40
public/assets/css/style.css
Normal file
40
public/assets/css/style.css
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
body {
|
||||||
|
text-align: center;
|
||||||
|
max-width: 100vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
padding: 0;
|
||||||
|
max-width: 586.033px;
|
||||||
|
max-height: 586.033px;
|
||||||
|
margin: 0 auto;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid steelblue;
|
||||||
|
background-color: gray;
|
||||||
|
}
|
||||||
|
|
||||||
|
img, object {
|
||||||
|
touch-action: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-width: 100%;
|
||||||
|
border: 1px solid silver;
|
||||||
|
transform: scale(0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.container object, .container.switch img {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
BIN
public/assets/images/41156165560-4438592e93-o.webp
Normal file
BIN
public/assets/images/41156165560-4438592e93-o.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 571 KiB |
108
public/assets/images/image.svg
Normal file
108
public/assets/images/image.svg
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
<?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="-200 -150 400 300" version="1.1" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<style>
|
||||||
|
svg {
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid silver;
|
||||||
|
transform: scale(0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
circle, rect {
|
||||||
|
fill-opacity: 0.9;
|
||||||
|
filter: drop-shadow(5px 5px 2px rgba(0, 0, 0, .5));
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<script type="text/javascript">//<![CDATA[
|
||||||
|
const svgns = 'http://www.w3.org/2000/svg',
|
||||||
|
svg = document.querySelector('svg'),
|
||||||
|
{ x: vbX, y: vbY, width: vbWidth, height: vbHeight } = svg.viewBox.baseVal,
|
||||||
|
|
||||||
|
shapeCount = 100,
|
||||||
|
circleRadius = { min: 5, max: 45 },
|
||||||
|
rectSideLength = { min: 5, max: 95 },
|
||||||
|
colorValRange = { min: 0, max: 255 },
|
||||||
|
shadeFactorRange = { min: 0.3, max: 0.7 };
|
||||||
|
|
||||||
|
// source: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/random#getting_a_random_integer_between_two_values_inclusive
|
||||||
|
function getRandomIntInclusive(min, max) {
|
||||||
|
const minCeiled = Math.ceil(min),
|
||||||
|
maxFloored = Math.floor(max);
|
||||||
|
|
||||||
|
return Math.floor(Math.random() * (maxFloored - minCeiled + 1) + minCeiled);
|
||||||
|
}
|
||||||
|
|
||||||
|
// source: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/random#getting_a_random_number_between_two_values
|
||||||
|
function getRandomArbitrary(min, max) {
|
||||||
|
return Math.random() * (max - min) + min;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRandomColorValue() {
|
||||||
|
return getRandomIntInclusive(colorValRange.min, colorValRange.max);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRandomShadeFactor() {
|
||||||
|
return getRandomArbitrary(shadeFactorRange.min, shadeFactorRange.max);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRandomOrigin() {
|
||||||
|
return {
|
||||||
|
x: getRandomIntInclusive(vbX, vbX + vbWidth),
|
||||||
|
y: getRandomIntInclusive(vbY, vbY + vbHeight)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRandomFillAndStrokeVals() {
|
||||||
|
const fill = ['r', 'g', 'b'].map(() => getRandomColorValue()),
|
||||||
|
stroke = fill.map(v => Math.floor(v * getRandomShadeFactor()));
|
||||||
|
|
||||||
|
return {
|
||||||
|
fill: fill,
|
||||||
|
stroke: stroke
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRandomCircle(fill, stroke) {
|
||||||
|
const el = document.createElementNS(svgns, 'circle'),
|
||||||
|
r = getRandomIntInclusive(circleRadius.max, circleRadius.min),
|
||||||
|
origin = getRandomOrigin();
|
||||||
|
|
||||||
|
el.setAttributeNS(null, 'cx', origin.x);
|
||||||
|
el.setAttributeNS(null, 'cy', origin.y);
|
||||||
|
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(() =>
|
||||||
|
getRandomIntInclusive(rectSideLength.max, rectSideLength.min)
|
||||||
|
),
|
||||||
|
origin = getRandomOrigin();
|
||||||
|
|
||||||
|
el.setAttributeNS(null, 'x', origin.x);
|
||||||
|
el.setAttributeNS(null, 'y', origin.y);
|
||||||
|
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 shape = [getRandomCircle, getRandomRect][Math.round(Math.random())],
|
||||||
|
[fill, stroke] = [fillVals, strokeVals].map(v => `rgb(${v.join(', ')})`);
|
||||||
|
|
||||||
|
return shape(fill, stroke);
|
||||||
|
}
|
||||||
|
|
||||||
|
[...Array(shapeCount)]
|
||||||
|
.map(() => getRandomFillAndStrokeVals())
|
||||||
|
.forEach(fillAndStrokeVal => svg.appendChild(getRandomShape(fillAndStrokeVal)));
|
||||||
|
//]]></script>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 3.6 KiB |
@ -1,82 +0,0 @@
|
|||||||
<?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>
|
|
Before Width: | Height: | Size: 2.6 KiB |
@ -3,31 +3,28 @@
|
|||||||
<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>SVG Pan & Zoom Example</title>
|
<title>JavaScript/CSS Pan & Zoom Demo</title>
|
||||||
<style>
|
<link rel="stylesheet" href="assets/css/style.css">
|
||||||
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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>Pan & Zoom an SVG Image with JavaScript</h1>
|
<h1>Pan & Zoom an Element with CSS/JavaScript</h1>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
Click and drag on the image to pan. Use the mouse wheel
|
Click and drag on the image to pan. Use the mouse wheel
|
||||||
to zoom in and out.
|
to zoom in and out.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<object type="image/svg+xml" data="image.svg"></object>
|
<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>
|
||||||
|
<img src="assets/images/41156165560-4438592e93-o.webp"/>
|
||||||
|
</div>
|
||||||
<script src="app.js"></script>
|
<script src="app.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
35
src/app.js
35
src/app.js
@ -1,18 +1,31 @@
|
|||||||
import zoom from './modules/zoom.js';
|
import zoom from './modules/zoom.js';
|
||||||
import pan from './modules/pan.js';
|
import pan from './modules/pan.js';
|
||||||
|
|
||||||
|
const optionalZoomFactor = 0.1,
|
||||||
|
container = document.querySelector('.container'),
|
||||||
|
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
|
||||||
|
// page has loaded before querying its `contentDocument`, otherwise it will be
|
||||||
|
// `null`.
|
||||||
|
|
||||||
window.addEventListener('load', function () {
|
window.addEventListener('load', function () {
|
||||||
const svg = document.querySelector('object').contentDocument.querySelector('svg');
|
const svg = object.contentDocument.querySelector('svg'),
|
||||||
|
pannableAndZoomableElements = [img, svg];
|
||||||
|
|
||||||
svg.addEventListener('wheel', e => {
|
button.addEventListener('click', () => {
|
||||||
e.preventDefault();
|
[button, container].forEach(el => el.classList.toggle('switch'));
|
||||||
|
reset(pannableAndZoomableElements);
|
||||||
|
});
|
||||||
|
|
||||||
svg.setAttributeNS(null, 'viewBox', zoom(svg, e));
|
pannableAndZoomableElements.forEach(el => {
|
||||||
}, { passive: false });
|
el.addEventListener('wheel', e => zoom(el, e, optionalZoomFactor), { passive: false });
|
||||||
|
el.addEventListener('pointerdown', e => pan(el, e), { passive: false });
|
||||||
svg.addEventListener('pointerdown', e => {
|
});
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
pan(svg, e);
|
|
||||||
}, { passive: false });
|
|
||||||
});
|
});
|
||||||
|
@ -1,5 +1,14 @@
|
|||||||
|
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);
|
||||||
}
|
}
|
||||||
@ -8,46 +17,21 @@ function minDistanceThresholdIsMet(startPt, endPt) {
|
|||||||
return distanceBetween(startPt, endPt) >= minDistanceThreshold;
|
return distanceBetween(startPt, endPt) >= minDistanceThreshold;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPositionChangeInLocalCoords(svg, startPt, endPt) {
|
function stopEventPropagationToChildren(el, type) {
|
||||||
const matrix = svg.getScreenCTM().inverse(),
|
el.addEventListener(type, e => e.stopPropagation(), { capture: true, once: true });
|
||||||
localStartPt = startPt.matrixTransform(matrix),
|
|
||||||
localEndPt = endPt.matrixTransform(matrix);
|
|
||||||
|
|
||||||
return {
|
|
||||||
x: localStartPt.x - localEndPt.x,
|
|
||||||
y: localStartPt.y - localEndPt.y
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function stopEventPropagationToChildren(svg, type) {
|
function getTranslateMatrix(startPt, movePt) {
|
||||||
svg.addEventListener(type, e => e.stopPropagation(), { capture: true, once: true });
|
const translateMatrix = new DOMMatrix();
|
||||||
|
|
||||||
|
return translateMatrix.translate(movePt.x - startPt.x, movePt.y - startPt.y);
|
||||||
}
|
}
|
||||||
|
|
||||||
function setToCurrentPointerCoords(point, e) {
|
export default function (el, e) {
|
||||||
point.x = e.clientX;
|
e.preventDefault();
|
||||||
point.y = e.clientY;
|
|
||||||
|
|
||||||
return point;
|
const mtx = getComputedTransformMatrix(el),
|
||||||
}
|
startPt = new DOMPoint(e.clientX, e.clientY),
|
||||||
|
|
||||||
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();
|
movePt = new DOMPoint();
|
||||||
|
|
||||||
let isPanning = false;
|
let isPanning = false;
|
||||||
@ -59,19 +43,19 @@ export default function (svg, e) {
|
|||||||
isPanning = true;
|
isPanning = true;
|
||||||
e.target.setPointerCapture(e.pointerId);
|
e.target.setPointerCapture(e.pointerId);
|
||||||
setToCurrentPointerCoords(startPt, e);
|
setToCurrentPointerCoords(startPt, e);
|
||||||
stopEventPropagationToChildren(svg, 'click');
|
stopEventPropagationToChildren(el, 'click');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isPanning) {
|
if (isPanning) {
|
||||||
setViewBoxPosition(svg, getPanCoords(svg, startPt, movePt, { x, y }));
|
el.style.transform = getTranslateMatrix(startPt, movePt).multiply(mtx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
svg.addEventListener('pointermove', pointerMove);
|
el.addEventListener('pointermove', pointerMove);
|
||||||
|
|
||||||
svg.addEventListener(
|
el.addEventListener(
|
||||||
'pointerup',
|
'pointerup',
|
||||||
() => svg.removeEventListener('pointermove', pointerMove),
|
() => el.removeEventListener('pointermove', pointerMove),
|
||||||
{ once: true }
|
{ once: true }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
8
src/modules/utils.js
Normal file
8
src/modules/utils.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
const digits = /-?\d+\.?\d*/g;
|
||||||
|
|
||||||
|
export default function getComputedTransformMatrix(el) {
|
||||||
|
const matrixSequence = getComputedStyle(el).transform.match(digits),
|
||||||
|
identityMatrix = '';
|
||||||
|
|
||||||
|
return new DOMMatrix(matrixSequence || identityMatrix);
|
||||||
|
}
|
@ -1,57 +1,56 @@
|
|||||||
const zoomStepRatio = 0.25,
|
import getComputedTransformMatrix from './utils.js';
|
||||||
positive = 1,
|
|
||||||
negative = -1;
|
|
||||||
|
|
||||||
function toLocalCoords(svg, x, y) {
|
|
||||||
const clientP = new DOMPoint(x, y);
|
|
||||||
|
|
||||||
return clientP.matrixTransform(svg.getScreenCTM().inverse());
|
|
||||||
}
|
|
||||||
|
|
||||||
function zoomIn(deltaY) {
|
function zoomIn(deltaY) {
|
||||||
return deltaY < 0;
|
return deltaY < 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
function calcSizeChangeAmounts(width, height) {
|
function getScale(e, factor) {
|
||||||
return {
|
return zoomIn(e.deltaY) ? 1 + factor : 1 - factor;
|
||||||
width: width * zoomStepRatio,
|
|
||||||
height: height * zoomStepRatio
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function calcValChangeRatios(focusPoint, x, y, width, height) {
|
function getFocalPointBeforeTransform(el, e) {
|
||||||
return {
|
const { x, y, width, height } = el.getBoundingClientRect();
|
||||||
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 {
|
return {
|
||||||
x: valChangeRatio.x * changeAmount.width,
|
x: e.clientX,
|
||||||
y: valChangeRatio.y * changeAmount.height,
|
y: e.clientY,
|
||||||
width: valChangeRatio.width * changeAmount.width,
|
relativeToImageSize: {
|
||||||
height: valChangeRatio.height * changeAmount.height
|
x: (e.clientX - x) / width,
|
||||||
|
y: (e.clientY - y) / height
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function (svg, e) {
|
function getFocalPointAfterTransform(el, fpBeforeTrans) {
|
||||||
const pointerPosition = toLocalCoords(svg, e.clientX, e.clientY),
|
const { x, y, width, height } = el.getBoundingClientRect(),
|
||||||
sign = zoomIn(e.deltaY) ? positive : negative,
|
relativeFocalPoint = fpBeforeTrans.relativeToImageSize;
|
||||||
{ x, y, width, height } = svg.viewBox.baseVal,
|
|
||||||
changeAmount = calcValChangeAmounts(pointerPosition, x, y, width, height),
|
|
||||||
|
|
||||||
attr = {
|
return {
|
||||||
x: x + sign * changeAmount.x,
|
x: x + width * relativeFocalPoint.x,
|
||||||
y: y + sign * changeAmount.y,
|
y: y + height * relativeFocalPoint.y
|
||||||
width: width + sign * (-changeAmount.x - changeAmount.width),
|
|
||||||
height: height + sign * (-changeAmount.y - changeAmount.height)
|
|
||||||
};
|
};
|
||||||
|
}
|
||||||
return `${attr.x} ${attr.y} ${attr.width} ${attr.height}`;
|
|
||||||
|
function getTranslateMatrix(el, e, scaleMatrix) {
|
||||||
|
const fpBeforeTrans = getFocalPointBeforeTransform(el, e);
|
||||||
|
|
||||||
|
el.style.transform = scaleMatrix;
|
||||||
|
|
||||||
|
const fpAfterTrans = getFocalPointAfterTransform(el, fpBeforeTrans),
|
||||||
|
translateMatrix = new DOMMatrix();
|
||||||
|
|
||||||
|
return translateMatrix.translate(
|
||||||
|
fpBeforeTrans.x - fpAfterTrans.x,
|
||||||
|
fpBeforeTrans.y - fpAfterTrans.y
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function (el, e, factor = 0.1) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const mtx = getComputedTransformMatrix(el),
|
||||||
|
scale = getScale(e, factor),
|
||||||
|
transMtx = getTranslateMatrix(el, e, mtx.scale(scale));
|
||||||
|
|
||||||
|
el.style.transform = transMtx.multiply(mtx).scale(scale);
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user