前言: 最近对于canvas产生了一些兴趣,我天生是对这种视觉的东西感到好奇的,因此投入了一些时间来玩一玩,仿照网上的一个案例实现了一个基于canvas的图形拖动效果。我主要是借鉴了他们的思想,代码部分是自己独立实现的,也因此走了一些canvas的坑。
Canvas图形拖动效果展示
主要实现的功能是:
- 鼠标点击会出现红色选中框,鼠标松开选中框消失;
- 鼠标移动,图片也会跟随移动;
- 图片只能在有限空间内移动,无法移动出边界。
实现逻辑
鼠标拖动逻辑
一点个人理解:鼠标事件或者其它浏览器事件都是独立的,但是在开发中用户通常都是执行多个事件的组合。
所以有必要将事件进行组合起来使用,通过使用相应的变量,过滤某些无效的操作。
例如用户在页面上执行
绘制操作:这个过程必然是包括:鼠标按下、鼠标移动、鼠标松开,这个三个完整的事情,且它们都是有序的。
这三种操作的其它任意组合都被认为是无效操作,不应该响应,在程序的执行逻辑中进行过滤。且上述动作在
程序执行过程中的调用表现是:鼠标按下执行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)函数的使用是有条件限制的,有些函数绘制出来的图形是不支持此函数的,所以如果使用错误,可能会出现与预期不符合的情况,会让人很困惑。特别是我,被它困扰了很久。