diff --git a/src/lib/contexts/store.svelte.ts b/src/lib/contexts/store.svelte.ts index 339360a..8495c75 100644 --- a/src/lib/contexts/store.svelte.ts +++ b/src/lib/contexts/store.svelte.ts @@ -1,4 +1,3 @@ -// lib/stores/mockup.svelte.ts import deepHorizon from '$lib/assets/deep_horizon.webp'; const STORE_KEY = 'mockup-store'; @@ -33,7 +32,6 @@ async function idbSet(key: string, value: string): Promise { // ── store ───────────────────────────────────────────────────── class MockupStore { uploadedImage = $state(null); - backgroundColor = $state('#FF6B6B'); backgroundType = $state<'solid'>('solid'); backgroundImage = $state(deepHorizon); aspectRatio = $state('16:9'); @@ -56,10 +54,6 @@ class MockupStore { this.uploadedImage = v; this.#save(); } - setBackgroundColor(v: string) { - this.backgroundColor = v; - this.#save(); - } setBackgroundType(v: 'solid') { this.backgroundType = v; this.#save(); @@ -113,7 +107,6 @@ class MockupStore { STORE_KEY, JSON.stringify({ uploadedImage: this.uploadedImage, - backgroundColor: this.backgroundColor, backgroundType: this.backgroundType, backgroundImage: this.backgroundImage, aspectRatio: this.aspectRatio, @@ -137,7 +130,6 @@ class MockupStore { const s = JSON.parse(raw); if (s.uploadedImage !== undefined) this.uploadedImage = s.uploadedImage; - if (s.backgroundColor !== undefined) this.backgroundColor = s.backgroundColor; 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/Backgrounds.svelte b/src/lib/editor/Backgrounds.svelte index 67f8341..f309c2f 100644 --- a/src/lib/editor/Backgrounds.svelte +++ b/src/lib/editor/Backgrounds.svelte @@ -27,7 +27,6 @@ interface GradientPreset { name: string; - colors: string[]; image: string; } @@ -44,23 +43,23 @@ const MAX_FILE_SIZE = 10 * 1024 * 1024; const gradientPresets: GradientPreset[] = [ - { name: 'Deep Horizon', colors: ['#141e30', '#243b55'], image: deepHorizon }, - { name: 'Ocean Glow', colors: ['#56ccf2', '#2f80ed'], image: oceanGlow }, - { name: 'Ocean Breeze', colors: ['#ff9a9e', '#fecfef'], image: oceanBreeze }, - { name: 'Purple Haze', colors: ['#c471ed', '#f64f59'], image: purpleHaze }, - { name: 'Summer Vibes', colors: ['#56ab2f', '#a8e6cf'], image: summerVibes }, - { name: 'Rainbow Dreams', colors: ['#ff6b6b', '#4ecdc4'], image: rainbowDreams }, - { name: 'Neon Heat', colors: ['#ff0844', '#ffb199'], image: neonHeat }, - { name: 'Purple Magic', colors: ['#667eea', '#764ba2'], image: purpleMagic }, - { name: 'Sunset Glow', colors: ['#ff9a56', '#ff6b9d'], image: sunsetGlow }, - { name: 'Warm Embrace', colors: ['#ff9472', '#f2d388'], image: warmEmbrace }, - { name: 'Cosmic Night', colors: ['#667eea', '#764ba2'], image: cosmicNight }, - { name: 'Mint Breeze', colors: ['#a8edea', '#fed6e3'], image: mintBreeze }, - { name: 'Neon Midnight', colors: ['#c471ed', '#f64f59'], image: neonMidnight }, - { name: 'Monochrome', colors: ['#2c3e50', '#34495e'], image: monochrome }, - { name: 'Arctic Pulse', colors: ['#cce3df', '#3a6c7a', '#0e1a1f'], image: arcticPulse }, - { name: 'Molten Dusk', colors: ['#f0e7da', '#f857a6', '#2c2c2c'], image: moltenDusk }, - { name: 'Twilight Ember', colors: ['#ffb88c', '#ea5753', '#111d2f'], image: twilightEmber } + { name: 'Deep Horizon', image: deepHorizon }, + { name: 'Ocean Glow', image: oceanGlow }, + { name: 'Ocean Breeze', image: oceanBreeze }, + { name: 'Purple Haze', image: purpleHaze }, + { name: 'Summer Vibes', image: summerVibes }, + { name: 'Rainbow Dreams', image: rainbowDreams }, + { name: 'Neon Heat', image: neonHeat }, + { name: 'Purple Magic', image: purpleMagic }, + { name: 'Sunset Glow', image: sunsetGlow }, + { name: 'Warm Embrace', image: warmEmbrace }, + { name: 'Cosmic Night', image: cosmicNight }, + { name: 'Mint Breeze', image: mintBreeze }, + { name: 'Neon Midnight', image: neonMidnight }, + { name: 'Monochrome', image: monochrome }, + { name: 'Arctic Pulse', image: arcticPulse }, + { name: 'Molten Dusk', image: moltenDusk }, + { name: 'Twilight Ember', image: twilightEmber } ]; const handleCustomBackground = (gradient: GradientPreset | CustomBackground) => { diff --git a/src/lib/editor/Canvas.svelte b/src/lib/editor/Canvas.svelte index 8f0b28a..fcf8e1c 100644 --- a/src/lib/editor/Canvas.svelte +++ b/src/lib/editor/Canvas.svelte @@ -5,19 +5,22 @@ import ClipboardIcon from 'phosphor-svelte/lib/ClipboardIcon'; import demoImage from '$lib/assets/demo.webp'; import { mockupStore } from '$lib/contexts/store.svelte'; - - // ─── State ─────────────────────────────────────────────────────────────────── + import Button from '$lib/components/Button.svelte'; let fileInputRef = $state(null); let isDragOver = $state(false); let showPasteHint = $state(false); - let naturalSize = $state<{ w: number; h: number } | null>(null); + + /* ---------------- viewport ---------------- */ + let viewport = $state({ - width: window.innerWidth, - height: window.innerHeight + width: 0, + height: 0 }); - // ─── Image natural size (single $effect) ──────────────────────────────────── + /* ---------------- image natural size ---------------- */ + + let naturalSize = $state<{ w: number; h: number } | null>(null); $effect(() => { const src = mockupStore.uploadedImage; @@ -25,71 +28,157 @@ naturalSize = null; return; } + const img = new Image(); img.onload = () => { - naturalSize = { w: img.naturalWidth, h: img.naturalHeight }; + naturalSize = { + w: img.naturalWidth, + h: img.naturalHeight + }; }; img.src = src; }); - // ─── Derived: scaled dimensions ────────────────────────────────────────────── + /* ---------------- layout (ALL responsive logic) ---------------- */ + + const layout = $derived.by(() => { + if (!naturalSize) { + return { + width: 400, + height: 300, + canvasWidth: '100%', + canvasHeight: '100%' + }; + } + + const vw = viewport.width; + const vh = viewport.height; + + const isMobile = vw < 768; + const isTablet = vw >= 768 && vw < 1024; + + const basePadding = isMobile ? 40 : isTablet ? 100 : 200; + const scaleMultiplier = isMobile ? 0.95 : isTablet ? 0.85 : 0.8; + + const maxWidth = isMobile ? 1000 : isTablet ? 1100 : 1200; + const maxHeight = isMobile ? 700 : isTablet ? 750 : 800; + + const availableWidth = vw - basePadding; + const availableHeight = vh - basePadding; + + const containerMaxW = Math.min(availableWidth * scaleMultiplier, maxWidth); + const containerMaxH = Math.min(availableHeight * scaleMultiplier, maxHeight); + + const scale = Math.min(containerMaxW / naturalSize.w, containerMaxH / naturalSize.h, 1); - const scaledDims = $derived.by(() => { - if (!naturalSize) return { width: 400, height: 300 }; + const width = Math.round(naturalSize.w * scale); + const height = Math.round(naturalSize.h * scale); - const maxWidth = Math.min(viewport.width * 0.8, 1200); - const maxHeight = Math.min(viewport.height * 0.8, 800); + let canvasWidth: string | number = '100%'; + let canvasHeight: string | number = '100%'; - const scale = Math.min(maxWidth / naturalSize.w, maxHeight / naturalSize.h, 1); + if (mockupStore.fixedMargin && mockupStore.uploadedImage) { + canvasWidth = width + mockupStore.margin.left + mockupStore.margin.right + 'px'; + + canvasHeight = height + mockupStore.margin.top + mockupStore.margin.bottom + 'px'; + } return { - width: Math.round(naturalSize.w * scale), - height: Math.round(naturalSize.h * scale) + width, + height, + canvasWidth, + canvasHeight }; }); - // ─── Derived: inline styles ────────────────────────────────────────────────── - - const canvasStyle = $derived( - mockupStore.fixedMargin && mockupStore.uploadedImage - ? `width:${scaledDims.width + mockupStore.margin.left + mockupStore.margin.right}px;height:${scaledDims.height + mockupStore.margin.top + mockupStore.margin.bottom}px` - : 'width:100%;height:100%' - ); + /* ---------------- styles ---------------- */ const backgroundStyle = $derived.by(() => { const s = mockupStore; - return `background-image:url(${s.backgroundImage});background-size:cover;background-position:center;background-repeat:no-repeat;width:100%;height:100%`; + + if (s.backgroundImage) { + return ` + background-image:url(${s.backgroundImage}); + background-size:cover; + background-position:center; + background-repeat:no-repeat; + width:100%; + height:100%; + `; + } }); - const imageContainerStyle = $derived( - mockupStore.fixedMargin && mockupStore.uploadedImage - ? `position:absolute;top:${mockupStore.margin.top}px;right:${mockupStore.margin.right}px;bottom:${mockupStore.margin.bottom}px;left:${mockupStore.margin.left}px;display:flex;align-items:center;justify-content:center` - : 'display:flex;align-items:center;justify-content:center;width:100%;height:100%' - ); + const imageContainerStyle = $derived.by(() => { + if (mockupStore.fixedMargin && mockupStore.uploadedImage) { + return ` + position:absolute; + top:${mockupStore.margin.top}px; + right:${mockupStore.margin.right}px; + bottom:${mockupStore.margin.bottom}px; + left:${mockupStore.margin.left}px; + display:flex; + align-items:center; + justify-content:center; + `; + } + + return ` + display:flex; + align-items:center; + justify-content:center; + width:100%; + height:100%; + `; + }); const imageStyle = $derived.by(() => { const { x, y, scale, rotation } = mockupStore.devicePosition; const { rotateX, rotateY, rotateZ, skew } = mockupStore.rotation3D; - const { width, height } = scaledDims; - const transform = `translate(${x}px,${y}px) scale(${scale}) rotate(${rotation}deg) rotateX(${rotateX}deg) rotateY(${rotateY}deg) rotateZ(${rotateZ}deg) skew(${skew}deg)`; - let s = `width:${width}px;height:${height}px;transform:${transform};transform-origin:center center;transform-style:preserve-3d;`; + + const transform = ` + translate(${x}px,${y}px) + scale(${scale}) + rotate(${rotation}deg) + rotateX(${rotateX}deg) + rotateY(${rotateY}deg) + rotateZ(${rotateZ}deg) + skew(${skew}deg) + `; + + let style = ` + width:${layout.width}px; + height:${layout.height}px; + transform:${transform}; + transform-origin:center center; + transform-style:preserve-3d; + `; + const b = mockupStore.imageBorder; - if (b.enabled) - s += `border:${b.width}px solid ${b.color};border-radius:${b.radius}px;box-shadow:${b.shadow};`; - return s; + + if (b.enabled) { + style += ` + border:${b.width}px solid ${b.color}; + border-radius:${b.radius}px; + box-shadow:${b.shadow}; + `; + } + + return style; }); - // ─── Clipboard paste ($effect #2, intentionally kept — needs document listener) ── + /* ---------------- clipboard paste ---------------- */ $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: { @@ -101,6 +190,7 @@ } } }); + setTimeout(() => (showPasteHint = false), 3000); } else { await uploadFile(file); @@ -108,11 +198,12 @@ toast('Image pasted successfully!'); } }; + document.addEventListener('paste', onPaste); return () => document.removeEventListener('paste', onPaste); }); - // ─── Upload helpers ─────────────────────────────────────────────────────────── + /* ---------------- upload helpers ---------------- */ const hexToRgb = (hex: string) => ({ r: parseInt(hex.slice(1, 3), 16), @@ -127,6 +218,7 @@ return `rgba(${r},${g},${b},0.5)`; })() : 'rgba(156,163,137,0.5)'; + mockupStore.setImageBorder({ enabled: true, width: 8, @@ -144,16 +236,24 @@ r.readAsDataURL(file); return; } + const img = new Image(); + img.onload = () => { const canvas = document.createElement('canvas'); + const ratio = Math.min(2400 / img.width, 1800 / img.height, 1); + canvas.width = Math.round(img.width * ratio); canvas.height = Math.round(img.height * ratio); + canvas.getContext('2d')!.drawImage(img, 0, 0, canvas.width, canvas.height); + resolve(canvas.toDataURL('image/png')); }; + img.onerror = reject; + img.src = URL.createObjectURL(file); }); @@ -162,23 +262,30 @@ file.size > 1024 * 1024 ? toast('Processing image...', { duration: Infinity }) : undefined; const dataUrl = await toDataUrl(file); + mockupStore.setUploadedImage(dataUrl); + if (loadingToast) toast.dismiss(loadingToast); try { const dominant = await extractDominantColor(dataUrl); + applyBorder(/^#[0-9A-Fa-f]{6}$/.test(dominant) ? dominant : undefined); } catch { applyBorder(); } + toast('Image uploaded!'); }; const onDrop = (e: DragEvent) => { e.preventDefault(); e.stopPropagation(); + isDragOver = false; + const file = Array.from(e.dataTransfer?.files ?? []).find((f) => f.type.startsWith('image/')); + if (file) { localStorage.removeItem('demoImage'); uploadFile(file); @@ -187,6 +294,7 @@ const onFileSelect = (e: Event) => { const file = (e.target as HTMLInputElement).files?.[0]; + if (file) { localStorage.removeItem('demoImage'); uploadFile(file); @@ -195,9 +303,12 @@ 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); } catch { toast.error('Failed to load demo image.'); @@ -205,22 +316,17 @@ }; - { - viewport.width = window.innerWidth; - viewport.height = window.innerHeight; - }} -/> +
{ @@ -237,11 +343,11 @@ Uploaded mockup e.stopPropagation()} /> + {#if isDragOver}
{/if} + {#if showPasteHint}
{:else} -
{ e.preventDefault(); isDragOver = true; }} + onkeydown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + fileInputRef?.click(); + } + }} ondragleave={() => (isDragOver = false)} onclick={() => fileInputRef?.click()} - role="button" - tabindex="0" - onkeydown={(e) => e.key === 'Enter' && fileInputRef?.click()} > +

Drop image here or click to upload

Supports JPG, PNG

+
Or paste (Ctrl+V)
- - Use demo Image -
{/if}
diff --git a/src/lib/editor/Navbar.svelte b/src/lib/editor/Navbar.svelte index 5f7398d..8a620e0 100644 --- a/src/lib/editor/Navbar.svelte +++ b/src/lib/editor/Navbar.svelte @@ -284,13 +284,13 @@
{#if !isMobile} -
+
crafted by Jaydip