svelte-product-mockup 1.0.9 → 1.0.11
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/LayerControls.svelte +5 -6
- package/dist/components/MockupEditor.svelte +14 -17
- package/dist/components/sections/TransformSection.svelte +5 -2
- package/dist/image-transformations/warp/types.d.ts +2 -2
- package/dist/mockup-normalize.js +44 -101
- package/dist/renderMockupCanvas.js +15 -19
- package/dist/types.d.ts +6 -6
- package/package.json +2 -2
- package/src/lib/components/LayerControls.svelte +5 -6
- package/src/lib/components/MockupEditor.svelte +14 -17
- package/src/lib/components/sections/TransformSection.svelte +5 -2
- package/src/lib/image-transformations/warp/types.ts +2 -2
- package/src/lib/mockup-normalize.ts +49 -135
- package/src/lib/renderMockupCanvas.ts +15 -19
- package/src/lib/types.ts +6 -6
- package/src/routes/test/+page.svelte +2 -0
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { Layer, LayerTransform } from '../types';
|
|
3
|
-
import { defaultTransform } from '../types';
|
|
4
3
|
import type { ResizeEffect, PositionEffect, WarpEffect, RestyleEffect, SvgEffect } from '../image-transformations/types';
|
|
5
4
|
import { defaultRestyleEffect } from '../image-transformations/types';
|
|
6
5
|
import { verifySvgSource } from '../image-transformations/svg';
|
|
@@ -121,10 +120,10 @@
|
|
|
121
120
|
<div class="layer-header">
|
|
122
121
|
<button
|
|
123
122
|
class="visibility-toggle"
|
|
124
|
-
onclick={() => onVisibilityChange(!
|
|
125
|
-
aria-label={
|
|
123
|
+
onclick={() => onVisibilityChange(!layer.visible)}
|
|
124
|
+
aria-label={layer.visible ? 'Hide layer' : 'Show layer'}
|
|
126
125
|
>
|
|
127
|
-
{#if layer.visible
|
|
126
|
+
{#if layer.visible}
|
|
128
127
|
<EyeOpenIcon />
|
|
129
128
|
{:else}
|
|
130
129
|
<EyeClosedIcon />
|
|
@@ -174,8 +173,8 @@
|
|
|
174
173
|
/>
|
|
175
174
|
<RestyleSection effect={restyleEffect} onChange={handleRestyleChange} />
|
|
176
175
|
<TransformSection
|
|
177
|
-
transform={layer.transform
|
|
178
|
-
opacity={layer.opacity
|
|
176
|
+
transform={layer.transform}
|
|
177
|
+
opacity={layer.opacity}
|
|
179
178
|
{canvasWidth}
|
|
180
179
|
{canvasHeight}
|
|
181
180
|
onTransformChange={onTransformChange}
|
|
@@ -446,23 +446,20 @@
|
|
|
446
446
|
{/if}
|
|
447
447
|
</div>
|
|
448
448
|
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
{
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
/>
|
|
464
|
-
{/if}
|
|
465
|
-
{/if}
|
|
449
|
+
{#if selectedLayer}
|
|
450
|
+
<LayerControls
|
|
451
|
+
layer={selectedLayer}
|
|
452
|
+
canvasWidth={mockup.composition.width}
|
|
453
|
+
canvasHeight={mockup.composition.height}
|
|
454
|
+
onTransformChange={(transform) => handleLayerTransform(selectedLayer.id, transform)}
|
|
455
|
+
onOpacityChange={(opacity) => handleLayerOpacityChange(selectedLayer.id, opacity)}
|
|
456
|
+
onVisibilityChange={(visible) => handleLayerVisibilityChange(selectedLayer.id, visible)}
|
|
457
|
+
onEffectsChange={(effects) => handleLayerEffectsChange(selectedLayer.id, effects)}
|
|
458
|
+
onNameChange={(name) => handleLayerNameChange(selectedLayer.id, name)}
|
|
459
|
+
onSrcChange={(src) => handleLayerSrcChange(selectedLayer.id, src)}
|
|
460
|
+
onDelete={() => handleDeleteLayer(selectedLayer.id)}
|
|
461
|
+
/>
|
|
462
|
+
{/if}
|
|
466
463
|
</div>
|
|
467
464
|
</sidebar>
|
|
468
465
|
<div class="output-canvas">
|
|
@@ -25,10 +25,13 @@
|
|
|
25
25
|
const defaults = {
|
|
26
26
|
x: 0,
|
|
27
27
|
y: 0,
|
|
28
|
+
scale: 1,
|
|
28
29
|
rotation: 0,
|
|
29
30
|
opacity: 1
|
|
30
31
|
};
|
|
31
32
|
|
|
33
|
+
const rotationValue = $derived(transform.rotation ?? defaults.rotation);
|
|
34
|
+
|
|
32
35
|
function updateTransform(updates: Partial<LayerTransform>) {
|
|
33
36
|
onTransformChange({ ...transform, ...updates });
|
|
34
37
|
}
|
|
@@ -92,14 +95,14 @@
|
|
|
92
95
|
step="1"
|
|
93
96
|
min="-360"
|
|
94
97
|
max="360"
|
|
95
|
-
|
|
98
|
+
value={rotationValue}
|
|
96
99
|
oninput={(e) => updateTransform({ rotation: Number(e.currentTarget.value) })}
|
|
97
100
|
/>
|
|
98
101
|
<input
|
|
99
102
|
type="number"
|
|
100
103
|
class="manual-input"
|
|
101
104
|
step="1"
|
|
102
|
-
|
|
105
|
+
value={rotationValue}
|
|
103
106
|
oninput={(e) => updateTransform({ rotation: Number(e.currentTarget.value) })}
|
|
104
107
|
/>
|
|
105
108
|
</ControlGroup>
|
|
@@ -37,8 +37,8 @@ export interface WarpEffect {
|
|
|
37
37
|
type: 'warp';
|
|
38
38
|
shape: WarpShape;
|
|
39
39
|
enabled: boolean;
|
|
40
|
-
quality
|
|
41
|
-
image
|
|
40
|
+
quality?: number;
|
|
41
|
+
image?: ImageOnSurface;
|
|
42
42
|
debug?: WarpDebug;
|
|
43
43
|
cylinder?: CylinderWarpParams;
|
|
44
44
|
plane?: PlaneWarpParams;
|
package/dist/mockup-normalize.js
CHANGED
|
@@ -29,22 +29,6 @@ function omitDefaults(obj, defaults) {
|
|
|
29
29
|
}
|
|
30
30
|
return result;
|
|
31
31
|
}
|
|
32
|
-
function omitDefaultsAlwaysInclude(obj, defaults, alwaysInclude) {
|
|
33
|
-
const result = omitDefaults(obj, defaults);
|
|
34
|
-
for (const key of alwaysInclude) {
|
|
35
|
-
if (obj.hasOwnProperty(key)) {
|
|
36
|
-
result[key] = obj[key];
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
return result;
|
|
40
|
-
}
|
|
41
|
-
const PX_SIZING_KEYS = {
|
|
42
|
-
resize: ['width', 'height'],
|
|
43
|
-
position: ['containerWidth', 'containerHeight'],
|
|
44
|
-
composition: ['width', 'height'],
|
|
45
|
-
cylinder: ['diameter'],
|
|
46
|
-
sphere: ['radius']
|
|
47
|
-
};
|
|
48
32
|
function normalizeTransform(transform) {
|
|
49
33
|
return omitDefaults(transform, defaultTransform);
|
|
50
34
|
}
|
|
@@ -53,11 +37,11 @@ function normalizeRestyleEffect(effect) {
|
|
|
53
37
|
return { type: 'restyle', ...normalized };
|
|
54
38
|
}
|
|
55
39
|
function normalizeResizeEffect(effect) {
|
|
56
|
-
const normalized =
|
|
40
|
+
const normalized = omitDefaults(effect, defaultResizeEffect);
|
|
57
41
|
return { type: 'resize', ...normalized };
|
|
58
42
|
}
|
|
59
43
|
function normalizePositionEffect(effect) {
|
|
60
|
-
const normalized =
|
|
44
|
+
const normalized = omitDefaults(effect, defaultPositionEffect);
|
|
61
45
|
return { type: 'position', ...normalized };
|
|
62
46
|
}
|
|
63
47
|
function normalizeSvgEffect(effect) {
|
|
@@ -91,10 +75,10 @@ function normalizeWarpEffect(effect) {
|
|
|
91
75
|
const shape = effect.shape || 'none';
|
|
92
76
|
if (shape !== 'none') {
|
|
93
77
|
if (shape === 'cylinder' && effect.cylinder) {
|
|
94
|
-
const cylinderNormalized =
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
78
|
+
const cylinderNormalized = omitDefaults(effect.cylinder, defaultCylinderWarp);
|
|
79
|
+
// diameter is required - always include it
|
|
80
|
+
cylinderNormalized.diameter = effect.cylinder.diameter;
|
|
81
|
+
normalized.cylinder = cylinderNormalized;
|
|
98
82
|
}
|
|
99
83
|
else if (shape === 'plane' && effect.plane) {
|
|
100
84
|
const planeNormalized = omitDefaults(effect.plane, defaultPlaneWarp);
|
|
@@ -103,10 +87,10 @@ function normalizeWarpEffect(effect) {
|
|
|
103
87
|
}
|
|
104
88
|
}
|
|
105
89
|
else if (shape === 'sphere' && effect.sphere) {
|
|
106
|
-
const sphereNormalized =
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
90
|
+
const sphereNormalized = omitDefaults(effect.sphere, defaultSphereWarp);
|
|
91
|
+
// radius is required - always include it
|
|
92
|
+
sphereNormalized.radius = effect.sphere.radius;
|
|
93
|
+
normalized.sphere = sphereNormalized;
|
|
110
94
|
}
|
|
111
95
|
}
|
|
112
96
|
if (Object.keys(normalized).length <= 2) {
|
|
@@ -138,16 +122,15 @@ function normalizeLayer(layer) {
|
|
|
138
122
|
if (layer.name) {
|
|
139
123
|
normalized.name = layer.name;
|
|
140
124
|
}
|
|
141
|
-
const
|
|
142
|
-
const transformNormalized = normalizeTransform(transform);
|
|
125
|
+
const transformNormalized = normalizeTransform(layer.transform);
|
|
143
126
|
if (Object.keys(transformNormalized).length > 0) {
|
|
144
127
|
normalized.transform = transformNormalized;
|
|
145
128
|
}
|
|
146
|
-
if (
|
|
147
|
-
normalized.opacity = layer.opacity
|
|
129
|
+
if (layer.opacity !== defaultLayer.opacity) {
|
|
130
|
+
normalized.opacity = layer.opacity;
|
|
148
131
|
}
|
|
149
|
-
if (
|
|
150
|
-
normalized.visible = layer.visible
|
|
132
|
+
if (layer.visible !== defaultLayer.visible) {
|
|
133
|
+
normalized.visible = layer.visible;
|
|
151
134
|
}
|
|
152
135
|
if (layer.effects.length > 0) {
|
|
153
136
|
const effectsNormalized = layer.effects
|
|
@@ -160,12 +143,11 @@ function normalizeLayer(layer) {
|
|
|
160
143
|
return normalized;
|
|
161
144
|
}
|
|
162
145
|
function normalizeComposition(composition) {
|
|
163
|
-
const normalized = {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
}
|
|
146
|
+
const normalized = {
|
|
147
|
+
width: composition.width,
|
|
148
|
+
height: composition.height,
|
|
149
|
+
layers: composition.layers.length > 0 ? composition.layers.map(normalizeLayer) : []
|
|
150
|
+
};
|
|
169
151
|
if (composition.effects && composition.effects.length > 0) {
|
|
170
152
|
normalized.effects = composition.effects;
|
|
171
153
|
}
|
|
@@ -177,13 +159,20 @@ function normalizeView(view) {
|
|
|
177
159
|
export function normalizeMockup(mockup, clean = false) {
|
|
178
160
|
const normalized = {
|
|
179
161
|
id: mockup.id,
|
|
180
|
-
name: mockup.name
|
|
162
|
+
name: mockup.name,
|
|
163
|
+
canvasSize: {
|
|
164
|
+
width: mockup.canvasSize.width,
|
|
165
|
+
height: mockup.canvasSize.height
|
|
166
|
+
}
|
|
181
167
|
};
|
|
182
168
|
if (mockup.composition) {
|
|
183
169
|
const compNormalized = normalizeComposition(mockup.composition);
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
170
|
+
normalized.composition = {
|
|
171
|
+
width: compNormalized.width,
|
|
172
|
+
height: compNormalized.height,
|
|
173
|
+
layers: compNormalized.layers,
|
|
174
|
+
effects: compNormalized.effects || []
|
|
175
|
+
};
|
|
187
176
|
}
|
|
188
177
|
if (mockup.view) {
|
|
189
178
|
const viewNormalized = normalizeView(mockup.view);
|
|
@@ -216,28 +205,6 @@ function mergeDefaults(obj, defaults) {
|
|
|
216
205
|
}
|
|
217
206
|
return result;
|
|
218
207
|
}
|
|
219
|
-
function mergeDefaultsExcluding(obj, defaults, excludeKeys) {
|
|
220
|
-
const result = { ...defaults };
|
|
221
|
-
for (const key in obj) {
|
|
222
|
-
if (obj.hasOwnProperty(key)) {
|
|
223
|
-
if (excludeKeys.includes(key)) {
|
|
224
|
-
result[key] = obj[key];
|
|
225
|
-
}
|
|
226
|
-
else if (typeof obj[key] === 'object' && obj[key] !== null && !Array.isArray(obj[key])) {
|
|
227
|
-
result[key] = mergeDefaults(obj[key], defaults[key]);
|
|
228
|
-
}
|
|
229
|
-
else {
|
|
230
|
-
result[key] = obj[key];
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
for (const key of excludeKeys) {
|
|
235
|
-
if (!obj.hasOwnProperty(key)) {
|
|
236
|
-
result[key] = undefined;
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
return result;
|
|
240
|
-
}
|
|
241
208
|
function denormalizeTransform(transform) {
|
|
242
209
|
return mergeDefaults(transform, defaultTransform);
|
|
243
210
|
}
|
|
@@ -245,48 +212,25 @@ function denormalizeRestyleEffect(effect) {
|
|
|
245
212
|
return mergeDefaults(effect, defaultRestyleEffect);
|
|
246
213
|
}
|
|
247
214
|
function denormalizeResizeEffect(effect) {
|
|
248
|
-
|
|
249
|
-
throw new Error('ResizeEffect requires explicit width and height (px sizing cannot use defaults)');
|
|
250
|
-
}
|
|
251
|
-
return mergeDefaultsExcluding(effect, defaultResizeEffect, [
|
|
252
|
-
...PX_SIZING_KEYS.resize,
|
|
253
|
-
'debug'
|
|
254
|
-
]);
|
|
215
|
+
return mergeDefaults(effect, defaultResizeEffect);
|
|
255
216
|
}
|
|
256
217
|
function denormalizePositionEffect(effect) {
|
|
257
|
-
|
|
258
|
-
effect.containerHeight === undefined) {
|
|
259
|
-
throw new Error('PositionEffect requires explicit containerWidth and containerHeight (px sizing cannot use defaults)');
|
|
260
|
-
}
|
|
261
|
-
return mergeDefaultsExcluding(effect, defaultPositionEffect, [
|
|
262
|
-
...PX_SIZING_KEYS.position,
|
|
263
|
-
'debug'
|
|
264
|
-
]);
|
|
218
|
+
return mergeDefaults(effect, defaultPositionEffect);
|
|
265
219
|
}
|
|
266
220
|
function denormalizeSvgEffect(effect) {
|
|
267
|
-
return
|
|
268
|
-
'strokeWidth',
|
|
269
|
-
'strokeColor',
|
|
270
|
-
'fillColor'
|
|
271
|
-
]);
|
|
221
|
+
return mergeDefaults(effect, defaultSvgEffect);
|
|
272
222
|
}
|
|
273
223
|
function denormalizeWarpEffect(effect) {
|
|
274
224
|
const denormalized = mergeDefaults(effect, defaultWarpEffect);
|
|
275
225
|
const shape = denormalized.shape || 'none';
|
|
276
|
-
if (shape === 'cylinder') {
|
|
277
|
-
|
|
278
|
-
throw new Error('WarpEffect cylinder shape requires explicit cylinder.diameter (px sizing cannot use defaults)');
|
|
279
|
-
}
|
|
280
|
-
denormalized.cylinder = mergeDefaultsExcluding(effect.cylinder, defaultCylinderWarp, PX_SIZING_KEYS.cylinder);
|
|
226
|
+
if (shape === 'cylinder' && !denormalized.cylinder) {
|
|
227
|
+
denormalized.cylinder = { ...defaultCylinderWarp };
|
|
281
228
|
}
|
|
282
|
-
|
|
229
|
+
if (shape === 'plane' && !denormalized.plane) {
|
|
283
230
|
denormalized.plane = { ...defaultPlaneWarp };
|
|
284
231
|
}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
throw new Error('WarpEffect sphere shape requires explicit sphere.radius (px sizing cannot use defaults)');
|
|
288
|
-
}
|
|
289
|
-
denormalized.sphere = mergeDefaultsExcluding(effect.sphere, defaultSphereWarp, PX_SIZING_KEYS.sphere);
|
|
232
|
+
if (shape === 'sphere' && !denormalized.sphere) {
|
|
233
|
+
denormalized.sphere = { ...defaultSphereWarp };
|
|
290
234
|
}
|
|
291
235
|
if (!denormalized.image) {
|
|
292
236
|
denormalized.image = { ...defaultImageOnSurface };
|
|
@@ -324,12 +268,9 @@ function denormalizeLayer(layer) {
|
|
|
324
268
|
return denormalized;
|
|
325
269
|
}
|
|
326
270
|
function denormalizeComposition(composition) {
|
|
327
|
-
if (composition.width === undefined || composition.height === undefined) {
|
|
328
|
-
throw new Error('Composition requires explicit width and height (px sizing cannot use defaults)');
|
|
329
|
-
}
|
|
330
271
|
return {
|
|
331
|
-
width: composition.width,
|
|
332
|
-
height: composition.height,
|
|
272
|
+
width: composition.width ?? defaultComposition.width,
|
|
273
|
+
height: composition.height ?? defaultComposition.height,
|
|
333
274
|
layers: (composition.layers || []).map(denormalizeLayer),
|
|
334
275
|
effects: composition.effects || []
|
|
335
276
|
};
|
|
@@ -338,11 +279,13 @@ function denormalizeView(view) {
|
|
|
338
279
|
return mergeDefaults(view, defaultView);
|
|
339
280
|
}
|
|
340
281
|
export function denormalizeMockup(mockup) {
|
|
282
|
+
const composition = denormalizeComposition(mockup.composition || {});
|
|
341
283
|
return {
|
|
342
284
|
id: mockup.id || crypto.randomUUID(),
|
|
343
285
|
name: mockup.name || 'Untitled Mockup',
|
|
344
286
|
canvasSize: mockup.canvasSize || DEFAULT_CANVAS_SIZE,
|
|
345
|
-
|
|
287
|
+
layers: composition.layers,
|
|
288
|
+
composition: composition,
|
|
346
289
|
view: denormalizeView(mockup.view || {}),
|
|
347
290
|
createdAt: mockup.createdAt || Date.now(),
|
|
348
291
|
updatedAt: mockup.updatedAt || Date.now()
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { defaultTransform } from './types';
|
|
2
1
|
import { applyWarp, processLayerImage } from './image-transformations';
|
|
3
2
|
import { processSvg, verifySvgSource } from './image-transformations/svg';
|
|
4
3
|
import { RenderScheduler } from './RenderScheduler';
|
|
@@ -90,8 +89,7 @@ export function renderMockupCanvas(canvasEl, config, overrides = {}, onRenderTim
|
|
|
90
89
|
const warpKey = warpEffect ? `${warpEffect.enabled}:${warpEffect.shape}` : 'none';
|
|
91
90
|
const restyleKey = restyleEffect ? JSON.stringify(restyleEffect) : 'none';
|
|
92
91
|
const svgKey = svgEffect ? JSON.stringify(svgEffect) : 'none';
|
|
93
|
-
|
|
94
|
-
return `${l.id}:${t.x},${t.y},${t.scale},${t.rotation},${l.opacity ?? 1},${l.visible ?? true}:${warpKey}:${restyleKey}:${svgKey}`;
|
|
92
|
+
return `${l.id}:${l.transform.x},${l.transform.y},${l.transform.scale ?? 1},${l.transform.rotation ?? 0},${l.opacity},${l.visible}:${warpKey}:${restyleKey}:${svgKey}`;
|
|
95
93
|
})
|
|
96
94
|
.join('|');
|
|
97
95
|
if (canvasEl.width !== canvas.width || canvasEl.height !== canvas.height) {
|
|
@@ -304,7 +302,7 @@ export function renderMockupCanvas(canvasEl, config, overrides = {}, onRenderTim
|
|
|
304
302
|
const processedImages = [];
|
|
305
303
|
let hasInvalidBitmaps = false;
|
|
306
304
|
for (const layer of canvas.layers) {
|
|
307
|
-
if (layer.visible
|
|
305
|
+
if (!layer.visible)
|
|
308
306
|
continue;
|
|
309
307
|
const cacheKey = getCacheKey(layer);
|
|
310
308
|
const img = state.loadedImages.get(cacheKey);
|
|
@@ -319,9 +317,8 @@ export function renderMockupCanvas(canvasEl, config, overrides = {}, onRenderTim
|
|
|
319
317
|
const warpEffect = layer.effects.find((e) => e.type === 'warp');
|
|
320
318
|
let displayWidth = processed.width;
|
|
321
319
|
let displayHeight = processed.height;
|
|
322
|
-
const
|
|
323
|
-
const
|
|
324
|
-
const scaledHeight = displayHeight * t.scale;
|
|
320
|
+
const scaledWidth = displayWidth * (layer.transform.scale ?? 1);
|
|
321
|
+
const scaledHeight = displayHeight * (layer.transform.scale ?? 1);
|
|
325
322
|
let processedBitmap;
|
|
326
323
|
if (processed.image instanceof ImageBitmap) {
|
|
327
324
|
processedBitmap = processed.image;
|
|
@@ -350,14 +347,14 @@ export function renderMockupCanvas(canvasEl, config, overrides = {}, onRenderTim
|
|
|
350
347
|
} : null;
|
|
351
348
|
layers.push({
|
|
352
349
|
id: String(layer.id),
|
|
353
|
-
x: Number(
|
|
354
|
-
y: Number(
|
|
350
|
+
x: Number(layer.transform.x),
|
|
351
|
+
y: Number(layer.transform.y),
|
|
355
352
|
offsetX: Number(processed.offsetX),
|
|
356
353
|
offsetY: Number(processed.offsetY),
|
|
357
354
|
width: Number(scaledWidth),
|
|
358
355
|
height: Number(scaledHeight),
|
|
359
|
-
rotation: Number(
|
|
360
|
-
opacity: Number(layer.opacity
|
|
356
|
+
rotation: Number(layer.transform.rotation ?? 0),
|
|
357
|
+
opacity: Number(layer.opacity),
|
|
361
358
|
warp,
|
|
362
359
|
position
|
|
363
360
|
});
|
|
@@ -392,7 +389,7 @@ export function renderMockupCanvas(canvasEl, config, overrides = {}, onRenderTim
|
|
|
392
389
|
state.ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
393
390
|
const layerTimes = [];
|
|
394
391
|
for (const layer of canvas.layers) {
|
|
395
|
-
if (layer.visible
|
|
392
|
+
if (!layer.visible)
|
|
396
393
|
continue;
|
|
397
394
|
const layerStartTime = performance.now();
|
|
398
395
|
const cacheKey = getCacheKey(layer);
|
|
@@ -409,10 +406,9 @@ export function renderMockupCanvas(canvasEl, config, overrides = {}, onRenderTim
|
|
|
409
406
|
let displayWidth = processed.width;
|
|
410
407
|
let displayHeight = processed.height;
|
|
411
408
|
state.ctx.save();
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
const
|
|
415
|
-
const scaledHeight = displayHeight * t.scale;
|
|
409
|
+
state.ctx.globalAlpha = layer.opacity;
|
|
410
|
+
const scaledWidth = displayWidth * (layer.transform.scale ?? 1);
|
|
411
|
+
const scaledHeight = displayHeight * (layer.transform.scale ?? 1);
|
|
416
412
|
// Calculate image top-left position (centered on canvas by default)
|
|
417
413
|
let imgLeft = (canvas.width - scaledWidth) / 2;
|
|
418
414
|
let imgTop = (canvas.height - scaledHeight) / 2;
|
|
@@ -424,13 +420,13 @@ export function renderMockupCanvas(canvasEl, config, overrides = {}, onRenderTim
|
|
|
424
420
|
imgTop = containerTop + processed.offsetY;
|
|
425
421
|
}
|
|
426
422
|
// Apply layer transform offset
|
|
427
|
-
imgLeft +=
|
|
428
|
-
imgTop +=
|
|
423
|
+
imgLeft += layer.transform.x;
|
|
424
|
+
imgTop += layer.transform.y;
|
|
429
425
|
// Calculate center point for rotation
|
|
430
426
|
const centerX = imgLeft + scaledWidth / 2;
|
|
431
427
|
const centerY = imgTop + scaledHeight / 2;
|
|
432
428
|
state.ctx.translate(centerX, centerY);
|
|
433
|
-
state.ctx.rotate((
|
|
429
|
+
state.ctx.rotate(((layer.transform.rotation ?? 0) * Math.PI) / 180);
|
|
434
430
|
try {
|
|
435
431
|
if (warpEffect && warpEffect.enabled && warpEffect.shape !== 'none') {
|
|
436
432
|
applyWarp(state.ctx, processed.image, 0, 0, scaledWidth, scaledHeight, warpEffect);
|
package/dist/types.d.ts
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
export interface LayerTransform {
|
|
2
|
-
x
|
|
3
|
-
y
|
|
4
|
-
scale
|
|
5
|
-
rotation
|
|
2
|
+
x?: number;
|
|
3
|
+
y?: number;
|
|
4
|
+
scale?: number;
|
|
5
|
+
rotation?: number;
|
|
6
6
|
}
|
|
7
7
|
import type { LayerEffect, CanvasEffect } from './image-transformations/types';
|
|
8
8
|
export interface Layer {
|
|
9
9
|
id: string;
|
|
10
10
|
src: string;
|
|
11
11
|
name?: string;
|
|
12
|
-
transform
|
|
12
|
+
transform: LayerTransform;
|
|
13
13
|
opacity?: number;
|
|
14
14
|
visible?: boolean;
|
|
15
|
-
effects
|
|
15
|
+
effects?: LayerEffect[];
|
|
16
16
|
}
|
|
17
17
|
export interface CanvasSize {
|
|
18
18
|
width: number;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "svelte-product-mockup",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.11",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.js",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
|
31
31
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
|
32
32
|
"prepublishOnly": "pnpm run build:package",
|
|
33
|
-
"publish": "
|
|
33
|
+
"publish": "sh -c 'set -a && . ../.env && set +a && npm publish'"
|
|
34
34
|
},
|
|
35
35
|
"peerDependencies": {
|
|
36
36
|
"svelte": "^5.0.0"
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { Layer, LayerTransform } from '$lib/types';
|
|
3
|
-
import { defaultTransform } from '$lib/types';
|
|
4
3
|
import type { ResizeEffect, PositionEffect, WarpEffect, RestyleEffect, SvgEffect } from '$lib/image-transformations/types';
|
|
5
4
|
import { defaultRestyleEffect } from '$lib/image-transformations/types';
|
|
6
5
|
import { verifySvgSource } from '$lib/image-transformations/svg';
|
|
@@ -121,10 +120,10 @@
|
|
|
121
120
|
<div class="layer-header">
|
|
122
121
|
<button
|
|
123
122
|
class="visibility-toggle"
|
|
124
|
-
onclick={() => onVisibilityChange(!
|
|
125
|
-
aria-label={
|
|
123
|
+
onclick={() => onVisibilityChange(!layer.visible)}
|
|
124
|
+
aria-label={layer.visible ? 'Hide layer' : 'Show layer'}
|
|
126
125
|
>
|
|
127
|
-
{#if layer.visible
|
|
126
|
+
{#if layer.visible}
|
|
128
127
|
<EyeOpenIcon />
|
|
129
128
|
{:else}
|
|
130
129
|
<EyeClosedIcon />
|
|
@@ -174,8 +173,8 @@
|
|
|
174
173
|
/>
|
|
175
174
|
<RestyleSection effect={restyleEffect} onChange={handleRestyleChange} />
|
|
176
175
|
<TransformSection
|
|
177
|
-
transform={layer.transform
|
|
178
|
-
opacity={layer.opacity
|
|
176
|
+
transform={layer.transform}
|
|
177
|
+
opacity={layer.opacity}
|
|
179
178
|
{canvasWidth}
|
|
180
179
|
{canvasHeight}
|
|
181
180
|
onTransformChange={onTransformChange}
|
|
@@ -446,23 +446,20 @@
|
|
|
446
446
|
{/if}
|
|
447
447
|
</div>
|
|
448
448
|
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
{
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
/>
|
|
464
|
-
{/if}
|
|
465
|
-
{/if}
|
|
449
|
+
{#if selectedLayer}
|
|
450
|
+
<LayerControls
|
|
451
|
+
layer={selectedLayer}
|
|
452
|
+
canvasWidth={mockup.composition.width}
|
|
453
|
+
canvasHeight={mockup.composition.height}
|
|
454
|
+
onTransformChange={(transform) => handleLayerTransform(selectedLayer.id, transform)}
|
|
455
|
+
onOpacityChange={(opacity) => handleLayerOpacityChange(selectedLayer.id, opacity)}
|
|
456
|
+
onVisibilityChange={(visible) => handleLayerVisibilityChange(selectedLayer.id, visible)}
|
|
457
|
+
onEffectsChange={(effects) => handleLayerEffectsChange(selectedLayer.id, effects)}
|
|
458
|
+
onNameChange={(name) => handleLayerNameChange(selectedLayer.id, name)}
|
|
459
|
+
onSrcChange={(src) => handleLayerSrcChange(selectedLayer.id, src)}
|
|
460
|
+
onDelete={() => handleDeleteLayer(selectedLayer.id)}
|
|
461
|
+
/>
|
|
462
|
+
{/if}
|
|
466
463
|
</div>
|
|
467
464
|
</sidebar>
|
|
468
465
|
<div class="output-canvas">
|
|
@@ -25,10 +25,13 @@
|
|
|
25
25
|
const defaults = {
|
|
26
26
|
x: 0,
|
|
27
27
|
y: 0,
|
|
28
|
+
scale: 1,
|
|
28
29
|
rotation: 0,
|
|
29
30
|
opacity: 1
|
|
30
31
|
};
|
|
31
32
|
|
|
33
|
+
const rotationValue = $derived(transform.rotation ?? defaults.rotation);
|
|
34
|
+
|
|
32
35
|
function updateTransform(updates: Partial<LayerTransform>) {
|
|
33
36
|
onTransformChange({ ...transform, ...updates });
|
|
34
37
|
}
|
|
@@ -92,14 +95,14 @@
|
|
|
92
95
|
step="1"
|
|
93
96
|
min="-360"
|
|
94
97
|
max="360"
|
|
95
|
-
|
|
98
|
+
value={rotationValue}
|
|
96
99
|
oninput={(e) => updateTransform({ rotation: Number(e.currentTarget.value) })}
|
|
97
100
|
/>
|
|
98
101
|
<input
|
|
99
102
|
type="number"
|
|
100
103
|
class="manual-input"
|
|
101
104
|
step="1"
|
|
102
|
-
|
|
105
|
+
value={rotationValue}
|
|
103
106
|
oninput={(e) => updateTransform({ rotation: Number(e.currentTarget.value) })}
|
|
104
107
|
/>
|
|
105
108
|
</ControlGroup>
|
|
@@ -44,8 +44,8 @@ export interface WarpEffect {
|
|
|
44
44
|
type: 'warp';
|
|
45
45
|
shape: WarpShape;
|
|
46
46
|
enabled: boolean;
|
|
47
|
-
quality
|
|
48
|
-
image
|
|
47
|
+
quality?: number;
|
|
48
|
+
image?: ImageOnSurface;
|
|
49
49
|
debug?: WarpDebug;
|
|
50
50
|
cylinder?: CylinderWarpParams;
|
|
51
51
|
plane?: PlaneWarpParams;
|
|
@@ -58,28 +58,6 @@ function omitDefaults<T extends Record<string, any>>(obj: T, defaults: Partial<T
|
|
|
58
58
|
return result;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
function omitDefaultsAlwaysInclude<T extends Record<string, any>>(
|
|
62
|
-
obj: T,
|
|
63
|
-
defaults: Partial<T>,
|
|
64
|
-
alwaysInclude: (keyof T)[]
|
|
65
|
-
): Partial<T> {
|
|
66
|
-
const result = omitDefaults(obj, defaults);
|
|
67
|
-
for (const key of alwaysInclude) {
|
|
68
|
-
if (obj.hasOwnProperty(key)) {
|
|
69
|
-
result[key] = obj[key];
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
return result;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const PX_SIZING_KEYS = {
|
|
76
|
-
resize: ['width', 'height'] as const,
|
|
77
|
-
position: ['containerWidth', 'containerHeight'] as const,
|
|
78
|
-
composition: ['width', 'height'] as const,
|
|
79
|
-
cylinder: ['diameter'] as const,
|
|
80
|
-
sphere: ['radius'] as const
|
|
81
|
-
};
|
|
82
|
-
|
|
83
61
|
function normalizeTransform(transform: LayerTransform): Partial<LayerTransform> {
|
|
84
62
|
return omitDefaults(transform, defaultTransform);
|
|
85
63
|
}
|
|
@@ -90,12 +68,12 @@ function normalizeRestyleEffect(effect: RestyleEffect): Partial<RestyleEffect> {
|
|
|
90
68
|
}
|
|
91
69
|
|
|
92
70
|
function normalizeResizeEffect(effect: ResizeEffect): Partial<ResizeEffect> {
|
|
93
|
-
const normalized =
|
|
71
|
+
const normalized = omitDefaults(effect, defaultResizeEffect);
|
|
94
72
|
return { type: 'resize', ...normalized };
|
|
95
73
|
}
|
|
96
74
|
|
|
97
75
|
function normalizePositionEffect(effect: PositionEffect): Partial<PositionEffect> {
|
|
98
|
-
const normalized =
|
|
76
|
+
const normalized = omitDefaults(effect, defaultPositionEffect);
|
|
99
77
|
return { type: 'position', ...normalized };
|
|
100
78
|
}
|
|
101
79
|
|
|
@@ -137,28 +115,20 @@ function normalizeWarpEffect(effect: WarpEffect): Partial<WarpEffect> | null {
|
|
|
137
115
|
const shape = effect.shape || 'none';
|
|
138
116
|
if (shape !== 'none') {
|
|
139
117
|
if (shape === 'cylinder' && effect.cylinder) {
|
|
140
|
-
const cylinderNormalized =
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
);
|
|
145
|
-
if (Object.keys(cylinderNormalized).length > 0) {
|
|
146
|
-
normalized.cylinder = cylinderNormalized as typeof effect.cylinder;
|
|
147
|
-
}
|
|
118
|
+
const cylinderNormalized = omitDefaults(effect.cylinder, defaultCylinderWarp);
|
|
119
|
+
// diameter is required - always include it
|
|
120
|
+
cylinderNormalized.diameter = effect.cylinder.diameter;
|
|
121
|
+
normalized.cylinder = cylinderNormalized as typeof effect.cylinder;
|
|
148
122
|
} else if (shape === 'plane' && effect.plane) {
|
|
149
123
|
const planeNormalized = omitDefaults(effect.plane, defaultPlaneWarp);
|
|
150
124
|
if (Object.keys(planeNormalized).length > 0) {
|
|
151
125
|
normalized.plane = planeNormalized as typeof effect.plane;
|
|
152
126
|
}
|
|
153
127
|
} else if (shape === 'sphere' && effect.sphere) {
|
|
154
|
-
const sphereNormalized =
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
);
|
|
159
|
-
if (Object.keys(sphereNormalized).length > 0) {
|
|
160
|
-
normalized.sphere = sphereNormalized as typeof effect.sphere;
|
|
161
|
-
}
|
|
128
|
+
const sphereNormalized = omitDefaults(effect.sphere, defaultSphereWarp);
|
|
129
|
+
// radius is required - always include it
|
|
130
|
+
sphereNormalized.radius = effect.sphere.radius;
|
|
131
|
+
normalized.sphere = sphereNormalized as typeof effect.sphere;
|
|
162
132
|
}
|
|
163
133
|
}
|
|
164
134
|
|
|
@@ -196,18 +166,17 @@ function normalizeLayer(layer: Layer): Partial<Layer> {
|
|
|
196
166
|
normalized.name = layer.name;
|
|
197
167
|
}
|
|
198
168
|
|
|
199
|
-
const
|
|
200
|
-
const transformNormalized = normalizeTransform(transform);
|
|
169
|
+
const transformNormalized = normalizeTransform(layer.transform);
|
|
201
170
|
if (Object.keys(transformNormalized).length > 0) {
|
|
202
171
|
normalized.transform = transformNormalized as LayerTransform;
|
|
203
172
|
}
|
|
204
173
|
|
|
205
|
-
if (
|
|
206
|
-
normalized.opacity = layer.opacity
|
|
174
|
+
if (layer.opacity !== defaultLayer.opacity) {
|
|
175
|
+
normalized.opacity = layer.opacity;
|
|
207
176
|
}
|
|
208
177
|
|
|
209
|
-
if (
|
|
210
|
-
normalized.visible = layer.visible
|
|
178
|
+
if (layer.visible !== defaultLayer.visible) {
|
|
179
|
+
normalized.visible = layer.visible;
|
|
211
180
|
}
|
|
212
181
|
|
|
213
182
|
if (layer.effects.length > 0) {
|
|
@@ -215,22 +184,19 @@ function normalizeLayer(layer: Layer): Partial<Layer> {
|
|
|
215
184
|
.map(normalizeLayerEffect)
|
|
216
185
|
.filter((e): e is Partial<LayerEffect> => e !== null);
|
|
217
186
|
if (effectsNormalized.length > 0) {
|
|
218
|
-
normalized.effects = effectsNormalized as
|
|
187
|
+
normalized.effects = effectsNormalized as any;
|
|
219
188
|
}
|
|
220
189
|
}
|
|
221
190
|
|
|
222
191
|
return normalized;
|
|
223
192
|
}
|
|
224
193
|
|
|
225
|
-
function normalizeComposition(composition: Composition): Partial<Composition> {
|
|
226
|
-
const normalized: Partial<Composition> = {
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
if (composition.layers.length > 0) {
|
|
232
|
-
normalized.layers = composition.layers.map(normalizeLayer);
|
|
233
|
-
}
|
|
194
|
+
function normalizeComposition(composition: Composition): Partial<Composition> & { width: number; height: number; layers: Layer[] } {
|
|
195
|
+
const normalized: Partial<Composition> & { width: number; height: number; layers: Layer[] } = {
|
|
196
|
+
width: composition.width,
|
|
197
|
+
height: composition.height,
|
|
198
|
+
layers: composition.layers.length > 0 ? composition.layers.map(normalizeLayer) as Layer[] : []
|
|
199
|
+
};
|
|
234
200
|
|
|
235
201
|
if (composition.effects && composition.effects.length > 0) {
|
|
236
202
|
normalized.effects = composition.effects;
|
|
@@ -246,14 +212,21 @@ function normalizeView(view: ViewSettings): Partial<ViewSettings> {
|
|
|
246
212
|
export function normalizeMockup(mockup: Mockup, clean: boolean = false): Partial<Mockup> {
|
|
247
213
|
const normalized: Partial<Mockup> = {
|
|
248
214
|
id: mockup.id,
|
|
249
|
-
name: mockup.name
|
|
215
|
+
name: mockup.name,
|
|
216
|
+
canvasSize: {
|
|
217
|
+
width: mockup.canvasSize.width,
|
|
218
|
+
height: mockup.canvasSize.height
|
|
219
|
+
}
|
|
250
220
|
};
|
|
251
221
|
|
|
252
222
|
if (mockup.composition) {
|
|
253
223
|
const compNormalized = normalizeComposition(mockup.composition);
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
224
|
+
normalized.composition = {
|
|
225
|
+
width: compNormalized.width,
|
|
226
|
+
height: compNormalized.height,
|
|
227
|
+
layers: compNormalized.layers,
|
|
228
|
+
effects: compNormalized.effects || []
|
|
229
|
+
};
|
|
257
230
|
}
|
|
258
231
|
|
|
259
232
|
if (mockup.view) {
|
|
@@ -290,31 +263,6 @@ function mergeDefaults<T extends Record<string, any>>(obj: Partial<T>, defaults:
|
|
|
290
263
|
return result;
|
|
291
264
|
}
|
|
292
265
|
|
|
293
|
-
function mergeDefaultsExcluding<T extends Record<string, any>>(
|
|
294
|
-
obj: Partial<T>,
|
|
295
|
-
defaults: T,
|
|
296
|
-
excludeKeys: (keyof T)[]
|
|
297
|
-
): T {
|
|
298
|
-
const result = { ...defaults };
|
|
299
|
-
for (const key in obj) {
|
|
300
|
-
if (obj.hasOwnProperty(key)) {
|
|
301
|
-
if (excludeKeys.includes(key)) {
|
|
302
|
-
result[key] = obj[key] as T[Extract<keyof T, string>];
|
|
303
|
-
} else if (typeof obj[key] === 'object' && obj[key] !== null && !Array.isArray(obj[key])) {
|
|
304
|
-
result[key] = mergeDefaults(obj[key] as Partial<T[Extract<keyof T, string>]>, defaults[key]);
|
|
305
|
-
} else {
|
|
306
|
-
result[key] = obj[key] as T[Extract<keyof T, string>];
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
for (const key of excludeKeys) {
|
|
311
|
-
if (!obj.hasOwnProperty(key)) {
|
|
312
|
-
(result as Record<string, unknown>)[key] = undefined;
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
return result;
|
|
316
|
-
}
|
|
317
|
-
|
|
318
266
|
function denormalizeTransform(transform: Partial<LayerTransform>): LayerTransform {
|
|
319
267
|
return mergeDefaults(transform, defaultTransform);
|
|
320
268
|
}
|
|
@@ -324,62 +272,29 @@ function denormalizeRestyleEffect(effect: Partial<RestyleEffect>): RestyleEffect
|
|
|
324
272
|
}
|
|
325
273
|
|
|
326
274
|
function denormalizeResizeEffect(effect: Partial<ResizeEffect>): ResizeEffect {
|
|
327
|
-
|
|
328
|
-
throw new Error('ResizeEffect requires explicit width and height (px sizing cannot use defaults)');
|
|
329
|
-
}
|
|
330
|
-
return mergeDefaultsExcluding(effect, defaultResizeEffect, [
|
|
331
|
-
...PX_SIZING_KEYS.resize,
|
|
332
|
-
'debug'
|
|
333
|
-
]);
|
|
275
|
+
return mergeDefaults(effect, defaultResizeEffect);
|
|
334
276
|
}
|
|
335
277
|
|
|
336
278
|
function denormalizePositionEffect(effect: Partial<PositionEffect>): PositionEffect {
|
|
337
|
-
|
|
338
|
-
effect.containerWidth === undefined ||
|
|
339
|
-
effect.containerHeight === undefined
|
|
340
|
-
) {
|
|
341
|
-
throw new Error(
|
|
342
|
-
'PositionEffect requires explicit containerWidth and containerHeight (px sizing cannot use defaults)'
|
|
343
|
-
);
|
|
344
|
-
}
|
|
345
|
-
return mergeDefaultsExcluding(effect, defaultPositionEffect, [
|
|
346
|
-
...PX_SIZING_KEYS.position,
|
|
347
|
-
'debug'
|
|
348
|
-
]);
|
|
279
|
+
return mergeDefaults(effect, defaultPositionEffect);
|
|
349
280
|
}
|
|
350
281
|
|
|
351
282
|
function denormalizeSvgEffect(effect: Partial<SvgEffect>): SvgEffect {
|
|
352
|
-
return
|
|
353
|
-
'strokeWidth',
|
|
354
|
-
'strokeColor',
|
|
355
|
-
'fillColor'
|
|
356
|
-
]);
|
|
283
|
+
return mergeDefaults(effect, defaultSvgEffect);
|
|
357
284
|
}
|
|
358
285
|
|
|
359
286
|
function denormalizeWarpEffect(effect: Partial<WarpEffect>): WarpEffect {
|
|
360
287
|
const denormalized = mergeDefaults(effect, defaultWarpEffect);
|
|
288
|
+
|
|
361
289
|
const shape = denormalized.shape || 'none';
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
}
|
|
367
|
-
denormalized.cylinder = mergeDefaultsExcluding(
|
|
368
|
-
effect.cylinder,
|
|
369
|
-
defaultCylinderWarp,
|
|
370
|
-
PX_SIZING_KEYS.cylinder
|
|
371
|
-
);
|
|
372
|
-
} else if (shape === 'plane' && !denormalized.plane) {
|
|
290
|
+
if (shape === 'cylinder' && !denormalized.cylinder) {
|
|
291
|
+
denormalized.cylinder = { ...defaultCylinderWarp };
|
|
292
|
+
}
|
|
293
|
+
if (shape === 'plane' && !denormalized.plane) {
|
|
373
294
|
denormalized.plane = { ...defaultPlaneWarp };
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
}
|
|
378
|
-
denormalized.sphere = mergeDefaultsExcluding(
|
|
379
|
-
effect.sphere,
|
|
380
|
-
defaultSphereWarp,
|
|
381
|
-
PX_SIZING_KEYS.sphere
|
|
382
|
-
);
|
|
295
|
+
}
|
|
296
|
+
if (shape === 'sphere' && !denormalized.sphere) {
|
|
297
|
+
denormalized.sphere = { ...defaultSphereWarp };
|
|
383
298
|
}
|
|
384
299
|
if (!denormalized.image) {
|
|
385
300
|
denormalized.image = { ...defaultImageOnSurface };
|
|
@@ -423,12 +338,9 @@ function denormalizeLayer(layer: Partial<Layer>): Layer {
|
|
|
423
338
|
}
|
|
424
339
|
|
|
425
340
|
function denormalizeComposition(composition: Partial<Composition>): Composition {
|
|
426
|
-
if (composition.width === undefined || composition.height === undefined) {
|
|
427
|
-
throw new Error('Composition requires explicit width and height (px sizing cannot use defaults)');
|
|
428
|
-
}
|
|
429
341
|
return {
|
|
430
|
-
width: composition.width,
|
|
431
|
-
height: composition.height,
|
|
342
|
+
width: composition.width ?? defaultComposition.width,
|
|
343
|
+
height: composition.height ?? defaultComposition.height,
|
|
432
344
|
layers: (composition.layers || []).map(denormalizeLayer),
|
|
433
345
|
effects: composition.effects || []
|
|
434
346
|
};
|
|
@@ -439,11 +351,13 @@ function denormalizeView(view: Partial<ViewSettings>): ViewSettings {
|
|
|
439
351
|
}
|
|
440
352
|
|
|
441
353
|
export function denormalizeMockup(mockup: Partial<Mockup>): Mockup {
|
|
354
|
+
const composition = denormalizeComposition(mockup.composition || {});
|
|
442
355
|
return {
|
|
443
356
|
id: mockup.id || crypto.randomUUID(),
|
|
444
357
|
name: mockup.name || 'Untitled Mockup',
|
|
445
358
|
canvasSize: mockup.canvasSize || DEFAULT_CANVAS_SIZE,
|
|
446
|
-
|
|
359
|
+
layers: composition.layers,
|
|
360
|
+
composition: composition,
|
|
447
361
|
view: denormalizeView(mockup.view || {}),
|
|
448
362
|
createdAt: mockup.createdAt || Date.now(),
|
|
449
363
|
updatedAt: mockup.updatedAt || Date.now()
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { Composition, Layer } from '$lib/types';
|
|
2
|
-
import { defaultTransform } from '$lib/types';
|
|
3
2
|
import { applyWarp, processLayerImage } from '$lib/image-transformations';
|
|
4
3
|
import type { WarpEffect, RestyleEffect, SvgEffect, PositionEffect } from '$lib/image-transformations/types';
|
|
5
4
|
import { processSvg, verifySvgSource } from '$lib/image-transformations/svg';
|
|
@@ -120,8 +119,7 @@ export function renderMockupCanvas(
|
|
|
120
119
|
const warpKey = warpEffect ? `${warpEffect.enabled}:${warpEffect.shape}` : 'none';
|
|
121
120
|
const restyleKey = restyleEffect ? JSON.stringify(restyleEffect) : 'none';
|
|
122
121
|
const svgKey = svgEffect ? JSON.stringify(svgEffect) : 'none';
|
|
123
|
-
|
|
124
|
-
return `${l.id}:${t.x},${t.y},${t.scale},${t.rotation},${l.opacity ?? 1},${l.visible ?? true}:${warpKey}:${restyleKey}:${svgKey}`;
|
|
122
|
+
return `${l.id}:${l.transform.x},${l.transform.y},${l.transform.scale ?? 1},${l.transform.rotation ?? 0},${l.opacity},${l.visible}:${warpKey}:${restyleKey}:${svgKey}`;
|
|
125
123
|
})
|
|
126
124
|
.join('|');
|
|
127
125
|
|
|
@@ -343,7 +341,7 @@ export function renderMockupCanvas(
|
|
|
343
341
|
let hasInvalidBitmaps = false;
|
|
344
342
|
|
|
345
343
|
for (const layer of canvas.layers) {
|
|
346
|
-
if (layer.visible
|
|
344
|
+
if (!layer.visible) continue;
|
|
347
345
|
|
|
348
346
|
const cacheKey = getCacheKey(layer);
|
|
349
347
|
const img = state.loadedImages.get(cacheKey);
|
|
@@ -360,9 +358,8 @@ export function renderMockupCanvas(
|
|
|
360
358
|
let displayWidth = processed.width;
|
|
361
359
|
let displayHeight = processed.height;
|
|
362
360
|
|
|
363
|
-
const
|
|
364
|
-
const
|
|
365
|
-
const scaledHeight = displayHeight * t.scale;
|
|
361
|
+
const scaledWidth = displayWidth * (layer.transform.scale ?? 1);
|
|
362
|
+
const scaledHeight = displayHeight * (layer.transform.scale ?? 1);
|
|
366
363
|
|
|
367
364
|
let processedBitmap: ImageBitmap;
|
|
368
365
|
if (processed.image instanceof ImageBitmap) {
|
|
@@ -393,14 +390,14 @@ export function renderMockupCanvas(
|
|
|
393
390
|
|
|
394
391
|
layers.push({
|
|
395
392
|
id: String(layer.id),
|
|
396
|
-
x: Number(
|
|
397
|
-
y: Number(
|
|
393
|
+
x: Number(layer.transform.x),
|
|
394
|
+
y: Number(layer.transform.y),
|
|
398
395
|
offsetX: Number(processed.offsetX),
|
|
399
396
|
offsetY: Number(processed.offsetY),
|
|
400
397
|
width: Number(scaledWidth),
|
|
401
398
|
height: Number(scaledHeight),
|
|
402
|
-
rotation: Number(
|
|
403
|
-
opacity: Number(layer.opacity
|
|
399
|
+
rotation: Number(layer.transform.rotation ?? 0),
|
|
400
|
+
opacity: Number(layer.opacity),
|
|
404
401
|
warp,
|
|
405
402
|
position
|
|
406
403
|
});
|
|
@@ -441,7 +438,7 @@ export function renderMockupCanvas(
|
|
|
441
438
|
|
|
442
439
|
const layerTimes: number[] = [];
|
|
443
440
|
for (const layer of canvas.layers) {
|
|
444
|
-
if (layer.visible
|
|
441
|
+
if (!layer.visible) continue;
|
|
445
442
|
|
|
446
443
|
const layerStartTime = performance.now();
|
|
447
444
|
const cacheKey = getCacheKey(layer);
|
|
@@ -460,11 +457,10 @@ export function renderMockupCanvas(
|
|
|
460
457
|
let displayHeight = processed.height;
|
|
461
458
|
|
|
462
459
|
state.ctx.save();
|
|
463
|
-
|
|
464
|
-
state.ctx.globalAlpha = layer.opacity ?? 1;
|
|
460
|
+
state.ctx.globalAlpha = layer.opacity;
|
|
465
461
|
|
|
466
|
-
const scaledWidth = displayWidth *
|
|
467
|
-
const scaledHeight = displayHeight *
|
|
462
|
+
const scaledWidth = displayWidth * (layer.transform.scale ?? 1);
|
|
463
|
+
const scaledHeight = displayHeight * (layer.transform.scale ?? 1);
|
|
468
464
|
|
|
469
465
|
// Calculate image top-left position (centered on canvas by default)
|
|
470
466
|
let imgLeft = (canvas.width - scaledWidth) / 2;
|
|
@@ -479,15 +475,15 @@ export function renderMockupCanvas(
|
|
|
479
475
|
}
|
|
480
476
|
|
|
481
477
|
// Apply layer transform offset
|
|
482
|
-
imgLeft +=
|
|
483
|
-
imgTop +=
|
|
478
|
+
imgLeft += layer.transform.x;
|
|
479
|
+
imgTop += layer.transform.y;
|
|
484
480
|
|
|
485
481
|
// Calculate center point for rotation
|
|
486
482
|
const centerX = imgLeft + scaledWidth / 2;
|
|
487
483
|
const centerY = imgTop + scaledHeight / 2;
|
|
488
484
|
|
|
489
485
|
state.ctx.translate(centerX, centerY);
|
|
490
|
-
state.ctx.rotate((
|
|
486
|
+
state.ctx.rotate(((layer.transform.rotation ?? 0) * Math.PI) / 180);
|
|
491
487
|
|
|
492
488
|
try {
|
|
493
489
|
if (warpEffect && warpEffect.enabled && warpEffect.shape !== 'none') {
|
package/src/lib/types.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
export interface LayerTransform {
|
|
2
|
-
x
|
|
3
|
-
y
|
|
4
|
-
scale
|
|
5
|
-
rotation
|
|
2
|
+
x?: number;
|
|
3
|
+
y?: number;
|
|
4
|
+
scale?: number;
|
|
5
|
+
rotation?: number;
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
import type { LayerEffect, CanvasEffect } from './image-transformations/types';
|
|
@@ -11,10 +11,10 @@ export interface Layer {
|
|
|
11
11
|
id: string;
|
|
12
12
|
src: string;
|
|
13
13
|
name?: string;
|
|
14
|
-
transform
|
|
14
|
+
transform: LayerTransform;
|
|
15
15
|
opacity?: number;
|
|
16
16
|
visible?: boolean;
|
|
17
|
-
effects
|
|
17
|
+
effects?: LayerEffect[];
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
export interface CanvasSize {
|
|
@@ -309,11 +309,13 @@
|
|
|
309
309
|
<div class="render-output">
|
|
310
310
|
<h3>Contain (400×300)</h3>
|
|
311
311
|
<div class="render-container" style="width: 400px; height: 300px;">
|
|
312
|
+
{#key selectedMockup.id}
|
|
312
313
|
<MockupRenderer
|
|
313
314
|
mockup={selectedMockup}
|
|
314
315
|
imageOverrides={imageOverrides}
|
|
315
316
|
objectFit="contain"
|
|
316
317
|
/>
|
|
318
|
+
{/key}
|
|
317
319
|
</div>
|
|
318
320
|
</div>
|
|
319
321
|
<!-- <div class="render-output">
|