svelte-product-mockup 1.0.2 โ 1.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/InputBatcher.d.ts +18 -0
- package/dist/InputBatcher.js +56 -0
- package/dist/RenderScheduler.d.ts +52 -0
- package/dist/RenderScheduler.js +87 -0
- package/dist/components/ImageAdder.svelte +148 -0
- package/dist/components/ImageAdder.svelte.d.ts +7 -0
- package/dist/components/InfoSections.svelte +152 -0
- package/dist/components/InfoSections.svelte.d.ts +18 -0
- package/dist/components/LayerControls.svelte +258 -0
- package/dist/components/LayerControls.svelte.d.ts +15 -0
- package/dist/components/MockupEditor.svelte +931 -0
- package/dist/components/MockupEditor.svelte.d.ts +8 -0
- package/dist/components/MockupEditorRenderer.svelte +178 -0
- package/dist/components/MockupEditorRenderer.svelte.d.ts +10 -0
- package/dist/components/MockupListModal.svelte +333 -0
- package/dist/components/MockupListModal.svelte.d.ts +11 -0
- package/dist/components/MockupRenderer.svelte +64 -0
- package/dist/components/MockupRenderer.svelte.d.ts +9 -0
- package/dist/components/Modal.svelte +121 -0
- package/dist/components/Modal.svelte.d.ts +8 -0
- package/dist/components/ZoomableCanvas.svelte +126 -0
- package/dist/components/ZoomableCanvas.svelte.d.ts +6 -0
- package/dist/components/sections/ControlGroup.svelte +82 -0
- package/dist/components/sections/ControlGroup.svelte.d.ts +9 -0
- package/dist/components/sections/RestyleSection.svelte +356 -0
- package/dist/components/sections/RestyleSection.svelte.d.ts +8 -0
- package/dist/components/sections/Section.svelte +65 -0
- package/dist/components/sections/Section.svelte.d.ts +9 -0
- package/dist/components/sections/SizingSection.svelte +233 -0
- package/dist/components/sections/SizingSection.svelte.d.ts +8 -0
- package/dist/components/sections/SvgSection.svelte +174 -0
- package/dist/components/sections/SvgSection.svelte.d.ts +8 -0
- package/dist/components/sections/TransformSection.svelte +229 -0
- package/dist/components/sections/TransformSection.svelte.d.ts +13 -0
- package/dist/components/sections/WarpSection.svelte +505 -0
- package/dist/components/sections/WarpSection.svelte.d.ts +8 -0
- package/dist/components/sections/types.d.ts +12 -0
- package/dist/components/sections/types.js +1 -0
- package/dist/config.d.ts +2 -0
- package/dist/config.js +2 -0
- package/dist/icons/ChevronIcon.svelte +23 -0
- package/dist/icons/ChevronIcon.svelte.d.ts +8 -0
- package/dist/icons/EyeClosedIcon.svelte +23 -0
- package/dist/icons/EyeClosedIcon.svelte.d.ts +7 -0
- package/dist/icons/EyeOpenIcon.svelte +23 -0
- package/dist/icons/EyeOpenIcon.svelte.d.ts +7 -0
- package/dist/icons/ImageIcon.svelte +23 -0
- package/dist/icons/ImageIcon.svelte.d.ts +8 -0
- package/dist/icons/PlusIcon.svelte +20 -0
- package/dist/icons/PlusIcon.svelte.d.ts +7 -0
- package/dist/icons/ResetIcon.svelte +23 -0
- package/dist/icons/ResetIcon.svelte.d.ts +7 -0
- package/dist/icons/TrashIcon.svelte +20 -0
- package/dist/icons/TrashIcon.svelte.d.ts +7 -0
- package/dist/image-transformations/index.d.ts +13 -0
- package/dist/image-transformations/index.js +61 -0
- package/dist/image-transformations/resize.d.ts +10 -0
- package/dist/image-transformations/resize.js +106 -0
- package/dist/image-transformations/restyle.d.ts +2 -0
- package/dist/image-transformations/restyle.js +121 -0
- package/dist/image-transformations/svg.d.ts +3 -0
- package/dist/image-transformations/svg.js +106 -0
- package/dist/image-transformations/types.d.ts +44 -0
- package/dist/image-transformations/types.js +27 -0
- package/dist/image-transformations/warp/custom.d.ts +2 -0
- package/dist/image-transformations/warp/custom.js +3 -0
- package/dist/image-transformations/warp/cylinder.d.ts +2 -0
- package/dist/image-transformations/warp/cylinder.js +179 -0
- package/dist/image-transformations/warp/helpers.d.ts +8 -0
- package/dist/image-transformations/warp/helpers.js +233 -0
- package/dist/image-transformations/warp/index.d.ts +8 -0
- package/dist/image-transformations/warp/index.js +44 -0
- package/dist/image-transformations/warp/plane.d.ts +2 -0
- package/dist/image-transformations/warp/plane.js +136 -0
- package/dist/image-transformations/warp/sphere.d.ts +2 -0
- package/dist/image-transformations/warp/sphere.js +99 -0
- package/dist/image-transformations/warp/types.d.ts +79 -0
- package/dist/image-transformations/warp/types.js +40 -0
- package/dist/image-transformations/warp.d.ts +1 -0
- package/dist/image-transformations/warp.js +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +12 -2759
- package/dist/renderMockupCanvas.d.ts +2 -0
- package/dist/renderMockupCanvas.js +440 -0
- package/dist/renderMockupCanvasEditor.d.ts +6 -0
- package/dist/renderMockupCanvasEditor.js +32 -0
- package/dist/renderWorkerInline.d.ts +2 -0
- package/dist/renderWorkerInline.js +9 -0
- package/dist/storage.d.ts +8 -0
- package/dist/storage.js +98 -0
- package/dist/stores/mockupStore.d.ts +17 -0
- package/dist/stores/mockupStore.js +144 -0
- package/dist/types.d.ts +46 -0
- package/dist/types.js +27 -0
- package/package.json +8 -5
- package/src/{lib/components โ app}/CarshoToolsLogo.svelte +1 -1
- package/src/index.ts +0 -1
- package/src/lib/RenderScheduler.ts +140 -0
- package/src/lib/index.ts +15 -0
- package/src/lib/renderMockupCanvas.ts +5 -61
- package/src/lib/renderMockupCanvasEditor.ts +1 -1
- package/src/lib/renderWorkerInline.ts +10 -0
- package/src/lib/storage.ts +2 -0
- package/src/lib/types.ts +14 -2
- package/src/routes/+layout.svelte +1 -1
- package/src/routes/+page.svelte +1 -1
- package/src/workers/InputBatcher.ts +65 -0
- package/src/{lib/workers โ workers}/RenderScheduler.ts +8 -8
- package/src/{lib/workers โ workers}/render.worker.ts +3 -3
- package/src/workers/vite-worker.d.ts +4 -0
- /package/src/{lib โ app}/assets/carshotools.svg +0 -0
- /package/src/{lib โ app}/assets/favicon.svg +0 -0
- /package/src/lib/{workers/InputBatcher.ts โ InputBatcher.ts} +0 -0
- /package/src/{lib/workers โ workers}/index.ts +0 -0
- /package/src/{lib/workers โ workers}/types.ts +0 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* InputBatcher - Batches rapid input events using requestAnimationFrame
|
|
3
|
+
* with a trailing timeout to ensure the last event always fires.
|
|
4
|
+
*
|
|
5
|
+
* - During rapid input: batches to ~60fps via rAF
|
|
6
|
+
* - After input stops: trailing timeout ensures final state renders within TRAILING_MS
|
|
7
|
+
*/
|
|
8
|
+
export declare class InputBatcher<T> {
|
|
9
|
+
private latestInput;
|
|
10
|
+
private rafPending;
|
|
11
|
+
private timeoutId;
|
|
12
|
+
private onFlush;
|
|
13
|
+
private readonly TRAILING_MS;
|
|
14
|
+
constructor(onFlush: (input: T) => void);
|
|
15
|
+
queue(input: T): void;
|
|
16
|
+
private flush;
|
|
17
|
+
destroy(): void;
|
|
18
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* InputBatcher - Batches rapid input events using requestAnimationFrame
|
|
3
|
+
* with a trailing timeout to ensure the last event always fires.
|
|
4
|
+
*
|
|
5
|
+
* - During rapid input: batches to ~60fps via rAF
|
|
6
|
+
* - After input stops: trailing timeout ensures final state renders within TRAILING_MS
|
|
7
|
+
*/
|
|
8
|
+
export class InputBatcher {
|
|
9
|
+
latestInput = null;
|
|
10
|
+
rafPending = false;
|
|
11
|
+
timeoutId = null;
|
|
12
|
+
onFlush;
|
|
13
|
+
TRAILING_MS = 32;
|
|
14
|
+
constructor(onFlush) {
|
|
15
|
+
this.onFlush = onFlush;
|
|
16
|
+
}
|
|
17
|
+
queue(input) {
|
|
18
|
+
this.latestInput = input;
|
|
19
|
+
// Clear previous trailing timeout
|
|
20
|
+
if (this.timeoutId !== null) {
|
|
21
|
+
clearTimeout(this.timeoutId);
|
|
22
|
+
}
|
|
23
|
+
// Schedule rAF if not already pending (60fps batching)
|
|
24
|
+
if (!this.rafPending) {
|
|
25
|
+
this.rafPending = true;
|
|
26
|
+
requestAnimationFrame(() => {
|
|
27
|
+
this.rafPending = false;
|
|
28
|
+
this.flush();
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
// Trailing timeout: ensures last event ALWAYS fires within TRAILING_MS
|
|
32
|
+
this.timeoutId = setTimeout(() => {
|
|
33
|
+
this.timeoutId = null;
|
|
34
|
+
this.flush();
|
|
35
|
+
}, this.TRAILING_MS);
|
|
36
|
+
}
|
|
37
|
+
flush() {
|
|
38
|
+
if (this.latestInput === null)
|
|
39
|
+
return;
|
|
40
|
+
// Clear timeout if we're flushing via rAF
|
|
41
|
+
if (this.timeoutId !== null) {
|
|
42
|
+
clearTimeout(this.timeoutId);
|
|
43
|
+
this.timeoutId = null;
|
|
44
|
+
}
|
|
45
|
+
const input = this.latestInput;
|
|
46
|
+
this.latestInput = null;
|
|
47
|
+
this.onFlush(input);
|
|
48
|
+
}
|
|
49
|
+
destroy() {
|
|
50
|
+
if (this.timeoutId !== null) {
|
|
51
|
+
clearTimeout(this.timeoutId);
|
|
52
|
+
this.timeoutId = null;
|
|
53
|
+
}
|
|
54
|
+
this.latestInput = null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { WarpEffect } from './image-transformations/types';
|
|
2
|
+
export interface LayerRenderData {
|
|
3
|
+
id: string;
|
|
4
|
+
x: number;
|
|
5
|
+
y: number;
|
|
6
|
+
width: number;
|
|
7
|
+
height: number;
|
|
8
|
+
rotation: number;
|
|
9
|
+
opacity: number;
|
|
10
|
+
warp: WarpEffect | null;
|
|
11
|
+
}
|
|
12
|
+
export interface RenderRequest {
|
|
13
|
+
id: number;
|
|
14
|
+
canvasWidth: number;
|
|
15
|
+
canvasHeight: number;
|
|
16
|
+
layers: LayerRenderData[];
|
|
17
|
+
images: ImageBitmap[];
|
|
18
|
+
}
|
|
19
|
+
export interface WorkerMessage {
|
|
20
|
+
type: 'render';
|
|
21
|
+
payload: RenderRequest;
|
|
22
|
+
}
|
|
23
|
+
export interface WorkerResponse {
|
|
24
|
+
type: 'complete' | 'error';
|
|
25
|
+
payload: {
|
|
26
|
+
id: number;
|
|
27
|
+
result?: ImageBitmap;
|
|
28
|
+
frameTime?: number;
|
|
29
|
+
error?: string;
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
export interface RenderSchedulerOptions {
|
|
33
|
+
onResult: (bitmap: ImageBitmap, frameTime: number) => void;
|
|
34
|
+
onError?: (error: string) => void;
|
|
35
|
+
}
|
|
36
|
+
export declare class RenderScheduler {
|
|
37
|
+
private worker;
|
|
38
|
+
private batcher;
|
|
39
|
+
private pendingRequest;
|
|
40
|
+
private isWorking;
|
|
41
|
+
private frameId;
|
|
42
|
+
private lastCompletedId;
|
|
43
|
+
private options;
|
|
44
|
+
constructor(options: RenderSchedulerOptions);
|
|
45
|
+
private initWorker;
|
|
46
|
+
request(req: Omit<RenderRequest, 'id'>): void;
|
|
47
|
+
private submitToWorker;
|
|
48
|
+
private sendToWorker;
|
|
49
|
+
private handleWorkerMessage;
|
|
50
|
+
private processNext;
|
|
51
|
+
destroy(): void;
|
|
52
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { InputBatcher } from './InputBatcher';
|
|
2
|
+
import { createRenderWorker } from './renderWorkerInline';
|
|
3
|
+
export class RenderScheduler {
|
|
4
|
+
worker = null;
|
|
5
|
+
batcher;
|
|
6
|
+
pendingRequest = null;
|
|
7
|
+
isWorking = false;
|
|
8
|
+
frameId = 0;
|
|
9
|
+
lastCompletedId = 0;
|
|
10
|
+
options;
|
|
11
|
+
constructor(options) {
|
|
12
|
+
this.options = options;
|
|
13
|
+
this.batcher = new InputBatcher((req) => this.submitToWorker(req));
|
|
14
|
+
this.initWorker();
|
|
15
|
+
}
|
|
16
|
+
initWorker() {
|
|
17
|
+
try {
|
|
18
|
+
this.worker = createRenderWorker();
|
|
19
|
+
this.worker.onmessage = (e) => {
|
|
20
|
+
this.handleWorkerMessage(e.data);
|
|
21
|
+
};
|
|
22
|
+
this.worker.onerror = (e) => {
|
|
23
|
+
console.error('Worker error:', e);
|
|
24
|
+
this.options.onError?.(`Worker error: ${e.message}`);
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
catch (e) {
|
|
28
|
+
console.error('Failed to create worker:', e);
|
|
29
|
+
this.options.onError?.('Failed to create render worker');
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
request(req) {
|
|
33
|
+
this.batcher.queue(req);
|
|
34
|
+
}
|
|
35
|
+
submitToWorker(req) {
|
|
36
|
+
const id = ++this.frameId;
|
|
37
|
+
const request = { ...req, id };
|
|
38
|
+
if (this.isWorking) {
|
|
39
|
+
this.pendingRequest = request;
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
this.sendToWorker(request);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
sendToWorker(request) {
|
|
46
|
+
if (!this.worker)
|
|
47
|
+
return;
|
|
48
|
+
this.isWorking = true;
|
|
49
|
+
const message = { type: 'render', payload: request };
|
|
50
|
+
const transferables = request.images.filter(img => img instanceof ImageBitmap);
|
|
51
|
+
this.worker.postMessage(message, transferables);
|
|
52
|
+
}
|
|
53
|
+
handleWorkerMessage(response) {
|
|
54
|
+
if (response.type === 'error') {
|
|
55
|
+
console.error('Render error:', response.payload.error);
|
|
56
|
+
this.isWorking = false;
|
|
57
|
+
this.processNext();
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const { id, result, frameTime } = response.payload;
|
|
61
|
+
this.isWorking = false;
|
|
62
|
+
if (id <= this.lastCompletedId) {
|
|
63
|
+
result?.close();
|
|
64
|
+
this.processNext();
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
this.lastCompletedId = id;
|
|
68
|
+
if (result && frameTime !== undefined) {
|
|
69
|
+
this.options.onResult(result, frameTime);
|
|
70
|
+
}
|
|
71
|
+
this.processNext();
|
|
72
|
+
}
|
|
73
|
+
processNext() {
|
|
74
|
+
if (this.pendingRequest) {
|
|
75
|
+
const next = this.pendingRequest;
|
|
76
|
+
this.pendingRequest = null;
|
|
77
|
+
this.sendToWorker(next);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
destroy() {
|
|
81
|
+
this.batcher.destroy();
|
|
82
|
+
if (this.worker) {
|
|
83
|
+
this.worker.terminate();
|
|
84
|
+
this.worker = null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
interface Props {
|
|
3
|
+
onAddImage: (src: string) => void;
|
|
4
|
+
localImages?: string[];
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
let { onAddImage, localImages = [] }: Props = $props();
|
|
8
|
+
|
|
9
|
+
let urlInput = $state('');
|
|
10
|
+
let showUrlInput = $state(false);
|
|
11
|
+
|
|
12
|
+
function handleUrlSubmit() {
|
|
13
|
+
const trimmed = urlInput.trim();
|
|
14
|
+
if (trimmed) {
|
|
15
|
+
onAddImage(trimmed);
|
|
16
|
+
urlInput = '';
|
|
17
|
+
showUrlInput = false;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function handleFileSelect(e: Event) {
|
|
22
|
+
const input = e.currentTarget as HTMLInputElement;
|
|
23
|
+
const file = input.files?.[0];
|
|
24
|
+
if (file) {
|
|
25
|
+
const reader = new FileReader();
|
|
26
|
+
reader.onload = (e) => {
|
|
27
|
+
const result = e.target?.result;
|
|
28
|
+
if (typeof result === 'string') {
|
|
29
|
+
onAddImage(result);
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
reader.readAsDataURL(file);
|
|
33
|
+
}
|
|
34
|
+
input.value = '';
|
|
35
|
+
}
|
|
36
|
+
</script>
|
|
37
|
+
|
|
38
|
+
<div class="image-adder">
|
|
39
|
+
<div class="add-buttons">
|
|
40
|
+
<button class="btn" onclick={() => (showUrlInput = !showUrlInput)}>
|
|
41
|
+
{showUrlInput ? 'Cancel' : 'Add from URL'}
|
|
42
|
+
</button>
|
|
43
|
+
<label class="btn">
|
|
44
|
+
Add from File
|
|
45
|
+
<input type="file" accept="image/*" onchange={handleFileSelect} style="display: none;" />
|
|
46
|
+
</label>
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
{#if showUrlInput}
|
|
50
|
+
<div class="url-input-group">
|
|
51
|
+
<input
|
|
52
|
+
type="url"
|
|
53
|
+
placeholder="https://example.com/image.jpg"
|
|
54
|
+
bind:value={urlInput}
|
|
55
|
+
onkeydown={(e) => e.key === 'Enter' && handleUrlSubmit()}
|
|
56
|
+
/>
|
|
57
|
+
<button class="btn" onclick={handleUrlSubmit}>Add</button>
|
|
58
|
+
</div>
|
|
59
|
+
{/if}
|
|
60
|
+
|
|
61
|
+
{#if localImages.length > 0}
|
|
62
|
+
<div class="local-images">
|
|
63
|
+
<h3>Local Images</h3>
|
|
64
|
+
<div class="image-grid">
|
|
65
|
+
{#each localImages as src}
|
|
66
|
+
<button class="image-item" onclick={() => onAddImage(src)}>
|
|
67
|
+
<img src={src} alt="" />
|
|
68
|
+
</button>
|
|
69
|
+
{/each}
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
{/if}
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
<style>
|
|
76
|
+
.image-adder {
|
|
77
|
+
display: flex;
|
|
78
|
+
flex-direction: column;
|
|
79
|
+
gap: 1rem;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.add-buttons {
|
|
83
|
+
display: flex;
|
|
84
|
+
gap: 0.5rem;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.btn {
|
|
88
|
+
padding: 0.5rem 1rem;
|
|
89
|
+
background: var(--color-primary, #3b82f6);
|
|
90
|
+
color: white;
|
|
91
|
+
border: none;
|
|
92
|
+
border-radius: 4px;
|
|
93
|
+
cursor: pointer;
|
|
94
|
+
font-size: 0.875rem;
|
|
95
|
+
transition: background 200ms;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.btn:hover {
|
|
99
|
+
background: var(--color-primary-dark, #2563eb);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.url-input-group {
|
|
103
|
+
display: flex;
|
|
104
|
+
gap: 0.5rem;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.url-input-group input {
|
|
108
|
+
flex: 1;
|
|
109
|
+
padding: 0.5rem;
|
|
110
|
+
border: 1px solid var(--color-border, #ccc);
|
|
111
|
+
border-radius: 4px;
|
|
112
|
+
font-size: 0.875rem;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.local-images h3 {
|
|
116
|
+
margin: 0 0 0.5rem;
|
|
117
|
+
font-size: 0.875rem;
|
|
118
|
+
font-weight: 600;
|
|
119
|
+
color: var(--color-text-muted, #666);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.image-grid {
|
|
123
|
+
display: grid;
|
|
124
|
+
grid-template-columns: repeat(auto-fill, minmax(80px, 1fr));
|
|
125
|
+
gap: 0.5rem;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.image-item {
|
|
129
|
+
aspect-ratio: 1;
|
|
130
|
+
border: 2px solid var(--color-border, #ccc);
|
|
131
|
+
border-radius: 4px;
|
|
132
|
+
overflow: hidden;
|
|
133
|
+
cursor: pointer;
|
|
134
|
+
padding: 0;
|
|
135
|
+
background: #f5f5f5;
|
|
136
|
+
transition: border-color 200ms;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.image-item:hover {
|
|
140
|
+
border-color: var(--color-primary, #3b82f6);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.image-item img {
|
|
144
|
+
width: 100%;
|
|
145
|
+
height: 100%;
|
|
146
|
+
object-fit: cover;
|
|
147
|
+
}
|
|
148
|
+
</style>
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
</script>
|
|
3
|
+
|
|
4
|
+
<div class="info-sections">
|
|
5
|
+
<div class="info-card">
|
|
6
|
+
<h3>๐ How to Use</h3>
|
|
7
|
+
<ol>
|
|
8
|
+
<li>Add images to your mockup using the "File" or "URL" buttons</li>
|
|
9
|
+
<li>Adjust canvas size to match your desired output dimensions</li>
|
|
10
|
+
<li>Select a layer to transform, resize, or apply effects</li>
|
|
11
|
+
<li>Use pan and zoom controls to position your mockup perfectly</li>
|
|
12
|
+
<li>Save your mockup to work on it later</li>
|
|
13
|
+
</ol>
|
|
14
|
+
</div>
|
|
15
|
+
<div class="info-card">
|
|
16
|
+
<h3>๐จ Features</h3>
|
|
17
|
+
<p>
|
|
18
|
+
<span class="tag">Layer Management</span>
|
|
19
|
+
<span class="tag">Transform Controls</span>
|
|
20
|
+
<span class="tag">Image Effects</span>
|
|
21
|
+
<span class="tag">Pan & Zoom</span>
|
|
22
|
+
<span class="tag">Custom Canvas</span>
|
|
23
|
+
<span class="tag">Save & Load</span>
|
|
24
|
+
</p>
|
|
25
|
+
<p style="margin-top: 0.5rem;">Create professional product mockups by layering images, applying transformations, and fine-tuning every detail. All your work is saved automatically in your browser.</p>
|
|
26
|
+
</div>
|
|
27
|
+
<div class="info-card">
|
|
28
|
+
<h3>๐ Privacy</h3>
|
|
29
|
+
<p>This tool runs 100% locally in your browser. No image data is uploaded to any server. Your mockups and images are processed and stored entirely on your machine.</p>
|
|
30
|
+
</div>
|
|
31
|
+
<div class="info-card">
|
|
32
|
+
<h3>โ FAQ</h3>
|
|
33
|
+
<div class="faq-item">
|
|
34
|
+
<strong>What image formats are supported?</strong>
|
|
35
|
+
<p>All common image formats including PNG, JPG, WebP, and GIF. Images are displayed directly in the browser.</p>
|
|
36
|
+
</div>
|
|
37
|
+
<div class="faq-item">
|
|
38
|
+
<strong>How do I save my mockup?</strong>
|
|
39
|
+
<p>Mockups are automatically saved to your browser's local storage. Use the "View All" button to see and manage all your saved mockups.</p>
|
|
40
|
+
</div>
|
|
41
|
+
<div class="faq-item">
|
|
42
|
+
<strong>Can I export my mockup?</strong>
|
|
43
|
+
<p>You can use your browser's screenshot or print features to capture your mockup, or right-click on the canvas to save the rendered image.</p>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<footer class="info-footer">
|
|
49
|
+
<p>Mockup Renderer ยท Part of <a href="https://tools.carsho.dev">Carsho Tools</a></p>
|
|
50
|
+
</footer>
|
|
51
|
+
|
|
52
|
+
<style>
|
|
53
|
+
:global(:root) {
|
|
54
|
+
--bg: var(--bg-primary);
|
|
55
|
+
--text: var(--text-primary);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.info-sections {
|
|
59
|
+
background: var(--bg);
|
|
60
|
+
padding: 3rem 2rem;
|
|
61
|
+
display: grid;
|
|
62
|
+
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
|
63
|
+
gap: 1.5rem;
|
|
64
|
+
max-width: 1400px;
|
|
65
|
+
margin: 0 auto;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.info-card {
|
|
69
|
+
background: var(--bg-secondary);
|
|
70
|
+
border: 1px solid var(--border);
|
|
71
|
+
border-radius: 8px;
|
|
72
|
+
padding: 1.5rem;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.info-card h3 {
|
|
76
|
+
font-size: 1rem;
|
|
77
|
+
font-weight: 600;
|
|
78
|
+
margin: 0 0 1rem 0;
|
|
79
|
+
color: var(--text);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.info-card p {
|
|
83
|
+
font-size: 0.85rem;
|
|
84
|
+
color: var(--text-secondary);
|
|
85
|
+
line-height: 1.6;
|
|
86
|
+
margin: 0.5rem 0;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.info-card ol {
|
|
90
|
+
margin: 0;
|
|
91
|
+
padding-left: 1.25rem;
|
|
92
|
+
color: var(--text-secondary);
|
|
93
|
+
font-size: 0.85rem;
|
|
94
|
+
line-height: 1.8;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.info-card .faq-item {
|
|
98
|
+
margin-bottom: 1rem;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.info-card .faq-item:last-child {
|
|
102
|
+
margin-bottom: 0;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.info-card .faq-item strong {
|
|
106
|
+
color: var(--text);
|
|
107
|
+
font-size: 0.85rem;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.info-card .faq-item p {
|
|
111
|
+
margin-top: 0.25rem;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.info-card .tag {
|
|
115
|
+
display: inline-block;
|
|
116
|
+
background: var(--accent);
|
|
117
|
+
color: white;
|
|
118
|
+
padding: 0.2rem 0.5rem;
|
|
119
|
+
border-radius: 4px;
|
|
120
|
+
font-size: 0.75rem;
|
|
121
|
+
margin-right: 0.25rem;
|
|
122
|
+
margin-bottom: 0.25rem;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.info-footer {
|
|
126
|
+
background: var(--bg-secondary);
|
|
127
|
+
border-top: 1px solid var(--border);
|
|
128
|
+
padding: 1.5rem 2rem;
|
|
129
|
+
text-align: center;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.info-footer p {
|
|
133
|
+
font-size: 0.8rem;
|
|
134
|
+
color: var(--text-secondary);
|
|
135
|
+
margin: 0;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.info-footer a {
|
|
139
|
+
color: var(--accent);
|
|
140
|
+
text-decoration: none;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.info-footer a:hover {
|
|
144
|
+
text-decoration: underline;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
@media (max-width: 1000px) {
|
|
148
|
+
.info-sections {
|
|
149
|
+
padding: 2rem 1rem;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
</style>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
|
|
2
|
+
new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
|
|
3
|
+
$$bindings?: Bindings;
|
|
4
|
+
} & Exports;
|
|
5
|
+
(internal: unknown, props: {
|
|
6
|
+
$$events?: Events;
|
|
7
|
+
$$slots?: Slots;
|
|
8
|
+
}): Exports & {
|
|
9
|
+
$set?: any;
|
|
10
|
+
$on?: any;
|
|
11
|
+
};
|
|
12
|
+
z_$$bindings?: Bindings;
|
|
13
|
+
}
|
|
14
|
+
declare const InfoSections: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
|
|
15
|
+
[evt: string]: CustomEvent<any>;
|
|
16
|
+
}, {}, {}, string>;
|
|
17
|
+
type InfoSections = InstanceType<typeof InfoSections>;
|
|
18
|
+
export default InfoSections;
|