style:还原缩小编辑器工具栏按钮间距,适应小屏幕
This commit is contained in:
@@ -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;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
390
js/script.js
390
js/script.js
@@ -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();
|
||||||
};
|
};
|
||||||
|
|
||||||
// 插入到标题后面
|
// 插入到标题后面
|
||||||
|
|||||||
Reference in New Issue
Block a user