crop QOL
All checks were successful
Build Images and Deploy / Update-PROD-Stack (push) Successful in 1m26s

This commit is contained in:
2026-02-09 21:59:57 -05:00
parent 640582df33
commit 24b0c07620

View File

@@ -664,6 +664,12 @@
let rotationDegrees = 0; let rotationDegrees = 0;
let muteAudio = false; let muteAudio = false;
let compressionQuality = 23; let compressionQuality = 23;
let isDraggingHandle = false;
let isDraggingBox = false;
let activeHandle = null;
let dragOffsetX = 0;
let dragOffsetY = 0;
const handleSize = 12;
// Upload area handling // Upload area handling
const uploadArea = document.getElementById('upload-area'); const uploadArea = document.getElementById('upload-area');
@@ -737,7 +743,7 @@
<p><strong>Codec:</strong> ${videoInfo.codec}</p> <p><strong>Codec:</strong> ${videoInfo.codec}</p>
<div style="margin-top: 15px; text-align: center;"> <div style="margin-top: 15px; text-align: center;">
<button class="crop-mode-btn" id="crop-mode-btn">🎯 Click to Draw Crop Area</button> <button class="crop-mode-btn" id="crop-mode-btn">🎯 Click to Draw Crop Area</button>
<p class="crop-hint" id="crop-hint" style="display: none;">Click and drag on the video to select crop area</p> <p class="crop-hint" id="crop-hint" style="display: none;">Click and drag to select • Drag corners to resize • Drag center to move</p>
</div> </div>
`; `;
@@ -793,7 +799,7 @@
if (cropMode) { if (cropMode) {
cropCanvas.classList.add('active'); cropCanvas.classList.add('active');
cropModeBtn.classList.add('active'); cropModeBtn.classList.add('active');
cropModeBtn.textContent = '❌ Cancel Crop Selection'; cropModeBtn.textContent = '✅ Done with Crop';
cropHint.style.display = 'block'; cropHint.style.display = 'block';
document.getElementById('video-preview').pause(); document.getElementById('video-preview').pause();
} else { } else {
@@ -802,6 +808,7 @@
cropModeBtn.textContent = '🎯 Click to Draw Crop Area'; cropModeBtn.textContent = '🎯 Click to Draw Crop Area';
cropHint.style.display = 'none'; cropHint.style.display = 'none';
clearCropCanvas(); clearCropCanvas();
cropRect = null;
} }
}); });
} }
@@ -812,71 +819,105 @@
cropCanvas.addEventListener('mousedown', (e) => { cropCanvas.addEventListener('mousedown', (e) => {
if (!cropMode) return; if (!cropMode) return;
isDrawing = true;
const rect = cropCanvas.getBoundingClientRect(); const rect = cropCanvas.getBoundingClientRect();
startX = e.clientX - rect.left; const mouseX = e.clientX - rect.left;
startY = e.clientY - rect.top; 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) => { cropCanvas.addEventListener('mousemove', (e) => {
if (!cropMode || !isDrawing) return; if (!cropMode) return;
const rect = cropCanvas.getBoundingClientRect(); const rect = cropCanvas.getBoundingClientRect();
const currentX = e.clientX - rect.left; const mouseX = e.clientX - rect.left;
const currentY = e.clientY - rect.top; 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) => { cropCanvas.addEventListener('mouseup', (e) => {
if (!cropMode || !isDrawing) return; if (!cropMode) return;
if (isDrawing || isDraggingHandle || isDraggingBox) {
updateFormFields();
}
isDrawing = false; isDrawing = false;
isDraggingHandle = false;
isDraggingBox = false;
activeHandle = null;
const rect = cropCanvas.getBoundingClientRect(); // Keep crop mode active and canvas visible
const endX = e.clientX - rect.left; drawCropRect();
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);
}); });
function drawCropRect(x, y, width, height) { function drawCropRect() {
clearCropCanvas(); clearCropCanvas();
if (!cropRect) return;
const { x, y, width, height } = cropRect;
// Draw semi-transparent overlay // Draw semi-transparent overlay
cropCtx.fillStyle = 'rgba(0, 0, 0, 0.5)'; cropCtx.fillStyle = 'rgba(0, 0, 0, 0.5)';
cropCtx.fillRect(0, 0, cropCanvas.width, cropCanvas.height); cropCtx.fillRect(0, 0, cropCanvas.width, cropCanvas.height);
@@ -889,6 +930,12 @@
cropCtx.lineWidth = 3; cropCtx.lineWidth = 3;
cropCtx.strokeRect(x, y, width, height); 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 // Calculate actual video dimensions for display
const video = document.getElementById('video-preview'); const video = document.getElementById('video-preview');
const scaleX = videoInfo.width / video.offsetWidth; const scaleX = videoInfo.width / video.offsetWidth;
@@ -900,7 +947,136 @@
cropCtx.fillStyle = '#667eea'; cropCtx.fillStyle = '#667eea';
cropCtx.font = 'bold 14px sans-serif'; cropCtx.font = 'bold 14px sans-serif';
const text = `${actualWidth} × ${actualHeight}`; 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() { function clearCropCanvas() {