如何在 <path> SVG 中交换两个元素的位置



我有一个<svg>包含在我的HTML文件与一堆<path>元素。

我期望的行为是能够随机打乱<path>元素的位置,然后随后将它们排序回适当的位置。

示例:如果位置1、2和3有3个<path>。对于洗牌功能,我将路径1移动到位置3,将路径2移动到位置1,将路径3移动到位置2。然后我做某种视觉排序(例如插入排序),在那里我一次交换两个<path>s的位置,直到<path>s回到正确的位置,SVG看起来又正常了。

如果这些都是"正常的"HTML元素,我只想设置x和y属性,但根据我的研究,<path>元素没有这些属性,所以我不得不使用transform: translate(x y)

使用我当前的方法,第一次交换工作得很好。但是任何后续的互换都是不正常的,并且在两个方向上都走得太远了。

如果我只是来回交换两个<path>s,我可以通过跟踪哪个元素在哪个位置(例如elem.setAttribute('currPos', otherElem.id)),以及当currPos == currElem.id设置transform: translate(0 0),但当我开始添加更多元素时,它们最终会移动到以前没有<path>元素的地方,从而使其始终工作。

我当前的代码如下。由于某些原因,CSS转换在这里不能正常工作,但它在其他地方工作(编辑:它在桌面上工作得很好,只是不在我的手机上)

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>

您可以通过应用多个翻译转换来实现这一点。

比方说,红色方块应该被放置在黑色方块的位置:

<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)取消正方形的原始x,y偏移量,并将该元素移动到svg的坐标原点。
第二个变换translate(10 10)将这个元素移动到黑色方块的位置。

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>

工作原理

  1. shuffleEls()通过getBBox()获取每个路径的位置,并将x和y坐标保存到数据属性
  2. 我们洗牌路径元素数组
  3. 每个路径从它的洗牌对应
  4. 继承位置

互换位置:

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

更新:包括以前的转换

如果一个<path>元素已经被转换(例如旋转),你可能想要保留它。

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>

可以在data属性后面添加一个matrix()
如果路径有不同的大小或宽高比,您还可以根据实际的边界框设置居中的x/y坐标。
这样,所有的洗牌元素都将被放置在相同的中心点周围。

let style = window.getComputedStyle(el);
let matrix = style.transform!='none' ? style.transform : '';
el.dataset.pos = [x+width/2, y+height/2, matrix].join("|");

我认为如果所有<path>元素具有相同的起点(因此,到0,0的距离相同),然后使用transform/translate来定位它们,则更容易跟踪位置。您可以使用元素变换矩阵来查找位置。

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>

最新更新