the relationship between scale when centering one svg element in an other

Viewed 221

I have written these functions to center one svg element in another:

import { Matrix, translate } from 'transformation-matrix';

export const getElementCenter = (element: SVGGraphicsElement) => {
  const bBox = element.getBBox();

  return { x: bBox.x + bBox.width / 2, y: bBox.y + bBox.height / 2 };
};

export const centerElementInOther = (
  element: SVGGraphicsElement,
  other: SVGGraphicsElement,
  scaleFactor: number = 1
): Matrix => {
  const elementCentre = getElementCenter(element);
  const otherCentre = getElementCenter(other);

  const x = elementCentre.x - otherCentre.x;
  const y = elementCentre.y - otherCentre.y;

  // how can I work out the scaleFactor?  If it the actual scale is 0.5 then I need to divide by 2 but if it is 1 then I need to divide by 1

  return translate(-x / scaleFactor, -y / scaleFactor);
};

Everything works unless the element is scaled then I need to apply some maths but I do not understand the ration.

Everything worked fine until I changed the scale of the element to 0.5 and then I had to divide the center x and center y by 2.

2 Answers

Actually you need to multiply x, and y with scaleFactor because scaleFactor is a Decimal Fraction:

return translate(-x * scaleFactor, -y * scaleFactor);

If it the actual scale is 0.5 then I need to divide by 2 but if it is 1 then I need to divide by 1

and what that means is:

x / 2 <=> x * 0.5
x / 1 <=> x * 1

so x * scaleFactor and y * scaleFactor will work just fine with all values of scaleFactor decimal fraction.

Here is some examples on some test x, y values:

const centerElementInOtherDiv = (x, y, scaleFactor = 1) => {
  return {
    x: -x / scaleFactor,
    y: -y / scaleFactor
  };
};

const centerElementInOtherMul = (x, y, scaleFactor = 1) => {
  return {
    x: -x * scaleFactor,
    y: -y * scaleFactor
  };
};


const svgCoords = [
  { x: 100, y: 100 },
  { x: 200, y: 200 },
  { x: 100, y: 300 }
];

for(svgCoord of svgCoords) {
  let mulXY = centerElementInOtherMul(svgCoord.x, svgCoord.y);
  let divXY = centerElementInOtherDiv(svgCoord.x, svgCoord.y);
  console.log(`scaleFactor = 1   -> mul(x: ${mulXY.x}, y: ${mulXY.y}), div(x: ${divXY.x}, y: ${divXY.y})`)

  mulXY = centerElementInOtherMul(svgCoord.x, svgCoord.y, 0.5);
  divXY = centerElementInOtherDiv(svgCoord.x, svgCoord.y, 0.5);
  console.log(`scaleFactor = 0.5 -> mul(x: ${mulXY.x}, y: ${mulXY.y}), div(x: ${divXY.x}, y: ${divXY.y})`)
}

and the result output will be:

scaleFactor = 1   -> mul(x: -100, y: -100), div(x: -100, y: -100)
scaleFactor = 0.5 -> mul(x: -50, y: -50), div(x: -200, y: -200)
scaleFactor = 1   -> mul(x: -200, y: -200), div(x: -200, y: -200)
scaleFactor = 0.5 -> mul(x: -100, y: -100), div(x: -400, y: -400)
scaleFactor = 1   -> mul(x: -100, y: -300), div(x: -100, y: -300)
scaleFactor = 0.5 -> mul(x: -50, y: -150), div(x: -200, y: -600)

as you can see, multiplication works as expected because you're dealing with decimal fractions.

I would have loved to see the SVG code.

One way to center an svg element inside another transformed one is to wrap both elements in a <g> and apply the transformation to the group like so:

const bBox1 = theRect1.getBBox();

c1.setAttributeNS(null,"cx",bBox1.x + bBox1.width / 2)
c1.setAttributeNS(null,"cy",bBox1.y + bBox1.height / 2);
svg{border:1px solid;}
<svg viewBox="0 0 200 100">
  <g transform="scale(.5,.5) translate(150,50) rotate(30,30,10)">
  <rect id="theRect1" x="30" y="10" width="80" height="40" />
  <circle id="c1" r="2" fill="red" />
  </g>
</svg>

If wrapping the elements in a group is not possible you may apply the same transformation to the element you want to center

const bBox2 = theRect2.getBBox();
// get the transformation applied to the rect
let theTransformation = theRect2.getAttribute("transform")

c2.setAttributeNS(null,"cx",bBox2.x + bBox2.width / 2)
c2.setAttributeNS(null,"cy",bBox2.y + bBox2.height / 2);
//apply the same transformation to the circle
c2.setAttributeNS(null,"transform",theTransformation) 
svg{border:1px solid;}
<svg viewBox="0 0 200 100">
  <rect id="theRect2" x="30" y="10" width="80" height="40" transform="scale(.5,.5) translate(150,50) rotate(30,30,10)" />
  <circle id="c2" r="2" fill="red" />
</svg>

In the case you want only the rect to be downscaled but not the circle you can wrap the circle in a group <g>, use the group and upscale the circle the right amount:

const bBox3 = theRect3.getBBox();
// get the transformation applied to the rect
let theTransformation = theRect3.getAttribute("transform");
// get the scale applied
let scaleRy = theTransformation
  .split("scale(")[1]
  .split(")")[0]
  .split(","); //[".5", ".5"]
//calculate the circle's scale
let circleScale = `scale(${1 / Number(scaleRy[0])}, ${1 / Number(scaleRy[1])})`;

theUse.setAttributeNS(null, "x", bBox3.x + bBox3.width / 2);
theUse.setAttributeNS(null, "y", bBox3.y + bBox3.height / 2);
//apply the same transformation to the circle
theUse.setAttributeNS(null, "transform", theTransformation);
//scale the circle
c3.setAttributeNS(null, "transform", circleScale);
svg{border:1px solid;}
<svg viewBox="0 0 200 100">
  <defs>
  <g id="cWrap">
  <circle id="c3" r="2" fill="red" transform="scale(2,2)" ></circle>
  </g>
  </defs>
  <rect id="theRect3" x="30" y="10" width="80" height="40" transform="scale(.5,.5) translate(150,50) rotate(30,30,10)" />
  <use id="theUse" xlink:href="#cWrap" />
</svg>

Related