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}
-
Click and drag on the video to select crop area
+
Click and drag to select • Drag corners to resize • Drag center to move
`;
@@ -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() {