Reputation: 2693
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 (edit: it works fine on desktop just not on my phone)
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>
Upvotes: 1
Views: 378
Reputation: 17195
You could achieve this by applying multiple translate transformations.
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>
shuffleEls()
gets each path's position via getBBox()
and saves x and y coordinates to a data attributeswap positions:
let el1 = shuffledPaths[i];
let el2 = paths[i];
copyPosFrom(el1, el2);
If a <path>
element is already transformed (e.g rotated), you probably want to retain it.
const paths = document.querySelectorAll(".pathToshuffle");
function revertShuffling(paths) {
if (paths[0].getAttribute('data-pos')) {
paths.forEach((path) => {
copyPosFrom(path, path);
});
}
}
//shuffleEls(paths)
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]);
let shuffledElCount = 0;
for (let i = 0; i < shuffledPaths.length; i += 1) {
let el1 = shuffledPaths[i];
let el2 = paths[i];
shuffledElCount += copyPosFrom(el1, el2);
}
// repeat shuffling if result is identical to previous one
if (shuffledElCount < 1) {
shuffleEls(paths);
}
}
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);
});
// include other transformations
let style = window.getComputedStyle(el);
let matrix = style.transform != 'none' ? style.transform : '';
el.dataset.pos = [x + width / 2, y + height / 2, matrix].join("|");
}
function copyPosFrom(el1, el2) {
let [x1, y1, matrix1] = el1.dataset.pos.split("|");
let [x2, y2, matrix2] = el2.dataset.pos.split("|");
/**
* original position is negated by negative x/y offsets
* new position copied from 2nd element
*/
let transformAtt = `translate(-${x1} -${y1}) translate(${x2} ${y2}) ${matrix1}`;
// compare previous transformations to prevent identical/non-shuffled results
let transFormChange = el1.getAttribute('transform') != transformAtt ? 1 : 0;
el1.setAttribute("transform", transformAtt);
return transFormChange;
}
function shuffleArr(arr) {
let 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>
<button onclick="revertShuffling(paths)">revertShuffling()</button>
</p>
<svg width="500" height="800" xmlns="http://www.w3.org/2000/svg">
<path class="pathToshuffle" id="blackSquare" d="M 20 30 h 60 v 40 h -60 z" fill="#999" />
<path class="pathToshuffle" id="redSquare" d="M 130 70 h 80 v 80 h -80 v -80" fill="red" />
<path class="pathToshuffle" id="greenSquare" d="M 20 120 h 80 v 80 h -80 v -80" fill="green" />
<path class="pathToshuffle" transform="rotate(45 275 35)" id="purpleSquare" d="M 250 10 h 50 v 50 h -50 v -50" fill="purple" />
<path id="bg" d="M 10 10 H 90 V 90 H 10 L 10 10z
M 130 70 h 80 v 80 h -80 v -80z
M 20 120 h 80 v 80 h -80 v -80z
M 250 10 h 50 v 50 h -50 v -50z" fill="none" stroke="#000" stroke-width="1" stroke-dasharray="1 2"/>
</svg>
We can append a matrix()
to the data attribute.
If paths have different sizes or aspect ratios, you can also set centered x/y coordinates according the the actual bounding box.
This way, all shuffled elements will be positioned around the same center points.
let style = window.getComputedStyle(el);
let matrix = style.transform!='none' ? style.transform : '';
el.dataset.pos = [x+width/2, y+height/2, matrix].join("|");
Upvotes: 2
Reputation: 13100
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>
Upvotes: 1