1850 lines
62 KiB
JavaScript
1850 lines
62 KiB
JavaScript
// 极简邀请函制作工具
|
||
(function() {
|
||
const fileInput = document.getElementById('fileInput');
|
||
const addImageBtn = document.getElementById('addImageBtn');
|
||
const addOverlayBtn = document.getElementById('addOverlayBtn');
|
||
const addTextBtn = document.getElementById('addTextBtn');
|
||
const saveBtn = document.getElementById('saveBtn');
|
||
const imageContainer = document.getElementById('imageContainer');
|
||
const bgLayer = document.getElementById('bgLayer');
|
||
const bgImage = document.getElementById('bgImage');
|
||
const emptyTip = document.getElementById('emptyTip');
|
||
const input1 = document.getElementById('input1');
|
||
const input2 = document.getElementById('input2');
|
||
const exportCanvas = document.getElementById('exportCanvas');
|
||
const longPressTip = document.getElementById('longPressTip');
|
||
const canvasArea = document.getElementById('canvasArea');
|
||
const textEditorToolbar = document.getElementById('textEditorToolbar');
|
||
const boldBtn = document.getElementById('boldBtn');
|
||
const italicBtn = document.getElementById('italicBtn');
|
||
const colorPicker = document.getElementById('colorPicker');
|
||
const fontPicker = document.getElementById('fontPicker');
|
||
const fontSizePicker = document.getElementById('fontSizePicker');
|
||
const overlayLayer = document.getElementById('overlayLayer');
|
||
const overlayInput = document.createElement('input');
|
||
overlayInput.type = 'file';
|
||
overlayInput.accept = 'image/*';
|
||
overlayInput.hidden = true;
|
||
document.body.appendChild(overlayInput);
|
||
const guideH = document.getElementById('guideH');
|
||
const guideV = document.getElementById('guideV');
|
||
|
||
let currentImage = null;
|
||
let selectedInput = null;
|
||
let editingInput = null;
|
||
let longPressTimer = null;
|
||
|
||
// 图片滑动调整位置相关变量
|
||
let imageOffsetX = 0;
|
||
let isDraggingImage = false;
|
||
let imageStartX = 0;
|
||
let imageStartOffsetX = 0;
|
||
let minOffsetX = 0;
|
||
let maxOffsetX = 0;
|
||
|
||
// 初始化
|
||
init();
|
||
|
||
function init() {
|
||
// 添加图片按钮
|
||
addImageBtn.addEventListener('click', () => {
|
||
fileInput.click();
|
||
});
|
||
|
||
// 层叠图按钮
|
||
addOverlayBtn.addEventListener('click', () => {
|
||
overlayInput.click();
|
||
});
|
||
|
||
// 层叠图文件选择
|
||
overlayInput.addEventListener('change', (e) => {
|
||
const file = e.target.files[0];
|
||
if (file) {
|
||
loadOverlayImage(file);
|
||
}
|
||
overlayInput.value = '';
|
||
});
|
||
|
||
// 添加文本按钮
|
||
addTextBtn.addEventListener('click', () => {
|
||
addTextInput();
|
||
});
|
||
|
||
// 文件选择
|
||
fileInput.addEventListener('change', (e) => {
|
||
const file = e.target.files[0];
|
||
if (file) {
|
||
loadImage(file);
|
||
}
|
||
fileInput.value = '';
|
||
});
|
||
|
||
// 保存按钮
|
||
saveBtn.addEventListener('click', saveImage);
|
||
|
||
// 初始化输入框
|
||
initInput(input1);
|
||
initInput(input2);
|
||
|
||
// 为原有两个输入框添加删除按钮
|
||
[input1, input2].forEach(inputEl => {
|
||
const deleteBtn = document.createElement('button');
|
||
deleteBtn.className = 'text-delete-btn';
|
||
deleteBtn.innerHTML = '×';
|
||
deleteBtn.addEventListener('click', (e) => {
|
||
e.stopPropagation();
|
||
inputEl.remove();
|
||
});
|
||
inputEl.appendChild(deleteBtn);
|
||
});
|
||
|
||
// 初始化图片滑动
|
||
initImageDrag();
|
||
|
||
// 点击空白区域取消选中
|
||
document.addEventListener('click', (e) => {
|
||
// 只有点击真正的空白区域(不是输入框,不是图片容器,不是工具栏)才取消选中
|
||
const isTextInput = e.target.closest('.text-input');
|
||
const isImageContainer = e.target.closest('.image-container');
|
||
const isToolbar = e.target.closest('.toolbar');
|
||
const isTextEditorToolbar = e.target.closest('.text-editor-toolbar');
|
||
|
||
if (!isTextInput && !isImageContainer && !isToolbar && !isTextEditorToolbar) {
|
||
deselectAll();
|
||
}
|
||
});
|
||
|
||
// 点击容器空白处取消选中
|
||
function handleContainerClick(e) {
|
||
const isBackground = e.target === imageContainer ||
|
||
e.target === bgLayer ||
|
||
e.target === bgImage ||
|
||
e.target.closest('.preview-bg-layer') ||
|
||
e.target === document.getElementById('emptyTip');
|
||
const isOverlayImage = e.target.closest('.overlay-image');
|
||
const isTextInput = e.target.closest('.text-input');
|
||
|
||
// 点击背景/空白区域
|
||
if (isBackground && !isOverlayImage) {
|
||
// 如果正在编辑,先结束编辑
|
||
if (editingInput) {
|
||
endEdit(editingInput);
|
||
}
|
||
// 移除输入框选中状态
|
||
if (selectedInput) {
|
||
selectedInput.classList.remove('selected');
|
||
selectedInput = null;
|
||
}
|
||
// 取消层叠图选中状态
|
||
if (selectedOverlay) {
|
||
selectedOverlay.classList.remove('selected');
|
||
selectedOverlay = null;
|
||
}
|
||
// 隐藏辅助线
|
||
hideGuides();
|
||
// 隐藏文本编辑器工具栏
|
||
textEditorToolbar.classList.remove('active');
|
||
}
|
||
|
||
// 点击空白区域(不是层叠图和输入框)
|
||
if (!isOverlayImage && !isTextInput && !isBackground) {
|
||
if (selectedOverlay) {
|
||
selectedOverlay.classList.remove('selected');
|
||
selectedOverlay = null;
|
||
}
|
||
}
|
||
}
|
||
|
||
imageContainer.addEventListener('click', handleContainerClick);
|
||
imageContainer.addEventListener('touchend', handleContainerClick);
|
||
|
||
// 初始化长按保存(只在3:5预览区域)
|
||
initLongPressSave();
|
||
|
||
// 初始化右键菜单(只在3:5预览区域)
|
||
initContextMenu();
|
||
|
||
// 初始化文本编辑器工具栏
|
||
initTextEditorToolbar();
|
||
|
||
// 显示长按提示
|
||
setTimeout(() => {
|
||
longPressTip.classList.add('show');
|
||
setTimeout(() => {
|
||
longPressTip.classList.remove('show');
|
||
}, 3000);
|
||
}, 1000);
|
||
}
|
||
|
||
// 加载图片
|
||
function loadImage(file) {
|
||
const reader = new FileReader();
|
||
reader.onload = (e) => {
|
||
const img = new Image();
|
||
img.onload = () => {
|
||
currentImage = img;
|
||
bgImage.src = img.src;
|
||
imageContainer.classList.remove('empty');
|
||
emptyTip.classList.add('hidden');
|
||
// 计算图片尺寸和边界
|
||
calculateImageBounds();
|
||
// 背景图选择后默认居中显示
|
||
imageOffsetX = 0;
|
||
updateImagePosition();
|
||
};
|
||
img.src = e.target.result;
|
||
};
|
||
reader.readAsDataURL(file);
|
||
}
|
||
|
||
// 添加文本输入框
|
||
function addTextInput() {
|
||
const container = imageContainer;
|
||
const containerWidth = container.clientWidth;
|
||
const containerHeight = container.clientHeight;
|
||
|
||
const textEl = document.createElement('div');
|
||
textEl.className = 'text-input';
|
||
textEl.contentEditable = 'false';
|
||
textEl.textContent = '新文本';
|
||
|
||
const left = Math.random() * (containerWidth * 0.6) + containerWidth * 0.1;
|
||
const top = Math.random() * (containerHeight * 0.6) + containerHeight * 0.1;
|
||
textEl.style.left = (left / containerWidth * 100) + '%';
|
||
textEl.style.top = (top / containerHeight * 100) + '%';
|
||
|
||
// 删除按钮
|
||
const deleteBtn = document.createElement('button');
|
||
deleteBtn.className = 'text-delete-btn';
|
||
deleteBtn.innerHTML = '×';
|
||
deleteBtn.addEventListener('click', (e) => {
|
||
e.stopPropagation();
|
||
textEl.remove();
|
||
});
|
||
textEl.appendChild(deleteBtn);
|
||
|
||
const inputLayer = container.querySelector('.preview-input-layer');
|
||
inputLayer.appendChild(textEl);
|
||
|
||
initInput(textEl);
|
||
|
||
selectInput(textEl);
|
||
}
|
||
|
||
// 加载层叠图
|
||
function loadOverlayImage(file) {
|
||
const reader = new FileReader();
|
||
reader.onload = (e) => {
|
||
const img = new Image();
|
||
img.onload = () => {
|
||
createOverlayElement(img);
|
||
};
|
||
img.src = e.target.result;
|
||
};
|
||
reader.readAsDataURL(file);
|
||
}
|
||
|
||
// 创建层叠图元素
|
||
function createOverlayElement(img) {
|
||
const container = imageContainer;
|
||
const containerWidth = container.clientWidth;
|
||
const containerHeight = container.clientHeight;
|
||
|
||
// 计算初始尺寸(不超过容器1/3)
|
||
let width = img.naturalWidth;
|
||
let height = img.naturalHeight;
|
||
const maxSize = Math.min(containerWidth, containerHeight) / 2;
|
||
|
||
if (width > maxSize || height > maxSize) {
|
||
if (width > height) {
|
||
height = (height / width) * maxSize;
|
||
width = maxSize;
|
||
} else {
|
||
width = (width / height) * maxSize;
|
||
height = maxSize;
|
||
}
|
||
}
|
||
|
||
// 创建层叠图元素
|
||
const overlayEl = document.createElement('div');
|
||
overlayEl.className = 'overlay-image';
|
||
overlayEl.dataset.scale = 1;
|
||
|
||
const imgEl = document.createElement('img');
|
||
imgEl.src = img.src;
|
||
imgEl.style.width = '100%';
|
||
imgEl.style.height = '100%';
|
||
imgEl.style.objectFit = 'contain';
|
||
overlayEl.appendChild(imgEl);
|
||
|
||
// 删除按钮
|
||
const deleteBtn = document.createElement('button');
|
||
deleteBtn.className = 'delete-btn';
|
||
deleteBtn.innerHTML = '×';
|
||
deleteBtn.addEventListener('click', (e) => {
|
||
e.stopPropagation();
|
||
overlayEl.remove();
|
||
});
|
||
overlayEl.appendChild(deleteBtn);
|
||
|
||
// 居中显示
|
||
overlayEl.style.left = (containerWidth / 2 - width / 2) + 'px';
|
||
overlayEl.style.top = (containerHeight / 2 - height / 2) + 'px';
|
||
overlayEl.style.width = width + 'px';
|
||
overlayEl.style.height = height + 'px';
|
||
|
||
// 初始化拖拽和缩放
|
||
initOverlayImage(overlayEl);
|
||
|
||
overlayLayer.appendChild(overlayEl);
|
||
|
||
// 选中新添加的层叠图
|
||
selectOverlay(overlayEl);
|
||
}
|
||
|
||
// 初始化层叠图交互
|
||
function initOverlayImage(overlayEl) {
|
||
let isDragging = false;
|
||
let isResizing = false;
|
||
let startX, startY, startLeft, startTop, startWidth, startHeight;
|
||
const minSize = 30;
|
||
|
||
// 点击选中(区分点击和拖拽)
|
||
overlayEl.addEventListener('click', (e) => {
|
||
e.stopPropagation();
|
||
selectOverlay(overlayEl);
|
||
});
|
||
|
||
// 拖拽移动(不选中也可拖动)
|
||
overlayEl.addEventListener('mousedown', handleDragStart);
|
||
overlayEl.addEventListener('touchstart', handleDragStart, { passive: false });
|
||
|
||
function handleDragStart(e) {
|
||
if (e.target.classList.contains('delete-btn') || e.target.classList.contains('resize-handle')) return;
|
||
|
||
e.stopPropagation(); // 阻止事件冒泡到 imageContainer
|
||
|
||
const isTouch = e.type === 'touchstart';
|
||
const clientX = isTouch ? e.touches[0].clientX : e.clientX;
|
||
const clientY = isTouch ? e.touches[0].clientY : e.clientY;
|
||
|
||
isDragging = true;
|
||
startX = clientX;
|
||
startY = clientY;
|
||
startLeft = overlayEl.offsetLeft;
|
||
startTop = overlayEl.offsetTop;
|
||
|
||
overlayEl.classList.add('dragging');
|
||
|
||
// 拖动时设置为选中状态
|
||
selectOverlay(overlayEl);
|
||
|
||
document.addEventListener('mousemove', handleDragMove);
|
||
document.addEventListener('mouseup', handleDragEnd);
|
||
document.addEventListener('touchmove', handleDragMove, { passive: false });
|
||
document.addEventListener('touchend', handleDragEnd);
|
||
}
|
||
|
||
function handleDragMove(e) {
|
||
if (!isDragging) return;
|
||
|
||
const isTouch = e.type === 'touchmove';
|
||
const clientX = isTouch ? e.touches[0].clientX : e.clientX;
|
||
const clientY = isTouch ? e.touches[0].clientY : e.clientY;
|
||
|
||
const dx = clientX - startX;
|
||
const dy = clientY - startY;
|
||
|
||
const moveDistance = Math.sqrt(dx * dx + dy * dy);
|
||
if (moveDistance > 5) {
|
||
let newLeft = startLeft + dx;
|
||
let newTop = startTop + dy;
|
||
|
||
// 使用吸附功能
|
||
const snapped = getSnapPosition(overlayEl, newLeft, newTop);
|
||
newLeft = snapped.left;
|
||
newTop = snapped.top;
|
||
|
||
// 显示对齐辅助线
|
||
if (snapped.showGuide) {
|
||
showAlignmentGuides(overlayEl);
|
||
} else {
|
||
hideGuides();
|
||
}
|
||
|
||
overlayEl.style.left = newLeft + 'px';
|
||
overlayEl.style.top = newTop + 'px';
|
||
}
|
||
|
||
e.preventDefault();
|
||
}
|
||
|
||
function handleDragEnd() {
|
||
isDragging = false;
|
||
overlayEl.classList.remove('dragging');
|
||
hideGuides();
|
||
|
||
document.removeEventListener('mousemove', handleDragMove);
|
||
document.removeEventListener('mouseup', handleDragEnd);
|
||
document.removeEventListener('touchmove', handleDragMove);
|
||
document.removeEventListener('touchend', handleDragEnd);
|
||
}
|
||
|
||
// 缩放控制(四角)
|
||
const corners = ['tl', 'tr', 'bl', 'br'];
|
||
corners.forEach(corner => {
|
||
const handle = document.createElement('div');
|
||
handle.className = 'resize-handle resize-' + corner;
|
||
handle.style.cssText = `
|
||
position: absolute;
|
||
width: 12px;
|
||
height: 12px;
|
||
background: #4a90e2;
|
||
border: 1px solid #fff;
|
||
`;
|
||
|
||
switch(corner) {
|
||
case 'tl': handle.style.top = '-6px'; handle.style.left = '-6px'; handle.style.cursor = 'nw-resize'; break;
|
||
case 'tr': handle.style.top = '-6px'; handle.style.right = '-6px'; handle.style.cursor = 'ne-resize'; break;
|
||
case 'bl': handle.style.bottom = '-6px'; handle.style.left = '-6px'; handle.style.cursor = 'sw-resize'; break;
|
||
case 'br': handle.style.bottom = '-6px'; handle.style.right = '-6px'; handle.style.cursor = 'se-resize'; break;
|
||
}
|
||
|
||
handle.addEventListener('mousedown', startResize);
|
||
handle.addEventListener('touchstart', startResize, { passive: false });
|
||
|
||
overlayEl.appendChild(handle);
|
||
|
||
function startResize(e) {
|
||
e.stopPropagation();
|
||
isResizing = true;
|
||
const isTouch = e.type === 'touchstart';
|
||
const clientX = isTouch ? e.touches[0].clientX : e.clientX;
|
||
const clientY = isTouch ? e.touches[0].clientY : e.clientY;
|
||
|
||
startX = clientX;
|
||
startY = clientY;
|
||
startLeft = overlayEl.offsetLeft;
|
||
startTop = overlayEl.offsetTop;
|
||
startWidth = overlayEl.offsetWidth;
|
||
startHeight = overlayEl.offsetHeight;
|
||
|
||
document.addEventListener('mousemove', handleResize);
|
||
document.addEventListener('mouseup', endResize);
|
||
document.addEventListener('touchmove', handleResize, { passive: false });
|
||
document.addEventListener('touchend', endResize);
|
||
|
||
e.preventDefault();
|
||
}
|
||
|
||
function handleResize(e) {
|
||
if (!isResizing) return;
|
||
|
||
const isTouch = e.type === 'touchmove';
|
||
const clientX = isTouch ? e.touches[0].clientX : e.clientX;
|
||
const clientY = isTouch ? e.touches[0].clientY : e.clientY;
|
||
|
||
const dx = clientX - startX;
|
||
const dy = clientY - startY;
|
||
|
||
let newWidth = startWidth;
|
||
let newHeight = startHeight;
|
||
let newLeft = startLeft;
|
||
let newTop = startTop;
|
||
|
||
const aspectRatio = startWidth / startHeight;
|
||
|
||
// 根据角落调整尺寸
|
||
if (corner === 'br') {
|
||
newWidth = Math.max(minSize, startWidth + dx);
|
||
newHeight = newWidth / aspectRatio;
|
||
} else if (corner === 'bl') {
|
||
newWidth = Math.max(minSize, startWidth - dx);
|
||
newHeight = newWidth / aspectRatio;
|
||
newLeft = startLeft - (newWidth - startWidth);
|
||
} else if (corner === 'tr') {
|
||
newWidth = Math.max(minSize, startWidth + dx);
|
||
newHeight = newWidth / aspectRatio;
|
||
newTop = startTop - (newHeight - startHeight);
|
||
} else if (corner === 'tl') {
|
||
newWidth = Math.max(minSize, startWidth - dx);
|
||
newHeight = newWidth / aspectRatio;
|
||
newLeft = startLeft - (newWidth - startWidth);
|
||
newTop = startTop - (newHeight - startHeight);
|
||
}
|
||
|
||
overlayEl.style.width = newWidth + 'px';
|
||
overlayEl.style.height = newHeight + 'px';
|
||
overlayEl.style.left = newLeft + 'px';
|
||
overlayEl.style.top = newTop + 'px';
|
||
|
||
e.preventDefault();
|
||
}
|
||
|
||
function endResize() {
|
||
isResizing = false;
|
||
document.removeEventListener('mousemove', handleResize);
|
||
document.removeEventListener('mouseup', endResize);
|
||
document.removeEventListener('touchmove', handleResize);
|
||
document.removeEventListener('touchend', endResize);
|
||
}
|
||
});
|
||
}
|
||
|
||
// 选中层叠图(同时取消输入框选中)
|
||
let selectedOverlay = null;
|
||
function selectOverlay(overlayEl) {
|
||
if (selectedInput) {
|
||
selectedInput.classList.remove('selected');
|
||
selectedInput = null;
|
||
}
|
||
if (editingInput) {
|
||
endEdit(editingInput);
|
||
}
|
||
if (selectedOverlay && selectedOverlay !== overlayEl) {
|
||
selectedOverlay.classList.remove('selected');
|
||
}
|
||
selectedOverlay = overlayEl;
|
||
overlayEl.classList.add('selected');
|
||
}
|
||
|
||
// 计算图片滑动边界
|
||
function calculateImageBounds() {
|
||
if (!currentImage) return;
|
||
|
||
const containerWidth = imageContainer.clientWidth;
|
||
const containerHeight = imageContainer.clientHeight;
|
||
const imgRatio = currentImage.naturalWidth / currentImage.naturalHeight;
|
||
const containerRatio = containerWidth / containerHeight;
|
||
|
||
let imgWidth, imgHeight;
|
||
|
||
if (imgRatio > containerRatio) {
|
||
// 图片更宽,以高度为基准计算宽度
|
||
imgHeight = containerHeight;
|
||
imgWidth = imgHeight * imgRatio;
|
||
} else {
|
||
// 图片更高或等比,以宽度为基准
|
||
imgWidth = containerWidth;
|
||
imgHeight = imgWidth / imgRatio;
|
||
}
|
||
// 计算滑动边界:严格以图片边缘为边界,不能露出白板
|
||
// 滑动公式:imageOffsetX = imageStartOffsetX + deltaX
|
||
// 手指向右滑动(deltaX>0) → imageOffsetX增大 → 图片右移,显示右侧
|
||
// 手指向左滑动(deltaX<0) → imageOffsetX减小 → 图片左移,显示左侧
|
||
// 初始位置居中:imageOffsetX = 0,此时 baseLeft 已使图片居中
|
||
if (imgWidth > containerWidth) {
|
||
// 图片宽度大于容器,可以滑动
|
||
// minOffsetX = -(imgWidth - containerWidth) / 2:向左滑到极限(显示左侧)
|
||
// maxOffsetX = (imgWidth - containerWidth) / 2:向右滑到极限(显示右侧)
|
||
const halfRange = (imgWidth - containerWidth) / 2;
|
||
minOffsetX = -halfRange;
|
||
maxOffsetX = halfRange;
|
||
} else {
|
||
// 图片宽度小于等于容器,居中显示,不能滑动
|
||
minOffsetX = 0;
|
||
maxOffsetX = 0;
|
||
}
|
||
}
|
||
|
||
// 更新图片位置
|
||
function updateImagePosition() {
|
||
if (!currentImage) return;
|
||
|
||
// 限制滑动范围,确保不露出白板
|
||
imageOffsetX = Math.max(minOffsetX, Math.min(maxOffsetX, imageOffsetX));
|
||
|
||
const containerWidth = imageContainer.clientWidth;
|
||
const containerHeight = imageContainer.clientHeight;
|
||
const imgRatio = currentImage.naturalWidth / currentImage.naturalHeight;
|
||
const containerRatio = containerWidth / containerHeight;
|
||
|
||
let imgWidth, imgHeight;
|
||
|
||
if (imgRatio > containerRatio) {
|
||
imgHeight = containerHeight;
|
||
imgWidth = imgHeight * imgRatio;
|
||
} else {
|
||
imgWidth = containerWidth;
|
||
imgHeight = imgWidth / imgRatio;
|
||
}
|
||
|
||
// 应用位置
|
||
const baseLeft = (containerWidth - imgWidth) / 2;
|
||
bgImage.style.width = imgWidth + 'px';
|
||
bgImage.style.height = imgHeight + 'px';
|
||
bgImage.style.left = (baseLeft + imageOffsetX) + 'px';
|
||
bgImage.style.transform = 'none';
|
||
}
|
||
|
||
// 初始化图片滑动调整位置
|
||
function initImageDrag() {
|
||
// 鼠标按下
|
||
imageContainer.addEventListener('mousedown', handleImageStart);
|
||
imageContainer.addEventListener('touchstart', handleImageStart, { passive: false });
|
||
|
||
// 鼠标移动
|
||
document.addEventListener('mousemove', handleImageMove);
|
||
document.addEventListener('touchmove', handleImageMove, { passive: false });
|
||
|
||
// 鼠标释放
|
||
document.addEventListener('mouseup', handleImageEnd);
|
||
document.addEventListener('touchend', handleImageEnd);
|
||
|
||
function handleImageStart(e) {
|
||
// 如果点击的是输入框,不处理图片滑动
|
||
if (e.target.closest('.text-input')) {
|
||
return;
|
||
}
|
||
|
||
// 如果正在编辑,先退出编辑状态
|
||
if (editingInput) {
|
||
deselectAll();
|
||
}
|
||
|
||
// 如果没有图片,不处理
|
||
if (!currentImage) {
|
||
return;
|
||
}
|
||
|
||
const isTouch = e.type === 'touchstart';
|
||
const clientX = isTouch ? e.touches[0].clientX : e.clientX;
|
||
|
||
// 只有点击图片本身或背景图层时才启动图片滑动
|
||
const isImageOrBgLayer = e.target === bgImage ||
|
||
e.target === bgLayer ||
|
||
e.target === imageContainer;
|
||
|
||
if (!isImageOrBgLayer) {
|
||
return;
|
||
}
|
||
|
||
isDraggingImage = true;
|
||
imageStartX = clientX;
|
||
imageStartOffsetX = imageOffsetX;
|
||
|
||
if (isTouch) {
|
||
e.preventDefault();
|
||
}
|
||
}
|
||
|
||
function handleImageMove(e) {
|
||
if (!isDraggingImage) return;
|
||
|
||
const isTouch = e.type === 'touchmove';
|
||
const clientX = isTouch ? e.touches[0].clientX : e.clientX;
|
||
|
||
const deltaX = clientX - imageStartX;
|
||
// 反转拖拽:手指向右滑动(deltaX>0) → imageOffsetX增大 → 图片右移,显示右侧
|
||
// 手指向左滑动(deltaX<0) → imageOffsetX减小 → 图片左移,显示左侧
|
||
imageOffsetX = imageStartOffsetX + deltaX;
|
||
|
||
updateImagePosition();
|
||
|
||
if (isTouch) {
|
||
e.preventDefault();
|
||
}
|
||
}
|
||
|
||
function handleImageEnd() {
|
||
isDraggingImage = false;
|
||
}
|
||
}
|
||
|
||
// 初始化输入框 - 每个输入框独立管理自己的拖拽状态
|
||
function initInput(element) {
|
||
let isMouseDown = false;
|
||
let isDragging = false;
|
||
let dragTimer = null;
|
||
let startX, startY, startLeft, startTop;
|
||
let clickStartTime = 0;
|
||
let clickStartX = 0;
|
||
let clickStartY = 0;
|
||
let hasMoved = false;
|
||
|
||
// 鼠标/触摸按下
|
||
element.addEventListener('mousedown', handleStart);
|
||
element.addEventListener('touchstart', handleStart, { passive: false });
|
||
|
||
function handleStart(e) {
|
||
const isTouch = e.type === 'touchstart';
|
||
|
||
// 如果正在编辑此输入框,不处理拖拽事件,让浏览器处理文本选择
|
||
if (editingInput === element) {
|
||
return;
|
||
}
|
||
|
||
const clientX = isTouch ? e.touches[0].clientX : e.clientX;
|
||
const clientY = isTouch ? e.touches[0].clientY : e.clientY;
|
||
|
||
isMouseDown = true;
|
||
clickStartTime = Date.now();
|
||
clickStartX = clientX;
|
||
clickStartY = clientY;
|
||
hasMoved = false;
|
||
isDragging = false;
|
||
|
||
// 选中此输入框
|
||
selectInput(element);
|
||
|
||
startX = clientX;
|
||
startY = clientY;
|
||
startLeft = element.offsetLeft;
|
||
startTop = element.offsetTop;
|
||
|
||
// 延迟判断是否为拖拽
|
||
dragTimer = setTimeout(() => {
|
||
if (isMouseDown) {
|
||
isDragging = true;
|
||
element.classList.add('dragging');
|
||
}
|
||
}, 150);
|
||
|
||
// 阻止事件冒泡,避免触发图片滑动
|
||
e.stopPropagation();
|
||
}
|
||
|
||
// 鼠标/触摸移动
|
||
element.addEventListener('mousemove', handleMove);
|
||
element.addEventListener('touchmove', handleMove, { passive: false });
|
||
|
||
function handleMove(e) {
|
||
// 如果正在编辑,不拦截事件,让浏览器处理文本选择
|
||
if (editingInput === element) {
|
||
return;
|
||
}
|
||
|
||
if (!isMouseDown) return;
|
||
if (!selectedInput) return;
|
||
|
||
const isTouch = e.type === 'touchmove';
|
||
const clientX = isTouch ? e.touches[0].clientX : e.clientX;
|
||
const clientY = isTouch ? e.touches[0].clientY : e.clientY;
|
||
|
||
const moveDistance = Math.sqrt(
|
||
Math.pow(clientX - clickStartX, 2) +
|
||
Math.pow(clientY - clickStartY, 2)
|
||
);
|
||
|
||
if (moveDistance > 5) {
|
||
hasMoved = true;
|
||
if (dragTimer) {
|
||
clearTimeout(dragTimer);
|
||
dragTimer = null;
|
||
}
|
||
if (!isDragging) {
|
||
isDragging = true;
|
||
element.classList.add('dragging');
|
||
}
|
||
}
|
||
|
||
if (isDragging) {
|
||
const dx = clientX - startX;
|
||
const dy = clientY - startY;
|
||
const newLeft = startLeft + dx;
|
||
const newTop = startTop + dy;
|
||
|
||
// 检测对齐并吸附
|
||
const snapped = getSnapPosition(element, newLeft, newTop);
|
||
element.style.left = snapped.left + 'px';
|
||
element.style.top = snapped.top + 'px';
|
||
|
||
// 显示对齐辅助线
|
||
if (snapped.showGuide) {
|
||
showAlignmentGuides(element);
|
||
} else {
|
||
hideGuides();
|
||
}
|
||
}
|
||
|
||
// 阻止事件冒泡,避免触发图片滑动
|
||
e.stopPropagation();
|
||
}
|
||
|
||
// 鼠标/触摸释放
|
||
element.addEventListener('mouseup', handleEnd);
|
||
element.addEventListener('touchend', handleEnd);
|
||
|
||
element.addEventListener('mouseleave', (e) => {
|
||
if (isMouseDown) {
|
||
handleEnd(e);
|
||
}
|
||
});
|
||
|
||
function handleEnd(e) {
|
||
isMouseDown = false;
|
||
|
||
if (dragTimer) {
|
||
clearTimeout(dragTimer);
|
||
dragTimer = null;
|
||
}
|
||
|
||
if (isDragging) {
|
||
isDragging = false;
|
||
element.classList.remove('dragging');
|
||
hideGuides();
|
||
}
|
||
|
||
// 阻止事件冒泡
|
||
if (e) {
|
||
e.stopPropagation();
|
||
}
|
||
}
|
||
|
||
// 双击进入编辑模式(电脑端和移动端通用)
|
||
element.addEventListener('dblclick', (e) => {
|
||
e.stopPropagation();
|
||
startEdit(element);
|
||
element.dataset.justEdited = 'true';
|
||
setTimeout(() => { element.dataset.justEdited = 'false'; }, 200);
|
||
});
|
||
|
||
// 单击选中显示删除按钮
|
||
element.addEventListener('click', (e) => {
|
||
if (hasMoved) return;
|
||
if (element.dataset.justEdited === 'true') return;
|
||
element.classList.add('selected');
|
||
});
|
||
|
||
// 触摸双击检测(移动端双击进入编辑)
|
||
let lastTapTime = 0;
|
||
element.addEventListener('touchend', (e) => {
|
||
const currentTime = Date.now();
|
||
if (currentTime - lastTapTime < 300) {
|
||
e.stopPropagation();
|
||
startEdit(element);
|
||
}
|
||
lastTapTime = currentTime;
|
||
});
|
||
}
|
||
|
||
// 选中输入框
|
||
function selectInput(element) {
|
||
if (selectedOverlay) {
|
||
selectedOverlay.classList.remove('selected');
|
||
selectedOverlay = null;
|
||
}
|
||
if (editingInput && editingInput !== element) {
|
||
endEdit(editingInput);
|
||
}
|
||
|
||
if (selectedInput && selectedInput !== element) {
|
||
selectedInput.classList.remove('selected');
|
||
}
|
||
|
||
selectedInput = element;
|
||
element.classList.add('selected');
|
||
element.classList.remove('editing');
|
||
element.contentEditable = 'false';
|
||
|
||
// 显示文本编辑器工具栏
|
||
textEditorToolbar.classList.add('active');
|
||
|
||
// 更新工具栏状态
|
||
updateToolbarState(element);
|
||
}
|
||
|
||
// 开始编辑
|
||
function startEdit(element) {
|
||
if (editingInput && editingInput !== element) {
|
||
endEdit(editingInput);
|
||
}
|
||
|
||
editingInput = element;
|
||
selectedInput = element;
|
||
|
||
element.classList.add('editing');
|
||
element.classList.remove('selected');
|
||
element.contentEditable = 'true';
|
||
|
||
// 显示文本编辑器工具栏
|
||
textEditorToolbar.classList.add('active');
|
||
|
||
// 更新工具栏状态
|
||
updateToolbarState(element);
|
||
|
||
// 如果没有选中文本,则定位光标到末尾
|
||
setTimeout(() => {
|
||
const sel = window.getSelection();
|
||
if (sel.isCollapsed) {
|
||
const range = document.createRange();
|
||
range.selectNodeContents(element);
|
||
range.collapse(false);
|
||
sel.removeAllRanges();
|
||
sel.addRange(range);
|
||
}
|
||
element.focus();
|
||
}, 10);
|
||
|
||
if ('virtualKeyboard' in navigator) {
|
||
navigator.virtualKeyboard.show();
|
||
}
|
||
}
|
||
|
||
// 结束编辑
|
||
function endEdit(element) {
|
||
element.classList.remove('editing');
|
||
element.classList.add('selected');
|
||
element.contentEditable = 'false';
|
||
element.blur();
|
||
editingInput = null;
|
||
// 隐藏文本编辑器工具栏
|
||
textEditorToolbar.classList.remove('active');
|
||
}
|
||
|
||
// 更新工具栏状态
|
||
function updateToolbarState(element) {
|
||
const style = window.getComputedStyle(element);
|
||
const fontWeight = style.fontWeight;
|
||
const fontStyle = style.fontStyle;
|
||
const color = style.color;
|
||
const fontFamily = style.fontFamily.replace(/['"]/g, '');
|
||
const fontSize = parseInt(style.fontSize);
|
||
|
||
// 更新加粗按钮状态
|
||
boldBtn.classList.toggle('active', fontWeight === 'bold' || fontWeight === '700' || parseInt(fontWeight) >= 700);
|
||
|
||
// 更新斜体按钮状态
|
||
italicBtn.classList.toggle('active', fontStyle === 'italic');
|
||
|
||
// 更新颜色选择器
|
||
colorPicker.value = rgbToHex(color);
|
||
|
||
// 更新字体选择器
|
||
for (let option of fontPicker.options) {
|
||
if (option.value === fontFamily || fontFamily.includes(option.value)) {
|
||
fontPicker.value = option.value;
|
||
break;
|
||
}
|
||
}
|
||
|
||
// 更新字号选择器
|
||
const fontSizeValueEl = fontSizePicker.querySelector('.roller-value');
|
||
if (fontSizeValueEl) {
|
||
const displaySize = isNaN(fontSize) ? 16 : fontSize;
|
||
fontSizeValueEl.textContent = displaySize.toString();
|
||
fontSizeValue = displaySize;
|
||
}
|
||
}
|
||
|
||
// 获取吸附后的位置
|
||
function getSnapPosition(draggedEl, newLeft, newTop) {
|
||
const container = imageContainer;
|
||
const containerRect = container.getBoundingClientRect();
|
||
|
||
const elWidth = draggedEl.offsetWidth;
|
||
const elHeight = draggedEl.offsetHeight;
|
||
|
||
// 计算元素各参考点相对容器的位置
|
||
const elCenterX = newLeft + elWidth / 2;
|
||
const elCenterY = newTop + elHeight / 2;
|
||
const elLeft = newLeft;
|
||
const elRight = newLeft + elWidth;
|
||
const elTop = newTop;
|
||
const elBottom = newTop + elHeight;
|
||
|
||
// 容器尺寸
|
||
const cWidth = containerRect.width;
|
||
const cHeight = containerRect.height;
|
||
const cCenterX = cWidth / 2;
|
||
const cCenterY = cHeight / 2;
|
||
|
||
const threshold = 5;
|
||
let snapLeft = newLeft;
|
||
let snapTop = newTop;
|
||
let showGuide = false;
|
||
|
||
// 水平垂直分别检测
|
||
let snapX = false;
|
||
let snapY = false;
|
||
let snapXPos = null;
|
||
let snapYPos = null;
|
||
|
||
// 检测容器中心对齐
|
||
if (Math.abs(elCenterX - cCenterX) < threshold) {
|
||
snapX = true;
|
||
snapXPos = cCenterX - elWidth / 2;
|
||
}
|
||
if (Math.abs(elCenterY - cCenterY) < threshold) {
|
||
snapY = true;
|
||
snapYPos = cCenterY - elHeight / 2;
|
||
}
|
||
|
||
// 检测容器边缘对齐
|
||
if (Math.abs(elLeft) < threshold) {
|
||
snapX = true;
|
||
snapXPos = 0;
|
||
}
|
||
if (Math.abs(elRight - cWidth) < threshold) {
|
||
snapX = true;
|
||
snapXPos = cWidth;
|
||
}
|
||
if (Math.abs(elTop) < threshold) {
|
||
snapY = true;
|
||
snapYPos = 0;
|
||
}
|
||
if (Math.abs(elBottom - cHeight) < threshold) {
|
||
snapY = true;
|
||
snapYPos = cHeight;
|
||
}
|
||
|
||
// 检测其他元素对齐(输入框和层叠图)
|
||
const otherInputs = [input1, input2].filter(el => el && el !== draggedEl);
|
||
const otherOverlays = Array.from(overlayLayer.querySelectorAll('.overlay-image'))
|
||
.filter(el => el !== draggedEl);
|
||
|
||
for (const otherEl of otherInputs) {
|
||
if (!otherEl) continue;
|
||
|
||
const oLeft = otherEl.offsetLeft;
|
||
const oTop = otherEl.offsetTop;
|
||
const oWidth = otherEl.offsetWidth;
|
||
const oHeight = otherEl.offsetHeight;
|
||
const oRight = oLeft + oWidth;
|
||
const oBottom = oTop + oHeight;
|
||
const oCenterX = oLeft + oWidth / 2;
|
||
const oCenterY = oTop + oHeight / 2;
|
||
|
||
// 垂直边缘对齐
|
||
if (Math.abs(elLeft - oLeft) < threshold) {
|
||
snapX = true;
|
||
snapXPos = oLeft;
|
||
}
|
||
if (Math.abs(elRight - oRight) < threshold) {
|
||
snapX = true;
|
||
snapXPos = oRight - elWidth;
|
||
}
|
||
if (Math.abs(elLeft - oRight) < threshold) {
|
||
snapX = true;
|
||
snapXPos = oRight;
|
||
}
|
||
if (Math.abs(elRight - oLeft) < threshold) {
|
||
snapX = true;
|
||
snapXPos = oLeft - elWidth;
|
||
}
|
||
|
||
// 水平边缘对齐
|
||
if (Math.abs(elTop - oTop) < threshold) {
|
||
snapY = true;
|
||
snapYPos = oTop;
|
||
}
|
||
if (Math.abs(elBottom - oBottom) < threshold) {
|
||
snapY = true;
|
||
snapYPos = oBottom - elHeight;
|
||
}
|
||
if (Math.abs(elTop - oBottom) < threshold) {
|
||
snapY = true;
|
||
snapYPos = oBottom;
|
||
}
|
||
if (Math.abs(elBottom - oTop) < threshold) {
|
||
snapY = true;
|
||
snapYPos = oTop - elHeight;
|
||
}
|
||
|
||
// 中心对齐
|
||
if (Math.abs(elCenterX - oCenterX) < threshold) {
|
||
snapX = true;
|
||
snapXPos = oCenterX - elWidth / 2;
|
||
}
|
||
if (Math.abs(elCenterY - oCenterY) < threshold) {
|
||
snapY = true;
|
||
snapYPos = oCenterY - elHeight / 2;
|
||
}
|
||
}
|
||
|
||
// 检测层叠图之间对齐
|
||
for (const otherEl of otherOverlays) {
|
||
if (!otherEl) continue;
|
||
|
||
const oLeft = otherEl.offsetLeft;
|
||
const oTop = otherEl.offsetTop;
|
||
const oWidth = otherEl.offsetWidth;
|
||
const oHeight = otherEl.offsetHeight;
|
||
const oRight = oLeft + oWidth;
|
||
const oBottom = oTop + oHeight;
|
||
const oCenterX = oLeft + oWidth / 2;
|
||
const oCenterY = oTop + oHeight / 2;
|
||
|
||
// 垂直边缘对齐
|
||
if (Math.abs(elLeft - oLeft) < threshold) {
|
||
snapX = true;
|
||
snapXPos = oLeft;
|
||
}
|
||
if (Math.abs(elRight - oRight) < threshold) {
|
||
snapX = true;
|
||
snapXPos = oRight - elWidth;
|
||
}
|
||
if (Math.abs(elLeft - oRight) < threshold) {
|
||
snapX = true;
|
||
snapXPos = oRight;
|
||
}
|
||
if (Math.abs(elRight - oLeft) < threshold) {
|
||
snapX = true;
|
||
snapXPos = oLeft - elWidth;
|
||
}
|
||
|
||
// 水平边缘对齐
|
||
if (Math.abs(elTop - oTop) < threshold) {
|
||
snapY = true;
|
||
snapYPos = oTop;
|
||
}
|
||
if (Math.abs(elBottom - oBottom) < threshold) {
|
||
snapY = true;
|
||
snapYPos = oBottom - elHeight;
|
||
}
|
||
if (Math.abs(elTop - oBottom) < threshold) {
|
||
snapY = true;
|
||
snapYPos = oBottom;
|
||
}
|
||
if (Math.abs(elBottom - oTop) < threshold) {
|
||
snapY = true;
|
||
snapYPos = oTop - elHeight;
|
||
}
|
||
|
||
// 中心对齐
|
||
if (Math.abs(elCenterX - oCenterX) < threshold) {
|
||
snapX = true;
|
||
snapXPos = oCenterX - elWidth / 2;
|
||
}
|
||
if (Math.abs(elCenterY - oCenterY) < threshold) {
|
||
snapY = true;
|
||
snapYPos = oCenterY - elHeight / 2;
|
||
}
|
||
}
|
||
|
||
// 应用吸附
|
||
if (snapX && snapXPos !== null) {
|
||
snapLeft = snapXPos;
|
||
showGuide = true;
|
||
}
|
||
if (snapY && snapYPos !== null) {
|
||
snapTop = snapYPos;
|
||
showGuide = true;
|
||
}
|
||
|
||
// 边界限制
|
||
snapLeft = Math.max(0, Math.min(snapLeft, cWidth - elWidth));
|
||
snapTop = Math.max(0, Math.min(snapTop, cHeight - elHeight));
|
||
|
||
return { left: snapLeft, top: snapTop, showGuide };
|
||
}
|
||
|
||
// RGB转十六进制
|
||
function rgbToHex(rgb) {
|
||
if (rgb.startsWith('#')) return rgb;
|
||
const match = rgb.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/);
|
||
if (!match) return '#000000';
|
||
return '#' + [match[1], match[2], match[3]].map(x => parseInt(x).toString(16).padStart(2, '0')).join('');
|
||
}
|
||
|
||
// 对齐辅助线 - 水平和垂直居中、对齐其他元素
|
||
function showAlignmentGuides(draggedEl) {
|
||
const container = imageContainer;
|
||
const cWidth = container.offsetWidth;
|
||
const cHeight = container.offsetHeight;
|
||
|
||
const elWidth = draggedEl.offsetWidth;
|
||
const elHeight = draggedEl.offsetHeight;
|
||
const elLeft = draggedEl.offsetLeft;
|
||
const elTop = draggedEl.offsetTop;
|
||
const elRight = elLeft + elWidth;
|
||
const elBottom = elTop + elHeight;
|
||
const elCenterX = elLeft + elWidth / 2;
|
||
const elCenterY = elTop + elHeight / 2;
|
||
|
||
const threshold = 5;
|
||
let guideHPos = null;
|
||
let guideVPos = null;
|
||
|
||
// 容器中心对齐
|
||
const cCenterX = cWidth / 2;
|
||
const cCenterY = cHeight / 2;
|
||
|
||
if (Math.abs(elCenterX - cCenterX) < threshold) {
|
||
guideVPos = cCenterX;
|
||
}
|
||
if (Math.abs(elCenterY - cCenterY) < threshold) {
|
||
guideHPos = cCenterY;
|
||
}
|
||
|
||
// 容器边缘对齐
|
||
if (Math.abs(elLeft) < threshold) {
|
||
guideVPos = 0;
|
||
}
|
||
if (Math.abs(elRight - cWidth) < threshold) {
|
||
guideVPos = cWidth;
|
||
}
|
||
if (Math.abs(elTop) < threshold) {
|
||
guideHPos = 0;
|
||
}
|
||
if (Math.abs(elBottom - cHeight) < threshold) {
|
||
guideHPos = cHeight;
|
||
}
|
||
|
||
// 其他输入框对齐检测
|
||
const otherInputs = [input1, input2].filter(el => el && el !== draggedEl);
|
||
const otherOverlays = Array.from(overlayLayer.querySelectorAll('.overlay-image'))
|
||
.filter(el => el !== draggedEl);
|
||
|
||
for (const otherEl of otherInputs) {
|
||
if (!otherEl) continue;
|
||
|
||
const oLeft = otherEl.offsetLeft;
|
||
const oTop = otherEl.offsetTop;
|
||
const oWidth = otherEl.offsetWidth;
|
||
const oHeight = otherEl.offsetHeight;
|
||
const oRight = oLeft + oWidth;
|
||
const oBottom = oTop + oHeight;
|
||
const oCenterX = oLeft + oWidth / 2;
|
||
const oCenterY = oTop + oHeight / 2;
|
||
|
||
// 垂直边缘对齐
|
||
if (Math.abs(elLeft - oLeft) < threshold) {
|
||
guideVPos = oLeft;
|
||
}
|
||
if (Math.abs(elRight - oRight) < threshold) {
|
||
guideVPos = oRight;
|
||
}
|
||
if (Math.abs(elLeft - oRight) < threshold) {
|
||
guideVPos = oRight;
|
||
}
|
||
if (Math.abs(elRight - oLeft) < threshold) {
|
||
guideVPos = oLeft;
|
||
}
|
||
|
||
// 水平边缘对齐
|
||
if (Math.abs(elTop - oTop) < threshold) {
|
||
guideHPos = oTop;
|
||
}
|
||
if (Math.abs(elBottom - oBottom) < threshold) {
|
||
guideHPos = oBottom;
|
||
}
|
||
if (Math.abs(elTop - oBottom) < threshold) {
|
||
guideHPos = oBottom;
|
||
}
|
||
if (Math.abs(elBottom - oTop) < threshold) {
|
||
guideHPos = oTop;
|
||
}
|
||
|
||
// 中心对齐
|
||
if (Math.abs(elCenterX - oCenterX) < threshold) {
|
||
guideVPos = oCenterX;
|
||
}
|
||
if (Math.abs(elCenterY - oCenterY) < threshold) {
|
||
guideHPos = oCenterY;
|
||
}
|
||
}
|
||
|
||
// 其他层叠图对齐检测
|
||
for (const otherEl of otherOverlays) {
|
||
if (!otherEl) continue;
|
||
|
||
const oLeft = otherEl.offsetLeft;
|
||
const oTop = otherEl.offsetTop;
|
||
const oWidth = otherEl.offsetWidth;
|
||
const oHeight = otherEl.offsetHeight;
|
||
const oRight = oLeft + oWidth;
|
||
const oBottom = oTop + oHeight;
|
||
const oCenterX = oLeft + oWidth / 2;
|
||
const oCenterY = oTop + oHeight / 2;
|
||
|
||
// 垂直边缘对齐
|
||
if (Math.abs(elLeft - oLeft) < threshold) {
|
||
guideVPos = oLeft;
|
||
}
|
||
if (Math.abs(elRight - oRight) < threshold) {
|
||
guideVPos = oRight;
|
||
}
|
||
if (Math.abs(elLeft - oRight) < threshold) {
|
||
guideVPos = oRight;
|
||
}
|
||
if (Math.abs(elRight - oLeft) < threshold) {
|
||
guideVPos = oLeft;
|
||
}
|
||
|
||
// 水平边缘对齐
|
||
if (Math.abs(elTop - oTop) < threshold) {
|
||
guideHPos = oTop;
|
||
}
|
||
if (Math.abs(elBottom - oBottom) < threshold) {
|
||
guideHPos = oBottom;
|
||
}
|
||
if (Math.abs(elTop - oBottom) < threshold) {
|
||
guideHPos = oBottom;
|
||
}
|
||
if (Math.abs(elBottom - oTop) < threshold) {
|
||
guideHPos = oTop;
|
||
}
|
||
|
||
// 中心对齐
|
||
if (Math.abs(elCenterX - oCenterX) < threshold) {
|
||
guideVPos = oCenterX;
|
||
}
|
||
if (Math.abs(elCenterY - oCenterY) < threshold) {
|
||
guideHPos = oCenterY;
|
||
}
|
||
}
|
||
|
||
if (guideHPos !== null) {
|
||
guideH.style.top = guideHPos + 'px';
|
||
guideH.classList.add('visible');
|
||
} else {
|
||
guideH.classList.remove('visible');
|
||
}
|
||
|
||
if (guideVPos !== null) {
|
||
guideV.style.left = guideVPos + 'px';
|
||
guideV.classList.add('visible');
|
||
} else {
|
||
guideV.classList.remove('visible');
|
||
}
|
||
}
|
||
|
||
function hideGuides() {
|
||
guideH.classList.remove('visible');
|
||
guideV.classList.remove('visible');
|
||
}
|
||
|
||
// 取消所有选中
|
||
function deselectAll() {
|
||
if (editingInput) {
|
||
endEdit(editingInput);
|
||
}
|
||
if (selectedInput) {
|
||
selectedInput.classList.remove('selected');
|
||
selectedInput = null;
|
||
}
|
||
// 隐藏文本编辑器工具栏
|
||
textEditorToolbar.classList.remove('active');
|
||
}
|
||
|
||
// 初始化长按保存(只在3:5预览区域)
|
||
function initLongPressSave() {
|
||
// 只在图片容器上触发长按保存
|
||
imageContainer.addEventListener('touchstart', (e) => {
|
||
// 如果触摸的是输入框,不触发长按保存
|
||
if (e.target.closest('.text-input')) {
|
||
return;
|
||
}
|
||
|
||
longPressTimer = setTimeout(() => {
|
||
if (navigator.vibrate) {
|
||
navigator.vibrate(50);
|
||
}
|
||
saveImage();
|
||
}, 800);
|
||
}, { passive: true });
|
||
|
||
imageContainer.addEventListener('touchend', () => {
|
||
if (longPressTimer) {
|
||
clearTimeout(longPressTimer);
|
||
longPressTimer = null;
|
||
}
|
||
});
|
||
|
||
imageContainer.addEventListener('touchmove', () => {
|
||
if (longPressTimer) {
|
||
clearTimeout(longPressTimer);
|
||
longPressTimer = null;
|
||
}
|
||
});
|
||
}
|
||
|
||
// 初始化右键菜单(只在3:5预览区域)
|
||
function initContextMenu() {
|
||
// 只在图片容器上禁用默认右键菜单并显示自定义菜单
|
||
imageContainer.addEventListener('contextmenu', (e) => {
|
||
// 如果右键点击的是输入框,显示默认菜单
|
||
if (e.target.closest('.text-input')) {
|
||
return;
|
||
}
|
||
|
||
e.preventDefault();
|
||
showContextMenu(e.clientX, e.clientY);
|
||
});
|
||
|
||
// 点击其他地方关闭菜单
|
||
document.addEventListener('click', () => {
|
||
hideContextMenu();
|
||
});
|
||
}
|
||
|
||
// 初始化文本编辑器工具栏
|
||
function initTextEditorToolbar() {
|
||
// 加粗按钮
|
||
boldBtn.addEventListener('click', () => {
|
||
if (!editingInput) return;
|
||
const selection = window.getSelection();
|
||
if (selection.rangeCount > 0 && !selection.isCollapsed) {
|
||
document.execCommand('bold', false, null);
|
||
} else {
|
||
const isBold = boldBtn.classList.contains('active');
|
||
if (isBold) {
|
||
editingInput.style.fontWeight = 'normal';
|
||
} else {
|
||
editingInput.style.fontWeight = 'bold';
|
||
}
|
||
boldBtn.classList.toggle('active');
|
||
}
|
||
});
|
||
|
||
// 斜体按钮
|
||
italicBtn.addEventListener('click', () => {
|
||
if (!editingInput) return;
|
||
const selection = window.getSelection();
|
||
if (selection.rangeCount > 0 && !selection.isCollapsed) {
|
||
document.execCommand('italic', false, null);
|
||
} else {
|
||
const isItalic = italicBtn.classList.contains('active');
|
||
if (isItalic) {
|
||
editingInput.style.fontStyle = 'normal';
|
||
} else {
|
||
editingInput.style.fontStyle = 'italic';
|
||
}
|
||
italicBtn.classList.toggle('active');
|
||
}
|
||
});
|
||
|
||
// 颜色选择器
|
||
colorPicker.addEventListener('input', () => {
|
||
if (!editingInput) return;
|
||
const selection = window.getSelection();
|
||
if (selection.rangeCount > 0 && !selection.isCollapsed) {
|
||
document.execCommand('foreColor', false, colorPicker.value);
|
||
} else {
|
||
editingInput.style.color = colorPicker.value;
|
||
}
|
||
});
|
||
|
||
// 字体选择器
|
||
fontPicker.addEventListener('change', () => {
|
||
if (!editingInput) return;
|
||
editingInput.style.fontFamily = fontPicker.value + ', sans-serif';
|
||
});
|
||
|
||
// 字号选择器 - 密码锁式滚轮
|
||
let fontSizeValue = 16;
|
||
const fontSizeRoller = document.getElementById('fontSizePicker');
|
||
const fontSizeValueEl = fontSizeRoller.querySelector('.roller-value');
|
||
const fontSizeUpBtn = fontSizeRoller.querySelector('.roller-up');
|
||
const fontSizeDownBtn = fontSizeRoller.querySelector('.roller-down');
|
||
|
||
function updateFontSize(newSize, forceUpdate = false) {
|
||
newSize = Math.max(10, Math.min(42, newSize));
|
||
const targetInput = editingInput || selectedInput;
|
||
|
||
if (forceUpdate || targetInput) {
|
||
targetInput.style.fontSize = newSize + 'px';
|
||
fontSizeValue = newSize;
|
||
fontSizeValueEl.textContent = newSize;
|
||
} else {
|
||
fontSizeValue = newSize;
|
||
fontSizeValueEl.textContent = newSize;
|
||
}
|
||
}
|
||
|
||
fontSizeUpBtn.addEventListener('click', () => {
|
||
const targetInput = editingInput || selectedInput;
|
||
if (targetInput) {
|
||
const currentSize = parseInt(window.getComputedStyle(targetInput).fontSize) || fontSizeValue;
|
||
updateFontSize(currentSize + 1, true);
|
||
}
|
||
});
|
||
|
||
fontSizeDownBtn.addEventListener('click', () => {
|
||
const targetInput = editingInput || selectedInput;
|
||
if (targetInput) {
|
||
const currentSize = parseInt(window.getComputedStyle(targetInput).fontSize) || fontSizeValue;
|
||
updateFontSize(currentSize - 1, true);
|
||
}
|
||
});
|
||
|
||
fontSizeRoller.addEventListener('wheel', (e) => {
|
||
e.preventDefault();
|
||
const targetInput = editingInput || selectedInput;
|
||
if (targetInput) {
|
||
const currentSize = parseInt(window.getComputedStyle(targetInput).fontSize) || fontSizeValue;
|
||
const delta = e.deltaY > 0 ? -1 : 1;
|
||
updateFontSize(currentSize + delta, true);
|
||
}
|
||
}, { passive: false });
|
||
}
|
||
|
||
// 显示右键菜单
|
||
function showContextMenu(x, y) {
|
||
hideContextMenu();
|
||
|
||
const menu = document.createElement('div');
|
||
menu.className = 'context-menu';
|
||
menu.id = 'contextMenu';
|
||
menu.style.left = x + 'px';
|
||
menu.style.top = y + 'px';
|
||
|
||
const previewItem = document.createElement('div');
|
||
previewItem.className = 'context-menu-item';
|
||
previewItem.textContent = '预览图片';
|
||
previewItem.addEventListener('click', () => {
|
||
showPreview();
|
||
hideContextMenu();
|
||
});
|
||
|
||
const copyItem = document.createElement('div');
|
||
copyItem.className = 'context-menu-item';
|
||
copyItem.textContent = '复制图片';
|
||
copyItem.addEventListener('click', async () => {
|
||
await copyImageToClipboard();
|
||
hideContextMenu();
|
||
});
|
||
|
||
const saveItem = document.createElement('div');
|
||
saveItem.className = 'context-menu-item';
|
||
saveItem.textContent = '保存图片';
|
||
saveItem.addEventListener('click', () => {
|
||
saveImage();
|
||
hideContextMenu();
|
||
});
|
||
|
||
menu.appendChild(previewItem);
|
||
menu.appendChild(copyItem);
|
||
menu.appendChild(saveItem);
|
||
document.body.appendChild(menu);
|
||
}
|
||
|
||
// 显示预览
|
||
async function showPreview() {
|
||
const canvas = await generateCanvas();
|
||
const dataUrl = canvas.toDataURL('image/png');
|
||
|
||
const tipOverlay = document.createElement('div');
|
||
tipOverlay.id = 'previewTip';
|
||
tipOverlay.style.cssText = `
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: rgba(0,0,0,0.9);
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 10000;
|
||
padding: 20px;
|
||
`;
|
||
|
||
const tipText = document.createElement('p');
|
||
tipText.textContent = '预览图片';
|
||
tipText.style.cssText = `
|
||
color: #fff;
|
||
font-size: 18px;
|
||
margin-bottom: 20px;
|
||
text-align: center;
|
||
`;
|
||
|
||
const img = document.createElement('img');
|
||
img.src = dataUrl;
|
||
img.style.cssText = `
|
||
max-width: 100%;
|
||
max-height: 80vh;
|
||
border-radius: 8px;
|
||
box-shadow: 0 4px 20px rgba(255,255,255,0.2);
|
||
`;
|
||
|
||
const closeBtn = document.createElement('button');
|
||
closeBtn.textContent = '关闭';
|
||
closeBtn.style.cssText = `
|
||
margin-top: 20px;
|
||
padding: 12px 40px;
|
||
font-size: 16px;
|
||
background: #fff;
|
||
color: #333;
|
||
border: none;
|
||
border-radius: 25px;
|
||
cursor: pointer;
|
||
`;
|
||
closeBtn.onclick = () => tipOverlay.remove();
|
||
|
||
tipOverlay.appendChild(tipText);
|
||
tipOverlay.appendChild(img);
|
||
tipOverlay.appendChild(closeBtn);
|
||
document.body.appendChild(tipOverlay);
|
||
}
|
||
|
||
// 隐藏右键菜单
|
||
function hideContextMenu() {
|
||
const existingMenu = document.getElementById('contextMenu');
|
||
if (existingMenu) {
|
||
existingMenu.remove();
|
||
}
|
||
}
|
||
|
||
// 复制图片到剪贴板
|
||
async function copyImageToClipboard() {
|
||
if (!currentImage) {
|
||
alert('请先添加背景图片');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
deselectAll();
|
||
const canvas = await generateCanvas();
|
||
const blob = await new Promise(resolve => canvas.toBlob(resolve, 'image/png'));
|
||
|
||
await navigator.clipboard.write([
|
||
new ClipboardItem({ 'image/png': blob })
|
||
]);
|
||
|
||
alert('图片已复制到剪贴板');
|
||
} catch (err) {
|
||
console.error('复制失败:', err);
|
||
alert('复制失败,请尝试保存图片');
|
||
}
|
||
}
|
||
|
||
// 生成画布(预览与保存样式一致)
|
||
async function generateCanvas() {
|
||
const canvas = exportCanvas;
|
||
const ctx = canvas.getContext('2d');
|
||
const container = imageContainer;
|
||
|
||
const containerWidth = container.clientWidth;
|
||
const containerHeight = container.clientHeight;
|
||
|
||
canvas.width = containerWidth;
|
||
canvas.height = containerHeight;
|
||
|
||
// 绘制白色背景
|
||
ctx.fillStyle = '#fff';
|
||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||
|
||
// 绘制背景图(考虑滑动偏移)
|
||
if (currentImage) {
|
||
drawImageWithOffset(ctx, currentImage, containerWidth, containerHeight);
|
||
}
|
||
|
||
// 绘制层叠图(等待图片加载完成)
|
||
await drawOverlayImages(ctx);
|
||
|
||
// 绘制文字(动态获取所有文本输入框)
|
||
const textInputs = container.querySelectorAll('.preview-input-layer .text-input');
|
||
textInputs.forEach(textInput => {
|
||
drawTextOnCanvas(ctx, textInput);
|
||
});
|
||
|
||
return canvas;
|
||
}
|
||
|
||
// 绘制层叠图
|
||
function drawOverlayImages(ctx) {
|
||
return new Promise((resolve) => {
|
||
const overlayImages = overlayLayer.querySelectorAll('.overlay-image');
|
||
if (overlayImages.length === 0) {
|
||
resolve();
|
||
return;
|
||
}
|
||
|
||
let loadedCount = 0;
|
||
overlayImages.forEach(overlayEl => {
|
||
const imgEl = overlayEl.querySelector('img');
|
||
if (!imgEl) {
|
||
loadedCount++;
|
||
return;
|
||
}
|
||
|
||
const left = overlayEl.offsetLeft;
|
||
const top = overlayEl.offsetTop;
|
||
const width = overlayEl.offsetWidth;
|
||
const height = overlayEl.offsetHeight;
|
||
|
||
const img = new Image();
|
||
img.onload = () => {
|
||
ctx.drawImage(img, left, top, width, height);
|
||
loadedCount++;
|
||
if (loadedCount >= overlayImages.length) {
|
||
resolve();
|
||
}
|
||
};
|
||
img.onerror = () => {
|
||
loadedCount++;
|
||
if (loadedCount >= overlayImages.length) {
|
||
resolve();
|
||
}
|
||
};
|
||
img.src = imgEl.src;
|
||
});
|
||
});
|
||
}
|
||
|
||
// 绘制图片(考虑滑动偏移)
|
||
function drawImageWithOffset(ctx, img, containerWidth, containerHeight) {
|
||
const imgRatio = img.naturalWidth / img.naturalHeight;
|
||
const containerRatio = containerWidth / containerHeight;
|
||
|
||
let imgWidth, imgHeight;
|
||
|
||
if (imgRatio > containerRatio) {
|
||
imgHeight = containerHeight;
|
||
imgWidth = imgHeight * imgRatio;
|
||
} else {
|
||
imgWidth = containerWidth;
|
||
imgHeight = imgWidth / imgRatio;
|
||
}
|
||
|
||
const baseLeft = (containerWidth - imgWidth) / 2;
|
||
const drawX = baseLeft + imageOffsetX;
|
||
|
||
ctx.drawImage(img, drawX, 0, imgWidth, imgHeight);
|
||
}
|
||
|
||
// 在画布上绘制文字
|
||
function drawTextOnCanvas(ctx, inputElement) {
|
||
const text = inputElement.textContent;
|
||
if (!text) return;
|
||
|
||
const style = window.getComputedStyle(inputElement);
|
||
const inputLeft = inputElement.offsetLeft;
|
||
const inputTop = inputElement.offsetTop;
|
||
|
||
const fontSize = parseInt(style.fontSize);
|
||
ctx.font = `${style.fontWeight} ${fontSize}px ${style.fontFamily}`;
|
||
ctx.fillStyle = style.color;
|
||
ctx.textBaseline = 'top';
|
||
|
||
ctx.fillText(text, inputLeft, inputTop);
|
||
}
|
||
|
||
// 保存图片
|
||
async function saveImage() {
|
||
if (!currentImage) {
|
||
alert('请先添加背景图片');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const canvas = await generateCanvas();
|
||
showMobileSaveTip();
|
||
} catch (err) {
|
||
console.error('保存失败:', err);
|
||
alert('保存失败,请重试');
|
||
}
|
||
}
|
||
|
||
// 显示移动端保存选项
|
||
function showMobileSaveTip() {
|
||
const canvas = exportCanvas;
|
||
const dataUrl = canvas.toDataURL('image/png');
|
||
const filename = '邀请函_' + Date.now() + '.png';
|
||
|
||
const tipOverlay = document.createElement('div');
|
||
tipOverlay.id = 'mobileSaveTip';
|
||
tipOverlay.style.cssText = `
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: rgba(0,0,0,0.6);
|
||
display: flex;
|
||
align-items: flex-end;
|
||
justify-content: center;
|
||
z-index: 10000;
|
||
`;
|
||
|
||
const savePanel = document.createElement('div');
|
||
savePanel.style.cssText = `
|
||
width: 100%;
|
||
background: #fff;
|
||
border-radius: 16px 16px 0 0;
|
||
padding: 20px;
|
||
padding-bottom: 40px;
|
||
`;
|
||
|
||
const title = document.createElement('p');
|
||
title.textContent = '保存图片';
|
||
title.style.cssText = `
|
||
font-size: 18px;
|
||
font-weight: bold;
|
||
text-align: center;
|
||
margin-bottom: 20px;
|
||
color: #333;
|
||
`;
|
||
|
||
const saveBtn = document.createElement('button');
|
||
saveBtn.textContent = '保存到相册';
|
||
saveBtn.style.cssText = `
|
||
width: 100%;
|
||
padding: 16px;
|
||
font-size: 16px;
|
||
background: #007AFF;
|
||
color: #fff;
|
||
border: none;
|
||
border-radius: 10px;
|
||
cursor: pointer;
|
||
margin-bottom: 12px;
|
||
`;
|
||
saveBtn.onclick = () => {
|
||
tipOverlay.remove();
|
||
const link = document.createElement('a');
|
||
link.download = filename;
|
||
link.href = dataUrl;
|
||
link.click();
|
||
};
|
||
|
||
const cancelBtn = document.createElement('button');
|
||
cancelBtn.textContent = '取消';
|
||
cancelBtn.style.cssText = `
|
||
width: 100%;
|
||
padding: 16px;
|
||
font-size: 16px;
|
||
background: #F2F2F7;
|
||
color: #333;
|
||
border: none;
|
||
border-radius: 10px;
|
||
cursor: pointer;
|
||
`;
|
||
cancelBtn.onclick = () => tipOverlay.remove();
|
||
|
||
tipOverlay.addEventListener('click', (e) => {
|
||
if (e.target === tipOverlay) {
|
||
tipOverlay.remove();
|
||
}
|
||
});
|
||
|
||
savePanel.appendChild(title);
|
||
savePanel.appendChild(saveBtn);
|
||
savePanel.appendChild(cancelBtn);
|
||
tipOverlay.appendChild(savePanel);
|
||
document.body.appendChild(tipOverlay);
|
||
|
||
// 添加预览按钮
|
||
const previewBtn = document.createElement('button');
|
||
previewBtn.textContent = '预览图片';
|
||
previewBtn.style.cssText = `
|
||
width: 100%;
|
||
padding: 16px;
|
||
font-size: 16px;
|
||
background: #34C759;
|
||
color: #fff;
|
||
border: none;
|
||
border-radius: 10px;
|
||
cursor: pointer;
|
||
margin-bottom: 12px;
|
||
`;
|
||
previewBtn.onclick = () => {
|
||
tipOverlay.remove();
|
||
showPreview();
|
||
};
|
||
|
||
// 插入到标题后面
|
||
savePanel.insertBefore(previewBtn, saveBtn);
|
||
}
|
||
|
||
// 窗口大小改变时更新图片位置和边界
|
||
window.addEventListener('resize', () => {
|
||
if (currentImage) {
|
||
calculateImageBounds();
|
||
updateImagePosition();
|
||
}
|
||
});
|
||
})();
|