canvas tools
canvas
常用工具,可以使用封装的工具实现在 canvas
上绘制点、直线、折现、圆、椭圆等图案
<canvas></canvas>
canvas {
width: 1000px;
height: 800px;
}
interface Pos {
x: number;
y: number;
}
interface StrokeStyle {
width?: number;
color?: string;
fillColor?: string;
}
interface CanvasStrokeStyle {
width: number;
color: string;
fillColor: string;
}
interface PointOptions {
src?: string;
width?: number;
height?: number;
}
/**
* 在 canvas 上绘制图形
* @params canvas canvas dom
* @params strokeStyle 可选 样式设置
*/
export default class CanvasDraw {
canvas!: HTMLCanvasElement; // canvas 画布
ctx!: CanvasRenderingContext2D; // canvas 上下文
startPos!: Pos; // 开始位置
imgDataList!: ImageData[]; // 画布像素数据列表
method!: string | null; // 绘制方法
moreMethod!: string | null; // 绘制类型
strokeStyle!: CanvasStrokeStyle; // 样式
pointList!: Pos[]; // 点坐标列表
isDraw!: boolean; // more method 是否在绘制中
drawList!: Pos[]; // type 为 draw 时 点坐标列表
events!: object; // 发布订阅事件
timer!: any; // 延时器
pointStyle!: PointOptions; // 标注点的样式
constructor(canvas: HTMLCanvasElement, strokeStyle?: StrokeStyle) {
// 获取 canvas
this.canvas = canvas;
// 设置 canvas 宽高
const { width, height } = canvas.getBoundingClientRect();
this.canvas.width = width;
this.canvas.height = height;
// 获取 canvas 上下文
const ctx: CanvasRenderingContext2D | null = canvas.getContext("2d", {
willReadFrequently: true
});
if (!ctx) {
new Error("canvas 上下文为 null");
return;
}
this.ctx = ctx;
// 画布像素数据列表默认为空
this.imgDataList = [];
// more method 的点坐标位置
this.pointList = [];
// more method 的绘制状态
this.isDraw = false;
// type 为 draw 时 点坐标列表
this.drawList = [];
// 设置默认样式
const defineStrokeStyle = {
width: 1,
color: "#f00",
fillColor: "rgba(255, 0, 0, 0.1)"
};
this.strokeStyle = defineStrokeStyle;
for (const i in defineStrokeStyle) {
if (strokeStyle?.["i"]) {
this.strokeStyle[i] = strokeStyle["i"];
}
}
this.events = {};
this.drawEvent();
// 监听屏幕大小变化
window.addEventListener("resize", this.debounce());
}
// 获取样式
get defineStrokeStyle() {
return this.strokeStyle;
}
// 设置样式
set defineStrokeStyle(style: StrokeStyle) {
for (const i in style) {
this.strokeStyle[i] = style[i];
}
}
// 每次绘制时
private init() {
// 移除所有绘制的图像
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
// 将之前绘制的图像贴到画布上
this.imgDataList.length &&
this.ctx.putImageData(this.imgDataList.slice(-1)[0], 0, 0);
}
/**
* 开始绘制图像
* @param type 绘制类型 暂时只支持 rect | arc | ellipse | arrow | draw 五种基本类型
* 和 point | polyline | polygon 三种 more 类型
* @param options 绘制的点样式,只有类型为 point 时需要传参
*/
startDraw(type: string, pointStyle?: PointOptions) {
this.endMoreDraw();
const methodIndex = {
rect: "drawRect",
arc: "drawArc",
ellipse: "drawEllipse",
arrow: "drawArrow",
draw: "drawDraw"
};
const method = methodIndex[type];
if (method && method !== this.method) {
this.method = method;
this.moreMethod = null;
return;
}
const moreMethodIndex = {
point: "drawPoint",
polyline: "drawPolyline",
polygon: "drawPolygon"
};
const moreMethod = moreMethodIndex[type];
if (type === "point") {
// 如果是戳点获取样式
this.pointStyle = pointStyle ?? {};
}
if (moreMethod && moreMethod !== this.moreMethod) {
this.moreMethod = moreMethod;
this.method = null;
}
}
// 结束绘制
endDraw() {
if (this.method) {
this.method = null;
}
if (this.moreMethod) {
this.endMoreDraw();
this.moreMethod = null;
}
}
// 撤回画布
back() {
if (this.isDraw) {
this.endMoreDraw();
return;
}
const length = this.imgDataList.length; // 画布记录的数据列表
if (!length) return;
if (length === 1) {
// 如果只有一个数据,清空画布
this.clear();
return;
}
if (length) {
// 如果有多个,移除最后一个记录
this.imgDataList.splice(length - 1, length);
this.ctx.putImageData(this.imgDataList.slice(-1)[0], 0, 0);
}
}
// 清空画布
clear() {
this.endMoreDraw();
// 清空画布
if (!this.imgDataList.length) return;
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
// 清空记录的绘制记录列表
this.imgDataList = [];
}
// 退出当前页面时移除监听的事件
destroyEvent() {
this.method = null;
this.moreMethod = null;
this.clear();
this.pointList = [];
this.drawList = [];
this.timer && clearTimeout(this.timer);
// 移除鼠标按下事件
this.canvas.removeEventListener("mousedown", this.downEvent);
// 移除鼠标移动事件
this.canvas.removeEventListener("mousemove", this.moveEvent);
// 移除鼠标弹起事件
this.canvas.removeEventListener("mouseup", this.upEvent);
// 移除鼠标点击事件
this.canvas.removeEventListener("click", this.clickEvent);
// 移除鼠标双击事件
this.canvas.removeEventListener("dblclick", this.dblclickEvent);
// 移除鼠标移动事件
this.canvas.removeEventListener("mousemove", this.moveEvent);
// 移除鼠标右键事件
this.canvas.removeEventListener("contextmenu ", this.contextmenuEvent);
// 移除屏幕大小监听事件
window.removeEventListener("resize", this.debounce());
}
// 防抖
debounce = () => {
return () => {
this.timer && clearTimeout(this.timer);
this.timer = setTimeout(() => {
this.resize();
}, 300);
};
};
// 修改画布大小
private resize = () => {
// 设置 canvas 宽高
const { width, height } = this.canvas.getBoundingClientRect();
this.canvas.width = width;
this.canvas.height = height;
// TODO: 如果不清除画布,之前绘制的图形会错位
this.clear();
};
// 订阅事件
on(name: string, callback: any) {
if (!this.events[name]) {
this.events[name] = [];
}
this.events[name].push(callback);
}
// 发布事件
private emit(name: string, ...args: any[]) {
if (this.events[name]) {
this.events[name].forEach((callback) => {
callback(...args);
});
}
}
// 取消订阅
off(name: string, callback: any) {
if (this.events[name]) {
this.events[name] = this.events[name].filter(
(item: any) => item !== callback
);
}
}
// 结束 more method 的绘制中的绘制
private endMoreDraw() {
if (this.isDraw) {
// 如果处于绘制状态
// 清空记录的点坐标列表
this.pointList = [];
// 结束绘制状态
this.finishMoreDraw();
}
}
// 结束 more method 的绘制
private finishMoreDraw() {
// 需要满足的最少点
const pointLength = {
drawPolyline: 2,
drawPolygon: 3
};
this.init();
const length = this.pointList.length;
if (this.moreMethod && pointLength[this.moreMethod] <= length) {
// 如果有点坐标信息且满足最少点,绘制并保存
this[this.moreMethod]();
this.emit("drawEnd", this.pointList);
// 保存当前画布上的图像
this.imgDataList.push(
this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height)
);
}
// 清空坐标信息
this.pointList = [];
// 修改绘制状态
this.isDraw = false;
// 移除鼠标双击事件
this.canvas.removeEventListener("dblclick", this.dblclickEvent);
// 移除鼠标右键事件
this.canvas.removeEventListener("contextmenu ", this.contextmenuEvent);
// 移除鼠标移动事件
this.canvas.removeEventListener("mousemove", this.moveEvent);
}
// 双击事件
private dblclickEvent = () => {
const length = this.pointList.length;
// 移除双击产生的最后多余的点
this.pointList.splice(length - 1, length);
this.finishMoreDraw();
};
// 右击事件
private contextmenuEvent = (e: any) => {
// 阻止默认事件
e.preventDefault();
this.finishMoreDraw();
};
// 点击事件
private clickEvent = (e: any) => {
if (!this.moreMethod) return;
const pos = {
x: e.offsetX,
y: e.offsetY
};
if (this.moreMethod === "drawPoint") {
// 如果是戳点
this[this.moreMethod](e);
// 不需要用到移动等事件
return;
}
// 修改绘制状态
this.isDraw = true;
// 记录当前戳的点坐标
this.pointList.push(pos);
// 监听鼠标双击事件
this.canvas.addEventListener("dblclick", this.dblclickEvent);
// 监听鼠标右键事件
this.canvas.addEventListener("contextmenu", this.contextmenuEvent);
// 监听鼠标移动事件
this.canvas.addEventListener("mousemove", this.moveEvent);
};
// 移动事件
private moveEvent = (e: any) => {
this.init();
if (this.method) {
this[this.method](e);
return;
}
if (this.moreMethod) {
this[this.moreMethod](e);
}
};
// 鼠标弹起事件
private upEvent = (e: any) => {
// 保存当前画布上的图像
this.imgDataList.push(
this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height)
);
if (this.drawList.length) {
// 如果是 type 为 draw 时结束
this.drawList = [];
}
// 移除鼠标移动事件
this.canvas.removeEventListener("mousemove", this.moveEvent);
// 移除鼠标弹起事件
this.canvas.removeEventListener("mouseup", this.upEvent);
const res = {
startPos: this.startPos,
endPos: {
x: e.offsetX,
y: e.offsetY
}
};
this.emit("drawEnd", res);
};
// 鼠标按下事件
private downEvent = (e: any) => {
// 没有选中类型不绘制
if (!this.method) return;
this.startPos = {
x: e.offsetX,
y: e.offsetY
};
if (this.method === "drawDraw") {
// 如果 type 为 draw 时
this.drawList.push(this.startPos);
}
// 监听鼠标移动事件
this.canvas.addEventListener("mousemove", this.moveEvent);
// 监听鼠标弹起事件
this.canvas.addEventListener("mouseup", this.upEvent);
};
// 鼠标事件
private drawEvent() {
// 监听鼠标按下事件
this.canvas.addEventListener("mousedown", this.downEvent);
// 监听鼠标点击事件
this.canvas.addEventListener("click", this.clickEvent);
}
// 绘制矩形
private drawRect(e: any) {
// 开始绘制
this.ctx.beginPath();
// 设置绘制样式
const { width, color } = this.strokeStyle;
this.ctx.lineWidth = width;
this.ctx.strokeStyle = color;
// 绘制
const { x, y } = this.startPos;
this.ctx.rect(x, y, e.offsetX - x, e.offsetY - y);
this.ctx.stroke();
}
// 绘制圆
private drawArc(e: any) {
// 开始绘制
this.ctx.beginPath();
// 设置绘制样式
const { width, color } = this.strokeStyle;
this.ctx.lineWidth = width;
this.ctx.strokeStyle = color;
// 绘制
const { x, y } = this.startPos;
const centerX = Math.abs(e.offsetX + x) / 2;
const centerY = Math.abs(e.offsetY + y) / 2;
const radius = Math.abs(e.offsetX - x) / 2;
this.ctx.arc(centerX, centerY, radius, 0, 2 * Math.PI, true);
this.ctx.stroke();
}
// 绘制椭圆
private drawEllipse(e: any) {
// 开始绘制
this.ctx.beginPath();
// 设置绘制样式
const { width, color } = this.strokeStyle;
this.ctx.lineWidth = width;
this.ctx.strokeStyle = color;
// 绘制
const { x, y } = this.startPos;
const centerX = Math.abs(e.offsetX + x) / 2;
const centerY = Math.abs(e.offsetY + y) / 2;
const radiusX = Math.abs(e.offsetX - x) / 2;
const radiusY = Math.abs(e.offsetY - y) / 2;
this.ctx.ellipse(
centerX,
centerY,
radiusX,
radiusY,
0,
0,
2 * Math.PI,
true
);
this.ctx.stroke();
}
// 绘制箭头
private drawArrow(e: any) {
// 开始绘制
this.ctx.beginPath();
// 设置绘制样式
const { width, color } = this.strokeStyle;
this.ctx.lineWidth = width;
this.ctx.strokeStyle = color;
// 绘制
// 线段部分
const { x, y } = this.startPos;
this.ctx.moveTo(x, y);
this.ctx.lineTo(e.offsetX, e.offsetY);
// 箭头部分
const arrowDeg = 20; // 箭头夹角
const arrowLength = Math.min(10 * width, 50); // 箭头长度
// 上半部分箭头
const radius1 =
arrowDeg * (Math.PI / 180) + Math.atan2(e.offsetY - y, e.offsetX - x);
const arrow1 = { x: 0, y: 0 }; // 箭头坐标
arrow1.x = e.offsetX - Math.cos(radius1) * arrowLength;
arrow1.y = e.offsetY - Math.sin(radius1) * arrowLength;
this.ctx.moveTo(e.offsetX, e.offsetY);
this.ctx.lineTo(arrow1.x, arrow1.y);
// 下半半部分箭头
const radius2 =
arrowDeg * (Math.PI / 180) - Math.atan2(e.offsetY - y, e.offsetX - x);
const arrow2 = { x: 0, y: 0 }; // 箭头坐标
arrow2.x = e.offsetX - Math.cos(radius2) * arrowLength;
arrow2.y = e.offsetY + Math.sin(radius2) * arrowLength;
this.ctx.moveTo(e.offsetX, e.offsetY);
this.ctx.lineTo(arrow2.x, arrow2.y);
this.ctx.stroke();
}
// 绘画
private drawDraw(e: any) {
const pos = { x: e.offsetX, y: e.offsetY };
this.drawList.push(pos);
// 开始绘制
this.ctx.beginPath();
// 设置绘制样式
const { width, color } = this.strokeStyle;
this.ctx.lineWidth = width;
this.ctx.strokeStyle = color;
// 绘制
for (let i = 0; i < this.drawList.length; i++) {
const { x, y } = this.drawList[i];
if (i === 0) {
// 第一个点
this.ctx.moveTo(x, y);
} else {
// 其他的点
this.ctx.lineTo(x, y);
}
}
this.ctx.stroke();
}
// 绘制点
private drawPoint(e: any) {
// 开始绘制
this.ctx.beginPath();
// 设置绘制样式
const { offsetX, offsetY } = e;
const { color } = this.strokeStyle;
this.ctx.fillStyle = color;
const { src, width, height } = this.pointStyle;
const imageWidth = width ?? 20;
const imageHeight = height ?? 20;
// 绘制
if (src) {
// 有图片地址,绘制图片
const image = new Image();
image.src = src;
image.onload = () => {
this.ctx.drawImage(
image,
offsetX - imageWidth / 2,
offsetY - imageHeight / 2,
imageWidth,
imageHeight
);
// 保存当前画布上的图像
this.imgDataList.push(
this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height)
);
};
} else {
// 无图片地址,绘制圆点
this.ctx.ellipse(
offsetX,
offsetY,
imageWidth / 2,
imageHeight / 2,
0,
0,
2 * Math.PI,
true
);
this.ctx.fill();
// 保存当前画布上的图像
this.imgDataList.push(
this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height)
);
}
}
// 绘制折线
private drawPolyline(e?: any) {
// 开始绘制
this.ctx.beginPath();
// 设置绘制样式
const { width, color } = this.strokeStyle;
this.ctx.lineWidth = width;
this.ctx.strokeStyle = color;
// 绘制
for (let i = 0; i < this.pointList.length; i++) {
const { x, y } = this.pointList[i];
if (i === 0) {
// 第一个点
this.ctx.moveTo(x, y);
} else {
// 其他的点
this.ctx.lineTo(x, y);
}
}
// 添加移动的点
e && this.ctx.lineTo(e.offsetX, e.offsetY);
this.ctx.stroke();
}
// 绘制多边形
private drawPolygon(e?: any) {
// 开始绘制
this.ctx.beginPath();
// 设置绘制样式
const { width, color, fillColor } = this.strokeStyle;
this.ctx.lineWidth = width;
this.ctx.strokeStyle = color;
this.ctx.fillStyle = fillColor;
// 绘制
for (let i = 0; i < this.pointList.length; i++) {
const { x, y } = this.pointList[i];
if (i === 0) {
// 第一个点
this.ctx.moveTo(x, y);
} else {
// 其他的点
this.ctx.lineTo(x, y);
}
}
// 添加移动的点
e && this.ctx.lineTo(e.offsetX, e.offsetY);
// 连接第一个点
const { x, y } = this.pointList[0];
this.ctx.lineTo(x, y);
this.ctx.stroke();
this.ctx.fill();
}
}
demo
const canvasDraw = new CanvasDraw(canvas); // 实例化
const type = "rect" // "rect" | "arc" | "ellipse" | "arrow" | "draw" | "polyline" | "polygon" 类型
canvasDraw.setStrokeStyle = { width: 1, color: "red" }; // 设置样式
canvasDraw.startDraw(type); // 开始绘制 选择类型
canvasDraw.endDraw(); // 结束绘制 取消选择类型
canvasDraw.back(); // 撤回画布
canvasDraw.clear(); // 清空画布
canvasDraw.destroyEvent(); // 退出页面时的操作