import { appState } from '../state.js'; import { DETECTION_CONFIG } from '../config.js'; import { loadImage } from '../utils/imageUtils.js'; let cropState = { originalBox: null, currentBox: null, isDragging: false, isResizing: false, resizeHandle: null, dragStartX: 0, dragStartY: 0, imageWidth: 0, imageHeight: 0, loadedImage: null, canvas: null, scale: 1 }; export function initializeCropEditor() { const editCropBtn = document.getElementById('editCropBtn'); const applyCropBtn = document.getElementById('applyCropBtn'); const resetCropBtn = document.getElementById('resetCropBtn'); const cropControls = document.getElementById('cropControls'); if (editCropBtn) { editCropBtn.addEventListener('click', () => { showManualCropEditor(); }); } if (resetCropBtn) { resetCropBtn.addEventListener('click', () => { resetCropBox(); }); } if (applyCropBtn) { applyCropBtn.addEventListener('click', () => { applyCropSettings(); hideManualCropEditor(); }); } } export function showCropEditor(embryoIndex) { const editCropBtn = document.getElementById('editCropBtn'); const cropControls = document.getElementById('cropControls'); const embryo = appState.croppedEmbryos[embryoIndex]; if (!embryo || !embryo.box) { editCropBtn.style.display = 'none'; if (cropControls) cropControls.style.display = 'none'; return; } // Store original box coordinates cropState.originalBox = { ...embryo.box }; cropState.currentBox = { ...embryo.box }; editCropBtn.style.display = 'block'; // Always hide crop controls when switching embryos if (cropControls) cropControls.style.display = 'none'; // Reset the display to show the image, not the canvas const croppedImage = document.getElementById('croppedImage'); if (croppedImage) croppedImage.style.display = 'block'; // Hide crop canvas if exists const cropCanvas = document.getElementById('manualCropCanvas'); if (cropCanvas) cropCanvas.style.display = 'none'; } export function hideCropEditor() { const editCropBtn = document.getElementById('editCropBtn'); const cropControls = document.getElementById('cropControls'); editCropBtn.style.display = 'none'; cropControls.style.display = 'none'; hideManualCropEditor(); } function showManualCropEditor() { const cropControls = document.getElementById('cropControls'); const editCropBtn = document.getElementById('editCropBtn'); cropControls.style.display = 'block'; editCropBtn.style.display = 'none'; // Show touch hint on touch devices const touchHint = document.querySelector('.touch-hint'); const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0; if (touchHint && isTouchDevice) { touchHint.style.display = 'block'; } // Initialize the interactive crop canvas initializeInteractiveCrop(); } function hideManualCropEditor() { const cropControls = document.getElementById('cropControls'); const editCropBtn = document.getElementById('editCropBtn'); const cropCanvas = document.getElementById('manualCropCanvas'); const croppedImage = document.getElementById('croppedImage'); cropControls.style.display = 'none'; editCropBtn.style.display = 'block'; if (cropCanvas) cropCanvas.style.display = 'none'; if (croppedImage) croppedImage.style.display = 'block'; } async function initializeInteractiveCrop() { if (!appState.currentImage || !cropState.originalBox) { console.error('Missing image or box data'); return; } const container = document.getElementById('croppedImagePreview'); const croppedImage = document.getElementById('croppedImage'); if (!container) { console.error('Container not found'); return; } // Hide the static image if (croppedImage) croppedImage.style.display = 'none'; // Create or get canvas let canvas = document.getElementById('manualCropCanvas'); if (!canvas) { canvas = document.createElement('canvas'); canvas.id = 'manualCropCanvas'; canvas.className = 'manual-crop-canvas'; container.appendChild(canvas); } canvas.style.display = 'block'; try { // Load the original full image const img = await loadImage(appState.currentImage); // Store the image in cropState for redraws cropState.loadedImage = img; // Set canvas size to fit container while maintaining aspect ratio const containerWidth = Math.max(container.clientWidth - 40, 300); const containerHeight = 400; const scale = Math.min(containerWidth / img.width, containerHeight / img.height, 1); canvas.width = Math.floor(img.width * scale); canvas.height = Math.floor(img.height * scale); cropState.imageWidth = canvas.width; cropState.imageHeight = canvas.height; cropState.scale = scale; console.log('Crop editor initialized:', { imageSize: `${img.width}x${img.height}`, canvasSize: `${canvas.width}x${canvas.height}`, scale: scale, originalBox: cropState.originalBox }); // Scale the crop box coordinates cropState.currentBox = { x1: Math.floor(cropState.originalBox.x1 * scale), y1: Math.floor(cropState.originalBox.y1 * scale), x2: Math.floor(cropState.originalBox.x2 * scale), y2: Math.floor(cropState.originalBox.y2 * scale) }; console.log('Scaled crop box:', cropState.currentBox); // Draw initial state drawCropInterface(canvas); // Add event listeners setupCanvasEventListeners(canvas); } catch (error) { console.error('Error initializing crop editor:', error); } } function drawCropInterface(canvas) { if (!canvas || !cropState.loadedImage || !cropState.currentBox) { console.error('Canvas, image, or crop box not available', { canvas: !!canvas, image: !!cropState.loadedImage, box: !!cropState.currentBox }); return; } const ctx = canvas.getContext('2d'); const img = cropState.loadedImage; // Clear canvas ctx.clearRect(0, 0, canvas.width, canvas.height); // Draw the full image ctx.drawImage(img, 0, 0, canvas.width, canvas.height); // Ensure crop box is within bounds const box = cropState.currentBox; const x1 = Math.max(0, Math.min(box.x1, canvas.width)); const y1 = Math.max(0, Math.min(box.y1, canvas.height)); const x2 = Math.max(0, Math.min(box.x2, canvas.width)); const y2 = Math.max(0, Math.min(box.y2, canvas.height)); // Draw dark overlay outside crop box ctx.fillStyle = 'rgba(0, 0, 0, 0.5)'; // Top ctx.fillRect(0, 0, canvas.width, y1); // Bottom ctx.fillRect(0, y2, canvas.width, canvas.height - y2); // Left ctx.fillRect(0, y1, x1, y2 - y1); // Right ctx.fillRect(x2, y1, canvas.width - x2, y2 - y1); // Draw crop box border ctx.strokeStyle = '#00B8D4'; ctx.lineWidth = 3; ctx.strokeRect( x1, y1, x2 - x1, y2 - y1 ); // Determine if touch device (larger handles for touch) const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0; const handleSize = isTouchDevice ? 20 : 12; // Draw resize handles with white border for better visibility ctx.fillStyle = '#00B8D4'; ctx.strokeStyle = 'white'; ctx.lineWidth = 2; // Corner handles const corners = [ { x: x1, y: y1 }, { x: x2, y: y1 }, { x: x1, y: y2 }, { x: x2, y: y2 } ]; corners.forEach(corner => { ctx.fillRect(corner.x - handleSize / 2, corner.y - handleSize / 2, handleSize, handleSize); ctx.strokeRect(corner.x - handleSize / 2, corner.y - handleSize / 2, handleSize, handleSize); }); // Edge handles const edges = [ { x: (x1 + x2) / 2, y: y1 }, { x: (x1 + x2) / 2, y: y2 }, { x: x1, y: (y1 + y2) / 2 }, { x: x2, y: (y1 + y2) / 2 } ]; edges.forEach(edge => { ctx.fillRect(edge.x - handleSize / 2, edge.y - handleSize / 2, handleSize, handleSize); ctx.strokeRect(edge.x - handleSize / 2, edge.y - handleSize / 2, handleSize, handleSize); }); // Draw center move icon (larger for touch) const centerX = (x1 + x2) / 2; const centerY = (y1 + y2) / 2; const centerRadius = isTouchDevice ? 20 : 15; ctx.fillStyle = 'rgba(0, 184, 212, 0.3)'; ctx.beginPath(); ctx.arc(centerX, centerY, centerRadius, 0, Math.PI * 2); ctx.fill(); // Draw crosshair in center ctx.strokeStyle = 'white'; ctx.lineWidth = 2; const crosshairSize = isTouchDevice ? 10 : 8; ctx.beginPath(); ctx.moveTo(centerX - crosshairSize, centerY); ctx.lineTo(centerX + crosshairSize, centerY); ctx.moveTo(centerX, centerY - crosshairSize); ctx.lineTo(centerX, centerY + crosshairSize); ctx.stroke(); } function setupCanvasEventListeners(canvas) { // Store canvas reference cropState.canvas = canvas; // Mouse events canvas.addEventListener('mousedown', handleMouseDown); canvas.addEventListener('mousemove', handleMouseMove); canvas.addEventListener('mouseup', handleMouseUp); canvas.addEventListener('mouseleave', handleMouseUp); // Touch events for mobile/tablet support canvas.addEventListener('touchstart', handleTouchStart, { passive: false }); canvas.addEventListener('touchmove', handleTouchMove, { passive: false }); canvas.addEventListener('touchend', handleTouchEnd, { passive: false }); canvas.addEventListener('touchcancel', handleTouchEnd, { passive: false }); } function handleMouseDown(e) { const canvas = e.target; const rect = canvas.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top; // Check if clicking on a resize handle const handle = getResizeHandle(x, y); if (handle) { cropState.isResizing = true; cropState.resizeHandle = handle; cropState.dragStartX = x; cropState.dragStartY = y; e.preventDefault(); return; } // Check if clicking inside crop box for dragging if (isInsideCropBox(x, y)) { cropState.isDragging = true; cropState.dragStartX = x; cropState.dragStartY = y; e.preventDefault(); } } function handleMouseMove(e) { const canvas = e.target; const rect = canvas.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top; // Update cursor if (!cropState.isDragging && !cropState.isResizing) { const handle = getResizeHandle(x, y); if (handle) { canvas.style.cursor = getCursorStyle(handle); } else if (isInsideCropBox(x, y)) { canvas.style.cursor = 'move'; } else { canvas.style.cursor = 'default'; } } if (cropState.isResizing) { handleResize(x, y); drawCropInterface(canvas); e.preventDefault(); } else if (cropState.isDragging) { handleDrag(x, y); drawCropInterface(canvas); e.preventDefault(); } } function handleMouseUp(e) { cropState.isDragging = false; cropState.isResizing = false; cropState.resizeHandle = null; } // Touch event handlers function handleTouchStart(e) { // Only handle single touch if (e.touches.length !== 1) return; e.preventDefault(); // Prevent scrolling const canvas = e.target; const rect = canvas.getBoundingClientRect(); const touch = e.touches[0]; const x = touch.clientX - rect.left; const y = touch.clientY - rect.top; // Check if touching a resize handle (increased threshold for touch) const handle = getResizeHandle(x, y, true); if (handle) { cropState.isResizing = true; cropState.resizeHandle = handle; cropState.dragStartX = x; cropState.dragStartY = y; return; } // Check if touching inside crop box for dragging if (isInsideCropBox(x, y)) { cropState.isDragging = true; cropState.dragStartX = x; cropState.dragStartY = y; } } function handleTouchMove(e) { // Only handle single touch if (e.touches.length !== 1) return; // Prevent scrolling while manipulating crop box if (cropState.isDragging || cropState.isResizing) { e.preventDefault(); } const canvas = e.target; const rect = canvas.getBoundingClientRect(); const touch = e.touches[0]; const x = touch.clientX - rect.left; const y = touch.clientY - rect.top; if (cropState.isResizing) { handleResize(x, y); drawCropInterface(canvas); } else if (cropState.isDragging) { handleDrag(x, y); drawCropInterface(canvas); } } function handleTouchEnd(e) { e.preventDefault(); cropState.isDragging = false; cropState.isResizing = false; cropState.resizeHandle = null; } function getResizeHandle(x, y, isTouch = false) { // Larger threshold for touch screens const threshold = isTouch ? 25 : 15; // Corner handles if (Math.abs(x - cropState.currentBox.x1) < threshold && Math.abs(y - cropState.currentBox.y1) < threshold) return 'nw'; if (Math.abs(x - cropState.currentBox.x2) < threshold && Math.abs(y - cropState.currentBox.y1) < threshold) return 'ne'; if (Math.abs(x - cropState.currentBox.x1) < threshold && Math.abs(y - cropState.currentBox.y2) < threshold) return 'sw'; if (Math.abs(x - cropState.currentBox.x2) < threshold && Math.abs(y - cropState.currentBox.y2) < threshold) return 'se'; // Edge handles const centerX = (cropState.currentBox.x1 + cropState.currentBox.x2) / 2; const centerY = (cropState.currentBox.y1 + cropState.currentBox.y2) / 2; if (Math.abs(x - centerX) < threshold && Math.abs(y - cropState.currentBox.y1) < threshold) return 'n'; if (Math.abs(x - centerX) < threshold && Math.abs(y - cropState.currentBox.y2) < threshold) return 's'; if (Math.abs(x - cropState.currentBox.x1) < threshold && Math.abs(y - centerY) < threshold) return 'w'; if (Math.abs(x - cropState.currentBox.x2) < threshold && Math.abs(y - centerY) < threshold) return 'e'; return null; } function getCursorStyle(handle) { const cursors = { 'nw': 'nw-resize', 'ne': 'ne-resize', 'sw': 'sw-resize', 'se': 'se-resize', 'n': 'n-resize', 's': 's-resize', 'w': 'w-resize', 'e': 'e-resize' }; return cursors[handle] || 'default'; } function isInsideCropBox(x, y) { return x >= cropState.currentBox.x1 && x <= cropState.currentBox.x2 && y >= cropState.currentBox.y1 && y <= cropState.currentBox.y2; } function handleResize(x, y) { const minSize = 50; switch (cropState.resizeHandle) { case 'nw': cropState.currentBox.x1 = Math.max(0, Math.min(x, cropState.currentBox.x2 - minSize)); cropState.currentBox.y1 = Math.max(0, Math.min(y, cropState.currentBox.y2 - minSize)); break; case 'ne': cropState.currentBox.x2 = Math.min(cropState.imageWidth, Math.max(x, cropState.currentBox.x1 + minSize)); cropState.currentBox.y1 = Math.max(0, Math.min(y, cropState.currentBox.y2 - minSize)); break; case 'sw': cropState.currentBox.x1 = Math.max(0, Math.min(x, cropState.currentBox.x2 - minSize)); cropState.currentBox.y2 = Math.min(cropState.imageHeight, Math.max(y, cropState.currentBox.y1 + minSize)); break; case 'se': cropState.currentBox.x2 = Math.min(cropState.imageWidth, Math.max(x, cropState.currentBox.x1 + minSize)); cropState.currentBox.y2 = Math.min(cropState.imageHeight, Math.max(y, cropState.currentBox.y1 + minSize)); break; case 'n': cropState.currentBox.y1 = Math.max(0, Math.min(y, cropState.currentBox.y2 - minSize)); break; case 's': cropState.currentBox.y2 = Math.min(cropState.imageHeight, Math.max(y, cropState.currentBox.y1 + minSize)); break; case 'w': cropState.currentBox.x1 = Math.max(0, Math.min(x, cropState.currentBox.x2 - minSize)); break; case 'e': cropState.currentBox.x2 = Math.min(cropState.imageWidth, Math.max(x, cropState.currentBox.x1 + minSize)); break; } } function handleDrag(x, y) { const dx = x - cropState.dragStartX; const dy = y - cropState.dragStartY; const boxWidth = cropState.currentBox.x2 - cropState.currentBox.x1; const boxHeight = cropState.currentBox.y2 - cropState.currentBox.y1; let newX1 = cropState.currentBox.x1 + dx; let newY1 = cropState.currentBox.y1 + dy; // Clamp to image bounds if (newX1 < 0) newX1 = 0; if (newY1 < 0) newY1 = 0; if (newX1 + boxWidth > cropState.imageWidth) newX1 = cropState.imageWidth - boxWidth; if (newY1 + boxHeight > cropState.imageHeight) newY1 = cropState.imageHeight - boxHeight; cropState.currentBox.x1 = newX1; cropState.currentBox.y1 = newY1; cropState.currentBox.x2 = newX1 + boxWidth; cropState.currentBox.y2 = newY1 + boxHeight; cropState.dragStartX = x; cropState.dragStartY = y; } function resetCropBox() { if (!cropState.originalBox || !cropState.scale) return; const canvas = document.getElementById('manualCropCanvas'); if (!canvas) return; // Reset to original box with current scale cropState.currentBox = { x1: cropState.originalBox.x1 * cropState.scale, y1: cropState.originalBox.y1 * cropState.scale, x2: cropState.originalBox.x2 * cropState.scale, y2: cropState.originalBox.y2 * cropState.scale }; drawCropInterface(canvas); } async function applyCropSettings() { if (appState.currentEmbryoIndex === undefined || !cropState.currentBox) return; const embryo = appState.croppedEmbryos[appState.currentEmbryoIndex]; if (!embryo) return; // Load original image const img = await loadImage(appState.currentImage); // Convert canvas coordinates back to image coordinates const scaleX = img.width / cropState.imageWidth; const scaleY = img.height / cropState.imageHeight; const x1 = cropState.currentBox.x1 * scaleX; const y1 = cropState.currentBox.y1 * scaleY; const x2 = cropState.currentBox.x2 * scaleX; const y2 = cropState.currentBox.y2 * scaleY; // Crop the image const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); canvas.width = x2 - x1; canvas.height = y2 - y1; ctx.drawImage(img, x1, y1, x2 - x1, y2 - y1, 0, 0, canvas.width, canvas.height); const croppedImageData = canvas.toDataURL(); // Update the embryo's cropped image embryo.imageData = croppedImageData; // Update the box coordinates embryo.box = { x1: x1, y1: y1, x2: x2, y2: y2 }; // Update display const croppedImage = document.getElementById('croppedImage'); if (croppedImage) { croppedImage.src = croppedImageData; croppedImage.style.display = 'block'; } // Update thumbnail updateThumbnail(appState.currentEmbryoIndex, croppedImageData); } function updateThumbnail(index, imageData) { const thumbnail = document.querySelector(`[data-embryo-index="${index}"] img`); if (thumbnail) { thumbnail.src = imageData; } }