如何使用Javascript在SVG中绘制不可缩放的圆形

10

我正在使用SVG绘制线条的JavaScript地图。

我想添加一项功能,即可以搜索道路,并且如果找到道路,则在地图上出现一个圆圈。

我知道我可以在SVG中绘制一个圆圈,但我的问题是圆圈的大小不应根据缩放级别而改变。换句话说,圆圈必须始终具有相同的大小。 我的地图上的道路具有此功能,我所要做的就是添加

vector-effect="non-scaling-stroke"

对于线条属性来说...

一条线看起来像这样。

<line vector-effect="non-scaling-stroke" stroke-width="3" id = 'line1' x1 = '0' y1 = '0' x2 = '0' y2 = '0' style = 'stroke:rgb(255,215,0);'/> 

这个圆看起来像这样。

<circle id = "pointCircle" cx="0" cy="0" r="10" stroke="red" stroke-width="1" fill = "red"/>

是否有可能以某种方式将圆定义为“非缩放”的形式?


你怎么缩放?如果你要缩放整个地图,我解决这个问题的方法是通过按比例缩放对象的倒数来实现,即如果你将地图按比例缩放了s倍,则可以将圆形按比例缩放1/s倍。 - Felix Kling
我使用SVGPan进行缩放。我会尝试您的想法 :) http://code.google.com/p/svgpan/ - Prechtig
2
有点讽刺的是,在可缩放矢量图形中画一个不可缩放的圆形。 - Rune FS
2
@RuneFS 我想你大多数是在开玩笑,但也要考虑到常见的需要在3D场景上叠加2D“HUD”样式图形。一般来说,用户空间标签与它们可能注释的复杂交互内容有非常不同的需求。 - Phrogz
4个回答

7

我花了一些时间,但最终解决了数学问题。这个解决方案需要三个步骤:

  1. 在你的页面中包含this script(以及SVGPan.js脚本),例如:
    <script xlink:href="SVGPanUnscale.js"></script>
  2. 确定你不想缩放的项目(例如将它们放在一个特殊类或ID的组中,或者在每个元素上放置一个特定的类),然后告诉脚本如何找到这些项目,例如:
    unscaleEach("g.non-scaling > *, circle.non-scaling");
  3. 使用transform="translate(…,…)"来放置图表上的每个元素,而不是cx="…" cy="…"

只需完成这些步骤,使用SVGPan进行缩放和平移将不会影响标记元素的比例(或旋转、倾斜)。

演示:http://phrogz.net/svg/scale-independent-elements.svg

// Copyright 2012 © Gavin Kistner, !@phrogz.net
// License: http://phrogz.net/JS/_ReuseLicense.txt

// Undo the scaling to selected elements inside an SVGPan viewport
function unscaleEach(selector){
  if (!selector) selector = "g.non-scaling > *";
  window.addEventListener('mousewheel',     unzoom, false);
  window.addEventListener('DOMMouseScroll', unzoom, false);
  function unzoom(evt){
    // getRoot is a global function exposed by SVGPan
    var r = getRoot(evt.target.ownerDocument);
    [].forEach.call(r.querySelectorAll(selector), unscale);
  }
}

// Counteract all transforms applied above an element.
// Apply a translation to the element to have it remain at a local position
function unscale(el){
  var svg = el.ownerSVGElement;
  var xf = el.scaleIndependentXForm;
  if (!xf){
    // Keep a single transform matrix in the stack for fighting transformations
    // Be sure to apply this transform after existing transforms (translate)
    xf = el.scaleIndependentXForm = svg.createSVGTransform();
    el.transform.baseVal.appendItem(xf);
  }
  var m = svg.getTransformToElement(el.parentNode);
  m.e = m.f = 0; // Ignore (preserve) any translations done up to this point
  xf.setMatrix(m);
}

演示代码

<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
  <title>Scale-Independent Elements</title>
  <style>
    polyline { fill:none; stroke:#000; vector-effect:non-scaling-stroke; }
    circle, polygon { fill:#ff9; stroke:#f00; opacity:0.5 }
  </style>
  <g id="viewport" transform="translate(500,300)">
    <polyline points="-100,-50 50,75 100,50" />
    <g class="non-scaling">
      <circle  transform="translate(-100,-50)" r="10" />
      <polygon transform="translate(100,50)" points="0,-10 10,0 0,10 -10,0" />
    </g>
    <circle class="non-scaling" transform="translate(50,75)" r="10" />
  </g>
  <script xlink:href="SVGPan.js"></script>
  <script xlink:href="SVGPanUnscale.js"></script>
  <script>
    unscaleEach("g.non-scaling > *, circle.non-scaling");
  </script>
</svg>

1
这在Opera 22.0.1471.40中不起作用(虽然一个月前还可以...)或IE 10中也是如此。在Opera 12.17中运行良好。他们说,来做Web开发,只需要一个平台,他们说,可以在任何地方工作... - Eugene
1
我应该注意到,基于unscale的我的代码在Opera 12.17中运行良好,但演示本身不支持缩放。 - Eugene

3

如果你想要一种完全静态的方法来实现这个,你可以尝试将non-scaling-stroke与标记符(marker)结合使用,因为这些标记符(marker)相对于描边宽度进行定位。

换句话说,你可以将圆圈包装在

<svg width="500" height="500" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2000 2000">
    <marker id="Triangle"
      viewBox="0 0 10 10" refX="0" refY="5" 
      markerUnits="strokeWidth"
      markerWidth="4" markerHeight="3"
      orient="auto">
      <path d="M 0 0 L 10 5 L 0 10 z" />
    </marker>
        <path d="M 100 100 l 200 0" vector-effect="non-scaling-stroke"
        fill="none" stroke="black" stroke-width="10" 
        marker-end="url(#Triangle)"  />
        <path d="M 100 200 l 200 0" 
        fill="none" stroke="black" stroke-width="10" 
        marker-end="url(#Triangle)"  />
</svg>

同样的内容也可以在这里查看和调整。由于标记不在SVG Tiny 1.2中,而vector-effect也不在SVG 1.1中,因此svg规范并没有完全明确在这种情况下应该发生什么。我目前的想法是它可能会影响标记的大小,但似乎目前没有观察者这样做(尝试在支持vector-effect的观察者中使用,例如Opera或Chrome)。


你能举个例子展示一下代码是什么样子的吗? 比如说,如果我想要一个始终宽度为15像素的圆形。 - Prechtig

0

这里讨论了这个问题,还有这里

看起来现在的浏览器没有按预期执行,所以需要对<marker>内的内容应用缩放 (scale) 的反向变换,例如<marker>的使用者需要进行transform: translate(...) scaleX(0.2)操作,并伴随一个 transorm: scaleX(5) , 如果需要,也要考虑x/y/width/height/transform-origin值。


0

看起来在webkit上做了一些工作(可能与这个bug相关:320635),当像那样简单附加时,新的变换不会保留下来

transform.baseVal.appendItem

这似乎效果更好。甚至在IE 10中也可以工作。

编辑:修复了代码,以适应前面多个翻译转换的更一般情况,并可能在之后进行其他转换。所有翻译后的第一个矩阵变换必须保留为不缩放。

translate(1718.07 839.711) translate(0 0) matrix(0.287175 0 0 0.287175 0 0) rotate(45 100 100)

function unscale()
{
    var xf = this.ownerSVGElement.createSVGTransform();
    var m = this.ownerSVGElement.getTransformToElement(this.parentNode);
    m.e = m.f = 0; // Ignore (preserve) any translations done up to this point
    xf.setMatrix(m);

    // Keep a single transform matrix in the stack for fighting transformations
    // Be sure to apply this transform after existing transforms (translate)
    var SVG_TRANSFORM_MATRIX = 1;
    var SVG_TRANSFORM_TRANSLATE = 2;
    var baseVal = this.transform.baseVal;
    if(baseVal.numberOfItems == 0)
        baseVal.appendItem(xf);
    else
    {
        for(var i = 0; i < baseVal.numberOfItems; ++i)
        {
            if(baseVal.getItem(i).type == SVG_TRANSFORM_TRANSLATE && i == baseVal.numberOfItems - 1)
        {
                baseVal.appendItem(xf);
            }

            if(baseVal.getItem(i).type != SVG_TRANSFORM_TRANSLATE)
            {
                if(baseVal.getItem(i).type == SVG_TRANSFORM_MATRIX)
                    baseVal.replaceItem(xf, i);
                else
                    baseVal.insertItemBefore(xf, i);
                break;
            }
        }
    }
}

编辑2: Chrome由于某些原因删除了getTransformToElement函数,因此需要手动检索矩阵:

var m = this.parentNode.getScreenCTM().inverse().multiply(this.ownerSVGElement.getScreenCTM());

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接