Better

Ethan的博客,欢迎访问交流

fabric 绘制带洞的多边形

在 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);
  }
}

资料



留言