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