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)
|
# Get quality (CRF value)
|
||||||
quality = data.get('quality', 23)
|
quality = data.get('quality', 23)
|
||||||
# Ensure quality is within valid range (18-32)
|
# Ensure quality is within valid range (18-50)
|
||||||
quality = max(18, min(32, int(quality)))
|
quality = max(18, min(50, int(quality)))
|
||||||
|
|
||||||
# Output settings for H.264 MP4
|
# Output settings for H.264 MP4
|
||||||
cmd.extend([
|
cmd.extend([
|
||||||
@@ -211,6 +211,25 @@ def download_video(file_id):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({'error': str(e)}), 500
|
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'])
|
@app.route('/cleanup/<file_id>', methods=['DELETE'])
|
||||||
def cleanup_files(file_id):
|
def cleanup_files(file_id):
|
||||||
"""Clean up uploaded and processed files"""
|
"""Clean up uploaded and processed files"""
|
||||||
|
|||||||
@@ -456,6 +456,8 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
gap: 15px;
|
gap: 15px;
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.loader {
|
.loader {
|
||||||
@@ -543,6 +545,9 @@
|
|||||||
<!-- Crop Controls -->
|
<!-- Crop Controls -->
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>✂️ Crop Video</label>
|
<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="crop-controls">
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<div>
|
<div>
|
||||||
@@ -596,16 +601,17 @@
|
|||||||
<span>Quality: <span class="scale-value" id="quality-value-display">High</span></span>
|
<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>
|
<span style="color: #6c757d;">CRF: <strong id="crf-value-display">23</strong></span>
|
||||||
</div>
|
</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">
|
<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>
|
<p style="margin: 0;">💡 <strong>Lower CRF</strong> = Better quality, larger file. <strong>Higher CRF</strong> = More compressed, smaller file.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="preset-resolutions" style="margin-top: 15px;">
|
<div class="preset-resolutions" style="margin-top: 15px;">
|
||||||
<button class="preset-btn" data-quality="18">Best Quality</button>
|
<button class="preset-btn" data-quality="18">Overkill</button>
|
||||||
<button class="preset-btn" data-quality="21">High</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="23">Balanced</button>
|
||||||
<button class="preset-btn" data-quality="26">Low</button>
|
<button class="preset-btn" data-quality="26">Compressed</button>
|
||||||
<button class="preset-btn" data-quality="30">Smallest File</button>
|
<button class="preset-btn" data-quality="30">Small File</button>
|
||||||
|
<button class="preset-btn" data-quality="40">Dog 💩</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -636,8 +642,16 @@
|
|||||||
<div class="section hidden" id="download-section">
|
<div class="section hidden" id="download-section">
|
||||||
<h2>3. Download Processed Video</h2>
|
<h2>3. Download Processed Video</h2>
|
||||||
<p style="margin-bottom: 20px;">Your video has been processed successfully!</p>
|
<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">
|
<div class="button-group">
|
||||||
<button class="btn" id="download-btn">⬇️ Download Video</button>
|
<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>
|
<button class="btn btn-secondary" id="new-video-btn">🎬 Process Another Video</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -655,6 +669,14 @@
|
|||||||
let rotationDegrees = 0;
|
let rotationDegrees = 0;
|
||||||
let muteAudio = false;
|
let muteAudio = false;
|
||||||
let compressionQuality = 23;
|
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
|
// Upload area handling
|
||||||
const uploadArea = document.getElementById('upload-area');
|
const uploadArea = document.getElementById('upload-area');
|
||||||
@@ -728,7 +750,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 and edges to resize or double click them to max • Drag center to move</p>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -737,6 +759,12 @@
|
|||||||
scalePercentage = 100;
|
scalePercentage = 100;
|
||||||
scaleSlider.value = 100;
|
scaleSlider.value = 100;
|
||||||
updateScaleDisplay();
|
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
|
// Setup canvas for cropping
|
||||||
setupCropCanvas();
|
setupCropCanvas();
|
||||||
@@ -784,7 +812,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 {
|
||||||
@@ -793,6 +821,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;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -803,71 +832,207 @@
|
|||||||
|
|
||||||
cropCanvas.addEventListener('mousedown', (e) => {
|
cropCanvas.addEventListener('mousedown', (e) => {
|
||||||
if (!cropMode) return;
|
if (!cropMode) return;
|
||||||
isDrawing = true;
|
e.preventDefault();
|
||||||
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;
|
||||||
|
|
||||||
|
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) => {
|
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);
|
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) => {
|
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;
|
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 rect = cropCanvas.getBoundingClientRect();
|
||||||
const endX = e.clientX - rect.left;
|
const mouseX = e.clientX - rect.left;
|
||||||
const endY = e.clientY - rect.top;
|
const mouseY = e.clientY - rect.top;
|
||||||
|
|
||||||
const width = Math.abs(endX - startX);
|
// Check for edge click first (between corners)
|
||||||
const height = Math.abs(endY - startY);
|
const edge = getEdgeAtPosition(mouseX, mouseY);
|
||||||
const x = Math.min(startX, endX);
|
if (edge) {
|
||||||
const y = Math.min(startY, endY);
|
snapEdgeToSide(edge);
|
||||||
|
updateFormFields();
|
||||||
// Calculate actual video coordinates
|
drawCropRect();
|
||||||
const video = document.getElementById('video-preview');
|
return;
|
||||||
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
|
// Fall back to corner handling
|
||||||
setTimeout(() => {
|
const handle = getHandleAtPosition(mouseX, mouseY);
|
||||||
cropCanvas.classList.remove('active');
|
if (handle) {
|
||||||
}, 500);
|
snapHandleToEdge(handle);
|
||||||
|
updateFormFields();
|
||||||
|
drawCropRect();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
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);
|
||||||
@@ -880,6 +1045,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;
|
||||||
@@ -891,7 +1062,273 @@
|
|||||||
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;
|
||||||
|
|
||||||
|
// 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() {
|
function clearCropCanvas() {
|
||||||
@@ -986,11 +1423,12 @@
|
|||||||
|
|
||||||
// Update quality label
|
// Update quality label
|
||||||
let qualityLabel = 'Balanced';
|
let qualityLabel = 'Balanced';
|
||||||
if (compressionQuality <= 19) qualityLabel = 'Best Quality';
|
if (compressionQuality <= 19) qualityLabel = 'Overkill';
|
||||||
else if (compressionQuality <= 22) qualityLabel = 'High';
|
else if (compressionQuality <= 22) qualityLabel = 'Quality';
|
||||||
else if (compressionQuality <= 25) qualityLabel = 'Balanced';
|
else if (compressionQuality <= 25) qualityLabel = 'Balanced';
|
||||||
else if (compressionQuality <= 28) qualityLabel = 'Low';
|
else if (compressionQuality <= 28) qualityLabel = 'Compressed';
|
||||||
else qualityLabel = 'Smallest File';
|
else if (compressionQuality <= 32) qualityLabel = 'Small File';
|
||||||
|
else qualityLabel = 'Dog 💩';
|
||||||
|
|
||||||
qualityValueDisplay.textContent = qualityLabel;
|
qualityValueDisplay.textContent = qualityLabel;
|
||||||
}
|
}
|
||||||
@@ -1067,6 +1505,10 @@
|
|||||||
throw new Error(data.error || 'Processing failed');
|
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
|
// Show download section
|
||||||
document.getElementById('edit-section').classList.add('hidden');
|
document.getElementById('edit-section').classList.add('hidden');
|
||||||
document.getElementById('download-section').classList.remove('hidden');
|
document.getElementById('download-section').classList.remove('hidden');
|
||||||
@@ -1085,9 +1527,22 @@
|
|||||||
window.location.href = `/download/${currentFileId}`;
|
window.location.href = `/download/${currentFileId}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Back to edit button
|
||||||
|
document.getElementById('back-to-edit-btn').addEventListener('click', backToEdit);
|
||||||
|
|
||||||
// Reset buttons
|
// Reset buttons
|
||||||
document.getElementById('reset-btn').addEventListener('click', resetApp);
|
document.getElementById('reset-btn').addEventListener('click', resetApp);
|
||||||
document.getElementById('new-video-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() {
|
async function resetApp() {
|
||||||
if (currentFileId) {
|
if (currentFileId) {
|
||||||
|
|||||||
Reference in New Issue
Block a user