index : pan-zoom | |
SVG pan/zoom library. |
aboutsummaryrefslogtreecommitdiff |
diff options
author | Catalin Mititiuc <webdevcat@proton.me> | 2024-04-20 19:34:06 -0700 |
---|---|---|
committer | Catalin Mititiuc <webdevcat@proton.me> | 2024-04-23 10:09:51 -0700 |
commit | 2d3fc1cd22ffcc61ec178eeaf97f3a4d7cba98bf (patch) | |
tree | a072ea398ce00b68dd0e5e670b32ac5ee1a812ad /src | |
parent | 263201d869956b94660d4efa8297e89dadbe36a8 (diff) |
Use CSS transformations instead of manipulating the viewBox
Diffstat (limited to 'src')
-rw-r--r-- | src/app.js | 33 | ||||
-rw-r--r-- | src/modules/pan.js | 62 | ||||
-rw-r--r-- | src/modules/utils.js | 8 | ||||
-rw-r--r-- | src/modules/zoom.js | 75 |
4 files changed, 91 insertions, 87 deletions
@@ -1,18 +1,31 @@ import zoom from './modules/zoom.js'; import pan from './modules/pan.js'; -window.addEventListener('load', function () { - const svg = document.querySelector('object').contentDocument.querySelector('svg'); +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')); +} - svg.addEventListener('wheel', e => { - e.preventDefault(); +// 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`. - svg.setAttributeNS(null, 'viewBox', zoom(svg, e)); - }, { passive: false }); +window.addEventListener('load', function () { + const svg = object.contentDocument.querySelector('svg'), + pannableAndZoomableElements = [img, svg]; - svg.addEventListener('pointerdown', e => { - e.preventDefault(); + button.addEventListener('click', () => { + [button, container].forEach(el => el.classList.toggle('switch')); + reset(pannableAndZoomableElements); + }); - pan(svg, e); - }, { passive: false }); + pannableAndZoomableElements.forEach(el => { + el.addEventListener('wheel', e => zoom(el, e, optionalZoomFactor), { passive: false }); + el.addEventListener('pointerdown', e => pan(el, e), { passive: false }); + }); }); diff --git a/src/modules/pan.js b/src/modules/pan.js index 9faae0a..201c2f1 100644 --- a/src/modules/pan.js +++ b/src/modules/pan.js @@ -1,5 +1,14 @@ +import getComputedTransformMatrix from './utils.js'; + 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 }) { return Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2); } @@ -8,46 +17,21 @@ 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 stopEventPropagationToChildren(el, type) { + el.addEventListener(type, e => e.stopPropagation(), { capture: true, once: true }); } -function getPanCoords(svg, startPt, movePt, initialPos) { - const posChange = getPositionChangeInLocalCoords(svg, startPt, movePt); +function getTranslateMatrix(startPt, movePt) { + const translateMatrix = new DOMMatrix(); - return { - x: initialPos.x + posChange.x, - y: initialPos.y + posChange.y - }; + return translateMatrix.translate(movePt.x - startPt.x, movePt.y - startPt.y); } -function setViewBoxPosition(svg, { x, y }) { - const { width, height } = svg.viewBox.baseVal; - - svg.setAttributeNS(null, 'viewBox', `${x} ${y} ${width} ${height}`); -} +export default function (el, e) { + e.preventDefault(); -export default function (svg, e) { - const { x, y } = svg.viewBox.baseVal, - startPt = setToCurrentPointerCoords(new DOMPoint(), e), + const mtx = getComputedTransformMatrix(el), + startPt = new DOMPoint(e.clientX, e.clientY), movePt = new DOMPoint(); let isPanning = false; @@ -59,19 +43,19 @@ export default function (svg, e) { isPanning = true; e.target.setPointerCapture(e.pointerId); setToCurrentPointerCoords(startPt, e); - stopEventPropagationToChildren(svg, 'click'); + stopEventPropagationToChildren(el, 'click'); } 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', - () => svg.removeEventListener('pointermove', pointerMove), + () => el.removeEventListener('pointermove', pointerMove), { once: true } ); } diff --git a/src/modules/utils.js b/src/modules/utils.js new file mode 100644 index 0000000..e7f5c55 --- /dev/null +++ b/src/modules/utils.js @@ -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); +} diff --git a/src/modules/zoom.js b/src/modules/zoom.js index 24ddb79..97a23e1 100644 --- a/src/modules/zoom.js +++ b/src/modules/zoom.js @@ -1,57 +1,56 @@ -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()); -} +import getComputedTransformMatrix from './utils.js'; function zoomIn(deltaY) { return deltaY < 0; } -function calcSizeChangeAmounts(width, height) { - return { - width: width * zoomStepRatio, - height: height * zoomStepRatio - }; +function getScale(e, factor) { + return zoomIn(e.deltaY) ? 1 + factor : 1 - factor; } -function calcValChangeRatios(focusPoint, x, y, width, height) { +function getFocalPointBeforeTransform(el, e) { + const { x, y, width, height } = el.getBoundingClientRect(); + return { - x: (focusPoint.x - x) / width, - y: (focusPoint.y - y) / height, - width: (width + x - focusPoint.x) / width, - height: (height + y - focusPoint.y) / height + x: e.clientX, + y: e.clientY, + relativeToImageSize: { + x: (e.clientX - x) / width, + y: (e.clientY - y) / height + } }; } -function calcValChangeAmounts(focusPoint, x, y, width, height) { - const changeAmount = calcSizeChangeAmounts(width, height), - valChangeRatio = calcValChangeRatios(focusPoint, x, y, width, height); +function getFocalPointAfterTransform(el, fpBeforeTrans) { + const { x, y, width, height } = el.getBoundingClientRect(), + relativeFocalPoint = fpBeforeTrans.relativeToImageSize; return { - x: valChangeRatio.x * changeAmount.width, - y: valChangeRatio.y * changeAmount.height, - width: valChangeRatio.width * changeAmount.width, - height: valChangeRatio.height * changeAmount.height + x: x + width * relativeFocalPoint.x, + y: y + height * relativeFocalPoint.y }; } -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), +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(); - 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) - }; + const mtx = getComputedTransformMatrix(el), + scale = getScale(e, factor), + transMtx = getTranslateMatrix(el, e, mtx.scale(scale)); - return `${attr.x} ${attr.y} ${attr.width} ${attr.height}`; + el.style.transform = transMtx.multiply(mtx).scale(scale); } |