Initial commit

This commit is contained in:
2025-06-16 23:25:25 -07:00
commit 9f82c19ed4
11 changed files with 731 additions and 0 deletions

77
src/modules/pan.js Normal file
View File

@@ -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 }
);
}

57
src/modules/zoom.js Normal file
View File

@@ -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}`;
}