fix: 优化背景图滑动逻辑,确保居中显示、左右滑动完整,全内容可拖动到预览区
This commit is contained in:
297
js/script.js
297
js/script.js
@@ -4,6 +4,7 @@
|
||||
const addImageBtn = document.getElementById('addImageBtn');
|
||||
const saveBtn = document.getElementById('saveBtn');
|
||||
const imageContainer = document.getElementById('imageContainer');
|
||||
const bgLayer = document.getElementById('bgLayer');
|
||||
const bgImage = document.getElementById('bgImage');
|
||||
const emptyTip = document.getElementById('emptyTip');
|
||||
const input1 = document.getElementById('input1');
|
||||
@@ -17,6 +18,14 @@
|
||||
let editingInput = null;
|
||||
let longPressTimer = null;
|
||||
|
||||
// 图片滑动调整位置相关变量
|
||||
let imageOffsetX = 0;
|
||||
let isDraggingImage = false;
|
||||
let imageStartX = 0;
|
||||
let imageStartOffsetX = 0;
|
||||
let minOffsetX = 0;
|
||||
let maxOffsetX = 0;
|
||||
|
||||
// 初始化
|
||||
init();
|
||||
|
||||
@@ -42,17 +51,33 @@
|
||||
initInput(input1);
|
||||
initInput(input2);
|
||||
|
||||
// 点击外部取消选中
|
||||
// 初始化图片滑动
|
||||
initImageDrag();
|
||||
|
||||
// 点击空白区域取消选中
|
||||
document.addEventListener('click', (e) => {
|
||||
if (!e.target.closest('.text-input') && !e.target.closest('.image-container')) {
|
||||
// 只有点击真正的空白区域(不是输入框,不是图片容器)才取消选中
|
||||
const isTextInput = e.target.closest('.text-input');
|
||||
const isImageContainer = e.target.closest('.image-container');
|
||||
const isToolbar = e.target.closest('.toolbar');
|
||||
|
||||
if (!isTextInput && !isImageContainer && !isToolbar) {
|
||||
deselectAll();
|
||||
}
|
||||
});
|
||||
|
||||
// 初始化长按保存
|
||||
// 点击容器空白处取消选中
|
||||
imageContainer.addEventListener('click', (e) => {
|
||||
// 如果点击的是容器本身或背景图层,取消输入框选中
|
||||
if (e.target === imageContainer || e.target === bgLayer || e.target === bgImage) {
|
||||
deselectAll();
|
||||
}
|
||||
});
|
||||
|
||||
// 初始化长按保存(只在3:5预览区域)
|
||||
initLongPressSave();
|
||||
|
||||
// 初始化右键菜单
|
||||
// 初始化右键菜单(只在3:5预览区域)
|
||||
initContextMenu();
|
||||
|
||||
// 显示长按提示
|
||||
@@ -74,15 +99,158 @@
|
||||
bgImage.src = img.src;
|
||||
imageContainer.classList.remove('empty');
|
||||
emptyTip.classList.add('hidden');
|
||||
// 计算图片尺寸和边界
|
||||
calculateImageBounds();
|
||||
// 背景图选择后默认居中显示
|
||||
imageOffsetX = 0;
|
||||
updateImagePosition();
|
||||
};
|
||||
img.src = e.target.result;
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
|
||||
// 计算图片滑动边界
|
||||
function calculateImageBounds() {
|
||||
if (!currentImage) return;
|
||||
|
||||
const containerWidth = imageContainer.clientWidth;
|
||||
const containerHeight = imageContainer.clientHeight;
|
||||
const imgRatio = currentImage.naturalWidth / currentImage.naturalHeight;
|
||||
const containerRatio = containerWidth / containerHeight;
|
||||
|
||||
let imgWidth, imgHeight;
|
||||
|
||||
if (imgRatio > containerRatio) {
|
||||
// 图片更宽,以高度为基准计算宽度
|
||||
imgHeight = containerHeight;
|
||||
imgWidth = imgHeight * imgRatio;
|
||||
} else {
|
||||
// 图片更高或等比,以宽度为基准
|
||||
imgWidth = containerWidth;
|
||||
imgHeight = imgWidth / imgRatio;
|
||||
}
|
||||
|
||||
// 计算滑动边界:严格以图片边缘为边界,不能露出白板
|
||||
// 滑动公式:imageOffsetX = imageStartOffsetX + deltaX
|
||||
// 手指向右滑动(deltaX>0) → imageOffsetX增大 → 图片右移,显示右侧
|
||||
// 手指向左滑动(deltaX<0) → imageOffsetX减小 → 图片左移,显示左侧
|
||||
// 初始位置居中:imageOffsetX = 0,此时 baseLeft 已使图片居中
|
||||
if (imgWidth > containerWidth) {
|
||||
// 图片宽度大于容器,可以滑动
|
||||
// minOffsetX = -(imgWidth - containerWidth) / 2:向左滑到极限(显示左侧)
|
||||
// maxOffsetX = (imgWidth - containerWidth) / 2:向右滑到极限(显示右侧)
|
||||
const halfRange = (imgWidth - containerWidth) / 2;
|
||||
minOffsetX = -halfRange;
|
||||
maxOffsetX = halfRange;
|
||||
} else {
|
||||
// 图片宽度小于等于容器,居中显示,不能滑动
|
||||
minOffsetX = 0;
|
||||
maxOffsetX = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 更新图片位置
|
||||
function updateImagePosition() {
|
||||
if (!currentImage) return;
|
||||
|
||||
// 限制滑动范围,确保不露出白板
|
||||
imageOffsetX = Math.max(minOffsetX, Math.min(maxOffsetX, imageOffsetX));
|
||||
|
||||
const containerWidth = imageContainer.clientWidth;
|
||||
const containerHeight = imageContainer.clientHeight;
|
||||
const imgRatio = currentImage.naturalWidth / currentImage.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;
|
||||
bgImage.style.width = imgWidth + 'px';
|
||||
bgImage.style.height = imgHeight + 'px';
|
||||
bgImage.style.left = (baseLeft + imageOffsetX) + 'px';
|
||||
bgImage.style.transform = 'none';
|
||||
}
|
||||
|
||||
// 初始化图片滑动调整位置
|
||||
function initImageDrag() {
|
||||
// 鼠标按下
|
||||
imageContainer.addEventListener('mousedown', handleImageStart);
|
||||
imageContainer.addEventListener('touchstart', handleImageStart, { passive: false });
|
||||
|
||||
// 鼠标移动
|
||||
document.addEventListener('mousemove', handleImageMove);
|
||||
document.addEventListener('touchmove', handleImageMove, { passive: false });
|
||||
|
||||
// 鼠标释放
|
||||
document.addEventListener('mouseup', handleImageEnd);
|
||||
document.addEventListener('touchend', handleImageEnd);
|
||||
|
||||
function handleImageStart(e) {
|
||||
// 如果点击的是输入框,不处理图片滑动
|
||||
if (e.target.closest('.text-input')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果没有图片,不处理
|
||||
if (!currentImage) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isTouch = e.type === 'touchstart';
|
||||
const clientX = isTouch ? e.touches[0].clientX : e.clientX;
|
||||
|
||||
// 只有点击图片本身或背景图层时才启动图片滑动
|
||||
const isImageOrBgLayer = e.target === bgImage ||
|
||||
e.target === bgLayer ||
|
||||
e.target === imageContainer;
|
||||
|
||||
if (!isImageOrBgLayer) {
|
||||
return;
|
||||
}
|
||||
|
||||
isDraggingImage = true;
|
||||
imageStartX = clientX;
|
||||
imageStartOffsetX = imageOffsetX;
|
||||
|
||||
if (isTouch) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
function handleImageMove(e) {
|
||||
if (!isDraggingImage) return;
|
||||
|
||||
const isTouch = e.type === 'touchmove';
|
||||
const clientX = isTouch ? e.touches[0].clientX : e.clientX;
|
||||
|
||||
const deltaX = clientX - imageStartX;
|
||||
// 反转拖拽:手指向右滑动(deltaX>0) → imageOffsetX增大 → 图片右移,显示右侧
|
||||
// 手指向左滑动(deltaX<0) → imageOffsetX减小 → 图片左移,显示左侧
|
||||
imageOffsetX = imageStartOffsetX + deltaX;
|
||||
|
||||
updateImagePosition();
|
||||
|
||||
if (isTouch) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
function handleImageEnd() {
|
||||
isDraggingImage = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化输入框 - 每个输入框独立管理自己的拖拽状态
|
||||
function initInput(element) {
|
||||
// 每个输入框独立的状态变量
|
||||
let isMouseDown = false;
|
||||
let isDragging = false;
|
||||
let dragTimer = null;
|
||||
@@ -101,7 +269,6 @@
|
||||
const clientX = isTouch ? e.touches[0].clientX : e.clientX;
|
||||
const clientY = isTouch ? e.touches[0].clientY : e.clientY;
|
||||
|
||||
// 标记鼠标/手指按下
|
||||
isMouseDown = true;
|
||||
clickStartTime = Date.now();
|
||||
clickStartX = clientX;
|
||||
@@ -122,7 +289,7 @@
|
||||
startLeft = element.offsetLeft;
|
||||
startTop = element.offsetTop;
|
||||
|
||||
// 延迟判断是否为拖拽(150ms后判定为拖拽)
|
||||
// 延迟判断是否为拖拽
|
||||
dragTimer = setTimeout(() => {
|
||||
if (isMouseDown) {
|
||||
isDragging = true;
|
||||
@@ -133,14 +300,16 @@
|
||||
if (isTouch) {
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
// 阻止事件冒泡,避免触发图片滑动
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
// 鼠标/触摸移动 - 绑定到元素上
|
||||
// 鼠标/触摸移动
|
||||
element.addEventListener('mousemove', handleMove);
|
||||
element.addEventListener('touchmove', handleMove, { passive: false });
|
||||
|
||||
function handleMove(e) {
|
||||
// 必须按住鼠标/手指才能拖动
|
||||
if (!isMouseDown) return;
|
||||
if (!selectedInput || editingInput === element) return;
|
||||
|
||||
@@ -148,7 +317,6 @@
|
||||
const clientX = isTouch ? e.touches[0].clientX : e.clientX;
|
||||
const clientY = isTouch ? e.touches[0].clientY : e.clientY;
|
||||
|
||||
// 判断是否移动
|
||||
const moveDistance = Math.sqrt(
|
||||
Math.pow(clientX - clickStartX, 2) +
|
||||
Math.pow(clientY - clickStartY, 2)
|
||||
@@ -175,13 +343,18 @@
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
// 阻止事件冒泡和默认行为,确保输入框拖动完全独立,不影响背景图
|
||||
e.stopPropagation();
|
||||
if (isTouch) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
// 鼠标/触摸释放 - 绑定到元素上
|
||||
// 鼠标/触摸释放
|
||||
element.addEventListener('mouseup', handleEnd);
|
||||
element.addEventListener('touchend', handleEnd);
|
||||
|
||||
// 鼠标离开元素时也结束拖拽
|
||||
element.addEventListener('mouseleave', (e) => {
|
||||
if (isMouseDown) {
|
||||
handleEnd(e);
|
||||
@@ -189,30 +362,22 @@
|
||||
});
|
||||
|
||||
function handleEnd(e) {
|
||||
// 标记鼠标/手指释放
|
||||
isMouseDown = false;
|
||||
|
||||
// 清除拖拽定时器
|
||||
if (dragTimer) {
|
||||
clearTimeout(dragTimer);
|
||||
dragTimer = null;
|
||||
}
|
||||
|
||||
const clickDuration = Date.now() - clickStartTime;
|
||||
|
||||
// 如果是快速点击且未移动,判定为单击(仅选中,不编辑)
|
||||
if (!hasMoved && clickDuration < 200 && !isDragging) {
|
||||
// 单击仅选中,不进入编辑模式
|
||||
if (e && e.preventDefault) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
// 立即结束拖拽状态
|
||||
if (isDragging) {
|
||||
isDragging = false;
|
||||
element.classList.remove('dragging');
|
||||
}
|
||||
|
||||
// 阻止事件冒泡
|
||||
if (e) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
}
|
||||
|
||||
// 双击进入编辑模式
|
||||
@@ -226,7 +391,6 @@
|
||||
element.addEventListener('touchend', (e) => {
|
||||
const currentTime = Date.now();
|
||||
if (currentTime - lastTapTime < 300) {
|
||||
// 双击检测
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
startEdit(element);
|
||||
@@ -237,17 +401,14 @@
|
||||
|
||||
// 选中输入框
|
||||
function selectInput(element) {
|
||||
// 如果有其他输入框在编辑,先退出编辑
|
||||
if (editingInput && editingInput !== element) {
|
||||
endEdit(editingInput);
|
||||
}
|
||||
|
||||
// 取消之前的选中
|
||||
if (selectedInput && selectedInput !== element) {
|
||||
selectedInput.classList.remove('selected');
|
||||
}
|
||||
|
||||
// 选中当前输入框
|
||||
selectedInput = element;
|
||||
element.classList.add('selected');
|
||||
element.classList.remove('editing');
|
||||
@@ -276,7 +437,6 @@
|
||||
sel.removeAllRanges();
|
||||
sel.addRange(range);
|
||||
|
||||
// 移动端显示键盘
|
||||
if ('virtualKeyboard' in navigator) {
|
||||
navigator.virtualKeyboard.show();
|
||||
}
|
||||
@@ -301,32 +461,31 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化长按保存
|
||||
// 初始化长按保存(只在3:5预览区域)
|
||||
function initLongPressSave() {
|
||||
canvasArea.addEventListener('touchstart', (e) => {
|
||||
// 只在图片容器上触发长按保存
|
||||
imageContainer.addEventListener('touchstart', (e) => {
|
||||
// 如果触摸的是输入框,不触发长按保存
|
||||
if (e.target.closest('.text-input')) {
|
||||
return;
|
||||
}
|
||||
|
||||
longPressTimer = setTimeout(() => {
|
||||
// 震动反馈
|
||||
if (navigator.vibrate) {
|
||||
navigator.vibrate(50);
|
||||
}
|
||||
// 保存图片
|
||||
saveImage();
|
||||
}, 800);
|
||||
}, { passive: true });
|
||||
|
||||
canvasArea.addEventListener('touchend', () => {
|
||||
imageContainer.addEventListener('touchend', () => {
|
||||
if (longPressTimer) {
|
||||
clearTimeout(longPressTimer);
|
||||
longPressTimer = null;
|
||||
}
|
||||
});
|
||||
|
||||
canvasArea.addEventListener('touchmove', () => {
|
||||
imageContainer.addEventListener('touchmove', () => {
|
||||
if (longPressTimer) {
|
||||
clearTimeout(longPressTimer);
|
||||
longPressTimer = null;
|
||||
@@ -334,10 +493,10 @@
|
||||
});
|
||||
}
|
||||
|
||||
// 初始化右键菜单
|
||||
// 初始化右键菜单(只在3:5预览区域)
|
||||
function initContextMenu() {
|
||||
// 禁用默认右键菜单
|
||||
canvasArea.addEventListener('contextmenu', (e) => {
|
||||
// 只在图片容器上禁用默认右键菜单并显示自定义菜单
|
||||
imageContainer.addEventListener('contextmenu', (e) => {
|
||||
// 如果右键点击的是输入框,显示默认菜单
|
||||
if (e.target.closest('.text-input')) {
|
||||
return;
|
||||
@@ -363,7 +522,6 @@
|
||||
menu.style.left = x + 'px';
|
||||
menu.style.top = y + 'px';
|
||||
|
||||
// 复制到剪贴板选项
|
||||
const copyItem = document.createElement('div');
|
||||
copyItem.className = 'context-menu-item';
|
||||
copyItem.textContent = '复制图片';
|
||||
@@ -372,7 +530,6 @@
|
||||
hideContextMenu();
|
||||
});
|
||||
|
||||
// 保存图片选项
|
||||
const saveItem = document.createElement('div');
|
||||
saveItem.className = 'context-menu-item';
|
||||
saveItem.textContent = '保存图片';
|
||||
@@ -402,9 +559,7 @@
|
||||
}
|
||||
|
||||
try {
|
||||
// 取消所有选中和编辑状态
|
||||
deselectAll();
|
||||
|
||||
const canvas = await generateCanvas();
|
||||
const blob = await new Promise(resolve => canvas.toBlob(resolve, 'image/png'));
|
||||
|
||||
@@ -424,11 +579,9 @@
|
||||
const canvas = exportCanvas;
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
// 获取容器实际尺寸
|
||||
const containerWidth = imageContainer.clientWidth;
|
||||
const containerHeight = imageContainer.clientHeight;
|
||||
|
||||
// 设置画布尺寸与容器一致
|
||||
canvas.width = containerWidth;
|
||||
canvas.height = containerHeight;
|
||||
|
||||
@@ -436,60 +589,53 @@
|
||||
ctx.fillStyle = '#fff';
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
// 如果有背景图,绘制背景图(保持cover效果)
|
||||
// 绘制背景图(考虑滑动偏移)
|
||||
if (currentImage) {
|
||||
drawImageCover(ctx, currentImage, 0, 0, containerWidth, containerHeight);
|
||||
drawImageWithOffset(ctx, currentImage, containerWidth, containerHeight);
|
||||
}
|
||||
|
||||
// 绘制文字(保持与预览完全一致的位置和样式)
|
||||
drawTextOnCanvas(ctx, input1, containerWidth, containerHeight);
|
||||
drawTextOnCanvas(ctx, input2, containerWidth, containerHeight);
|
||||
// 绘制文字
|
||||
drawTextOnCanvas(ctx, input1);
|
||||
drawTextOnCanvas(ctx, input2);
|
||||
|
||||
return canvas;
|
||||
}
|
||||
|
||||
// 绘制图片保持cover效果(与CSS object-fit: cover一致)
|
||||
function drawImageCover(ctx, img, x, y, width, height) {
|
||||
// 绘制图片(考虑滑动偏移)
|
||||
function drawImageWithOffset(ctx, img, containerWidth, containerHeight) {
|
||||
const imgRatio = img.naturalWidth / img.naturalHeight;
|
||||
const containerRatio = width / height;
|
||||
const containerRatio = containerWidth / containerHeight;
|
||||
|
||||
let drawWidth, drawHeight, drawX, drawY;
|
||||
let imgWidth, imgHeight;
|
||||
|
||||
if (imgRatio > containerRatio) {
|
||||
// 图片更宽,以高度为基准
|
||||
drawHeight = height;
|
||||
drawWidth = height * imgRatio;
|
||||
drawX = x + (width - drawWidth) / 2;
|
||||
drawY = y;
|
||||
imgHeight = containerHeight;
|
||||
imgWidth = imgHeight * imgRatio;
|
||||
} else {
|
||||
// 图片更高,以宽度为基准
|
||||
drawWidth = width;
|
||||
drawHeight = width / imgRatio;
|
||||
drawX = x;
|
||||
drawY = y + (height - drawHeight) / 2;
|
||||
imgWidth = containerWidth;
|
||||
imgHeight = imgWidth / imgRatio;
|
||||
}
|
||||
|
||||
ctx.drawImage(img, drawX, drawY, drawWidth, drawHeight);
|
||||
const baseLeft = (containerWidth - imgWidth) / 2;
|
||||
const drawX = baseLeft + imageOffsetX;
|
||||
|
||||
ctx.drawImage(img, drawX, 0, imgWidth, imgHeight);
|
||||
}
|
||||
|
||||
// 在画布上绘制文字(保持与预览完全一致)
|
||||
function drawTextOnCanvas(ctx, inputElement, containerWidth, containerHeight) {
|
||||
// 在画布上绘制文字
|
||||
function drawTextOnCanvas(ctx, inputElement) {
|
||||
const text = inputElement.textContent;
|
||||
if (!text) return;
|
||||
|
||||
const style = window.getComputedStyle(inputElement);
|
||||
|
||||
// 获取输入框位置(相对于容器)
|
||||
const inputLeft = inputElement.offsetLeft;
|
||||
const inputTop = inputElement.offsetTop;
|
||||
|
||||
// 设置字体样式(与预览完全一致)
|
||||
const fontSize = parseInt(style.fontSize);
|
||||
ctx.font = `${style.fontWeight} ${fontSize}px ${style.fontFamily}`;
|
||||
ctx.fillStyle = style.color;
|
||||
ctx.textBaseline = 'top';
|
||||
|
||||
// 绘制文字(保持与预览完全一致的位置)
|
||||
ctx.fillText(text, inputLeft, inputTop);
|
||||
}
|
||||
|
||||
@@ -503,7 +649,6 @@
|
||||
try {
|
||||
const canvas = await generateCanvas();
|
||||
|
||||
// 下载图片
|
||||
const link = document.createElement('a');
|
||||
link.download = '邀请函_' + Date.now() + '.png';
|
||||
link.href = canvas.toDataURL('image/png');
|
||||
@@ -513,4 +658,12 @@
|
||||
alert('保存失败,请重试');
|
||||
}
|
||||
}
|
||||
|
||||
// 窗口大小改变时更新图片位置和边界
|
||||
window.addEventListener('resize', () => {
|
||||
if (currentImage) {
|
||||
calculateImageBounds();
|
||||
updateImagePosition();
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user