跳到主要内容

填充与描边的顺序

Canvas 画一个有填充也有描边的图形时,两者必有先后。 Konva 用 fillAfterStrokeEnabled 控制这个顺序。

new Konva.Rect({
fill: 'tomato',
stroke: 'rgba(0, 0, 255, 0.5)',
strokeWidth: 20,
fillAfterStrokeEnabled: true, // 默认 false
});

默认值是 false,也就是先填充、后描边,描边压在填充之上。

什么时候看得出区别​

这是关键的一节,也解释了为什么大多数人从没注意过这个属性。

Canvas 的描边是以路径为中心线的:strokeWidth: 20 意味着 路径两侧各占 10px,其中向内的 10px 会盖住填充区域。

两种顺序的差别只在这条重叠带上,所以:

描边半透明时有区别。 先填充后描边,描边压在填充上, 两层颜色叠加,看到的是"蓝色混橙色"; 先描边后填充,填充盖住了描边向内的那一半, 只剩外侧一半与背景混色。

描边很粗时有区别。 重叠带越宽,差异越明显。

描边不透明且很细时完全没有区别。 不透明意味着叠加与否结果一样, 细意味着重叠带只有一两个像素。这两个条件覆盖了绝大多数实际用法—— 所以这个属性大部分时候确实无关紧要。

上面的演示里,上排是半透明的 20px 粗边(差别一眼可见), 下排是不透明的 2px 细边(两边完全相同)。

实测把探针放在重叠带上(距离边界向内 5px),采到的颜色是:

描边fillAfterStrokeEnabled: falsefillAfterStrokeEnabled: true
rgba(0,0,255,0.5),20px127,49,163(蓝叠在橙上)255,99,71(纯填充色)
#003366,2px255,99,71255,99,71(完全相同)

第一行说明重叠带确实被描边染了色;第二行说明细的不透明描边下 两种顺序采到的是同一个值——这不是"差别很小",是没有差别。

常见问题​

为什么我的半透明描边看起来比设定的深?​

因为默认顺序下描边压在填充上,描边向内的那一半与填充色叠加, 向外的一半与背景叠加——同一条描边呈现出两种颜色, 内侧看起来更深。

想让整条描边颜色均匀,设 fillAfterStrokeEnabled: true, 填充会盖住内侧那一半,剩下的外侧部分只与背景混色。

粗描边让图形看起来变小了?​

也是中心线的缘故:描边向内吃掉了 strokeWidth / 2 的填充区域。 一个 100×100、描边 20px 的矩形,可见的填充区只有 80×80。

Canvas 没有 CSS 那样的 box-sizing,也没有"外描边"选项。 要让描边完全在外侧,三个办法:

  1. 把图形放大一圈:尺寸加上 strokeWidth,位置减去 strokeWidth / 2。
  2. 叠两个图形:底下一个只有描边、尺寸大一圈,上面一个只有填充。
  3. 用 sceneFunc 自己画:完全控制路径和绘制顺序, 见自定义图形。

第一种最简单,但图形是 Konva.Path 或复杂形状时不好算; 第二种通用但节点数翻倍。

这个属性影响性能吗?​

不影响。两种顺序都是 fill() 和 stroke() 各调用一次, 只是先后不同。

真正影响性能的是描边本身——尤其是配合 shadowBlur 或非整数的 strokeWidth 时,见优化描边。

缩放时描边跟着变粗,和这个有关吗?​

没关系,那是另一个属性:strokeScaleEnabled。

shape.strokeScaleEnabled(false); // 描边宽度不随 scale 变化

两者会叠加影响视觉效果——放大之后描边变粗,重叠带随之变宽, 原本看不出差别的顺序问题可能就显现了。 见忽略描边。

与其他方案的取舍​

fillAfterStrokeEnabled 只解决"两层谁在上面"这一个问题。 需求稍微复杂一点,它就不够了。

只要描边颜色均匀 → fillAfterStrokeEnabled: true, 一个属性搞定,零成本。

要真正的外描边(描边完全在图形外,不侵占填充) → 这个属性做不到,因为它改变的是绘制顺序不是路径几何。 用上面「粗描边」那一节的三个办法。

要内外双色描边(像文字描边那样内白外黑) → 必须叠多个图形,或者在 sceneFunc 里连续 stroke() 多次、 每次改 lineWidth 和 strokeStyle。后者节点数不增加, 但要自己处理命中区域。

只是想要一个边框效果 → 如果图形是矩形, 考虑用两个 Konva.Rect 而不是描边—— 可以分别控制圆角、可以做渐变边框,比调描边参数灵活得多, 代价是多一个节点。

文字描边 → Konva.Text 同样支持 fillAfterStrokeEnabled, 而且这是它最有用的场景:文字笔画细,描边压在填充上会明显"吃掉"笔画, 让字看起来更瘦甚至糊成一团。给描边文字设 fillAfterStrokeEnabled: true 通常能明显改善可读性。