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 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() {
|
||||||
|
|||||||
Reference in New Issue
Block a user