Home > Net >  How to swap the position of two <path> elements inside an SVG
How to swap the position of two <path> elements inside an SVG

Time:12-12

I have an <svg> included in my HTML file with a bunch of <path> elements.

My desired behavior is to be able to randomly shuffle the positioning of the <path> elements, and then to subsequently sort them back into their proper position.

Example: if I have 3 <path>s at positions 1, 2, and 3. For the shuffle functionality, I move path 1 to position 3, path 2 to position 1, and path 3 to position 2. Then I do some kind of visual sort (e.g. insertion sort), where I swap two <path>s' positions at a time until the <path>s are back in their proper place and the SVG looks normal again.

If these were "normal" HTML elements I would just set the x and y properties, but based on my research <path> elements don't have those properties, so I've resorted to using the transform: translate(x y).

With my current approach, the first swap works fine. But any subsequent swaps get way out of whack, and go too far in both directions.

If I'm just swapping two <path>s back and forth, I can get it to work consistently by keeping track of which element is in which position (e.g. elem.setAttribute('currPos', otherElem.id)), and when currPos == currElem.id, setting transform: translate(0 0), but when I start adding more elements, they end up moving to places where there previously wasn’t a <path> element.

My current code is below. For some reason the CSS transition isn’t working properly here but it works elsewhere.

function delay(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

function getPos(elem) {
  let rect = elem.getBoundingClientRect();
  let x = rect.left   (rect.right - rect.left) / 2;
  let y = rect.top   (rect.bottom - rect.top) / 2;
  return [x, y];
}

async function swap(e1, e2, delayMs = 3000) {
  let e1Pos = getPos(e1);
  let e2Pos = getPos(e2);
  console.log(e1Pos, e2Pos);
  e2.setAttribute('transform', `translate(${e1Pos[0]-e2Pos[0]}, ${e1Pos[1]-e2Pos[1]})`);
  e1.setAttribute('transform', `translate(${e2Pos[0]-e1Pos[0]}, ${e2Pos[1]-e1Pos[1]})`);
  if (delayMs) {
    await delay(delayMs);
  }
}

let blackSquare = document.getElementById('black-square');
let redSquare = document.getElementById('red-square');

swap(blackSquare, redSquare)
  .then(() => swap(blackSquare, redSquare))
  .then(() => swap(blackSquare, redSquare));
* {
  position: absolute;
}

path {
  transition: transform 3s
}
<svg width="500" height="800" xmlns="http://www.w3.org/2000/svg">
  <path id="black-square" d="M 10 10 H 90 V 90 H 10 L 10 10" fill="black" />
  <path id="red-square" d="M 130 70 h 80 v 80 h -80 v -80" fill="red" />
  <path id="green-square" d="M 20 120 h 80 v 80 h -80 v -80" fill="green" />
</svg>

CodePudding user response:

I think that is is easier to keep track of the positions if all the <path> elements have the same starting point (so, the same distance to 0,0) and then use transform/translate to position them. You can use elements transform matrix to find the position.

function delay(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

function getPos(elem) {
  let x = elem.transform.baseVal[0].matrix.e;
  let y = elem.transform.baseVal[0].matrix.f;
  return [x,y];
}

async function swap(e1, e2, delayMs = 3000) {
  let e1Pos = getPos(e1);
  let e2Pos = getPos(e2);
  e2.setAttribute('transform', `translate(${e1Pos[0]} ${e1Pos[1]})`);
  e1.setAttribute('transform', `translate(${e2Pos[0]} ${e2Pos[1]})`);
  if (delayMs) {
    await delay(delayMs);
  }
}

let blackSquare = document.getElementById('black-square');
let redSquare = document.getElementById('red-square');
let greenSquare = document.getElementById('green-square');

swap(blackSquare, redSquare)
  .then(() => swap(blackSquare, redSquare))
  .then(() => swap(blackSquare, greenSquare));
path {
  transition: transform 3s
}
<svg viewBox="0 0 500 400" width="500"
  xmlns="http://www.w3.org/2000/svg">
  <path id="black-square" d="M 0 0 H 80 V 80 H 0 Z"
    fill="black" transform="translate(200 50)" />
  <path id="red-square" d="M 0 0 H 80 V 80 H 0 Z"
    fill="red" transform="translate(50 20)" />
  <path id="green-square" d="M 0 0 H 80 V 80 H 0 Z"
    fill="green" transform="translate(100 120)" />
</svg>

CodePudding user response:

You could achieve this by applying multiple translate transform.

Lets say, the red square should be positioned at the black square position:

<path transform="translate(-130 -70) translate(10 10)" id="redSquare" d="M 130 70 h 80 v 80 h -80 v -80" fill="red" ></path>

translate(-130 -70) negates the square's original x,y offset and moves this element to the svg's coordinate origin.
The second transformation translate(10 10) will move this element to the black square's position.

const paths = document.querySelectorAll("path");

function shuffleEls(paths) {
  /**
  * get current positions and save to data attribute
  * skip this step if data attribute is already set
  */
  if(!paths[0].getAttribute('data-pos')){
    paths.forEach((path) => {
      posToDataAtt(path);
    });
  }
  // shuffle path element array
  const shuffledPaths = shuffleArr([...paths]);
  for (let i = 0; i < shuffledPaths.length; i  = 1) {
    let len = shuffledPaths.length;
    //let el1 = i>0 ? shuffledPaths[i-1] : shuffledPaths[len-1] ;
    let el1 = shuffledPaths[i];
    let el2 = paths[i];
    copyPosFrom(el1, el2);
  }
}

function posToDataAtt(el) {
  let bb = el.getBBox();
  let [x, y, width, height] = [bb.x, bb.y, bb.width, bb.height].map((val) => {
    return  val.toFixed(2);
  });
  el.dataset.pos = [x, y].join(" ");
}

function copyPosFrom(el1, el2) {
  let [x1, y1] = el1.dataset.pos.split(" ").map((val) => {
    return  val;
  });
  let [x2, y2] = el2.dataset.pos.split(" ").map((val) => {
    return  val;
  });
  /**
   * original position is negated by negative x/y offsets
   * new position copied from 2nd element
   */
  el1.setAttribute(
    "transform",
    `translate(-${x1} -${y1}) translate(${x2} ${y2})`
  );
}

function shuffleArr(arr) {
  const newArr = arr.slice();
  for (let i = newArr.length - 1; i > 0; i--) {
    const rand = Math.floor(Math.random() * (i   1));
    [newArr[i], newArr[rand]] = [newArr[rand], newArr[i]];
  }
  return newArr;
}
svg{
  border:1px solid red;
}

path{
  transition: 0.5s;
}
<p>
  <button onclick="shuffleEls(paths)">shuffleAll()</button>
</p>

<svg width="500" height="800" xmlns="http://www.w3.org/2000/svg">
  <path id="blackSquare" d="M 10 10 H 90 V 90 H 10 L 10 10" fill="black" />
  <path id="redSquare" d="M 130 70 h 80 v 80 h -80 v -80" fill="red" />
  <path id="greenSquare" d="M 20 120 h 80 v 80 h -80 v -80" fill="green" />
  <path id="purpleSquare" d="M 250 10 h 80 v 80 h -80 v -80" fill="purple" />
</svg>

How it works

  1. shuffleEls() gets each path's position via getBBox() and saves x and y coordinates to a data attribute
  2. We shuffle the path element array
  3. each path inherits the position from it's shuffled counterpart

swap positions:

let el1 = shuffledPaths[i];  
let el2 = paths[i];  
copyPosFrom(el1, el2);  
  • Related