Conjugate scaling operation by translating to the origin

https://stackoverflow.com/questions/38446666/scaling-around-a-specific-point-in-2d-coordinate-system
This commit is contained in:
Catalin Constantin Mititiuc 2025-06-16 23:25:25 -07:00
parent 3fa7f02571
commit d2a670731f
4 changed files with 36 additions and 60 deletions

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "pan-zoom", "name": "pan-zoom",
"version": "0.3.0", "version": "0.3.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "pan-zoom", "name": "pan-zoom",
"version": "0.3.0", "version": "0.3.1",
"license": "ISC", "license": "ISC",
"devDependencies": { "devDependencies": {
"esbuild": "^0.20.2", "esbuild": "^0.20.2",

View File

@ -1,6 +1,6 @@
{ {
"name": "pan-zoom", "name": "pan-zoom",
"version": "0.3.0", "version": "0.3.1",
"description": "Pan/zoom SVG images in the browser", "description": "Pan/zoom SVG images in the browser",
"browser": "index.js", "browser": "index.js",
"files": [ "files": [

View File

@ -1,4 +1,4 @@
import getComputedTransformMatrix from './utils.js'; import { default as getComputedTransformMatrix } from './utils';
const minDistanceThreshold = 5; const minDistanceThreshold = 5;
@ -31,23 +31,26 @@ function getTransformMatrices(el) {
}; };
} }
function clientToSvgPt({ clientX, clientY }, { inverseScreen }, pt = new DOMPoint()) { function clientToSvgPt(e, inverseScreenMtx, pt = new DOMPoint()) {
pt.x = clientX; pt.x = e.clientX;
pt.y = clientY; pt.y = e.clientY;
return pt.matrixTransform(inverseScreen); return pt.matrixTransform(inverseScreenMtx);
} }
function setPanTransform(el, { computed }, startPt, endPt) { function setTransform(el, computedMtx, startPt, endPt) {
el.style.transform = computed.multiply(getTranslateMatrix(startPt, endPt)); const translateMtx = getTranslateMatrix(startPt, endPt);
const transformMtx = computedMtx.multiply(translateMtx);
el.style.transform = transformMtx;
} }
export function programmaticPan(el, from, to) { export function programmaticPan(el, from, to) {
const matrices = getTransformMatrices(el); const matrices = getTransformMatrices(el);
const startPt = clientToSvgPt(from, matrices); const startPt = clientToSvgPt(from, matrices.inverseScreen);
const endPt = clientToSvgPt(to, matrices); const endPt = clientToSvgPt(to, matrices.inverseScreen);
el.style.transition = 'transform 0.5s'; el.style.transition = 'transform 0.5s';
setPanTransform(el, matrices, startPt, endPt); setTransform(el, matrices.computed, startPt, endPt);
el.addEventListener('transitionend', () => el.style.transition = '', { once: true }); el.addEventListener('transitionend', () => el.style.transition = '', { once: true });
} }
@ -60,13 +63,13 @@ export default function (el) {
if (!isPanning && exceedsMinDistanceThreshhold(startPt, movePt)) { if (!isPanning && exceedsMinDistanceThreshhold(startPt, movePt)) {
isPanning = true; isPanning = true;
startPt = clientToSvgPt(e, matrices, startPt); startPt = clientToSvgPt(e, matrices.inverseScreen, startPt);
stopEventPropagationToChildren(el, 'click'); stopEventPropagationToChildren(el, 'click');
} }
if (isPanning) { if (isPanning) {
movePt = clientToSvgPt(e, matrices, movePt); movePt = clientToSvgPt(e, matrices.inverseScreen, movePt);
setPanTransform(el, matrices, startPt, movePt); setTransform(el, matrices.computed, startPt, movePt);
} }
} }

View File

@ -1,67 +1,40 @@
import getComputedTransformMatrix from './utils.js'; import { default as getComputedTransformMatrix } from './utils';
function zoomIn(deltaY) { function zoomIn(deltaY) {
return deltaY < 0; return deltaY < 0;
} }
function getScale(e, factor) { function getScale(deltaY, factor) {
const outMult = 1 - factor; const outMult = 1 - factor;
const inMult = 1 + factor / outMult const inMult = 1 + factor / outMult
return zoomIn(e.deltaY) ? inMult : outMult; return zoomIn(deltaY) ? inMult : outMult;
} }
function getFocalPointBeforeTransform(el, e, inverseScreenCTM) { function getTranslateMatrix(el, clientX, clientY) {
const { x, y, width, height } = el.getBoundingClientRect();
const pointer = (new DOMPoint(e.clientX, e.clientY)).matrixTransform(inverseScreenCTM);
const origin = (new DOMPoint(x, y)).matrixTransform(inverseScreenCTM);
const terminus = (new DOMPoint(x + width, y + height)).matrixTransform(inverseScreenCTM);
return {
x: pointer.x,
y: pointer.y,
relativeToImageSize: {
x: (pointer.x - origin.x) / (terminus.x - origin.x),
y: (pointer.y - origin.y) / (terminus.y - origin.y)
}
};
}
function getFocalPointAfterTransform(el, fpBeforeTrans, inverseScreenCTM) {
const { x, y, width, height } = el.getBoundingClientRect();
const origin = (new DOMPoint(x, y)).matrixTransform(inverseScreenCTM);
const terminus = (new DOMPoint(x + width, y + height)).matrixTransform(inverseScreenCTM);
const relativeFocalPoint = fpBeforeTrans.relativeToImageSize;
return {
x: origin.x + (terminus.x - origin.x) * relativeFocalPoint.x,
y: origin.y + (terminus.y - origin.y) * relativeFocalPoint.y
};
}
function getTranslateMatrix(el, e, scaleMatrix) {
const inverseScreenCTM = el.getScreenCTM().inverse(); const inverseScreenCTM = el.getScreenCTM().inverse();
const fpBeforeTrans = getFocalPointBeforeTransform(el, e, inverseScreenCTM); const translateMtx = new DOMMatrix();
const pointer = new DOMPoint(clientX, clientY)
const { x, y } = pointer.matrixTransform(inverseScreenCTM);
el.style.transform = scaleMatrix; return translateMtx.translate(x, y);
}
const fpAfterTrans = getFocalPointAfterTransform(el, fpBeforeTrans, inverseScreenCTM); function setTransform(el, computedMtx, translateMtx, scale) {
const translateMatrix = new DOMMatrix(); const transformMtx =
computedMtx.multiply(translateMtx).scale(scale).multiply(translateMtx.inverse());
return translateMatrix.translate( el.style.transform = transformMtx;
fpBeforeTrans.x - fpAfterTrans.x,
fpBeforeTrans.y - fpAfterTrans.y
);
} }
export default function (el, factor = 0.1) { export default function (el, factor = 0.1) {
return e => { return e => {
e.preventDefault(); e.preventDefault();
const mtx = getComputedTransformMatrix(el); const computedMtx = getComputedTransformMatrix(el);
const scale = getScale(e, factor); const scale = getScale(e.deltaY, factor);
const transMtx = getTranslateMatrix(el, e, mtx.scale(scale)); const translateMtx = getTranslateMatrix(el, e.clientX, e.clientY);
el.style.transform = mtx.multiply(transMtx).scale(scale); setTransform(el, computedMtx, translateMtx, scale);
} }
} }