style:还原缩小编辑器工具栏按钮间距,适应小屏幕

This commit is contained in:
Zhukj
2026-04-21 09:05:51 +08:00
parent d34bed9200
commit b3a3ca91d5
3 changed files with 47 additions and 365 deletions

View File

@@ -513,23 +513,3 @@ html, body {
.guide-line.visible { .guide-line.visible {
display: block; 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;
}

View File

@@ -80,8 +80,6 @@
</div> </div>
<!-- 层叠图图层 --> <!-- 层叠图图层 -->
<div class="overlay-layer" id="overlayLayer"></div> <div class="overlay-layer" id="overlayLayer"></div>
<!-- 1px 网格线 -->
<div class="grid-lines" id="gridLines"></div>
<!-- 拖拽辅助线 --> <!-- 拖拽辅助线 -->
<div class="guide-line guide-horizontal" id="guideH"></div> <div class="guide-line guide-horizontal" id="guideH"></div>
<div class="guide-line guide-vertical" id="guideV"></div> <div class="guide-line guide-vertical" id="guideV"></div>

View File

@@ -38,7 +38,6 @@
document.body.appendChild(overlayInput); document.body.appendChild(overlayInput);
const guideH = document.getElementById('guideH'); const guideH = document.getElementById('guideH');
const guideV = document.getElementById('guideV'); const guideV = document.getElementById('guideV');
const gridLines = document.getElementById('gridLines');
let currentImage = null; let currentImage = null;
let bgImageScale = 1; let bgImageScale = 1;
@@ -208,9 +207,6 @@
// 初始化文本编辑器工具栏 // 初始化文本编辑器工具栏
initTextEditorToolbar(); initTextEditorToolbar();
// 初始化键盘微调功能
initKeyboardNudge();
// 加载默认背景图 // 加载默认背景图
loadDefaultBackground(); loadDefaultBackground();
@@ -454,23 +450,27 @@
const dx = clientX - startX; const dx = clientX - startX;
const dy = clientY - startY; const dy = clientY - startY;
// 移除移动距离限制,实现 1px 精确移动 const moveDistance = Math.sqrt(dx * dx + dy * dy);
let newLeft = startLeft + dx; if (moveDistance > 5) {
let newTop = startTop + dy; let newLeft = startLeft + dx;
let newTop = startTop + dy;
// 使用吸附功能 // 使用吸附功能
const snapped = getSnapPosition(overlayEl, newLeft, newTop); const snapped = getSnapPosition(overlayEl, newLeft, newTop);
newLeft = snapped.left; newLeft = snapped.left;
newTop = snapped.top; newTop = snapped.top;
// 显示对齐辅助线(网格线已在 getSnapPosition 中显示) // 显示对齐辅助线
if (snapped.showGuide) { if (snapped.showGuide) {
showAlignmentGuides(overlayEl); 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(); e.preventDefault();
} }
@@ -974,8 +974,7 @@
Math.pow(clientY - clickStartY, 2) Math.pow(clientY - clickStartY, 2)
); );
// 只要有移动就开始拖拽,实现 1px 精确移动 if (moveDistance > 5) {
if (moveDistance > 0) {
hasMoved = true; hasMoved = true;
if (dragTimer) { if (dragTimer) {
clearTimeout(dragTimer); clearTimeout(dragTimer);
@@ -998,9 +997,11 @@
element.style.left = snapped.left + 'px'; element.style.left = snapped.left + 'px';
element.style.top = snapped.top + 'px'; element.style.top = snapped.top + 'px';
// 显示对齐辅助线(网格线已在 getSnapPosition 中显示) // 显示对齐辅助线
if (snapped.showGuide) { if (snapped.showGuide) {
showAlignmentGuides(element); showAlignmentGuides(element);
} else {
hideGuides();
} }
} }
@@ -1216,16 +1217,11 @@
const cCenterX = cWidth / 2; const cCenterX = cWidth / 2;
const cCenterY = cHeight / 2; const cCenterY = cHeight / 2;
const threshold = 1; const threshold = 5;
let snapLeft = newLeft; let snapLeft = newLeft;
let snapTop = newTop; let snapTop = newTop;
let showGuide = false; let showGuide = false;
// 显示网格线
if (gridLines) {
gridLines.classList.add('visible');
}
// 水平垂直分别检测 // 水平垂直分别检测
let snapX = false; let snapX = false;
let snapY = 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')) const otherOverlays = Array.from(overlayLayer.querySelectorAll('.overlay-image'))
.filter(el => el !== draggedEl); .filter(el => el !== draggedEl);
@@ -1424,7 +1420,7 @@
const elCenterX = elLeft + elWidth / 2; const elCenterX = elLeft + elWidth / 2;
const elCenterY = elTop + elHeight / 2; const elCenterY = elTop + elHeight / 2;
const threshold = 1; const threshold = 5;
let guideHPos = null; let guideHPos = null;
let guideVPos = null; let guideVPos = null;
@@ -1575,9 +1571,6 @@
function hideGuides() { function hideGuides() {
guideH.classList.remove('visible'); guideH.classList.remove('visible');
guideV.classList.remove('visible'); guideV.classList.remove('visible');
if (gridLines) {
gridLines.classList.remove('visible');
}
} }
// 取消所有选中 // 取消所有选中
@@ -1745,53 +1738,6 @@
}, { passive: false }); }, { 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) { function showContextMenu(x, y) {
hideContextMenu(); hideContextMenu();
@@ -1832,49 +1778,10 @@
document.body.appendChild(menu); 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() { async function showPreview() {
const canvas = await generateCanvas(); const canvas = await generateCanvas();
console.log('预览画布尺寸:', canvas.width, 'x', canvas.height); const dataUrl = canvas.toDataURL('image/png');
// 尝试使用 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 tipOverlay = document.createElement('div'); const tipOverlay = document.createElement('div');
tipOverlay.id = 'previewTip'; tipOverlay.id = 'previewTip';
@@ -1894,7 +1801,7 @@
`; `;
const tipText = document.createElement('p'); const tipText = document.createElement('p');
tipText.innerHTML = '长按图片保存到相册<br><span style="font-size:14px;opacity:0.8">(预览已缩放,实际保存为高清原图)</span>'; tipText.textContent = '长按图片保存到相册';
tipText.style.cssText = ` tipText.style.cssText = `
color: #fff; color: #fff;
font-size: 18px; font-size: 18px;
@@ -1934,12 +1841,7 @@
`; `;
saveBtn.onclick = async () => { saveBtn.onclick = async () => {
tipOverlay.remove(); tipOverlay.remove();
await saveImageToGallery(dataUrl, blob); await saveImageToGallery(dataUrl);
// 重新隐藏 canvas
exportCanvas.setAttribute('hidden', '');
exportCanvas.style.position = '';
exportCanvas.style.left = '';
exportCanvas.style.top = '';
}; };
const closeBtn = document.createElement('button'); const closeBtn = document.createElement('button');
@@ -1953,14 +1855,7 @@
border-radius: 25px; border-radius: 25px;
cursor: pointer; cursor: pointer;
`; `;
closeBtn.onclick = () => { closeBtn.onclick = () => tipOverlay.remove();
tipOverlay.remove();
// 重新隐藏 canvas
exportCanvas.setAttribute('hidden', '');
exportCanvas.style.position = '';
exportCanvas.style.left = '';
exportCanvas.style.top = '';
};
btnContainer.appendChild(saveBtn); btnContainer.appendChild(saveBtn);
btnContainer.appendChild(closeBtn); btnContainer.appendChild(closeBtn);
@@ -1971,17 +1866,13 @@
} }
// 保存图片到相册 // 保存图片到相册
async function saveImageToGallery(dataUrl, blob) { async function saveImageToGallery(dataUrl) {
const filename = '邀请函_' + Date.now() + '.png'; const filename = '邀请函_' + Date.now() + '.png';
// 如果没有传入 blob,尝试从 window._lastExportBlob 获取
if (!blob && window._lastExportBlob) {
blob = window._lastExportBlob;
}
// 尝试使用 File System Access API (Android/Chrome) // 尝试使用 File System Access API (Android/Chrome)
if ('showSaveFilePicker' in window) { if ('showSaveFilePicker' in window) {
try { try {
const blob = await (await fetch(dataUrl)).blob();
const handle = await window.showSaveFilePicker({ const handle = await window.showSaveFilePicker({
suggestedName: filename, suggestedName: filename,
types: [{ types: [{
@@ -2002,6 +1893,7 @@
// 尝试使用 Web Share API (iOS Safari) // 尝试使用 Web Share API (iOS Safari)
if (navigator.share && navigator.canShare) { if (navigator.share && navigator.canShare) {
try { try {
const blob = await (await fetch(dataUrl)).blob();
const file = new File([blob], filename, { type: 'image/png' }); const file = new File([blob], filename, { type: 'image/png' });
if (navigator.canShare({ files: [file] })) { if (navigator.canShare({ files: [file] })) {
@@ -2060,57 +1952,30 @@
} }
} }
// 生成画布(高清无损,固定 1080×1920 分辨率) // 生成画布(所见即所得 - 使用显示尺寸)
async function generateCanvas() { async function generateCanvas() {
const canvas = exportCanvas; const canvas = exportCanvas;
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext('2d');
const container = imageContainer; const container = imageContainer;
// 固定高清分辨率 1080×1920 const width = container.clientWidth;
const TARGET_WIDTH = 1080; const height = container.clientHeight;
const TARGET_HEIGHT = 1920;
console.log('目标画布尺寸:', TARGET_WIDTH, 'x', TARGET_HEIGHT); canvas.width = width;
canvas.height = 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);
// 启用最高质量渲染
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = 'high';
// 白色背景
ctx.fillStyle = '#fff'; ctx.fillStyle = '#fff';
ctx.fillRect(0, 0, TARGET_WIDTH, TARGET_HEIGHT); ctx.fillRect(0, 0, width, height);
// 计算从显示尺寸到目标分辨率的缩放比例
const scaleX = TARGET_WIDTH / displayWidth;
const scaleY = TARGET_HEIGHT / displayHeight;
console.log('缩放比例:', scaleX, 'x', scaleY);
if (currentImage) { if (currentImage) {
console.log('背景图原始尺寸:', currentImage.naturalWidth, 'x', currentImage.naturalHeight); drawBgImageOnCanvas(ctx, currentImage, width, height);
drawBgImageOnCanvasHD(ctx, currentImage, TARGET_WIDTH, TARGET_HEIGHT, scaleX, scaleY);
} }
await drawOverlayImagesHD(ctx, scaleX, scaleY); await drawOverlayImages(ctx);
const textInputs = container.querySelectorAll('.preview-input-layer .text-input'); const textInputs = container.querySelectorAll('.preview-input-layer .text-input');
textInputs.forEach(textInput => { textInputs.forEach(textInput => {
drawTextOnCanvasHD(ctx, textInput, scaleX, scaleY); drawTextOnCanvas(ctx, textInput);
}); });
return canvas; return canvas;
@@ -2148,39 +2013,6 @@
ctx.globalAlpha = 1; 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) { function drawOverlayImages(ctx) {
return new Promise((resolve) => { 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) { function drawTextOnCanvas(ctx, inputElement) {
const deleteBtn = inputElement.querySelector('.text-delete-btn'); const deleteBtn = inputElement.querySelector('.text-delete-btn');
@@ -2311,35 +2088,6 @@
ctx.fillText(text, inputLeft, inputTop + textOffsetY); 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() { async function saveImage() {
if (!currentImage) { if (!currentImage) {
@@ -2349,7 +2097,7 @@
try { try {
const canvas = await generateCanvas(); const canvas = await generateCanvas();
await showMobileSaveTip(canvas); showMobileSaveTip();
} catch (err) { } catch (err) {
console.error('保存失败:', err); console.error('保存失败:', err);
alert('保存失败,请重试'); alert('保存失败,请重试');
@@ -2357,43 +2105,11 @@
} }
// 显示移动端保存选项 // 显示移动端保存选项
async function showMobileSaveTip(canvas) { function showMobileSaveTip() {
console.log('导出画布尺寸:', canvas.width, 'x', canvas.height); const canvas = exportCanvas;
const dataUrl = canvas.toDataURL('image/png');
// 在 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');
const filename = '邀请函_' + Date.now() + '.png'; const filename = '邀请函_' + Date.now() + '.png';
// 保存 blob 供后续使用
window._lastExportBlob = blob;
const tipOverlay = document.createElement('div'); const tipOverlay = document.createElement('div');
tipOverlay.id = 'mobileSaveTip'; tipOverlay.id = 'mobileSaveTip';
tipOverlay.style.cssText = ` tipOverlay.style.cssText = `
@@ -2443,7 +2159,7 @@
`; `;
saveBtn.onclick = async () => { saveBtn.onclick = async () => {
tipOverlay.remove(); tipOverlay.remove();
await saveImageToGallery(dataUrl, blob); await saveImageToGallery(dataUrl);
}; };
const cancelBtn = document.createElement('button'); const cancelBtn = document.createElement('button');
@@ -2458,23 +2174,11 @@
border-radius: 10px; border-radius: 10px;
cursor: pointer; cursor: pointer;
`; `;
cancelBtn.onclick = () => { cancelBtn.onclick = () => tipOverlay.remove();
tipOverlay.remove();
// 重新隐藏 canvas
exportCanvas.setAttribute('hidden', '');
exportCanvas.style.position = '';
exportCanvas.style.left = '';
exportCanvas.style.top = '';
};
tipOverlay.addEventListener('click', (e) => { tipOverlay.addEventListener('click', (e) => {
if (e.target === tipOverlay) { if (e.target === tipOverlay) {
tipOverlay.remove(); tipOverlay.remove();
// 重新隐藏 canvas
exportCanvas.setAttribute('hidden', '');
exportCanvas.style.position = '';
exportCanvas.style.left = '';
exportCanvas.style.top = '';
} }
}); });
@@ -2500,7 +2204,7 @@
`; `;
previewBtn.onclick = () => { previewBtn.onclick = () => {
tipOverlay.remove(); tipOverlay.remove();
showPreviewFromBlob(blob); showPreview();
}; };
// 插入到标题后面 // 插入到标题后面