canvas 裁剪图片功能实现
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0" />
<title>裁剪图片</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
.app {
display: grid;
place-content: center;
row-gap: 20px;
}
.avatar-layout {
display: flex;
place-content: center;
column-gap: 20px;
margin-top: 20px;
}
.preview {
overflow: hidden;
display: grid;
place-content: center;
width: 240px;
height: 240px;
border-radius: 6px;
user-select: none;
}
.origin-preview {
outline: 1px solid #999;
.img-wrap {
position: relative;
display: flex;
}
.mask {
--mask-x: 0px;
--mask-y: 0px;
--mask-width: 50px;
--mask-height: 50px;
--mask-show: none;
display: var(--mask-show);
position: absolute;
inset: 0;
translate: var(--mask-x) var(--mask-y);
width: var(--mask-width);
height: var(--mask-height);
background-color: rgba(0, 0, 0, 0.5);
/* border: 1px solid pink; */
z-index: 2;
will-change: translate;
.drag-point {
--size: 20px;
position: absolute;
width: var(--size);
height: var(--size);
border: inherit;
background-color: inherit;
z-index: 3;
}
.point-lt {
top: 0;
left: 0;
translate: calc(var(--size) * -1) calc(var(--size) * -1);
cursor: nw-resize;
}
.point-lb {
bottom: 0;
left: 0;
translate: calc(var(--size) * -1) calc(var(--size));
cursor: sw-resize;
}
.point-rt {
top: 0;
right: 0;
translate: calc(var(--size)) calc(var(--size) * -1);
cursor: ne-resize;
}
.point-rb {
bottom: 0;
right: 0;
translate: var(--size) var(--size);
cursor: se-resize;
}
}
}
.clip-preview {
row-gap: 20px;
text-align: center;
outline: 1px solid #999;
.clip-wrap {
/* overflow: hidden; */
display: grid;
place-content: center;
width: 120px;
height: 120px;
/* border-radius: 50%; */
background-color: #dedede;
outline: inherit;
.clip {
width: inherit;
height: inherit;
}
}
}
img {
width: 100%;
height: 100%;
object-fit: contain;
}
.btn {
margin-inline: auto;
}
</style>
</head>
<body>
<div class="app">
<div class="avatar-layout">
<div class="preview origin-preview">
<div class="img-wrap">
<img class="origin" alt="" draggable="false" />
<input type="file" id="file" hidden />
<div class="mask">
<span data-dir="lefttop" class="drag-point point-lt"></span>
<span data-dir="leftbottom" class="drag-point point-lb"></span>
<span data-dir="righttop" class="drag-point point-rt"></span>
<span data-dir="rightbottom" class="drag-point point-rb"></span>
</div>
</div>
</div>
<hr />
<div class="preview clip-preview">
<span>预览头像</span>
<div class="clip-wrap">
<canvas class="clip"></canvas>
</div>
</div>
</div>
<div class="btn">
<button class="btn-upload">重新选择</button>
<button class="btn-crop-preview">预览裁剪图片</button>
</div>
</div>
<script>
/*
canvas 裁剪图片功能实现
功能:
1. [x] 选择图片文件,预览原图
2. [x] 裁剪图片
3. [x] 显示裁剪后的图片
4. [x] 调整裁剪区域大小
5. [x] 获取裁剪后的图片
6. [] 图片裁剪功能优化
*/
const originImg = document.querySelector('.origin');
/** @type {HTMLCanvasElement} */
const clipCanvas = document.querySelector('.clip');
const mask = document.querySelector('.mask');
const dragPoint = Array.from(mask.querySelectorAll('.drag-point'));
const clipInfo = {
width: mask.offsetWidth,
height: mask.offsetHeight,
x: originImg.offsetLeft,
y: originImg.offsetTop,
container: null,
// 图片缩放比例
scaleX: originImg.naturalWidth / originImg.offsetWidth,
scaleY: originImg.naturalHeight / originImg.offsetHeight,
// 限制最小的 mask 大小
minMaskSize: 30,
};
// mask.style.setProperty('--mask-x', clipInfo.x + 'px');
// mask.style.setProperty('--mask-y', clipInfo.y + 'px');
/** @type {CanvasRenderingContext2D} */
const clipCtx = clipCanvas.getContext('2d');
let pointX = 0,
pointY = 0;
init();
function init() {
/** @type {HTMLInputElement} */
const file = document.getElementById('file');
const updloadBtn = document.querySelector('.btn-upload');
const imgWrap = document.querySelector('.img-wrap');
const cropPreviewBtn = document.querySelector('.btn-crop-preview');
// 裁切canvas的宽高
clipCanvas.width = clipCanvas.offsetWidth * devicePixelRatio;
clipCanvas.height = clipCanvas.offsetHeight * devicePixelRatio;
clipInfo.container = imgWrap;
// 预览裁剪图片
cropPreviewBtn.addEventListener('click', () => {
clipCanvas.toBlob((blob) => {
const file = new File([blob], 'clip.png', { type: 'image/png' });
const url = URL.createObjectURL(file);
open(url);
});
});
// 监听文件选择
file.addEventListener('change', handleFile);
// 点击按钮触发文件选择
updloadBtn.addEventListener('click', () => file.click());
// 监听鼠标按下
mask.addEventListener('pointerdown', (e) => {
// const isPoint = dragPoint.includes(e.target);
// if (isPoint) return;
const {
container: { offsetLeft, offsetTop },
} = clipInfo;
pointX = e.offsetX + offsetLeft;
pointY = e.offsetY + offsetTop;
mask.style.cursor = 'grabbing';
// 监听鼠标移动
window.addEventListener('pointermove', handleMaskMove);
// 监听鼠标抬起
window.addEventListener('pointerup', handleMaskUp);
});
dragPoint.forEach((point) => {
point.addEventListener('pointerdown', (e) => {
e.stopPropagation();
const sx = e.clientX;
const sy = e.clientY;
const { minMaskSize, container } = clipInfo;
const {
left: appLeft,
top: appTop,
width: appWidth,
height: appHeight,
} = container.getBoundingClientRect();
let { left, top, width, height, right, bottom } = mask.getBoundingClientRect();
const { dir } = e.target.dataset;
window.addEventListener('pointermove', handleDragPointMove);
window.addEventListener('pointerup', handleDragPointUp);
function handleDragPointMove(e) {
const { clientX, clientY } = e;
const dx = clientX - sx,
dy = clientY - sy;
const handlers = {
lefttop: (box) => updateSizeAndPosition(box, -dx, -dy, true, true),
leftbottom: (box) => updateSizeAndPosition(box, -dx, dy, true, false),
righttop: (box) => updateSizeAndPosition(box, dx, -dy, false, true),
rightbottom: (box) => updateSizeAndPosition(box, dx, dy, false, false),
};
handlers[dir]?.(mask);
function updateSizeAndPosition(box, dx, dy, x, y) {
let _width = Math.round(width + dx),
_height = Math.round(height + dy),
_x = Math.round(left + -1 * dx - appLeft),
_y = Math.round(top + -1 * dy - appTop);
// 限制范围
_x = Math.max(0, _x);
_x = Math.min(right - minMaskSize - appLeft, _x);
_y = Math.max(0, _y);
_y = Math.min(bottom - minMaskSize - appTop, _y);
_width = Math.max(minMaskSize, _width);
_width = Math.min(appWidth - (left - appLeft), _width);
_height = Math.max(minMaskSize, _height);
_height = Math.min(appHeight - (top - appTop), _height);
box.style.setProperty('--mask-width', `${_width}px`);
box.style.setProperty('--mask-height', `${_height}px`);
if (x) box.style.setProperty('--mask-x', `${_x}px`);
if (y) box.style.setProperty('--mask-y', `${_y}px`);
clipInfo.width = _width;
clipInfo.height = _height;
clipInfo.x = _x;
clipInfo.y = _y;
drawClipImg();
}
}
function handleDragPointUp(e) {
dragPoint.forEach((point) => {
window.removeEventListener('pointermove', handleDragPointMove);
window.removeEventListener('pointerup', handleDragPointUp);
});
}
});
});
}
function handleMaskMove(e) {
// 遮罩 mask 在容器内的位置
let x = Math.round(e.clientX - pointX);
let y = Math.round(e.clientY - pointY);
// (边界处理) 限制 mask 移动范围
x = Math.max(x, 0, originImg.offsetLeft);
x = Math.min(x, originImg.offsetLeft + originImg.offsetWidth - clipInfo.width);
y = Math.max(y, 0, originImg.offsetTop);
y = Math.min(y, originImg.offsetTop + originImg.offsetHeight - clipInfo.height);
clipInfo.x = x;
clipInfo.y = y;
mask.style.setProperty('--mask-x', clipInfo.x + 'px');
mask.style.setProperty('--mask-y', clipInfo.y + 'px');
drawClipImg();
}
function handleMaskUp() {
this.removeEventListener('pointermove', handleMaskMove);
this.removeEventListener('pointerup', handleMaskUp);
pointX = pointY = 0;
mask.style.cursor = 'grab';
}
function handleFile(e) {
/** @type {File} */
const file = e.target.files[0];
if (!file) return;
// 读取文件内容
const reader = new FileReader();
reader.onload = function () {
originImg.onload = () => {
// 图片加载完成后,设置图片大小
if (originImg.width < originImg.height) {
const previewWrap = document.querySelector('.origin-preview');
clipInfo.container.style.height = previewWrap.offsetHeight + 'px';
clipInfo.container.style.width = originImg.width * previewWrap.offsetHeight / originImg.height + 'px';
}
// 图片加载完成后,添加蒙层
addMask();
// 绘制裁剪图片
drawClipImg();
};
// 赋值原图图片 base64地址
originImg.src = reader.result;
};
reader.readAsDataURL(file);
}
// 绘制裁剪图片
function drawClipImg() {
const { x, y, width, height, scaleX, scaleY } = clipInfo;
clipCtx.clearRect(0, 0, clipCanvas.width, clipCanvas.height);
clipCtx.drawImage(
originImg,
/* 裁剪区域 */
(x - originImg.offsetLeft) * scaleX,
(y - originImg.offsetTop) * scaleY,
clipInfo.width * scaleX,
clipInfo.height * scaleY,
/* 绘制区域 */
0,
0,
clipCanvas.width,
clipCanvas.height
);
}
/**
* 添加蒙层
*/
function addMask() {
mask.style.setProperty('--mask-show', 'block');
clipInfo.width = mask.offsetWidth;
clipInfo.height = mask.offsetHeight;
clipInfo.x = originImg.offsetLeft;
clipInfo.y = originImg.offsetTop;
clipInfo.scaleX = originImg.naturalWidth / originImg.offsetWidth;
clipInfo.scaleY = originImg.naturalHeight / originImg.offsetHeight;
mask.style.setProperty('--mask-x', clipInfo.x + 'px');
mask.style.setProperty('--mask-y', clipInfo.y + 'px');
}
</script>
</body>
</html>
标签:canvas,--,clipInfo,裁剪,mask,height,width,originImg,js
From: https://www.cnblogs.com/chlai/p/18162704