Canvas拖动图形实现效果完整版
上次那个博客实现只能支持单张图片,并且代码的逻辑不够清晰。所以中秋节,正好一个人也没事做,专门抽出时间来对它进行一个扩展,添加对多张图片的支持。
效果展示
下面我们先来看看实现的效果,这里先提一点:当你移动图片时,注意它是在所有图片的最下层,这是我的技术实现的特点。
1.GIF动图
2.B站视频
canvas图片拖动
实现分析
多张图片移动实现分析
所有的图片存储在一个数组中,使用一个current变量存储当前选中的图片(初始值为-1,即未选中)。当用户开始点击画板时,记录下此时鼠标的位置。当鼠标在画板上进行移动时,首先清空整个画板,然后依次绘制每一张图片和外围的矩形框,并且在绘制完成之后,立刻判断鼠标是否点击到了这张图片上(矩形框内部)。如果鼠标在某个矩形框内部,说明它选中了某张图片。 然后交换它和数组第一个元素的位置,这样它就变成了整个数组的第一个元素了。然后重新绘制所有的图形,由于被选中的图片是数组中的第一个元素,所以它是最先绘制的。这样呈现的效果就是你会看到它在所有图片的底部。之所以采用这种方式进行处理,是因为如果它在其它图片的上层时,当鼠标点击的位置位于两个图片的层叠处,两张图片会出现同时移动的问题。所以,我采用了这种方式进行规避,不过最终的效果也是不错的,你只是看到它处于其它图片的底层而已。
多张图片移动边界限制分析
上一个博客中,我实现了图片不能移动出边界的功能。这里也是要加上的,你可能觉得图片变多了,不好分析了。但是,其实还是很简单的。我的图片数组会存储每一个图片的坐上顶点坐标,并且图片的宽高我们也是知道的(图片元素自身会存储这个值)。因此,当我们鼠标点击某张图片时,我们也就可以算出来图片的右下顶点了。所以我们其实是时刻知道某张图片的四个顶点的位置的,但是我们只需要知道(x1, y1)和 (x3,y3)。并且我们只需要判断这个四个数字的取值值即可。在上一篇博客是判断一个点的位置不能在某个位置,这其实是一种组合判断,我昨天发现其实只要单独判断就行了。
所以最终的判断代码大大简化了:
// 判断位置,当点越界时,进行处理
function judgePosition(obj) {
// 这里只需要考虑点的坐标值即可,两个点,4个坐标判断四种情况。
obj.x = obj.x < 0 ? 0 : (obj.x + obj.img.width) < width ? obj.x : width-obj.img.width;
obj.y = obj.y < 0 ? 0 : (obj.y + obj.img.height) < height ? obj.y : height-obj.img.height;
}
或者一个更容易理解的写法:
// 判断位置,当点越界时,进行处理
function judgePosition(obj) {
// 这里只需要考虑点的坐标值即可,两个点,4个坐标判断四种情况。
let x3 = obj.x + obj.img.width;
let y3 = obj.y + obj.img.height;
if (obj.x < 0) {
obj.x = 0;
}
if (obj.y < 0) {
obj.y = 0;
}
if (x3 > width) {
obj.x = width-obj.img.width;
}
if (y3 > height) {
obj.y = height-obj.img.height;
}
}
完整代码
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>中秋快乐!</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">
'use strict'
let canvas = document.getElementById("cs");
// 获取canvas的宽高
let width = canvas.width;
let height = canvas.height;
let ctx = canvas.getContext("2d");
// 这里使用默认值即可
//ctx.globalCompositeOperation = "source-over";
let now = {x: 0, y: 0}; // 鼠标的位置
let isDown = false;
let current = -1;
let rewidth = 50; // 重定义图片的宽度
let imgs = [];
// 创建img元素
Array.from({length: 40}, (a, i)=>i).forEach(index => {
let img = document.createElement("img")
img.src = "doge.png";
// 当图片加载完成之后,将其绘制到canvas上,图片没有加载完成时,
// 图片的宽高是0,因为这时候图片其实是空的
img.onload = () => {
let h = resize(img, rewidth);
img.width = rewidth;
img.height = h;
let obj = {img: img, x: 0, y: 0};
console.log(obj);
imgs.push(obj); // 每张图片的默认位置
drawCanvas(obj, "#FFFFFF");
};
});
// 添加鼠标事情,来实现图片的拖放功能。
// 当鼠标按下时,获取当前的坐标
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);
// 这里取消了点击产生选中框的逻辑
};
// 在鼠标移动时,不断重绘制整个canvas
canvas.onmousemove = e => {
if (!isDown) { // 鼠标未按下则直接返回,不去响应该事件。
return;
}
// 获取点的坐标可以封装成函数
let x = e.pageX;
let y = e.pageY;
// 清空当前的canvas图形
ctx.clearRect(0, 0, canvas.width, canvas.height);
imgs.forEach((obj, index) => {
drawCanvas(obj, "#FFFFFF");
if (!ctx.isPointInPath(x, y)) {
console.log("onmousemove: 鼠标没在路径内");
return;
}
// 更新current
if (current == -1) {
current = index;
}
if (current == index) {
imgs[current].x = imgs[current].x + (x-now.x);
imgs[current].y = imgs[current].y + (y-now.y);
// 判断并即时矫正图片的坐标
judgePosition(imgs[current]);
if (current != 0) { // 这里去掉也是可以的
let tobj = imgs[0];
imgs[0] = imgs[current];
imgs[current] = tobj;
}
current = 0;
}
console.log(imgs);
// 重绘制偏移之后的canvas
imgs.forEach(obj => drawCanvas(obj, "#FFFFFF"));
ctx.beginPath();
ctx.strokeStyle="red";
ctx.rect(imgs[index].x, imgs[index].y, imgs[index].img.width, imgs[index].img.height);
ctx.stroke();
now.x = x; now.y = y;
console.log("鼠标在移动..." + x + " ==> " + y, obj.img);
});
}
canvas.onmouseup = e => {
isDown = false; // 鼠标松开,则上述封装的动作结束。
current = -1;
console.log("鼠标松开, current: ", current);
imgs.forEach(obj => drawCanvas(obj, "#FFFFFF"));
}
// 如果鼠标按下然后移动的过程中离开了当前元素,再松开,但是无法触发鼠标松开事件了,
// 所以当监听到鼠标移出元素时,必须也要将isDown设置成false。
canvas.onmouseout = e => {
isDown = false;
current = -1;
console.log("鼠标离开了画布元素, current: ", current);
imgs.forEach(obj => drawCanvas(obj, "#FFFFFF"));
}
function drawCanvas(obj, color) {
// 绘制所有的图片,这里的宽高是缩放后的宽高
ctx.drawImage(obj.img, obj.x, obj.y, obj.img.width, obj.img.height);
ctx.beginPath();
ctx.strokeStyle = color;
ctx.rect(obj.x, obj.y, obj.img.width, obj.img.height);
ctx.stroke();
}
// 根据传如的指定宽度,自动调整高度
function resize(img, width) {
let w = img.width;
let h = img.height;
return parseInt(h*width/w);
}
// 判断位置,当点越界时,进行处理
function judgePosition(obj) {
// 这里只需要考虑点的坐标值即可,两个点,4个坐标判断四种情况。
obj.x = obj.x < 0 ? 0 : (obj.x + obj.img.width) < width ? obj.x : width-obj.img.width;
obj.y = obj.y < 0 ? 0 : (obj.y + obj.img.height) < height ? obj.y : height-obj.img.height;
}
</script>
</body>
</html>