2

I am building SVG canvas program for making shapes and editing them.

I need to be able to display <rect /> using x, y, width, height attributes and I need to be able to use transform rotate on them. After using this rotate, I am kinda forced to use transform-origin attribute and it's value will be the center coordinates of this rectangle.

My struggle is when I want to select two of such a rectangles and rotate them around one origin point.

If I just give the transfer-origin the value of coordinates in between those two rectangles and start rotating, it of course jumps.

I know using css matrix instead of just transfer-origin and transform rotate and translate is just different notation, but it seems to be easier to work with them. However, I have this issue that I cannot achieve the same behavior by css matrix without using transfer-origin (which if am doing some calc I am including in the matrix), and getting this offset of when I rotate the object with he matrix and modify it's x, y, it goes different direction.

I know this question might be getting into very broad question but will give it a try: What approach would you use for this "transfer" or transformations, from rotating around its center into rotating around center between two shapes and that those two shapes will remain the same distance in between each other and same angle.

I also tried this with matrix using gl-matrix and d3-select

    const selectedElement = select(`[id="${id}"]`);

    const transformOriginX = centerOfElement.x;
    const transformOriginY = centerOfElement.y;

    // Suppose this is your new transform-origin
    const newTransformOriginX = centerOrigin!.x;
    const newTransformOriginY = centerOrigin!.y;

    const angleInRadians = angleDegrees * (Math.PI / 180);

    // Create identity matrix
    let matrix = mat2d.create();

    // Translate to the origin by the transform-origin point
    let translateToOrigin = mat2d.fromTranslation(mat2d.create(), [
      -transformOriginX,
      -transformOriginY,
    ]);
    matrix = mat2d.multiply(matrix, matrix, translateToOrigin);

    // Apply rotation transformation
    let rotationMatrix = mat2d.fromRotation(mat2d.create(), angleInRadians);
    matrix = mat2d.multiply(matrix, matrix, rotationMatrix);

    // Translate back to its original location by the transform-origin point
    let translateBack = mat2d.fromTranslation(mat2d.create(), [
      transformOriginX,
      transformOriginY,
    ]);
    matrix = mat2d.multiply(matrix, matrix, translateBack);

    // Now, counteract the effect of the new transform-origin
    let translateDiff = mat2d.fromTranslation(mat2d.create(), [
      transformOriginX - newTransformOriginX,
      transformOriginY - newTransformOriginY,
    ]);
    
    matrix = mat2d.multiply(matrix, matrix, translateDiff);

selectedElement.attr(
      'transform',
      `matrix(${matrix[0]}, ${matrix[1]}, ${matrix[2]}, ${matrix[3]}, ${matrix[4]},     ${matrix[5]})`,
    );

For reasons related to my specific scenario, I cannot use SVG groups.

I tried to use only css matrix without transfer-origin and the translation was taken into consideration while calculating the matrix, but getting bad behavior when dragging rotated rectangle.

I tried keeping the transform-origin attribute as a center of each of those rectangles but it causes not easy calculation when transfering from one to another trasnform-origin.

  • 1
    Groups are probably your best chance to apply ... grouped rotations. Can you elaborate why you can't use them? Another approach could be to "harcode" transformations after each user interaction – but this will also result in irreversible transformations and a lot of rounding errors. However it might help if you only need to get a robust final export file. See ["Baking transforms into SVG Path Element commands"](https://stackoverflow.com/questions/5149301/baking-transforms-into-svg-path-element-commands/) – herrstrietzel Aug 21 '23 at 22:37

1 Answers1

0

Provided you can use native browser methods like getBBox() – requiring an element to be attached to the current DOM – you might play with this approach:

(We don't use <g> elements or transform-origin attributes/properties)

  • select a group of SVG elements
  • calculate a group bounding box according to each element's bounding boxes
  • create a <rect> element based on the previously retrieved group bounding box values – this element will act as a "pseudo-group"
  • apply a transformation to this pseudo group rectangle
  • get this <rect> element's transform matrix
  • apply this transform matrix to "pseudo child" elements – actually the elements included to calculate the "pseudo group" bounding box

let group = document.querySelectorAll("rect");

inputRotation.addEventListener("input", (e) => {
  let rotation = +e.currentTarget.value;
  rotateGroup(group, rotation);
});

inputRotation.dispatchEvent(new Event("input"));

function rotateGroup(group, deg = 0) {
  let parentSvg = group[0].closest("svg");
  let ns = "http://www.w3.org/2000/svg";

  // get bounding box based on all elements bounding boxes
  let xAll = [];
  let yAll = [];
  group.forEach((el) => {
    let { x, y, width, height } = el.getBBox();
    xAll.push(x, x + width);
    yAll.push(y, y + height);
  });

  let x = Math.min(...xAll);
  let y = Math.min(...yAll);
  let right = Math.max(...xAll);
  let bottom = Math.max(...yAll);
  let width = right - x;
  let height = bottom - y;

  // add pseudo group rectangle if it doesn't exist
  let gPseudo = document.querySelector("#" + "gTransform");
  if (!gPseudo) {
    gPseudo = document.createElementNS(ns, "rect");
    gPseudo.id = "gTransform";
    parentSvg.append(gPseudo);
    gPseudo.setAttribute("x", x);
    gPseudo.setAttribute("y", y);
    gPseudo.setAttribute("width", width);
    gPseudo.setAttribute("height", height);
    gPseudo.setAttribute("fill-opacity", "0.1");
  }

  gPseudo.setAttribute(
    "transform",
    `rotate(${deg} ${x + width / 2} ${y + height / 2})`
  );
  
  // get group transformation matrix
  let matrix = parentSvg
    .getScreenCTM()
    .inverse()
    .multiply(gPseudo.getScreenCTM());
  let { a, b, c, d, e, f, g } = matrix;

  //apply matrix transform to group children
  group.forEach((el) => {
    el.setAttribute("transform", `matrix(${[a, b, c, d, e, f, g].join(" ")})`);
  });

}
svg{
height:70vmin;
border: 1px solid #ccc
}
<h3>Rotate group</h3>
<p><input type="range" id="inputRotation" value="0" min="0" max="360" step="0.1"></p>
<svg viewBox="0 0 100 100">
  <rect  x="25%" y="25%" width="20%" height="20%"/>
  <rect  x="65%" y="25%" width="20%" height="20%" fill="green"/>
  <rect  x="45%" y="60%" width="20%" height="20%" fill="orange"/>
</svg>
herrstrietzel
  • 11,541
  • 2
  • 12
  • 34