头像剪切上传
- 文章说明
- 核心Api
- 示例源码
- 效果展示
- 源码下载
文章说明
本文主要为了学习头像裁剪功能,以及熟悉canvas绘图和转文件的相关操作,参考教程(Web渡一前端–图片裁剪上传原理)
核心Api
主要就一个在canvas绘图的操作
context.drawImage(image, imgX, imgY, rectangleWidth, rectangleHeight, 0, 0, canvas.width, canvas.height);
以及canvas转为file对象的操作
let formData = new FormData();
const file = new File([blob], data.selectFileName, {type: data.selectFileType})
formData.append(“file”, file);
关于其中的绘制区域的大小缩放以及移动,也算是一个小难点;一般也有另一种裁剪区域风格,即四条线风格,可通过代码进行理解
示例源码
AvatarUpload.vue
<template>
<div class="container">
<div class="img-container">
<div class="select-file" @click="selectFile" v-if="!data.selectFile">
<p>jpg/png file with a size less than 5MB<em>click to upload</em></p>
</div>
<img alt="" :src="data.src" class="img" v-if="data.selectFile" draggable="false"
:style="{'height' : data.imgHeight }"/>
<div class="rectangle" v-if="data.selectFile" @mousedown="dragStart($event)" @mousemove="changePos($event)"
@mousewheel="wheel($event)"></div>
</div>
<canvas width="100" height="100" class="canvas"></canvas>
<el-button type="primary" class="upload-button" @click="uploadAvatar">上传</el-button>
</div>
</template>
<script>
import {onBeforeUnmount, onMounted, reactive} from "vue";
import {axiosRequest, message} from "@/util/api";
import {MethodType} from "@/util/constant";
export default {
setup: function () {
const data = reactive({
src: null,
imgHeight: "300px",
selectFile: false,
selectFileName: "",
selectFileType: "",
});
async function selectFile() {
const pickerOpts = {
types: [
{
description: "Images",
accept: {
"image/*": [".png", ".jpeg", ".jpg"],
},
},
],
excludeAcceptAllOption: true,
multiple: false,
};
try {
const fileHandle = await window.showOpenFilePicker(pickerOpts);
const file = await fileHandle[0].getFile();
data.selectFileName = file.name;
data.selectFileType = file.type;
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;
setTimeout(() => {
data.imgHeight = image.height + "px";
rectangle = imgContainer.getElementsByClassName("rectangle")[0];
rectangleWidth = rectangle.clientWidth;
rectangleHeight = rectangle.clientHeight;
context.drawImage(image, (image.width / 2 - rectangleWidth / 2), (image.height / 2 - rectangleHeight / 2), rectangleWidth, rectangleHeight, 0, 0, canvas.width, canvas.height);
}, 0);
};
} catch (e) {
if (!(e.name === 'AbortError' && e.message === 'The user aborted a request.')) {
throw e;
}
}
}
let isDragging = false;
let mouseUpListener;
let containerX;
let containerY;
let imgContainer;
let imgWidth;
let imgHeight;
let rectangle;
let initialX;
let initialY;
let rectangleWidth;
let rectangleHeight;
let imgX;
let imgY;
let headerHeight;
let canvas;
let context;
let image;
function dragStart(e) {
isDragging = true;
containerX = imgContainer.offsetLeft;
containerY = imgContainer.offsetTop;
imgWidth = imgContainer.clientWidth;
imgHeight = imgContainer.clientHeight;
initialX = e.offsetX;
initialY = e.offsetY;
rectangle = imgContainer.getElementsByClassName("rectangle")[0];
rectangleWidth = rectangle.clientWidth;
rectangleHeight = rectangle.clientHeight;
}
function changePos(e) {
if (!isDragging) {
return;
}
const x = e.clientX - containerX - initialX + rectangleWidth / 2;
const y = e.clientY - containerY - initialY - headerHeight + rectangleHeight / 2;
if (x >= rectangleWidth / 2 + 3 && x < imgWidth - rectangleWidth / 2 - 2) {
imgX = x - rectangleWidth / 2;
rectangle.style.left = x + "px";
centerX = imgX + rectangleWidth / 2;
context.drawImage(image, imgX, imgY, rectangleWidth, rectangleHeight, 0, 0, canvas.width, canvas.height);
}
if (y >= rectangleHeight / 2 + 3 && y < imgHeight - rectangleHeight / 2 - 4) {
imgY = y - rectangleHeight / 2;
rectangle.style.top = y + "px";
centerY = imgY + rectangleHeight / 2;
context.drawImage(image, imgX, imgY, rectangleWidth, rectangleHeight, 0, 0, canvas.width, canvas.height);
}
}
onMounted(() => {
mouseUpListener = () => {
isDragging = false;
}
document.addEventListener("mouseup", mouseUpListener);
const containerList = document.getElementsByClassName("container");
const container = containerList[containerList.length - 1];
headerHeight = container.parentNode["getBoundingClientRect"]().y;
imgContainer = container.getElementsByClassName("img-container")[0];
canvas = container.getElementsByClassName("canvas")[0];
context = canvas.getContext("2d");
});
onBeforeUnmount(() => {
document.removeEventListener("mouseup", mouseUpListener);
});
const gap = 2;
const minRange = 20;
let centerX;
let centerY;
function wheel(e) {
if (!centerX) {
centerX = image.width / 2;
}
if (!centerY) {
centerY = image.height / 2;
}
if (e.deltaY > 0) {
if (rectangleWidth + gap >= image.width || rectangleHeight + gap >= image.height) {
return;
}
if ((centerX - rectangleWidth / 2 - gap < 0) || (centerY - rectangleHeight / 2 - gap < 0)) {
return;
}
rectangleWidth += gap;
rectangleHeight += gap;
rectangle.style.width = rectangleWidth + "px";
rectangle.style.height = rectangleHeight + "px";
} else {
if (rectangleWidth - gap < minRange || rectangleHeight - gap < minRange) {
return;
}
rectangleWidth -= gap;
rectangleHeight -= gap;
rectangle.style.width = rectangleWidth + "px";
rectangle.style.height = rectangleHeight + "px";
}
context.drawImage(image, (centerX - rectangleWidth / 2), (centerY - rectangleHeight / 2), rectangleWidth, rectangleHeight, 0, 0, canvas.width, canvas.height);
}
function uploadAvatar() {
if (!data.selectFile) {
message("请先选择图片", "info");
return;
}
canvas.toBlob((blob) => {
let formData = new FormData();
const file = new File([blob], data.selectFileName, {type: data.selectFileType})
formData.append("file", file);
axiosRequest(MethodType.post, "/user/uploadAvatar", formData, (res) => {
message(res.data.msg, "info");
});
}, data.selectFileType);
}
return {
data,
selectFile,
dragStart,
changePos,
wheel,
uploadAvatar,
}
}
}
</script>
<style scoped>
.container {
margin: 0 auto;
padding-top: 100px;
width: fit-content;
user-select: none;
display: flex;
justify-content: center;
align-items: center;
}
.img-container {
position: relative;
width: fit-content;
}
.rectangle {
width: 100px;
height: 100px;
border: 1px dashed #409eff;
position: absolute;
left: 50%;
top: 50%;
transform: translateX(-50%) translateY(-50%);
z-index: 999;
box-shadow: #888888 0 0 1px 1px;
cursor: pointer;
}
.select-file {
width: 500px;
height: 300px;
border: 1px dashed #dcdfe6;
border-radius: 20px;
display: flex;
justify-content: center;
align-items: center;
}
.select-file:hover {
border: 1px dashed #409eff;
cursor: pointer;
}
.select-file p {
font-size: 14px;
color: #606266;
}
.select-file p em {
color: #409eff;
font-style: normal;
margin-left: 5px;
}
.img {
border-radius: 20px;
border: 1px dashed #409eff;
}
.canvas {
margin-left: 100px;
border: 1px dashed #409eff;
float: left;
border-radius: 50%;
}
.upload-button {
position: absolute;
width: 180px;
height: 50px;
top: 460px;
font-size: 20px;
}
</style>
效果展示
关于裁剪区域的风格,设置为四条线可移动那种,需要改动一些代码,考虑后续补充
源码下载
参见Gitee链接(WEB-OS-SYSTEM)