在上节中,主要是针对鼠标的onmousedown onmousemove onmouseup事件来讲,在整个绘制功能中,除了鼠标事件外,还有canvas的绘制事件也是较为核心的一部分,那么,在本节,就对canvas的事件来重点讲讲。
再来回忆一下,在上上节中,我通过构造函数,初始化了一个元素的对象,如下代码:
/**
* 创建绘制元素工厂函数
*
* */
class ElementFactory {
constructor(startX, startY, endX, endY) {
this.startX = startX; // 鼠标 按下 X点
this.startY = startY; // 鼠标 按下 Y点
this.endX = endX; // 鼠标 抬起 X点
this.endY = endY; // 鼠标 抬起 Y点
this.type = 0; // 绘制类型:图形、文字、图片
this.pipelineInfo = {}; // 图形(管线)私有信息
this.equipmentInfo = {}; // 图片(设备)私有信息
this.textInfo = {}; // 文字(文字)私有信息
}
get minX() {
return Math.min(this.startX, this.endX);
}
get maxX() {
return Math.max(this.startX, this.endX);
}
get minY() {
return Math.min(this.startY, this.endY);
}
get maxY() {
return Math.max(this.startY, this.endY);
}
get middleX() {
return this.endX - (this.endX - this.startX) / 2
}
get middleY() {
return this.endY - (this.endY - this.startY) / 2
}
// 判断点击的是否存在元素绘制的范围之内
isInside(x, y) {
return x >= this.minX && x <= this.maxX && y >= this.minY && y <= this.maxY
}
// 绘制管线
drawPipeline() {}
// 绘制设备
drawEquipment() {}
// 绘制设备上方文字
drawEquipmentText() {}
// 绘制纯文本
drawText() {}
// 根据条件来调用不同的绘制方法
drawAllElement() {
parseInt(this.type) === 0 ? this.drawPipeline() : (parseInt(this.type) === 1 ? this.drawEquipment() : this.drawText())
}
}
结合我们当前业务,我先来对 constructor 中的 pipelineInfo equipmentInfo textInfo来做一个说明。
pipelineInfo equipmentInfo textInfo 信息说明:
pipelineInfo 信息说明
pipelineInfo是构造函数中 管线/管廊 的信息,信息包括:
- 管线/管廊 方向
direction:横向true,竖向false - 水流类型(冷水、热水)
waterType
其他的信息因为我们的业务没有涉及,所以,若各位同学在引用的时候,需要的话可以根据实际情况来添加。因此,在对pipelineInfo初始化时,可以如下:
this.pipelineInfo = {
waterType: pipeline_water_type,
direction: false
};
pipeline_water_type在上节中,全局定义的变量中有说明。
equipmentInfo 信息说明
equipmentInfo是构造函数中 设备 的信息,信息包括:
- 设备ID
id:在实际的前后端数据对接中,可以通过设备ID来对其进行数据更新展示,ID由前后端共同商议 - 设备图标地址
iconPath - 设备名称
name - 设备数值单位
unit - 设备图标缩放比例
scale:图标的大小我没有通过具体的宽高值来设置,而是根据UI人员的切图来进行等比缩放,以此能够保证图片不会被拉伸 - 设备图标旋转度数
rotate:默认图标的旋转度数为0度,实际业务中,分别有90度、-90度、180度 - 设备是否展示图标上方的信息文字
show - 设备图标展示的数值
value - 设备图标的其他信息
others - 设备图标旋转后的四个坐标位置对象
rotateCoordinate
对equipmentInfo初始化时,可以如下:
this.equipmentInfo = {
id: '',
iconPath: equipment_select.iconPath,
name: equipment_select.name,
unit: 'm/s',
scale: ICON_SCALE_RATIO,
rotate: 0,
show: true,
value: '12378',
others: [],
rotateCoordinate: {}
};
其中 equipment_select ICON_SCALE_RATIO 在上节中,全局定义的变量中有说明。
textInfo 信息说明
equipmentInfo是构造函数中 自定义文字 的信息,信息包括:
- 文字大小
fontSize - 文字内容
text - 文字颜色
color
对textInfo初始化时,可以如下:
this.textInfo = {
fontSize: '24',
text: '',
color: '#000'
};
上述的基本信息,在对构造函数进行 new 操作时,可根据条件进行删除,保证每个 new 的对象简介明了。
说完基本信息,接下来是构造函数的绘制方法了,绘制方法基本是canvas方法的引用,主要涉及canvas的图形绘制、canvas的文字绘制、canvas的图片绘制
构造函数中canvas绘制方法封装
drawPipeline:管线/管廊绘制
// 绘制管线
drawPipeline() {
ctx.beginPath();
ctx.moveTo(this.minX * devicePixelRatio, this.minY * devicePixelRatio);
ctx.lineTo(this.maxX * devicePixelRatio, this.minY * devicePixelRatio);
ctx.lineTo(this.maxX * devicePixelRatio, this.maxY * devicePixelRatio);
ctx.lineTo(this.minX * devicePixelRatio, this.maxY * devicePixelRatio);
ctx.lineTo(this.minX * devicePixelRatio, this.minY * devicePixelRatio);
ctx.fillStyle = ['#19c2ff', '#ffa600'][this.pipelineInfo.waterType];
ctx.fill();
ctx.strokeStyle = '#fff';
ctx.lineCap = 'square';
ctx.lineWidth = 1;
ctx.stroke();
// 绘制流动效果
ctx.beginPath();
if (this.pipelineInfo.direction) {
ctx.moveTo(this.minX * devicePixelRatio, this.middleY * devicePixelRatio);
ctx.lineTo(this.maxX * devicePixelRatio, this.middleY * devicePixelRatio);
ctx.lineDashOffset = this.startX < this.endX ? - pipeline_offset : pipeline_offset;
} else {
ctx.moveTo(this.middleX * devicePixelRatio, this.minY * devicePixelRatio);
ctx.lineTo(this.middleX * devicePixelRatio, this.maxY * devicePixelRatio);
ctx.lineDashOffset = this.startY < this.endY ? - pipeline_offset : pipeline_offset;
}
ctx.strokeStyle = ['#18719f', '#ff4316'][this.pipelineInfo.waterType];
ctx.lineWidth = 5;
ctx.setLineDash([15, 15]);
ctx.stroke();
}
注意: 在绘制管线流动的线条时,需要从中间开始绘制,分为两种情况:
- 横向
- 竖向
而水流的方向,是根据鼠标的拖动方向有关,判断条件就是通过比较 startX与endX 及 startY与endY 即可。
drawEquipment:设备绘制
drawEquipment() {
this.equipmentInfo.rotateCoordinate = this.coordinate
drawIcon.startX = this.startX;
drawIcon.startY = this.startY;
drawIcon.iconPath = this.equipmentInfo.iconPath;
drawIcon.rotate = this.equipmentInfo.rotate;
drawIcon.scale = this.equipmentInfo.scale;
drawIcon.show = this.equipmentInfo.show;
drawIcon.draw()
if (equipment_area_show) {
ctx.beginPath();
ctx.moveTo(this.minX * devicePixelRatio, this.minY * devicePixelRatio);
ctx.lineTo(this.maxX * devicePixelRatio, this.minY * devicePixelRatio);
ctx.lineTo(this.maxX * devicePixelRatio, this.maxY * devicePixelRatio);
ctx.lineTo(this.minX * devicePixelRatio, this.maxY * devicePixelRatio);
ctx.lineTo(this.minX * devicePixelRatio, this.minY * devicePixelRatio);
ctx.fillStyle = 'rgba(0,0,0,0.41)';
ctx.fill();
ctx.strokeStyle = '#fff';
ctx.lineCap = 'square';
ctx.lineWidth = 1;
ctx.stroke();
ctx.closePath();
}
// 添加文字描述
if (drawIcon.show) this.drawEquipmentText()
}
注意: 绘制图片时,因为this的指向问题,我在构造函数的外边写了一个绘制图片的对象,如下:
const drawIcon = {
startX: 0,
startY: 0,
iconPath: '',
scale: 0,
rotate: 0,
draw: function () {
let img = new Image();
img.src = require(`../assets/images/${this.iconPath}`);
ctx.save();
// 平移转换,改变画笔的原点位置为画布的中心点
ctx.translate(this.startX + ((img.width / this.scale) / 2), this.startY + ((img.height / this.scale) / 2))
// 旋转转换,改变画笔的旋转角度
ctx.rotate(this.rotate * Math.PI / 180);
ctx.translate(-(this.startX + ((img.width / this.scale) / 2)), -(this.startY + ((img.height / this.scale) / 2)));
// 调用绘制图片的方法把图片绘制到canvas中
ctx.drawImage(img, this.startX, this.startY, (img.width / this.scale), (img.height / this.scale));
// 使用 restore()进行恢复
ctx.restore();
}
};
图片绘制过程中,涉及到图片的旋转问题,因为canvas的旋转牵扯太多的知识点,在这个功能中不做具体讲解,感兴趣的同学可以自己去找找相关的文档,本案例中的图片旋转是 围绕所绘制图片的中心点进行旋转的。
展示图片的绘制范围和 绘制图形的方法一样,不做赘述。
drawEquipmentText:设备文字绘制
canvas的文字绘制,其起始点坐标位于文字整体的左下方,如下图:
为什么要把设备上方的文字绘制单独摘出来,是因为文字在绘制时,默认情况,文字的左侧是和图片的左侧齐平的,如图:
但是,在实际情况下,我们需要做到文字的中心线与图片的中心线保持一致, 如图:
还有这种情况:
这两种情况都需要我们去计算,而且,竖向或横向的图片在旋转 ±90度 后,都会变为横向或竖向, 那么,在其上方的文字,相对应的位置也需要重新计算,不然就会出现下方这种情况:
或者是这种情况:
针对以上两种情况,都需要对文字进行重新绘制,具体代码如下:
drawEquipmentText() {
ctx.beginPath()
ctx.fillStyle = '#000'
ctx.font = "normal normal normal 12px Microsoft YaHei"
const measureText = ctx.measureText(`${this.equipmentInfo.name}: ${this.equipmentInfo.value} ${this.equipmentInfo.unit}`)
let fontWidth = measureText.width,
fontHeight = measureText.actualBoundingBoxAscent + measureText.actualBoundingBoxDescent + 2;
let fontX = this.equipmentInfo.rotateCoordinate.startX + ((this.equipmentInfo.rotateCoordinate.endX - this.equipmentInfo.rotateCoordinate.startX - fontWidth) / 2)
ctx.fillText(`${this.equipmentInfo.name}: ${this.equipmentInfo.value} ${this.equipmentInfo.unit}`, fontX, this.equipmentInfo.rotateCoordinate.startY - 5)
if (this.equipmentInfo.others.filter(item => item).length) {
this.equipmentInfo.others.filter(item => item).map((item, index) => {
ctx.fillText(`${item}`, fontX, this.equipmentInfo.rotateCoordinate.startY - 5 - (fontHeight * (index +1)))
})
}
ctx.closePath()
}
drawText:文字绘制
这个文字绘制和上述的图标文字绘制有相似的地方,但是比上述的功能简单了很多,只需要额外设置文字的大小和颜色,代码如下:
drawText() {
ctx.beginPath();
ctx.fillStyle = this.textInfo.color || '#000';
ctx.font = `normal normal normal ${this.textInfo.fontSize + 'px' || '16px'} Microsoft YaHei`;
let content = this.textInfo.text;
ctx.fillText(`${content}`, this.startX, this.startY);
ctx.closePath();
}
好了,本节写的有点啰嗦,但是,要想达到想要的效果,每一个细节都需要注意到位,综上,canvas绘制管线/管廊的功能基本分享完毕,在最后一节,我会结合VUE,将弹窗修改、数据提交等相关边缘功能全部完善,并附上源码地址,觉得有用的同学可以多多评论点赞呀~