最近选型使用Antv的G6来做流程设计器,研究G6的源码发现其操作Canvas的方法是使用自己封装的g-canvas库,也是来自于Antv的g开源项目,所以我们一起看下大厂如何用ts做canvas的类库;
我们可以进入g 的代码仓库一步步来看,大家可以看下read.me,我习惯看一个开源工程的package.json
一眼我就看到了一个没有见过的库
"@antv/gatsby-theme-antv": "^0.10.17",
"gatsby": "^2.15.34",
简单搜索下npm gatsby不难得知这是一个React的快速建站工具,手动羡慕眼;
做为一个资深的vue开发,坚信应该也会有类似的vue的快速建站工具,尝试google搜索"gatsby-vue",啊哈,Gridsome映入眼帘,果然react有的,vue社区也不会拉下,看下github 6.2K的start,和6天前的PR,果断start一波,加入我的试用List;还没开始读源码,就有收获,开心!
带着愉快的心情继续往下看,明明是一个库,但是能打包出g-canvas,g-svg供G6使用,果不其然在devDependencies发现了
"lerna": "^3.4.3",
接下来在扫描下哪些库看起来比较陌生
"benchmark": "^2.1.4",
"prettier": "^1.18.2",
以上这都是什么鬼?
benchmark:A benchmarking library that supports high-resolution timers & returns statistically significant results. 高分辨率计时器的基准库,所以这是G6 canvas动画更流畅的原因么?
prettier:Prettier is an opinionated code formatter. It enforces a consistent style by parsing your code and re-printing it with its own rules that take the maximum line length into account, wrapping code when necessary. 欧呦,还有这种美化代码的工具,学到了学到了,mark。
OK,既然是g-canvas那么我们还是直入正题来到g-canvas的部分
import * as Shape from './shape';
const pkg = require('../package.json');
export const version = pkg.version;
export * from './types';
export * from './interfaces';
export { Event } from '@antv/g-base';
export { default as Canvas } from './canvas';
export { default as Group } from './group';
export { Shape };
入口的index.ts向我们展示了在G6中我们使用的shape,event,canvas都是出自哪里,既然是处理canvas,我们就从canvas.ts看起;
首先我们可以看到import,以及声明了Canvas类
import { AbstractCanvas } from '@antv/g-base';
import { ChangeType } from '@antv/g-base/lib/types';
import { IElement } from './interfaces';
import { getShape } from './util/hit';
import * as Shape from './shape';
import Group from './group';
import { applyAttrsToContext, drawChildren, getMergedRegion, mergeView, checkRefresh, clearChanged } from './util/draw';
import { getPixelRatio, requestAnimationFrame, clearAnimationFrame } from './util/util';
class Canvas extends AbstractCanvas {
而从代码中可以得知这里引入了抽象的AbstractCanvas,并让Canvas继承了AbstractCanvas,我们很快可以找到同一工程中的另一个packageg-base,g-base官方定义是:可视化的绘图引擎的接口定义和抽象实现。
AbstractCanvas,细节暂且不表,这个AbstractCanvas继承了AbstractCanvas,实现了ICanvas接口
import Container from './container';
import { ICanvas } from '../interfaces';
abstract class Canvas extends Container implements ICanvas {
进一步我们可以去找到Container的源码,同样我们也只关注继承关系,和实现的接口
import { IContainer, IShape, IGroup, IElement, ICanvas } from '../interfaces';
import Element from './element';
abstract class Container extends Element implements IContainer {
俄罗斯套娃一样,没事,我们继续往里看
import { IElement, IShape, IGroup, ICanvas, ICtor } from '../interfaces';
import Base from './base';
abstract class Element extends Base implements IElement {
看到了base仿佛看到了最后一层
import EE from '@antv/event-emitter';
import { IBase } from '../interfaces';
abstract class Base extends EE implements IBase {
等等说好的最后一层呢,怎么还有个EE,EE还是个新的依赖,感觉💊; 但是我还不能放弃,EE走着, 这个代码量比较少,全量贴一下;
interface EventType {
readonly callback: Function;
readonly once: boolean;
}
type EventsType = Record<string, EventType[]>;
const WILDCARD = '*';
/* event-emitter */
export default class EventEmitter {
private _events: EventsType = {};
/**
* 监听一个事件
* @param evt
* @param callback
* @param once
*/
on(evt: string, callback: Function, once?: boolean) {
if (!this._events[evt]) {
this._events[evt] = [];
}
this._events[evt].push({
callback,
once: !!once,
});
return this;
}
/**
* 监听一个事件一次
* @param evt
* @param callback
*/
once(evt: string, callback: Function) {
return this.on(evt, callback, true);
}
/**
* 触发一个事件
* @param evt
* @param args
*/
emit(evt: string, ...args: any[]) {
const events = this._events[evt] || [];
const wildcardEvents = this._events[WILDCARD] || [];
// 实际的处理 emit 方法
const doEmit = (es: EventType[]) => {
let length = es.length;
for (let i = 0; i < length; i++) {
if (!es[i]) {
continue;
}
const { callback, once } = es[i];
if (once) {
es.splice(i, 1);
if (es.length === 0) {
delete this._events[evt];
}
length--;
i--;
}
callback.apply(this, args);
}
};
doEmit(events);
doEmit(wildcardEvents);
}
/**
* 取消监听一个事件,或者一个channel
* @param evt
* @param callback
*/
off(evt?: string, callback?: Function) {
if (!evt) {
// evt 为空全部清除
this._events = {};
} else {
if (!callback) {
// evt 存在,callback 为空,清除事件所有方法
delete this._events[evt];
} else {
// evt 存在,callback 存在,清除匹配的
const events = this._events[evt] || [];
let length = events.length;
for (let i = 0; i < length; i++) {
if (events[i].callback === callback) {
events.splice(i, 1);
length--;
i--;
}
}
if (events.length === 0) {
delete this._events[evt];
}
}
}
return this;
}
/* 当前所有的事件 */
getEvents() {
return this._events;
}
}
不知道你啥么感觉,这不就是个ts写的观察者模式么,那么回头去看base.ts,扫一眼构造函数;
/**
* @protected
* 默认的配置项
* @returns {object} 默认的配置项
*/
getDefaultCfg() {
return {};
}
constructor(cfg) {
super();
const defaultCfg = this.getDefaultCfg();
this.cfg = mix(defaultCfg, cfg);
}
然后再看看每一层都在用的interface.ts了解下抽象的Ibase接口,基本确定base只是提供了底层抽象,1、在当前对象上实现了事件的绑定/解绑/触发,2、初始化cfg;
再回到上一层套娃依然只是看构造函数
/**
* @protected
* 图形属性
* @type {ShapeAttrs}
*/
attrs: ShapeAttrs = {};
constructor(cfg) {
super(cfg);
const attrs = this.getDefaultAttrs();
mix(attrs, cfg.attrs);
this.attrs = attrs;
this.initAttrs(attrs);
this.initAnimate(); // 初始化动画
}
相比于base,这一层进一步的做了初始化attrs,具体attrs是什么请参看api,此外初始化了动画;接下来看内部方法,或者直接看IElement的抽象接口可知,这一层是在提取单个元素的书属性,或者是裁剪/移动/缩放/旋转,或者是直接通过矩阵转换,同时提供动画,大概率这一层是对canvas内的元素做的一层通用方法的封装,只是Container是element的一个实现;所以我们在Container.ts都没有看到构造函数,再看下IContainer的抽象接口;
export interface IContainer extends IElement {
/**
* 添加图形
* @param {ShapeCfg} cfg 图形配置项
* @returns 添加的图形对象
*/
addShape(cfg: ShapeCfg): IShape;
/**
* 添加图形
* @param {string} type 图形类型
* @param {ShapeCfg} cfg 图形配置项
* @returns 添加的图形对象
*/
addShape(type: string, cfg: ShapeCfg): IShape;
/**
* 容器是否是 Canvas 画布
*/
isCanvas();
/**
* 添加图形分组,增加一个默认的 Group
* @returns 添加的图形分组
*/
addGroup(): IGroup;
/**
* 添加图形分组,并设置配置项
* @param {GroupCfg} cfg 图形分组的配置项
* @returns 添加的图形分组
*/
addGroup(cfg: GroupCfg): IGroup;
/**
* 添加图形分组,指定类型
* @param {IGroup} classConstructor 图形分组的构造函数
* @param {GroupCfg} cfg 图形分组配置项
* @returns 添加的图形分组
*/
addGroup(classConstructor: IGroup, cfg: GroupCfg): IGroup;
/**
* 根据 x,y 获取对应的图形
* @param {number} x x 坐标
* @param {number} y y 坐标
* @param {Event} 浏览器事件对象
* @returns 添加的图形分组
*/
getShape(x: number, y: number, ev: Event): IShape;
/**
* 添加图形元素,已经在外面构造好的类
* @param {IElement} element 图形元素(图形或者分组)
*/
add(element: IElement);
/**
* 获取父元素
* @return {IContainer} 父元素一般是 Group 或者是 Canvas
*/
getParent(): IContainer;
/**
* 获取所有的子元素
* @return {IElement[]} 子元素的集合
*/
getChildren(): IElement[];
/**
* 子元素按照 zIndex 进行排序
*/
sort();
/**
* 清理所有的子元素
*/
clear();
/**
* 获取第一个子元素
* @return {IElement} 第一个元素
*/
getFirst(): IElement;
/**
* 获取最后一个子元素
* @return {IElement} 元素
*/
getLast(): IElement;
/**
* 根据索引获取子元素
* @return {IElement} 第一个元素
*/
getChildByIndex(index: number): IElement;
/**
* 子元素的数量
* @return {number} 子元素数量
*/
getCount(): number;
/**
* 是否包含对应元素
* @param {IElement} element 元素
* @return {boolean}
*/
contain(element: IElement): boolean;
/**
* 移除对应子元素
* @param {IElement} element 子元素
* @param {boolean} destroy 是否销毁子元素,默认为 true
*/
removeChild(element: IElement, destroy?: boolean);
/**
* 查找所有匹配的元素
* @param {ElementFilterFn} fn 匹配函数
* @return {IElement[]} 元素数组
*/
findAll(fn: ElementFilterFn): IElement[];
/**
* 查找元素,找到第一个返回
* @param {ElementFilterFn} fn 匹配函数
* @return {IElement|null} 元素,可以为空
*/
find(fn: ElementFilterFn): IElement;
/**
* 根据 ID 查找元素
* @param {string} id 元素 id
* @return {IElement | null} 元素
*/
findById(id: string): IElement;
/**
* 该方法即将废弃,不建议使用
* 根据 className 查找元素
* TODO: 该方法暂时只给 G6 3.3 以后的版本使用,待 G6 中的 findByClassName 方法移除后,G 也需要同步移除
* @param {string} className 元素 className
* @return {IElement | null} 元素
*/
findByClassName(className: string): IElement;
/**
* 根据 name 查找元素列表
* @param {string} name 元素名称
* @return {IElement[]} 元素
* 是否是实体分组,即对应实际的渲染元素
* @return {boolean} 是否是实体分组
*/
findAllByName(name: string): IElement[];
}
不难看出这是在做容器内的元素,元素组的增、删、清、查;
接下来我们回到canvas.ts,还是先看构造函数:
constructor(cfg: CanvasCfg) {
super(cfg);
this.initContainer();
this.initDom();
this.initEvents();
this.initTimeline();
}
做了大量的初始化,初始化容器,初始化Dom,初始化事件,初始化时间线
然后再看看ICanvas的抽象接口
/**
* @interface ICanvas
* 画布,图形的容器
*/
export interface ICanvas extends IContainer {
/**
* 获取当前的渲染引擎
* @return {Renderer} 返回当前的渲染引擎
*/
getRenderer(): Renderer;
/**
* 为了兼容持续向上查找 parent
* @return {IContainer} 返回元素的父容器,在 canvas 中始终是 null
*/
getParent(): IContainer;
/**
* 获取画布的 cursor 样式
* @return {Cursor}
*/
getCursor(): Cursor;
/**
* 设置画布的 cursor 样式
* @param {Cursor} cursor cursor 样式
*/
setCursor(cursor: Cursor);
/**
* 改变画布大小
* @param {number} width 宽度
* @param {number} height 高度
*/
changeSize(width: number, height: number);
/**
* 根据事件对象获取画布坐标
* @param {Event} ev 事件对象
* @return {object} 画布坐标
*/
getPointByEvent(ev: Event): Point;
/**
* 根据事件对象获取窗口坐标
* @param {Event} ev 事件对象
* @return {object} 窗口坐标
*/
getClientByEvent(ev: Event): Point;
/**
* 将窗口坐标转变成画布坐标
* @param {number} clientX 窗口 x 坐标
* @param {number} clientY 窗口 y 坐标
* @return {object} 画布坐标
*/
getPointByClient(clientX: number, clientY: number): Point;
/**
* 将 canvas 坐标转换成窗口坐标
* @param {number} x canvas 上的 x 坐标
* @param {number} y canvas 上的 y 坐标
* @returns {object} 窗口坐标
*/
getClientByPoint(x: number, y: number): Point;
/**
* 绘制
*/
draw();
}
到了这一层是不是看到了之前canvas学习笔记中耳熟能详的东西,获取画布坐标,获取窗口坐标,画布/坐标的窗口转换,绘图表面那一节我们有提到;
结合canvas实现的初始化方法,基本已经理清了了g-canvas中对于g-base一侧依赖路径,具体实现细节暂且按下不表,且待下回分解;