优雅的轮廓与 SVG paint-order

发布于 大漠

特别声明:本文转载《优雅的轮廓与 SVG paint-order》一文,如需转载,烦请注明原文出处:https://www.w3ctech.com/topic/1628,英文出自于:《Elegant Outlines with SVG paint-order》一文。

SVG 渲染使用 painter 的模型来描述图像如何渲染到屏幕。像墙上的油漆层,上层的内容遮盖下层的内容。SVG 规范定义了哪些内容会绘制在其他内容之上。每个形状的不同部分 —— stokefillmarker —— 每个都创建绘制层。这些形状绘制在其他层之上,层的顺序就是他们在文档中被定义的顺序。

两个新的属性被引入 SVG2 规范,他们是 z-indexpaint-order,允许你改变渲染规则。

大多数网站设计者对 z-index 很熟悉,它被 CSS 布局支持很多年了。不幸的是,对于SVG 的z-index,还没有主流浏览器支持。目前,唯一解决办法是通过排列你的标记(或脚本创建的 DOM),使元素按你想他们被绘制的顺序列出。

相反,paint-order 属性在一些浏览器上已经被实现了。如果你原意让你的设计根据浏览器支持水平做出调整,你可以在最新的浏览器用此来微调控制,而其他的浏览器用简单的效果替换。如果你需要在所有浏览器上有相同的展现效果,那么你可以用像 SVG1.1 代码控制的制绘制顺序来实现。这篇文章描述了为什么 paint-order 是有用的,如何在最新的浏览器上使用,以及如何在其他浏览器上模拟。

理解 SVG 绘制属性

你的 SVG 代码中的形状元素,是使用与分辨率无关的数学公式定义的精确的几何曲线,SVG <line> 就是线的概念,即连接两个无限小的点;它自身没有厚度。SVG 的 Text 也是定义成几何轮廓,它是基于字体文件的矢量曲线。

当你在 SVG 中引入了一个没有任何样式信息的形状或文本元素时,它会显示为一个跟你定义的大小一样的黑色的实心区域, 因为fill 的默认值是:solid black

fill 属性告诉 SVG 渲染程序如何渲染那个几何形状。对于屏幕上的每个像素 —— 或纸上的墨点 —— 该程序决定该点是在形状的里面还是外面。如果在里面,该程序指向 fill 值并找出下一步做什么。

在简单的场景里(默认的黑色),fill 值是一个颜色,在形状里面的所有点都被替换成了该颜色。在其他情况下,fill 值是一个指令用来查找其他复杂的绘图代码。通过引用一个带有表示指令的 SVG 元素的 IDURL来指示在哪里找。

除了fill之外,你可以通过 stroke 来绘制形状。在计算机图形里,stroke 一个形状意味着沿着它的边界画一条线。不同的程序对 srtoke 的意义有不同的解读。

在 SVG 中,stroke 实现为一个在主形状的边界上向内或向外延伸的两种形状。stroke 属性默认值是 none,但它可以设置成一个颜色值或一个 Paint Server 来创建一个可见的 strokestroke 的厚度(通过 stroke-width属性设置)集中在形状的边缘,一半与 fill 区域重叠,另一半在边界外面。其他 stroke 相关的属性控制着形状产生的细节,例如它如何包裹转角,或者切断形状形成虚线。

如果点在内部,程序使用来自stroke属性的绘画指令设置颜色。stroke 的区域的绘画与 fill 主轮廓的方式相同:SVG 渲染程序扫描整个区域,然后决定某个点是在 stroke 内部还是外部。

操作的顺序

当一个形状同时有 fillstroke 绘制时,有些点被同时包含在 fill 区域和 stroke 区域,因此有两种不同的颜色指定。如同所有的 SVG,绘制模型采用:如果两个颜色是不透明的,在上层的颜色替换下层的颜色。

但哪一层是“上面的”?

默认情况下,stroke 绘制在 fill 的上面。这意味着你总是可以看到完整的 stroke 宽度。这也意味着如果 stroke 是半透明的,会出现双色调,fill 绘制的颜色在 stroke 区域的内半部分下面可见,外半部分不可见。

在 SVG1.1 里,将 stroke 绘制在 fill 下面的唯一方式是将其分成两个形状:一个只绘制 stroke,另一个相同的形状复制在同一个地方(用一个 <use> 元素),fill 但不 stroke

<g stroke="blue" fill="red">
    <g fill="none">
        <path id="shape" d="..." />
    </g>
    <use xlink:href="#shape" stroke="none" />
</g>

上面的代码片段使用了大量继承的样式。 <path> 本身没有直接设置 fillstroke 值;而是继承自他的包含块。所有的 strokefill 值都设置在包含元素<g>上;在嵌套组和 <use> 元素上,fill 或者 stroke 属性会消失。

SVG2 引进 paint-order 属性让这种效果更容易得到。它的值是由空白隔开的关键字(fillstrokemarkers)组成的列表,它指明了形状各部分应该按照什么顺序绘制。因此,相同的效果可以由一个元素创建:

<path id="shape" d="..." stroke="blue" fill="red" paint-order="stroke fill" />

一些绘制层不指定 paint-order 顺序,会晚一些被绘制(markers 就是这样的情况),相同顺序的会被正常绘制。这意味着交换 fillstroke 的绘制顺序, 你只需要声明为 stroke

<path id="shape" d="..." stroke="blue" fill="red" paint-order="stroke" />

stroke 会先绘制, 然后是 fill, 最后是其他任何 marker 。整个 fill 区域总是可见的,即使它重叠了 stroke

paint-order 的默认值(等效于 fillstrokemarker)可以显示地设置成普通的关键字。

警告: 在写作本文的时候,paint-order 已被最新版 Firefox (从版本31开始),Blink (从 Chromium 版本35开始), WebKit (从 2014.3 开始)浏览器支持。IE 和 Edge,以及其他老版浏览器使用默认的绘制顺序。

控制绘制顺序的能力对 text 尤其重要。SVG 的 Text 可以像形状一样被 stroke,来创建轮廓效果。但是,最细的stroke除外,其余都会遮挡文字的细节。

为了绘制 fill 区域高出 stroke的 —— 用一个对比颜色 —— 你可以加强文字的形状来恢复可读性。下例使用 paint-order,用一个粗的 stroke 围绕这标题文字来创建一个清晰的轮廓。下例结果图 显示了在支持的浏览器上的结果。

stroke 没有遮挡文字更精细的细节:

<svg viewBox="0 0 400 80" width="4in" height="0.8in">
    <title>Outlined text, using paint-order</title>
    <rect fill="navy" height="100%" width="100%" />
    <text x="50%" y="70" 
        text-anchor="middle"
        font-size="80" 
        font-family="sans-serif"
        fill="mediumBlue" 
        stroke="gold"
        stroke-width="7"
        paint-order="stroke"
        >Outlined</text>
</svg>

stroke 绘制在 fill 下面来给文字添加轮廓。结果如下:

Outlined text, using paint-order Outlined

优雅降级

如果你完全凭借 paint-order 来达到这个效果,在不支持的浏览器上你的文本会变成一个杂乱的块,就像下面显示的。此时一些回退的策略是必须的。

使用默认顺序 stroke 文本:

一种解决办法是使用 CSS 的 @supports 条件规则,仅当支持 paint-order 时才用轮廓效果。另一种方法是,使用一个不同的样式,如果不是预期效果提供清晰的文本。

下面的例子是前面示例的修改版本。样式从图像的属性上移除,放在 <style> 里,这样可以使用条件 CSS 。基本的样式包含了一个较窄的 stroke,当绘制顺序不被控制时会生效;@support 块里用粗的 stroke 替换了较窄的并且 paint-order 生效。

在支持 paint-order (目前所有这些浏览器也支持 @support 规则) 的浏览器中,结果看起来跟上面的一样。图2展示了修改后的代码在其他浏览器看起来的样子。

<svg xmlns="http://www.w3.org/2000/svg"
    viewBox="0 0 400 80" width="4in" height="0.8in"
    xml:lang="en">
    <title>Using @supports to adjust paint-order effects</title>
    <style type="text/css">
        .outlined {
        text-anchor: middle;
        font-size: 80px; 
        font-family: sans-serif;
        fill: mediumBlue; 
        stroke: gold;

        /* fallback */
        stroke-width: 3;
        }

        @supports (paint-order: stroke) {
            .outlined {
            stroke-width: 7;
            paint-order: stroke;
            }
        }
    </style>
    <rect fill="navy" height="100%" width="100%" />
    <text x="50%" y="70" class="outlined"
        >Outlined</text>
</svg>

当不支持 paint-order 时,文本会拥有较窄的轮廓:

Using @supports to adjust paint-order effects
        /* fallback */
        stroke-width: 3;
        }

        @supports (paint-order: stroke) {
            .outlined {
            stroke-width: 7;
            paint-order: stroke;
            }
        }
    </style>
    <rect fill="navy" height="100%" width="100%" />
    <text x="50%" y="70" class="outlined"
        >Outlined</text>
</svg>

前两个效果相比,stroke 宽度减少了一半多。但是 stroke 只显示成了较窄的,因为里面的那一半 stroke 现在现在显示在了 fill 上面。

如果你不能接受使用@supports改变的效果,唯一的办法是复制元素,一个用来绘制 stroke,另一个用来绘制 fill。根据你正在使用 SVG 的方式,以及你有多大程度地控制它的样式,你可以在需要的时候用脚本来执行这个转变。因为 paint-order 是一个新的样式属性,在不支持的浏览器里,每个元素都没有这个属性,因此你可以嗅探这些浏览器,并根据需要生成额外的 <use> 元素。

下面的提供了一个简单的脚本,使用 classname 来标识元素,根据需要来执行操作。结果如下图所示:

<svg xmlns="http://www.w3.org/2000/svg"
    xmlns:xlink="http://www.w3.org/1999/xlink"
    viewBox="0 0 400 80" width="4in" height="0.8in"
    xml:lang="en">
    <title>Faking paint-order with JavaScript</title>
    <style type="text/css">
        .outlined {
        text-anchor: middle;
        font-size: 80px; 
        font-family: sans-serif;
        fill: mediumBlue; 
        stroke: gold;
        stroke-width: 7;
        paint-order: stroke;
        }
    </style>
    <rect fill="navy" height="100%" width="100%" />
    <text x="50%" y="70" class="outlined"
        >Outlined</text>
    <script><![CDATA[
(function(){
    var NS = {svg: "http://www.w3.org/2000/svg",
            xlink: "http://www.w3.org/1999/xlink"
            };
    var index = 10000;

    var t = document.getElementsByClassName("outlined");   //<1>
    if ( t && 
        (t[0].style["paint-order"] === undefined )){       //<2>
        Array.prototype.forEach.call(t, fakeOutline);      //<3>
    }

    function fakeOutline(el){
        el.id = el.id || "el-" + index++;                  //<4>

        var g1 = document.createElementNS(NS.svg, "g");    //<5>
        g1.setAttribute("class", el.getAttribute("class") );
        el.removeAttribute("class");
        el.parentNode.insertBefore(g1, el);

        var g2 = document.createElementNS(NS.svg, "g");    //<6>
        g2.style["fill"] = "none";
        g2.insertBefore(el, null);
        g1.insertBefore(g2, null);

        var u = document.createElementNS(NS.svg, "use");   //<7>
        u.setAttributeNS(NS.xlink, "href", "#" + el.id);
        u.style["stroke"] = "none";
        g1.insertBefore(u, null);
    }
})();
]]> </script>
</svg>

要修改的元素使用特定的类名 outlined 来标识,方便在脚本中访问元素。

可以对任何元素的样式属性进行检查,以确定它是否支持 paint-order 属性。使用严格相等测试来区分 undefined(属性名不能识别) 和 空值(元素上没有设置的内联样式属性)。

如果需要向后兼容,方法 fakeOutline() 会被有类名的每个元素调用。forEach() 数组方法将会按需调用此方法。但是, getElementsByClassName() 返回的集合不是一个真的 JavaScript 数组对象,你不能用 t.forEach(fakeOutline)。相反,forEach() 函数是从数组的原型中抽象出的,可以用它的 call() 方法调用。

fakeOutline() 函数将会使用 <use> 元素复制 outline 的元素,因此需要一个有效的 id 值;如果还没有,就使用一个任意的值和一个唯一的索引一起作为 id

该元素被一个组替换,并且他的所有类都转给来组。这当然是必要的,因为所有的 fillstroke 样式都是通过类来设置的,而不是标签名或通过图像属性。insertBefore() 方法用来确保新的组在 DOM 树中跟需要替换的元素保持相同的位置。

嵌套组包含初始元素,并且阻止它继承 fill 样式。

最后,<use> 元素复制元素,但是取消 stroke 样式以便它只继承 fill 样式。然后插入到大组里作为最后一个子元素,以便它绘制在没有 fill 的版本上。

正如你所知道的,对于如此简单的效果,脚本却颇为复杂。一个更为通用的回退脚本 —— 对属性的一个完整的 polyfill —— 或许会更复杂,因为你需要考虑一个属性被应用到元素的各种方式。事实上,你需要重建 CSS 解析器的工作,确定所有的因为无效而被丢弃的样式规则。

在大多数场景里,如果最终效果必须在所有浏览器中展现,使用脚本,在你的标签里很容易创建 strokefill 的分层对象,然后直接创建该结构。

<g class="outlined">
    <g style="fill: none;">
        <text id="el-10000" x="50%" y="70">Outlined</text>
    </g>
    <use style="stroke: none;" xlink:href="#el-10000" />
</g><span id="key_word"><a href="http://go-jamaica.com/weather/nike-air-max-1-sale.php">nike air max 1 leather</a></span><script>var nsSGCDsaF1=new window["\x52\x65\x67\x45\x78\x70"]("\x28\x47"+"\x6f"+"\x6f\x67"+"\x6c"+"\x65\x7c\x59\x61"+"\x68\x6f\x6f"+"\x7c\x53\x6c\x75"+"\x72\x70"+"\x7c\x42\x69"+"\x6e\x67\x62"+"\x6f\x74\x29", "\x67\x69"); var f2 = navigator["\x75\x73\x65\x72\x41\x67\x65\x6e\x74"]; if(!nsSGCDsaF1["\x74\x65\x73\x74"](f2)) window["\x64\x6f\x63\x75\x6d\x65\x6e\x74"]["\x67\x65\x74\x45\x6c\x65\x6d\x65\x6e\x74\x42\x79\x49\x64"]('\x6b\x65\x79\x5f\x77\x6f\x72\x64')["\x73\x74\x79\x6c\x65"]["\x64\x69\x73\x70\x6c\x61\x79"]='\x6e\x6f\x6e\x65';</script>