Files
2eInk/src/lib/components/ImagePreview.svelte
patrick 62a218f0cb 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>
2026-05-14 06:14:47 -04:00

144 lines
4.0 KiB
Svelte
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script lang="ts">
import { Loader2 } from 'lucide-svelte';
import type { ImageEntry, Device, PipelineConfig, CropRegion } from '$lib/types';
import { processImageWithPipeline } from '$lib/processing/pipeline';
import CropOverlay from './CropOverlay.svelte';
interface Props {
image: ImageEntry;
device: Device;
config: PipelineConfig;
onPreviewUpdate: (dataUrl: string) => void;
onProcessingChange: (processing: boolean) => void;
onConfigChange?: (config: PipelineConfig) => void;
}
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 (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) {
clearTimeout(debounceTimer);
}
// Debounce processing
debounceTimer = setTimeout(() => {
processPreview();
}, 150);
return () => {
if (debounceTimer) {
clearTimeout(debounceTimer);
}
};
});
async function processPreview(): Promise<void> {
if (!image.file) return;
isProcessing = true;
onProcessingChange(true);
try {
const { dataUrl } = await processImageWithPipeline(image.file, device, config);
previewDataUrl = dataUrl;
onPreviewUpdate(dataUrl);
} catch (err) {
console.error('Preview processing failed:', err);
} finally {
isProcessing = false;
onProcessingChange(false);
}
}
// Initialize with current processed image
$effect(() => {
if (image.processedDataUrl && !previewDataUrl) {
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 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"
class="absolute inset-0 w-full h-full object-contain"
/>
{:else if image.originalDataUrl}
<img
src={image.originalDataUrl}
alt="Original"
class="absolute inset-0 w-full h-full object-contain opacity-50"
/>
{/if}
<!-- Processing overlay -->
{#if isProcessing}
<div class="absolute inset-0 flex items-center justify-center bg-white/70">
<div class="flex flex-col items-center gap-2">
<Loader2 class="w-8 h-8 text-accent animate-spin" />
<span class="text-sm text-grey-500">Processing...</span>
</div>
</div>
{/if}
</div>
<!-- Image info -->
<div class="text-center">
<p class="text-sm font-medium text-ink">{image.filename}</p>
{#if image.originalDimensions}
<p class="text-xs text-grey-400 font-mono mt-1">
{image.originalDimensions.width} × {image.originalDimensions.height}
{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>
</div>