Files
Cola/cropper.js
2025-12-23 01:19:53 +08:00

391 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 通用图片裁剪器模块
* 支持不同比例的裁剪头像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();
}