517 lines
16 KiB
JavaScript
517 lines
16 KiB
JavaScript
// 极简邀请函制作工具
|
||
(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('保存失败,请重试');
|
||
}
|
||
}
|
||
})();
|