使用SVG相对简单-在您想要混合DOM和矢量交互之前。
SVG在viewBox
属性中定义了自己的坐标系。例如:
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 600">
从处开始设置宽度为800单位,高度为600单位0,0
。这些单位是用于绘图目的的任意度量,并且可以使用单位的分数。如果将此SVG放置在800
按600
像素区域中,则每个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://www.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及以下版本中不支持,但与.right
minus相同.left
) -
.height
:元素的高度(在IE8及以下版本中不支持,但与.bottom
负号相同.top
)
所有坐标都相对于浏览器视口,因此将在滚动页面时发生变化。可以通过添加window.scrollX
到.left
和window.scrollY
来计算页面上的绝对位置.top
。
DOM到SVG坐标转换
这更具挑战性。假设您要viewBox
在单击事件发生的位置上放置一个新的SVG元素。事件处理程序对象提供DOM.clientX
和.clientY
像素坐标,但是必须将其转换为SVG单位。
很容易想到您可以通过应用乘法因子来计算SVG点的坐标。例如,如果将1000个单位宽度的SVG放置在500px宽度的容器中,则可以将任何DOMx
坐标乘以2以获得SVG位置。它行不通!…
-
不能保证SVG完全适合您的容器。
-
如果元素尺寸发生变化(可能是响应用户调整浏览器大小的结果),则必须重新计算宽度和高度因子。
-
SVG或一个或多个元素可以在2D或3D空间中转换。
-
即使您克服了这些障碍,它也永远不会像您预期的那样有效,并且经常会有误差。
幸运的是,SVG提供了自己的矩阵分解机制来转换坐标。第一步是使用createSVGPoint()
方法在SVG上创建一个点,并传入屏幕/事件x
和y
坐标:
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://www.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坐标之间的平移,但是,在这种情况下,请使用上述方法来确保该过程在所有页面尺寸上均可靠。