什么是Canvas
按照MDN官方文档中的定义:
实际上就是一套比使用DOM更加灵活的API,让开发者可以画出更加多样的图形和路径,在一些画板软件、阅读器软件或者数学函数展示平台上运用较多,使用canvas写的图形操作系统在同等数量级的情况下会比浏览器DOM有更好的性能
还是贴上MDN文档中的简单Canvas实例:
js
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
ctx.fillStyle = "green";
ctx.fillRect(10, 10, 150, 100);
canvas基本使用
canvas本身提供了一套基于状态机的上下文绘制方式,使用以下方式可以获取到这个上下文
js
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
在获取到上下文之后就可以对这个canvas的ctx设置各种属性,然后canvas就会使用这个上下文的各种属性来绘制传入的图形:
ctx中最基本的一些风格属性有: fillStyle strokeStyle lineWidth等,设置方式如下:
js
ctx.lineWidth = 10;
接下来可以按照MDN文档中的各种标准API去使用绘制想要的图形:
js
ctx.beginPath();
ctx.moveTo(50, 140);
ctx.lineTo(150, 60);
ctx.lineTo(250, 140);
ctx.closePath();
ctx.stroke();
Canvas处理事件的难点
由于canvas本身是调用显卡的API在屏幕上展示出一系列图形,这些图形本身没有经过浏览器的代理,所以用canvas画出来的东西就是一幅静态的画,没有任何事件处理机制,而很多软件在开发的时候是需要让屏幕上的元素对鼠标的动作有一些响应,所以我们需要自己对canvas实现一套事件的监听机制
最基本的就是浏览器本身提供了mousemove mouseup mousedown等事件的监听机制,我们可以使用mousemove来获取当前鼠标在画布上的坐标,然后通过维护恰当的数据结构,通过遍历找到鼠标悬停的元素,并对这个元素执行一系列处理操作
并且当画布上的元素发生变化的时候,我们还需要擦除原先的图像,然后重新绘制
下面是绑定事件的代码(省略了其他无关代码):
ts
private bindEventHandler() {
this.targetDom.addEventListener("mousedown", (event) => {
mouseDownHandler(event, this);
});
this.targetDom.addEventListener("mousemove", (event) => {
mouseMoveHandler(event, this);
});
this.targetDom.addEventListener("mouseup", (event) => {
mouseUpHandler(event, this);
});
}
本章节先大概介绍基本的代码设计,不会涉及细节的讲解。。。因为细节确实有点多,很难在这里展开讲,会在后面分成多章讲
元素的基类属性
ts
export interface ISimpleEvent {
on(name: string, handler: Function): void;
emit(name: string, args: SimpleEventData): void;
delete(name: string): void;
}
/**
* 部件抽象基类
*/
export abstract class VerbalWidget implements ISimpleEvent {
// 包围盒位置
x: number = 0;
y: number = 0;
width: number = 0;
height: number = 0;
scaleWidth: number = 0;
scaleHeight: number = 0;
basePoint: Point = { x: 0, y: 0 };
// 变换信息
degree: number = 0;
scaleX: number = 1;
scaleY: number = 1;
// 点数组
boundingBoxPoints: Point[] = [];
pathPoints: Point[] = [];
cornerPoints: Point[][] = [];
// 是否激活事件检测
isEventActive: boolean = true;
// 风格
style: any = {};
// 携带的变换器
transformer: VerbalWidget | null = null;
// 类型
shapeType: string = "unknown";
// 事件
eventObject: any = {};
// 所属的组部件
groupWidget: VerbalWidget | null = null;
}
Canvas位置的检测
那么像下面这种情况,有两个图形,然后当鼠标放在紫色的矩形上面时,要显示出一个悬停的状态
首先我们要维护canvas画布上的东西到一个可遍历的数据结构,这里可以考虑使用链表或者数组(数组的话,在有大量元素删除时可能不太方便),我自己写的时候用的是链表 + Map的结构去存储。那么按照上文中我们已经绑定了浏览器鼠标移动的监听函数,我们可以在监听函数中按照和添加元素顺序相反的顺序遍历元素,第一个检测到的就是我们要的元素:
ts
lookupPointOnWidget(x: number, y: number): VerbalWidget | null {
if (this.widgetToNode.size === 0) return null;
const head = this.renderList.getHead();
let cursor = this.renderList.getTail();
cursor = cursor.prev!;
while (cursor !== head) {
if (cursor.isRender) {
const widget = cursor.value!;
if (widget.get("isEventActive"))
if (widget.isPointOnWidget(x, y)) return widget;
}
cursor = cursor.prev!;
}
return null;
}
在拿到这个元素的结构之后,我们就可以获取悬停框应该在的位置信息,然后就将这个悬停框绘制在指定位置即可
Canvas选中
当鼠标按下时还需要实现一个这样的选中样式,这里可以采用一个对象记录鼠标操作时的各种状态,当有悬停元素并且按下鼠标时变成选中(这里的源码实现细节会在后续章节陆续发布):
ts
function mouseDownCommon(event: MouseEvent, ec: EventCenter) {
const hovering = ec.getHovering();
const { offsetX, offsetY } = event;
if (hovering) {
ec.setHitting(hovering);
removeHoveringFlag(ec);
placeHittingFlag(hovering, ec);
ec.getActionRemark().mouseDownOffset = {
x: offsetX - hovering.get("x"),
y: offsetY - hovering.get("y"),
};
ec.transferToEventCanvas(hovering);
ec.setState(StateEnum.CATCHING);
} else {
ec.getActionRemark().mouseDownPoint = { x: offsetX, y: offsetY };
ec.setState(StateEnum.BOXSELECT);
}
}
Canvas基本变换
这里运用了Canvas的几大变换API:rotate translate sclae:
CanvasRenderingContext2D.rotate()
是 Canvas 2D API 在变换矩阵中增加旋转的方法。角度变量表示一个顺时针旋转角度并且用弧度表示- Canvas 2D API 的
CanvasRenderingContext2D.translate()
方法对当前网格添加平移变换的方法 CanvasRenderingContext2D.scale()
是 Canvas 2D API 根据 x 水平方向和 y 垂直方向,为 canvas 单位添加缩放变换的方法
这个简易手绘风的库放在了github上:github.com/Prince-Herv...)
还有些bug在开发中hhh,欢迎大家交流学习