目录
1、演示
2、实现原理
这里有详细介绍: 前端开发攻略---图片裁剪上传的原理-CSDN博客
3、实现功能
上传图像:
- 用户选择文件后,
changeFile
方法读取文件内容并将其转换为 Data URL,这样可以在页面上显示图像。裁剪区域显示与交互:
- 使用
cutContainerStyle
计算并设置裁剪区域的样式(位置和尺寸)。- 通过
handleMouseDown
、handleMouseMove
和handleMouseUp
方法处理用户对裁剪区域的拖动操作。elMapFn
对象包含处理不同裁剪区域调整(例如四角、边界点)的具体逻辑。@mousemove
和@wheel
事件处理函数允许用户在调整裁剪区域的同时进行拖动和缩放操作。裁剪图像:
cutImageBtn
方法通过以下步骤裁剪图像:
- 获取裁剪参数:获取裁剪区域的位置信息和尺寸。
- 计算缩放比例:根据图像的显示尺寸和原始尺寸计算缩放比例。
- 裁剪图像:创建一个 canvas 元素,使用
drawImage
方法在 canvas 上绘制裁剪区域的图像部分。- 生成裁剪后的图像:将 canvas 内容转换为 Blob 对象,再通过 FileReader 转换为 Data URL,显示在页面上。
4、代码
本案例采用Vue3框架实现。可以直接复制全部代码,放到一个干净的vue文件中即可
<template>
<div class="app" @mouseup="handleMouseUp">
<div>
<input type="file" @change="changeFile" /> <br />
<br />
<button @click="cutImageBtn" v-if="originImage">确认裁剪</button>
<br />
<img :src="cutAfterUrl" alt="" />
</div>
<div class="previewContainer" ref="previewContainer" @mousemove="handleMouseMove" v-if="originImage" @wheel="handleWheel">
<img :src="originImage" alt="" ref="originImageEl" />
<div
class="cutContainer"
@mousedown="handleMouseDown"
action="cutContainer"
ref="cutContainer"
:style="{
transform: `translateX(${cutContainerStyle.translateX}px) translateY(${cutContainerStyle.translateY}px)`,
width: `${cutContainerStyle.width}px`,
height: `${cutContainerStyle.height}px`,
}"
>
<img
:src="originImage"
alt=""
:style="{
transform: `translateX(-${cutContainerStyle.translateX}px) translateY(-${cutContainerStyle.translateY}px)`,
}"
/>
<span class="point lt" action="lt"></span>
<span class="point rt" action="rt"></span>
<span class="point lb" action="lb"></span>
<span class="point rb" action="rb"></span>
<span class="point tm" action="tm"></span>
<span class="point rm" action="rm"></span>
<span class="point bm" action="bm"></span>
<span class="point lm" action="lm"></span>
</div>
</div>
</div>
</template>
<script setup>
import { ref, reactive } from 'vue'
const originImage = ref('')
const originImageEl = ref(null)
const cutContainer = ref(null)
const cutAfterUrl = ref('')
const previewContainer = ref(null)
const cutContainerStyle = reactive({
translateX: 50,
translateY: 50,
width: 400,
height: 400,
})
const handleMouseUp = () => {
elMapFn.isMove = false
}
const handleMouseDown = e => {
e.preventDefault()
elMapFn.isMove = true
elMapFn.clickX = e.offsetX
elMapFn.clickY = e.offsetY
elMapFn.clickTarget = e.target.getAttribute('action')
}
const elMapFn = {
isMove: false,
clickX: 0,
clickY: 0,
clickTarget: null,
cutContainer: function (e) {
const { left, top, width, height } = previewContainer.value.getBoundingClientRect()
let x = e.clientX - left - this.clickX
let y = e.clientY - top - this.clickY
if (y <= 0) {
y = 0
}
if (x <= 0) {
x = 0
}
if (x >= width - cutContainer.value.offsetWidth) {
x = width - cutContainer.value.offsetWidth
}
if (y >= height - cutContainer.value.offsetHeight) {
y = height - cutContainer.value.offsetHeight
}
cutContainerStyle.translateX = x
cutContainerStyle.translateY = y
},
lt: function (e) {
const { left, top, width, height } = previewContainer.value.getBoundingClientRect()
let y = e.clientY - top - this.clickY
let h = height - y - (height - cutContainerStyle.translateY - cutContainerStyle.height)
let x = e.clientX - left
let w = width - x - (width - cutContainerStyle.translateX - cutContainerStyle.width)
cutContainerStyle.translateX = x
cutContainerStyle.width = w
cutContainerStyle.translateY = y
cutContainerStyle.height = h
},
rt: function (e) {
const { left, top, height } = previewContainer.value.getBoundingClientRect()
let w = e.clientX - left - cutContainerStyle.translateX
let y = e.clientY - top - this.clickY
let h = height - y - (height - cutContainerStyle.translateY - cutContainerStyle.height)
cutContainerStyle.translateY = y
cutContainerStyle.width = w
cutContainerStyle.height = h
},
lb: function (e) {
const { left, top, width } = previewContainer.value.getBoundingClientRect()
let x = e.clientX - left
let w = width - x - (width - cutContainerStyle.translateX - cutContainerStyle.width)
let h = e.clientY - top - cutContainerStyle.translateY
cutContainerStyle.translateX = x
cutContainerStyle.width = w
cutContainerStyle.height = h
},
rb: function (e) {
const { left, top } = previewContainer.value.getBoundingClientRect()
let w = e.clientX - left - cutContainerStyle.translateX
let h = e.clientY - top - cutContainerStyle.translateY
cutContainerStyle.width = w
cutContainerStyle.height = h
},
tm: function (e) {
const { top, height } = previewContainer.value.getBoundingClientRect()
let y = e.clientY - top - this.clickY
let h = height - y - (height - cutContainerStyle.translateY - cutContainerStyle.height)
cutContainerStyle.translateY = y
cutContainerStyle.height = h
},
rm: function (e) {
const { left } = previewContainer.value.getBoundingClientRect()
let w = e.clientX - left - cutContainerStyle.translateX
cutContainerStyle.width = w
},
bm: function (e) {
const { top } = previewContainer.value.getBoundingClientRect()
let h = e.clientY - top - cutContainerStyle.translateY
cutContainerStyle.height = h
},
lm: function (e) {
const { left, top, height, width } = previewContainer.value.getBoundingClientRect()
let x = e.clientX - left
let w = width - x - (width - cutContainerStyle.translateX - cutContainerStyle.width)
cutContainerStyle.translateX = x
cutContainerStyle.width = w
},
}
const handleMouseMove = e => {
if (!elMapFn.isMove || !elMapFn.clickTarget) return
elMapFn[elMapFn.clickTarget] && elMapFn[elMapFn.clickTarget](e)
}
const changeFile = e => {
const file = e.target.files[0]
const fileReader = new FileReader()
fileReader.onload = e => {
originImage.value = e.target.result
}
fileReader.readAsDataURL(file)
}
const cutImageBtn = () => {
const { translateX, translateY, width, height } = cutContainerStyle
// 获取用户输入的裁剪位置
const x = parseInt(translateX, 10)
const y = parseInt(translateY, 10)
const w = parseInt(width, 10)
const h = parseInt(height, 10)
// 获取图片的显示尺寸
const displayWidth = originImageEl.value.clientWidth
const displayHeight = originImageEl.value.clientHeight
// 获取图片的原始尺寸
const originalWidth = originImageEl.value.naturalWidth
const originalHeight = originImageEl.value.naturalHeight
// 计算缩放比例
const xScale = originalWidth / displayWidth
const yScale = originalHeight / displayHeight
// 将裁剪位置转换为原始图像上的坐标
const cropX = x * xScale
const cropY = y * yScale
const cropW = w * xScale
const cropH = h * yScale
// 创建一个 canvas 元素来进行裁剪
const cvs = document.createElement('canvas')
const ctx = cvs.getContext('2d')
// 设置 canvas 大小与图像相同
cvs.width = originalWidth
cvs.height = originalHeight
// 将图像绘制到 canvas 上
ctx.drawImage(originImageEl.value, cropX, cropY, cropW, cropH, 0, 0, cvs.width, cvs.height)
cvs.toBlob(blob => {
// 拿到file对象 可以将裁剪好后的图片上传服务器
const file = new File([blob], 'cut-png', { type: 'image/png' })
const fileReader = new FileReader()
fileReader.onload = e => {
cutAfterUrl.value = e.target.result
}
fileReader.readAsDataURL(file)
})
}
</script>
<style scoped>
.app {
width: 100vw;
height: 100vh;
background-color: saddlebrown;
display: flex;
justify-content: center;
align-items: center;
}
#cvs {
width: 500px;
height: 500px;
border: 1px solid red;
}
.previewContainer {
width: 500px;
height: 500px;
background-color: #000;
box-shadow: rgba(0, 0, 0, 0.2) 0px 8px 24px;
overflow: hidden;
position: relative;
user-select: none;
}
.previewContainer > img {
position: absolute;
width: 500px;
height: 500px;
opacity: 0.5;
}
.previewContainer .cutContainer {
width: 400px;
height: 400px;
border: 1px solid #266fff;
overflow: hidden;
position: relative;
}
.cutContainer:hover {
cursor: move;
}
.cutContainer > img {
position: absolute;
width: 500px;
height: 500px;
z-index: -100;
}
.cutContainer::before {
content: '';
position: absolute;
width: calc(100% / 3);
height: 100%;
border-left: 1px dashed #eee;
border-right: 1px dashed #eee;
left: 50%;
margin-left: calc(100% / -3 / 2);
}
.cutContainer::after {
content: '';
position: absolute;
width: 100%;
height: calc(100% / 3);
border-top: 1px dashed #eee;
border-bottom: 1px dashed #eee;
top: 50%;
margin-top: calc(100% / -3 / 2);
z-index: 999;
}
.point {
display: inline-block;
position: absolute;
width: 5px;
height: 5px;
background-color: #266fff;
z-index: 9999;
}
.point.lt {
left: 0;
top: 0;
}
.point.tm {
left: 50%;
transform: translateX(-50%);
}
.point.rt {
right: 0;
}
.point.rm {
right: 0;
top: 50%;
transform: translateY(-50%);
}
.point.rb {
right: 0;
bottom: 0;
}
.point.bm {
bottom: 0;
left: 50%;
transform: translateX(-50%);
}
.point.lb {
bottom: 0;
left: 0;
}
.point.lm {
left: 0;
top: 50%;
transform: translateY(-50%);
}
.point.tm:hover,
.point.bm:hover {
cursor: s-resize;
}
.point.lm:hover,
.point.rm:hover {
cursor: w-resize;
}
.point.lt:hover,
.point.rb:hover {
cursor: se-resize;
}
.point.rt:hover,
.point.lb:hover {
cursor: ne-resize;
}
</style>