fix:校准画布编辑与图片导出文字水平垂直位置偏移问题,修改屏幕尺寸为960x1361像素
This commit is contained in:
@@ -60,7 +60,7 @@
|
|||||||
20. 当选择文字的时候工具栏对应出现文字编辑器,当选择图片的时候工具栏对应出现图片编辑器,点击其他位置时隐藏工具栏,两个编辑栏的宽高保持和文本编辑器工具栏一致
|
20. 当选择文字的时候工具栏对应出现文字编辑器,当选择图片的时候工具栏对应出现图片编辑器,点击其他位置时隐藏工具栏,两个编辑栏的宽高保持和文本编辑器工具栏一致
|
||||||
21. 调整图文工具栏布局,解决顶部遮挡问题,图片透明度滑块添加百分比
|
21. 调整图文工具栏布局,解决顶部遮挡问题,图片透明度滑块添加百分比
|
||||||
22. 给背景图添加编辑器,与层叠图编辑器文本编辑器通用一个工具栏,背景图工具栏功能为:背景图可以放大缩小,可以调节透明度,其余功能不受影响,点击层叠图只显示当前编辑器工具栏
|
22. 给背景图添加编辑器,与层叠图编辑器文本编辑器通用一个工具栏,背景图工具栏功能为:背景图可以放大缩小,可以调节透明度,其余功能不受影响,点击层叠图只显示当前编辑器工具栏
|
||||||
|
23. 校准画布编辑与图片导出文字垂直位置偏移问题,修改屏幕尺寸为960x1361像素
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -261,7 +261,7 @@ html, body {
|
|||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 360px;
|
max-width: 360px;
|
||||||
aspect-ratio: 3 / 5;
|
aspect-ratio: 960 / 1361;
|
||||||
box-shadow: 0 4px 20px rgba(0,0,0,0.15);
|
box-shadow: 0 4px 20px rgba(0,0,0,0.15);
|
||||||
background: #fff;
|
background: #fff;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -372,36 +372,34 @@ html, body {
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
min-width: 80px;
|
min-width: 80px;
|
||||||
min-height: 32px;
|
min-height: 32px;
|
||||||
padding: 4px 8px;
|
padding: 0;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
color: #000;
|
color: #000;
|
||||||
font-family: "Microsoft YaHei", "微软雅黑", sans-serif;
|
font-family: "Microsoft YaHei", "微软雅黑", sans-serif;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: 2px solid transparent;
|
outline: 2px solid transparent;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
outline: none;
|
|
||||||
cursor: grab;
|
cursor: grab;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
-webkit-user-select: none;
|
-webkit-user-select: none;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
transition: border-color 0.2s;
|
transition: outline-color 0.2s;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
/* 确保输入框内容超出时不影响父容器和其他元素 */
|
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
max-width: none;
|
max-width: none;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 选中状态(单击/长按) - 仅显示虚线边框 */
|
/* 选中状态(单击/长按) - 仅显示虚线轮廓 */
|
||||||
.text-input.selected {
|
.text-input.selected {
|
||||||
border: 2px dashed rgba(74, 144, 226, 0.6);
|
outline: 2px dashed rgba(74, 144, 226, 0.6);
|
||||||
cursor: grab;
|
cursor: grab;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 编辑状态(双击) - 显示实线边框 */
|
/* 编辑状态(双击) - 显示实线轮廓 */
|
||||||
.text-input.editing {
|
.text-input.editing {
|
||||||
border: 2px solid #4a90e2;
|
outline: 2px solid #4a90e2;
|
||||||
background: rgba(255,255,255,0.1);
|
background: rgba(255,255,255,0.1);
|
||||||
cursor: text;
|
cursor: text;
|
||||||
-webkit-user-select: text;
|
-webkit-user-select: text;
|
||||||
|
|||||||
104
js/script.js
104
js/script.js
@@ -864,8 +864,10 @@
|
|||||||
|
|
||||||
startX = clientX;
|
startX = clientX;
|
||||||
startY = clientY;
|
startY = clientY;
|
||||||
startLeft = element.offsetLeft;
|
const containerRect = imageContainer.getBoundingClientRect();
|
||||||
startTop = element.offsetTop;
|
const elemRect = element.getBoundingClientRect();
|
||||||
|
startLeft = elemRect.left - containerRect.left;
|
||||||
|
startTop = elemRect.top - containerRect.top;
|
||||||
|
|
||||||
// 延迟判断是否为拖拽
|
// 延迟判断是否为拖拽
|
||||||
dragTimer = setTimeout(() => {
|
dragTimer = setTimeout(() => {
|
||||||
@@ -1782,31 +1784,27 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 生成画布(预览与保存样式一致)
|
// 生成画布(所见即所得 - 使用显示尺寸)
|
||||||
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;
|
||||||
|
|
||||||
const containerWidth = container.clientWidth;
|
const width = container.clientWidth;
|
||||||
const containerHeight = container.clientHeight;
|
const height = container.clientHeight;
|
||||||
|
|
||||||
canvas.width = containerWidth;
|
canvas.width = width;
|
||||||
canvas.height = containerHeight;
|
canvas.height = height;
|
||||||
|
|
||||||
// 绘制白色背景
|
|
||||||
ctx.fillStyle = '#fff';
|
ctx.fillStyle = '#fff';
|
||||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
ctx.fillRect(0, 0, width, height);
|
||||||
|
|
||||||
// 绘制背景图(考虑滑动偏移)
|
|
||||||
if (currentImage) {
|
if (currentImage) {
|
||||||
drawImageWithOffset(ctx, currentImage, containerWidth, containerHeight);
|
drawBgImageOnCanvas(ctx, currentImage, width, height);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 绘制层叠图(等待图片加载完成)
|
|
||||||
await drawOverlayImages(ctx);
|
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 => {
|
||||||
drawTextOnCanvas(ctx, textInput);
|
drawTextOnCanvas(ctx, textInput);
|
||||||
@@ -1815,6 +1813,38 @@
|
|||||||
return canvas;
|
return canvas;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 绘制背景图
|
||||||
|
function drawBgImageOnCanvas(ctx, img, width, height) {
|
||||||
|
// 使用与编辑时完全相同的计算逻辑
|
||||||
|
const containerWidth = width;
|
||||||
|
const containerHeight = height;
|
||||||
|
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;
|
||||||
|
|
||||||
|
// 计算位置(与 updateBgImage 函数完全一致)
|
||||||
|
const baseLeft = (containerWidth - scaledWidth) / 2;
|
||||||
|
const drawX = baseLeft + imageOffsetX;
|
||||||
|
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) => {
|
||||||
@@ -1832,20 +1862,25 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 使用 offsetLeft/offsetTop 获取相对于 overlayLayer 的位置
|
||||||
const left = overlayEl.offsetLeft;
|
const left = overlayEl.offsetLeft;
|
||||||
const top = overlayEl.offsetTop;
|
const top = overlayEl.offsetTop;
|
||||||
const width = overlayEl.offsetWidth;
|
const width = overlayEl.offsetWidth;
|
||||||
const height = overlayEl.offsetHeight;
|
const height = overlayEl.offsetHeight;
|
||||||
|
|
||||||
|
ctx.globalAlpha = overlayEl.style.opacity || 1;
|
||||||
|
|
||||||
const img = new Image();
|
const img = new Image();
|
||||||
img.onload = () => {
|
img.onload = () => {
|
||||||
ctx.drawImage(img, left, top, width, height);
|
ctx.drawImage(img, left, top, width, height);
|
||||||
|
ctx.globalAlpha = 1;
|
||||||
loadedCount++;
|
loadedCount++;
|
||||||
if (loadedCount >= overlayImages.length) {
|
if (loadedCount >= overlayImages.length) {
|
||||||
resolve();
|
resolve();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
img.onerror = () => {
|
img.onerror = () => {
|
||||||
|
ctx.globalAlpha = 1;
|
||||||
loadedCount++;
|
loadedCount++;
|
||||||
if (loadedCount >= overlayImages.length) {
|
if (loadedCount >= overlayImages.length) {
|
||||||
resolve();
|
resolve();
|
||||||
@@ -1856,42 +1891,33 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 绘制图片(考虑滑动偏移)
|
|
||||||
function drawImageWithOffset(ctx, img, containerWidth, containerHeight) {
|
|
||||||
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 baseLeft = (containerWidth - imgWidth) / 2;
|
|
||||||
const drawX = baseLeft + imageOffsetX;
|
|
||||||
|
|
||||||
ctx.drawImage(img, drawX, 0, imgWidth, imgHeight);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 在画布上绘制文字
|
// 在画布上绘制文字
|
||||||
function drawTextOnCanvas(ctx, inputElement) {
|
function drawTextOnCanvas(ctx, inputElement) {
|
||||||
const text = inputElement.textContent;
|
const deleteBtn = inputElement.querySelector('.text-delete-btn');
|
||||||
if (!text) return;
|
let text = inputElement.textContent;
|
||||||
|
if (deleteBtn) {
|
||||||
|
text = text.replace(deleteBtn.textContent, '');
|
||||||
|
}
|
||||||
|
if (!text.trim()) return;
|
||||||
|
|
||||||
const style = window.getComputedStyle(inputElement);
|
// 使用 offsetLeft/offsetTop 获取相对于 preview-input-layer 的位置
|
||||||
const inputLeft = inputElement.offsetLeft;
|
const inputLeft = inputElement.offsetLeft;
|
||||||
const inputTop = inputElement.offsetTop;
|
const inputTop = inputElement.offsetTop;
|
||||||
|
|
||||||
|
const style = window.getComputedStyle(inputElement);
|
||||||
const fontSize = parseInt(style.fontSize);
|
const fontSize = parseInt(style.fontSize);
|
||||||
|
const lineHeight = parseFloat(style.lineHeight);
|
||||||
|
|
||||||
ctx.font = `${style.fontWeight} ${fontSize}px ${style.fontFamily}`;
|
ctx.font = `${style.fontWeight} ${fontSize}px ${style.fontFamily}`;
|
||||||
ctx.fillStyle = style.color;
|
ctx.fillStyle = style.color;
|
||||||
ctx.textBaseline = 'top';
|
ctx.textBaseline = 'alphabetic';
|
||||||
|
|
||||||
ctx.fillText(text, inputLeft, inputTop);
|
// 计算文本垂直偏移量,使其与 DOM 渲染一致
|
||||||
|
// DOM 中 line-height 会影响文本位置,需要计算实际文本基线位置
|
||||||
|
const actualLineHeight = lineHeight || fontSize * 1.4;
|
||||||
|
const textOffsetY = (actualLineHeight - fontSize) / 2 + fontSize * 0.85;
|
||||||
|
|
||||||
|
ctx.fillText(text, inputLeft, inputTop + textOffsetY);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 保存图片
|
// 保存图片
|
||||||
|
|||||||
Reference in New Issue
Block a user