svelte-product-mockup 1.0.2 → 1.0.3
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/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 +14 -0
- package/dist/index.js +13 -2759
- package/dist/renderMockupCanvas.d.ts +2 -0
- package/dist/renderMockupCanvas.js +488 -0
- package/dist/renderMockupCanvasEditor.d.ts +6 -0
- package/dist/renderMockupCanvasEditor.js +32 -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/dist/workers/InputBatcher.d.ts +18 -0
- package/dist/workers/InputBatcher.js +56 -0
- package/dist/workers/RenderScheduler.d.ts +30 -0
- package/dist/workers/RenderScheduler.js +99 -0
- package/dist/workers/index.d.ts +3 -0
- package/dist/workers/index.js +2 -0
- package/dist/workers/render.worker.d.ts +1 -0
- package/dist/workers/render.worker.js +68 -0
- package/dist/workers/types.d.ts +42 -0
- package/dist/workers/types.js +1 -0
- package/dist/workers/vite-worker.d.ts +4 -0
- package/package.json +3 -2
- package/src/{lib/components → app}/CarshoToolsLogo.svelte +1 -1
- package/src/lib/index.ts +16 -0
- package/src/lib/storage.ts +2 -0
- package/src/lib/types.ts +14 -2
- package/src/lib/workers/RenderScheduler.ts +8 -8
- package/src/lib/workers/render.worker.ts +1 -1
- package/src/lib/workers/vite-worker.d.ts +4 -0
- package/src/routes/+layout.svelte +1 -1
- package/src/routes/+page.svelte +1 -1
- /package/src/{lib → app}/assets/carshotools.svg +0 -0
- /package/src/{lib → app}/assets/favicon.svg +0 -0
|
@@ -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;
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Layer, LayerTransform } from '../types';
|
|
3
|
+
import type { ResizeEffect, WarpEffect, RestyleEffect, SvgEffect } from '../image-transformations/types';
|
|
4
|
+
import { defaultResizeEffect, defaultRestyleEffect } from '../image-transformations/types';
|
|
5
|
+
import { verifySvgSource } from '../image-transformations/svg';
|
|
6
|
+
import EyeOpenIcon from '../icons/EyeOpenIcon.svelte';
|
|
7
|
+
import EyeClosedIcon from '../icons/EyeClosedIcon.svelte';
|
|
8
|
+
import TrashIcon from '../icons/TrashIcon.svelte';
|
|
9
|
+
import TransformSection from './sections/TransformSection.svelte';
|
|
10
|
+
import SizingSection from './sections/SizingSection.svelte';
|
|
11
|
+
import WarpSection from './sections/WarpSection.svelte';
|
|
12
|
+
import RestyleSection from './sections/RestyleSection.svelte';
|
|
13
|
+
import SvgSection from './sections/SvgSection.svelte';
|
|
14
|
+
|
|
15
|
+
interface Props {
|
|
16
|
+
layer: Layer;
|
|
17
|
+
canvasWidth: number;
|
|
18
|
+
canvasHeight: number;
|
|
19
|
+
onTransformChange: (transform: LayerTransform) => void;
|
|
20
|
+
onOpacityChange: (opacity: number) => void;
|
|
21
|
+
onVisibilityChange: (visible: boolean) => void;
|
|
22
|
+
onEffectsChange: (effects: Layer['effects']) => void;
|
|
23
|
+
onNameChange?: (name: string) => void;
|
|
24
|
+
onDelete: () => void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let {
|
|
28
|
+
layer,
|
|
29
|
+
canvasWidth,
|
|
30
|
+
canvasHeight,
|
|
31
|
+
onTransformChange,
|
|
32
|
+
onOpacityChange,
|
|
33
|
+
onVisibilityChange,
|
|
34
|
+
onEffectsChange,
|
|
35
|
+
onNameChange,
|
|
36
|
+
onDelete
|
|
37
|
+
}: Props = $props();
|
|
38
|
+
|
|
39
|
+
const resizeEffect = $derived.by(() => {
|
|
40
|
+
const effect = layer.effects.find((e) => e.type === 'resize') as ResizeEffect | undefined;
|
|
41
|
+
return effect ?? defaultResizeEffect;
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const restyleEffect = $derived.by(() => {
|
|
45
|
+
const effect = layer.effects.find((e) => e.type === 'restyle') as RestyleEffect | undefined;
|
|
46
|
+
return effect ?? defaultRestyleEffect;
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const warpEffect = $derived.by(() => {
|
|
50
|
+
return layer.effects.find((e) => e.type === 'warp') as WarpEffect | undefined;
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const svgEffect = $derived.by(() => {
|
|
54
|
+
return layer.effects.find((e) => e.type === 'svg') as SvgEffect | undefined;
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
let isVerifiedSvg = $state<boolean | null>(null);
|
|
58
|
+
|
|
59
|
+
$effect(() => {
|
|
60
|
+
isVerifiedSvg = null;
|
|
61
|
+
verifySvgSource(layer.src).then((verified) => {
|
|
62
|
+
isVerifiedSvg = verified;
|
|
63
|
+
}).catch(() => {
|
|
64
|
+
isVerifiedSvg = false;
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
function handleResizeChange(effect: ResizeEffect) {
|
|
69
|
+
const otherEffects = layer.effects.filter((e) => e.type !== 'resize');
|
|
70
|
+
onEffectsChange([...otherEffects, effect]);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function handleRestyleChange(effect: RestyleEffect) {
|
|
74
|
+
const otherEffects = layer.effects.filter((e) => e.type !== 'restyle');
|
|
75
|
+
onEffectsChange([...otherEffects, effect]);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function handleWarpChange(effect: WarpEffect | undefined) {
|
|
79
|
+
const otherEffects = layer.effects.filter((e) => e.type !== 'warp');
|
|
80
|
+
if (effect) {
|
|
81
|
+
onEffectsChange([...otherEffects, effect]);
|
|
82
|
+
} else {
|
|
83
|
+
onEffectsChange(otherEffects);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function handleSvgChange(effect: SvgEffect | undefined) {
|
|
88
|
+
const otherEffects = layer.effects.filter((e) => e.type !== 'svg');
|
|
89
|
+
if (effect) {
|
|
90
|
+
onEffectsChange([...otherEffects, effect]);
|
|
91
|
+
} else {
|
|
92
|
+
onEffectsChange(otherEffects);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
</script>
|
|
96
|
+
|
|
97
|
+
<div class="layer-controls">
|
|
98
|
+
<div class="layer-header">
|
|
99
|
+
<button
|
|
100
|
+
class="visibility-toggle"
|
|
101
|
+
onclick={() => onVisibilityChange(!layer.visible)}
|
|
102
|
+
aria-label={layer.visible ? 'Hide layer' : 'Show layer'}
|
|
103
|
+
>
|
|
104
|
+
{#if layer.visible}
|
|
105
|
+
<EyeOpenIcon />
|
|
106
|
+
{:else}
|
|
107
|
+
<EyeClosedIcon />
|
|
108
|
+
{/if}
|
|
109
|
+
</button>
|
|
110
|
+
<span class="layer-preview">
|
|
111
|
+
<img src={layer.src} alt="Layer preview" />
|
|
112
|
+
</span>
|
|
113
|
+
<button class="delete-btn" onclick={onDelete} aria-label="Delete layer">
|
|
114
|
+
<TrashIcon />
|
|
115
|
+
</button>
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
{#if onNameChange}
|
|
119
|
+
<div class="layer-name-section">
|
|
120
|
+
<label class="form-label" for="layer-name-{layer.id}">Layer Name (for overrides)</label>
|
|
121
|
+
<input
|
|
122
|
+
id="layer-name-{layer.id}"
|
|
123
|
+
type="text"
|
|
124
|
+
class="form-input"
|
|
125
|
+
placeholder="e.g., front, back, logo"
|
|
126
|
+
value={layer.name || ''}
|
|
127
|
+
oninput={(e) => onNameChange(e.currentTarget.value)}
|
|
128
|
+
/>
|
|
129
|
+
<p class="form-hint">Use this name as the key when overriding images in the renderer</p>
|
|
130
|
+
</div>
|
|
131
|
+
{/if}
|
|
132
|
+
|
|
133
|
+
<div class="sections">
|
|
134
|
+
{#if isVerifiedSvg === true}
|
|
135
|
+
<SvgSection effect={svgEffect} onChange={handleSvgChange} />
|
|
136
|
+
{/if}
|
|
137
|
+
<SizingSection effect={resizeEffect} onChange={handleResizeChange} />
|
|
138
|
+
<RestyleSection effect={restyleEffect} onChange={handleRestyleChange} />
|
|
139
|
+
<TransformSection
|
|
140
|
+
transform={layer.transform}
|
|
141
|
+
opacity={layer.opacity}
|
|
142
|
+
{canvasWidth}
|
|
143
|
+
{canvasHeight}
|
|
144
|
+
onTransformChange={onTransformChange}
|
|
145
|
+
onOpacityChange={onOpacityChange}
|
|
146
|
+
layerId={layer.id}
|
|
147
|
+
/>
|
|
148
|
+
<WarpSection effect={warpEffect} onChange={handleWarpChange} />
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
|
|
152
|
+
<style>
|
|
153
|
+
.layer-controls {
|
|
154
|
+
border-radius: 4px;
|
|
155
|
+
padding-top: 0.75rem;
|
|
156
|
+
padding-bottom: 0.75rem;
|
|
157
|
+
margin-bottom: 0.5rem;
|
|
158
|
+
background: var(--color-surface, #fff);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.layer-header {
|
|
162
|
+
display: flex;
|
|
163
|
+
align-items: center;
|
|
164
|
+
gap: 0.5rem;
|
|
165
|
+
margin-bottom: 0.75rem;
|
|
166
|
+
padding-bottom: 0.75rem;
|
|
167
|
+
border-bottom: 1px solid var(--color-border, #eee);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.visibility-toggle {
|
|
171
|
+
background: none;
|
|
172
|
+
border: none;
|
|
173
|
+
cursor: pointer;
|
|
174
|
+
padding: 0.25rem;
|
|
175
|
+
display: flex;
|
|
176
|
+
align-items: center;
|
|
177
|
+
justify-content: center;
|
|
178
|
+
color: var(--color-text, #000);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.layer-preview {
|
|
182
|
+
flex: 1;
|
|
183
|
+
display: flex;
|
|
184
|
+
align-items: center;
|
|
185
|
+
justify-content: center;
|
|
186
|
+
height: 40px;
|
|
187
|
+
overflow: hidden;
|
|
188
|
+
border-radius: 4px;
|
|
189
|
+
background: #f5f5f5;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
.layer-preview img {
|
|
193
|
+
max-width: 100%;
|
|
194
|
+
max-height: 100%;
|
|
195
|
+
object-fit: contain;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.delete-btn {
|
|
199
|
+
background: #ef4444;
|
|
200
|
+
color: white;
|
|
201
|
+
border: none;
|
|
202
|
+
border-radius: 4px;
|
|
203
|
+
width: 24px;
|
|
204
|
+
height: 24px;
|
|
205
|
+
cursor: pointer;
|
|
206
|
+
display: flex;
|
|
207
|
+
align-items: center;
|
|
208
|
+
justify-content: center;
|
|
209
|
+
padding: 0;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
.delete-btn:hover {
|
|
213
|
+
background: #dc2626;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
.layer-name-section {
|
|
217
|
+
padding: 0 0.75rem;
|
|
218
|
+
margin-bottom: 0.75rem;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
.layer-name-section .form-label {
|
|
222
|
+
display: block;
|
|
223
|
+
font-size: 0.75rem;
|
|
224
|
+
font-weight: 500;
|
|
225
|
+
color: var(--color-text-muted, #666);
|
|
226
|
+
margin-bottom: 0.25rem;
|
|
227
|
+
text-transform: uppercase;
|
|
228
|
+
letter-spacing: 0.5px;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
.layer-name-section .form-input {
|
|
232
|
+
width: 100%;
|
|
233
|
+
padding: 0.375rem 0.5rem;
|
|
234
|
+
border: 1px solid var(--color-border, #ccc);
|
|
235
|
+
border-radius: 4px;
|
|
236
|
+
font-size: 0.875rem;
|
|
237
|
+
font-family: inherit;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
.layer-name-section .form-input:focus {
|
|
241
|
+
outline: none;
|
|
242
|
+
border-color: var(--color-primary, #3b82f6);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
.form-hint {
|
|
246
|
+
font-size: 0.75rem;
|
|
247
|
+
color: var(--color-text-muted, #666);
|
|
248
|
+
margin: 0.25rem 0 0 0;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
.sections {
|
|
252
|
+
display: flex;
|
|
253
|
+
flex-direction: column;
|
|
254
|
+
gap: 0;
|
|
255
|
+
overflow: hidden;
|
|
256
|
+
min-width: 0;
|
|
257
|
+
}
|
|
258
|
+
</style>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Layer, LayerTransform } from '../types';
|
|
2
|
+
interface Props {
|
|
3
|
+
layer: Layer;
|
|
4
|
+
canvasWidth: number;
|
|
5
|
+
canvasHeight: number;
|
|
6
|
+
onTransformChange: (transform: LayerTransform) => void;
|
|
7
|
+
onOpacityChange: (opacity: number) => void;
|
|
8
|
+
onVisibilityChange: (visible: boolean) => void;
|
|
9
|
+
onEffectsChange: (effects: Layer['effects']) => void;
|
|
10
|
+
onNameChange?: (name: string) => void;
|
|
11
|
+
onDelete: () => void;
|
|
12
|
+
}
|
|
13
|
+
declare const LayerControls: import("svelte").Component<Props, {}, "">;
|
|
14
|
+
type LayerControls = ReturnType<typeof LayerControls>;
|
|
15
|
+
export default LayerControls;
|