一个绘图工具
故事的起源是因为开发一个可视化项目的时候,使用了第三方的可视化插件。但是因为我的需求并不复杂而且使用插件过于笨重导致页面卡顿。因此我就萌生了自己写一个库的想法。
工具包含的一些函数
说干就干,在我简单查看了下 canvas 的文档之后,从简单入手开始写一些简单的画图函数。
因为我这里只贴了主要的代码,为了减少代码阅读的困难,我先做一下代码解释:
this.ctx 是创建的canvas画布
例如:
this.ctx = document.getElementById('canvas').getContext('2d');
关于 canvas 的一些 api
canvas 的很多的 api 需要组合使用,以下就对 canvas 的一些常用 api 做下简单介绍,当然后面也是会加上注释。
// 一般这两个是成对出现的,也就是save和restore之间的设置不会保存。
ctx.save();
ctx.restore();
// 开始落笔,如果之前没有移动过canvas的起点位置,默认是左上角(0,0)
ctx.beginPath();
// 设置填充颜色,相当于你设置了当前画笔填充使用的颜色
ctx.fillStyle = rgba | hex;
// 填充颜色
ctx.fill();
// 设置画笔的颜色,告诉canvas你希望用什么颜色进行绘制
ctx.strokeStyle = rgba | hex;
// 设置线宽,但是你需要注意的是canvas绘制的线宽度可能并不是1像素的,具体以后会说哈。
ctx.lineWidth = number;
// 真正开始绘制你的线
ctx.stroke();
// 闭合绘制,当前笔的位置回归到你的下笔点
ctx.closePath();
// 移动你的下笔的位置
ctx.moveTo(x, y);
// 平移你的canvas画布,平移到的位置将会作为(0,0)点
ctx.translate(x, y);
画一个点
/**
* 创建一个点
* @param x
* @param y
* @param radius
* @param options
*/
drawPoint(x: number, y: number, radius: number, options?: OptionProps) {
const ctx = this.ctx;
if (ctx) {
// 先保存下之前canvas对象
ctx.save();
// 开始落笔,如果之前没有移动过canvas的起点位置,默认是左上角(0,0)
ctx.beginPath();
// 设置填充颜色
ctx.fillStyle = options?.color || _color;
// 绘制一个圆
ctx.arc(x, y, radius, 0, 2 * Math.PI);
// 闭合,但是这里是不需要的,因为本身绘制的就是一个闭合的图形
ctx.closePath();
// 开始填充闭合
ctx.fill();
// 还原canvas对象 在save()和restore()之前的设置不做保存,但是绘制的内容会留在画布上
ctx.restore();
}
}
画一个线段
/**
* 创建一条线段
* @param start
* @param end
*/
drawLine(start: Point, end: Point, options?: DrawLineOptionProps) {
const ctx = this.ctx;
if (ctx) {
ctx.save();
// 设置两个线段的相连位置的样式
ctx.lineJoin = 'round'; // round bevel miter
ctx.beginPath();
if (options?.isLineDash) {
// 设置实线和线间隙之间的交替长度
ctx.setLineDash([6, 6]);
// 设置虚线的偏移量
ctx.lineDashOffset = options.lineDashOffset || 2;
}
// 移动下笔位置
ctx.moveTo(start.x, start.y);
// 指定线段绘制的终点位置
ctx.lineTo(end.x, end.y);
// 填充线段颜色
ctx.strokeStyle = options?.strokeStyle || _color;
// 设置线宽
ctx.lineWidth = options?.width || _lineWidth;
// 绘制线段
ctx.stroke();
ctx.closePath();
ctx.restore();
}
}
绘制一个圆
/**
* 创建一个圆
* @param point
* @param radius
* @param options
*/
drawCircle(point: Point, radius: number, options?: DrawCircleOptionProps) {
const ctx = this.ctx;
if (ctx) {
ctx.save();
ctx.beginPath();
// 填充颜色
ctx.fillStyle = options?.fillColor || _color;
// border颜色
ctx.strokeStyle = options?.strokeColor || _color;
// border宽度
ctx.lineWidth = options?.lineWidth || _lineWidth;
ctx.arc(point.x, point.y, radius, 0, 2 * Math.PI);
if (options?.isFill) {
// 填充颜色
ctx.fill();
}
ctx.stroke();
ctx.closePath();
ctx.restore();
}
}
创建矩形
/**
* 创建一个矩形
* @param point
* @param width
* @param height
* @param options
*/
drawRect(point: Point, width: number, height: number, options?: DrawRectOptionProps) {
const ctx = this.ctx;
if (ctx) {
ctx.save();
ctx.beginPath();
ctx.lineJoin = 'round'; // round bevel miter
ctx.lineWidth = options?.lineWidth || _lineWidth;
ctx.strokeStyle = options?.strokeColor || _color;
if (options?.isLineDash) {
ctx.setLineDash([4, 8]);
ctx.lineDashOffset = options.lineDashOffset || 2;
}
ctx.strokeRect(point.x, point.y, width, height);
ctx.fillStyle = options?.fillColor || _color;
if (options?.isFill) {
ctx.fill();
}
ctx.stroke();
ctx.closePath();
ctx.restore();
}
}
生成图片
例如:
const image = new Image();
image.src = '';
// 注意你传递的 image 对象应该等到 onload 之后运行,否则会概率出现无法绘制图片的问题
image.onload = () => {
drawImage(...);
}
/**
* 画图片
* @param image
* @param options
*/
drawImage(image: CanvasImageSource, options: DrawImageOptionProps) {
const ctx = this.ctx;
if (ctx) {
const {sWidth, sHeight, dWidth, dHeight, point} = options;
ctx.save();
// 将绘制点移动到目标宽高的一半,这样在绘制图片的时候才能保证图片的正中心是当前落笔点。
this.setStartPath(point.x - dWidth / 2, point.y - dHeight / 2);
// 绘制图片
ctx.drawImage(image, 0, 0, sWidth, sHeight, 0, 0, dWidth, dHeight);
ctx.restore();
}
}
画箭头
主要核心的思想也就是找准一个点,然后以这个点开始绘制一个凹四边形,通过平移和旋转来达到指向不同方向的箭头
/**
* 画箭头
* @param startPoint
* @param options
*/
drawArrow(startPoint: Point, options?: DrawArrowOptionProps) {
const ctx = this.ctx;
if (ctx) {
ctx.save();
// 设置画布的初始点
this.setStartPath(startPoint.x, startPoint.y);
// 默认是向下
if (options?.direction === 'top') {
// 旋转画布
ctx.rotate((-180 * Math.PI) / 180);
// 平移画布
ctx.translate(-8, -4);
} else if (options?.direction === 'right') {
ctx.rotate((-90 * Math.PI) / 180);
ctx.translate(-8, -8);
} else if (options?.direction === 'down') {
ctx.rotate((0 * Math.PI) / 180);
ctx.translate(-8, -4);
} else if (options?.direction === 'left') {
ctx.rotate((90 * Math.PI) / 180);
ctx.translate(-8, -4);
}
ctx.beginPath();
// 基本就是为了画一个凹四边形
ctx.lineTo(8, 2);
ctx.lineTo(16, 0);
ctx.lineTo(8, 12);
ctx.lineTo(0, 0);
ctx.fillStyle = options?.fillColor || _color;
ctx.strokeStyle = options?.strokeColor || _color;
ctx.stroke();
ctx.fill();
ctx.closePath();
ctx.restore();
}
}
三次贝塞尔曲线
// 获取贝塞尔控制点
_getBezierCurveTo(point1: Point, point2: Point, point3: Point, point4: Point, curvature: number) {
const cp1x = point2.x + (point3.x - point1.x) * curvature;
const cp1y = point2.y + (point3.y - point1.y) * curvature;
const cp2x = point3.x - (point4.x - point2.x) * curvature;
const cp2y = point3.y - (point4.y - point2.y) * curvature;
return {
cp1x,
cp1y,
cp2x,
cp2y
};
}
/**
* 绘制三次贝塞尔曲线
* @param paths
* @param options
*/
drawBezierCurve(paths: Point[], options?: DrawBezierOptionProps) {
const ctx = this.ctx;
const curvature = 0.1;
if (ctx) {
ctx.save();
ctx.beginPath();
ctx.strokeStyle = options?.color || _color;
ctx.lineWidth = options?.lineWidth || 1;
ctx.moveTo(paths[0].x, paths[0].y);
for (let index = 0; index < paths.length; index++) {
if (index === paths.length - 1) {
continue;
}
let point1 = paths[index - 1];
const point2 = paths[index];
const point3 = paths[index + 1];
let point4 = paths[index + 2];
// 处理边界问题
if (index === 0) {
point1 = point2;
}
if (index === paths.length - 2) {
point4 = point3;
}
// 三次贝塞尔曲线
const {cp1x, cp1y, cp2x, cp2y} = this._getBezierCurveTo(point1, point2, point3, point4, curvature);
// 绘制曲线
ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, point3.x, point3.y);
}
ctx.stroke();
ctx.closePath();
ctx.restore();
}
}