×

如何从DOM转换为SVG坐标并再次返回

作者:Terry2020.12.04来源:Web前端之家浏览:8725评论:0
关键词:svg

500.jpg

使用SVG相对简单-在您想要混合DOM和矢量交互之前。

SVG在viewBox属性中定义了自己的坐标系。例如:

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 600">

从处开始设置宽度为800单位,高度为600单位0,0。这些单位是用于绘图目的的任意度量,并且可以使用单位的分数。如果将此SVG放置在800600像素区域中,则每个SVG单元应直接映射到屏幕像素。

但是,矢量图像可以缩放到任意大小,尤其是在自适应设计中。你可能SVG缩小到400通过300,甚至延伸面目全非在10通过1000空间。如果要根据光标位置放置其他元素,则向该SVG添加更多元素将变得更加困难。

注意:有关SVG坐标的更多信息,请参见Sara Soueidan的视口,viewBox和preserveAspectRatio文章。

避免协调翻译

您也许可以避免完全在坐标系之间转换。

嵌入HTML页面(而不是图像或CSS背景)中的SVG成为DOM的一部分,并且可以通过与其他元素类似的方式进行操作。例如,以一个圆形的基本SVG为例:

<svg id="mysvg" xmlns="https://www.w3.org/2000/svg" viewBox="0 0 800 600" preserveAspectRatio="xMidYMid meet">
  <circle id="mycircle" cx="400" cy="300" r="50" />
<svg>

您可以对此应用CSS效果:

circle {
  stroke-width: 5;
  stroke: #f00;
  fill: #ff0;}
circle:hover {
  stroke: #090;
  fill: #fff;}

您还可以附加事件处理程序以修改属性:

const mycircle = document.getElementById('mycircle');
mycircle.addEventListener('click', (e) => {
  console.log('circle clicked - enlarging');
  mycircle.setAttribute('r', 60);
});

下面的示例将30个随机圆添加到SVG图像,在CSS中应用悬停效果,并在单击圆时使用JavaScript将半径增加10个单位。

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>将30个随机圆添加到SVG图像(Web前端之家https://jiangweishan.com/)</title>
<style>
    *, *:before, *:after {
  box-sizing: border-box;
  padding: 0;
  margin: 0;
}

html {
  height: 100%;
}

body {
  min-height: 100%;
  font-family: Lato, sans-serif;
  font-size: 100%;
  padding: 0;
  margin: 10px;
  color: #444;
  background-color: #fff;
  overflow: hidden;
}

svg {
  background: radial-gradient(ellipse at center, #fefefe 0%, #cbeeff 100%);
}

circle {
  stroke-width: 5;
  stroke: #f00;
  fill: #ff0;
}

circle:hover {
  stroke: #090;
  fill: #fff;
}
</style>
</head>
<body>

    <svg id="mysvg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 1600 800" preserveAspectRatio="xMidYMid meet"></svg>

<script>
var
  svg = document.getElementById('mysvg'),
  NS = svg.getAttribute('xmlns');
  
// add random circles
var c, i;
for (i = 0; i < 30; i++) {
  c = document.createElementNS(NS, 'circle');
  c.setAttributeNS(null, 'cx', Math.round(Math.random() * 1600));
  c.setAttributeNS(null, 'cy', Math.round(Math.random() * 800));
  c.setAttributeNS(null, 'r', 20 + Math.round(Math.random() * 30));
  svg.appendChild(c);
}

// click a circle
svg.addEventListener('click', function(e) {
  var t = e.target;
  if (t.nodeName != 'circle') return;
  t.setAttributeNS(null, 'r', 
    parseFloat(t.getAttributeNS(null, 'r')) + 10
  );
  console.log(t.getBoundingClientRect());
}, false);
</script>

</body>
</html>

SVG到DOM坐标转换

可能有必要在SVG元素的顶部覆盖DOM元素-例如,在世界地图上显示的活动国家/地区上的菜单或信息框。假设SVG嵌入到HTML中,则元素成为DOM的一部分,因此getBoundingClientRect()方法可用于提取位置和尺寸。(在上面的示例中打开控制台,以显示半径增加后单击的圆的新属性。)

Element.getBoundingClientRect()在所有浏览器中均受支持,并返回一个DOMrect具有以下像素尺寸属性的对象:

  • .x.left:元素左侧相对于视口原点的x坐标

  • .right:相对于视口原点的元素右侧的x坐标

  • .y.top:元素顶侧相对于视口原点的y坐标

  • .bottom:元素相对于视口原点的y坐标

  • .width:元素的宽度(在IE8及以下版本中不支持,但与.rightminus相同.left

  • .height:元素的高度(在IE8及以下版本中不支持,但与.bottom负号相同.top

所有坐标都相对于浏览器视口,因此将在滚动页面时发生变化。可以通过添加window.scrollX.leftwindow.scrollY来计算页面上的绝对位置.top

DOM到SVG坐标转换

这更具挑战性。假设您要viewBox在单击事件发生的位置上放置一个新的SVG元素。事件处理程序对象提供DOM.clientX.clientY像素坐标,但是必须将其转换为SVG单位。

很容易想到您可以通过应用乘法因子来计算SVG点的坐标。例如,如果将1000个单位宽度的SVG放置在500px宽度的容器中,则可以将任何DOMx坐标乘以2以获得SVG位置。它行不通!…

  • 不能保证SVG完全适合您的容器。

  • 如果元素尺寸发生变化(可能是响应用户调整浏览器大小的结果),则必须重新计算宽度和高度因子。

  • SVG或一个或多个元素可以在2D或3D空间中转换。

  • 即使您克服了这些障碍,它也永远不会像您预期的那样有效,并且经常会有误差。

幸运的是,SVG提供了自己的矩阵分解机制来转换坐标。第一步是使用createSVGPoint()方法在SVG上创建一个点,并传入屏幕/事件xy坐标:

const
  svg = document.getElementById('mysvg'),
  pt = svg.createSVGPoint();
  pt.x = 100;
  pt.y = 200;

然后,您可以应用从SVG.getScreenCTM()方法的逆函数创建的矩阵变换,该变换将SVG单位映射到屏幕坐标:

const svgP = pt.matrixTransform( svg.getScreenCTM().inverse() );

svgP现在具有.x.y属性,可提供SVG上的坐标viewBox

以下代码在SVG画布上单击的点处放置了一个圆圈:

// get SVG element and namespace
const
  svg = document.getElementById('mysvg'),
  NS = svg.getAttribute('xmlns');

// click event
svg.addEventListener('click', (e) => {

  const pt = svg.createSVGPoint();

  // pass event coordinates
  pt.x = e.clientX;
  pt.y = e.clientY;

  // transform to SVG coordinates
  const svgP = pt.matrixTransform( svg.getScreenCTM().inverse() );

  // add new SVG element
  const circle = document.createElementNS(NS, 'circle');
  circle.setAttribute('cx', svgP.x);
  circle.setAttribute('cy', svgP.y);
  circle.setAttribute('r', 10);

  svg.appendChild(circle);

});

注意:这些createElementNS()方法与标准DOMcreateElement()方法相同,除了它指定XML名称空间URI之外。换句话说,它作用于SVG文档而不是HTML。

转换为已转换的SVG坐标

还有进一步的复杂化。可以通过平移,缩放,旋转和/或倾斜来变换SVG或单个元素,这会影响所得的SVG坐标。例如,下<g>一层比标准SVG单位大4倍,因此坐标将是包含SVG的坐标的四分之一:

<g id="transformed" transform="scale(4)">
  <rect x="50" y="50" width="100" height="100" />
</g>

将得到的矩形显示为具有400在位置单元的宽度和高度200, 200

幸运的是,该.getScreenCTM()方法可以应用于任何元素以及SVG本身。结果矩阵考虑了所有转换,因此您可以创建一个简单的svgPoint()转换函数:

const
  svg = document.getElementById('mysvg'),
  transformed = svg.getElementById('transformed');

console.log( svgPoint(svg, 10, 10) ); // returns x, y
console.log( svgPoint(transformed, 10, 10) ); // = x/4, y/4

// translate page to SVG coordinate
function svgPoint(element, x, y) {

  const pt = svg.createSVGPoint();
  pt.x = x;
  pt.y = y;

  return pt.matrixTransform( element.getScreenCTM().inverse() );

}

以下演示适用于所有现代浏览器(如果将JavaScript转换为ES5,则将在IE11中使用!)。轻击/单击SVG时,将在光标处添加一个圆圈。

<g>单击转换的区域时也会发生这种情况,但是将该元素而不是SVG本身传递给svgPoint()函数以确保计算出正确的坐标:

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>轻击/单击SVG时,将在光标处添加一个圆圈(Web前端之家https://jiangweishan.com/)</title>
<style>
   *, *:before, *:after {
  box-sizing: border-box;
  padding: 0;
  margin: 0;
}

html {
  height: 100%;
}

body {
  height: 100%;
  font-family: Lato, helvetica, sans-serif;
  font-size: 100%;
  padding: 0;
  margin: 0;
  color: #333;
  background-color: #fff;
  overflow: hidden;
}

#mysvg {
  display: block;
  width: auto;
  height: 100%;
  margin: 0;
  background: radial-gradient(ellipse at center, #fefefe 0%, #cbeeff 100%);
  border: 5px solid #333;
  touch-action: none;
}

@media (max-aspect-ratio: 1/1) {

  #mysvg {
    width: 100%;
    height: auto;
  }

}

#mysvg rect {
  fill: #069;
}

#mysvg circle {
  stroke-width: 3;
  stroke: #f00;
  fill: #ff0;
}

#mysvg text {
  fill: #fff;
}

#output {
  position: fixed;
  display: grid;
  grid-template-columns: auto 3em 3em;
  gap: 0.2em;
  bottom: 10px;
  right: 10px;
  padding: 0.2em;
  background-color: rgba(255,255,255,0.9);
  border-radius: 5px;
  pointer-events: none;
}

#output strong, #output span {
  text-align: right;
  font-variant-numeric: tabular-nums;
}

h1 {
  font-size: 1.1em;
  grid-column-start: span 3;
}

#targetID {
  grid-column-start: span 2;
}
</style>
</head>
<body>

    <svg xmlns="http://www.w3.org/2000/svg" id="mysvg" viewBox="0 0 1000 1000" preserveAspectRatio="xMidYMid meet">

        <g id="transform1" transform="scale(4)">
          <rect x="50" y="50" width="100" height="100" rx="10" />
          <text x="100" y="100" text-anchor="middle" dominant-baseline="middle">scale(4)</text>
        </g>
        
        <g id="transform2" transform="scale(2) rotate(10 350 350) skewY(5)">
          <rect x="300" y="300" width="100" height="100" rx="10" />
          <text x="350" y="350" text-anchor="middle" dominant-baseline="middle">scale(2)</text>
        </g>
      
      </svg>
      
      <div id="output">
        <h1>DOM to SVG Co-ordinates</h1>
        <strong>DOM client X,Y:</strong> <span id="clientX"></span> <span id="clientY"></span>
        <strong>SVG X,Y:</strong> <span id="svgX"></span> <span id="svgY"></span>
        <strong>target X,Y:</strong> <span id="targetX"></span> <span id="targetY"></span>
        <strong>add target:</strong> <span id="targetID"></span>
        <strong>add X,Y:</strong> <span id="addX"></span> <span id="addY"></span>
      </div>

<script>
// initialize
const
  svg = document.getElementById('mysvg'),
  NS = svg.getAttribute('xmlns'),
  out = {};

'clientX,clientY,svgX,svgY,targetX,targetY,targetID,addX,addY'.split(',').map(s => {
  out[s] = { node: document.getElementById(s), value: '-' }
});

// events
svg.addEventListener('pointermove', getCoordinates);
svg.addEventListener('pointerdown', getCoordinates);
svg.addEventListener('pointerdown', createCircle);


// update co-ordinates
function getCoordinates(event) {

  // DOM co-ordinate
  out.clientX.value = event.clientX;
  out.clientY.value = event.clientY;

  // SVG co-ordinate
  const svgP = svgPoint(svg, out.clientX.value, out.clientY.value);
  out.svgX.value = svgP.x;
  out.svgY.value = svgP.y;

  // target co-ordinate
  const svgT = svgPoint(event.target, out.clientX.value, out.clientY.value);
  out.targetX.value = svgT.x;
  out.targetY.value = svgT.y;

  updateInfo();

};


// add a circle to the target
function createCircle(event) {

  // circle clicked?
  if (event.target.nodeName === 'circle') return;

  // add circle to containing element
  const
    target = event.target.closest('g') || event.target.ownerSVGElement || event.target,
    svgP = svgPoint(target, event.clientX, event.clientY),
    cX = Math.round(svgP.x),
    cY = Math.round(svgP.y),
    circle = document.createElementNS(NS, 'circle');

  circle.setAttribute('cx', cX);
  circle.setAttribute('cy', cY);
  circle.setAttribute('r', 30);

  target.appendChild(circle);

  // output information
  out.targetID.value = target.id || target.nodeName;
  out.addX.value = cX;
  out.addY.value = cY;
  updateInfo();

}


// translate page to SVG co-ordinate
function svgPoint(element, x, y) {

  var pt = svg.createSVGPoint();
  pt.x = x;
  pt.y = y;
  return pt.matrixTransform(element.getScreenCTM().inverse());

}


// output values
function updateInfo() {

  for (p in out) {
    out[p].node.textContent = isNaN(out[p].value) ? out[p].value : Math.round(out[p].value);
  }

}
</script>

</body>
</html>

理想情况下,最好避免DOM与SVG坐标之间的平移,但是,在这种情况下,请使用上述方法来确保该过程在所有页面尺寸上均可靠。

您的支持是我们创作的动力!
温馨提示:本文作者系Terry ,经Web前端之家编辑修改或补充,转载请注明出处和本文链接:
https://jiangweishan.com/article/svg20201204a1.html

网友评论文明上网理性发言 已有0人参与

发表评论: