修正版头像上传组件
- 文章说明
- 核心源码展示
- 运行效果展示
- 源码下载
文章说明
在头像剪切上传一文中,我采用div做裁剪效果,感觉会有一些小问题,在昨天基于canvas绘制的功能中改进了一版,让代码变得更简洁,而且通用性相对高一些,源码及效果展示如下;包含拖拽和调整裁剪框的效果
核心源码展示
主要包括App.vue中的元素和事件,以及Rectangle.js内的绘图方法和鼠标移动事件
App.vue
<script setup>
import {nextTick, reactive} from "vue";
import {Rectangle} from "@/Rectangle";
const data = reactive({
selectFile: false,
imgWidth: 0,
imgHeight: 0,
});
let image;
let leftCanvas;
let leftContext;
let rightCanvas;
let rightContext;
let rect;
let rectangle;
async function selectFile() {
const pickerOpts = {
types: [
{
description: "Images",
accept: {
"image/*": [".png", ".jpeg", ".jpg"],
},
},
],
excludeAcceptAllOption: true,
multiple: false,
};
const fileHandle = await window.showOpenFilePicker(pickerOpts);
const file = await fileHandle[0].getFile();
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = function (e) {
data.src = e.target.result;
data.selectFile = true;
image = new Image();
image.src = e.target.result;
nextTick(() => {
data.imgWidth = image.width;
data.imgHeight = image.height;
nextTick(() => {
leftCanvas = document.getElementsByClassName("left-canvas")[0];
rect = leftCanvas.getBoundingClientRect();
rightCanvas = document.getElementsByClassName("right-canvas")[0];
leftContext = leftCanvas.getContext("2d");
rightContext = rightCanvas.getContext("2d");
rectangle = new Rectangle(data.imgWidth, data.imgHeight);
change = true;
draw();
leftCanvas.onmousedown = (e) => {
omMouseDown(e);
};
leftCanvas.onmousemove = (e) => {
changeSize(e);
};
});
});
};
}
let change;
function draw() {
if (change) {
drawLeftImage(image, rectangle);
drawRightImage(image, rectangle);
change = false;
}
requestAnimationFrame(draw);
}
function drawLeftImage(image, rectangle) {
leftContext.drawImage(image, 0, 0, data.imgWidth, data.imgHeight, 0, 0, data.imgWidth, data.imgHeight);
rectangle.draw(leftContext);
}
function drawRightImage(image, rectangle) {
rightContext.drawImage(image, rectangle.startX, rectangle.startY, rectangle.width, rectangle.height, 0, 0, rightCanvas.width, rightCanvas.height);
}
function omMouseDown(e) {
const clickX = e.clientX - rect.left;
const clickY = e.clientY - rect.top;
const {startX, startY, endX, endY} = rectangle;
const inGap = rectangle.inGap(clickX, clickY);
if (inGap > 0) {
leftCanvas.onmousemove = (e) => {
rectangle.mouseMoveChangeSize(e, rect, startX, startY, endX, endY, clickX, clickY, inGap, leftCanvas);
change = true;
};
} else {
leftCanvas.onmousemove = (e) => {
rectangle.mouseMoveChangePos(e, rect, startX, startY, endX, endY, clickX, clickY, leftCanvas);
change = true;
};
}
window.onmouseup = () => {
leftCanvas.onmousemove = null;
leftCanvas.onmousemove = (e) => {
changeSize(e);
};
};
}
function changeSize(e) {
const clickX = e.clientX - rect.left;
const clickY = e.clientY - rect.top;
const inGap = rectangle.inGap(clickX, clickY);
const {startX, startY, endX, endY} = rectangle;
rectangle.mouseMoveChangeSize(e, rect, startX, startY, endX, endY, clickX, clickY, inGap, leftCanvas);
}
</script>
<template>
<div class="avatar-container">
<div class="img-container">
<div v-show="!data.selectFile" class="select-file" @click="selectFile">
<p>jpg/png file with a size less than 5MB<em>click to upload</em></p>
</div>
<canvas v-show="data.selectFile" :height="data.imgHeight" :width="data.imgWidth" class="left-canvas"></canvas>
</div>
<canvas class="right-canvas" height="100" width="100"></canvas>
</div>
</template>
<style scoped>
.avatar-container {
margin: 0 auto;
width: fit-content;
user-select: none;
display: flex;
justify-content: center;
align-items: center;
padding-top: 100px;
.img-container {
position: relative;
width: fit-content;
.select-file {
width: 500px;
height: 300px;
border: 1px dashed #dcdfe6;
border-radius: 20px;
display: flex;
justify-content: center;
align-items: center;
&:hover {
border: 1px dashed #409eff;
cursor: pointer;
}
p {
font-size: 14px;
color: #606266;
em {
color: #409eff;
font-style: normal;
margin-left: 5px;
}
}
}
}
.left-canvas {
margin-left: 30px;
border: 1px dashed #409eff;
float: left;
}
.right-canvas {
margin-left: 30px;
border: 1px dashed #409eff;
float: left;
}
}
</style>
Rectangle.js
const gap = 10;
const initValue = 100;
export class Rectangle {
constructor(imageWidth, imageHeight) {
const startX = (imageWidth - initValue) / 2;
const startY = (imageHeight - initValue) / 2;
this.startX = startX;
this.startY = startY;
this.endX = this.startX + initValue;
this.endY = this.startY + initValue;
this.imageWidth = imageWidth;
this.imageHeight = imageHeight;
}
get minX() {
return Math.min(this.startX, this.endX);
}
get maxX() {
return Math.max(this.startX, this.endX);
}
get minY() {
return Math.min(this.startY, this.endY);
}
get maxY() {
return Math.max(this.startY, this.endY);
}
get width() {
return this.maxX - this.minX;
}
get height() {
return this.maxY - this.minY;
}
draw(ctx) {
ctx.beginPath();
ctx.moveTo(this.minX, this.minY);
ctx.setLineDash([3, 2]);
ctx.lineTo(this.maxX, this.minY);
ctx.lineTo(this.maxX, this.maxY);
ctx.lineTo(this.minX, this.maxY);
ctx.lineTo(this.minX, this.minY);
ctx.strokeStyle = "#409eff";
ctx.lineWidth = 1;
ctx.lineCap = "square";
ctx.stroke();
}
// 上1、下2、左4、右8
// 得到上1、下2、左4、左上5、左下6、右8、右上9、右下10
inGap(x, y) {
let result = 0;
if (Math.abs(this.minY - y) <= gap && (this.minX - gap <= x && this.maxX + gap >= x)) {
result += 1;
}
if (Math.abs(this.maxY - y) <= gap && (this.minX - gap <= x && this.maxX + gap >= x)) {
result += 2;
}
if (Math.abs(this.minX - x) <= gap && (this.minY - gap <= y && this.maxY + gap >= y)) {
result += 4;
}
if (Math.abs(this.maxX - x) <= gap && (this.minY - gap <= y && this.maxY + gap >= y)) {
result += 8;
}
return result;
}
mouseMoveChangePos(e, rect, startX, startY, endX, endY, clickX, clickY, canvas) {
const disX = e.clientX - rect.left - clickX;
const disY = e.clientY - rect.top - clickY;
if (startX + disX >= 0) {
this.startX = startX + disX;
}
if (endX + disX <= this.imageWidth) {
this.endX = endX + disX;
}
if (startY + disY >= 0) {
this.startY = startY + disY;
}
if (endY + disY <= this.imageHeight) {
this.endY = endY + disY;
}
canvas.style.cursor = "move";
}
mouseMoveChangeSize(e, rect, startX, startY, endX, endY, clickX, clickY, inGap, canvas) {
const disX = e.clientX - rect.left - clickX;
const disY = e.clientY - rect.top - clickY;
if (endX + disX < startX || endY + disY < startY || startX + disX > endX || startY + disY > endY) {
return;
}
switch (inGap) {
case 1:
canvas.style.cursor = "n-resize";
this.startY = startY + disY;
break;
case 2:
canvas.style.cursor = "s-resize";
this.endY = endY + disY;
break;
case 4:
canvas.style.cursor = "w-resize";
this.startX = startX + disX;
break;
case 5:
canvas.style.cursor = "nw-resize";
this.startX = startX + disX;
this.startY = startY + disY;
break;
case 6:
canvas.style.cursor = "sw-resize";
this.startX = startX + disX;
this.endY = endY + disY;
break;
case 8:
canvas.style.cursor = "e-resize";
this.endX = endX + disX;
break;
case 9:
canvas.style.cursor = "ne-resize";
this.endX = endX + disX;
this.startY = startY + disY;
break;
case 10:
canvas.style.cursor = "se-resize";
this.endX = endX + disX;
this.endY = endY + disY;
break;
default:
canvas.style.cursor = "default";
break;
}
}
}
运行效果展示
点击选择图片
可以拖动裁剪框
可以调整裁剪框大小
源码下载
头像上传组件