图片裁剪是现代 Web 应用中常见的功能需求,无论是社交媒体平台、电商网站还是个人博客,用户经常需要对上传的图片进行裁剪以获得最佳显示效果。本文将介绍如何使用纯前端技术实现一个功能完整的图片裁剪工具,无需后端支持即可在浏览器中完成图片裁剪操作。
实现原理概述
我们的图片裁剪工具基于 HTML5、CSS3 和原生 JavaScript 实现,主要利用以下技术特性:
- 使用 HTML5 的 File API 处理图片上传
- 通过 CSS 的 clip-path 属性实现图片裁剪效果预览
- 使用 Canvas API 生成最终裁剪结果
- 结合鼠标事件实现裁剪框的拖拽和缩放交互
实现效果展示
完整功能代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>图片裁剪demo</title>
<style type="text/css">
body {
background: #555;
}
#box {
position: relative;
}
#img1 {
opacity: 0.6;
position: absolute;
top: 0;
left: 0;
user-select: none;
}
#img2 {
opacity: 1;
position: absolute;
top: 0;
left: 0;
user-select: none;
/* clip: rect(0px,200px,200px,0px); */
clip-path: polygon(0px 0px,
/* 左上角 */
200px 0px,
/* 右上角 */
200px 200px,
/* 右下角 */
0px 200px
/* 左下角 */
);
}
#main {
position: absolute;
width: 200px;
height: 200px;
cursor: move;
display: none;
}
.minDiv {
position: absolute;
width: 8px;
height: 8px;
background: #fff;
}
.left-up {
top: -4px;
left: -4px;
cursor: nw-resize;
}
.up {
left: 50%;
margin-left: -4px;
margin-top: -4px;
cursor: n-resize;
}
.right-up {
right: -4px;
top: -4px;
cursor: ne-resize;
}
.right {
right: -4px;
top: 50%;
margin-top: -4px;
cursor: e-resize;
}
.right-down {
right: -4px;
bottom: -4px;
cursor: se-resize;
}
.down {
bottom: -4px;
right: 50%;
margin-left: -4px;
cursor: s-resize;
}
.left-down {
left: -4px;
bottom: -4px;
cursor: sw-resize;
}
.left {
left: -4px;
top: 50%;
margin-top: -4px;
cursor: w-resize;
}
</style>
</head>
<body>
<div class="contain">
<input id="imgFile" type="file" name="imgFile">
<button id="preview">预览裁剪结果</button>
<div id="box">
<img src="" id="img1">
<img src="" id="img2">
<div id="main">
<div class="minDiv left-up"></div>
<div class="minDiv up"></div>
<div class="minDiv right-up"></div>
<div class="minDiv right"></div>
<div class="minDiv right-down"></div>
<div class="minDiv down"></div>
<div class="minDiv left-down"></div>
<div class="minDiv left"></div>
</div>
</div>
</div>
</body>
<script>
const doms = {
imgFile: document.getElementById('imgFile'),
preview: document.getElementById('preview'),
box: document.getElementById('box'),
img1: document.getElementById('img1'),
img2: document.getElementById('img2'),
main: document.getElementById('main'),
leftUp: document.querySelector('.left-up'),
up: document.querySelector('.up'),
rightUp: document.querySelector('.right-up'),
right: document.querySelector('.right'),
rightDown: document.querySelector('.right-down'),
down: document.querySelector('.down'),
leftDown: document.querySelector('.left-down'),
left: document.querySelector('.left')
}
const dirDoms = [
doms.leftUp,
doms.up,
doms.rightUp,
doms.right,
doms.rightDown,
doms.down,
doms.leftDown,
doms.left
]
// 封装:更新裁剪框位置和大小(带边界检查)
function updateCropBoxPosition(left, top, width, height) {
// console.log('left, top, width, height', left, top, width, height);
// 边界检查
const imgWidth = doms.img1.offsetWidth;
const imgHeight = doms.img1.offsetHeight;
// 设置最小尺寸限制
const minWidth = 60;
const minHeight = 60;
// 确保宽度和高度不小于最小值
if (width < minWidth) width = minWidth;
if (height < minHeight) height = minHeight;
// 防止左侧溢出
if (left < 0) {
left = 0;
}
// 防止右侧溢出
if (left + width > imgWidth) {
// 尝试调整位置保持宽度
if (width <= imgWidth) {
left = imgWidth - width;
} else {
// 如果宽度大于图片宽度,限制宽度并居中显示
width = imgWidth;
left = 0;
}
}
// 防止顶部溢出
if (top < 0) {
top = 0;
}
// 防止底部溢出
if (top + height > imgHeight) {
// 尝试调整位置保持高度
if (height <= imgHeight) {
top = imgHeight - height;
} else {
// 如果高度大于图片高度,限制高度并居中显示
height = imgHeight;
top = 0;
}
}
// 设置裁剪框样式
doms.main.style.left = `${left}px`;
doms.main.style.top = `${top}px`;
doms.main.style.width = `${width}px`;
doms.main.style.height = `${height}px`;
// 更新裁剪路径
doms.img2.style.clipPath = `polygon(
${left}px ${top}px,
${left + width}px ${top}px,
${left + width}px ${top + height}px,
${left}px ${top + height}px
)`;
}
// 实现图片的上传
doms.imgFile.onchange = function (e) {
// 获取上传的图片
const file = e.target.files[0];
// 创建一个文件读取器
const reader = new FileReader();
// 读取文件
reader.readAsDataURL(file);
// 读取文件成功时,将图片显示在img1和img2中
reader.onload = function (e) {
const imgUrl = e.target.result;
// 设置图片 src 后,监听图片的 load 事件
doms.img1.src = imgUrl;
doms.img2.src = imgUrl;
// 监听 img1 的 load 事件(确保两张图片共享同一 URL,避免重复加载)
const img = new Image();
img.src = imgUrl;
img.onload = function () {
// 此时图片已完全渲染,可获取正确尺寸
const imgWidth = doms.img1.offsetWidth;
const imgHeight = doms.img1.offsetHeight;
// 初始化裁剪框
const width = imgWidth / 2;
const height = imgHeight / 2;
const left = imgWidth / 4;
const top = imgHeight / 4;
updateCropBoxPosition(left, top, width, height);
doms.main.style.display = 'block';
};
// const url = URL.createObjectURL(file);
// doms.img1.src = url;
// doms.img2.src = url;
};
};
// 预览裁剪结果
doms.preview.onclick = function () {
// 获取裁剪框的位置和大小
const left = doms.main.offsetLeft;
const top = doms.main.offsetTop;
const width = doms.main.offsetWidth;
const height = doms.main.offsetHeight;
// console.log(left, top, width, height);
// 使用canvas将裁剪框中的图片裁剪出来
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
ctx.drawImage(doms.img1, left, top, width, height, 0, 0, width, height);
// 将裁剪后的图片显示在页面上 不要被裁剪框遮挡
canvas.style.position = 'absolute';
canvas.style.left = doms.img1.offsetWidth + 50 + 'px';
// 如果canvas已经存在,就删除
if (document.querySelector('canvas')) {
document.querySelector('canvas').remove();
}
document.body.appendChild(canvas);
canvas.toBlob(function (blob) {
// console.log(blob);
const file = new File([blob], 'cut.png', { type: 'image/png' });
console.log('file', file);
})
}
// 实现裁剪框的拖拽效果
doms.main.onmousedown = function (e) {
// 鼠标按下时,获取鼠标的位置
const startX = e.clientX;
const startY = e.clientY;
// console.log(startX, startY);
// 裁剪框初始位置
const startLeft = doms.main.offsetLeft;
const startTop = doms.main.offsetTop;
// 鼠标移动时,获取鼠标的位置
document.onmousemove = function (e) {
const moveX = e.clientX;
const moveY = e.clientY;
// 计算鼠标移动的距离
const disX = moveX - startX;
const disY = moveY - startY;
// console.log(disX, disY);
// 计算裁剪框的位置
const left = startLeft + disX;
const top = startTop + disY;
updateCropBoxPosition(left, top, doms.main.offsetWidth, doms.main.offsetHeight);
}
// 鼠标抬起时,停止移动
document.onmouseup = function () {
document.onmousemove = null;
document.onmouseup = null;
}
}
// 实现裁剪框的缩放效果
dirDoms.forEach(dirDom => {
dirDom.onmousedown = function (e) {
// 阻止父元素的事件
e.stopPropagation();
// 鼠标按下时,获取鼠标的位置
const startX = e.clientX;
const startY = e.clientY;
// 裁剪框初始位置
const startLeft = doms.main.offsetLeft;
const startTop = doms.main.offsetTop;
// 裁剪框初始大小
const startWidth = doms.main.offsetWidth;
const startHeight = doms.main.offsetHeight;
// 鼠标移动时,获取鼠标的位置
document.onmousemove = function (e) {
const moveX = e.clientX;
const moveY = e.clientY;
// 计算鼠标移动的距离
const disX = moveX - startX;
const disY = moveY - startY;
// 根据不同点计算裁剪框的位置和大小
let left;
let top;
let width;
let height;
if (dirDom === doms.leftUp) {
left = startLeft + disX;
top = startTop + disY;
width = startWidth - disX;
height = startHeight - disY;
}
else if (dirDom === doms.up) {
left = startLeft;
top = startTop + disY;
width = startWidth;
height = startHeight - disY;
}
else if (dirDom === doms.rightUp) {
left = startLeft;
top = startTop + disY;
width = startWidth + disX;
height = startHeight - disY;
}
else if (dirDom === doms.right) {
left = startLeft;
top = startTop;
width = startWidth + disX;
height = startHeight;
}
else if (dirDom === doms.rightDown) {
left = startLeft;
top = startTop;
width = startWidth + disX;
height = startHeight + disY;
}
else if (dirDom === doms.down) {
left = startLeft;
top = startTop;
width = startWidth;
height = startHeight + disY;
}
else if (dirDom === doms.leftDown) {
left = startLeft + disX;
top = startTop;
width = startWidth - disX;
height = startHeight + disY;
}
else if (dirDom === doms.left) {
left = startLeft + disX;
top = startTop;
width = startWidth - disX;
height = startHeight;
}
else {
left = startLeft;
top = startTop;
width = startWidth;
height = startHeight;
}
updateCropBoxPosition(left, top, width, height);
}
// 鼠标抬起时,停止移动
document.onmouseup = function () {
document.onmousemove = null;
document.onmouseup = null;
}
}
})
</script>
</html>
核心功能解析
1.双图片叠加预览技术
我们使用两张相同的图片叠加显示,底层图片 (img1) 设置为半透明作为背景参考,上层图片 (img2) 使用 clip-path 属性进行裁剪,从而实现裁剪效果的实时预览:
#img1 {
opacity: 0.6;
position: absolute;
top: 0;
left: 0;
}
#img2 {
opacity: 1;
position: absolute;
top: 0;
left: 0;
clip-path: polygon(0px 0px, 200px 0px, 200px 200px, 0px 200px);
}
2.图片上传与预览
通过 HTML5 的 File API 实现图片上传功能,使用 FileReader 将图片转换为 DataURL 格式并显示在页面上:
doms.imgFile.onchange = function (e) {
// 获取上传的图片
const file = e.target.files[0];
// 创建一个文件读取器
const reader = new FileReader();
// 读取文件
reader.readAsDataURL(file);
// 读取文件成功时,将图片显示在img1和img2中
reader.onload = function (e) {
const imgUrl = e.target.result;
// 设置图片 src 后,监听图片的 load 事件
doms.img1.src = imgUrl;
doms.img2.src = imgUrl;
// 监听 img1 的 load 事件(确保两张图片共享同一 URL,避免重复加载)
const img = new Image();
img.src = imgUrl;
img.onload = function () {
// 此时图片已完全渲染,可获取正确尺寸
const imgWidth = doms.img1.offsetWidth;
const imgHeight = doms.img1.offsetHeight;
// 初始化裁剪框
const width = imgWidth / 2;
const height = imgHeight / 2;
const left = imgWidth / 4;
const top = imgHeight / 4;
updateCropBoxPosition(left, top, width, height);
doms.main.style.display = 'block';
};
// const url = URL.createObjectURL(file);
// doms.img1.src = url;
// doms.img2.src = url;
};
};
3.裁剪框的拖拽与缩放交互
核心就是确定裁剪框最终的left,top,width以及height的值,通过监听鼠标事件实现裁剪框的拖拽和缩放功能:
// 实现裁剪框的拖拽效果
doms.main.onmousedown = function (e) {
// 鼠标按下时,获取鼠标的位置
const startX = e.clientX;
const startY = e.clientY;
// console.log(startX, startY);
// 裁剪框初始位置
const startLeft = doms.main.offsetLeft;
const startTop = doms.main.offsetTop;
// 鼠标移动时,获取鼠标的位置
document.onmousemove = function (e) {
const moveX = e.clientX;
const moveY = e.clientY;
// 计算鼠标移动的距离
const disX = moveX - startX;
const disY = moveY - startY;
// console.log(disX, disY);
// 计算裁剪框的位置
const left = startLeft + disX;
const top = startTop + disY;
updateCropBoxPosition(left, top, doms.main.offsetWidth, doms.main.offsetHeight);
}
// 鼠标抬起时,停止移动
document.onmouseup = function () {
document.onmousemove = null;
document.onmouseup = null;
}
}
// 实现裁剪框的缩放效果
dirDoms.forEach(dirDom => {
dirDom.onmousedown = function (e) {
// 阻止父元素的事件
e.stopPropagation();
// 鼠标按下时,获取鼠标的位置
const startX = e.clientX;
const startY = e.clientY;
// 裁剪框初始位置
const startLeft = doms.main.offsetLeft;
const startTop = doms.main.offsetTop;
// 裁剪框初始大小
const startWidth = doms.main.offsetWidth;
const startHeight = doms.main.offsetHeight;
// 鼠标移动时,获取鼠标的位置
document.onmousemove = function (e) {
const moveX = e.clientX;
const moveY = e.clientY;
// 计算鼠标移动的距离
const disX = moveX - startX;
const disY = moveY - startY;
// 根据不同点计算裁剪框的位置和大小
let left;
let top;
let width;
let height;
if (dirDom === doms.leftUp) {
left = startLeft + disX;
top = startTop + disY;
width = startWidth - disX;
height = startHeight - disY;
}
else if (dirDom === doms.up) {
left = startLeft;
top = startTop + disY;
width = startWidth;
height = startHeight - disY;
}
else if (dirDom === doms.rightUp) {
left = startLeft;
top = startTop + disY;
width = startWidth + disX;
height = startHeight - disY;
}
else if (dirDom === doms.right) {
left = startLeft;
top = startTop;
width = startWidth + disX;
height = startHeight;
}
else if (dirDom === doms.rightDown) {
left = startLeft;
top = startTop;
width = startWidth + disX;
height = startHeight + disY;
}
else if (dirDom === doms.down) {
left = startLeft;
top = startTop;
width = startWidth;
height = startHeight + disY;
}
else if (dirDom === doms.leftDown) {
left = startLeft + disX;
top = startTop;
width = startWidth - disX;
height = startHeight + disY;
}
else if (dirDom === doms.left) {
left = startLeft + disX;
top = startTop;
width = startWidth - disX;
height = startHeight;
}
else {
left = startLeft;
top = startTop;
width = startWidth;
height = startHeight;
}
updateCropBoxPosition(left, top, width, height);
}
// 鼠标抬起时,停止移动
document.onmouseup = function () {
document.onmousemove = null;
document.onmouseup = null;
}
}
})
4.边界检查与尺寸限制
为了确保裁剪框不会超出图片范围,我们实现了边界检查功能:
function updateCropBoxPosition(left, top, width, height) {
// console.log('left, top, width, height', left, top, width, height);
// 边界检查
const imgWidth = doms.img1.offsetWidth;
const imgHeight = doms.img1.offsetHeight;
// 设置最小尺寸限制
const minWidth = 60;
const minHeight = 60;
// 确保宽度和高度不小于最小值
if (width < minWidth) width = minWidth;
if (height < minHeight) height = minHeight;
// 防止左侧溢出
if (left < 0) {
left = 0;
}
// 防止右侧溢出
if (left + width > imgWidth) {
// 尝试调整位置保持宽度
if (width <= imgWidth) {
left = imgWidth - width;
} else {
// 如果宽度大于图片宽度,限制宽度并居中显示
width = imgWidth;
left = 0;
}
}
// 防止顶部溢出
if (top < 0) {
top = 0;
}
// 防止底部溢出
if (top + height > imgHeight) {
// 尝试调整位置保持高度
if (height <= imgHeight) {
top = imgHeight - height;
} else {
// 如果高度大于图片高度,限制高度并居中显示
height = imgHeight;
top = 0;
}
}
// 设置裁剪框样式
doms.main.style.left = `${left}px`;
doms.main.style.top = `${top}px`;
doms.main.style.width = `${width}px`;
doms.main.style.height = `${height}px`;
// 更新裁剪路径
doms.img2.style.clipPath = `polygon(
${left}px ${top}px,
${left + width}px ${top}px,
${left + width}px ${top + height}px,
${left}px ${top + height}px
)`;
}
5.最终裁剪结果生成
使用 Canvas API 将裁剪区域导出为新的图片(需要注意使用drawImage时如果img区域与原图大小如果不一致的话,需要额外计算缩放比例来确定最终的裁剪区域):
doms.preview.onclick = function () {
// 获取裁剪框的位置和大小
const left = doms.main.offsetLeft;
const top = doms.main.offsetTop;
const width = doms.main.offsetWidth;
const height = doms.main.offsetHeight;
// console.log(left, top, width, height);
// 使用canvas将裁剪框中的图片裁剪出来
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
ctx.drawImage(doms.img1, left, top, width, height, 0, 0, width, height);
// 将裁剪后的图片显示在页面上 不要被裁剪框遮挡
canvas.style.position = 'absolute';
canvas.style.left = doms.img1.offsetWidth + 50 + 'px';
// 如果canvas已经存在,就删除
if (document.querySelector('canvas')) {
document.querySelector('canvas').remove();
}
document.body.appendChild(canvas);
canvas.toBlob(function (blob) {
// console.log(blob);
const file = new File([blob], 'cut.png', { type: 'image/png' });
console.log('file', file);
})
}
总结
通过结合 HTML5 的 File API、CSS 的 clip-path 属性和 Canvas API,我们实现了一个功能完整的纯前端图片裁剪工具。这个实现不需要后端支持,所有操作都在浏览器中完成,具有良好的用户体验和性能表现。
可以根据自己的需求进一步扩展这个工具,例如添加预设裁剪比例、旋转功能、滤镜效果等,使其成为一个更强大的图片编辑工具。







