Files
invitation-maker/script.js

524 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');
const mobileInputControls = document.getElementById('mobileInputControls');
const mobileInput1 = document.getElementById('mobileInput1');
const mobileInput2 = document.getElementById('mobileInput2');
let currentImage = null;
let selectedInput = null;
let editingInput = null;
let longPressTimer = null;
let isMobile = window.matchMedia('(max-width: 768px)').matches;
// 初始化
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);
// 初始化手机端输入控制
initMobileInputControls();
// 点击外部取消选中
document.addEventListener('click', (e) => {
if (!e.target.closest('.text-input') &&
!e.target.closest('.image-container') &&
!e.target.closest('.mobile-input-controls')) {
deselectAll();
}
});
// 初始化长按保存
initLongPressSave();
// 初始化右键菜单
initContextMenu();
// 显示长按提示
setTimeout(() => {
longPressTip.classList.add('show');
setTimeout(() => {
longPressTip.classList.remove('show');
}, 3000);
}, 1000);
// 监听屏幕尺寸变化
window.addEventListener('resize', () => {
isMobile = window.matchMedia('(max-width: 768px)').matches;
});
}
// 初始化手机端输入控制
function initMobileInputControls() {
// 姓名输入同步
mobileInput1.addEventListener('input', () => {
input1.textContent = mobileInput1.value || '姓名';
});
// 编号输入同步
mobileInput2.addEventListener('input', () => {
input2.textContent = mobileInput2.value || '编号';
});
// 聚焦时选中对应的输入框
mobileInput1.addEventListener('focus', () => {
selectInput(input1);
});
mobileInput2.addEventListener('focus', () => {
selectInput(input2);
});
}
// 加载图片
function loadImage(file) {
const reader = new FileReader();
reader.onload = (e) => {
const img = new Image();
img.onload = () => {
currentImage = img;
bgImage.src = img.src;
imageContainer.classList.add('show');
emptyTip.classList.add('hidden');
mobileInputControls.classList.add('show');
setDefaultPositions();
};
img.src = e.target.result;
};
reader.readAsDataURL(file);
}
// 设置默认位置(左下角)
function setDefaultPositions() {
if (!currentImage) return;
setTimeout(() => {
const imgRect = bgImage.getBoundingClientRect();
// 输入框1 - 左下角偏上
input1.style.left = '20px';
input1.style.top = (imgRect.height - 80) + 'px';
// 输入框2 - 左下角偏下
input2.style.left = '20px';
input2.style.top = (imgRect.height - 40) + 'px';
}, 100);
}
// 初始化输入框 - 每个输入框独立管理自己的拖拽状态
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 = parseInt(element.style.left) || 0;
startTop = parseInt(element.style.top) || 0;
// 延迟判断是否为拖拽(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) {
// 单击仅选中,不进入编辑模式
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';
// 同步手机端输入框的值
if (element === input1) {
mobileInput1.value = element.textContent === '姓名' ? '' : element.textContent;
} else if (element === input2) {
mobileInput2.value = element.textContent === '编号' ? '' : element.textContent;
}
}
// 开始编辑
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;
// 同步到手机端输入框
if (element === input1) {
mobileInput1.value = element.textContent === '姓名' ? '' : element.textContent;
} else if (element === input2) {
mobileInput2.value = element.textContent === '编号' ? '' : element.textContent;
}
}
// 取消所有选中
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') || e.target.closest('.mobile-input-controls')) {
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 saveItem = document.createElement('div');
saveItem.className = 'context-menu-item';
saveItem.textContent = '保存图片';
saveItem.addEventListener('click', () => {
saveImage();
hideContextMenu();
});
menu.appendChild(saveItem);
document.body.appendChild(menu);
}
// 隐藏右键菜单
function hideContextMenu() {
const existingMenu = document.getElementById('contextMenu');
if (existingMenu) {
existingMenu.remove();
}
}
// 保存图片
function saveImage() {
if (!currentImage) {
alert('请先添加背景图片');
return;
}
// 取消所有选中和编辑状态
deselectAll();
const canvas = exportCanvas;
const ctx = canvas.getContext('2d');
// 设置画布尺寸为图片原始尺寸
canvas.width = currentImage.naturalWidth;
canvas.height = currentImage.naturalHeight;
// 绘制背景图
ctx.drawImage(currentImage, 0, 0);
// 计算缩放比例
const scaleX = currentImage.naturalWidth / bgImage.clientWidth;
const scaleY = currentImage.naturalHeight / bgImage.clientHeight;
// 绘制文字
drawTextOnCanvas(ctx, input1, scaleX, scaleY);
drawTextOnCanvas(ctx, input2, scaleX, scaleY);
// 下载图片
const link = document.createElement('a');
link.download = '邀请函_' + Date.now() + '.png';
link.href = canvas.toDataURL('image/png');
link.click();
}
// 在画布上绘制文字
function drawTextOnCanvas(ctx, inputElement, scaleX, scaleY) {
const text = inputElement.textContent;
const style = window.getComputedStyle(inputElement);
// 获取输入框位置
const inputRect = inputElement.getBoundingClientRect();
const imgRect = bgImage.getBoundingClientRect();
const relativeX = inputRect.left - imgRect.left;
const relativeY = inputRect.top - imgRect.top;
// 转换为画布坐标
const canvasX = relativeX * scaleX;
const canvasY = relativeY * scaleY;
// 设置字体样式
const fontSize = parseInt(style.fontSize) * scaleX;
ctx.font = `${fontSize}px "${style.fontFamily}"`;
ctx.fillStyle = style.color;
ctx.textBaseline = 'top';
// 绘制文字背景
const padding = 8 * scaleX;
const textMetrics = ctx.measureText(text);
const textWidth = textMetrics.width;
const textHeight = fontSize * 1.4;
ctx.fillStyle = 'rgba(255,255,255,0.9)';
ctx.fillRect(canvasX - padding, canvasY - padding, textWidth + padding * 2, textHeight + padding);
// 绘制文字
ctx.fillStyle = style.color;
ctx.fillText(text, canvasX, canvasY);
}
})();