简言
使用canvas实现图片裁剪功能。
图像裁剪
图像裁剪是一个比较常见的功能,使用canvas的相关方法和属性可以实现简易的图片裁剪功能。
功能分析
- 上传图片
- 图片显示
- 绘制裁剪框
- 裁剪操作
- 生成预览
- 提供下载
上传图片
使用input的file类型的元素可以上传图片文件数据,然后转成base64供使用。
file.addEventListener('change', (e) => {
const file = e.target.files[0];
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = (e) => {
img.src = e.target.result;
img.onload = () => {
console.log(img.width, img.height);
// 更新图片缩放比例
if (img.width > img.height) {
scale = 500 / img.width
} else {
scale = 500 / img.height
}
drawOperatingLine()
}
}
})
图片显示
canvas的drawImage()方法可以接收一个img元素来显示图片。
我这边canvas固定宽高了,所以做了缩放。
ctx.clearRect(0, 0, origin.width, origin.height)
// 绘制图像
dx = (origin.width - scale * img.width) / 2 // 图片偏移 x
dy = (origin.height - scale * img.height) / 2 // 图片偏移 y
dw = scale * img.width
dh = scale * img.height
ctx.drawImage(img, 0, 0, img.width, img.height, dx, dy, dw, dh);
绘制裁剪框
主要绘制半透明蒙层和裁剪框,以及四周边缘点。
理想情况下是使用canvas的globalCompositeOperation(组合)实现,可以绘制多样性裁剪框
// 透明度50%蒙版
ctx.save()
ctx.fillStyle = 'rgba(0,0,0,0.4)';
ctx.fillRect(0, 0, origin.width, initY);
ctx.fillRect(0, endY, origin.width, origin.height - initY);
ctx.fillRect(0, initY, initX, endY - initY);
ctx.fillRect(endX, initY, origin.width - endX, endY - initY);
ctx.restore()
// 边缘线
ctx.save()
ctx.strokeStyle = 'red';
ctx.setLineDash([5, 5])
ctx.moveTo(initX, initY);
ctx.lineTo(endX, initY);
ctx.lineTo(endX, endY);
ctx.lineTo(initX, endY);
ctx.closePath();
ctx.stroke();
// 矩形四周点
ctx.fillStyle = '#fff';
ctx.fillRect(initX - 5, initY - 5, 10, 10);
ctx.fillRect(endX - 5, initY - 5, 10, 10);
ctx.fillRect(endX - 5, endY - 5, 10, 10);
ctx.fillRect(initX - 5, endY - 5, 10, 10);
ctx.beginPath();
ctx.restore()
裁剪操作
裁剪操作有:
- 操作边缘线改变裁剪框大小
- 操作四周边缘点改变裁剪框大小
- 操作裁剪框中间移动裁剪框
改变裁剪框位置和大小需要按下、移动、抬起三个事件。
操作后记得及时更新画布
origin.addEventListener("mousedown", (e) => {
if (now !== 0) {
modify = true
lastX = e.offsetX
lastY = e.offsetY
}
});
origin.addEventListener("mousemove", (e) => {
if (!ctx) return
let x = e.offsetX
let y = e.offsetY
// 判定类型
if ((x > initX + 5 && x < endX - 5) && (y < initY + 5 && y > initY - 5)) { // 上边
origin.style.cursor = 'ns-resize'
now = 1
} else if ((x < endX + 5 && x > endX - 5) && (y > initY + 5 && y < endY - 5)) { // 右边
origin.style.cursor = 'ew-resize'
now = 2
} else if ((x > initX + 5 && x < endX - 5) && (y < endY + 5 && y > endY - 5)) { // 下边
origin.style.cursor = 'ns-resize'
now = 3
} else if ((x < initX + 5 && x > initX - 5) && (y > initY + 5 && y < endY - 5)) { // 左边
origin.style.cursor = 'ew-resize'
now = 4
} else if (x <= initX + 5 && x >= initX - 5 && y <= initY + 5 && y >= initY - 5) { // 左上角
origin.style.cursor = 'nwse-resize'
now = 5
} else if (x <= endX + 5 && x >= endX - 5 && y <= endY + 5 && y >= endY - 5) { // 右下角
origin.style.cursor = 'nwse-resize'
now = 6
} else if (x <= endX + 5 && x >= endX - 5 && y <= initY + 5 && y >= initY - 5) { // 右上角
origin.style.cursor = 'nesw-resize'
now = 7
} else if (x <= initX + 5 && x >= initX - 5 && y <= endY + 5 && y >= endY - 5) { // 左下角
origin.style.cursor = 'nesw-resize'
now = 8
}
else if (x > initX + 5 && x < endX - 5 && y > initY + 5 && y < endY - 5) { // 移动
origin.style.cursor = 'all-scroll'
now = 9
}
else {
origin.style.cursor = 'auto'
now = 0
}
// 若在拖动根据类型更改值
if (modify) {
switch (now) {
case 1: // 上边
initY = e.offsetY
break
case 2: // 右边
endX = e.offsetX
break
case 3: // 下边
endY = e.offsetY
break
case 4: // 左边
initX = e.offsetX
break
case 5: // 左上角
initX = e.offsetX
initY = e.offsetY
break
case 6: // 右下角
endY = e.offsetY
endX = e.offsetX
break
case 7: // 右上角
endX = e.offsetX
initY = e.offsetY
break
case 8: // 左下角
initX = e.offsetX
endY = e.offsetY
break
case 9: // 左下角
let vx = e.offsetX - lastX
let vy = e.offsetY - lastY
initX += vx
endX += vx
initY += vy
endY += vy
// 更新
lastX = e.offsetX
lastY = e.offsetY
break
}
// 更新
drawOperatingLine()
}
})
origin.addEventListener('mouseup', (e) => {
if (modify) {
modify = false
}
})
生成预览
我实现的是预览要裁剪后的原图片部分内容。
使用另外一个canvas来绘制img元素的裁剪包含部分。
有缩放的话,注意还原
下载裁剪后图片
canvas有一个toDataURL()方法可以将canvas转成base64数据。
btn.onclick = () => {
// 生成图片
let imgData = canvas.toDataURL('image/png', 1)
let a = document.createElement('a')
a.href = imgData
a.download = 'image.png'
a.click()
}
源码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>图像剪辑</title>
<style>
html,
body {
width: 100vw;
height: 100%;
margin: 0;
padding: 0;
}
.box {
box-sizing: border-box;
width: 100%;
height: 100%;
padding: 24px;
}
.box-content {
margin-top: 16px;
box-sizing: border-box;
width: 100%;
/* height: 500px; */
/* display: flex; */
align-items: center;
border: 1px solid #000;
}
#origin {
width: 500px;
height: 500px;
}
</style>
</head>
<body>
<div class="box">
<div>
<h1>图像剪辑</h1>
<input type="file" name="file" id="file">
</div>
<div class="box-content">
<canvas id="origin" width="500" height="500">
</canvas>
<div>
<div>预览:</div>
<canvas id="canvas">
</canvas>
</div>
</div>
<div>
<button id="btn">下载裁剪后的内容</button>
</div>
</div>
<script>
const file = document.getElementById('file');
const origin = document.getElementById('origin');
const canvas = document.getElementById('canvas')
const btn = document.getElementById('btn');
const ctx = origin.getContext('2d');
const img = document.createElement('img');
// 缩放
let scale = 1 // 图片缩放比例
let dx = 0 // 图片偏移 x
let dy = 0// 图片偏移 y
let dw = origin.width
let dh = origin.height
// 裁剪位置
let initX = 50
let initY = 50
let endX = origin.width - initX
let endY = origin.height - initY
// 裁剪状态
let now = 0;
let modify = false
let lastX = 0, lastY = 0
origin.addEventListener("mousedown", (e) => {
if (now !== 0) {
modify = true
lastX = e.offsetX
lastY = e.offsetY
}
});
origin.addEventListener("mousemove", (e) => {
if (!ctx) return
let x = e.offsetX
let y = e.offsetY
// 判定类型
if ((x > initX + 5 && x < endX - 5) && (y < initY + 5 && y > initY - 5)) { // 上边
origin.style.cursor = 'ns-resize'
now = 1
} else if ((x < endX + 5 && x > endX - 5) && (y > initY + 5 && y < endY - 5)) { // 右边
origin.style.cursor = 'ew-resize'
now = 2
} else if ((x > initX + 5 && x < endX - 5) && (y < endY + 5 && y > endY - 5)) { // 下边
origin.style.cursor = 'ns-resize'
now = 3
} else if ((x < initX + 5 && x > initX - 5) && (y > initY + 5 && y < endY - 5)) { // 左边
origin.style.cursor = 'ew-resize'
now = 4
} else if (x <= initX + 5 && x >= initX - 5 && y <= initY + 5 && y >= initY - 5) { // 左上角
origin.style.cursor = 'nwse-resize'
now = 5
} else if (x <= endX + 5 && x >= endX - 5 && y <= endY + 5 && y >= endY - 5) { // 右下角
origin.style.cursor = 'nwse-resize'
now = 6
} else if (x <= endX + 5 && x >= endX - 5 && y <= initY + 5 && y >= initY - 5) { // 右上角
origin.style.cursor = 'nesw-resize'
now = 7
} else if (x <= initX + 5 && x >= initX - 5 && y <= endY + 5 && y >= endY - 5) { // 左下角
origin.style.cursor = 'nesw-resize'
now = 8
}
else if (x > initX + 5 && x < endX - 5 && y > initY + 5 && y < endY - 5) { // 移动
origin.style.cursor = 'all-scroll'
now = 9
}
else {
origin.style.cursor = 'auto'
now = 0
}
// 若在拖动根据类型更改值
if (modify) {
switch (now) {
case 1: // 上边
initY = e.offsetY
break
case 2: // 右边
endX = e.offsetX
break
case 3: // 下边
endY = e.offsetY
break
case 4: // 左边
initX = e.offsetX
break
case 5: // 左上角
initX = e.offsetX
initY = e.offsetY
break
case 6: // 右下角
endY = e.offsetY
endX = e.offsetX
break
case 7: // 右上角
endX = e.offsetX
initY = e.offsetY
break
case 8: // 左下角
initX = e.offsetX
endY = e.offsetY
break
case 9: // 左下角
let vx = e.offsetX - lastX
let vy = e.offsetY - lastY
initX += vx
endX += vx
initY += vy
endY += vy
// 更新
lastX = e.offsetX
lastY = e.offsetY
break
}
// 更新
drawOperatingLine()
}
})
origin.addEventListener('mouseup', (e) => {
if (modify) {
modify = false
}
})
file.addEventListener('change', (e) => {
const file = e.target.files[0];
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = (e) => {
img.src = e.target.result;
img.onload = () => {
console.log(img.width, img.height);
// 更新图片缩放比例
if (img.width > img.height) {
scale = 500 / img.width
} else {
scale = 500 / img.height
}
drawOperatingLine()
}
}
})
// 绘制裁剪控制图像
function drawOperatingLine() {
ctx.clearRect(0, 0, origin.width, origin.height)
// 绘制图像
dx = (origin.width - scale * img.width) / 2 // 图片偏移 x
dy = (origin.height - scale * img.height) / 2 // 图片偏移 y
dw = scale * img.width
dh = scale * img.height
ctx.drawImage(img, 0, 0, img.width, img.height, dx, dy, dw, dh);
// 透明度50%蒙版
ctx.save()
ctx.fillStyle = 'rgba(0,0,0,0.4)';
ctx.fillRect(0, 0, origin.width, initY);
ctx.fillRect(0, endY, origin.width, origin.height - initY);
ctx.fillRect(0, initY, initX, endY - initY);
ctx.fillRect(endX, initY, origin.width - endX, endY - initY);
ctx.restore()
// 边缘线
ctx.save()
ctx.strokeStyle = 'red';
ctx.setLineDash([5, 5])
ctx.moveTo(initX, initY);
ctx.lineTo(endX, initY);
ctx.lineTo(endX, endY);
ctx.lineTo(initX, endY);
ctx.closePath();
ctx.stroke();
// 矩形四周点
ctx.fillStyle = '#fff';
ctx.fillRect(initX - 5, initY - 5, 10, 10);
ctx.fillRect(endX - 5, initY - 5, 10, 10);
ctx.fillRect(endX - 5, endY - 5, 10, 10);
ctx.fillRect(initX - 5, endY - 5, 10, 10);
ctx.beginPath();
ctx.restore()
// 生成预览
preview()
}
// 生成预览
function preview() {
let lsx = initX < dx ? dx : initX
let lsy = initY < dy ? dy : initY
let lex = endX > dx + dw ? dx + dw : endX
let ley = endY > dy + dh ? dy + dh : endY
let sx = 0
let sy = 0
if (initX > dx + dw) {
sx = img.width
} else if (initX > dx) {
sx = (initX - dx) / scale
}
if (initY > dy + dh) {
sy = img.height
} else if (initY > dy) {
sy = (initY - dy) / scale
}
const sw = (lex - lsx) / scale // 实际图片裁剪宽度
const sh = (ley - lsy) / scale // 实际图片裁剪高度
canvas.width = sw
canvas.height = sh
canvas.getContext('2d').drawImage(img, sx, sy, sw, sh, 0, 0, sw, sh)
}
btn.onclick = () => {
// 生成图片
let imgData = canvas.toDataURL('image/png', 1)
let a = document.createElement('a')
a.href = imgData
a.download = 'image.png'
a.click()
}
</script>
</body>
</html>
结语
有一些边界情况要处理。例如,裁剪框边界情况,图片过小缩放后模糊情况等。
觉得有用的话,麻烦动动小手点个赞,关注一下吧!