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-03-17 09:57:58 -07:00
parent f0b5c1a511
commit 99d137cc09
4 changed files with 36 additions and 60 deletions

4
package-lock.json generated
View File

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

View File

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

View File

@ -1,4 +1,4 @@
import getComputedTransformMatrix from './utils.js';
import { default as getComputedTransformMatrix } from './utils';
const minDistanceThreshold = 5;
@ -31,23 +31,26 @@ function getTransformMatrices(el) {
};
}
function clientToSvgPt({ clientX, clientY }, { inverseScreen }, pt = new DOMPoint()) {
pt.x = clientX;
pt.y = clientY;
return pt.matrixTransform(inverseScreen);
function clientToSvgPt(e, inverseScreenMtx, pt = new DOMPoint()) {
pt.x = e.clientX;
pt.y = e.clientY;
return pt.matrixTransform(inverseScreenMtx);
}
function setPanTransform(el, { computed }, startPt, endPt) {
el.style.transform = computed.multiply(getTranslateMatrix(startPt, endPt));
function setTransform(el, computedMtx, startPt, endPt) {
const translateMtx = getTranslateMatrix(startPt, endPt);
const transformMtx = computedMtx.multiply(translateMtx);
el.style.transform = transformMtx;
}
export function programmaticPan(el, from, to) {
const matrices = getTransformMatrices(el);
const startPt = clientToSvgPt(from, matrices);
const endPt = clientToSvgPt(to, matrices);
const startPt = clientToSvgPt(from, matrices.inverseScreen);
const endPt = clientToSvgPt(to, matrices.inverseScreen);
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 });
}
@ -60,13 +63,13 @@ export default function (el) {
if (!isPanning && exceedsMinDistanceThreshhold(startPt, movePt)) {
isPanning = true;
startPt = clientToSvgPt(e, matrices, startPt);
startPt = clientToSvgPt(e, matrices.inverseScreen, startPt);
stopEventPropagationToChildren(el, 'click');
}
if (isPanning) {
movePt = clientToSvgPt(e, matrices, movePt);
setPanTransform(el, matrices, startPt, movePt);
movePt = clientToSvgPt(e, matrices.inverseScreen, 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) {
return deltaY < 0;
}
function getScale(e, factor) {
function getScale(deltaY, factor) {
const outMult = 1 - factor;
const inMult = 1 + factor / outMult
return zoomIn(e.deltaY) ? inMult : outMult;
return zoomIn(deltaY) ? inMult : outMult;
}
function getFocalPointBeforeTransform(el, e, inverseScreenCTM) {
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) {
function getTranslateMatrix(el, clientX, clientY) {
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);
const translateMatrix = new DOMMatrix();
function setTransform(el, computedMtx, translateMtx, scale) {
const transformMtx =
computedMtx.multiply(translateMtx).scale(scale).multiply(translateMtx.inverse());
return translateMatrix.translate(
fpBeforeTrans.x - fpAfterTrans.x,
fpBeforeTrans.y - fpAfterTrans.y
);
el.style.transform = transformMtx;
}
export default function (el, factor = 0.1) {
return e => {
e.preventDefault();
const mtx = getComputedTransformMatrix(el);
const scale = getScale(e, factor);
const transMtx = getTranslateMatrix(el, e, mtx.scale(scale));
const computedMtx = getComputedTransformMatrix(el);
const scale = getScale(e.deltaY, factor);
const translateMtx = getTranslateMatrix(el, e.clientX, e.clientY);
el.style.transform = mtx.multiply(transMtx).scale(scale);
setTransform(el, computedMtx, translateMtx, scale);
}
}