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

@@ -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 = '长按图片保存到相册<br><span style="font-size:14px;opacity:0.8">(预览已缩放,实际保存为高清原图)</span>';
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();
};
// 插入到标题后面