mirror of
https://github.com/Cola-Echo/Cola.git
synced 2026-06-06 03:35:50 +00:00
Add files via upload
This commit is contained in:
390
cropper.js
Normal file
390
cropper.js
Normal file
@@ -0,0 +1,390 @@
|
||||
/**
|
||||
* 通用图片裁剪器模块
|
||||
* 支持不同比例的裁剪(头像1:1, 封面16:9等)
|
||||
*/
|
||||
|
||||
import { showToast } from './toast.js';
|
||||
|
||||
// 裁剪器状态
|
||||
let cropperState = {
|
||||
image: null,
|
||||
canvas: null,
|
||||
ctx: null,
|
||||
imageWidth: 0,
|
||||
imageHeight: 0,
|
||||
imageX: 0,
|
||||
imageY: 0,
|
||||
cropBox: { x: 0, y: 0, width: 100, height: 100 },
|
||||
isDragging: false,
|
||||
isResizing: false,
|
||||
dragStart: { x: 0, y: 0 },
|
||||
boxStart: { x: 0, y: 0, width: 0, height: 0 },
|
||||
resizeHandle: null,
|
||||
aspectRatio: 1, // 宽高比
|
||||
callback: null // 裁剪完成回调
|
||||
};
|
||||
|
||||
/**
|
||||
* 初始化裁剪器事件
|
||||
*/
|
||||
export function initCropper() {
|
||||
// 取消按钮
|
||||
document.getElementById('wechat-cropper-cancel')?.addEventListener('click', closeCropper);
|
||||
|
||||
// 确认按钮
|
||||
document.getElementById('wechat-cropper-confirm')?.addEventListener('click', confirmCrop);
|
||||
|
||||
// 裁剪框拖拽事件
|
||||
const cropperBox = document.getElementById('wechat-cropper-box');
|
||||
if (cropperBox) {
|
||||
cropperBox.addEventListener('mousedown', handleCropBoxMouseDown);
|
||||
cropperBox.addEventListener('touchstart', handleCropBoxTouchStart, { passive: false });
|
||||
}
|
||||
|
||||
// 全局移动和释放事件
|
||||
document.addEventListener('mousemove', handleCropperMouseMove);
|
||||
document.addEventListener('mouseup', handleCropperMouseUp);
|
||||
document.addEventListener('touchmove', handleCropperTouchMove, { passive: false });
|
||||
document.addEventListener('touchend', handleCropperTouchEnd);
|
||||
|
||||
// 四角拖拽手柄
|
||||
document.querySelectorAll('.wechat-cropper-handle').forEach(handle => {
|
||||
handle.addEventListener('mousedown', (e) => handleResizeStart(e, handle));
|
||||
handle.addEventListener('touchstart', (e) => handleResizeTouchStart(e, handle), { passive: false });
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开裁剪器
|
||||
* @param {string} imageSrc - 图片数据URL
|
||||
* @param {number} aspectRatio - 宽高比 (例如 1 表示 1:1, 16/9 表示 16:9)
|
||||
* @param {function} callback - 裁剪完成回调函数,接收裁剪后的base64图片
|
||||
*/
|
||||
export function openCropper(imageSrc, aspectRatio = 1, callback = null) {
|
||||
const modal = document.getElementById('wechat-cropper-modal');
|
||||
const canvas = document.getElementById('wechat-cropper-canvas');
|
||||
const container = document.getElementById('wechat-cropper-container');
|
||||
|
||||
if (!modal || !canvas || !container) return;
|
||||
|
||||
cropperState.aspectRatio = aspectRatio;
|
||||
cropperState.callback = callback;
|
||||
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
cropperState.image = img;
|
||||
cropperState.canvas = canvas;
|
||||
cropperState.ctx = canvas.getContext('2d');
|
||||
|
||||
// 计算适应容器的尺寸
|
||||
const containerWidth = container.clientWidth || 320;
|
||||
const containerHeight = container.clientHeight || 320;
|
||||
|
||||
const scale = Math.min(
|
||||
containerWidth / img.width,
|
||||
containerHeight / img.height
|
||||
);
|
||||
|
||||
const displayWidth = img.width * scale;
|
||||
const displayHeight = img.height * scale;
|
||||
|
||||
canvas.width = displayWidth;
|
||||
canvas.height = displayHeight;
|
||||
|
||||
cropperState.imageWidth = displayWidth;
|
||||
cropperState.imageHeight = displayHeight;
|
||||
cropperState.imageX = (containerWidth - displayWidth) / 2;
|
||||
cropperState.imageY = (containerHeight - displayHeight) / 2;
|
||||
|
||||
cropperState.ctx.drawImage(img, 0, 0, displayWidth, displayHeight);
|
||||
|
||||
// 初始化裁剪框(居中,保持比例)
|
||||
initCropBox();
|
||||
|
||||
modal.classList.remove('hidden');
|
||||
updateCropBoxUI();
|
||||
};
|
||||
img.src = imageSrc;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据宽高比初始化裁剪框
|
||||
*/
|
||||
function initCropBox() {
|
||||
const { imageWidth, imageHeight, aspectRatio } = cropperState;
|
||||
|
||||
let boxWidth, boxHeight;
|
||||
|
||||
if (aspectRatio >= 1) {
|
||||
// 宽 >= 高的比例(如 1:1, 16:9)
|
||||
boxWidth = Math.min(imageWidth * 0.8, imageHeight * 0.8 * aspectRatio);
|
||||
boxHeight = boxWidth / aspectRatio;
|
||||
} else {
|
||||
// 高 > 宽的比例(如 9:16)
|
||||
boxHeight = Math.min(imageHeight * 0.8, imageWidth * 0.8 / aspectRatio);
|
||||
boxWidth = boxHeight * aspectRatio;
|
||||
}
|
||||
|
||||
// 确保裁剪框不超过图片边界
|
||||
boxWidth = Math.min(boxWidth, imageWidth);
|
||||
boxHeight = Math.min(boxHeight, imageHeight);
|
||||
|
||||
cropperState.cropBox = {
|
||||
x: (imageWidth - boxWidth) / 2,
|
||||
y: (imageHeight - boxHeight) / 2,
|
||||
width: boxWidth,
|
||||
height: boxHeight
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新裁剪框UI
|
||||
*/
|
||||
function updateCropBoxUI() {
|
||||
const cropBox = document.getElementById('wechat-cropper-box');
|
||||
const canvas = cropperState.canvas;
|
||||
|
||||
if (!cropBox || !canvas) return;
|
||||
|
||||
const container = document.getElementById('wechat-cropper-container');
|
||||
if (!container) return;
|
||||
|
||||
// 计算偏移(使裁剪框相对于容器居中的canvas)
|
||||
const offsetX = (container.clientWidth - canvas.width) / 2;
|
||||
const offsetY = (container.clientHeight - canvas.height) / 2;
|
||||
|
||||
cropBox.style.left = (cropperState.cropBox.x + offsetX) + 'px';
|
||||
cropBox.style.top = (cropperState.cropBox.y + offsetY) + 'px';
|
||||
cropBox.style.width = cropperState.cropBox.width + 'px';
|
||||
cropBox.style.height = cropperState.cropBox.height + 'px';
|
||||
}
|
||||
|
||||
// 裁剪框拖拽开始
|
||||
function handleCropBoxMouseDown(e) {
|
||||
if (e.target.classList.contains('wechat-cropper-handle')) return;
|
||||
|
||||
e.preventDefault();
|
||||
cropperState.isDragging = true;
|
||||
cropperState.dragStart = { x: e.clientX, y: e.clientY };
|
||||
cropperState.boxStart = { ...cropperState.cropBox };
|
||||
}
|
||||
|
||||
function handleCropBoxTouchStart(e) {
|
||||
if (e.target.classList.contains('wechat-cropper-handle')) return;
|
||||
|
||||
e.preventDefault();
|
||||
const touch = e.touches[0];
|
||||
cropperState.isDragging = true;
|
||||
cropperState.dragStart = { x: touch.clientX, y: touch.clientY };
|
||||
cropperState.boxStart = { ...cropperState.cropBox };
|
||||
}
|
||||
|
||||
// 四角拖拽开始
|
||||
function handleResizeStart(e, handle) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
cropperState.isResizing = true;
|
||||
cropperState.resizeHandle = handle.classList.contains('nw') ? 'nw' :
|
||||
handle.classList.contains('ne') ? 'ne' :
|
||||
handle.classList.contains('sw') ? 'sw' : 'se';
|
||||
cropperState.dragStart = { x: e.clientX, y: e.clientY };
|
||||
cropperState.boxStart = { ...cropperState.cropBox };
|
||||
}
|
||||
|
||||
function handleResizeTouchStart(e, handle) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const touch = e.touches[0];
|
||||
cropperState.isResizing = true;
|
||||
cropperState.resizeHandle = handle.classList.contains('nw') ? 'nw' :
|
||||
handle.classList.contains('ne') ? 'ne' :
|
||||
handle.classList.contains('sw') ? 'sw' : 'se';
|
||||
cropperState.dragStart = { x: touch.clientX, y: touch.clientY };
|
||||
cropperState.boxStart = { ...cropperState.cropBox };
|
||||
}
|
||||
|
||||
function handleCropperMouseMove(e) {
|
||||
if (!cropperState.isDragging && !cropperState.isResizing) return;
|
||||
|
||||
const dx = e.clientX - cropperState.dragStart.x;
|
||||
const dy = e.clientY - cropperState.dragStart.y;
|
||||
|
||||
if (cropperState.isDragging) {
|
||||
moveCropBox(dx, dy);
|
||||
} else if (cropperState.isResizing) {
|
||||
resizeCropBox(dx, dy);
|
||||
}
|
||||
}
|
||||
|
||||
function handleCropperTouchMove(e) {
|
||||
if (!cropperState.isDragging && !cropperState.isResizing) return;
|
||||
|
||||
e.preventDefault();
|
||||
const touch = e.touches[0];
|
||||
const dx = touch.clientX - cropperState.dragStart.x;
|
||||
const dy = touch.clientY - cropperState.dragStart.y;
|
||||
|
||||
if (cropperState.isDragging) {
|
||||
moveCropBox(dx, dy);
|
||||
} else if (cropperState.isResizing) {
|
||||
resizeCropBox(dx, dy);
|
||||
}
|
||||
}
|
||||
|
||||
// 移动裁剪框
|
||||
function moveCropBox(dx, dy) {
|
||||
let newX = cropperState.boxStart.x + dx;
|
||||
let newY = cropperState.boxStart.y + dy;
|
||||
|
||||
// 限制在图片范围内
|
||||
newX = Math.max(0, Math.min(newX, cropperState.imageWidth - cropperState.cropBox.width));
|
||||
newY = Math.max(0, Math.min(newY, cropperState.imageHeight - cropperState.cropBox.height));
|
||||
|
||||
cropperState.cropBox.x = newX;
|
||||
cropperState.cropBox.y = newY;
|
||||
updateCropBoxUI();
|
||||
}
|
||||
|
||||
// 调整裁剪框大小(保持宽高比)
|
||||
function resizeCropBox(dx, dy) {
|
||||
const { aspectRatio } = cropperState;
|
||||
const handle = cropperState.resizeHandle;
|
||||
let { x, y, width, height } = cropperState.boxStart;
|
||||
|
||||
// 根据拖动的角计算新尺寸
|
||||
let delta;
|
||||
|
||||
switch (handle) {
|
||||
case 'se': // 右下角
|
||||
delta = Math.max(dx, dy / aspectRatio);
|
||||
width = Math.max(50, width + delta);
|
||||
height = width / aspectRatio;
|
||||
break;
|
||||
case 'sw': // 左下角
|
||||
delta = Math.max(-dx, dy / aspectRatio);
|
||||
width = Math.max(50, width + delta);
|
||||
height = width / aspectRatio;
|
||||
x = cropperState.boxStart.x + cropperState.boxStart.width - width;
|
||||
break;
|
||||
case 'ne': // 右上角
|
||||
delta = Math.max(dx, -dy / aspectRatio);
|
||||
width = Math.max(50, width + delta);
|
||||
height = width / aspectRatio;
|
||||
y = cropperState.boxStart.y + cropperState.boxStart.height - height;
|
||||
break;
|
||||
case 'nw': // 左上角
|
||||
delta = Math.max(-dx, -dy / aspectRatio);
|
||||
width = Math.max(50, width + delta);
|
||||
height = width / aspectRatio;
|
||||
x = cropperState.boxStart.x + cropperState.boxStart.width - width;
|
||||
y = cropperState.boxStart.y + cropperState.boxStart.height - height;
|
||||
break;
|
||||
}
|
||||
|
||||
// 限制边界
|
||||
if (x < 0) {
|
||||
width = width + x;
|
||||
height = width / aspectRatio;
|
||||
x = 0;
|
||||
}
|
||||
if (y < 0) {
|
||||
height = height + y;
|
||||
width = height * aspectRatio;
|
||||
y = 0;
|
||||
}
|
||||
if (x + width > cropperState.imageWidth) {
|
||||
width = cropperState.imageWidth - x;
|
||||
height = width / aspectRatio;
|
||||
}
|
||||
if (y + height > cropperState.imageHeight) {
|
||||
height = cropperState.imageHeight - y;
|
||||
width = height * aspectRatio;
|
||||
}
|
||||
|
||||
// 最小尺寸限制
|
||||
if (width < 50 || height < 50) return;
|
||||
|
||||
cropperState.cropBox = { x, y, width, height };
|
||||
updateCropBoxUI();
|
||||
}
|
||||
|
||||
function handleCropperMouseUp() {
|
||||
cropperState.isDragging = false;
|
||||
cropperState.isResizing = false;
|
||||
}
|
||||
|
||||
function handleCropperTouchEnd() {
|
||||
cropperState.isDragging = false;
|
||||
cropperState.isResizing = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭裁剪器
|
||||
*/
|
||||
export function closeCropper() {
|
||||
document.getElementById('wechat-cropper-modal')?.classList.add('hidden');
|
||||
cropperState.image = null;
|
||||
cropperState.callback = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 确认裁剪
|
||||
*/
|
||||
function confirmCrop() {
|
||||
if (!cropperState.image || !cropperState.canvas) {
|
||||
showToast('裁剪失败', 'info');
|
||||
return;
|
||||
}
|
||||
|
||||
// 计算原图裁剪区域
|
||||
const scaleX = cropperState.image.width / cropperState.imageWidth;
|
||||
const scaleY = cropperState.image.height / cropperState.imageHeight;
|
||||
|
||||
const cropX = cropperState.cropBox.x * scaleX;
|
||||
const cropY = cropperState.cropBox.y * scaleY;
|
||||
const cropWidth = cropperState.cropBox.width * scaleX;
|
||||
const cropHeight = cropperState.cropBox.height * scaleY;
|
||||
|
||||
// 创建裁剪后的画布
|
||||
const croppedCanvas = document.createElement('canvas');
|
||||
croppedCanvas.width = cropWidth;
|
||||
croppedCanvas.height = cropHeight;
|
||||
const croppedCtx = croppedCanvas.getContext('2d');
|
||||
|
||||
croppedCtx.drawImage(
|
||||
cropperState.image,
|
||||
cropX, cropY, cropWidth, cropHeight,
|
||||
0, 0, cropWidth, cropHeight
|
||||
);
|
||||
|
||||
const croppedDataUrl = croppedCanvas.toDataURL('image/jpeg', 0.9);
|
||||
|
||||
// 调用回调
|
||||
if (cropperState.callback) {
|
||||
cropperState.callback(croppedDataUrl);
|
||||
}
|
||||
|
||||
closeCropper();
|
||||
}
|
||||
|
||||
/**
|
||||
* 便捷方法:选择文件并打开裁剪器
|
||||
* @param {number} aspectRatio - 宽高比
|
||||
* @param {function} callback - 裁剪完成回调
|
||||
*/
|
||||
export function selectAndCrop(aspectRatio, callback) {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.accept = 'image/*';
|
||||
input.onchange = (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (event) => {
|
||||
openCropper(event.target.result, aspectRatio, callback);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
};
|
||||
input.click();
|
||||
}
|
||||
Reference in New Issue
Block a user