VUE:使用canvas绘制管线/管廊(四)

1,514 阅读4分钟

在上节中,主要是针对鼠标的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是构造函数中 设备 的信息,信息包括:

  • 设备IDid:在实际的前后端数据对接中,可以通过设备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();
}

注意: 在绘制管线流动的线条时,需要从中间开始绘制,分为两种情况:

  • 横向

image.png

  • 竖向

image.png

而水流的方向,是根据鼠标的拖动方向有关,判断条件就是通过比较 startXendXstartYendY 即可。

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的文字绘制,其起始点坐标位于文字整体的左下方,如下图:

image.png

为什么要把设备上方的文字绘制单独摘出来,是因为文字在绘制时,默认情况,文字的左侧是和图片的左侧齐平的,如图:

image.png 但是,在实际情况下,我们需要做到文字的中心线与图片的中心线保持一致, 如图:

image.png 还有这种情况:

image.png

这两种情况都需要我们去计算,而且,竖向或横向的图片在旋转 ±90度 后,都会变为横向或竖向, 那么,在其上方的文字,相对应的位置也需要重新计算,不然就会出现下方这种情况:

image.png 或者是这种情况:

image.png

针对以上两种情况,都需要对文字进行重新绘制,具体代码如下:

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,将弹窗修改、数据提交等相关边缘功能全部完善,并附上源码地址,觉得有用的同学可以多多评论点赞呀~