效果预览:
👉 项目地址✨
实现[Vue3+TypeScript]
1.创建画布并初始化
<canvas id="canvas" width="500" height="500"></canvas>
初始化画布:
const canvas = ref<HTMLCanvasElement>();
let ctx: CanvasRenderingContext2D;
const width = 500; // 画布的固定宽度
const height = 500; // 画布的固定高度
canvas.value = document.getElementById("canvas") as HTMLCanvasElement;
ctx = canvas.value!.getContext("2d") as CanvasRenderingContext2D;
2.导入猫猫头图片,存储为数组
const imgs = [
{ name: "p1", url: "/img/cat1.png" },
{ name: "p2", url: "/img/cat2.png" },
{ name: "p3", url: "/img/cat3.png" },
];
创建新数组imagesData用来存储猫猫头数据和图片在画布中的位置信息:
const imagesData: {
img: HTMLImageElement; // 图片信息
name: string; // 图片名称
x: number; // x方向位置
y: number; // y方向位置
w: number; // 绘制宽度
h: number; // 绘制高度
}[] = [];
3.将猫猫头随机绘制在画布上
imgs.forEach(item => {
let img = new Image();
img.src = item.url;
const name = item.name;
img.onload = function () {
// 这里取固定宽度100
const w = 100;
const h = (w / img.width) * img.height;
const x = Math.random() * (width - w);
const y = Math.random() * (height - h);
const obj = { img, name, x, y, w, h };
imagesData.push(obj);
drawByObj(obj);
};
});
function drawByObj(obj) {
ctx.drawImage(obj.img, obj.x, obj.y, obj.w, obj.h);
}
4.猫猫头之点击
首先要进行判断,在鼠标点下去时的目标是否为我们绘制的图片,这里可以使用isPointInPath方法。
isPointInPath:
是 Canvas 2D API 用于检测某点是否在路径的描边线上的方法。返回值是一个布尔值,当这个点在路径的描边线上,则返回true,否则返回false。使用方法:context.isPointInPath(x,y)。其中x,y分别是给定点的横、纵坐标。
注意:该方法是针对路径的,比如canvas中的rect、arc方法都可以用,但是fillRect不可以用,因为它不是路径;而且仅对当前的路径有效,而且如果当前路径有多个子路径,则只对第一个子路径有效。
给画布添加鼠标按下事件mousedown,记录当前点击的位置,对该位置进行判断,若该位置是猫猫头,记录下这张图片的在imagesData中的位置target:
let clickCoordinate = { x: 0, y: 0 };
let target;
canvas.value.addEventListener("mousedown", mousedownFn, false);
function mousedownFn(e: MouseEvent) {
clickCoordinate.x = e.pageX - canvas.value!.offsetLeft;
clickCoordinate.y = e.pageY - canvas.value!.offsetTop;
console.log("初次点击位置:" + clickCoordinate.x + "--" + clickCoordinate.y);
checkElement();
if (target == undefined) return;
canvas.value!.addEventListener("mousemove", mousemoveFn, false);
canvas.value!.addEventListener("mouseup", mouseupFn, false);
}
/** 判断点击的是哪个 */
function checkElement() {
imagesData.forEach((item, index) => {
// 绘制辅助路径
drawGuideByObj(item);
if (ctx.isPointInPath(clickCoordinate.x, clickCoordinate.y)) {
console.log("点击的元素是:", item.name);
target = index;
}
});
}
// 根据猫猫头的宽度和高度绘制辅助路径,获取正确的ctx,才能进行判断
function drawGuideByObj(obj: any) {
ctx.beginPath();
ctx.lineWidth = 4;
ctx.strokeStyle = "green";
ctx.rect(obj.x, obj.y, obj.w, obj.h);
ctx.stroke();
}
5.猫猫头之拖拽
结果判断后,如果target的值存在,那么可以对猫猫头进行拖拽,这时需要给画布添加鼠标移动事件mousemove和鼠标抬起mouseup事件。
function mousedownFn(e: MouseEvent) {
clickCoordinate.x = e.pageX - canvas.value!.offsetLeft;
clickCoordinate.y = e.pageY - canvas.value!.offsetTop;
console.log("初次点击位置:" + clickCoordinate.x + "--" + clickCoordinate.y);
checkElement();
if (target == undefined) return;
canvas.value!.addEventListener("mousemove", mousemoveFn, false);
canvas.value!.addEventListener("mouseup", mouseupFn, false);
}
mousemove触发事件:根据MouseEvent的值,更新猫猫头的最新位置,并清空画布重新绘制:
function mousemoveFn(e: MouseEvent) {
// 计算点击位置和图片原点位置的差
let sx = clickCoordinate.x - imagesData[target].x;
let sy = clickCoordinate.y - imagesData[target].y;
// 计算移动元素的坐标
imagesData[target].x = e.pageX - canvas.value!.offsetLeft - sx;
imagesData[target].y = e.pageY - canvas.value!.offsetTop - sy;
// 重新赋值点击位置
clickCoordinate.x = e.pageX - canvas.value!.offsetLeft;
clickCoordinate.y = e.pageY - canvas.value!.offsetTop;
// 清空画布
ctx.clearRect(0, 0, width, height);
// 清空画布以后重新绘制
imagesData.forEach(i => drawByObj(i));
}
mouseup触发事件:鼠标抬起后,移除画布相关事件:
function mouseupFn(e: MouseEvent) {
console.log(e.pageX + "----" + e.pageY);
// 鼠标抬起以后移除事件
canvas.value!.removeEventListener("mousemove", mousemoveFn, false);
canvas.value!.removeEventListener("mouseup", mouseupFn, false);
target = undefined;
}
6.猫猫头之下载
新增下载触发事件:
<div class="btn" @click="save">保存</div>
将canvas存储为本地图片有3步:
- 将canvas转换为base64的url:toDataURL
- 将base64转换为文件对象
- 通过虚拟点击下载文件
代码如下:
const save = () => {
// 将canvas转换成base64的url
let url = canvas.value!.toDataURL("image/png");
imgOnPage.value!.src = url;
// 将base64转换为文件对象
let arr = url.split(",");
let mime = arr[0].match(/:(.*?);/)![1];
let binaryString = window.atob(arr[1]);
let binaryLen = binaryString.length;
let bytes = new Uint8Array(binaryLen);
for (let i = 0; i < bytes.length; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
let file = new File([bytes], "filename", { type: mime });
// 虚拟点击
const link = document.createElement("a");
link.download = file.name;
let href = URL.createObjectURL(file);
link.href = href;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(href);
};