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 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { Loader2 } from 'lucide-svelte';
|
||||
import type { ImageEntry, Device, PipelineConfig } from '$lib/types';
|
||||
import type { ImageEntry, Device, PipelineConfig, CropRegion } from '$lib/types';
|
||||
import { processImageWithPipeline } from '$lib/processing/pipeline';
|
||||
import CropOverlay from './CropOverlay.svelte';
|
||||
|
||||
interface Props {
|
||||
image: ImageEntry;
|
||||
@@ -9,18 +10,27 @@
|
||||
config: PipelineConfig;
|
||||
onPreviewUpdate: (dataUrl: string) => void;
|
||||
onProcessingChange: (processing: boolean) => void;
|
||||
onConfigChange?: (config: PipelineConfig) => void;
|
||||
}
|
||||
|
||||
let { image, device, config, onPreviewUpdate, onProcessingChange }: Props = $props();
|
||||
let { image, device, config, onPreviewUpdate, onProcessingChange, onConfigChange }: Props = $props();
|
||||
|
||||
let previewDataUrl = $state<string | null>(null);
|
||||
let isProcessing = $state(false);
|
||||
let debounceTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
const isManualCrop = $derived(config.crop.enabled && config.crop.mode === 'manual');
|
||||
const targetAspect = $derived(device.width / device.height);
|
||||
|
||||
// Track config changes and trigger debounced processing
|
||||
$effect(() => {
|
||||
// Access config to create dependency
|
||||
const _ = JSON.stringify(config);
|
||||
// Access config to create dependency (but skip processing in manual crop mode without region)
|
||||
const configStr = JSON.stringify(config);
|
||||
|
||||
// Don't process if in manual crop mode - wait for region to be set
|
||||
if (isManualCrop && !config.crop.region) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear existing timer
|
||||
if (debounceTimer) {
|
||||
@@ -63,12 +73,35 @@
|
||||
previewDataUrl = image.processedDataUrl;
|
||||
}
|
||||
});
|
||||
|
||||
function handleCropRegionChange(region: CropRegion): void {
|
||||
if (onConfigChange) {
|
||||
onConfigChange({
|
||||
...config,
|
||||
crop: { ...config.crop, region }
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col items-center gap-4">
|
||||
<!-- Preview container -->
|
||||
<div class="relative w-full max-w-md aspect-[3/4] bg-grey-100 rounded-lg overflow-hidden shadow-inner">
|
||||
{#if previewDataUrl}
|
||||
{#if isManualCrop && image.originalDataUrl && image.originalDimensions}
|
||||
<!-- Show original image with crop overlay in manual mode -->
|
||||
<img
|
||||
src={image.originalDataUrl}
|
||||
alt="Original"
|
||||
class="absolute inset-0 w-full h-full object-contain"
|
||||
/>
|
||||
<CropOverlay
|
||||
imageWidth={image.originalDimensions.width}
|
||||
imageHeight={image.originalDimensions.height}
|
||||
{targetAspect}
|
||||
region={config.crop.region}
|
||||
onRegionChange={handleCropRegionChange}
|
||||
/>
|
||||
{:else if previewDataUrl}
|
||||
<img
|
||||
src={previewDataUrl}
|
||||
alt="Preview"
|
||||
@@ -103,11 +136,8 @@
|
||||
{device.width} × {device.height}
|
||||
</p>
|
||||
{/if}
|
||||
{#if isManualCrop}
|
||||
<p class="text-xs text-accent mt-1">Drag to position, drag corners to resize</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Original vs Preview toggle (future enhancement) -->
|
||||
<!-- <div class="flex items-center gap-2">
|
||||
<button class="text-sm text-grey-500 hover:text-ink">Original</button>
|
||||
<button class="text-sm text-accent font-medium">Preview</button>
|
||||
</div> -->
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user