// 极简邀请函制作工具
(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 input3 = document.getElementById('input3');
const exportCanvas = document.getElementById('exportCanvas');
const longPressTip = document.getElementById('longPressTip');
const canvasArea = document.getElementById('canvasArea');
const editorToolbar = document.getElementById('editorToolbar');
const textTools = document.getElementById('textTools');
const imageTools = document.getElementById('imageTools');
const bgTools = document.getElementById('bgTools');
const opacitySlider = document.getElementById('opacitySlider');
const opacityValue = document.getElementById('opacityValue');
const bgScaleSlider = document.getElementById('bgScaleSlider');
const bgScaleValue = document.getElementById('bgScaleValue');
const bgOpacitySlider = document.getElementById('bgOpacitySlider');
const bgOpacityValue = document.getElementById('bgOpacityValue');
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');
const gridLines = document.getElementById('gridLines');
let currentImage = null;
let bgImageScale = 1;
let bgImageOpacity = 1;
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;
let isInteractingWithBgImage = false;
// 初始化
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);
initInput(input3);
// 为三个输入框添加删除按钮
[input1, input2, input3].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();
// 透明度滑块
opacitySlider.addEventListener('input', (e) => {
if (selectedOverlay) {
selectedOverlay.style.opacity = e.target.value / 100;
opacityValue.textContent = e.target.value + '%';
}
});
// 背景图缩放滑块
bgScaleSlider.addEventListener('input', (e) => {
bgImageScale = e.target.value / 100;
bgScaleValue.textContent = e.target.value + '%';
updateBgImage();
});
// 背景图透明度滑块
bgOpacitySlider.addEventListener('input', (e) => {
bgImageOpacity = e.target.value / 100;
bgOpacityValue.textContent = e.target.value + '%';
bgImage.style.opacity = bgImageOpacity;
});
// 点击空白区域取消选中
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 isEditorToolbar = e.target.closest('.editor-toolbar');
if (!isTextInput && !isImageContainer && !isToolbar && !isEditorToolbar) {
deselectAll();
}
});
// 点击容器空白处取消选中
function handleContainerClick(e) {
if (isInteractingWithBgImage) {
return;
}
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();
// 隐藏编辑器工具栏
editorToolbar.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();
// 初始化键盘微调功能
initKeyboardNudge();
// 加载默认背景图
loadDefaultBackground();
// 显示长按提示
setTimeout(() => {
longPressTip.classList.add('show');
setTimeout(() => {
longPressTip.classList.remove('show');
}, 3000);
}, 1000);
}
// 加载默认背景图
function loadDefaultBackground() {
const defaultBgUrl = './images/default-bg.jpg';
const img = new Image();
img.onload = () => {
currentImage = img;
bgImage.src = img.src;
imageContainer.classList.remove('empty');
emptyTip.classList.add('hidden');
// 计算图片尺寸和边界
calculateImageBounds();
// 背景图选择后默认居中显示
imageOffsetX = 0;
// 重置缩放
bgImageScale = 1;
bgScaleSlider.value = 100;
bgScaleValue.textContent = '100%';
// 重置透明度
bgImageOpacity = 1;
bgOpacitySlider.value = 100;
bgOpacityValue.textContent = '100%';
updateBgImage();
// 显示预设的文本输入框
showPresetTextInputs();
};
img.onerror = () => {
console.log('默认背景图加载失败,使用空状态');
};
img.src = defaultBgUrl;
}
// 加载图片
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;
// 重置缩放
bgImageScale = 1;
bgScaleSlider.value = 100;
bgScaleValue.textContent = '100%';
// 重置透明度
bgImageOpacity = 1;
bgOpacitySlider.value = 100;
bgOpacityValue.textContent = '100%';
updateBgImage();
// 显示预设的文本输入框
showPresetTextInputs();
};
img.src = e.target.result;
};
reader.readAsDataURL(file);
}
// 显示预设的文本输入框
function showPresetTextInputs() {
[input1, input2, input3].forEach(inputEl => {
if (inputEl) {
inputEl.classList.remove('hidden');
}
});
}
// 添加文本输入框
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;
// 移除移动距离限制,实现 1px 精确移动
let newLeft = startLeft + dx;
let newTop = startTop + dy;
// 不启用吸附,只限制边界,但显示辅助线
const snapped = getSnapPosition(overlayEl, newLeft, newTop, false);
newLeft = snapped.left;
newTop = snapped.top;
// 显示对齐辅助线
showAlignmentGuides(overlayEl);
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');
// 显示编辑器工具栏和图片工具
editorToolbar.classList.add('active');
textTools.classList.remove('active');
imageTools.classList.add('active');
bgTools.classList.remove('active');
// 更新透明度滑块为当前层叠图的透明度
const currentOpacity = overlayEl.style.opacity || '1';
opacitySlider.value = parseFloat(currentOpacity) * 100;
opacityValue.textContent = opacitySlider.value + '%';
}
// 计算图片滑动边界
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 updateBgImage() {
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;
}
// 应用缩放
bgImage.style.width = (imgWidth * bgImageScale) + 'px';
bgImage.style.height = (imgHeight * bgImageScale) + 'px';
// 应用透明度
bgImage.style.opacity = bgImageOpacity;
// 重新计算边界
const scaledWidth = imgWidth * bgImageScale;
const scaledHeight = imgHeight * bgImageScale;
if (scaledWidth > containerWidth) {
minOffsetX = -(scaledWidth - containerWidth) / 2;
maxOffsetX = (scaledWidth - containerWidth) / 2;
} else {
minOffsetX = 0;
maxOffsetX = 0;
}
// 限制当前位置在新范围内
imageOffsetX = Math.max(minOffsetX, Math.min(maxOffsetX, imageOffsetX));
// 重新应用位置(居中 + 偏移)
const baseLeft = (containerWidth - scaledWidth) / 2;
bgImage.style.left = (baseLeft + imageOffsetX) + 'px';
bgImage.style.top = ((containerHeight - scaledHeight) / 2) + 'px';
}
// 更新图片位置
function updateImagePosition() {
if (!currentImage) return;
const containerWidth = imageContainer.clientWidth;
const containerHeight = imageContainer.clientHeight;
const imgRatio = currentImage.naturalWidth / currentImage.naturalHeight;
let imgWidth, imgHeight;
if (imgRatio > containerWidth / containerHeight) {
imgHeight = containerHeight;
imgWidth = imgHeight * imgRatio;
} else {
imgWidth = containerWidth;
imgHeight = imgWidth / imgRatio;
}
// 应用缩放
const scaledWidth = imgWidth * bgImageScale;
const scaledHeight = imgHeight * bgImageScale;
// 限制滑动范围
if (scaledWidth > containerWidth) {
minOffsetX = -(scaledWidth - containerWidth) / 2;
maxOffsetX = (scaledWidth - containerWidth) / 2;
} else {
minOffsetX = 0;
maxOffsetX = 0;
}
imageOffsetX = Math.max(minOffsetX, Math.min(maxOffsetX, imageOffsetX));
// 应用位置
const baseLeft = (containerWidth - scaledWidth) / 2;
bgImage.style.width = scaledWidth + 'px';
bgImage.style.height = scaledHeight + 'px';
bgImage.style.left = (baseLeft + imageOffsetX) + 'px';
bgImage.style.top = ((containerHeight - scaledHeight) / 2) + 'px';
bgImage.style.opacity = bgImageOpacity;
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);
// 点击背景图显示工具栏
bgImage.addEventListener('click', (e) => {
if (!currentImage) return;
e.stopPropagation();
isInteractingWithBgImage = true;
// 显示背景图工具栏
editorToolbar.classList.add('active');
textTools.classList.remove('active');
imageTools.classList.remove('active');
bgTools.classList.add('active');
// 同步滑块值
bgScaleSlider.value = bgImageScale * 100;
bgScaleValue.textContent = Math.round(bgImageScale * 100) + '%';
bgOpacitySlider.value = bgImageOpacity * 100;
bgOpacityValue.textContent = Math.round(bgImageOpacity * 100) + '%';
setTimeout(() => {
isInteractingWithBgImage = false;
}, 100);
});
bgLayer.addEventListener('click', (e) => {
if (!currentImage) return;
e.stopPropagation();
isInteractingWithBgImage = true;
// 显示背景图工具栏
editorToolbar.classList.add('active');
textTools.classList.remove('active');
imageTools.classList.remove('active');
bgTools.classList.add('active');
// 同步滑块值
bgScaleSlider.value = bgImageScale * 100;
bgScaleValue.textContent = Math.round(bgImageScale * 100) + '%';
bgOpacitySlider.value = bgImageOpacity * 100;
bgOpacityValue.textContent = Math.round(bgImageOpacity * 100) + '%';
setTimeout(() => {
isInteractingWithBgImage = false;
}, 100);
});
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;
isInteractingWithBgImage = true;
imageStartX = clientX;
imageStartOffsetX = imageOffsetX;
// 显示背景图工具栏
editorToolbar.classList.add('active');
textTools.classList.remove('active');
imageTools.classList.remove('active');
bgTools.classList.add('active');
// 同步滑块值
bgScaleSlider.value = bgImageScale * 100;
bgScaleValue.textContent = Math.round(bgImageScale * 100) + '%';
bgOpacitySlider.value = bgImageOpacity * 100;
bgOpacityValue.textContent = Math.round(bgImageOpacity * 100) + '%';
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;
isInteractingWithBgImage = 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;
// 检查是否需要显示 placeholder
function updatePlaceholder() {
// 获取纯文本内容(排除删除按钮)
const textContent = Array.from(element.childNodes)
.filter(node => node.nodeType === Node.TEXT_NODE || (node.nodeType === Node.ELEMENT_NODE && !node.classList.contains('text-delete-btn')))
.map(node => node.textContent)
.join('')
.trim();
if (!textContent) {
element.classList.add('show-placeholder');
} else {
element.classList.remove('show-placeholder');
}
}
// 初始化时检查
updatePlaceholder();
// 监听输入事件,控制 placeholder 显示
element.addEventListener('input', updatePlaceholder);
// 鼠标/触摸按下
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;
const containerRect = imageContainer.getBoundingClientRect();
const elemRect = element.getBoundingClientRect();
startLeft = elemRect.left - containerRect.left;
startTop = elemRect.top - containerRect.top;
// 延迟判断是否为拖拽
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)
);
// 只要有移动就开始拖拽,实现 1px 精确移动
if (moveDistance > 0) {
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, false);
element.style.left = snapped.left + 'px';
element.style.top = snapped.top + 'px';
// 显示对齐辅助线
showAlignmentGuides(element);
}
// 阻止事件冒泡,避免触发图片滑动
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;
// 如果已经在编辑中,不重复进入
if (editingInput === element) return;
// 选中并立即进入编辑模式
selectInput(element);
startEdit(element);
});
}
// 选中输入框
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';
// 显示编辑器工具栏和文本工具
editorToolbar.classList.add('active');
textTools.classList.add('active');
imageTools.classList.remove('active');
bgTools.classList.remove('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';
// 显示编辑器工具栏和文本工具
editorToolbar.classList.add('active');
textTools.classList.add('active');
imageTools.classList.remove('active');
bgTools.classList.remove('active');
// 更新工具栏状态
updateToolbarState(element);
// 立即 focus 以触发键盘(必须在用户交互的同步上下文中)
element.focus();
// 如果没有选中文本,则定位光标到末尾
setTimeout(() => {
const sel = window.getSelection();
if (sel.isCollapsed) {
const range = document.createRange();
range.selectNodeContents(element);
range.collapse(false);
sel.removeAllRanges();
sel.addRange(range);
}
}, 0);
// 使用 Virtual Keyboard API(如果支持)
if ('virtualKeyboard' in navigator) {
navigator.virtualKeyboard.show();
}
}
// 结束编辑
function endEdit(element) {
element.classList.remove('editing');
element.classList.add('selected');
element.contentEditable = 'false';
// 检查是否需要显示 placeholder
const textContent = Array.from(element.childNodes)
.filter(node => node.nodeType === Node.TEXT_NODE || (node.nodeType === Node.ELEMENT_NODE && !node.classList.contains('text-delete-btn')))
.map(node => node.textContent)
.join('')
.trim();
if (!textContent) {
element.classList.add('show-placeholder');
} else {
element.classList.remove('show-placeholder');
}
element.blur();
editingInput = null;
// 隐藏编辑器工具栏
editorToolbar.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, enableSnap = false) {
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;
// 默认返回原始位置(不吸附)
let snapLeft = newLeft;
let snapTop = newTop;
let showGuide = false;
// 如果不启用吸附,只限制边界并返回
if (!enableSnap) {
// 边界限制
snapLeft = Math.max(0, Math.min(snapLeft, cWidth - elWidth));
snapTop = Math.max(0, Math.min(snapTop, cHeight - elHeight));
return { left: snapLeft, top: snapTop, showGuide };
}
const threshold = 5;
// 显示网格线
if (gridLines) {
gridLines.classList.add('visible');
}
// 水平垂直分别检测
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, input3].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 = 1;
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');
if (gridLines) {
gridLines.classList.remove('visible');
}
}
// 取消所有选中
function deselectAll() {
if (editingInput) {
endEdit(editingInput);
}
if (selectedInput) {
selectedInput.classList.remove('selected');
selectedInput = null;
}
// 隐藏编辑器工具栏
editorToolbar.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 });
}
// 初始化键盘微调功能(方向键 1px 移动)
function initKeyboardNudge() {
document.addEventListener('keydown', (e) => {
// 如果正在编辑文本,不处理方向键
if (editingInput) return;
// 获取当前选中的元素
const targetEl = selectedInput || selectedOverlay;
if (!targetEl) return;
// 防止默认行为(页面滚动)
if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key)) {
e.preventDefault();
}
let currentLeft = parseInt(targetEl.style.left) || targetEl.offsetLeft;
let currentTop = parseInt(targetEl.style.top) || targetEl.offsetTop;
switch(e.key) {
case 'ArrowUp':
currentTop -= 1;
break;
case 'ArrowDown':
currentTop += 1;
break;
case 'ArrowLeft':
currentLeft -= 1;
break;
case 'ArrowRight':
currentLeft += 1;
break;
default:
return;
}
// 应用新位置(不启用吸附)
targetEl.style.left = currentLeft + 'px';
targetEl.style.top = currentTop + 'px';
});
}
// 显示右键菜单
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);
}
// 从 blob 显示预览
function showPreviewFromBlob(blob) {
console.log('预览图片大小:', (blob.size / 1024).toFixed(2), 'KB');
const dataUrl = URL.createObjectURL(blob);
showPreviewModal(dataUrl, blob);
}
// 显示预览
async function showPreview() {
const canvas = await generateCanvas();
console.log('预览画布尺寸:', canvas.width, 'x', canvas.height);
// 尝试使用 toDataURL 获取更高质量的图片
let dataUrl;
let blob;
try {
// 先尝试 toDataURL
dataUrl = canvas.toDataURL('image/png');
console.log('预览 toDataURL 成功,dataUrl 长度:', dataUrl.length);
// 将 dataUrl 转换为 blob
const response = await fetch(dataUrl);
blob = await response.blob();
console.log('预览 dataUrl 转 blob 成功,blob 尺寸:', blob.size, 'bytes');
} catch (err) {
console.log('预览 toDataURL 失败,使用 toBlob:', err);
// 如果 toDataURL 失败,使用 toBlob
blob = await new Promise(resolve => {
canvas.toBlob((b) => {
console.log('预览 toBlob 回调 - blob 尺寸:', b.size, 'bytes');
resolve(b);
}, 'image/png', 1.0);
});
dataUrl = URL.createObjectURL(blob);
}
console.log('预览图片大小:', (blob.size / 1024).toFixed(2), 'KB');
showPreviewModal(dataUrl, blob);
}
// 显示预览弹窗
function showPreviewModal(dataUrl, blob) {
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.innerHTML = '长按图片保存到相册
(预览已缩放,实际保存为高清原图)';
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: 70vh;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(255,255,255,0.2);
-webkit-touch-callout: default;
-webkit-user-select: auto;
user-select: auto;
`;
const btnContainer = document.createElement('div');
btnContainer.style.cssText = `
display: flex;
gap: 12px;
margin-top: 20px;
`;
const saveBtn = document.createElement('button');
saveBtn.textContent = '保存到相册';
saveBtn.style.cssText = `
padding: 12px 30px;
font-size: 16px;
background: #007AFF;
color: #fff;
border: none;
border-radius: 25px;
cursor: pointer;
`;
saveBtn.onclick = async () => {
tipOverlay.remove();
await saveImageToGallery(dataUrl, blob);
// 重新隐藏 canvas
exportCanvas.setAttribute('hidden', '');
exportCanvas.style.position = '';
exportCanvas.style.left = '';
exportCanvas.style.top = '';
};
const closeBtn = document.createElement('button');
closeBtn.textContent = '关闭';
closeBtn.style.cssText = `
padding: 12px 30px;
font-size: 16px;
background: #fff;
color: #333;
border: none;
border-radius: 25px;
cursor: pointer;
`;
closeBtn.onclick = () => {
tipOverlay.remove();
// 重新隐藏 canvas
exportCanvas.setAttribute('hidden', '');
exportCanvas.style.position = '';
exportCanvas.style.left = '';
exportCanvas.style.top = '';
};
btnContainer.appendChild(saveBtn);
btnContainer.appendChild(closeBtn);
tipOverlay.appendChild(tipText);
tipOverlay.appendChild(img);
tipOverlay.appendChild(btnContainer);
document.body.appendChild(tipOverlay);
}
// 保存图片到相册
async function saveImageToGallery(dataUrl, blob) {
const filename = '邀请函_' + Date.now() + '.png';
// 如果没有传入 blob,尝试从 window._lastExportBlob 获取
if (!blob && window._lastExportBlob) {
blob = window._lastExportBlob;
}
// 尝试使用 File System Access API (Android/Chrome)
if ('showSaveFilePicker' in window) {
try {
const handle = await window.showSaveFilePicker({
suggestedName: filename,
types: [{
description: 'PNG 图片',
accept: { 'image/png': ['.png'] }
}]
});
const writable = await handle.createWritable();
await writable.write(blob);
await writable.close();
showToast('图片已保存到相册');
return;
} catch (err) {
console.log('File System Access API 失败,使用传统方式');
}
}
// 尝试使用 Web Share API (iOS Safari)
if (navigator.share && navigator.canShare) {
try {
const file = new File([blob], filename, { type: 'image/png' });
if (navigator.canShare({ files: [file] })) {
await navigator.share({
files: [file],
title: '邀请函',
text: '保存邀请函图片'
});
showToast('图片已分享');
return;
}
} catch (err) {
console.log('Web Share API 失败,使用传统方式');
}
}
// 传统下载方式
const link = document.createElement('a');
link.download = filename;
link.href = dataUrl;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
showToast('图片已保存,请在相册中查看');
}
// 隐藏右键菜单
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('复制失败,请尝试保存图片');
}
}
// 检测是否在微信内置浏览器
function isWechat() {
return /MicroMessenger/i.test(navigator.userAgent);
}
// 生成画布(高清无损,保持与显示容器相同的宽高比)
async function generateCanvas() {
const canvas = exportCanvas;
const container = imageContainer;
// 获取显示容器的实际尺寸和宽高比
const displayWidth = container.clientWidth;
const displayHeight = container.clientHeight;
const displayRatio = displayWidth / displayHeight;
// 根据设备像素比决定输出分辨率,移动端至少 2x
const dpr = window.devicePixelRatio || 1;
const minScale = Math.max(dpr, 2); // 至少 2x
// 先计算目标尺寸
let TARGET_WIDTH = Math.round(displayWidth * minScale);
let TARGET_HEIGHT = Math.round(displayHeight * minScale);
// 微信特殊处理:微信内置浏览器 canvas 限制更严格
// 实测微信中 canvas 最大尺寸约为 4096,但超过 2048 可能有问题
const MAX_CANVAS_SIZE = isWechat() ? 2048 : 4096;
// 如果超出限制,按宽高比等比例缩放
if (TARGET_HEIGHT > MAX_CANVAS_SIZE || TARGET_WIDTH > MAX_CANVAS_SIZE) {
const maxDimension = Math.max(TARGET_WIDTH, TARGET_HEIGHT);
const scaleFactor = MAX_CANVAS_SIZE / maxDimension;
TARGET_WIDTH = Math.round(TARGET_WIDTH * scaleFactor);
TARGET_HEIGHT = Math.round(TARGET_HEIGHT * scaleFactor);
}
// 确保最小分辨率,同时保持宽高比
const minWidth = 1080;
const minHeight = 1920;
if (TARGET_WIDTH < minWidth || TARGET_HEIGHT < minHeight) {
const widthScale = minWidth / TARGET_WIDTH;
const heightScale = minHeight / TARGET_HEIGHT;
const scaleFactor = Math.max(widthScale, heightScale);
TARGET_WIDTH = Math.round(TARGET_WIDTH * scaleFactor);
TARGET_HEIGHT = Math.round(TARGET_HEIGHT * scaleFactor);
// 再次检查是否超出限制
if (TARGET_HEIGHT > MAX_CANVAS_SIZE || TARGET_WIDTH > MAX_CANVAS_SIZE) {
const maxDimension = Math.max(TARGET_WIDTH, TARGET_HEIGHT);
const limitScale = MAX_CANVAS_SIZE / maxDimension;
TARGET_WIDTH = Math.round(TARGET_WIDTH * limitScale);
TARGET_HEIGHT = Math.round(TARGET_HEIGHT * limitScale);
}
}
console.log('目标画布尺寸:', TARGET_WIDTH, 'x', TARGET_HEIGHT, 'dpr:', dpr, '微信:', isWechat());
console.log('显示容器尺寸:', displayWidth, 'x', displayHeight);
// 临时显示 canvas 以避免浏览器对隐藏 canvas 的尺寸限制
canvas.removeAttribute('hidden');
canvas.style.position = 'fixed';
canvas.style.left = '-9999px';
canvas.style.top = '-9999px';
// 设置画布为高清分辨率
canvas.width = TARGET_WIDTH;
canvas.height = TARGET_HEIGHT;
// 微信中:验证 canvas 尺寸是否设置成功
if (isWechat() && (canvas.width !== TARGET_WIDTH || canvas.height !== TARGET_HEIGHT)) {
console.warn('Canvas 尺寸设置失败,使用实际尺寸:', canvas.width, 'x', canvas.height);
TARGET_WIDTH = canvas.width;
TARGET_HEIGHT = canvas.height;
}
console.log('实际画布尺寸:', canvas.width, 'x', canvas.height);
// 获取 2d 上下文(必须在设置尺寸之后)
const ctx = canvas.getContext('2d');
// 启用最高质量渲染
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = 'high';
// 白色背景
ctx.fillStyle = '#fff';
ctx.fillRect(0, 0, TARGET_WIDTH, TARGET_HEIGHT);
// 计算从显示尺寸到目标分辨率的缩放比例
// 使用统一的缩放比例,保持宽高比,确保所见即所得
// 注意:必须在 canvas 尺寸验证之后计算 scale
const scale = TARGET_WIDTH / displayWidth;
console.log('统一缩放比例:', scale, '画布宽高比:', TARGET_WIDTH/TARGET_HEIGHT, '容器宽高比:', displayRatio);
if (currentImage) {
console.log('背景图原始尺寸:', currentImage.naturalWidth, 'x', currentImage.naturalHeight);
drawBgImageOnCanvasHD(ctx, currentImage, TARGET_WIDTH, TARGET_HEIGHT, scale);
}
await drawOverlayImagesHD(ctx, scale);
const textInputs = container.querySelectorAll('.preview-input-layer .text-input');
textInputs.forEach(textInput => {
drawTextOnCanvasHD(ctx, textInput, scale);
});
return canvas;
}
// 绘制背景图
function drawBgImageOnCanvas(ctx, img, width, height) {
// 使用与编辑时完全相同的计算逻辑
const containerWidth = width;
const containerHeight = height;
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 scaledWidth = imgWidth * bgImageScale;
const scaledHeight = imgHeight * bgImageScale;
// 计算位置(与 updateBgImage 函数完全一致)
const baseLeft = (containerWidth - scaledWidth) / 2;
const drawX = baseLeft + imageOffsetX;
const drawY = (containerHeight - scaledHeight) / 2;
ctx.globalAlpha = bgImageOpacity;
ctx.drawImage(img, drawX, drawY, scaledWidth, scaledHeight);
ctx.globalAlpha = 1;
}
// 绘制背景图(高清版本)
function drawBgImageOnCanvasHD(ctx, img, canvasWidth, canvasHeight, scale) {
// 先按显示容器的尺寸计算(与 updateBgImage 完全一致)
const displayContainer = imageContainer;
const displayWidth = displayContainer.clientWidth;
const displayHeight = displayContainer.clientHeight;
const imgRatio = img.naturalWidth / img.naturalHeight;
const displayRatio = displayWidth / displayHeight;
let imgWidth, imgHeight;
if (imgRatio > displayRatio) {
imgHeight = displayHeight;
imgWidth = imgHeight * imgRatio;
} else {
imgWidth = displayWidth;
imgHeight = imgWidth / imgRatio;
}
// 应用缩放(与编辑时的 bgImageScale 一致)
const scaledWidth = imgWidth * bgImageScale;
const scaledHeight = imgHeight * bgImageScale;
// 计算位置(与 updateBgImage 函数完全一致)
const baseLeft = (displayWidth - scaledWidth) / 2;
const displayDrawX = baseLeft + imageOffsetX;
const displayDrawY = (displayHeight - scaledHeight) / 2;
// 将显示坐标转换为 canvas 坐标
const drawX = displayDrawX * scale;
const drawY = displayDrawY * scale;
const canvasScaledWidth = scaledWidth * scale;
const canvasScaledHeight = scaledHeight * scale;
ctx.globalAlpha = bgImageOpacity;
// 调试信息
console.log('背景图绘制:', {
canvas: canvasWidth + 'x' + canvasHeight,
scale: scale,
displayDrawX: displayDrawX,
displayDrawY: displayDrawY,
drawX: drawX,
drawY: drawY,
canvasScaledWidth: canvasScaledWidth,
canvasScaledHeight: canvasScaledHeight
});
ctx.drawImage(img, drawX, drawY, canvasScaledWidth, canvasScaledHeight);
ctx.globalAlpha = 1;
}
// 绘制层叠图
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;
}
// 使用 offsetLeft/offsetTop 获取相对于 overlayLayer 的位置
const left = overlayEl.offsetLeft;
const top = overlayEl.offsetTop;
const width = overlayEl.offsetWidth;
const height = overlayEl.offsetHeight;
ctx.globalAlpha = overlayEl.style.opacity || 1;
const img = new Image();
img.onload = () => {
ctx.drawImage(img, left, top, width, height);
ctx.globalAlpha = 1;
loadedCount++;
if (loadedCount >= overlayImages.length) {
resolve();
}
};
img.onerror = () => {
ctx.globalAlpha = 1;
loadedCount++;
if (loadedCount >= overlayImages.length) {
resolve();
}
};
img.src = imgEl.src;
});
});
}
// 绘制层叠图(高清版本)
function drawOverlayImagesHD(ctx, scale) {
return new Promise((resolve) => {
const overlayImages = overlayLayer.querySelectorAll('.overlay-image');
if (overlayImages.length === 0) {
resolve();
return;
}
let loadedCount = 0;
const totalImages = overlayImages.length;
function checkComplete() {
loadedCount++;
if (loadedCount >= totalImages) {
resolve();
}
}
overlayImages.forEach(overlayEl => {
const imgEl = overlayEl.querySelector('img');
if (!imgEl) {
checkComplete();
return;
}
// 使用 offsetLeft/offsetTop 获取相对于 overlayLayer 的位置,并应用统一缩放
const left = overlayEl.offsetLeft * scale;
const top = overlayEl.offsetTop * scale;
const width = overlayEl.offsetWidth * scale;
const height = overlayEl.offsetHeight * scale;
ctx.globalAlpha = overlayEl.style.opacity || 1;
// 直接使用已加载的图片元素,避免重新加载导致质量下降
if (imgEl.complete && imgEl.naturalWidth > 0) {
ctx.drawImage(imgEl, left, top, width, height);
ctx.globalAlpha = 1;
checkComplete();
} else {
// 如果图片未加载完成,等待加载
imgEl.onload = () => {
ctx.drawImage(imgEl, left, top, width, height);
ctx.globalAlpha = 1;
checkComplete();
};
imgEl.onerror = () => {
ctx.globalAlpha = 1;
checkComplete();
};
}
});
});
}
// 在画布上绘制文字
function drawTextOnCanvas(ctx, inputElement) {
const deleteBtn = inputElement.querySelector('.text-delete-btn');
let text = inputElement.textContent;
if (deleteBtn) {
text = text.replace(deleteBtn.textContent, '');
}
if (!text.trim()) return;
// 使用 offsetLeft/offsetTop 获取相对于 preview-input-layer 的位置
const inputLeft = inputElement.offsetLeft;
const inputTop = inputElement.offsetTop;
const style = window.getComputedStyle(inputElement);
const fontSize = parseInt(style.fontSize);
const lineHeight = parseFloat(style.lineHeight);
ctx.font = `${style.fontWeight} ${fontSize}px ${style.fontFamily}`;
ctx.fillStyle = style.color;
ctx.textBaseline = 'alphabetic';
// 计算文本垂直偏移量,使其与 DOM 渲染一致
// DOM 中 line-height 会影响文本位置,需要计算实际文本基线位置
const actualLineHeight = lineHeight || fontSize * 1.4;
const textOffsetY = (actualLineHeight - fontSize) / 2 + fontSize * 0.85;
ctx.fillText(text, inputLeft, inputTop + textOffsetY);
}
// 在画布上绘制文字(高清版本)
function drawTextOnCanvasHD(ctx, inputElement, scale) {
const deleteBtn = inputElement.querySelector('.text-delete-btn');
let text = inputElement.textContent;
if (deleteBtn) {
text = text.replace(deleteBtn.textContent, '');
}
if (!text.trim()) return;
// 使用 offsetLeft/offsetTop 获取相对于 preview-input-layer 的位置,并应用统一缩放
const inputLeft = inputElement.offsetLeft * scale;
const inputTop = inputElement.offsetTop * scale;
const style = window.getComputedStyle(inputElement);
const originalFontSize = parseInt(style.fontSize);
const fontSize = originalFontSize * scale; // 字体大小也按比例缩放
// lineHeight 可能是像素值或数字(倍数),需要正确处理
let lineHeight = parseFloat(style.lineHeight);
// 如果 lineHeight 是数字(小于 10 认为是倍数),转换为像素值
if (lineHeight && lineHeight < 10) {
lineHeight = lineHeight * originalFontSize * scale;
} else if (lineHeight) {
// 已经是像素值,按比例缩放
lineHeight = lineHeight * scale;
}
// 微信中:使用更粗的字体权重以获得更好的可读性
let fontWeight = style.fontWeight;
if (isWechat() && (fontWeight === '400' || fontWeight === 'normal')) {
fontWeight = '500'; // 稍微加粗一点
}
// 使用高清字体渲染
ctx.font = `${fontWeight} ${fontSize}px ${style.fontFamily}`;
ctx.fillStyle = style.color;
ctx.textBaseline = 'alphabetic';
// 微信中:开启文字抗锯齿
if (isWechat()) {
ctx.textRendering = 'optimizeLegibility';
}
// 计算文本垂直偏移量,使其与 DOM 渲染一致
// 使用缩放后的 lineHeight 计算
const actualLineHeight = lineHeight || fontSize * 1.4;
const textOffsetY = (actualLineHeight - fontSize) / 2 + fontSize * 0.85;
ctx.fillText(text, inputLeft, inputTop + textOffsetY);
}
// 保存图片
async function saveImage() {
if (!currentImage) {
alert('请先添加背景图片');
return;
}
try {
const canvas = await generateCanvas();
await showMobileSaveTip(canvas);
} catch (err) {
console.error('保存失败:', err);
alert('保存失败,请重试');
}
}
// 显示移动端保存选项
async function showMobileSaveTip(canvas) {
console.log('导出画布尺寸:', canvas.width, 'x', canvas.height);
// 在 toBlob 之前再次确认 canvas 尺寸
console.log('toBlob 前 canvas 尺寸:', canvas.width, 'x', canvas.height);
// 微信中使用 toBlob 更稳定,且质量参数 1.0 确保无损
let dataUrl;
let blob;
// 微信中优先使用 toBlob,避免 toDataURL 的内存问题
if (isWechat()) {
try {
blob = await new Promise((resolve, reject) => {
canvas.toBlob((b) => {
if (b) {
console.log('微信 toBlob 成功,blob 尺寸:', b.size, 'bytes');
resolve(b);
} else {
reject(new Error('toBlob 返回 null'));
}
}, 'image/png', 1.0); // 1.0 = 最高质量无损 PNG
});
dataUrl = URL.createObjectURL(blob);
} catch (err) {
console.log('微信 toBlob 失败,回退到 toDataURL:', err);
dataUrl = canvas.toDataURL('image/png');
const response = await fetch(dataUrl);
blob = await response.blob();
}
} else {
// 非微信环境,先尝试 toDataURL
try {
dataUrl = canvas.toDataURL('image/png');
console.log('toDataURL 成功,dataUrl 长度:', dataUrl.length);
const response = await fetch(dataUrl);
blob = await response.blob();
console.log('dataUrl 转 blob 成功,blob 尺寸:', blob.size, 'bytes');
} catch (err) {
console.log('toDataURL 失败,使用 toBlob:', err);
blob = await new Promise(resolve => {
canvas.toBlob((b) => {
console.log('toBlob 回调 - blob 尺寸:', b.size, 'bytes');
resolve(b);
}, 'image/png', 1.0);
});
dataUrl = URL.createObjectURL(blob);
}
}
console.log('导出图片大小:', (blob.size / 1024).toFixed(2), 'KB');
const filename = '邀请函_' + Date.now() + '.png';
// 保存 blob 供后续使用
window._lastExportBlob = blob;
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 = async () => {
tipOverlay.remove();
await saveImageToGallery(dataUrl, blob);
};
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();
// 重新隐藏 canvas
exportCanvas.setAttribute('hidden', '');
exportCanvas.style.position = '';
exportCanvas.style.left = '';
exportCanvas.style.top = '';
};
tipOverlay.addEventListener('click', (e) => {
if (e.target === tipOverlay) {
tipOverlay.remove();
// 重新隐藏 canvas
exportCanvas.setAttribute('hidden', '');
exportCanvas.style.position = '';
exportCanvas.style.left = '';
exportCanvas.style.top = '';
}
});
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();
showPreviewFromBlob(blob);
};
// 插入到标题后面
savePanel.insertBefore(previewBtn, saveBtn);
}
// 显示 Toast 提示
function showToast(message) {
const toast = document.createElement('div');
toast.style.cssText = `
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(0, 0, 0, 0.8);
color: #fff;
padding: 16px 24px;
border-radius: 8px;
font-size: 14px;
z-index: 10001;
pointer-events: none;
animation: fadeInOut 2s ease forwards;
`;
toast.textContent = message;
// 添加动画样式
if (!document.getElementById('toastStyle')) {
const style = document.createElement('style');
style.id = 'toastStyle';
style.textContent = `
@keyframes fadeInOut {
0% { opacity: 0; transform: translate(-50%, -50%) scale(0.9); }
20% { opacity: 1; transform: translate(-50%, -50%) scale(1); }
80% { opacity: 1; transform: translate(-50%, -50%) scale(1); }
100% { opacity: 0; transform: translate(-50%, -50%) scale(0.9); }
}
`;
document.head.appendChild(style);
}
document.body.appendChild(toast);
setTimeout(() => toast.remove(), 2000);
}
// 窗口大小改变时更新图片位置和边界
window.addEventListener('resize', () => {
if (currentImage) {
calculateImageBounds();
updateImagePosition();
}
});
})();