crop QOL
All checks were successful
Build Images and Deploy / Update-PROD-Stack (push) Successful in 1m26s
All checks were successful
Build Images and Deploy / Update-PROD-Stack (push) Successful in 1m26s
This commit is contained in:
@@ -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 @@
|
||||
<p><strong>Codec:</strong> ${videoInfo.codec}</p>
|
||||
<div style="margin-top: 15px; text-align: center;">
|
||||
<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>
|
||||
`;
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user