index : pan-zoom | |
SVG pan/zoom library. |
aboutsummaryrefslogtreecommitdiff |
diff options
author | Catalin Mititiuc <webdevcat@proton.me> | 2024-04-12 10:57:15 -0700 |
---|---|---|
committer | Catalin Mititiuc <webdevcat@proton.me> | 2024-04-15 09:48:00 -0700 |
commit | 263201d869956b94660d4efa8297e89dadbe36a8 (patch) | |
tree | 8fa47922163548dda0739aaa5ff0612de96c9fd1 /src |
Initial commit
Diffstat (limited to 'src')
-rw-r--r-- | src/app.js | 18 | ||||
-rw-r--r-- | src/modules/pan.js | 77 | ||||
-rw-r--r-- | src/modules/zoom.js | 57 |
3 files changed, 152 insertions, 0 deletions
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}`; +} |