Compare commits
11 Commits
0d9c00ea29
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| e39153c8fc | |||
| 229f7c4b16 | |||
| c10c9d819c | |||
| 8a5822e556 | |||
| d94b2d6ab8 | |||
| fa287261d0 | |||
| 24b0c07620 | |||
| 640582df33 | |||
| 89e8c88a15 | |||
| 5d221bb51c | |||
| cae62e0697 |
23
app.py
23
app.py
@@ -159,8 +159,8 @@ def process_video():
|
||||
|
||||
# Get quality (CRF value)
|
||||
quality = data.get('quality', 23)
|
||||
# Ensure quality is within valid range (18-32)
|
||||
quality = max(18, min(32, int(quality)))
|
||||
# Ensure quality is within valid range (18-50)
|
||||
quality = max(18, min(50, int(quality)))
|
||||
|
||||
# Output settings for H.264 MP4
|
||||
cmd.extend([
|
||||
@@ -211,6 +211,25 @@ def download_video(file_id):
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@app.route('/preview/<file_id>')
|
||||
def preview_video(file_id):
|
||||
"""Serve processed video for inline preview"""
|
||||
try:
|
||||
output_filename = f"{file_id}_processed.mp4"
|
||||
output_path = os.path.join(app.config['OUTPUT_FOLDER'], output_filename)
|
||||
|
||||
if not os.path.exists(output_path):
|
||||
return jsonify({'error': 'File not found'}), 404
|
||||
|
||||
response = send_file(output_path, mimetype='video/mp4')
|
||||
# Prevent caching
|
||||
response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, max-age=0'
|
||||
response.headers['Pragma'] = 'no-cache'
|
||||
response.headers['Expires'] = '0'
|
||||
return response
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@app.route('/cleanup/<file_id>', methods=['DELETE'])
|
||||
def cleanup_files(file_id):
|
||||
"""Clean up uploaded and processed files"""
|
||||
|
||||
@@ -456,6 +456,8 @@
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
margin-top: 20px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.loader {
|
||||
@@ -543,6 +545,9 @@
|
||||
<!-- Crop Controls -->
|
||||
<div class="form-group">
|
||||
<label>✂️ Crop Video</label>
|
||||
<p style="font-size: 0.9em; color: #6c757d; margin-bottom: 10px;">
|
||||
💡 Draw crop area on video above • Double-click edges or corners to snap
|
||||
</p>
|
||||
<div class="crop-controls">
|
||||
<div class="form-row">
|
||||
<div>
|
||||
@@ -596,16 +601,17 @@
|
||||
<span>Quality: <span class="scale-value" id="quality-value-display">High</span></span>
|
||||
<span style="color: #6c757d;">CRF: <strong id="crf-value-display">23</strong></span>
|
||||
</div>
|
||||
<input type="range" class="scale-slider" id="quality-slider" min="18" max="32" value="23" step="1">
|
||||
<input type="range" class="scale-slider" id="quality-slider" min="18" max="42" value="23" step="1">
|
||||
<div class="resolution-preview">
|
||||
<p style="margin: 0;">💡 <strong>Lower CRF</strong> = Better quality, larger file. <strong>Higher CRF</strong> = More compressed, smaller file.</p>
|
||||
</div>
|
||||
<div class="preset-resolutions" style="margin-top: 15px;">
|
||||
<button class="preset-btn" data-quality="18">Best Quality</button>
|
||||
<button class="preset-btn" data-quality="21">High</button>
|
||||
<button class="preset-btn" data-quality="18">Overkill</button>
|
||||
<button class="preset-btn" data-quality="21">Quality</button>
|
||||
<button class="preset-btn" data-quality="23">Balanced</button>
|
||||
<button class="preset-btn" data-quality="26">Low</button>
|
||||
<button class="preset-btn" data-quality="30">Smallest File</button>
|
||||
<button class="preset-btn" data-quality="26">Compressed</button>
|
||||
<button class="preset-btn" data-quality="30">Small File</button>
|
||||
<button class="preset-btn" data-quality="40">Dog 💩</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -636,8 +642,16 @@
|
||||
<div class="section hidden" id="download-section">
|
||||
<h2>3. Download Processed Video</h2>
|
||||
<p style="margin-bottom: 20px;">Your video has been processed successfully!</p>
|
||||
|
||||
<div class="video-preview">
|
||||
<div class="video-wrapper">
|
||||
<video id="processed-video-preview" controls style="max-width: 100%; max-height: 500px; border-radius: 8px; box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);"></video>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="button-group">
|
||||
<button class="btn" id="download-btn">⬇️ Download Video</button>
|
||||
<button class="btn btn-secondary" id="back-to-edit-btn">✏️ Make More Changes</button>
|
||||
<button class="btn btn-secondary" id="new-video-btn">🎬 Process Another Video</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -655,6 +669,14 @@
|
||||
let rotationDegrees = 0;
|
||||
let muteAudio = false;
|
||||
let compressionQuality = 23;
|
||||
let isDraggingHandle = false;
|
||||
let isDraggingEdge = false;
|
||||
let isDraggingBox = false;
|
||||
let activeHandle = null;
|
||||
let activeEdge = null;
|
||||
let dragOffsetX = 0;
|
||||
let dragOffsetY = 0;
|
||||
const handleSize = 12;
|
||||
|
||||
// Upload area handling
|
||||
const uploadArea = document.getElementById('upload-area');
|
||||
@@ -728,7 +750,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 and edges to resize or double click them to max • Drag center to move</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -737,6 +759,12 @@
|
||||
scalePercentage = 100;
|
||||
scaleSlider.value = 100;
|
||||
updateScaleDisplay();
|
||||
|
||||
// Set max values for crop inputs
|
||||
document.getElementById('crop-x').max = videoInfo.width;
|
||||
document.getElementById('crop-y').max = videoInfo.height;
|
||||
document.getElementById('crop-width').max = videoInfo.width;
|
||||
document.getElementById('crop-height').max = videoInfo.height;
|
||||
|
||||
// Setup canvas for cropping
|
||||
setupCropCanvas();
|
||||
@@ -784,7 +812,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 {
|
||||
@@ -793,6 +821,7 @@
|
||||
cropModeBtn.textContent = '🎯 Click to Draw Crop Area';
|
||||
cropHint.style.display = 'none';
|
||||
clearCropCanvas();
|
||||
cropRect = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -803,71 +832,207 @@
|
||||
|
||||
cropCanvas.addEventListener('mousedown', (e) => {
|
||||
if (!cropMode) return;
|
||||
isDrawing = true;
|
||||
e.preventDefault();
|
||||
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;
|
||||
|
||||
handlePointerDown(mouseX, mouseY);
|
||||
});
|
||||
|
||||
cropCanvas.addEventListener('touchstart', (e) => {
|
||||
if (!cropMode) return;
|
||||
e.preventDefault();
|
||||
const rect = cropCanvas.getBoundingClientRect();
|
||||
const touch = e.touches[0];
|
||||
const mouseX = touch.clientX - rect.left;
|
||||
const mouseY = touch.clientY - rect.top;
|
||||
|
||||
handlePointerDown(mouseX, mouseY);
|
||||
});
|
||||
|
||||
function handlePointerDown(mouseX, mouseY) {
|
||||
// Check if clicking on a handle
|
||||
if (cropRect) {
|
||||
const handle = getHandleAtPosition(mouseX, mouseY);
|
||||
if (handle) {
|
||||
isDraggingHandle = true;
|
||||
activeHandle = handle;
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if clicking on an edge
|
||||
const edge = getEdgeAtPosition(mouseX, mouseY);
|
||||
if (edge) {
|
||||
isDraggingEdge = true;
|
||||
activeEdge = edge;
|
||||
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);
|
||||
handlePointerMove(mouseX, mouseY);
|
||||
});
|
||||
|
||||
cropCanvas.addEventListener('touchmove', (e) => {
|
||||
if (!cropMode) return;
|
||||
e.preventDefault();
|
||||
const rect = cropCanvas.getBoundingClientRect();
|
||||
const touch = e.touches[0];
|
||||
const mouseX = touch.clientX - rect.left;
|
||||
const mouseY = touch.clientY - rect.top;
|
||||
|
||||
handlePointerMove(mouseX, mouseY);
|
||||
});
|
||||
|
||||
function handlePointerMove(mouseX, mouseY) {
|
||||
// Update cursor based on position
|
||||
if (!isDrawing && !isDraggingHandle && !isDraggingEdge && !isDraggingBox && cropRect) {
|
||||
const handle = getHandleAtPosition(mouseX, mouseY);
|
||||
if (handle) {
|
||||
cropCanvas.style.cursor = getHandleCursor(handle);
|
||||
} else {
|
||||
const edge = getEdgeAtPosition(mouseX, mouseY);
|
||||
if (edge) {
|
||||
cropCanvas.style.cursor = getEdgeCursor(edge);
|
||||
} 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 edge resizing
|
||||
if (isDraggingEdge && cropRect) {
|
||||
resizeEdge(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;
|
||||
handlePointerUp();
|
||||
});
|
||||
|
||||
cropCanvas.addEventListener('touchend', (e) => {
|
||||
if (!cropMode) return;
|
||||
e.preventDefault();
|
||||
handlePointerUp();
|
||||
});
|
||||
|
||||
function handlePointerUp() {
|
||||
if (isDrawing || isDraggingHandle || isDraggingEdge || isDraggingBox) {
|
||||
updateFormFields();
|
||||
}
|
||||
|
||||
isDrawing = false;
|
||||
isDraggingHandle = false;
|
||||
isDraggingEdge = false;
|
||||
isDraggingBox = false;
|
||||
activeHandle = null;
|
||||
activeEdge = null;
|
||||
cropCanvas.style.cursor = 'crosshair';
|
||||
|
||||
// Keep crop mode active and canvas visible
|
||||
drawCropRect();
|
||||
}
|
||||
|
||||
// Handle mouse leaving canvas while dragging
|
||||
cropCanvas.addEventListener('mouseleave', (e) => {
|
||||
if (isDrawing || isDraggingHandle || isDraggingEdge || isDraggingBox) {
|
||||
handlePointerUp();
|
||||
}
|
||||
});
|
||||
|
||||
cropCanvas.addEventListener('touchcancel', (e) => {
|
||||
if (cropMode) {
|
||||
handlePointerUp();
|
||||
}
|
||||
});
|
||||
|
||||
// Double-click on handles to snap to edges
|
||||
cropCanvas.addEventListener('dblclick', (e) => {
|
||||
if (!cropMode || !cropRect) return;
|
||||
|
||||
const rect = cropCanvas.getBoundingClientRect();
|
||||
const endX = e.clientX - rect.left;
|
||||
const endY = e.clientY - rect.top;
|
||||
const mouseX = e.clientX - rect.left;
|
||||
const mouseY = 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';
|
||||
// Check for edge click first (between corners)
|
||||
const edge = getEdgeAtPosition(mouseX, mouseY);
|
||||
if (edge) {
|
||||
snapEdgeToSide(edge);
|
||||
updateFormFields();
|
||||
drawCropRect();
|
||||
return;
|
||||
}
|
||||
|
||||
// Keep the rectangle visible for a moment
|
||||
setTimeout(() => {
|
||||
cropCanvas.classList.remove('active');
|
||||
}, 500);
|
||||
// Fall back to corner handling
|
||||
const handle = getHandleAtPosition(mouseX, mouseY);
|
||||
if (handle) {
|
||||
snapHandleToEdge(handle);
|
||||
updateFormFields();
|
||||
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);
|
||||
@@ -880,6 +1045,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;
|
||||
@@ -891,7 +1062,273 @@
|
||||
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;
|
||||
|
||||
// Larger hit area on mobile (touch targets should be at least 44x44px)
|
||||
const hitSize = 'ontouchstart' in window ? 22 : handleSize;
|
||||
|
||||
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) <= hitSize && Math.abs(y - handle.y) <= hitSize) {
|
||||
return handle.name;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function getEdgeAtPosition(x, y) {
|
||||
if (!cropRect) return null;
|
||||
|
||||
// Larger tolerance on mobile for easier touch interaction
|
||||
const tolerance = 'ontouchstart' in window ? 25 : 15;
|
||||
const { x: rx, y: ry, width: rw, height: rh } = cropRect;
|
||||
const hitSize = 'ontouchstart' in window ? 22 : handleSize;
|
||||
|
||||
// Check if within the rectangle bounds (with tolerance)
|
||||
const inHorizontalRange = x >= rx - tolerance && x <= rx + rw + tolerance;
|
||||
const inVerticalRange = y >= ry - tolerance && y <= ry + rh + tolerance;
|
||||
|
||||
// Top edge (exclude corners)
|
||||
if (Math.abs(y - ry) <= tolerance && x > rx + hitSize && x < rx + rw - hitSize) {
|
||||
return 'top';
|
||||
}
|
||||
|
||||
// Bottom edge (exclude corners)
|
||||
if (Math.abs(y - (ry + rh)) <= tolerance && x > rx + hitSize && x < rx + rw - hitSize) {
|
||||
return 'bottom';
|
||||
}
|
||||
|
||||
// Left edge (exclude corners)
|
||||
if (Math.abs(x - rx) <= tolerance && y > ry + hitSize && y < ry + rh - hitSize) {
|
||||
return 'left';
|
||||
}
|
||||
|
||||
// Right edge (exclude corners)
|
||||
if (Math.abs(x - (rx + rw)) <= tolerance && y > ry + hitSize && y < ry + rh - hitSize) {
|
||||
return 'right';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function getHandleCursor(handle) {
|
||||
const cursors = {
|
||||
'tl': 'nw-resize',
|
||||
'tr': 'ne-resize',
|
||||
'bl': 'sw-resize',
|
||||
'br': 'se-resize'
|
||||
};
|
||||
return cursors[handle] || 'default';
|
||||
}
|
||||
|
||||
function getEdgeCursor(edge) {
|
||||
const cursors = {
|
||||
'top': 'ns-resize',
|
||||
'bottom': 'ns-resize',
|
||||
'left': 'ew-resize',
|
||||
'right': 'ew-resize'
|
||||
};
|
||||
return cursors[edge] || '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 resizeEdge(mouseX, mouseY) {
|
||||
const minSize = 20;
|
||||
|
||||
switch (activeEdge) {
|
||||
case 'top':
|
||||
const newHeight = cropRect.y + cropRect.height - mouseY;
|
||||
if (newHeight > minSize && mouseY >= 0) {
|
||||
cropRect.height = newHeight;
|
||||
cropRect.y = mouseY;
|
||||
}
|
||||
break;
|
||||
case 'bottom':
|
||||
const heightBottom = mouseY - cropRect.y;
|
||||
if (heightBottom > minSize && mouseY <= cropCanvas.height) {
|
||||
cropRect.height = heightBottom;
|
||||
}
|
||||
break;
|
||||
case 'left':
|
||||
const newWidth = cropRect.x + cropRect.width - mouseX;
|
||||
if (newWidth > minSize && mouseX >= 0) {
|
||||
cropRect.width = newWidth;
|
||||
cropRect.x = mouseX;
|
||||
}
|
||||
break;
|
||||
case 'right':
|
||||
const widthRight = mouseX - cropRect.x;
|
||||
if (widthRight > minSize && mouseX <= cropCanvas.width) {
|
||||
cropRect.width = widthRight;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Constrain to canvas bounds
|
||||
cropRect.x = Math.max(0, cropRect.x);
|
||||
cropRect.y = Math.max(0, cropRect.y);
|
||||
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 snapHandleToEdge(handle) {
|
||||
if (!cropRect) return;
|
||||
|
||||
switch (handle) {
|
||||
case 'tl': // Top-left -> snap to top-left corner
|
||||
cropRect.width += cropRect.x; // Extend width by current x
|
||||
cropRect.height += cropRect.y; // Extend height by current y
|
||||
cropRect.x = 0;
|
||||
cropRect.y = 0;
|
||||
break;
|
||||
case 'tr': // Top-right -> snap to top-right corner
|
||||
cropRect.width = cropCanvas.width - cropRect.x;
|
||||
cropRect.height += cropRect.y;
|
||||
cropRect.y = 0;
|
||||
break;
|
||||
case 'bl': // Bottom-left -> snap to bottom-left corner
|
||||
cropRect.width += cropRect.x;
|
||||
cropRect.height = cropCanvas.height - cropRect.y;
|
||||
cropRect.x = 0;
|
||||
break;
|
||||
case 'br': // Bottom-right -> snap to bottom-right corner
|
||||
cropRect.width = cropCanvas.width - cropRect.x;
|
||||
cropRect.height = cropCanvas.height - cropRect.y;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function snapEdgeToSide(edge) {
|
||||
if (!cropRect) return;
|
||||
|
||||
switch (edge) {
|
||||
case 'top': // Snap top edge to y=0
|
||||
cropRect.height += cropRect.y; // Extend height by current y
|
||||
cropRect.y = 0;
|
||||
break;
|
||||
case 'bottom': // Snap bottom edge to canvas height
|
||||
cropRect.height = cropCanvas.height - cropRect.y;
|
||||
break;
|
||||
case 'left': // Snap left edge to x=0
|
||||
cropRect.width += cropRect.x; // Extend width by current x
|
||||
cropRect.x = 0;
|
||||
break;
|
||||
case 'right': // Snap right edge to canvas width
|
||||
cropRect.width = cropCanvas.width - cropRect.x;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
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() {
|
||||
@@ -986,11 +1423,12 @@
|
||||
|
||||
// Update quality label
|
||||
let qualityLabel = 'Balanced';
|
||||
if (compressionQuality <= 19) qualityLabel = 'Best Quality';
|
||||
else if (compressionQuality <= 22) qualityLabel = 'High';
|
||||
if (compressionQuality <= 19) qualityLabel = 'Overkill';
|
||||
else if (compressionQuality <= 22) qualityLabel = 'Quality';
|
||||
else if (compressionQuality <= 25) qualityLabel = 'Balanced';
|
||||
else if (compressionQuality <= 28) qualityLabel = 'Low';
|
||||
else qualityLabel = 'Smallest File';
|
||||
else if (compressionQuality <= 28) qualityLabel = 'Compressed';
|
||||
else if (compressionQuality <= 32) qualityLabel = 'Small File';
|
||||
else qualityLabel = 'Dog 💩';
|
||||
|
||||
qualityValueDisplay.textContent = qualityLabel;
|
||||
}
|
||||
@@ -1067,6 +1505,10 @@
|
||||
throw new Error(data.error || 'Processing failed');
|
||||
}
|
||||
|
||||
// Load processed video preview with cache-busting
|
||||
const processedVideoPreview = document.getElementById('processed-video-preview');
|
||||
processedVideoPreview.src = `/preview/${currentFileId}?t=${Date.now()}`;
|
||||
|
||||
// Show download section
|
||||
document.getElementById('edit-section').classList.add('hidden');
|
||||
document.getElementById('download-section').classList.remove('hidden');
|
||||
@@ -1085,9 +1527,22 @@
|
||||
window.location.href = `/download/${currentFileId}`;
|
||||
});
|
||||
|
||||
// Back to edit button
|
||||
document.getElementById('back-to-edit-btn').addEventListener('click', backToEdit);
|
||||
|
||||
// Reset buttons
|
||||
document.getElementById('reset-btn').addEventListener('click', resetApp);
|
||||
document.getElementById('new-video-btn').addEventListener('click', resetApp);
|
||||
|
||||
function backToEdit() {
|
||||
// Go back to edit section without resetting values
|
||||
document.getElementById('download-section').classList.add('hidden');
|
||||
document.getElementById('edit-section').classList.remove('hidden');
|
||||
|
||||
// Clear the processed video preview to free memory
|
||||
const processedVideoPreview = document.getElementById('processed-video-preview');
|
||||
processedVideoPreview.src = '';
|
||||
}
|
||||
|
||||
async function resetApp() {
|
||||
if (currentFileId) {
|
||||
|
||||
Reference in New Issue
Block a user