Canvas拖动图形效果实现

前言: 最近对于canvas产生了一些兴趣,我天生是对这种视觉的东西感到好奇的,因此投入了一些时间来玩一玩,仿照网上的一个案例实现了一个基于canvas的图形拖动效果。我主要是借鉴了他们的思想,代码部分是自己独立实现的,也因此走了一些canvas的坑。

Canvas图形拖动效果展示

主要实现的功能是:

  1. 鼠标点击会出现红色选中框,鼠标松开选中框消失;
  2. 鼠标移动,图片也会跟随移动;
  3. 图片只能在有限空间内移动,无法移动出边界。

在这里插入图片描述

实现逻辑

鼠标拖动逻辑

一点个人理解:鼠标事件或者其它浏览器事件都是独立的,但是在开发中用户通常都是执行多个事件的组合。
所以有必要将事件进行组合起来使用,通过使用相应的变量,过滤某些无效的操作。

例如用户在页面上执行
绘制操作:这个过程必然是包括:鼠标按下、鼠标移动、鼠标松开,这个三个完整的事情,且它们都是有序的。
这三种操作的其它任意组合都被认为是无效操作,不应该响应,在程序的执行逻辑中进行过滤。且上述动作在
程序执行过程中的调用表现是:鼠标按下执行1次,鼠标移动执行1到n次,鼠标松开执行一次。这里需要注意,
当鼠标在按下、移动的过程中,移出了添加了鼠标事件的元素时,然后松开时,此时不会触发鼠标松开事件,
也就是说前面的限制条件即失败了。所以需要额外添加一个鼠标移出事件,且其逻辑同鼠标松开事件。

图片移动效果逻辑

图片移动效果的实现其实说白了很简单,当鼠标移动时,清空上一次的图形,在新的位置重新绘制图形,通过这种不断的清空、绘制过程,就形成了图片可以在canvas上面进行移动的效果了。图形在数学上的变换原理很简单,就是初中数学的知识了。
具体情况看下面这个图示:
左上方是图形的初始位置,右下方是图形的移动位置;坐标(x1,y1)是初始的左顶点,坐标(x1’, y1’)是移动后的左顶点(我们不知道它的值);坐标(x,y)是初始鼠标点击的位置,坐标(now.x, now.y)是移动后鼠标点击的位置。

考虑鼠标从(x,y)到(now.x, now.y),它在x方向上的偏移量为:now.x-x,同理它在y放上上的偏移量为:now.y-y。所以,可以由此推导出鼠标移动后左顶点(x1’, y1’)的位置,它即为:
x1’ = x1 + (now.x-x)
y1’ = y1 + (now.y-y)

注意:我的代码里面是 x-now.x,这并不是错误,因为我是用now.x记录的上一个点的位置,x是当前点,这里的逻辑是不变的。
在这里插入图片描述

在这里插入图片描述

红色选中框

这里红色选中框的实现也很简单,鼠标选中时,绘制一个红色的选中框;当鼠标松开或者离开画布元素时,重新绘制图形,并且绘制一个白色的选中框。(整个canvas的背景是白色的,所以给人感觉就是选中框消失了。)

图形移动边界限制

因为图形移动到边界之外很难看,并且移动回来也有点麻烦。所以这里特地对此做了一个限制,它就是限制四个方向的顶点不能移动出canvas的边界。但是其实后来才发现,只需要限制左上顶点和右下顶点,总共4个点的位置即可了。 不过,我也没有对代码进行简化,希望大家可以看到我的思考过程。具体的逻辑,参考我的代码即可,这里只是简单陈述一下。

完整实现代码(未做优化)

这是原始的代码,因为写完之后,发现很多地方其实是不必要的,有的判断逻辑多余了,但是我就不去删减了,代码冗余一些,也方便大家理解我的实现过程。

<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8">
		<title>mouse event</title>
		<style>
			body, div {
				margin: 0;
                padding: 0;
			}
		</style>
	</head>
	<body>
		<div id="cas">
			<canvas id="cs" width="800" height="600" style="border: 2px solid green"></canvas>
		</div>
		
		<script type="text/javascript">
			let canvas = document.getElementById("cs");
			// 获取canvas的宽高
			let width = canvas.width;
			let height = canvas.height;
			
			let ctx = canvas.getContext("2d");
			let now = {x: 0, y: 0};  // 鼠标的位置
			let pos = {x1: 0, y1: 0, x2: 0, y2: 0, x3: 0, y3: 0, x4: 0, y4: 0};  // 图片起始点的位置,这里默认(x1, y1)为(0, 0)
			// 鼠标按下、鼠标移动和鼠标松开是两个分散的动作,现在要取它两的交集且有顺序,
			// 即:鼠标按下、鼠标移动和鼠标松开,将其认为是一个动作。
			
			let isDown = false;  
			
			// 创建img元素
			let img = document.createElement("img")
			// 设置它的src,这样图片会被加载
			img.src = "./test.png";
			img.style.position = "absolute"
			// 当图片加载完成之后,将其绘制到canvas上
			// 如果没有这句话,图片绘制可能不会发生,因为图片未加载时为空。
			img.onload = () => {
				drawCanvas(img, "#FFFFFF");
				// 更新图片的起始点
				updatePos(pos.x1, pos.y1);
			};
			
			// 添加鼠标事情,来实现图片的拖放功能。
			// 当鼠标按下时,获取当前的坐标
			canvas.onmousedown = e => {
				isDown = true;     // 鼠标按下时,设置isDown为true,此时移动鼠标才认为是有效。
				console.log("鼠标按下")
				let x = e.pageX-canvas.offsetLeft;     // 后面这个是偏移量,但是在这里为0
				let y = e.pageY-canvas.offsetTop;
				now.x = x;
				now.y = y;
				console.log(x + " -> " + y);
				
				if (!ctx.isPointInPath(x, y)) {
					console.log("鼠标没在路径内");
					return;
				}

				drawCanvas(img, "red");
			};
			
			// 在鼠标移动时,不断重绘制整个canvas
			canvas.onmousemove = e => {
				if (!isDown) {  // 鼠标未按下则直接返回,不去响应该事件。
					return;
				}

				// 获取点的坐标可以封装成函数
				let x = e.pageX;
				let y = e.pageY;
				
				console.log(x + " " + y);
				if (!ctx.isPointInPath(x, y)) {
					console.log("鼠标没在路径内");
					return;
				}
				// 这里需要限制鼠标不能越过界限的问题,图片移动到界面外的效果不好
				
				// 清空当前的canvas图形
				ctx.clearRect(0, 0, canvas.width, canvas.height);
				
				// 计算图片的绘制起点的变化位置
				pos.x1 = pos.x1 + (x-now.x);
				pos.y1 = pos.y1 + (y-now.y);
				// 更新其它点的位置
				updatePos(pos.x1, pos.y1);
				// 判断四个点的位置情况,不能越界
				judgePosition();
				
				// 重绘制偏移之后的canvas
				ctx.drawImage(img, pos.x1, pos.y1);
				
				ctx.beginPath();
				ctx.strokeStyle="red";
				ctx.rect(pos.x1, pos.y1, img.width, img.height);  
				ctx.stroke();
				
				now.x = x; now.y = y;
				console.log("鼠标在移动..." + x + " --> " + y);
			}

			canvas.onmouseup = e => {
				isDown = false;  // 鼠标松开,则上述封装的动作结束。
				console.log("鼠标松开");
				drawCanvas(img, "#FFFFFF");
			}
			
			// 如果鼠标按下然后移动的过程中离开了当前元素,再松开,但是无法触发鼠标松开事件了,
			// 所以当监听到鼠标移出元素时,必须也要将isDown设置成false。
			canvas.onmouseout = e => {
				isDown = false;
				console.log("鼠标离开了画布元素");
				drawCanvas(img, "#FFFFFF");
				// 判断四个点的位置情况,不能越界
				judgePosition();
				// 重绘制偏移之后的canvas
				ctx.drawImage(img, pos.x1, pos.y1);
			}
			
			function drawCanvas(img, color) {
				ctx.clearRect(0, 0, canvas.width, canvas.height);   // 清空当前的canvas图形
			    ctx.drawImage(img, pos.x1, pos.y1);   // 这里不应该使用全局变量传参数的。
				ctx.beginPath();
				ctx.strokeStyle=color;
				ctx.rect(pos.x1, pos.y1, img.width, img.height);
				ctx.stroke();
			}
			
			// 因为图片是矩形,所以只要知道一点就可以确定其余四点了,
			// 这里我们取 (x1,y1),即左上角的点,这样比较方便。
			// 因为x1,y1也在改变,所以需要先更新x1,y1.
			function updatePos(x, y) {
			console.log("传入的参数值:", x, y);
			    pos.x1 = x;
				pos.y1 = y;
				pos.x2 = x + img.width; 
				pos.y2 = y;
				pos.x3 = x + img.width;
				pos.y3 = y + img.height;
				pos.x4 = x;
				pos.y4 = y + img.height;
			}
			
			// 判断位置,当点越界时,进行处理
			function judgePosition() {
				// 先看一下点的规则
				console.log("judgePosition:", pos);
				
				// 单边出界的情况和两边出界共三种情况
				// 只要保证左上角和右下角的三种情况都不出界,所有情况都不会出界了。
				if (pos.x1 < 0 && pos.y1 > 0) {
					updatePos(0, pos.y1);
				} else if (pos.x1 > 0 && pos.y1 < 0) {
					updatePos(pos.x1, 0);
				} else if (pos.x1 < 0 && pos.y1 < 0) {
					updatePos(0, 0)
				} else if (pos.x3 > width && pos.y3 < height) {
					updatePos(width-img.width, pos.y3-img.height);
				} else if (pos.x3 < width && pos.y3 > height) {
					updatePos(pos.x3-img.width, height-img.height);
				} else if (pos.x3 > width && pos.y3 > height) {
					updatePos(width-img.width, height-img.height);
				}
			}
		</script>
	</body>
</html>

注意点

isPointInPath(x, y)函数的使用是有条件限制的,有些函数绘制出来的图形是不支持此函数的,所以如果使用错误,可能会出现与预期不符合的情况,会让人很困惑。特别是我,被它困扰了很久。

  • 5
    点赞
  • 33
    收藏
    觉得还不错? 一键收藏
  • 9
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值