利用 SVG 中的对称性;误解或错误的渲染器实现?

Taking advantage of symmetry in SVG ; misunderstanding or buggy renderer-implementations?

我想在我的 SVG 设计中利用对称性,但大多数查看器(包括 Firefox 和 Chromium 浏览器)呈现间隙/线条,数字清楚地告诉我不应该有任何间隙/线条。使用缩放将这些设计渲染得更大可以减少那些错误渲染线条的数量和厚度。

我是不是误解了 svg 格式? 在数学上正确地实现 svg 渲染是否过于耗费资源? 或者没有人关心这种边缘情况?

一个更好地说明我在说什么的例子:

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" width="98" height="98" fill="red" fill-opacity="0.7">
  <g id="2">
    <g id="4">
      <path id="p" d="m49,35c-3,0 -4.575713,0.605785 -6.9934,1.888339L31.330807,18.397364 37,15C38,7.76 38,6 39,0.7 39.7,0 44,0 49,0Z"/>
      <use xlink:href="#p" transform="rotate(-60, 49, 49)"/>
      <use xlink:href="#p" transform="translate(98) scale(-1, 1) rotate(60, 49, 49)"/>
    </g>
    <use xlink:href="#4" transform="translate(98) scale(-1, 1)"/>
  </g>
  <use xlink:href="#2" transform="rotate(180, 49, 49)"/>
</svg>

我在这里找到: If two partially opaque shapes overlap, can I show only one shape where they overlap? 可以使用过滤器使重叠的形状在不透明度方面表现得好像不存在一样,但它仍然让我感到烦恼,因为需要这样的解决方法。它增加了复杂性、文件大小并降低了与未实现这些过滤器的查看器的兼容性。

将该解决方法应用于上述示例:

<svg width="98" height="98" fill-opacity=".7" fill="red" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
 <filter id="f">
  <feComponentTransfer>
   <feFuncA type="table" tableValues="0 .7 .7" />
  </feComponentTransfer>
 </filter>
 <g filter="url(#f)">
  <g id="2">
   <g id="4">
    <path id="p" d="m51 35c-3-0-6 0-10 3l-15-15 11-7c2-8 1-8 2-14 0.7-0.7 7-0.7 12-0.7"/>
    <use transform="rotate(-60 49 49)" xlink:href="#p"/>
    <use transform="translate(98) scale(-1 1) rotate(60 49 49)" xlink:href="#p"/>
   </g>
   <use transform="translate(98) scale(-1 1)" xlink:href="#4"/>
  </g>
  <use transform="rotate(180 49 49)" xlink:href="#2"/>
 </g>
</svg>

想一想任何类型的渲染引擎必须如何渲染您的 SVG。它遍历 SVG,一次绘制一个元素。

除非该形状是 正好 到达像素边界的直线或矩形,否则它需要平滑该形状的边缘。它使用一种称为 ant-aliasing 的技术来做到这一点。它绘制 semi-transparent 像素来近似仅部分覆盖像素的边缘。

例如,如果形状刚好覆盖半个像素,渲染器将以 50% 的 alpha 将颜色绘制到该像素中。

然后它继续移动并绘制下一个形状。即使该形状具有数学上一致的边界,您也可能不会得到完美的连接。

原因如下。

画出三个相邻像素,其中形状的边缘刚好穿过中心像素的一半。

                     +--------+--------+--------+
First shape drawn:   |  100%  |   50%  |    0%  |
                     +--------+--------+--------+

此处的百分比表示绘制到每个像素中的形状颜色量。左侧像素为 100%。中间像素的 50% 颜色 (alpha)。并且没有颜色绘制到正确的像素中。

现在想象第二个形状与第一个形状共享一个边框。您可能会想象发生以下情况。

                     +--------+--------+--------+
First shape drawn:   |  100%  |   50%  |    0%  |
                     +--------+--------+--------+
Second shape drawn:  |    0%  |   50%  |  100%  |
                     +--------+--------+--------+
Resulting image:     |  100%  |  100%  |  100%  |
                     +--------+--------+--------+

但事实并非如此。第一个形状已经渲染为像素。渲染器不记得它之前绘制的东西的形状。它只有之前渲染过的像素的颜色。

当它开始绘制中间像素时,在第一步或第二步中,它将 50% 的新颜色与已有的像素值混合。公式大致如下:

result = 0.5 * old_pixel_colour + 0.5 * new_pixel_colour

因此,例如,让我们以上面的像素百分比为例,假设我们在白色背景上绘制两个红色形状。

绘制第一个形状后,像素应如下所示。

  rgb(255, 0, 0)         rgb(255, 128, 128)        rgb(255, 255, 255)
[0 * bg + 1.0 * red]  [0.5 * bg + 0.5 * 50%_red]  [1.0 * bg + zero_red]

其中 bg 代表像素开始的白色背景颜色。 50%_red 表示抗锯齿用于表示 half-covered 像素的 50% 透明红色。

第二次通过后,像素将如下所示:

    rgb(255, 0, 0)            rgb(255, 192, 192)          rgb(255, 0, 0)
[1.0 * first + no_red]  [0.5 * first + 0.5 * 50%_red]  [0 * first + 1.0 * red]

其中first表示绘制第一个形状后像素点的颜色。我希望这是有道理的。

或者以颜色(红色)的百分比表示。

                     +--------+--------+--------+
First shape drawn:   |  100%  |   50%  |    0%  |
                     +--------+--------+--------+
Second shape drawn:  |    0%  |   50%  |  100%  |
                     +--------+--------+--------+
Resulting image:     |  100%  |   75%  |  100%  |
                     +--------+--------+--------+

所以我希望您能明白为什么这些边框像素最终会显示一条微弱的白线。这是因为您正在混合两层抗锯齿像素。它实际上是一条淡红色线。

理论上,渲染器可以分析一堆形状的像素覆盖率。但这在数学上非常复杂。它会大大减慢渲染过程。

Paul LeBeau 给出了 anti-aliasing 工作原理的一般解释。值得注意的是,您可以通过将 shape-rendering 表示属性设置为 crispEdges 来关闭此算法。渲染器将​​尝试绘制完全不透明或完全透明的每个像素:

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" width="98" height="98" fill="red" fill-opacity="0.7">
  <g id="2" shape-rendering="crispEdges">
    <g id="4">
      <path id="p" d="m49,35c-3,0 -4.575713,0.605785 -6.9934,1.888339L31.330807,18.397364 37,15C38,7.76 38,6 39,0.7 39.7,0 44,0 49,0Z"/>
      <use xlink:href="#p" transform="rotate(-60, 49, 49)"/>
      <use xlink:href="#p" transform="translate(98) scale(-1, 1) rotate(60, 49, 49)"/>
    </g>
    <use xlink:href="#4" transform="translate(98) scale(-1, 1)"/>
  </g>
  <use xlink:href="#2" transform="rotate(180, 49, 49)"/>
</svg>

这可能有用,也可能无用,具体取决于用例。边缘会显得“参差不齐”,但这可能是可以接受的。

但这并不是问题的结束。在理想的环境中,渲染器总是会决定位于“接缝”顶部的像素属于一侧或另一侧。但是在离散数字的真实世界中,它可能决定像素不属于任何一个,并保持透明。

特别是如果您对形状应用了一个或多个变换,应用程序可能会决定它必须多次舍入数字以计算最终渲染,并且舍入误差可能会累加。 (根据我的经验,Chrome 比其他浏览器更容易出现这种效果。)

最后,specification 使渲染器在“可能”(或不)使用该属性值时有很大的回旋余地:

To achieve crisp edges, the user agent might turn off anti-aliasing for all lines and curves or possibly just for straight lines which are close to vertical or horizontal. Also, the user agent might adjust line positions and line widths to align edges with device pixels.

不幸的是,结论是每个渲染器都使用自己的优化策略,您无法保证最终结果。