From 62a218f0cb70b5fb76c12acd04bdadb2f1ee14fd Mon Sep 17 00:00:00 2001 From: patrick Date: Thu, 14 May 2026 06:14:47 -0400 Subject: [PATCH] feat: add manual crop mode - CropOverlay component with draggable/resizable region - Maintains target device aspect ratio - Rule of thirds grid overlay - Dark mask outside crop area - Updates ImagePreview to show overlay in manual mode - Updates photonWorker to handle manual crop regions - Also fixes top/bottom crop modes Co-Authored-By: Claude Opus 4.5 --- src/lib/components/CropOverlay.svelte | 244 +++++++++++++++++++++++ src/lib/components/EditImageModal.svelte | 1 + src/lib/components/ImagePreview.svelte | 52 ++++- src/lib/components/PipelinePanel.svelte | 2 +- src/lib/processing/photonWorker.ts | 51 +++-- 5 files changed, 324 insertions(+), 26 deletions(-) create mode 100644 src/lib/components/CropOverlay.svelte diff --git a/src/lib/components/CropOverlay.svelte b/src/lib/components/CropOverlay.svelte new file mode 100644 index 0000000..d06f4e2 --- /dev/null +++ b/src/lib/components/CropOverlay.svelte @@ -0,0 +1,244 @@ + + +
+ +
+ +
+ +
+ +
+ +
+
+ + + +
+ +
+
+
+
+
+
+ + + + +
handleResizeStart(e, 'nw')} + ontouchstart={(e) => handleResizeStart(e, 'nw')} + >
+ +
handleResizeStart(e, 'ne')} + ontouchstart={(e) => handleResizeStart(e, 'ne')} + >
+ +
handleResizeStart(e, 'sw')} + ontouchstart={(e) => handleResizeStart(e, 'sw')} + >
+ +
handleResizeStart(e, 'se')} + ontouchstart={(e) => handleResizeStart(e, 'se')} + >
+
+
diff --git a/src/lib/components/EditImageModal.svelte b/src/lib/components/EditImageModal.svelte index 0240ba6..8b8e4d0 100644 --- a/src/lib/components/EditImageModal.svelte +++ b/src/lib/components/EditImageModal.svelte @@ -167,6 +167,7 @@ config={localConfig} onPreviewUpdate={handlePreviewUpdate} onProcessingChange={handleProcessingChange} + onConfigChange={handleConfigChange} /> diff --git a/src/lib/components/ImagePreview.svelte b/src/lib/components/ImagePreview.svelte index d02dd61..6e18725 100644 --- a/src/lib/components/ImagePreview.svelte +++ b/src/lib/components/ImagePreview.svelte @@ -1,7 +1,8 @@
- {#if previewDataUrl} + {#if isManualCrop && image.originalDataUrl && image.originalDimensions} + + Original + + {:else if previewDataUrl} Preview {/if} + {#if isManualCrop} +

Drag to position, drag corners to resize

+ {/if}
- - -
diff --git a/src/lib/components/PipelinePanel.svelte b/src/lib/components/PipelinePanel.svelte index 07bcdec..161f753 100644 --- a/src/lib/components/PipelinePanel.svelte +++ b/src/lib/components/PipelinePanel.svelte @@ -47,7 +47,7 @@ { value: 'center', label: 'Center' }, { value: 'top', label: 'Top' }, { value: 'bottom', label: 'Bottom' }, - { value: 'manual', label: 'Manual (coming soon)' } + { value: 'manual', label: 'Manual' } ]; const resizeModes: { value: ResizeMode; label: string }[] = [ diff --git a/src/lib/processing/photonWorker.ts b/src/lib/processing/photonWorker.ts index a4231e9..6f1470d 100644 --- a/src/lib/processing/photonWorker.ts +++ b/src/lib/processing/photonWorker.ts @@ -199,24 +199,47 @@ async function processImage( const width = img.get_width(); const height = img.get_height(); const targetAspect = device.width / device.height; - const currentAspect = width / height; let cropX1: number, cropY1: number, cropX2: number, cropY2: number; - if (currentAspect > targetAspect) { - const newWidth = Math.max(1, Math.floor(height * targetAspect)); - const xOffset = Math.floor((width - newWidth) / 2); - cropX1 = Math.max(0, xOffset); - cropY1 = 0; - cropX2 = Math.min(width, cropX1 + newWidth); - cropY2 = height; + if (config.crop.mode === 'manual' && config.crop.region) { + // Manual crop: use the provided region + const region = config.crop.region; + cropX1 = Math.max(0, Math.round(region.x)); + cropY1 = Math.max(0, Math.round(region.y)); + cropX2 = Math.min(width, Math.round(region.x + region.width)); + cropY2 = Math.min(height, Math.round(region.y + region.height)); } else { - const newHeight = Math.max(1, Math.floor(width / targetAspect)); - const yOffset = Math.floor((height - newHeight) / 2); - cropX1 = 0; - cropY1 = Math.max(0, yOffset); - cropX2 = width; - cropY2 = Math.min(height, cropY1 + newHeight); + // Auto crop modes: center, top, bottom + const currentAspect = width / height; + + if (currentAspect > targetAspect) { + const newWidth = Math.max(1, Math.floor(height * targetAspect)); + let xOffset: number; + if (config.crop.mode === 'top' || config.crop.mode === 'bottom') { + xOffset = Math.floor((width - newWidth) / 2); + } else { + xOffset = Math.floor((width - newWidth) / 2); + } + cropX1 = Math.max(0, xOffset); + cropY1 = 0; + cropX2 = Math.min(width, cropX1 + newWidth); + cropY2 = height; + } else { + const newHeight = Math.max(1, Math.floor(width / targetAspect)); + let yOffset: number; + if (config.crop.mode === 'top') { + yOffset = 0; + } else if (config.crop.mode === 'bottom') { + yOffset = height - newHeight; + } else { + yOffset = Math.floor((height - newHeight) / 2); + } + cropX1 = 0; + cropY1 = Math.max(0, yOffset); + cropX2 = width; + cropY2 = Math.min(height, cropY1 + newHeight); + } } if (cropX2 > cropX1 && cropY2 > cropY1) {