diff --git a/src/lib/contexts/store.svelte.ts b/src/lib/contexts/store.svelte.ts index 3183bf7..8d33952 100644 --- a/src/lib/contexts/store.svelte.ts +++ b/src/lib/contexts/store.svelte.ts @@ -32,6 +32,7 @@ async function idbSet(key: string, value: string): Promise { // ── store ───────────────────────────────────────────────────── class MockupStore { uploadedImage = $state(null); + naturalSize = $state<{ w: number; h: number } | null>(null); backgroundType = $state<'solid'>('solid'); backgroundImage = $state(deepHorizon); aspectRatio = $state('16:9'); @@ -50,8 +51,9 @@ class MockupStore { hydrated = $state(false); // ── actions ────────────────────────────────────────────── - setUploadedImage(v: string | null) { + setUploadedImage(v: string | null, size?: { w: number; h: number }) { this.uploadedImage = v; + this.naturalSize = size ?? null; this.#save(); } setBackgroundType(v: 'solid') { @@ -134,7 +136,15 @@ class MockupStore { if (!raw) return; const s = JSON.parse(raw); - if (s.uploadedImage !== undefined) this.uploadedImage = s.uploadedImage; + if (s.uploadedImage !== undefined) { + this.uploadedImage = s.uploadedImage; + + if (s.uploadedImage) { + const img = new Image(); + img.onload = () => (this.naturalSize = { w: img.naturalWidth, h: img.naturalHeight }); + img.src = s.uploadedImage; + } + } if (s.backgroundType !== undefined) this.backgroundType = s.backgroundType; if (s.backgroundImage !== undefined) this.backgroundImage = s.backgroundImage; if (s.aspectRatio !== undefined) this.aspectRatio = s.aspectRatio; diff --git a/src/lib/editor/Canvas.svelte b/src/lib/editor/Canvas.svelte index 653c980..b378196 100644 --- a/src/lib/editor/Canvas.svelte +++ b/src/lib/editor/Canvas.svelte @@ -13,23 +13,9 @@ let isDragOver = $state(false); let showPasteHint = $state(false); let viewport = $state({ width: 0, height: 0 }); - let naturalSize = $state<{ w: number; h: number } | null>(null); - - $effect(() => { - const src = mockupStore.uploadedImage; - if (!src) { - naturalSize = null; - return; - } - - const img = new Image(); - img.onload = () => { - naturalSize = { w: img.naturalWidth, h: img.naturalHeight }; - }; - img.src = src; - }); const layout = $derived.by(() => { + const naturalSize = mockupStore.naturalSize; if (!naturalSize) { return { width: 400, height: 300, canvasWidth: '100%', canvasHeight: '100%' }; } @@ -139,41 +125,6 @@ return style; }); - $effect(() => { - const onPaste = async (e: ClipboardEvent) => { - const file = Array.from(e.clipboardData?.items ?? []) - .find((i) => i.type.startsWith('image/')) - ?.getAsFile(); - - if (!file) return; - - if (mockupStore.uploadedImage) { - showPasteHint = true; - - toast('Image in clipboard detected!', { - duration: 3000, - action: { - label: 'Clear & Paste', - onClick: () => { - mockupStore.setUploadedImage(null); - localStorage.removeItem('demoImage'); - setTimeout(() => uploadFile(file), 100); - } - } - }); - - setTimeout(() => (showPasteHint = false), 3000); - } else { - await uploadFile(file); - localStorage.removeItem('demoImage'); - toast('Image pasted successfully!'); - } - }; - - document.addEventListener('paste', onPaste); - return () => document.removeEventListener('paste', onPaste); - }); - const hexToRgb = (hex: string) => ({ r: parseInt(hex.slice(1, 3), 16), g: parseInt(hex.slice(3, 5), 16), @@ -226,7 +177,11 @@ file.size > 1024 * 1024 ? toast('Processing image...', { duration: Infinity }) : undefined; const dataUrl = await toDataUrl(file); - mockupStore.setUploadedImage(dataUrl); + + const img = new Image(); + img.onload = () => + mockupStore.setUploadedImage(dataUrl, { w: img.naturalWidth, h: img.naturalHeight }); + img.src = dataUrl; if (loadingToast) toast.dismiss(loadingToast); @@ -260,19 +215,61 @@ } }; + const onPaste = async (e: ClipboardEvent) => { + const file = Array.from(e.clipboardData?.items ?? []) + .find((i) => i.type.startsWith('image/')) + ?.getAsFile(); + + if (!file) return; + + if (mockupStore.uploadedImage) { + showPasteHint = true; + + toast('Image in clipboard detected!', { + duration: 3000, + action: { + label: 'Clear & Paste', + onClick: () => { + mockupStore.setUploadedImage(null); + localStorage.removeItem('demoImage'); + setTimeout(() => uploadFile(file), 100); + } + } + }); + + setTimeout(() => (showPasteHint = false), 3000); + } else { + await uploadFile(file); + localStorage.removeItem('demoImage'); + toast('Image pasted successfully!'); + } + }; + const onDemoImage = async (e: MouseEvent) => { e.stopPropagation(); try { - const blob = await fetch(demoImage).then((r) => r.blob()); - await uploadFile(new File([blob], 'demo.webp', { type: 'image/webp' })); localStorage.setItem('demoImage', demoImage); + + const img = new Image(); + img.onload = () => + mockupStore.setUploadedImage(demoImage, { w: img.naturalWidth, h: img.naturalHeight }); + img.src = demoImage; + + const dominant = await extractDominantColor(demoImage); + applyBorder(/^#[0-9A-Fa-f]{6}$/.test(dominant) ? dominant : undefined); + toast('Image uploaded!'); } catch { - toast.error('Failed to load demo image.'); + applyBorder(); + toast('Image uploaded!'); } }; - +
{ e.preventDefault();