/** * ImagePreviewManager.js * จัดการการแสดงรูปภาพตัวอย่างและการเลือกพื้นที่ QR */ class ImagePreviewManager { constructor(debug = false) { this.debug = debug; this.currentImage = null; this.selectedBank = 'full'; this.cropSelector = null; this.savedPositions = this.loadSavedPositions(); this.imageElement = null; this.cropOverlay = null; this.cropSelection = null; // Mouse/Touch selection state this.isSelecting = false; this.isResizing = false; this.isDragging = false; this.startX = 0; this.startY = 0; this.offsetX = 0; this.offsetY = 0; // สำหรับ debounce การอัปเดตรูปที่ตัด this.cropUpdateTimeout = null; // ตำแหน่งเริ่มต้นสำหรับแต่ละธนาคาร (เป็นเปอร์เซ็นต์ของรูปภาพ) this.defaultBankPositions = { 'SCB': {x: 0.15, y: 0.7, width: 0.7, height: 0.25}, 'KBANK': {x: 0.1, y: 0.65, width: 0.8, height: 0.3}, 'BBL': {x: 0.1, y: 0.6, width: 0.8, height: 0.35}, 'KTB': {x: 0.15, y: 0.65, width: 0.7, height: 0.3}, 'TTB': {x: 0.1, y: 0.6, width: 0.8, height: 0.35}, 'BAY': {x: 0.1, y: 0.65, width: 0.8, height: 0.3}, 'GSB': {x: 0.15, y: 0.7, width: 0.7, height: 0.25}, 'full': {x: 0, y: 0, width: 1, height: 1}, 'custom': {x: 0.2, y: 0.2, width: 0.6, height: 0.6} }; this.init(); } /** * เริ่มต้นระบบ */ init() { this.bindEvents(); if (this.debug) { console.log('🖼️ ImagePreviewManager initialized'); } } /** * ผูก Event Listeners */ bindEvents() { // Bank selection const bankRadios = document.querySelectorAll('input[name="preview-bank-type"]'); bankRadios.forEach(radio => { radio.addEventListener('change', this.handleBankChange.bind(this)); }); // Action buttons const processBtn = document.getElementById('process-slip-btn'); const backBtn = document.getElementById('preview-back-btn'); const changeImageBtn = document.getElementById('change-image-btn'); if (processBtn) { processBtn.addEventListener('click', this.handleProcessSlip.bind(this)); } if (backBtn) { backBtn.addEventListener('click', this.handleBack.bind(this)); } if (changeImageBtn) { changeImageBtn.addEventListener('click', this.handleChangeImage.bind(this)); } // Image elements this.imageElement = document.getElementById('preview-image'); this.cropOverlay = document.getElementById('qr-crop-overlay'); this.cropSelection = document.getElementById('crop-selection'); if (this.imageElement) { this.imageElement.addEventListener('load', this.handleImageLoad.bind(this)); this.imageElement.addEventListener('click', this.handleImageClick.bind(this)); } } /** * แสดงรูปภาพตัวอย่าง */ showPreview(imageFile, imageDataUrl) { this.currentImage = { file: imageFile, dataUrl: imageDataUrl }; const imageElement = document.getElementById('preview-image'); if (imageElement) { imageElement.src = imageDataUrl; } // แสดงส่วน preview โดยใช้ SlipVerifier showSection if (window.slipVerifier && typeof window.slipVerifier.showSection === 'function') { window.slipVerifier.showSection('image-preview'); } else { this.showSection('image-preview'); } // รีเซ็ตการเลือกธนาคาร this.resetBankSelection(); if (this.debug) { console.log('🖼️ Showing image preview'); } } /** * จัดการเมื่อรูปภาพโหลดเสร็จ */ handleImageLoad() { if (this.debug) { console.log('🖼️ Image loaded, setting up crop area'); } // ตั้งค่าพื้นที่ crop เริ่มต้น this.setupCropArea(); } /** * ตั้งค่าพื้นที่ crop */ setupCropArea() { if (!this.imageElement || !this.cropOverlay || !this.cropSelection) { return; } const position = this.getCurrentBankPosition(); this.updateCropSelection(position); this.updateClickInstruction(); this.updateImageClickMode(); // ตั้งค่า interaction สำหรับ crop area this.setupCropInteraction(); } /** * ได้รับตำแหน่งปัจจุบันตามธนาคารที่เลือก */ getCurrentBankPosition() { // ตรวจสอบว่ามีตำแหน่งที่บันทึกไว้หรือไม่ if (this.savedPositions[this.selectedBank]) { return this.savedPositions[this.selectedBank]; } // ใช้ตำแหน่งเริ่มต้น return this.defaultBankPositions[this.selectedBank] || this.defaultBankPositions['full']; } /** * อัปเดตพื้นที่เลือก */ updateCropSelection(position) { if (!this.imageElement || !this.cropSelection) { return; } const rect = this.imageElement.getBoundingClientRect(); const imageWidth = rect.width; const imageHeight = rect.height; const left = position.x * imageWidth; const top = position.y * imageHeight; const width = position.width * imageWidth; const height = position.height * imageHeight; this.cropSelection.style.left = `${left}px`; this.cropSelection.style.top = `${top}px`; this.cropSelection.style.width = `${width}px`; this.cropSelection.style.height = `${height}px`; // แสดง/ซ่อน crop overlay if (this.selectedBank === 'full') { this.cropOverlay.style.display = 'none'; this.showCropControls(false); } else { this.cropOverlay.style.display = 'block'; this.showCropControls(true); // ตัดรูปและแสดงผลอัตโนมัติ setTimeout(() => this.cropAndShowPreview(), 100); } this.updateAreaDescription(); } /** * ตั้งค่าการ interact กับ crop area */ setupCropInteraction() { if (!this.cropSelection) { return; } // Remove existing event listeners first this.removeExistingListeners(); // Setup image click for creating new selection this.setupImageClick(); // Setup crop area interaction this.setupCropAreaInteraction(); // Setup resize handles this.setupResizeHandles(); // Debug: ตรวจสอบ setup if (this.debug) { console.log('🔧 Setting up crop interaction...'); console.log('Crop selection element:', this.cropSelection); console.log('Image element:', this.imageElement); } } /** * ลบ event listeners เดิม */ removeExistingListeners() { // Remove document listeners if they exist if (this.boundMouseMove) { document.removeEventListener('mousemove', this.boundMouseMove); document.removeEventListener('touchmove', this.boundTouchMove); } if (this.boundMouseUp) { document.removeEventListener('mouseup', this.boundMouseUp); document.removeEventListener('touchend', this.boundTouchEnd); } } /** * ตั้งค่าการคลิกบนรูปภาพเพื่อสร้าง crop area ใหม่ */ setupImageClick() { if (!this.imageElement) return; // Remove existing click listener this.imageElement.removeEventListener('mousedown', this.boundImageMouseDown); this.imageElement.removeEventListener('touchstart', this.boundImageTouchStart); // Create new bound functions this.boundImageMouseDown = this.handleImageMouseDown.bind(this); this.boundImageTouchStart = this.handleImageTouchStart.bind(this); // Add new listeners this.imageElement.addEventListener('mousedown', this.boundImageMouseDown); this.imageElement.addEventListener('touchstart', this.boundImageTouchStart, {passive: false}); } /** * ตั้งค่าการโต้ตอบกับ crop area (ลากเพื่อเลื่อน) */ setupCropAreaInteraction() { if (!this.cropSelection) return; // Remove existing listeners this.cropSelection.removeEventListener('mousedown', this.boundCropMouseDown); this.cropSelection.removeEventListener('touchstart', this.boundCropTouchStart); // Create new bound functions this.boundCropMouseDown = this.handleCropMouseDown.bind(this); this.boundCropTouchStart = this.handleCropTouchStart.bind(this); // Add new listeners this.cropSelection.addEventListener('mousedown', this.boundCropMouseDown); this.cropSelection.addEventListener('touchstart', this.boundCropTouchStart, {passive: false}); // Document-level move and up events this.boundMouseMove = this.handleMouseMove.bind(this); this.boundMouseUp = this.handleMouseUp.bind(this); this.boundTouchMove = this.handleTouchMove.bind(this); this.boundTouchEnd = this.handleTouchEnd.bind(this); document.addEventListener('mousemove', this.boundMouseMove); document.addEventListener('mouseup', this.boundMouseUp); document.addEventListener('touchmove', this.boundTouchMove, {passive: false}); document.addEventListener('touchend', this.boundTouchEnd); } /** * ตั้งค่า resize handles */ setupResizeHandles() { const handles = this.cropSelection.querySelectorAll('.crop-handle'); handles.forEach(handle => { // Remove existing listeners handle.removeEventListener('mousedown', this.boundHandleMouseDown); handle.removeEventListener('touchstart', this.boundHandleTouchStart); // Create bound functions if not exist if (!this.boundHandleMouseDown) { this.boundHandleMouseDown = this.handleResizeMouseDown.bind(this); this.boundHandleTouchStart = this.handleResizeTouchStart.bind(this); } // Add new listeners handle.addEventListener('mousedown', this.boundHandleMouseDown); handle.addEventListener('touchstart', this.boundHandleTouchStart, {passive: false}); }); } /** * จัดการการเปลี่ยนธนาคาร */ handleBankChange(event) { this.selectedBank = event.target.value; if (this.debug) { console.log('🏦 Bank changed to:', this.selectedBank); } // แสดงพื้นที่เริ่มต้นของธนาคารที่เลือก (ยกเว้น "ทั้งรูป") if (this.selectedBank !== 'full') { const position = this.getCurrentBankPosition(); this.updateCropSelection(position); } else { // ซ่อน crop overlay สำหรับ "ทั้งรูป" if (this.cropOverlay) { this.cropOverlay.style.display = 'none'; } } // แสดง/ซ่อนคำแนะนำการคลิก this.updateClickInstruction(); this.updateImageClickMode(); } /** * อัปเดตคำแนะนำการคลิก */ updateClickInstruction() { const instructionElement = document.getElementById('click-instruction'); if (!instructionElement) return; if (this.selectedBank === 'full') { instructionElement.style.display = 'none'; } else { instructionElement.style.display = 'block'; } } /** * อัปเดต cursor mode สำหรับการคลิก */ updateImageClickMode() { const wrapper = document.querySelector('.preview-image-wrapper'); if (!wrapper) return; if (this.selectedBank === 'full') { wrapper.classList.remove('click-mode'); } else { wrapper.classList.add('click-mode'); } } /** * อัปเดตคำอธิบายพื้นที่ */ updateAreaDescription() { const descElement = document.getElementById('qr-area-description'); if (!descElement) return; if (this.selectedBank === 'full') { descElement.textContent = 'ทั้งรูป'; } else if (this.selectedBank === 'custom') { descElement.textContent = 'กำหนดเอง'; } else { const bankNames = { 'SCB': 'ไทยพาณิชย์', 'KBANK': 'กสิกรไทย', 'BBL': 'กรุงเทพ', 'KTB': 'กรุงไทย', 'TTB': 'ทหารไทยธนชาต', 'BAY': 'กรุงศรีอยุธยา', 'GSB': 'ออมสิน' }; descElement.textContent = `${bankNames[this.selectedBank]} (QR Area)`; } } /** * แสดง/ซ่อนการควบคุม crop */ showCropControls(show) { // ไม่ต้องแสดงปุ่มควบคุมแล้ว เพราะใช้ auto-save return; } /** * จัดการการคลิกบนรูปภาพ (สร้าง crop area ใหม่ด้วยการลาก) */ handleImageMouseDown(event) { // ตรวจสอบว่าไม่ใช่การคลิกบน crop area หรือ handle if (event.target.closest('.crop-selection') || event.target.closest('.crop-handle')) { return; } // เฉพาะเมื่อไม่ใช่ "ทั้งรูป" if (this.selectedBank === 'full') { return; } this.isSelecting = true; const rect = this.imageElement.getBoundingClientRect(); this.startX = event.clientX - rect.left; this.startY = event.clientY - rect.top; // ซ่อน crop area ปัจจุบันชั่วคราว this.cropSelection.style.display = 'none'; event.preventDefault(); } /** * จัดการการลาก crop area (เลื่อนตำแหน่ง) */ handleCropMouseDown(event) { // ตรวจสอบว่าไม่ใช่ handle if (event.target.classList.contains('crop-handle')) { return; } this.isDragging = true; const cropRect = this.cropSelection.getBoundingClientRect(); const imageRect = this.imageElement.getBoundingClientRect(); this.offsetX = event.clientX - cropRect.left; this.offsetY = event.clientY - cropRect.top; // เพิ่ม visual feedback this.cropSelection.style.opacity = '0.8'; event.preventDefault(); event.stopPropagation(); } /** * จัดการการปรับขนาด */ handleResizeMouseDown(event) { this.isResizing = true; this.resizeHandle = event.target.classList[1]; // เช่น 'bottom-right' // เพิ่ม visual feedback this.cropSelection.style.opacity = '0.8'; event.stopPropagation(); event.preventDefault(); } /** * จัดการการเคลื่อนไหวของเมาส์ (สำหรับทุกโหมด) */ handleMouseMove(event) { if (this.isSelecting) { this.updateSelection(event.clientX, event.clientY); } else if (this.isDragging) { this.updateCropPosition(event.clientX, event.clientY); } else if (this.isResizing) { this.updateCropSize(event.clientX, event.clientY); } } /** * จัดการการปล่อยเมาส์ */ handleMouseUp(event) { if (this.isSelecting) { this.finishSelection(); } else if (this.isDragging || this.isResizing) { this.finishDragging(); } // รีเซ็ต state this.isSelecting = false; this.isDragging = false; this.isResizing = false; this.resizeHandle = null; // คืน visual feedback if (this.cropSelection) { this.cropSelection.style.opacity = '1'; } } /** * อัปเดตการเลือกพื้นที่ (ขณะลาก) */ updateSelection(clientX, clientY) { if (!this.imageElement || !this.cropSelection) return; const rect = this.imageElement.getBoundingClientRect(); const currentX = clientX - rect.left; const currentY = clientY - rect.top; // คำนวณขนาดและตำแหน่ง const left = Math.min(this.startX, currentX); const top = Math.min(this.startY, currentY); const width = Math.abs(currentX - this.startX); const height = Math.abs(currentY - this.startY); // ขนาดขั้นต่ำ const minSize = 30; if (width < minSize || height < minSize) return; // จำกัดไม่ให้เกินขอบรูปภาพ const maxWidth = rect.width - left; const maxHeight = rect.height - top; const finalWidth = Math.min(width, maxWidth); const finalHeight = Math.min(height, maxHeight); // แสดงและอัปเดต crop area this.cropSelection.style.display = 'block'; this.cropSelection.style.left = `${left}px`; this.cropSelection.style.top = `${top}px`; this.cropSelection.style.width = `${finalWidth}px`; this.cropSelection.style.height = `${finalHeight}px`; } /** * เสร็จสิ้นการเลือกพื้นที่ */ finishSelection() { if (!this.cropSelection) return; // ตรวจสอบว่ามีขนาดพอสมควร const width = this.cropSelection.offsetWidth; const height = this.cropSelection.offsetHeight; if (width < 30 || height < 30) { // คืนสู่ตำแหน่งเดิม const position = this.getCurrentBankPosition(); this.updateCropSelection(position); return; } // บันทึกตำแหน่งและอัปเดตรูปตัด this.saveCropPosition(); this.cropAndShowPreview(); } /** * เสร็จสิ้นการลาก/ปรับขนาด */ finishDragging() { // บันทึกตำแหน่งและอัปเดตรูปตัด this.saveCropPosition(); this.cropAndShowPreview(); } /** * อัปเดตตำแหน่ง crop (ขณะลาก) */ updateCropPosition(clientX, clientY) { if (!this.imageElement || !this.cropSelection) return; const imageRect = this.imageElement.getBoundingClientRect(); // คำนวณตำแหน่งใหม่ const newLeft = clientX - this.offsetX - imageRect.left; const newTop = clientY - this.offsetY - imageRect.top; // จำกัดไม่ให้เกินขอบรูปภาพ const cropWidth = this.cropSelection.offsetWidth; const cropHeight = this.cropSelection.offsetHeight; const maxLeft = imageRect.width - cropWidth; const maxTop = imageRect.height - cropHeight; const constrainedLeft = Math.max(0, Math.min(newLeft, maxLeft)); const constrainedTop = Math.max(0, Math.min(newTop, maxTop)); this.cropSelection.style.left = `${constrainedLeft}px`; this.cropSelection.style.top = `${constrainedTop}px`; // อัปเดตรูปที่ตัดแล้วแบบ real-time (debounced) this.debouncedCropUpdate(); } /** * อัปเดตขนาด crop (ขณะปรับขนาด) */ updateCropSize(clientX, clientY) { if (!this.imageElement || !this.cropSelection || !this.resizeHandle) return; const imageRect = this.imageElement.getBoundingClientRect(); // คำนวณตำแหน่งเมาส์ relative กับรูปภาพ const mouseX = clientX - imageRect.left; const mouseY = clientY - imageRect.top; const currentLeft = this.cropSelection.offsetLeft; const currentTop = this.cropSelection.offsetTop; const currentWidth = this.cropSelection.offsetWidth; const currentHeight = this.cropSelection.offsetHeight; let newLeft = currentLeft; let newTop = currentTop; let newWidth = currentWidth; let newHeight = currentHeight; // คำนวณขนาดใหม่ตาม handle ที่ถูกลาก switch (this.resizeHandle) { case 'top-left': newLeft = mouseX; newTop = mouseY; newWidth = currentLeft + currentWidth - mouseX; newHeight = currentTop + currentHeight - mouseY; break; case 'top-right': newTop = mouseY; newWidth = mouseX - currentLeft; newHeight = currentTop + currentHeight - mouseY; break; case 'bottom-left': newLeft = mouseX; newWidth = currentLeft + currentWidth - mouseX; newHeight = mouseY - currentTop; break; case 'bottom-right': newWidth = mouseX - currentLeft; newHeight = mouseY - currentTop; break; case 'top-center': newTop = mouseY; newHeight = currentTop + currentHeight - mouseY; break; case 'bottom-center': newHeight = mouseY - currentTop; break; case 'left-center': newLeft = mouseX; newWidth = currentLeft + currentWidth - mouseX; break; case 'right-center': newWidth = mouseX - currentLeft; break; } // ตรวจสอบขนาดขั้นต่ำ const minSize = 30; if (newWidth < minSize || newHeight < minSize) return; // ตรวจสอบไม่ให้เกินขอบรูปภาพ if (newLeft < 0 || newTop < 0 || newLeft + newWidth > imageRect.width || newTop + newHeight > imageRect.height) return; // อัปเดตตำแหน่งและขนาด this.cropSelection.style.left = `${newLeft}px`; this.cropSelection.style.top = `${newTop}px`; this.cropSelection.style.width = `${newWidth}px`; this.cropSelection.style.height = `${newHeight}px`; // อัปเดตรูปที่ตัดแล้วแบบ real-time (debounced) this.debouncedCropUpdate(); } /** * Debounced crop update เพื่อไม่ให้อัปเดตบ่อยเกินไป */ debouncedCropUpdate() { if (this.cropUpdateTimeout) { clearTimeout(this.cropUpdateTimeout); } this.cropUpdateTimeout = setTimeout(() => { this.cropAndShowPreview(); }, 200); // รอ 200ms หลังจากหยุดลากแล้วจึงอัปเดต } /** * รีเซ็ตการเลือกธนาคาร */ resetBankSelection() { const defaultRadio = document.querySelector('input[name="preview-bank-type"][value="full"]'); if (defaultRadio) { defaultRadio.checked = true; this.selectedBank = 'full'; this.updateCropSelection(this.defaultBankPositions['full']); } } /** * รีเซ็ต crop area */ handleResetCrop() { const position = this.defaultBankPositions[this.selectedBank] || this.defaultBankPositions['custom']; this.updateCropSelection(position); if (this.debug) { console.log('🔄 Crop area reset'); } } /** * บันทึกตำแหน่ง crop */ handleSaveCropPosition() { if (this.selectedBank === 'full') { this.showNotification('ไม่สามารถบันทึกตำแหน่งสำหรับ "ทั้งรูป" ได้', 'warning'); return; } const position = this.getCurrentCropPosition(); this.savedPositions[this.selectedBank] = position; this.savePosistionsToStorage(); this.showNotification('บันทึกตำแหน่งเรียบร้อยแล้ว', 'success'); if (this.debug) { console.log('💾 Saved position for', this.selectedBank, position); } } /** * ได้รับตำแหน่ง crop ปัจจุบัน */ getCurrentCropPosition() { if (!this.imageElement || !this.cropSelection) { return this.defaultBankPositions[this.selectedBank]; } // ใช้ getBoundingClientRect เพื่อให้ได้ตำแหน่งที่แม่นยำ const imageRect = this.imageElement.getBoundingClientRect(); const cropRect = this.cropSelection.getBoundingClientRect(); // คำนวณตำแหน่ง relative กับรูปภาพ const left = cropRect.left - imageRect.left; const top = cropRect.top - imageRect.top; const width = cropRect.width; const height = cropRect.height; // แปลงเป็นเปอร์เซ็นต์ของรูปภาพ return { x: left / imageRect.width, y: top / imageRect.height, width: width / imageRect.width, height: height / imageRect.height }; } /** * ได้รับพื้นที่ crop ในรูปแบบพิกเซล */ getCropAreaPixels() { if (this.selectedBank === 'full') { return null; // ใช้รูปเต็ม } const position = this.getCurrentCropPosition(); // สร้าง canvas เพื่อวัดขนาดรูปจริง const img = new Image(); img.src = this.currentImage.dataUrl; return new Promise((resolve) => { img.onload = () => { const cropArea = { x: Math.round(position.x * img.naturalWidth), y: Math.round(position.y * img.naturalHeight), width: Math.round(position.width * img.naturalWidth), height: Math.round(position.height * img.naturalHeight) }; resolve(cropArea); }; }); } /** * จัดการการประมวลผลสลิป */ async handleProcessSlip() { if (!this.currentImage) { this.showNotification('ไม่มีรูปภาพให้ประมวลผล', 'error'); return; } console.log('🔍 Processing slip...'); // บันทึกตำแหน่งอัตโนมัติก่อนประมวลผล this.saveCropPosition(); try { const cropArea = await this.getCropAreaPixels(); // ตรวจสอบ SlipVerifier instance const verifier = window.slipVerifier || window.app?.slipVerifier; if (!verifier) { console.error('SlipVerifier not found'); this.showNotification('ไม่พบระบบประมวลผล', 'error'); return; } console.log('📤 Sending to SlipVerifier:', { file: this.currentImage.file?.name, bank: this.selectedBank, cropArea: cropArea }); // ส่งข้อมูลไปยัง SlipVerifier await verifier.processImageWithCrop( this.currentImage.file, this.currentImage.dataUrl, cropArea, this.selectedBank ); } catch (error) { console.error('Error processing slip:', error); this.showNotification('เกิดข้อผิดพลาดในการประมวลผล: ' + error.message, 'error'); } } /** * จัดการการกลับ */ handleBack() { // กลับไปหน้าอัปโหลด โดยใช้ SlipVerifier showSection if (window.slipVerifier && typeof window.slipVerifier.showSection === 'function') { window.slipVerifier.showSection('upload'); } else { this.showSection('upload'); } } /** * จัดการการเปลี่ยนรูป */ handleChangeImage() { const fileInput = document.getElementById('file-input'); if (fileInput) { fileInput.click(); } } /** * แสดงส่วนต่างๆ */ showSection(sectionName) { const sections = ['upload', 'camera', 'image-preview', 'qr', 'processing', 'results', 'error']; sections.forEach(section => { const element = document.getElementById(`${section}-section`); if (element) { element.style.display = section === sectionName ? 'block' : 'none'; } }); } /** * โหลดตำแหน่งที่บันทึกไว้ */ loadSavedPositions() { try { const saved = localStorage.getItem('slip-verifier-crop-positions'); return saved ? JSON.parse(saved) : {}; } catch (error) { console.warn('Failed to load saved positions:', error); return {}; } } /** * บันทึกตำแหน่งลง localStorage */ savePosistionsToStorage() { try { localStorage.setItem('slip-verifier-crop-positions', JSON.stringify(this.savedPositions)); } catch (error) { console.warn('Failed to save positions:', error); } } /** * แสดงการแจ้งเตือน */ showNotification(message, type = 'info') { if (window.slipVerifier && window.slipVerifier.showNotification) { window.slipVerifier.showNotification(message, type); } else { console.log(`${type.toUpperCase()}: ${message}`); } } /** * จัดการการคลิกที่รูปภาพ (เริ่มต้นระบบ click-and-drag) */ handleImageClick(event) { if (this.selectedBank === 'full') { return; // ไม่ต้องเลือกพื้นที่สำหรับ "ทั้งรูป" } // เริ่มระบบ click-and-drag this.startRectangleSelection(event); } /** * เริ่มการเลือกพื้นที่แบบสี่เหลี่ยม (click-and-drag) */ startRectangleSelection(event) { if (!this.imageElement) return; event.preventDefault(); event.stopPropagation(); const imageRect = this.imageElement.getBoundingClientRect(); const startX = event.clientX - imageRect.left; const startY = event.clientY - imageRect.top; // ตรวจสอบว่าคลิกภายในรูปภาพหรือไม่ if (startX < 0 || startY < 0 || startX > imageRect.width || startY > imageRect.height) { return; } // บันทึกจุดเริ่มต้น this.selectionStart = { x: startX, y: startY }; this.isCreatingSelection = true; // แสดง crop overlay if (this.cropOverlay) { this.cropOverlay.style.display = 'block'; } // ซ่อน crop selection ชั่วคราวจนกว่าจะมีการลาก if (this.cropSelection) { this.cropSelection.style.display = 'none'; } // เปลี่ยน cursor document.body.style.cursor = 'crosshair'; // เพิ่ม event listeners document.addEventListener('mousemove', this.handleRectangleMove); document.addEventListener('mouseup', this.handleRectangleEnd); if (this.debug) { console.log('🎯 Start selection at:', {x: startX, y: startY}); } } /** * จัดการการเคลื่อนไหวเมาส์ขณะลาก */ handleRectangleMove = (event) => { if (!this.isCreatingSelection || !this.imageElement || !this.cropSelection) return; const imageRect = this.imageElement.getBoundingClientRect(); const currentX = event.clientX - imageRect.left; const currentY = event.clientY - imageRect.top; // จำกัดไม่ให้เกินขอบรูป const constrainedX = Math.max(0, Math.min(currentX, imageRect.width)); const constrainedY = Math.max(0, Math.min(currentY, imageRect.height)); // คำนวณกรอบสี่เหลี่ยม const left = Math.min(this.selectionStart.x, constrainedX); const top = Math.min(this.selectionStart.y, constrainedY); const width = Math.abs(constrainedX - this.selectionStart.x); const height = Math.abs(constrainedY - this.selectionStart.y); // ถ้าขนาดมากกว่า 5px ค่อยแสดงกรอบ if (width > 5 && height > 5) { this.cropSelection.style.display = 'block'; this.cropSelection.style.left = `${left}px`; this.cropSelection.style.top = `${top}px`; this.cropSelection.style.width = `${width}px`; this.cropSelection.style.height = `${height}px`; } if (this.debug) { console.log('📏 Rectangle:', {left, top, width, height}); } } /** * จัดการการปล่อยเมาส์ (สิ้นสุดการเลือก) */ handleRectangleEnd = (event) => { if (!this.isCreatingSelection) return; this.isCreatingSelection = false; // ลบ event listeners document.removeEventListener('mousemove', this.handleRectangleMove); document.removeEventListener('mouseup', this.handleRectangleEnd); // คืนค่า cursor document.body.style.cursor = ''; if (!this.imageElement || !this.cropSelection) return; const imageRect = this.imageElement.getBoundingClientRect(); const currentX = event.clientX - imageRect.left; const currentY = event.clientY - imageRect.top; const constrainedX = Math.max(0, Math.min(currentX, imageRect.width)); const constrainedY = Math.max(0, Math.min(currentY, imageRect.height)); const left = Math.min(this.selectionStart.x, constrainedX); const top = Math.min(this.selectionStart.y, constrainedY); const width = Math.abs(constrainedX - this.selectionStart.x); const height = Math.abs(constrainedY - this.selectionStart.y); // ตรวจสอบขนาดขั้นต่ำ if (width < 10 || height < 10) { // ถ้าเล็กเกินไป ให้สร้างกรอบเล็กๆ รอบจุดที่คลิก this.createMinimumSizeRectangle(this.selectionStart.x, this.selectionStart.y); return; } // แสดงกรอบขั้นสุดท้าย this.cropSelection.style.display = 'block'; this.cropSelection.style.left = `${left}px`; this.cropSelection.style.top = `${top}px`; this.cropSelection.style.width = `${width}px`; this.cropSelection.style.height = `${height}px`; // เพิ่ม animation this.cropSelection.classList.add('selection-created'); setTimeout(() => { this.cropSelection.classList.remove('selection-created'); }, 300); // ตั้งค่า interaction handles this.setupCropInteraction(); // บันทึกและตัดรูป this.saveCropPosition(); this.cropAndShowPreview(); if (this.debug) { console.log('✅ Rectangle completed:', {left, top, width, height}); } } /** * สร้างกรอบขนาดขั้นต่ำเมื่อคลิกเดียว */ createMinimumSizeRectangle(centerX, centerY) { if (!this.imageElement || !this.cropSelection) return; const imageRect = this.imageElement.getBoundingClientRect(); const size = 80; // ขนาดขั้นต่ำ const halfSize = size / 2; let left = centerX - halfSize; let top = centerY - halfSize; let width = size; let height = size; // ปรับให้อยู่ในขอบเขตรูป if (left < 0) left = 0; if (top < 0) top = 0; if (left + width > imageRect.width) left = imageRect.width - width; if (top + height > imageRect.height) top = imageRect.height - height; if (width > imageRect.width) width = imageRect.width; if (height > imageRect.height) height = imageRect.height; // แสดงกรอบ this.cropSelection.style.display = 'block'; this.cropSelection.style.left = `${left}px`; this.cropSelection.style.top = `${top}px`; this.cropSelection.style.width = `${width}px`; this.cropSelection.style.height = `${height}px`; // เพิ่ม animation this.cropSelection.classList.add('click-created'); setTimeout(() => { this.cropSelection.classList.remove('click-created'); }, 300); // ตั้งค่า interaction handles this.setupCropInteraction(); // บันทึกและตัดรูป this.saveCropPosition(); this.cropAndShowPreview(); if (this.debug) { console.log('🎯 Created minimum rectangle at:', {left, top, width, height}); } } /** * เริ่มระบบ click-and-drag selection */ startClickAndDragSelection(event) { if (!this.imageElement) return; event.preventDefault(); event.stopPropagation(); const imageRect = this.imageElement.getBoundingClientRect(); const startX = event.clientX - imageRect.left; const startY = event.clientY - imageRect.top; // ตรวจสอบว่าคลิกภายในรูปภาพหรือไม่ if (startX < 0 || startY < 0 || startX > imageRect.width || startY > imageRect.height) { return; } // เก็บตำแหน่งเริ่มต้น this.dragStartX = startX; this.dragStartY = startY; this.isCreatingSelection = true; // สร้าง crop selection ใหม่ (เริ่มต้นขนาดเล็ก) if (this.cropSelection) { this.cropSelection.style.left = `${startX}px`; this.cropSelection.style.top = `${startY}px`; this.cropSelection.style.width = '2px'; this.cropSelection.style.height = '2px'; this.cropSelection.style.display = 'block'; } // แสดง crop overlay if (this.cropOverlay) { this.cropOverlay.style.display = 'block'; } // เพิ่ม cursor แบบ crosshair this.imageElement.style.cursor = 'crosshair'; // เพิ่ม event listeners สำหรับการลาก const handleMouseMove = (e) => this.updateClickAndDragSelection(e); const handleMouseUp = (e) => this.finishClickAndDragSelection(e, handleMouseMove, handleMouseUp); document.addEventListener('mousemove', handleMouseMove); document.addEventListener('mouseup', handleMouseUp); if (this.debug) { console.log('🎯 Started click-and-drag at:', {x: startX, y: startY}); } } /** * อัปเดตการเลือกพื้นที่ขณะลาก */ updateClickAndDragSelection(event) { if (!this.isCreatingSelection || !this.imageElement || !this.cropSelection) return; const imageRect = this.imageElement.getBoundingClientRect(); const currentX = event.clientX - imageRect.left; const currentY = event.clientY - imageRect.top; // จำกัดไม่ให้เกินขอบรูป const constrainedX = Math.max(0, Math.min(currentX, imageRect.width)); const constrainedY = Math.max(0, Math.min(currentY, imageRect.height)); // คำนวณขนาดและตำแหน่งของกรอบ const left = Math.min(this.dragStartX, constrainedX); const top = Math.min(this.dragStartY, constrainedY); const width = Math.abs(constrainedX - this.dragStartX); const height = Math.abs(constrainedY - this.dragStartY); // อัปเดตตำแหน่งและขนาด this.cropSelection.style.left = `${left}px`; this.cropSelection.style.top = `${top}px`; this.cropSelection.style.width = `${width}px`; this.cropSelection.style.height = `${height}px`; // เพิ่ม class สำหรับ animation ขณะสร้าง if (width > 5 && height > 5) { this.cropSelection.classList.add('creating'); } // อัปเดตข้อมูลขนาดแบบเรียลไทม์ this.updateSelectionInfo(width, height); // แสดงข้อมูลขนาดในเวลาจริง (ถ้าต้องการ) if (this.debug && width > 5 && height > 5) { console.log('📏 Selection size:', {width, height}); } } /** * อัปเดตข้อมูลขนาดการเลือก */ updateSelectionInfo(width, height) { // หาบริเวณแสดงข้อมูลขนาด let sizeInfo = document.getElementById('selection-size-info'); if (!sizeInfo) { sizeInfo = document.createElement('div'); sizeInfo.id = 'selection-size-info'; sizeInfo.className = 'selection-size-info'; const wrapper = document.querySelector('.preview-image-wrapper'); if (wrapper) { wrapper.appendChild(sizeInfo); } } if (width > 5 && height > 5) { sizeInfo.textContent = `${Math.round(width)} × ${Math.round(height)} px`; sizeInfo.style.display = 'block'; } else { sizeInfo.style.display = 'none'; } } /** * ซ่อนข้อมูลขนาดการเลือก */ hideSelectionInfo() { const sizeInfo = document.getElementById('selection-size-info'); if (sizeInfo) { sizeInfo.style.display = 'none'; } } /** * เสร็จสิ้นการเลือกพื้นที่ click-and-drag */ finishClickAndDragSelection(event, mouseMoveHandler, mouseUpHandler) { this.isCreatingSelection = false; // ลบ event listeners document.removeEventListener('mousemove', mouseMoveHandler); document.removeEventListener('mouseup', mouseUpHandler); // คืนค่า cursor this.imageElement.style.cursor = ''; // ซ่อนข้อมูลขนาด this.hideSelectionInfo(); if (!this.imageElement || !this.cropSelection) return; // ลบ class creating this.cropSelection.classList.remove('creating'); const finalWidth = this.cropSelection.offsetWidth; const finalHeight = this.cropSelection.offsetHeight; // ตรวจสอบขนาดขั้นต่ำ const minSize = 10; if (finalWidth < minSize || finalHeight < minSize) { // ถ้าเล็กเกินไป ให้สร้างกรอบขนาดมาตรฐานรอบจุดที่คลิก this.createStandardCropArea(event); return; } // เพิ่ม animation class this.cropSelection.classList.add('selection-created'); setTimeout(() => { this.cropSelection.classList.remove('selection-created'); }, 300); // ตั้งค่า interaction สำหรับ crop area this.setupCropInteraction(); // บันทึกตำแหน่งอัตโนมัติและตัดรูป this.saveCropPosition(); this.cropAndShowPreview(); if (this.debug) { const position = this.getCurrentCropPosition(); console.log('🎯 Finished click-and-drag selection:', position); } } /** * สร้างพื้นที่ crop ขนาดมาตรฐานรอบจุดที่คลิก */ createStandardCropArea(event) { if (!this.imageElement) return; const imageRect = this.imageElement.getBoundingClientRect(); const clickX = event.clientX - imageRect.left; const clickY = event.clientY - imageRect.top; // สร้างพื้นที่ crop รอบๆ จุดที่คลิก const cropSize = 120; // ขนาดพื้นที่ crop มาตรฐาน const halfSize = cropSize / 2; let left = clickX - halfSize; let top = clickY - halfSize; let width = cropSize; let height = cropSize; // ปรับให้อยู่ในขอบเขตรูปภาพ if (left < 0) left = 0; if (top < 0) top = 0; if (left + width > imageRect.width) left = imageRect.width - width; if (top + height > imageRect.height) top = imageRect.height - height; if (width > imageRect.width) width = imageRect.width; if (height > imageRect.height) height = imageRect.height; // แปลงเป็นเปอร์เซ็นต์ const position = { x: left / imageRect.width, y: top / imageRect.height, width: width / imageRect.width, height: height / imageRect.height }; this.updateCropSelection(position); // แสดง crop overlay if (this.cropOverlay) { this.cropOverlay.style.display = 'block'; } // เพิ่ม animation class if (this.cropSelection) { this.cropSelection.classList.add('click-created'); setTimeout(() => { this.cropSelection.classList.remove('click-created'); }, 300); } // ตั้งค่า interaction สำหรับ crop area this.setupCropInteraction(); // บันทึกตำแหน่งอัตโนมัติและตัดรูป this.saveCropPosition(); this.cropAndShowPreview(); if (this.debug) { console.log('🎯 Created standard crop area at:', position); } } /** * บันทึกตำแหน่ง crop อัตโนมัติ */ saveCropPosition() { if (this.selectedBank === 'full') { return; } const position = this.getCurrentCropPosition(); this.savedPositions[this.selectedBank] = position; this.savePosistionsToStorage(); if (this.debug) { console.log('💾 Auto-saved position for', this.selectedBank, position); } } /** * ตัดรูปตาม crop selection และแสดงผล */ async cropAndShowPreview() { if (!this.currentImage || !this.currentImage.dataUrl) { this.showNotification('ไม่มีรูปภาพให้ตัด', 'error'); return null; } try { const cropArea = await this.getCropAreaPixels(); if (this.debug) { console.log('🔍 Crop area pixels:', cropArea); console.log('🔍 Current crop position:', this.getCurrentCropPosition()); } if (!cropArea) { // ถ้าเป็น "ทั้งรูป" ให้ใช้รูปต้นฉบับ return this.currentImage.dataUrl; } const croppedImageUrl = await this.cropImageWithCanvas( this.currentImage.dataUrl, cropArea ); return croppedImageUrl; } catch (error) { console.error('Error cropping image:', error); this.showNotification('เกิดข้อผิดพลาดในการตัดรูป: ' + error.message, 'error'); return null; } } /** * ตัดรูปด้วย Canvas */ async cropImageWithCanvas(imageDataUrl, cropArea) { return new Promise((resolve, reject) => { const img = new Image(); img.onload = () => { try { const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); // ตั้งขนาด canvas ตามพื้นที่ที่ต้องการตัด canvas.width = cropArea.width; canvas.height = cropArea.height; // วาดส่วนที่ต้องการตัดลงใน canvas ctx.drawImage( img, cropArea.x, cropArea.y, cropArea.width, cropArea.height, // source rectangle 0, 0, cropArea.width, cropArea.height // destination rectangle ); // แปลงเป็น data URL const croppedDataUrl = canvas.toDataURL('image/jpeg', 0.9); resolve(croppedDataUrl); } catch (error) { reject(error); } }; img.onerror = () => reject(new Error('Failed to load image for cropping')); img.src = imageDataUrl; }); } // Touch events for mobile support handleImageTouchStart(event) { if (event.touches.length === 1) { const touch = event.touches[0]; this.handleImageMouseDown({ clientX: touch.clientX, clientY: touch.clientY, target: event.target, preventDefault: () => event.preventDefault(), stopPropagation: () => event.stopPropagation() }); } } handleCropTouchStart(event) { if (event.touches.length === 1) { const touch = event.touches[0]; this.handleCropMouseDown({ clientX: touch.clientX, clientY: touch.clientY, target: event.target, preventDefault: () => event.preventDefault(), stopPropagation: () => event.stopPropagation() }); } } handleResizeTouchStart(event) { if (event.touches.length === 1) { const touch = event.touches[0]; this.handleResizeMouseDown({ clientX: touch.clientX, clientY: touch.clientY, target: event.target, preventDefault: () => event.preventDefault(), stopPropagation: () => event.stopPropagation() }); } } handleTouchMove(event) { if (event.touches.length === 1) { const touch = event.touches[0]; this.handleMouseMove({ clientX: touch.clientX, clientY: touch.clientY }); event.preventDefault(); } } handleTouchEnd(event) { this.handleMouseUp(); event.preventDefault(); } }