From b708a01b1ddcd3a919ad5433295c2ff7ab596214 Mon Sep 17 00:00:00 2001 From: Shuanglei Tao Date: Fri, 1 Aug 2025 02:29:58 +0800 Subject: [PATCH] add image crop support --- html/css/main.css | 18 +++- html/index.html | 19 ++++- html/js/crop.js | 211 ++++++++++++++++++++++++++++++++++++++++++++++ html/js/main.js | 84 ++++++++++++------ 4 files changed, 299 insertions(+), 33 deletions(-) create mode 100644 html/js/crop.js diff --git a/html/css/main.css b/html/css/main.css index 6369d06..2b57106 100644 --- a/html/css/main.css +++ b/html/css/main.css @@ -257,6 +257,11 @@ code { margin: 0 auto; } +.canvas-container.crop #canvas { + border: 2px dashed var(--primary-color); + cursor: grab; +} + button { padding: 0.375rem 0.75rem; border: 1px solid var(--primary-color); @@ -367,10 +372,21 @@ canvas.text-placement-mode { justify-content: center; } -.text-tools { +.text-tools, +.crop-tools { display: none; } +.canvas-container.crop .tool-buttons, +.canvas-container.crop .brush-buttons, +.canvas-container.crop .text-tools { + display: none; +} + +.canvas-container.crop .crop-tools { + display: flex; +} + .tool-button { width: 36px; height: 36px; diff --git a/html/index.html b/html/index.html index 2bc1aa5..0063ed4 100644 --- a/html/index.html +++ b/html/index.html @@ -51,7 +51,7 @@
- 图片处理 + 蓝牙传图
@@ -120,8 +120,9 @@
状态:
- - + + +
@@ -129,7 +130,7 @@
-
+
@@ -191,6 +192,15 @@
+
+ + + + + + + +
@@ -205,6 +215,7 @@ + diff --git a/html/js/crop.js b/html/js/crop.js new file mode 100644 index 0000000..b957fed --- /dev/null +++ b/html/js/crop.js @@ -0,0 +1,211 @@ +let backgroundZoom = 1; +let backgroundPanX = 0; +let backgroundPanY = 0; +let isPanning = false; +let lastPanX = 0; +let lastPanY = 0; +let lastTouchDistance = 0; + +function resetCropStates() { + backgroundZoom = 1; + backgroundPanX = 0; + backgroundPanY = 0; + isPanning = false; + lastPanX = 0; + lastPanY = 0; +} + +function removeEventListeners() { + canvas.removeEventListener('wheel', handleBackgroundZoom); + canvas.removeEventListener('mousedown', handleBackgroundPanStart); + canvas.removeEventListener('mousemove', handleBackgroundPan); + canvas.removeEventListener('mouseup', handleBackgroundPanEnd); + canvas.removeEventListener('mouseleave', handleBackgroundPanEnd); + + canvas.removeEventListener('touchstart', handleTouchStart); + canvas.removeEventListener('touchmove', handleTouchMove); + canvas.removeEventListener('touchend', handleBackgroundPanEnd); + canvas.removeEventListener('touchcancel', handleBackgroundPanEnd); +} + +function isCropMode() { + return canvas.parentNode.classList.contains('crop'); +} + +function exitCropMode() { + removeEventListeners(); + canvas.parentNode.classList.remove('crop'); + setCanvasTitle(""); +} + +function initializeCrop() { + const imageFile = document.getElementById('imageFile'); + if (imageFile.files.length == 0) { + fillCanvas('white'); + return; + } + + resetCropStates(); + removeEventListeners(); + + canvas.style.backgroundImage = `url(${URL.createObjectURL(imageFile.files[0])})`; + canvas.style.backgroundSize = '100%'; + canvas.style.backgroundPosition = ''; + canvas.style.backgroundRepeat = 'no-repeat'; + + // add event listeners for zoom and pan + canvas.addEventListener('wheel', handleBackgroundZoom); + canvas.addEventListener('mousedown', handleBackgroundPanStart); + canvas.addEventListener('mousemove', handleBackgroundPan); + canvas.addEventListener('mouseup', handleBackgroundPanEnd); + canvas.addEventListener('mouseleave', handleBackgroundPanEnd); + + // Touch events for mobile devices + canvas.addEventListener('touchstart', handleTouchStart); + canvas.addEventListener('touchmove', handleTouchMove); + canvas.addEventListener('touchend', handleBackgroundPanEnd); + canvas.addEventListener('touchcancel', handleBackgroundPanEnd); + + // Make the canvas transparent + ctx.clearRect(0, 0, canvas.width, canvas.height); + + setCanvasTitle("裁剪模式: 可用鼠标或触摸缩放移动图片"); + canvas.parentNode.classList.add('crop'); +} + +function finishCrop() { + const imageFile = document.getElementById('imageFile'); + if (imageFile.files.length == 0) return; + + const image = new Image(); + image.onload = function () { + URL.revokeObjectURL(this.src); + + const fieldsetRect = canvas.getBoundingClientRect(); + const scale = (image.width / fieldsetRect.width) / backgroundZoom; + + const sx = -backgroundPanX * scale; + const sy = -backgroundPanY * scale; + const sWidth = fieldsetRect.width * scale; + const sHeight = fieldsetRect.height * scale; + + fillCanvas('white'); + ctx.drawImage(image, sx, sy, sWidth, sHeight, 0, 0, canvas.width, canvas.height); + + redrawTextElements(); + redrawLineSegments(); + convertDithering(); + + exitCropMode(); + }; + image.src = URL.createObjectURL(imageFile.files[0]); +} + +function handleTouchStart(e) { + e.preventDefault(); + if (e.touches.length === 1) { + handleBackgroundPanStart(e.touches[0]); + } else if (e.touches.length === 2) { + isPanning = false; // Stop panning when zooming + lastTouchDistance = getTouchDistance(e.touches); + } +} + +function handleTouchMove(e) { + e.preventDefault(); + if (isPanning && e.touches.length === 1) { + handleBackgroundPan(e.touches[0]); + } else if (e.touches.length === 2) { + const newDist = getTouchDistance(e.touches); + if (lastTouchDistance > 0) { + const zoomFactor = newDist / lastTouchDistance; + backgroundZoom *= zoomFactor; + backgroundZoom = Math.max(0.1, Math.min(5, backgroundZoom)); // Limit zoom range + updateBackgroundTransform(); + } + lastTouchDistance = newDist; + } +} + +function handleBackgroundZoom(e) { + e.preventDefault(); + const zoomFactor = e.deltaY > 0 ? 0.9 : 1.1; + backgroundZoom *= zoomFactor; + backgroundZoom = Math.max(0.1, Math.min(5, backgroundZoom)); // Limit zoom range + updateBackgroundTransform(); +} + +function handleBackgroundPanStart(e) { + isPanning = true; + lastPanX = e.clientX; + lastPanY = e.clientY; + canvas.style.cursor = 'grabbing'; +} + +function handleBackgroundPan(e) { + if (isPanning) { + const deltaX = e.clientX - lastPanX; + const deltaY = e.clientY - lastPanY; + backgroundPanX += deltaX; + backgroundPanY += deltaY; + lastPanX = e.clientX; + lastPanY = e.clientY; + updateBackgroundTransform(); + } +} + +function handleBackgroundPanEnd() { + isPanning = false; + lastTouchDistance = 0; // Reset touch distance + canvas.style.cursor = 'grab'; +} + +function updateBackgroundTransform() { + canvas.style.backgroundSize = `${100 * backgroundZoom}%`; + canvas.style.backgroundPosition = `${backgroundPanX}px ${backgroundPanY}px`; +} + +function getTouchDistance(touches) { + const touch1 = touches[0]; + const touch2 = touches[1]; + return Math.sqrt( + Math.pow(touch2.clientX - touch1.clientX, 2) + + Math.pow(touch2.clientY - touch1.clientY, 2) + ); +} + +function initCropTools() { + document.getElementById('crop-zoom-in').addEventListener('click', (e) => { + e.preventDefault(); + handleBackgroundZoom({ preventDefault: () => {}, deltaY: -1 }); + }); + + document.getElementById('crop-zoom-out').addEventListener('click', (e) => { + e.preventDefault(); + handleBackgroundZoom({ preventDefault: () => {}, deltaY: 1 }); + }); + + document.getElementById('crop-move-left').addEventListener('click', (e) => { + e.preventDefault(); + backgroundPanX -= 10; + updateBackgroundTransform(); + }); + + document.getElementById('crop-move-right').addEventListener('click', (e) => { + e.preventDefault(); + backgroundPanX += 10; + updateBackgroundTransform(); + }); + + document.getElementById('crop-move-up').addEventListener('click', (e) => { + e.preventDefault(); + backgroundPanY -= 10; + updateBackgroundTransform(); + }); + + document.getElementById('crop-move-down').addEventListener('click', (e) => { + e.preventDefault(); + backgroundPanY += 10; + updateBackgroundTransform(); + }); +} \ No newline at end of file diff --git a/html/js/main.js b/html/js/main.js index 503a06f..faa29e1 100644 --- a/html/js/main.js +++ b/html/js/main.js @@ -159,6 +159,11 @@ async function sendcmd() { } async function sendimg() { + if (isCropMode()) { + addLog("请先完成图片裁剪!发送已取消。"); + return; + } + const canvasSize = document.getElementById('canvasSize').value; const ditherMode = document.getElementById('ditherMode').value; const epdDriverSelect = document.getElementById('epddriver'); @@ -207,6 +212,11 @@ async function sendimg() { } function downloadDataArray() { + if (isCropMode()) { + addLog("请先完成图片裁剪!下载已取消。"); + return; + } + const mode = document.getElementById('ditherMode').value; const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); const processedData = processImageData(imageData, mode); @@ -417,6 +427,35 @@ function clearLog() { document.getElementById("log").innerHTML = ''; } +function fillCanvas(style) { + ctx.fillStyle = style; + ctx.fillRect(0, 0, canvas.width, canvas.height); +} + +function updateImage() { + const imageFile = document.getElementById('imageFile'); + if (imageFile.files.length == 0) { + fillCanvas('white'); + return; + } + + const image = new Image(); + image.onload = function () { + URL.revokeObjectURL(this.src); + if (image.width / image.height == canvas.width / canvas.height) { + if (isCropMode()) exitCropMode(); + ctx.drawImage(image, 0, 0, image.width, image.height, 0, 0, canvas.width, canvas.height); + redrawTextElements(); + redrawLineSegments(); + convertDithering(); + } else { + addLog("图片宽高比例与画布不匹配,已进入裁剪模式。"); + initializeCrop(); + } + }; + image.src = URL.createObjectURL(imageFile.files[0]); +} + function updateCanvasSize() { const selectedSizeName = document.getElementById('canvasSize').value; const selectedSize = canvasSizes.find(size => size.name === selectedSizeName); @@ -424,7 +463,7 @@ function updateCanvasSize() { canvas.width = selectedSize.width; canvas.height = selectedSize.height; - updateImage(false); + updateImage(); } function updateDitcherOptions() { @@ -439,33 +478,21 @@ function updateDitcherOptions() { updateCanvasSize(); // always update image } -function updateImage(clear = false) { - const imageFile = document.getElementById('imageFile'); - if (imageFile.files.length == 0) return; - const file = imageFile.files[0]; - - if (clear) clearCanvas(); - - let image = new Image(); - image.src = URL.createObjectURL(file); - image.onload = function (event) { - URL.revokeObjectURL(this.src); - ctx.drawImage(image, 0, 0, image.width, image.height, 0, 0, canvas.width, canvas.height); - - // Redraw text and lines - redrawTextElements(); - redrawLineSegments(); - - convertDithering() - } +function rotateCanvas() { + const currentWidth = canvas.width; + const currentHeight = canvas.height; + canvas.width = currentHeight; + canvas.height = currentWidth; + addLog(`画布已旋转: ${currentWidth}x${currentHeight} -> ${canvas.width}x${canvas.height}`); + updateImage(); } function clearCanvas() { - if (confirm('清除画布已有内容?')) { - ctx.fillStyle = 'white'; - ctx.fillRect(0, 0, canvas.width, canvas.height); + if (confirm('清除画布内容?')) { + fillCanvas('white'); textElements = []; // Clear stored text positions lineSegments = []; // Clear stored line segments + if (isCropMode()) exitCropMode(); return true; } return false; @@ -492,15 +519,15 @@ function convertDithering() { function initEventHandlers() { document.getElementById("epddriver").addEventListener("change", updateDitcherOptions); - document.getElementById("imageFile").addEventListener("change", function () { updateImage(true); }); - document.getElementById("ditherMode").addEventListener("change", function () { updateImage(false); }); - document.getElementById("ditherAlg").addEventListener("change", function () { updateImage(false); }); + document.getElementById("imageFile").addEventListener("change", updateImage); + document.getElementById("ditherMode").addEventListener("change", finishCrop); + document.getElementById("ditherAlg").addEventListener("change", finishCrop); document.getElementById("ditherStrength").addEventListener("input", function () { - updateImage(false); + finishCrop(); document.getElementById("ditherStrengthValue").innerText = parseFloat(this.value).toFixed(1); }); document.getElementById("ditherContrast").addEventListener("input", function () { - updateImage(false); + finishCrop(); document.getElementById("ditherContrastValue").innerText = parseFloat(this.value).toFixed(1); }); document.getElementById("canvasSize").addEventListener("change", updateCanvasSize); @@ -532,6 +559,7 @@ document.body.onload = () => { ctx.fillRect(0, 0, canvas.width, canvas.height); initPaintTools(); + initCropTools(); initEventHandlers(); updateButtonStatus(); checkDebugMode();