前言
流程如下:本地预览 => 裁剪 => 上传
实现
1. 本地预览
将数据读为 dataurl
赋值给 img 标签的 src
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
.container {
display: flex;
}
#image-container {
position: relative;
width: 400px;
height: 400px;
border: 1px solid #ccc;
background-color: #f3f3f3;
}
#result {
margin-left: 20px;
border: 1px solid #ccc;
width: 200px;
height: 200px;
background-color: #f3f3f3;
}
</style>
</head>
<body>
<div>
<input type="file" id="file-input">
</div>
<div class="container">
<div id="image-container">
<img id="uploaded-image" style="width: 100%; height: 100%;" />
</div>
<canvas id="result" width="200" height="200"></canvas> <!-- 显示裁剪结果的画布 -->
</div>
<script>
const fileInput = document.getElementById('file-input');
const uploadedImage = document.getElementById('uploaded-image');
fileInput.onchange = (e) => {
const file = e.target.files[0];
const reader = new FileReader();
reader.onload = (e) => {
uploadedImage.src = e.target.result;
}
reader.readAsDataURL(file);
}
</script>
</body>
</html>
2. 裁剪
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>图片裁剪预览</title>
<style>
.container {
display: flex;
}
#image-container {
position: relative;
width: 400px;
height: 400px;
border: 1px solid #ccc;
background-color: #f3f3f3;
}
#crop-box {
position: absolute;
border: 2px dashed #ff0000;
cursor: move;
display: none;
}
#crop-box .resize-handle {
position: absolute;
width: 10px;
height: 10px;
background: #ff0000;
z-index: 10;
}
#crop-box .top-left {
left: -5px;
top: -5px;
cursor: nwse-resize;
}
#crop-box .top-right {
right: -5px;
top: -5px;
cursor: nesw-resize;
}
#crop-box .bottom-left {
left: -5px;
bottom: -5px;
cursor: nesw-resize;
}
#crop-box .bottom-right {
right: -5px;
bottom: -5px;
cursor: nwse-resize;
}
#result {
margin-left: 20px;
border: 1px solid #ccc;
width: 200px;
height: 200px;
background-color: #f3f3f3;
}
</style>
</head>
<body>
<div>
<input type="file" id="file-input">
</div>
<div class="container">
<div id="image-container">
<img id="uploaded-image" style="max-width: 100%; max-height: 100%; display: none;" />
<div id="crop-box">
<div class="resize-handle top-left"></div> <!-- 左上角调整手柄 -->
<div class="resize-handle top-right"></div> <!-- 右上角调整手柄 -->
<div class="resize-handle bottom-left"></div> <!-- 左下角调整手柄 -->
<div class="resize-handle bottom-right"></div> <!-- 右下角调整手柄 -->
</div>
</div>
<canvas id="result" width="200" height="200"></canvas> <!-- 显示裁剪结果的画布 -->
</div>
<script>
const fileInput = document.getElementById('file-input');
const uploadedImage = document.getElementById('uploaded-image');
const cropBox = document.getElementById('crop-box');
const resultCanvas = document.getElementById('result');
const ctx = resultCanvas.getContext('2d');
const commitBtn = document.getElementById('commit-btn');
let image = new Image();
let cropInfo = { x: 0, y: 0, width: 100, height: 100 }; // 裁剪框的位置和大小
let scale = 1; // 图片缩放比例
let isDragging = false; // 是否正在拖动裁剪框
let isResizing = false; // 是否正在调整裁剪框大小
let resizingHandle = null; // 当前调整手柄
const MIN_CROP_SIZE = 20; // 裁剪框的最小尺寸
fileInput.addEventListener('change', (event) => {
const file = event.target.files[0];
const reader = new FileReader();
reader.onload = function (e) {
image.src = e.target.result;
uploadedImage.src = e.target.result;
uploadedImage.style.display = 'block';
}
reader.readAsDataURL(file);
});
image.onload = function () {
// 获取容器宽高
const container = document.getElementById('image-container');
const containerWidth = container.offsetWidth;
const containerHeight = container.offsetHeight;
scale = Math.min(containerWidth / image.width, containerHeight / image.height); // 计算缩放比例
uploadedImage.style.width = image.width * scale + 'px'; // 根据缩放比例设置上传图片的宽度
uploadedImage.style.height = image.height * scale + 'px'; // 根据缩放比例设置上传图片的高度
// 重置裁剪框的位置和大小
cropInfo = { x: 0, y: 0, width: 100, height: 100 };
updateCropBox(); // 更新裁剪框位置和大小
cropBox.style.display = 'block'; // 显示裁剪框
drawCrop(); // 绘制裁剪结果
}
// 更新裁剪框的位置和大小
function updateCropBox() {
cropBox.style.left = cropInfo.x + 'px';
cropBox.style.top = cropInfo.y + 'px';
cropBox.style.width = cropInfo.width + 'px';
cropBox.style.height = cropInfo.height + 'px';
}
// 绘制裁剪结果
function drawCrop() {
const cropX = cropInfo.x / scale;
const cropY = cropInfo.y / scale;
const cropWidth = cropInfo.width / scale;
const cropHeight = cropInfo.height / scale;
ctx.clearRect(0, 0, resultCanvas.width, resultCanvas.height); // 清空画布
ctx.drawImage(image, cropX, cropY, cropWidth, cropHeight, 0, 0, resultCanvas.width, resultCanvas.height); // 绘制裁剪的图片
}
cropBox.addEventListener('mousedown', function (e) {
if (e.target.classList.contains('resize-handle')) {
// 如果按下的是调整手柄
isResizing = true;
resizingHandle = e.target; // 当前调整手柄
} else {
// 如果按下的是裁剪框,记录鼠标相对裁剪框的坐标
isDragging = true;
offsetX = e.offsetX;
offsetY = e.offsetY;
}
document.addEventListener('mousemove', mouseMove); // 添加鼠标移动事件
document.addEventListener('mouseup', mouseUp); // 添加鼠标抬起事件
});
function mouseMove(e) {
if (isDragging) {
// 正在拖动裁剪框,更新裁剪框坐标
cropInfo.x = e.clientX - offsetX - image.getBoundingClientRect().left;
cropInfo.y = e.clientY - offsetY - image.getBoundingClientRect().top;
updateCropBox(); // 更新裁剪框
drawCrop(); // 绘制裁剪结果
}
if (isResizing) {
// 正在调整裁剪框大小
const handleClass = resizingHandle.classList[1]; // 获取调整手柄的类名
if (handleClass.includes('top-left')) {
// 左上角手柄
const newWidth = cropInfo.width + (cropInfo.x - e.clientX + image.getBoundingClientRect().left);
const newHeight = cropInfo.height + (cropInfo.y - e.clientY + image.getBoundingClientRect().top);
// 如果新宽高合理,则更新裁剪框位置
if (newWidth > MIN_CROP_SIZE) {
cropInfo.x = e.clientX - image.getBoundingClientRect().left;
cropInfo.width = newWidth;
}
if (newHeight > MIN_CROP_SIZE) {
cropInfo.y = e.clientY - image.getBoundingClientRect().top;
cropInfo.height = newHeight;
}
} else if (handleClass.includes('top-right')) {
// 右上角手柄
const newWidth = e.clientX - cropInfo.x - image.getBoundingClientRect().left;
const newHeight = cropInfo.height + (cropInfo.y - e.clientY + image.getBoundingClientRect().top);
// 如果新宽高合理,则更新裁剪框位置
if (newWidth > MIN_CROP_SIZE) {
cropInfo.width = newWidth;
}
if (newHeight > MIN_CROP_SIZE) {
cropInfo.y = e.clientY - image.getBoundingClientRect().top;
cropInfo.height = newHeight;
}
} else if (handleClass.includes('bottom-left')) {
// 左下角手柄
const newWidth = cropInfo.width + (cropInfo.x - e.clientX + image.getBoundingClientRect().left);
const newHeight = e.clientY - cropInfo.y - image.getBoundingClientRect().top;
// 如果新宽高合理,则更新裁剪框位置
if (newWidth > MIN_CROP_SIZE) {
cropInfo.x = e.clientX - image.getBoundingClientRect().left;
cropInfo.width = newWidth;
}
if (newHeight > MIN_CROP_SIZE) {
cropInfo.height = newHeight;
}
} else if (handleClass.includes('bottom-right')) {
// 右下角手柄
const newWidth = e.clientX - cropInfo.x - image.getBoundingClientRect().left;
const newHeight = e.clientY - cropInfo.y - image.getBoundingClientRect().top;
// 如果新宽高合理,则更新裁剪框位置
if (newWidth > MIN_CROP_SIZE) {
cropInfo.width = newWidth;
}
if (newHeight > MIN_CROP_SIZE) {
cropInfo.height = newHeight;
}
}
updateCropBox(); // 更新裁剪框
drawCrop(); // 绘制裁剪结果
}
}
function mouseUp() {
isDragging = false; // 停止拖动
isResizing = false; // 停止调整大小
document.removeEventListener('mousemove', mouseMove); // 移除鼠标移动事件
document.removeEventListener('mouseup', mouseUp); // 移除鼠标抬起事件
}
</script>
</body>
</html>
3. 上传
通过 canvas
生成 blob
再变为 File
resultCanvas.toBlob((blob) => {
const file = new File([blob], 'cut.png', { type: 'image/png' });
console.log(file); // 执行上传逻辑即可
})
整体代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>图片裁剪预览</title>
<style>
.container {
display: flex;
}
#image-container {
position: relative;
width: 400px;
height: 400px;
border: 1px solid #ccc;
background-color: #f3f3f3;
}
#crop-box {
position: absolute;
border: 2px dashed #ff0000;
cursor: move;
display: none;
}
#crop-box .resize-handle {
position: absolute;
width: 10px;
height: 10px;
background: #ff0000;
z-index: 10;
}
#crop-box .top-left {
left: -5px;
top: -5px;
cursor: nwse-resize;
}
#crop-box .top-right {
right: -5px;
top: -5px;
cursor: nesw-resize;
}
#crop-box .bottom-left {
left: -5px;
bottom: -5px;
cursor: nesw-resize;
}
#crop-box .bottom-right {
right: -5px;
bottom: -5px;
cursor: nwse-resize;
}
#result {
margin-left: 20px;
border: 1px solid #ccc;
width: 200px;
height: 200px;
background-color: #f3f3f3;
}
#commit-btn {
margin-left: 20px;
border: 1px solid #ccc;
width: 60px;
height: 30px;
}
</style>
</head>
<body>
<div>
<input type="file" id="file-input">
</div>
<div class="container">
<div id="image-container">
<img id="uploaded-image" style="max-width: 100%; max-height: 100%; display: none;" />
<div id="crop-box">
<div class="resize-handle top-left"></div> <!-- 左上角调整手柄 -->
<div class="resize-handle top-right"></div> <!-- 右上角调整手柄 -->
<div class="resize-handle bottom-left"></div> <!-- 左下角调整手柄 -->
<div class="resize-handle bottom-right"></div> <!-- 右下角调整手柄 -->
</div>
</div>
<canvas id="result" width="200" height="200"></canvas> <!-- 显示裁剪结果的画布 -->
<button id="commit-btn">上传</button>
</div>
<script>
const fileInput = document.getElementById('file-input');
const uploadedImage = document.getElementById('uploaded-image');
const cropBox = document.getElementById('crop-box');
const resultCanvas = document.getElementById('result');
const ctx = resultCanvas.getContext('2d');
const commitBtn = document.getElementById('commit-btn');
let image = new Image();
let cropInfo = { x: 0, y: 0, width: 100, height: 100 }; // 裁剪框的位置和大小
let scale = 1; // 图片缩放比例
let isDragging = false; // 是否正在拖动裁剪框
let isResizing = false; // 是否正在调整裁剪框大小
let resizingHandle = null; // 当前调整手柄
const MIN_CROP_SIZE = 20; // 裁剪框的最小尺寸
fileInput.addEventListener('change', (event) => {
const file = event.target.files[0];
const reader = new FileReader();
reader.onload = function (e) {
image.src = e.target.result;
uploadedImage.src = e.target.result;
uploadedImage.style.display = 'block';
}
reader.readAsDataURL(file);
});
image.onload = function () {
// 获取容器宽高
const container = document.getElementById('image-container');
const containerWidth = container.offsetWidth;
const containerHeight = container.offsetHeight;
scale = Math.min(containerWidth / image.width, containerHeight / image.height); // 计算缩放比例
uploadedImage.style.width = image.width * scale + 'px'; // 根据缩放比例设置上传图片的宽度
uploadedImage.style.height = image.height * scale + 'px'; // 根据缩放比例设置上传图片的高度
// 重置裁剪框的位置和大小
cropInfo = { x: 0, y: 0, width: 100, height: 100 };
updateCropBox(); // 更新裁剪框位置和大小
cropBox.style.display = 'block'; // 显示裁剪框
drawCrop(); // 绘制裁剪结果
}
// 更新裁剪框的位置和大小
function updateCropBox() {
cropBox.style.left = cropInfo.x + 'px';
cropBox.style.top = cropInfo.y + 'px';
cropBox.style.width = cropInfo.width + 'px';
cropBox.style.height = cropInfo.height + 'px';
}
// 绘制裁剪结果
function drawCrop() {
const cropX = cropInfo.x / scale;
const cropY = cropInfo.y / scale;
const cropWidth = cropInfo.width / scale;
const cropHeight = cropInfo.height / scale;
ctx.clearRect(0, 0, resultCanvas.width, resultCanvas.height); // 清空画布
ctx.drawImage(image, cropX, cropY, cropWidth, cropHeight, 0, 0, resultCanvas.width, resultCanvas.height); // 绘制裁剪的图片
}
cropBox.addEventListener('mousedown', function (e) {
if (e.target.classList.contains('resize-handle')) {
// 如果按下的是调整手柄
isResizing = true;
resizingHandle = e.target; // 当前调整手柄
} else {
// 如果按下的是裁剪框,记录鼠标相对裁剪框的坐标
isDragging = true;
offsetX = e.offsetX;
offsetY = e.offsetY;
}
document.addEventListener('mousemove', mouseMove); // 添加鼠标移动事件
document.addEventListener('mouseup', mouseUp); // 添加鼠标抬起事件
});
function mouseMove(e) {
if (isDragging) {
// 正在拖动裁剪框,更新裁剪框坐标
cropInfo.x = e.clientX - offsetX - image.getBoundingClientRect().left;
cropInfo.y = e.clientY - offsetY - image.getBoundingClientRect().top;
updateCropBox(); // 更新裁剪框
drawCrop(); // 绘制裁剪结果
}
if (isResizing) {
// 正在调整裁剪框大小
const handleClass = resizingHandle.classList[1]; // 获取调整手柄的类名
if (handleClass.includes('top-left')) {
// 左上角手柄
const newWidth = cropInfo.width + (cropInfo.x - e.clientX + image.getBoundingClientRect().left);
const newHeight = cropInfo.height + (cropInfo.y - e.clientY + image.getBoundingClientRect().top);
// 如果新宽高合理,则更新裁剪框位置
if (newWidth > MIN_CROP_SIZE) {
cropInfo.x = e.clientX - image.getBoundingClientRect().left;
cropInfo.width = newWidth;
}
if (newHeight > MIN_CROP_SIZE) {
cropInfo.y = e.clientY - image.getBoundingClientRect().top;
cropInfo.height = newHeight;
}
} else if (handleClass.includes('top-right')) {
// 右上角手柄
const newWidth = e.clientX - cropInfo.x - image.getBoundingClientRect().left;
const newHeight = cropInfo.height + (cropInfo.y - e.clientY + image.getBoundingClientRect().top);
// 如果新宽高合理,则更新裁剪框位置
if (newWidth > MIN_CROP_SIZE) {
cropInfo.width = newWidth;
}
if (newHeight > MIN_CROP_SIZE) {
cropInfo.y = e.clientY - image.getBoundingClientRect().top;
cropInfo.height = newHeight;
}
} else if (handleClass.includes('bottom-left')) {
// 左下角手柄
const newWidth = cropInfo.width + (cropInfo.x - e.clientX + image.getBoundingClientRect().left);
const newHeight = e.clientY - cropInfo.y - image.getBoundingClientRect().top;
// 如果新宽高合理,则更新裁剪框位置
if (newWidth > MIN_CROP_SIZE) {
cropInfo.x = e.clientX - image.getBoundingClientRect().left;
cropInfo.width = newWidth;
}
if (newHeight > MIN_CROP_SIZE) {
cropInfo.height = newHeight;
}
} else if (handleClass.includes('bottom-right')) {
// 右下角手柄
const newWidth = e.clientX - cropInfo.x - image.getBoundingClientRect().left;
const newHeight = e.clientY - cropInfo.y - image.getBoundingClientRect().top;
// 如果新宽高合理,则更新裁剪框位置
if (newWidth > MIN_CROP_SIZE) {
cropInfo.width = newWidth;
}
if (newHeight > MIN_CROP_SIZE) {
cropInfo.height = newHeight;
}
}
updateCropBox(); // 更新裁剪框
drawCrop(); // 绘制裁剪结果
}
}
function mouseUp() {
isDragging = false; // 停止拖动
isResizing = false; // 停止调整大小
document.removeEventListener('mousemove', mouseMove); // 移除鼠标移动事件
document.removeEventListener('mouseup', mouseUp); // 移除鼠标抬起事件
}
commitBtn.addEventListener('click', function () {
resultCanvas.toBlob((blob) => {
const file = new File([blob], 'cut.png', { type: 'image/png' });
console.log(file); // 执行上传逻辑即可
})
})
</script>
</body>
</html>