diff --git a/templates/index.html b/templates/index.html index 37ae070..4dbfbb5 100644 --- a/templates/index.html +++ b/templates/index.html @@ -664,6 +664,12 @@ let rotationDegrees = 0; let muteAudio = false; let compressionQuality = 23; + let isDraggingHandle = false; + let isDraggingBox = false; + let activeHandle = null; + let dragOffsetX = 0; + let dragOffsetY = 0; + const handleSize = 12; // Upload area handling const uploadArea = document.getElementById('upload-area'); @@ -737,7 +743,7 @@

Codec: ${videoInfo.codec}

- +
`; @@ -793,7 +799,7 @@ if (cropMode) { cropCanvas.classList.add('active'); cropModeBtn.classList.add('active'); - cropModeBtn.textContent = '❌ Cancel Crop Selection'; + cropModeBtn.textContent = '✅ Done with Crop'; cropHint.style.display = 'block'; document.getElementById('video-preview').pause(); } else { @@ -802,6 +808,7 @@ cropModeBtn.textContent = '🎯 Click to Draw Crop Area'; cropHint.style.display = 'none'; clearCropCanvas(); + cropRect = null; } }); } @@ -812,71 +819,105 @@ cropCanvas.addEventListener('mousedown', (e) => { if (!cropMode) return; - isDrawing = true; const rect = cropCanvas.getBoundingClientRect(); - startX = e.clientX - rect.left; - startY = e.clientY - rect.top; + const mouseX = e.clientX - rect.left; + const mouseY = e.clientY - rect.top; + + // Check if clicking on a handle + if (cropRect) { + const handle = getHandleAtPosition(mouseX, mouseY); + if (handle) { + isDraggingHandle = true; + activeHandle = handle; + return; + } + + // Check if clicking inside the box to move it + if (isInsideRect(mouseX, mouseY)) { + isDraggingBox = true; + dragOffsetX = mouseX - cropRect.x; + dragOffsetY = mouseY - cropRect.y; + cropCanvas.style.cursor = 'move'; + return; + } + } + + // Start new selection + isDrawing = true; + cropRect = null; + startX = mouseX; + startY = mouseY; }); cropCanvas.addEventListener('mousemove', (e) => { - if (!cropMode || !isDrawing) return; + if (!cropMode) return; const rect = cropCanvas.getBoundingClientRect(); - const currentX = e.clientX - rect.left; - const currentY = e.clientY - rect.top; + const mouseX = e.clientX - rect.left; + const mouseY = e.clientY - rect.top; - drawCropRect(startX, startY, currentX - startX, currentY - startY); + // Update cursor based on position + if (!isDrawing && !isDraggingHandle && !isDraggingBox && cropRect) { + const handle = getHandleAtPosition(mouseX, mouseY); + if (handle) { + cropCanvas.style.cursor = getHandleCursor(handle); + } else if (isInsideRect(mouseX, mouseY)) { + cropCanvas.style.cursor = 'move'; + } else { + cropCanvas.style.cursor = 'crosshair'; + } + } + + // Handle resizing + if (isDraggingHandle && cropRect) { + resizeCropRect(mouseX, mouseY); + updateFormFields(); + return; + } + + // Handle moving + if (isDraggingBox && cropRect) { + moveCropRect(mouseX, mouseY); + updateFormFields(); + return; + } + + // Handle initial drawing + if (isDrawing) { + const width = mouseX - startX; + const height = mouseY - startY; + cropRect = { + x: Math.min(startX, mouseX), + y: Math.min(startY, mouseY), + width: Math.abs(width), + height: Math.abs(height) + }; + drawCropRect(); + } }); cropCanvas.addEventListener('mouseup', (e) => { - if (!cropMode || !isDrawing) return; + if (!cropMode) return; + + if (isDrawing || isDraggingHandle || isDraggingBox) { + updateFormFields(); + } + isDrawing = false; + isDraggingHandle = false; + isDraggingBox = false; + activeHandle = null; - const rect = cropCanvas.getBoundingClientRect(); - const endX = e.clientX - rect.left; - const endY = e.clientY - rect.top; - - const width = Math.abs(endX - startX); - const height = Math.abs(endY - startY); - const x = Math.min(startX, endX); - const y = Math.min(startY, endY); - - // Calculate actual video coordinates - const video = document.getElementById('video-preview'); - const scaleX = videoInfo.width / video.offsetWidth; - const scaleY = videoInfo.height / video.offsetHeight; - - const actualX = Math.round(x * scaleX); - const actualY = Math.round(y * scaleY); - const actualWidth = Math.round(width * scaleX); - const actualHeight = Math.round(height * scaleY); - - // Update form fields - document.getElementById('crop-x').value = actualX; - document.getElementById('crop-y').value = actualY; - document.getElementById('crop-width').value = actualWidth; - document.getElementById('crop-height').value = actualHeight; - - // Exit crop mode - cropMode = false; - const cropModeBtn = document.getElementById('crop-mode-btn'); - const cropHint = document.getElementById('crop-hint'); - if (cropModeBtn) { - cropModeBtn.classList.remove('active'); - cropModeBtn.textContent = '🎯 Click to Draw Crop Area'; - } - if (cropHint) { - cropHint.style.display = 'none'; - } - - // Keep the rectangle visible for a moment - setTimeout(() => { - cropCanvas.classList.remove('active'); - }, 500); + // Keep crop mode active and canvas visible + drawCropRect(); }); - function drawCropRect(x, y, width, height) { + function drawCropRect() { clearCropCanvas(); + if (!cropRect) return; + + const { x, y, width, height } = cropRect; + // Draw semi-transparent overlay cropCtx.fillStyle = 'rgba(0, 0, 0, 0.5)'; cropCtx.fillRect(0, 0, cropCanvas.width, cropCanvas.height); @@ -889,6 +930,12 @@ cropCtx.lineWidth = 3; cropCtx.strokeRect(x, y, width, height); + // Draw resize handles + drawHandle(x, y); // Top-left + drawHandle(x + width, y); // Top-right + drawHandle(x, y + height); // Bottom-left + drawHandle(x + width, y + height); // Bottom-right + // Calculate actual video dimensions for display const video = document.getElementById('video-preview'); const scaleX = videoInfo.width / video.offsetWidth; @@ -900,7 +947,136 @@ cropCtx.fillStyle = '#667eea'; cropCtx.font = 'bold 14px sans-serif'; const text = `${actualWidth} × ${actualHeight}`; - cropCtx.fillText(text, x + 5, y - 5); + const textY = y > 25 ? y - 10 : y + height + 20; + cropCtx.fillText(text, x + 5, textY); + } + + function drawHandle(x, y) { + cropCtx.fillStyle = '#ffffff'; + cropCtx.strokeStyle = '#667eea'; + cropCtx.lineWidth = 2; + const offset = handleSize / 2; + cropCtx.fillRect(x - offset, y - offset, handleSize, handleSize); + cropCtx.strokeRect(x - offset, y - offset, handleSize, handleSize); + } + + function getHandleAtPosition(x, y) { + if (!cropRect) return null; + + const handles = [ + { name: 'tl', x: cropRect.x, y: cropRect.y }, + { name: 'tr', x: cropRect.x + cropRect.width, y: cropRect.y }, + { name: 'bl', x: cropRect.x, y: cropRect.y + cropRect.height }, + { name: 'br', x: cropRect.x + cropRect.width, y: cropRect.y + cropRect.height } + ]; + + for (const handle of handles) { + if (Math.abs(x - handle.x) <= handleSize && Math.abs(y - handle.y) <= handleSize) { + return handle.name; + } + } + return null; + } + + function getHandleCursor(handle) { + const cursors = { + 'tl': 'nw-resize', + 'tr': 'ne-resize', + 'bl': 'sw-resize', + 'br': 'se-resize' + }; + return cursors[handle] || 'default'; + } + + function isInsideRect(x, y) { + if (!cropRect) return false; + return x >= cropRect.x && x <= cropRect.x + cropRect.width && + y >= cropRect.y && y <= cropRect.y + cropRect.height; + } + + function resizeCropRect(mouseX, mouseY) { + const minSize = 20; + + switch (activeHandle) { + case 'tl': + const newWidth = cropRect.x + cropRect.width - mouseX; + const newHeight = cropRect.y + cropRect.height - mouseY; + if (newWidth > minSize) { + cropRect.width = newWidth; + cropRect.x = mouseX; + } + if (newHeight > minSize) { + cropRect.height = newHeight; + cropRect.y = mouseY; + } + break; + case 'tr': + const widthTR = mouseX - cropRect.x; + const heightTR = cropRect.y + cropRect.height - mouseY; + if (widthTR > minSize) cropRect.width = widthTR; + if (heightTR > minSize) { + cropRect.height = heightTR; + cropRect.y = mouseY; + } + break; + case 'bl': + const widthBL = cropRect.x + cropRect.width - mouseX; + const heightBL = mouseY - cropRect.y; + if (widthBL > minSize) { + cropRect.width = widthBL; + cropRect.x = mouseX; + } + if (heightBL > minSize) cropRect.height = heightBL; + break; + case 'br': + const widthBR = mouseX - cropRect.x; + const heightBR = mouseY - cropRect.y; + if (widthBR > minSize) cropRect.width = widthBR; + if (heightBR > minSize) cropRect.height = heightBR; + break; + } + + // Constrain to canvas bounds + cropRect.x = Math.max(0, Math.min(cropRect.x, cropCanvas.width - cropRect.width)); + cropRect.y = Math.max(0, Math.min(cropRect.y, cropCanvas.height - cropRect.height)); + cropRect.width = Math.min(cropRect.width, cropCanvas.width - cropRect.x); + cropRect.height = Math.min(cropRect.height, cropCanvas.height - cropRect.y); + + drawCropRect(); + } + + function moveCropRect(mouseX, mouseY) { + const newX = mouseX - dragOffsetX; + const newY = mouseY - dragOffsetY; + + // Constrain to canvas bounds + cropRect.x = Math.max(0, Math.min(newX, cropCanvas.width - cropRect.width)); + cropRect.y = Math.max(0, Math.min(newY, cropCanvas.height - cropRect.height)); + + drawCropRect(); + } + + function updateFormFields() { + if (!cropRect) return; + + // Calculate actual video coordinates + const video = document.getElementById('video-preview'); + const scaleX = videoInfo.width / video.offsetWidth; + const scaleY = videoInfo.height / video.offsetHeight; + + const actualX = Math.round(cropRect.x * scaleX); + const actualY = Math.round(cropRect.y * scaleY); + const actualWidth = Math.round(cropRect.width * scaleX); + const actualHeight = Math.round(cropRect.height * scaleY); + + // Update form fields + document.getElementById('crop-x').value = actualX; + document.getElementById('crop-y').value = actualY; + document.getElementById('crop-width').value = actualWidth; + document.getElementById('crop-height').value = actualHeight; + + // Update scale display since crop changed + updateScaleDisplay(); } function clearCropCanvas() {