diff --git a/css/style.css b/css/style.css
index e49d0a1..49a8b74 100644
--- a/css/style.css
+++ b/css/style.css
@@ -513,23 +513,3 @@ html, body {
.guide-line.visible {
display: block;
}
-
-/* 1px 网格线 */
-.grid-lines {
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- pointer-events: none;
- z-index: 5;
- display: none;
- background-image:
- linear-gradient(to right, rgba(0, 0, 0, 0.05) 1px, transparent 1px),
- linear-gradient(to bottom, rgba(0, 0, 0, 0.05) 1px, transparent 1px);
- background-size: 10px 10px;
-}
-
-.grid-lines.visible {
- display: block;
-}
diff --git a/index.html b/index.html
index 81a0974..8e7937a 100644
--- a/index.html
+++ b/index.html
@@ -80,8 +80,6 @@
-
-
diff --git a/js/script.js b/js/script.js
index 2c7e68d..78eaff3 100644
--- a/js/script.js
+++ b/js/script.js
@@ -38,7 +38,6 @@
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;
@@ -208,9 +207,6 @@
// 初始化文本编辑器工具栏
initTextEditorToolbar();
- // 初始化键盘微调功能
- initKeyboardNudge();
-
// 加载默认背景图
loadDefaultBackground();
@@ -454,23 +450,27 @@
const dx = clientX - startX;
const dy = clientY - startY;
- // 移除移动距离限制,实现 1px 精确移动
- let newLeft = startLeft + dx;
- let newTop = startTop + dy;
+ const moveDistance = Math.sqrt(dx * dx + dy * dy);
+ if (moveDistance > 5) {
+ let newLeft = startLeft + dx;
+ let newTop = startTop + dy;
- // 使用吸附功能
- const snapped = getSnapPosition(overlayEl, newLeft, newTop);
- newLeft = snapped.left;
- newTop = snapped.top;
+ // 使用吸附功能
+ const snapped = getSnapPosition(overlayEl, newLeft, newTop);
+ newLeft = snapped.left;
+ newTop = snapped.top;
- // 显示对齐辅助线(网格线已在 getSnapPosition 中显示)
- if (snapped.showGuide) {
- showAlignmentGuides(overlayEl);
+ // 显示对齐辅助线
+ if (snapped.showGuide) {
+ showAlignmentGuides(overlayEl);
+ } else {
+ hideGuides();
+ }
+
+ overlayEl.style.left = newLeft + 'px';
+ overlayEl.style.top = newTop + 'px';
}
- overlayEl.style.left = newLeft + 'px';
- overlayEl.style.top = newTop + 'px';
-
e.preventDefault();
}
@@ -974,8 +974,7 @@
Math.pow(clientY - clickStartY, 2)
);
- // 只要有移动就开始拖拽,实现 1px 精确移动
- if (moveDistance > 0) {
+ if (moveDistance > 5) {
hasMoved = true;
if (dragTimer) {
clearTimeout(dragTimer);
@@ -998,9 +997,11 @@
element.style.left = snapped.left + 'px';
element.style.top = snapped.top + 'px';
- // 显示对齐辅助线(网格线已在 getSnapPosition 中显示)
+ // 显示对齐辅助线
if (snapped.showGuide) {
showAlignmentGuides(element);
+ } else {
+ hideGuides();
}
}
@@ -1216,16 +1217,11 @@
const cCenterX = cWidth / 2;
const cCenterY = cHeight / 2;
- const threshold = 1;
+ const threshold = 5;
let snapLeft = newLeft;
let snapTop = newTop;
let showGuide = false;
- // 显示网格线
- if (gridLines) {
- gridLines.classList.add('visible');
- }
-
// 水平垂直分别检测
let snapX = false;
let snapY = false;
@@ -1261,7 +1257,7 @@
}
// 检测其他元素对齐(输入框和层叠图)
- const otherInputs = [input1, input2, input3].filter(el => el && el !== draggedEl);
+ const otherInputs = [input1, input2].filter(el => el && el !== draggedEl);
const otherOverlays = Array.from(overlayLayer.querySelectorAll('.overlay-image'))
.filter(el => el !== draggedEl);
@@ -1424,7 +1420,7 @@
const elCenterX = elLeft + elWidth / 2;
const elCenterY = elTop + elHeight / 2;
- const threshold = 1;
+ const threshold = 5;
let guideHPos = null;
let guideVPos = null;
@@ -1575,9 +1571,6 @@
function hideGuides() {
guideH.classList.remove('visible');
guideV.classList.remove('visible');
- if (gridLines) {
- gridLines.classList.remove('visible');
- }
}
// 取消所有选中
@@ -1745,53 +1738,6 @@
}, { 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';
-
- // 显示对齐辅助线(网格线已在 getSnapPosition 中显示)
- const snapped = getSnapPosition(targetEl, currentLeft, currentTop);
- if (snapped.showGuide) {
- showAlignmentGuides(targetEl);
- }
- });
- }
-
// 显示右键菜单
function showContextMenu(x, y) {
hideContextMenu();
@@ -1832,49 +1778,10 @@
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 dataUrl = canvas.toDataURL('image/png');
const tipOverlay = document.createElement('div');
tipOverlay.id = 'previewTip';
@@ -1894,7 +1801,7 @@
`;
const tipText = document.createElement('p');
- tipText.innerHTML = '长按图片保存到相册
(预览已缩放,实际保存为高清原图)';
+ tipText.textContent = '长按图片保存到相册';
tipText.style.cssText = `
color: #fff;
font-size: 18px;
@@ -1934,12 +1841,7 @@
`;
saveBtn.onclick = async () => {
tipOverlay.remove();
- await saveImageToGallery(dataUrl, blob);
- // 重新隐藏 canvas
- exportCanvas.setAttribute('hidden', '');
- exportCanvas.style.position = '';
- exportCanvas.style.left = '';
- exportCanvas.style.top = '';
+ await saveImageToGallery(dataUrl);
};
const closeBtn = document.createElement('button');
@@ -1953,14 +1855,7 @@
border-radius: 25px;
cursor: pointer;
`;
- closeBtn.onclick = () => {
- tipOverlay.remove();
- // 重新隐藏 canvas
- exportCanvas.setAttribute('hidden', '');
- exportCanvas.style.position = '';
- exportCanvas.style.left = '';
- exportCanvas.style.top = '';
- };
+ closeBtn.onclick = () => tipOverlay.remove();
btnContainer.appendChild(saveBtn);
btnContainer.appendChild(closeBtn);
@@ -1971,17 +1866,13 @@
}
// 保存图片到相册
- async function saveImageToGallery(dataUrl, blob) {
+ async function saveImageToGallery(dataUrl) {
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 blob = await (await fetch(dataUrl)).blob();
const handle = await window.showSaveFilePicker({
suggestedName: filename,
types: [{
@@ -2002,6 +1893,7 @@
// 尝试使用 Web Share API (iOS Safari)
if (navigator.share && navigator.canShare) {
try {
+ const blob = await (await fetch(dataUrl)).blob();
const file = new File([blob], filename, { type: 'image/png' });
if (navigator.canShare({ files: [file] })) {
@@ -2060,57 +1952,30 @@
}
}
- // 生成画布(高清无损,固定 1080×1920 分辨率)
+ // 生成画布(所见即所得 - 使用显示尺寸)
async function generateCanvas() {
const canvas = exportCanvas;
const ctx = canvas.getContext('2d');
const container = imageContainer;
- // 固定高清分辨率 1080×1920
- const TARGET_WIDTH = 1080;
- const TARGET_HEIGHT = 1920;
-
- console.log('目标画布尺寸:', TARGET_WIDTH, 'x', TARGET_HEIGHT);
-
- // 显示尺寸(用于计算缩放比例)
- const displayWidth = container.clientWidth;
- const displayHeight = container.clientHeight;
- 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;
- console.log('实际画布尺寸:', canvas.width, 'x', canvas.height);
+ const width = container.clientWidth;
+ const height = container.clientHeight;
- // 启用最高质量渲染
- ctx.imageSmoothingEnabled = true;
- ctx.imageSmoothingQuality = 'high';
+ canvas.width = width;
+ canvas.height = height;
- // 白色背景
ctx.fillStyle = '#fff';
- ctx.fillRect(0, 0, TARGET_WIDTH, TARGET_HEIGHT);
-
- // 计算从显示尺寸到目标分辨率的缩放比例
- const scaleX = TARGET_WIDTH / displayWidth;
- const scaleY = TARGET_HEIGHT / displayHeight;
- console.log('缩放比例:', scaleX, 'x', scaleY);
+ ctx.fillRect(0, 0, width, height);
if (currentImage) {
- console.log('背景图原始尺寸:', currentImage.naturalWidth, 'x', currentImage.naturalHeight);
- drawBgImageOnCanvasHD(ctx, currentImage, TARGET_WIDTH, TARGET_HEIGHT, scaleX, scaleY);
+ drawBgImageOnCanvas(ctx, currentImage, width, height);
}
- await drawOverlayImagesHD(ctx, scaleX, scaleY);
+ await drawOverlayImages(ctx);
const textInputs = container.querySelectorAll('.preview-input-layer .text-input');
textInputs.forEach(textInput => {
- drawTextOnCanvasHD(ctx, textInput, scaleX, scaleY);
+ drawTextOnCanvas(ctx, textInput);
});
return canvas;
@@ -2148,39 +2013,6 @@
ctx.globalAlpha = 1;
}
- // 绘制背景图(高清版本)
- function drawBgImageOnCanvasHD(ctx, img, canvasWidth, canvasHeight, scaleX, scaleY) {
- // 使用原始图片尺寸进行高清渲染
- const containerWidth = canvasWidth;
- const containerHeight = canvasHeight;
- 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;
-
- // 计算位置(考虑高清缩放)
- const baseLeft = (containerWidth - scaledWidth) / 2;
- const drawX = baseLeft + (imageOffsetX * scaleX);
- const drawY = (containerHeight - scaledHeight) / 2;
-
- ctx.globalAlpha = bgImageOpacity;
- // 使用原始图片绘制,获得最佳清晰度
- ctx.drawImage(img, drawX, drawY, scaledWidth, scaledHeight);
- ctx.globalAlpha = 1;
- }
-
// 绘制层叠图
function drawOverlayImages(ctx) {
return new Promise((resolve) => {
@@ -2227,61 +2059,6 @@
});
}
- // 绘制层叠图(高清版本)
- function drawOverlayImagesHD(ctx, scaleX, scaleY) {
- 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 * scaleX;
- const top = overlayEl.offsetTop * scaleY;
- const width = overlayEl.offsetWidth * scaleX;
- const height = overlayEl.offsetHeight * scaleY;
-
- 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');
@@ -2311,35 +2088,6 @@
ctx.fillText(text, inputLeft, inputTop + textOffsetY);
}
- // 在画布上绘制文字(高清版本)
- function drawTextOnCanvasHD(ctx, inputElement, scaleX, scaleY) {
- 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 * scaleX;
- const inputTop = inputElement.offsetTop * scaleY;
-
- const style = window.getComputedStyle(inputElement);
- const fontSize = parseInt(style.fontSize) * scaleX; // 字体大小也按比例缩放
- const lineHeight = parseFloat(style.lineHeight);
-
- // 使用高清字体渲染
- ctx.font = `${style.fontWeight} ${fontSize}px ${style.fontFamily}`;
- ctx.fillStyle = style.color;
- ctx.textBaseline = 'alphabetic';
-
- // 计算文本垂直偏移量,使其与 DOM 渲染一致
- const actualLineHeight = (lineHeight || fontSize * 1.4) * scaleY;
- const textOffsetY = (actualLineHeight - fontSize) / 2 + fontSize * 0.85;
-
- ctx.fillText(text, inputLeft, inputTop + textOffsetY);
- }
-
// 保存图片
async function saveImage() {
if (!currentImage) {
@@ -2349,7 +2097,7 @@
try {
const canvas = await generateCanvas();
- await showMobileSaveTip(canvas);
+ showMobileSaveTip();
} catch (err) {
console.error('保存失败:', err);
alert('保存失败,请重试');
@@ -2357,42 +2105,10 @@
}
// 显示移动端保存选项
- async function showMobileSaveTip(canvas) {
- console.log('导出画布尺寸:', canvas.width, 'x', canvas.height);
-
- // 在 toBlob 之前再次确认 canvas 尺寸
- console.log('toBlob 前 canvas 尺寸:', 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');
+ function showMobileSaveTip() {
+ const canvas = exportCanvas;
+ const dataUrl = canvas.toDataURL('image/png');
const filename = '邀请函_' + Date.now() + '.png';
-
- // 保存 blob 供后续使用
- window._lastExportBlob = blob;
const tipOverlay = document.createElement('div');
tipOverlay.id = 'mobileSaveTip';
@@ -2443,7 +2159,7 @@
`;
saveBtn.onclick = async () => {
tipOverlay.remove();
- await saveImageToGallery(dataUrl, blob);
+ await saveImageToGallery(dataUrl);
};
const cancelBtn = document.createElement('button');
@@ -2458,23 +2174,11 @@
border-radius: 10px;
cursor: pointer;
`;
- cancelBtn.onclick = () => {
- tipOverlay.remove();
- // 重新隐藏 canvas
- exportCanvas.setAttribute('hidden', '');
- exportCanvas.style.position = '';
- exportCanvas.style.left = '';
- exportCanvas.style.top = '';
- };
+ cancelBtn.onclick = () => tipOverlay.remove();
tipOverlay.addEventListener('click', (e) => {
if (e.target === tipOverlay) {
tipOverlay.remove();
- // 重新隐藏 canvas
- exportCanvas.setAttribute('hidden', '');
- exportCanvas.style.position = '';
- exportCanvas.style.left = '';
- exportCanvas.style.top = '';
}
});
@@ -2500,7 +2204,7 @@
`;
previewBtn.onclick = () => {
tipOverlay.remove();
- showPreviewFromBlob(blob);
+ showPreview();
};
// 插入到标题后面