众所周知,canvas 是前端进军可视化领域的一大利器,借助 canvas 画布我们不仅可以实现许多 dom 和 css 难以实现的、各种绚丽多彩的视觉效果,而且在渲染数量繁多、内容复杂的场景下,其性能表现及优化空间也占据一定优势。
然而 canvas 却存在一个缺陷:由于 canvas 是作为一个整体画布存在,所有的内容只不过是其内部渲染的结果,我们不能像在 dom 元素上监听事件一样,在 canvas 所渲染的图形内绑定各种事件,因此基于 canvas 画布开发出一套交互式应用是件复杂的事情。虽然 gayhub 上很多 canvas 框架自带了事件系统,但如果想深入学习 canvas,笔者认为还是有必要了解其实现原理,因此本篇文章将实现一个简易版的 canvas 事件系统。
正文开始前,先贴下仓库地址,各位按需取用 canvas-event-system
环境搭建
要在 canvas 上实现事件系统,我们必须先做些准备工作 —— 首先我们得往 canvas 上填充些“内容”,没有内容,谈何事件监听,下文我们将这些可绑定事件的内容称之为元素。同时,为简明扼要,笔者这里仅实现了形状(Shape
) 这一类元素;当我们有了一个个元素后,我们还需要一个容器去管理它们,这个容器则是 —— 舞台(Stage
),舞台如同上帝一般,负责元素们的渲染、事件管理及事件触发,接下来我们先初始化这两大类
API 设计
在实现细节前,笔者是这样设想事件系统的:我们可以通过 new
操作符生成一个个的 Shape
实例,并可在实例上监听各类事件,然后再将它们add
进Stage
即可,就像这样:
const stage = new Stage(myCanvas);
// 生成形状
const rect = new Rect(props); // 矩形
const circle = new Rect(props); // 圆形
// 监听点击事件
rect.on('click', () => console.log('click rect!'));
circle.on('click', () => console.log('click circle!'));
// 将形状添加至舞台,即可渲染到画布上
stage.add(rect);
stage.add(circle);
构建 Shape
由于不同形状间有许多相似的逻辑,因此我们先实现一个Base
基类,然后让诸如Rect
、Circle
等形状继承此类:
import { Listener, EventName, Shape } from './types';
export default class Base implements Shape {
private listeners: { [eventName: string]: Listener[] };
constructor() {
this.listeners = {};
}
draw(ctx: CanvasRenderingContext2D): void {
throw new Error('Method not implemented.');
}
on(eventName: EventNames, listener: Listener): void {
if (this.listeners[eventName]) {
this.listeners[eventName].push(listener);
} else {
this.listeners[eventName] = [listener];
}
}
getListeners(): { [name: string]: Listener[] } {
return this.listeners;
}
}
Base
有三个对外暴露的 api:
draw
用于绘制内容,需要将 canvas 上下文CanvasRenderingContext2D
传入on
用于事件监听,收集到的事件回调会以事件名eventName
为 key,回调函数数组为 value 的形式存放在一个对象当中,此外我们还用了枚举类型定义了所有事件
typescript export enum EventNames { click = 'click', mousedown = 'mousedown', mousemove = 'mousemove', mouseup = 'mouseup', mouseenter = 'mouseenter', mouseleave = 'mouseleave', }
getListeners
获取此形状上所有的监听事件
有了Base
基类,我们就可以轻松定义其他具体的形状:
比如Rect
:
import Base from './Base';
interface RectProps {
x: number;
y: number;
width: number;
height: number;
strokeWidth?: number;
strokeColor?: string;
fillColor?: string;
}
export default class Rect extends Base {
constructor(private props: RectProps) {
super();
this.props.fillColor = this.props.fillColor || '#fff';
this.props.strokeColor = this.props.strokeColor || '#000';
this.props.strokeWidth = this.props.strokeWidth || 1;
}
draw(ctx: CanvasRenderingContext2D) {
const { x, y, width, height, strokeColor, strokeWidth, fillColor } =