canvas绘制记录

350 阅读5分钟

1. canvas介绍

Canvas API 提供了一个通过JavaScript 和 HTML的<canvas>元素来绘制图形的方式。它可以用于动画、游戏画面、数据可视化、图片编辑以及实时视频处理等方面。 Canvas API 主要聚焦于 2D 图形。 canvas的原点在左上角,webgl是在中心,这点需要区分下。

2. canvas绘制的基础示例

<canvas id='myCanvas' />
const canvas = document.getElementById("myCanvas");
const ctx = canvas.getContext("2d");
ctx.fillStyle = "red";
ctx.fillRect(10, 10, 100, 100);

3. canvas动态绘制

  • 3.1 数据格式部分

    const rectArea = {
    startX: 0,
    startY: 0,
    areaWidth: 0,
    areaHeight: 0
    }
    
  • 3.2 事件监听部分

    使用鼠标的mousedown、mousemove、mouseup三个事件监听就可以轻松实现图形的新绘制,这边依旧使用矩形举例说明。

    整个实现过程大概是这样的,在鼠标按下时获取起始坐标,并且记录,在鼠标移动的时候与起始坐标进行计算,算出矩形的长和宽,鼠标抬起的时候结束绘制。

    整体实现比较简单,对于图形的绘制难点在于计算,矩形的计算较为简单,如果遇到多边形等绘制,就有可能需要了解下交叉检测(快速排斥实验或者跨立实验等)。 下面根据代码简单说明下这个过程:

    • mousedown
    // 记录按下去那个点的坐标
    const mousedownEvent = (e) => {
    	isMouseDown = true;
    	mousedown.x = e.offsetX;
    	mousedown.y = e.offsetY;
    }
    
    • mousemove
    const mousemoveEvent = (e) => {
    	let x = e.offsetX;
    	let y = e.offsetY;
    	// 获取canvas的一个大小
    	const canvasDom = document.getElementById("myCanvas");
    	let bbox = canvasDom.getBoundingClientRect();
    	// 边界检测,如果鼠标移动到canvas外部,不应该继续增加矩形的长宽,因为图形绘制是在当期canvas的显示区域
    	x = x > bbox.right ? bbox.right : x;
    	x = x < bbox.left ? bbox.left : x;
    	y = y > bbox.bottom ? bbox.bottom : y;
    	y = y < bbox.top ? bbox.top : y;
    	const minWidth = 6;
    	const minHeight = 6;
    	// 矩形的左上顶点
    	rectArea.startX = x < this.mousedown.x ? x : this.mousedown.x;
    	rectArea.startY = y < this.mousedown.y ? y : this.mousedown.y;
    	// 矩形的长宽
    	rectArea.areaWidth = Math.max(Math.abs(x - mousedown.x), minWidth);
    	rectArea.areaHeight = Math.max(Math.abs(y - mousedown.y), minHeight);
    };
    
    • mouseup
    const mouseUpEvent = (e) => {
    	isMouseDown = false
    }
    

    新绘制大概就是以上这些就能实现了,但通常情况下也需要对已经绘制的图形进行编辑,于是就可以对以上代码进行一个调整,适配下编辑的情况。

    • 首先,需要检测下当前鼠标是否在我们之前已经绘制的矩形内部/边界
    const moveIsInArea = (e) => {
    	const x = e.offsetX;
    	const y = e.offsetY;
    	// 是否在矩形内部/边界
    	let isInArea = false;
    	// 事件类型
    	let drawEvent = 0;
    	// 鼠标样式
    	let cursor = '';
    	const { startX, startY, areaWidth, areaHeight } = rectArea;
    	const endX = startX + areaWidth;
    	const endY = startY + areaHeight;
    	if (startX <= x && endX >= x && startY <= y && endY >= y) {
    		if (x - startX < 5 && y - startY < 5) { // 在矩形的左上顶点
    			cursor = "nw-resize";
    			drawEvent = 2;
    		} else if (x - startX < 5 && endY - y < 5) { // 在矩形的左下顶点
    			cursor = "sw-resize";
    			drawEvent = 8;
    		} else if (endX - x < 5 && y - startY < 5) { // 在矩形的右上顶点
    			cursor = "ne-resize";
    			drawEvent = 4;
    		} else if (endX - x < 5 && endY - y < 5) { // 在矩形的右下顶点
    			cursor = "se-resize";
    			drawEvent = 6;
    		} else if (x - startX < 3) { // 在矩形的左边界
    			cursor = "w-resize";
    			drawEvent = 1;
    		} else if (endX - x < 3) { // 在矩形的右边界
    			cursor = "e-resize";
    			drawEvent = 5;
    		} else if (y - startY < 3) { // 在矩形的上边界
    			cursor = "n-resize";
    			drawEvent = 3;
    		} else if (endY - y < 3) { // 在矩形的下边界
    			cursor = "s-resize";
    			drawEvent = 7;
    		} else { // 在矩形内部
    			cursor = "move";
    			drawEvent = 9;
    		}
    	}
    	return {
    		isInArea,
    		drawEvent,
    	};
    };
    
    • 编辑可以看做另类的新绘制,比如拖动左边界,可以看做 右上顶点为起始绘制点的一个新绘制, 其他边界或顶点的拉伸也都可以类推,mousedown代码就可以相对应改成:
    const mousedownEvent = (e) => {
    	isMouseDown = true;
    	let x = e.clientX
    	let y = e.clientY
    	if (this.drawEvent !== 0) {
    		const { startX, startY, areaWidth, areaHeight } = rectArea;
    		const endX = startX + areaWidth;
    		const endY = startY + areaHeight;
    		switch (this.drawEvent) {
    			// 左边界&左下顶点
    			case 1:
    			case 8:
    			x = endX;
    			y = startY;
    			break;
    			// 上边界&左上顶点
    			case 2:
    			case 3:
    			x = endX;
    			y = endY;
    			break;
    			// 右边界&右上顶点
    			case 4:
    			case 5:
    			x = startX;
    			y = endY;
    			break;
    			// 下边界&右下顶点
    			case 6:
    			case 7:
    			x = startX;
    			y = startY;
    			break;
    		}
    	}
    	// 设置起始顶点
    	mousedown.x = x;
    	mousedown.y = y;
    }
    
    • 改写之前的mousemove方法
    const mousemoveEvent = (e) => {
    	// 鼠标还没按下,检测下是否在矩形内部/边界
    	if (!isMouseDown) {
    		moveIsInArea(e)
    		} else {
    		// 获取canvas的一个大小
    		let bbox = canvasDom.getBoundingClientRect();
    		// 边界检测,如果鼠标移动到canvas外部,不应该继续增加矩形的长宽,因为图形绘制是在当期canvas的显示区域
    		x = x > bbox.right ? bbox.right : x;
    		x = x < bbox.left ? bbox.left : x;
    		y = y > bbox.bottom ? bbox.bottom : y;
    		y = y < bbox.top ? bbox.top : y;
    		const minWidth = 6;
    		const minHeight = 6;
    		// 矩形的左上顶点
    		const left = x < this.mousedown.x ? x : this.mousedown.x;
    		const top = y < this.mousedown.y ? y : this.mousedown.y;
    		// 矩形的长宽
    		const width = Math.max(Math.abs(x - mousedown.x), minWidth);
    		const weight = Math.max(Math.abs(y - mousedown.y), minHeight);
    		switch (this.drawEvent) {
    			// 左右边界拉伸
    			case 1:
    			case 5:
    			rectArea.startX = left;
    			rectArea.areaWidth = width;
    			break;
    			// 上下边界拉伸
    			case 3:
    			case 7:
    			rectArea.startY = top;
    			rectArea.areaHeight = height;
    			break;
    			// 拖动整个矩形到新的位置(移动)
    			case 9:
    			rectArea.startX += x - mousedown.x;
    			rectArea.startY += y - mousedown.y;
    			rectArea.startX = Math.max(rectArea.startX, 0);
    			rectArea.startX = Math.min(rectArea.startX, canvasW - rectArea.areaWidth);
    			rectArea.startY = Math.max(rectArea.startY, 0);
    			rectArea.startY = Math.min(rectArea.startY, canvasH - rectArea.areaHeight);
    			mousedown.y = y;
    			mousedown.x = x;
    			break;
    			// 四个顶点拉伸以及新绘制
    			default:
    			rectArea.startX = left;
    			rectArea.startY = top;
    			rectArea.areaWidth = width;
    			rectArea.areaHeight = height;
    			break;
    		}
    	}
    };
    

    经过上面修改,就可以通过鼠标对绘制数据进行更改了

  • 3.3 数据格式部分

    绘制数据已经有了,接下来就是按照绘制数据直接将图形在canvas上绘制出来即可。

    const canvasDom = document.getElementById("myCanvas");
    const ctx = canvasDom.getContext("2d");
    // 设置线宽
    ctx.lineWidth = 2;
    // 清除画布,重新绘制
    ctx.clearRect(0, 0, canvasDom.width, canvasDom.height);
    const { startX, startY, areaHeight, areaWidth } = rectArea;
    // 设置边框颜色
    ctx.strokeStyle = '#FF0000';
    // 绘制非实心矩形
    ctx.strokeRect(startX, startY, areaWidth, areaHeight);
    // 绘制非实心矩形
    // ctx.fillRect(startX, startY, areaWidth, areaHeight);
    

4. 结语

     以上就是一个简单绘制的全过程了。其实绘制还有其他的细节,比如窗口大小变化导致canvas画布大小变化,这个时候往往需要对canvas进行大小变化监听,可以使用ResizeObserver或者MutationObserver,监听到大小变化重新计算坐标即可,这部分有时间再补充吧。 canvas的功能很强大,感兴趣的话也是可以玩一玩的。

待补充......