在 three.js 绘制带洞的多边形是很常见且自然支持的需求,在 fabric 中 Polygon 是不支持 holes 的,那么该如何处理呢。我们可以通过扩展 fabric.Polygon,通过 canvas fill-rule 来实现绘制带洞的多边形。
走的弯路
最开始想通过 canvas globalCompositeOperation 设置为 XOR 的方式实现带洞的多边形是不对的,但其实是不行的,因为重叠区域颜色会在某些地方会有改变。
社区办法
首先假设我们有如下多边形需要绘制
// A-------------B A(0,0), B(100,0), C(100,100), D(0,100)
// | E | E(50,10), F(10,90), G(90,90)
// | / \ | H(50,90), I(50,100)
// | / \ |
// | / \ |
// | F---H---G |
// D------I------C
在 2D 图形中绘制带洞的多边形办法
- 覆盖绘制 over-painting
- 优势:简单好理解
- 劣势:如果背景发生改变,则不会生效。存在多余的绘制,在绘制较多图形时会有性能问题
- 欺骗法:通过拆分线段的方式,绘制为简单图形,比如上面的图形可以这么做:ABCI > HGEFH > IDA
- 避免了覆盖绘制导致的背景色生效问题
- 会导致额外的边,如果你需要给描边时问题就暴露出来了。同时由于精度问题,可能导致缝隙
- 环绕计数
- 利用 nonzero 的判断规则
- 优势:真正意义上的画一个带洞的形状
- 劣势:必须保证顺时针和逆时针规则
- evenodd 规则
- 优势:比计数规则更好理解
- 劣势:在某些情形,计数规则会有更好的结果
- 裁剪区域
- 利用裁剪规则
- 优势:裁剪区域可以裁剪非常复杂的形状
- 劣势:需要做很多的额外工作
推荐利用 evenodd 填充规则去解决这个问题
了解填充规则
fill-rule 填充规则
- nonzero:default
- 用于判断该点属于该形状的内部还是外部
- 从该点向任意方向的无限远处绘制射线,然后检测形状与射线相交的位置。从 0 开始统计,路径上每一条从左到右(顺时针)跨过射线的线段都会让结果加 1,每条从右向左(逆时针)跨过射线的线段都会让结果减 1。当统计结束后,如果结果为 0,则点在外部;如果结果不为 0,则点在内部。
- evenodd
- 用于判断该点属于该形状的内部还是外部
- 从该点向任意方向无限远处绘制射线,并统计这个形状所有的路径段中,与射线相交的路径段的数量。如果有奇数个路径段与射线相交,则点在内部;如果有偶数个,则点在外部。
fabric 实现
自行建立一个 PolygonPro 类,实现 fabric.Polygon 类,重写 render 方法的方式来实现,简单实现如下
import { fabric } from 'fabric';
/**
* 扩展 Polygon 类以支持带洞的形状
*/
export default class PolygonPro extends fabric.Polygon {
fillRule: 'evenodd' | 'nonzero';
holes: fabric.Point[][];
constructor(paths: fabric.Point[][], options?: fabric.IPolylineOptions) {
const [outer, ...holes] = paths;
super(outer, options);
this.fillRule = 'evenodd';
this.holes = holes;
}
holesRender(ctx: CanvasRenderingContext2D) {
const { x, y } = this.pathOffset;
this.holes.forEach((hole) => {
const len = hole.length;
ctx.moveTo(hole[0].x - x, hole[0].y - y);
for (let i = 0; i < len; i += 1) {
const point = hole[i];
ctx.lineTo(point.x - x, point.y - y);
}
ctx.closePath();
});
}
// eslint-disable-next-line no-underscore-dangle
_render(ctx: CanvasRenderingContext2D) {
// this function will extend from parent, but it's a private, so use any instead
if (!(this as any).commonRender(ctx)) {
return;
}
ctx.closePath();
this.holesRender(ctx);
// eslint-disable-next-line no-underscore-dangle
this._renderPaintInOrder(ctx);
}
}