Files
invitation-maker/js/script.js

517 lines
16 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 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');
let currentImage = null;
let selectedInput = null;
let editingInput = null;
let longPressTimer = null;
// 初始化
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);
// 点击外部取消选中
document.addEventListener('click', (e) => {
if (!e.target.closest('.text-input') && !e.target.closest('.image-container')) {
deselectAll();
}
});
// 初始化长按保存
initLongPressSave();
// 初始化右键菜单
initContextMenu();
// 显示长按提示
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');
};
img.src = e.target.result;
};
reader.readAsDataURL(file);
}
// 初始化输入框 - 每个输入框独立管理自己的拖拽状态
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';
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;
// 如果正在编辑此输入框,不处理
if (editingInput === element) {
return;
}
// 选中此输入框
selectInput(element);
startX = clientX;
startY = clientY;
startLeft = element.offsetLeft;
startTop = element.offsetTop;
// 延迟判断是否为拖拽(150ms后判定为拖拽)
dragTimer = setTimeout(() => {
if (isMouseDown) {
isDragging = true;
element.classList.add('dragging');
}
}, 150);
if (isTouch) {
e.preventDefault();
}
}
// 鼠标/触摸移动 - 绑定到元素上
element.addEventListener('mousemove', handleMove);
element.addEventListener('touchmove', handleMove, { passive: false });
function handleMove(e) {
// 必须按住鼠标/手指才能拖动
if (!isMouseDown) return;
if (!selectedInput || editingInput === element) 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;
element.style.left = (startLeft + dx) + 'px';
element.style.top = (startTop + dy) + 'px';
if (isTouch) {
e.preventDefault();
}
}
}
// 鼠标/触摸释放 - 绑定到元素上
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;
}
const clickDuration = Date.now() - clickStartTime;
// 如果是快速点击且未移动,判定为单击(仅选中,不编辑)
if (!hasMoved && clickDuration < 200 && !isDragging) {
// 单击仅选中,不进入编辑模式
if (e && e.preventDefault) {
e.preventDefault();
}
}
// 立即结束拖拽状态
if (isDragging) {
isDragging = false;
element.classList.remove('dragging');
}
}
// 双击进入编辑模式
element.addEventListener('dblclick', (e) => {
e.stopPropagation();
startEdit(element);
});
// 触摸双击检测
let lastTapTime = 0;
element.addEventListener('touchend', (e) => {
const currentTime = Date.now();
if (currentTime - lastTapTime < 300) {
// 双击检测
e.preventDefault();
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';
}
// 开始编辑
function startEdit(element) {
if (editingInput && editingInput !== element) {
endEdit(editingInput);
}
editingInput = element;
selectedInput = element;
element.classList.add('editing');
element.classList.remove('selected');
element.contentEditable = 'true';
element.focus();
// 光标定位到末尾
const range = document.createRange();
const sel = window.getSelection();
range.selectNodeContents(element);
range.collapse(false);
sel.removeAllRanges();
sel.addRange(range);
// 移动端显示键盘
if ('virtualKeyboard' in navigator) {
navigator.virtualKeyboard.show();
}
}
// 结束编辑
function endEdit(element) {
element.classList.remove('editing');
element.contentEditable = 'false';
element.blur();
editingInput = null;
}
// 取消所有选中
function deselectAll() {
if (editingInput) {
endEdit(editingInput);
}
if (selectedInput) {
selectedInput.classList.remove('selected');
selectedInput = null;
}
}
// 初始化长按保存
function initLongPressSave() {
canvasArea.addEventListener('touchstart', (e) => {
// 如果触摸的是输入框,不触发长按保存
if (e.target.closest('.text-input')) {
return;
}
longPressTimer = setTimeout(() => {
// 震动反馈
if (navigator.vibrate) {
navigator.vibrate(50);
}
// 保存图片
saveImage();
}, 800);
}, { passive: true });
canvasArea.addEventListener('touchend', () => {
if (longPressTimer) {
clearTimeout(longPressTimer);
longPressTimer = null;
}
});
canvasArea.addEventListener('touchmove', () => {
if (longPressTimer) {
clearTimeout(longPressTimer);
longPressTimer = null;
}
});
}
// 初始化右键菜单
function initContextMenu() {
// 禁用默认右键菜单
canvasArea.addEventListener('contextmenu', (e) => {
// 如果右键点击的是输入框,显示默认菜单
if (e.target.closest('.text-input')) {
return;
}
e.preventDefault();
showContextMenu(e.clientX, e.clientY);
});
// 点击其他地方关闭菜单
document.addEventListener('click', () => {
hideContextMenu();
});
}
// 显示右键菜单
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 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(copyItem);
menu.appendChild(saveItem);
document.body.appendChild(menu);
}
// 隐藏右键菜单
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);
// 如果有背景图,绘制背景图(保持cover效果)
if (currentImage) {
drawImageCover(ctx, currentImage, 0, 0, containerWidth, containerHeight);
}
// 绘制文字(保持与预览完全一致的位置和样式)
drawTextOnCanvas(ctx, input1, containerWidth, containerHeight);
drawTextOnCanvas(ctx, input2, containerWidth, containerHeight);
return canvas;
}
// 绘制图片保持cover效果(与CSS object-fit: cover一致)
function drawImageCover(ctx, img, x, y, width, height) {
const imgRatio = img.naturalWidth / img.naturalHeight;
const containerRatio = width / height;
let drawWidth, drawHeight, drawX, drawY;
if (imgRatio > containerRatio) {
// 图片更宽,以高度为基准
drawHeight = height;
drawWidth = height * imgRatio;
drawX = x + (width - drawWidth) / 2;
drawY = y;
} else {
// 图片更高,以宽度为基准
drawWidth = width;
drawHeight = width / imgRatio;
drawX = x;
drawY = y + (height - drawHeight) / 2;
}
ctx.drawImage(img, drawX, drawY, drawWidth, drawHeight);
}
// 在画布上绘制文字(保持与预览完全一致)
function drawTextOnCanvas(ctx, inputElement, containerWidth, containerHeight) {
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();
// 下载图片
const link = document.createElement('a');
link.download = '邀请函_' + Date.now() + '.png';
link.href = canvas.toDataURL('image/png');
link.click();
} catch (err) {
console.error('保存失败:', err);
alert('保存失败,请重试');
}
}
})();