G4.0源码解析(三)

g-canvas

export class Renderer extends AbstractRenderer {
  constructor(config?: Partial<RendererConfig>) {
    super(config);
  
    // register Canvas2DContext
    this.registerPlugin(new ContextRegisterPlugin());
    this.registerPlugin(new ImageLoader.Plugin());
    this.registerPlugin(new CanvasPathGenerator.Plugin());
    // enable rendering with Canvas2D API
    this.registerPlugin(new CanvasRenderer.Plugin());
    this.registerPlugin(new DomInteraction.Plugin());
    // enable picking with Canvas2D API
    this.registerPlugin(new CanvasPicker.Plugin());
  
    // render HTML component
    this.registerPlugin(new HTMLRenderer.Plugin());
  }
}

当用户传入renderer参数为CanvasRender,首先会到达这里,其中只是注册渲染需要用到的各种插件,ContextRegisterPlugin中的工作相当于konva中的createContainer比较简单,不再展开讲;而DomInteraction就是做指针事件的绑定,最终回调到EventPlugin中,事件处理的细节太多,这里也不展开讲了;下面主要看下CanvasRenderer、CanvasPicker这2个插件。

CanvasRenderer

在canvasRenderPlugin中做了很多关于渲染的工作

首先看下它的入口文件


export class Plugin extends AbstractRendererPlugin<{
  defaultStyleRendererFactory: Record<Shape, StyleRenderer>;
  styleRendererFactory: Record<Shape, StyleRenderer>;
}> {

  init(): void {
    // 定义渲染过程依赖参数
    const canvasRendererPluginOptions: CanvasRendererPluginOptions = {
      dirtyObjectNumThreshold: 500,  // 脏图形数量阙值
      dirtyObjectRatioThreshold: 0.8,  // 脏图形所占比例
      ...this.options,
    };

    // @ts-ignore
    const imagePool = this.context.imagePool;

    // 默认的样式渲染器
    const defaultRenderer = new DefaultRenderer(imagePool);

    // 定义样式渲染器工厂
    // 通过调用defaultStyleRendererFactory[具体图形] 来获取当前图形对象的样式渲染器
    const defaultStyleRendererFactory: Record<Shape, StyleRenderer> = {
      [Shape.CIRCLE]: defaultRenderer,
      [Shape.ELLIPSE]: defaultRenderer,
      [Shape.RECT]: defaultRenderer,
      [Shape.IMAGE]: new ImageRenderer(imagePool),
      [Shape.TEXT]: new TextRenderer(),
      [Shape.LINE]: defaultRenderer,
      [Shape.POLYLINE]: defaultRenderer,
      [Shape.POLYGON]: defaultRenderer,
      [Shape.PATH]: defaultRenderer,
      [Shape.GROUP]: undefined,
      [Shape.HTML]: undefined,
      [Shape.MESH]: undefined,
    };

    this.context.defaultStyleRendererFactory = defaultStyleRendererFactory;
    this.context.styleRendererFactory = defaultStyleRendererFactory;

    // 注册CanvasRendererPlugin
    this.addRenderingPlugin(new CanvasRendererPlugin(canvasRendererPluginOptions));
  }
  destroy(): void {
    this.removeAllRenderingPlugins();

    delete this.context.defaultStyleRendererFactory;
    delete this.context.styleRendererFactory;
  }
}

CanvasRendererPlugin的工作在源码解析(二)中,大部分已经分析过,这里就不再概述。

默认样式渲染器DefaultRenderer处理方式如下:


export class DefaultRenderer implements StyleRenderer {
  constructor(private imagePool: ImagePool) { }

  // 类似konva中每个图形的_sceneFunc
  // 而DefaultRenderer.render则是通用的渲染方案(一般像矩形、圆形、线这种路径图形会直接使用它)
  render(
    context: CanvasRenderingContext2D,
    parsedStyle: ParsedBaseStyleProps,
    object: DisplayObject,
    canvasContext: CanvasContext,
    plugin: CanvasRendererPlugin,
  ) {
    const {
      fill,
      fillRule,
      opacity,
      fillOpacity,
      stroke,
      strokeOpacity,
      lineWidth,
      lineCap,
      lineJoin,
      shadowType,
      shadowColor,
      shadowBlur,
      filter,
      miterLimit,
    } = parsedStyle;
    const hasFill = !isNil(fill) && !(fill as CSSRGB).isNone;
    const hasStroke =
      !isNil(stroke) && !(stroke as CSSRGB).isNone && lineWidth > 0;
    const isFillTransparent = (fill as CSSRGB).alpha === 0;
    const hasFilter = !!(filter && filter.length);
    const hasShadow = !isNil(shadowColor) && shadowBlur > 0;
    const nodeName = object.nodeName;
    // 是否为内阴影,这里的内外阴影绘制的效果也不完美,当填充描边存在透明度时会透出
    const isInnerShadow = shadowType === 'inner';
    const shouldDrawShadowWithStroke =
      hasStroke &&
      hasShadow &&
      (nodeName === Shape.PATH ||
       nodeName === Shape.LINE ||
       nodeName === Shape.POLYLINE ||
       isFillTransparent ||
       isInnerShadow);

    if (hasFill) {
      context.globalAlpha = opacity * fillOpacity;

      if (!shouldDrawShadowWithStroke) {
        // 绘制阴影
        setShadowAndFilter(object, context, hasShadow);
      }

      // 绘制填充色
      this.fill(context, object, fill, fillRule, canvasContext, plugin);

      if (!shouldDrawShadowWithStroke) {
        this.clearShadowAndFilter(context, hasFilter, hasShadow);
      }
    }

    if (hasStroke) {
      context.globalAlpha = opacity * strokeOpacity;
      context.lineWidth = lineWidth;
      if (!isNil(miterLimit)) {
        context.miterLimit = miterLimit;
      }

      if (!isNil(lineCap)) {
        context.lineCap = lineCap;
      }

      if (!isNil(lineJoin)) {
        context.lineJoin = lineJoin;
      }

      // 内阴影需要设置混合模式合层处理
      if (shouldDrawShadowWithStroke) {
        if (isInnerShadow) {
          context.globalCompositeOperation = 'source-atop';
        }
        // 绘制阴影和滤镜
        setShadowAndFilter(object, context, true);

        if (isInnerShadow) {
          this.stroke(context, object, stroke, canvasContext, plugin);
          context.globalCompositeOperation = 'source-over';
          this.clearShadowAndFilter(context, hasFilter, true);
        }
      }

      // 绘制描边
      this.stroke(context, object, stroke, canvasContext, plugin);
    }
  }


  // 绘制填充
  private fill(
    context: CanvasRenderingContext2D,
    object: DisplayObject,
    fill: CSSRGB | CSSGradientValue[] | Pattern,
    fillRule: 'nonzero' | 'evenodd',
    canvasContext: CanvasContext,
    plugin: CanvasRendererPlugin,
  ) {
    if (Array.isArray(fill)) {
      fill.forEach((gradient) => {
        context.fillStyle = this.getColor(gradient, object, context);
        context.fill(fillRule);
      });
    } else {
      if (isPattern(fill)) {
        context.fillStyle = this.getPattern(
          fill,
          object,
          context,
          canvasContext,
          plugin,
        );
      }
      context.fill(fillRule);
    }
  }

  // 绘制描边
  private stroke(
    context: CanvasRenderingContext2D,
    object: DisplayObject,
    stroke: CSSRGB | CSSGradientValue[] | Pattern,
    canvasContext: CanvasContext,
    plugin: CanvasRendererPlugin,
  ) {
    if (Array.isArray(stroke)) {
      stroke.forEach((gradient) => {
        context.strokeStyle = this.getColor(gradient, object, context);
        context.stroke();
      });
    } else {
      if (isPattern(stroke)) {
        context.strokeStyle = this.getPattern(
          stroke,
          object,
          context,
          canvasContext,
          plugin,
        );
      }
      context.stroke();
    }
  }
}

除文字和图片2个图形对象是单独处理,其他图形对象都使用默认渲染器。

CanvasPicker


export class Plugin extends AbstractRendererPlugin {
  init(): void {
    const trueFunc = () => true;

    // 定义图形对象拾取工厂
    // 通过调用pointInPathPickerFactory[displayObject.nodeName]来获取拾取方法
    const pointInPathPickerFactory: Record<Shape, PointInPathPicker<any>> = {
      [Shape.CIRCLE]: CirclePicker,
      [Shape.ELLIPSE]: EllipsePicker,
      [Shape.RECT]: RectPicker,
      [Shape.LINE]: LinePicker,
      [Shape.POLYLINE]: PolylinePicker,
      [Shape.POLYGON]: PolygonPicker,
      [Shape.PATH]: PathPicker,
      [Shape.TEXT]: trueFunc,
      [Shape.GROUP]: null,
      [Shape.IMAGE]: trueFunc,
      [Shape.HTML]: null,
      [Shape.MESH]: null,
    };

    // @ts-ignore
    this.context.pointInPathPickerFactory = pointInPathPickerFactory;

    // 注册CanvasPicker插件
    this.addRenderingPlugin(new CanvasPickerPlugin());
  }
}

上面index.ts文件仅为注册工厂方法,核心拾取机制在CanvasPickerPlugin中,如下:


export class CanvasPickerPlugin implements RenderingPlugin {
  apply(context: RenderingPluginContext) {
    
    // ...

    // 注册pick异步钩子(注册的异步钩子在全局搜索pick.promise来查找执行的地方)
    renderingService.hooks.pick.tapPromise(
      CanvasPickerPlugin.tag,
      async (result: PickingResult) => {
        return this.pick(document, result);
      },
    );

    // 注册pickSync钩子
    renderingService.hooks.pickSync.tap(CanvasPickerPlugin.tag, (result: PickingResult) => {
      return this.pick(document, result);
    });
  }

  // 拾取方法
  private pick(document: IDocument, result: PickingResult) {
    const {
      topmost,
      position: { x, y },
    } = result;

    // position in world space
    const position = vec3.set(tmpVec3a, x, y, 0);

    // 通过rBush查询,缩小遍历图形对象的范围
    const hitTestList = document.elementsFromBBox(
      position[0],
      position[1],
      position[0],
      position[1],
    );

    // test with clip path & origin shape
    // @see https://github.com/antvis/g/issues/1064
    const pickedDisplayObjects: DisplayObject[] = [];
    // 遍历可能命中的图形对象
    for (const displayObject of hitTestList) {
      // 获取它的全局矩阵信息
      const worldTransform = displayObject.getWorldTransform();
      // 通过isHit方法查找出点击坐标命中的所有图形对象
      const isHitOriginShape = this.isHit(displayObject, position, worldTransform, false);
      if (isHitOriginShape) {
        // 是否应该在祖先节点中查找
        const clipped = findClosestClipPathTarget(displayObject);
        if (clipped) {
          const { clipPath } = clipped.parsedStyle as ParsedBaseStyleProps;
          const isHitClipPath = this.isHit(clipPath, position, clipPath.getWorldTransform(), true);
          if (isHitClipPath) {
            if (topmost) {
              // 返回拾取的图形对象
              result.picked = [displayObject];
              return result;
            } else {
              pickedDisplayObjects.push(displayObject);
            }
          }
        } else {
          if (topmost) {
            result.picked = [displayObject];
            return result;
          } else {
            pickedDisplayObjects.push(displayObject);
          }
        }
      }
    }

    // 返回拾取的图形对象
    result.picked = pickedDisplayObjects;
    return result;
  }

  // 查找坐标点命中的图形对象
  private isHit = (
    displayObject: DisplayObject,
    position: vec3,
    worldTransform: mat4,
    isClipPath: boolean,
  ) => {
    // 获取当前图形的拾取方法
    const pick = this.pointInPathPickerFactory[displayObject.nodeName];
    if (pick) {
      // invert with world matrix
      const invertWorldMat = mat4.invert(tmpMat4, worldTransform);

      // 这里为了方便计算矩阵,转换成局部坐标系
      const localPosition = vec3.transformMat4(
        tmpVec3b,
        vec3.set(tmpVec3c, position[0], position[1], 0),
        invertWorldMat,
      );

      // ???
      const { halfExtents } = displayObject.getGeometryBounds();
      const { anchor } = displayObject.parsedStyle as ParsedBaseStyleProps;
      localPosition[0] += ((anchor && anchor[0]) || 0) * halfExtents[0] * 2;
      localPosition[1] += ((anchor && anchor[1]) || 0) * halfExtents[1] * 2;

      // 如果拾取坐标在图形内,返回当前图形
      if (
        pick(
          displayObject,
          new Point(localPosition[0], localPosition[1]),
          isClipPath,
          this.isPointInPath,
        )
      ) {
        return true;
      }
    }

    return false;
  }

  // 这个方法使用原生canvas的isPointInPath接口,来检测是否在图形内
  // 有部分图形是通过这个方法来做拾取的,比如Path存在曲线时
  private isPointInPath = (displayObject: DisplayObject, position: Point) => {
    // 由于比较耗性能,在离屏canvas中绘制
    const context = runtime.offscreenCanvas.getOrCreateContext(
      this.canvasConfig.offscreenCanvas,
    ) as CanvasRenderingContext2D;

    const generatePath = this.pathGeneratorFactory[displayObject.nodeName];
    if (generatePath) {
      context.beginPath();
      generatePath(context, displayObject.parsedStyle);
      context.closePath();
    }

    return context.isPointInPath(position.x, position.y);
  }
}
  • 18
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值