Files
invitation-maker/js/script.js

1282 lines
41 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 极简邀请函制作工具
(function() {
const fileInput = document.getElementById('fileInput');
const addImageBtn = document.getElementById('addImageBtn');
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 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();
});
// 文件选择
fileInput.addEventListener('change', (e) => {
const file = e.target.files[0];
if (file) {
loadImage(file);
}
fileInput.value = '';
});
// 保存按钮
saveBtn.addEventListener('click', saveImage);
// 初始化输入框
initInput(input1);
initInput(input2);
// 初始化图片滑动
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();
}
});
// 点击容器空白处取消选中
imageContainer.addEventListener('click', (e) => {
// 点击容器、背景图层或背景图片,取消输入框选中
if (e.target === imageContainer ||
e.target === bgLayer ||
e.target === bgImage ||
e.target.closest('.preview-bg-layer')) {
deselectAll();
}
});
// 初始化长按保存(只在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 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);
});
// 触摸双击检测(移动端双击进入编辑)
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 (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';
// 更新工具栏状态
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';
// 更新工具栏状态
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.contentEditable = 'false';
element.blur();
editingInput = null;
}
// 更新工具栏状态
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;
}
if (Math.abs(elCenterY - cCenterY) < threshold) {
snapY = true;
snapYPos = cCenterY;
}
// 检测容器边缘对齐
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);
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;
}
}
// 应用吸附
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);
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;
}
}
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;
}
}
// 初始化长按保存(只在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 containerWidth = imageContainer.clientWidth;
const containerHeight = imageContainer.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);
}
// 绘制文字
drawTextOnCanvas(ctx, input1);
drawTextOnCanvas(ctx, input2);
return canvas;
}
// 绘制图片(考虑滑动偏移)
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();
}
});
})();