Embryo-One's picture
Upload 49 files
ed9f15f verified
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;
}
}