feat: 顶部工具栏新增叠加图片按钮
This commit is contained in:
@@ -222,6 +222,50 @@ html, body {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 层叠图层 - 位于背景图之上、文字之下 */
|
||||||
|
.overlay-layer {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay-layer .overlay-image {
|
||||||
|
position: absolute;
|
||||||
|
pointer-events: auto;
|
||||||
|
cursor: move;
|
||||||
|
touch-action: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay-layer .overlay-image.selected {
|
||||||
|
outline: 2px dashed rgba(74, 144, 226, 0.8);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay-layer .overlay-image .delete-btn {
|
||||||
|
position: absolute;
|
||||||
|
top: -10px;
|
||||||
|
right: -10px;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
background: #e74c3c;
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 50%;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 20px;
|
||||||
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
|
display: none;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay-layer .overlay-image.selected .delete-btn {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
/* 输入框图层 - 与背景图层隔离 */
|
/* 输入框图层 - 与背景图层隔离 */
|
||||||
.preview-input-layer {
|
.preview-input-layer {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
<!-- 顶部工具栏 -->
|
<!-- 顶部工具栏 -->
|
||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
<button class="btn" id="addImageBtn">添加图片</button>
|
<button class="btn" id="addImageBtn">添加图片</button>
|
||||||
|
<button class="btn" id="addOverlayBtn">层叠图</button>
|
||||||
<button class="btn btn-primary" id="saveBtn">保存/预览</button>
|
<button class="btn btn-primary" id="saveBtn">保存/预览</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -53,6 +54,8 @@
|
|||||||
<div class="preview-bg-layer" id="bgLayer">
|
<div class="preview-bg-layer" id="bgLayer">
|
||||||
<img id="bgImage" src="" alt="背景图">
|
<img id="bgImage" src="" alt="背景图">
|
||||||
</div>
|
</div>
|
||||||
|
<!-- 层叠图图层 -->
|
||||||
|
<div class="overlay-layer" id="overlayLayer"></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>
|
||||||
|
|||||||
291
js/script.js
291
js/script.js
@@ -2,6 +2,7 @@
|
|||||||
(function() {
|
(function() {
|
||||||
const fileInput = document.getElementById('fileInput');
|
const fileInput = document.getElementById('fileInput');
|
||||||
const addImageBtn = document.getElementById('addImageBtn');
|
const addImageBtn = document.getElementById('addImageBtn');
|
||||||
|
const addOverlayBtn = document.getElementById('addOverlayBtn');
|
||||||
const saveBtn = document.getElementById('saveBtn');
|
const saveBtn = document.getElementById('saveBtn');
|
||||||
const imageContainer = document.getElementById('imageContainer');
|
const imageContainer = document.getElementById('imageContainer');
|
||||||
const bgLayer = document.getElementById('bgLayer');
|
const bgLayer = document.getElementById('bgLayer');
|
||||||
@@ -18,6 +19,12 @@
|
|||||||
const colorPicker = document.getElementById('colorPicker');
|
const colorPicker = document.getElementById('colorPicker');
|
||||||
const fontPicker = document.getElementById('fontPicker');
|
const fontPicker = document.getElementById('fontPicker');
|
||||||
const fontSizePicker = document.getElementById('fontSizePicker');
|
const fontSizePicker = document.getElementById('fontSizePicker');
|
||||||
|
const overlayLayer = document.getElementById('overlayLayer');
|
||||||
|
const overlayInput = document.createElement('input');
|
||||||
|
overlayInput.type = 'file';
|
||||||
|
overlayInput.accept = 'image/*';
|
||||||
|
overlayInput.hidden = true;
|
||||||
|
document.body.appendChild(overlayInput);
|
||||||
const guideH = document.getElementById('guideH');
|
const guideH = document.getElementById('guideH');
|
||||||
const guideV = document.getElementById('guideV');
|
const guideV = document.getElementById('guideV');
|
||||||
|
|
||||||
@@ -43,6 +50,20 @@
|
|||||||
fileInput.click();
|
fileInput.click();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 层叠图按钮
|
||||||
|
addOverlayBtn.addEventListener('click', () => {
|
||||||
|
overlayInput.click();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 层叠图文件选择
|
||||||
|
overlayInput.addEventListener('change', (e) => {
|
||||||
|
const file = e.target.files[0];
|
||||||
|
if (file) {
|
||||||
|
loadOverlayImage(file);
|
||||||
|
}
|
||||||
|
overlayInput.value = '';
|
||||||
|
});
|
||||||
|
|
||||||
// 文件选择
|
// 文件选择
|
||||||
fileInput.addEventListener('change', (e) => {
|
fileInput.addEventListener('change', (e) => {
|
||||||
const file = e.target.files[0];
|
const file = e.target.files[0];
|
||||||
@@ -139,6 +160,276 @@
|
|||||||
reader.readAsDataURL(file);
|
reader.readAsDataURL(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 加载层叠图
|
||||||
|
function loadOverlayImage(file) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e) => {
|
||||||
|
const img = new Image();
|
||||||
|
img.onload = () => {
|
||||||
|
createOverlayElement(img);
|
||||||
|
};
|
||||||
|
img.src = e.target.result;
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建层叠图元素
|
||||||
|
function createOverlayElement(img) {
|
||||||
|
const container = imageContainer;
|
||||||
|
const containerWidth = container.clientWidth;
|
||||||
|
const containerHeight = container.clientHeight;
|
||||||
|
|
||||||
|
// 计算初始尺寸(不超过容器1/3)
|
||||||
|
let width = img.naturalWidth;
|
||||||
|
let height = img.naturalHeight;
|
||||||
|
const maxSize = Math.min(containerWidth, containerHeight) / 2;
|
||||||
|
|
||||||
|
if (width > maxSize || height > maxSize) {
|
||||||
|
if (width > height) {
|
||||||
|
height = (height / width) * maxSize;
|
||||||
|
width = maxSize;
|
||||||
|
} else {
|
||||||
|
width = (width / height) * maxSize;
|
||||||
|
height = maxSize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建层叠图元素
|
||||||
|
const overlayEl = document.createElement('div');
|
||||||
|
overlayEl.className = 'overlay-image';
|
||||||
|
overlayEl.dataset.scale = 1;
|
||||||
|
|
||||||
|
const imgEl = document.createElement('img');
|
||||||
|
imgEl.src = img.src;
|
||||||
|
imgEl.style.width = '100%';
|
||||||
|
imgEl.style.height = '100%';
|
||||||
|
imgEl.style.objectFit = 'contain';
|
||||||
|
overlayEl.appendChild(imgEl);
|
||||||
|
|
||||||
|
// 删除按钮
|
||||||
|
const deleteBtn = document.createElement('button');
|
||||||
|
deleteBtn.className = 'delete-btn';
|
||||||
|
deleteBtn.innerHTML = '×';
|
||||||
|
deleteBtn.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
overlayEl.remove();
|
||||||
|
});
|
||||||
|
overlayEl.appendChild(deleteBtn);
|
||||||
|
|
||||||
|
// 居中显示
|
||||||
|
overlayEl.style.left = (containerWidth / 2 - width / 2) + 'px';
|
||||||
|
overlayEl.style.top = (containerHeight / 2 - height / 2) + 'px';
|
||||||
|
overlayEl.style.width = width + 'px';
|
||||||
|
overlayEl.style.height = height + 'px';
|
||||||
|
|
||||||
|
// 初始化拖拽和缩放
|
||||||
|
initOverlayImage(overlayEl);
|
||||||
|
|
||||||
|
overlayLayer.appendChild(overlayEl);
|
||||||
|
|
||||||
|
// 选中新添加的层叠图
|
||||||
|
selectOverlay(overlayEl);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化层叠图交互
|
||||||
|
function initOverlayImage(overlayEl) {
|
||||||
|
let isDragging = false;
|
||||||
|
let isResizing = false;
|
||||||
|
let startX, startY, startLeft, startTop, startWidth, startHeight;
|
||||||
|
let currentScale = 1;
|
||||||
|
const minSize = 30;
|
||||||
|
|
||||||
|
// 点击选中
|
||||||
|
overlayEl.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
selectOverlay(overlayEl);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 拖拽移动
|
||||||
|
overlayEl.addEventListener('mousedown', handleDragStart);
|
||||||
|
overlayEl.addEventListener('touchstart', handleDragStart, { passive: false });
|
||||||
|
|
||||||
|
function handleDragStart(e) {
|
||||||
|
if (e.target.classList.contains('delete-btn')) return;
|
||||||
|
|
||||||
|
const isTouch = e.type === 'touchstart';
|
||||||
|
const clientX = isTouch ? e.touches[0].clientX : e.clientX;
|
||||||
|
const clientY = isTouch ? e.touches[0].clientY : e.clientY;
|
||||||
|
|
||||||
|
isDragging = true;
|
||||||
|
startX = clientX;
|
||||||
|
startY = clientY;
|
||||||
|
startLeft = overlayEl.offsetLeft;
|
||||||
|
startTop = overlayEl.offsetTop;
|
||||||
|
|
||||||
|
overlayEl.classList.add('dragging');
|
||||||
|
|
||||||
|
document.addEventListener('mousemove', handleDragMove);
|
||||||
|
document.addEventListener('mouseup', handleDragEnd);
|
||||||
|
document.addEventListener('touchmove', handleDragMove, { passive: false });
|
||||||
|
document.addEventListener('touchend', handleDragEnd);
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDragMove(e) {
|
||||||
|
if (!isDragging) return;
|
||||||
|
|
||||||
|
const isTouch = e.type === 'touchmove';
|
||||||
|
const clientX = isTouch ? e.touches[0].clientX : e.clientX;
|
||||||
|
const clientY = isTouch ? e.touches[0].clientY : e.clientY;
|
||||||
|
|
||||||
|
const dx = clientX - startX;
|
||||||
|
const dy = clientY - startY;
|
||||||
|
|
||||||
|
let newLeft = startLeft + dx;
|
||||||
|
let newTop = startTop + dy;
|
||||||
|
|
||||||
|
// 边界限制
|
||||||
|
newLeft = Math.max(-overlayEl.offsetWidth + minSize, Math.min(newLeft, imageContainer.offsetWidth - minSize));
|
||||||
|
newTop = Math.max(-overlayEl.offsetHeight + minSize, Math.min(newTop, imageContainer.offsetHeight - minSize));
|
||||||
|
|
||||||
|
overlayEl.style.left = newLeft + 'px';
|
||||||
|
overlayEl.style.top = newTop + 'px';
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDragEnd() {
|
||||||
|
isDragging = false;
|
||||||
|
overlayEl.classList.remove('dragging');
|
||||||
|
|
||||||
|
document.removeEventListener('mousemove', handleDragMove);
|
||||||
|
document.removeEventListener('mouseup', handleDragEnd);
|
||||||
|
document.removeEventListener('touchmove', handleDragMove);
|
||||||
|
document.removeEventListener('touchend', handleDragEnd);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 缩放控制(四角)
|
||||||
|
const corners = ['tl', 'tr', 'bl', 'br'];
|
||||||
|
corners.forEach(corner => {
|
||||||
|
const handle = document.createElement('div');
|
||||||
|
handle.className = 'resize-handle resize-' + corner;
|
||||||
|
handle.style.cssText = `
|
||||||
|
position: absolute;
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
background: #4a90e2;
|
||||||
|
border: 1px solid #fff;
|
||||||
|
`;
|
||||||
|
|
||||||
|
switch(corner) {
|
||||||
|
case 'tl': handle.style.top = '-6px'; handle.style.left = '-6px'; handle.style.cursor = 'nw-resize'; break;
|
||||||
|
case 'tr': handle.style.top = '-6px'; handle.style.right = '-6px'; handle.style.cursor = 'ne-resize'; break;
|
||||||
|
case 'bl': handle.style.bottom = '-6px'; handle.style.left = '-6px'; handle.style.cursor = 'sw-resize'; break;
|
||||||
|
case 'br': handle.style.bottom = '-6px'; handle.style.right = '-6px'; handle.style.cursor = 'se-resize'; break;
|
||||||
|
}
|
||||||
|
|
||||||
|
handle.addEventListener('mousedown', startResize);
|
||||||
|
handle.addEventListener('touchstart', startResize, { passive: false });
|
||||||
|
|
||||||
|
overlayEl.appendChild(handle);
|
||||||
|
|
||||||
|
function startResize(e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
isResizing = true;
|
||||||
|
const isTouch = e.type === 'touchstart';
|
||||||
|
const clientX = isTouch ? e.touches[0].clientX : e.clientX;
|
||||||
|
const clientY = isTouch ? e.touches[0].clientY : e.clientY;
|
||||||
|
|
||||||
|
startX = clientX;
|
||||||
|
startY = clientY;
|
||||||
|
startLeft = overlayEl.offsetLeft;
|
||||||
|
startTop = overlayEl.offsetTop;
|
||||||
|
startWidth = overlayEl.offsetWidth;
|
||||||
|
startHeight = overlayEl.offsetHeight;
|
||||||
|
|
||||||
|
document.addEventListener('mousemove', handleResize);
|
||||||
|
document.addEventListener('mouseup', endResize);
|
||||||
|
document.addEventListener('touchmove', handleResize, { passive: false });
|
||||||
|
document.addEventListener('touchend', endResize);
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleResize(e) {
|
||||||
|
if (!isResizing) return;
|
||||||
|
|
||||||
|
const isTouch = e.type === 'touchmove';
|
||||||
|
const clientX = isTouch ? e.touches[0].clientX : e.clientX;
|
||||||
|
const clientY = isTouch ? e.touches[0].clientY : e.clientY;
|
||||||
|
|
||||||
|
const dx = clientX - startX;
|
||||||
|
const dy = clientY - startY;
|
||||||
|
|
||||||
|
let newWidth = startWidth;
|
||||||
|
let newHeight = startHeight;
|
||||||
|
let newLeft = startLeft;
|
||||||
|
let newTop = startTop;
|
||||||
|
|
||||||
|
const aspectRatio = startWidth / startHeight;
|
||||||
|
|
||||||
|
// 根据角落调整尺寸
|
||||||
|
if (corner === 'br') {
|
||||||
|
newWidth = Math.max(minSize, startWidth + dx);
|
||||||
|
newHeight = newWidth / aspectRatio;
|
||||||
|
} else if (corner === 'bl') {
|
||||||
|
newWidth = Math.max(minSize, startWidth - dx);
|
||||||
|
newHeight = newWidth / aspectRatio;
|
||||||
|
newLeft = startLeft - (newWidth - startWidth);
|
||||||
|
} else if (corner === 'tr') {
|
||||||
|
newWidth = Math.max(minSize, startWidth + dx);
|
||||||
|
newHeight = newWidth / aspectRatio;
|
||||||
|
newTop = startTop - (newHeight - startHeight);
|
||||||
|
} else if (corner === 'tl') {
|
||||||
|
newWidth = Math.max(minSize, startWidth - dx);
|
||||||
|
newHeight = newWidth / aspectRatio;
|
||||||
|
newLeft = startLeft - (newWidth - startWidth);
|
||||||
|
newTop = startTop - (newHeight - startHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
overlayEl.style.width = newWidth + 'px';
|
||||||
|
overlayEl.style.height = newHeight + 'px';
|
||||||
|
overlayEl.style.left = newLeft + 'px';
|
||||||
|
overlayEl.style.top = newTop + 'px';
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
function endResize() {
|
||||||
|
isResizing = false;
|
||||||
|
document.removeEventListener('mousemove', handleResize);
|
||||||
|
document.removeEventListener('mouseup', endResize);
|
||||||
|
document.removeEventListener('touchmove', handleResize);
|
||||||
|
document.removeEventListener('touchend', endResize);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 选中层叠图
|
||||||
|
let selectedOverlay = null;
|
||||||
|
function selectOverlay(overlayEl) {
|
||||||
|
if (selectedOverlay) {
|
||||||
|
selectedOverlay.classList.remove('selected');
|
||||||
|
}
|
||||||
|
selectedOverlay = overlayEl;
|
||||||
|
overlayEl.classList.add('selected');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 点击背景取消选中层叠图
|
||||||
|
imageContainer.addEventListener('click', (e) => {
|
||||||
|
if (e.target === imageContainer ||
|
||||||
|
e.target === bgLayer ||
|
||||||
|
e.target === bgImage ||
|
||||||
|
e.target === emptyTip ||
|
||||||
|
e.target.closest('.preview-bg-layer')) {
|
||||||
|
if (selectedOverlay) {
|
||||||
|
selectedOverlay.classList.remove('selected');
|
||||||
|
selectedOverlay = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// 计算图片滑动边界
|
// 计算图片滑动边界
|
||||||
function calculateImageBounds() {
|
function calculateImageBounds() {
|
||||||
if (!currentImage) return;
|
if (!currentImage) return;
|
||||||
|
|||||||
Reference in New Issue
Block a user