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.
@@ -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(!(layer.visible ?? true))}
125
- aria-label={(layer.visible ?? true) ? 'Hide layer' : 'Show layer'}
123
+ onclick={() => onVisibilityChange(!layer.visible)}
124
+ aria-label={layer.visible ? 'Hide layer' : 'Show layer'}
126
125
  >
127
- {#if layer.visible ?? true}
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 ?? defaultTransform}
178
- opacity={layer.opacity ?? 1}
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
- {#if selectedLayerId}
450
- {@const selectedLayer = mockup.composition.layers.find((l) => l.id === selectedLayerId)}
451
- {#if selectedLayer}
452
- <LayerControls
453
- layer={selectedLayer}
454
- canvasWidth={mockup.composition.width}
455
- canvasHeight={mockup.composition.height}
456
- onTransformChange={(transform) => handleLayerTransform(selectedLayer.id, transform)}
457
- onOpacityChange={(opacity) => handleLayerOpacityChange(selectedLayer.id, opacity)}
458
- onVisibilityChange={(visible) => handleLayerVisibilityChange(selectedLayer.id, visible)}
459
- onEffectsChange={(effects) => handleLayerEffectsChange(selectedLayer.id, effects)}
460
- onNameChange={(name) => handleLayerNameChange(selectedLayer.id, name)}
461
- onSrcChange={(src) => handleLayerSrcChange(selectedLayer.id, src)}
462
- onDelete={() => handleDeleteLayer(selectedLayer.id)}
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
- bind:value={transform.rotation}
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
- bind:value={transform.rotation}
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: number;
41
- image: ImageOnSurface;
40
+ quality?: number;
41
+ image?: ImageOnSurface;
42
42
  debug?: WarpDebug;
43
43
  cylinder?: CylinderWarpParams;
44
44
  plane?: PlaneWarpParams;
@@ -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 = omitDefaultsAlwaysInclude(effect, defaultResizeEffect, PX_SIZING_KEYS.resize);
40
+ const normalized = omitDefaults(effect, defaultResizeEffect);
57
41
  return { type: 'resize', ...normalized };
58
42
  }
59
43
  function normalizePositionEffect(effect) {
60
- const normalized = omitDefaultsAlwaysInclude(effect, defaultPositionEffect, PX_SIZING_KEYS.position);
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 = omitDefaultsAlwaysInclude(effect.cylinder, defaultCylinderWarp, PX_SIZING_KEYS.cylinder);
95
- if (Object.keys(cylinderNormalized).length > 0) {
96
- normalized.cylinder = cylinderNormalized;
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 = omitDefaultsAlwaysInclude(effect.sphere, defaultSphereWarp, PX_SIZING_KEYS.sphere);
107
- if (Object.keys(sphereNormalized).length > 0) {
108
- normalized.sphere = sphereNormalized;
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 transform = layer.transform ?? defaultTransform;
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 ((layer.opacity ?? defaultLayer.opacity) !== defaultLayer.opacity) {
147
- normalized.opacity = layer.opacity ?? defaultLayer.opacity;
129
+ if (layer.opacity !== defaultLayer.opacity) {
130
+ normalized.opacity = layer.opacity;
148
131
  }
149
- if ((layer.visible ?? defaultLayer.visible) !== defaultLayer.visible) {
150
- normalized.visible = layer.visible ?? defaultLayer.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
- normalized.width = composition.width;
165
- normalized.height = composition.height;
166
- if (composition.layers.length > 0) {
167
- normalized.layers = composition.layers.map(normalizeLayer);
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
- if (Object.keys(compNormalized).length > 0) {
185
- normalized.composition = compNormalized;
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
- if (effect.width === undefined || effect.height === undefined) {
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
- if (effect.containerWidth === undefined ||
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 mergeDefaultsExcluding(effect, defaultSvgEffect, [
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
- if (!effect.cylinder || effect.cylinder.diameter === undefined) {
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
- else if (shape === 'plane' && !denormalized.plane) {
229
+ if (shape === 'plane' && !denormalized.plane) {
283
230
  denormalized.plane = { ...defaultPlaneWarp };
284
231
  }
285
- else if (shape === 'sphere') {
286
- if (!effect.sphere || effect.sphere.radius === undefined) {
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
- composition: denormalizeComposition(mockup.composition || {}),
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
- const t = l.transform ?? defaultTransform;
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 === false)
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 t = layer.transform ?? defaultTransform;
323
- const scaledWidth = displayWidth * t.scale;
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(t.x),
354
- y: Number(t.y),
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(t.rotation),
360
- opacity: Number(layer.opacity ?? 1),
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 === false)
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
- const t = layer.transform ?? defaultTransform;
413
- state.ctx.globalAlpha = layer.opacity ?? 1;
414
- const scaledWidth = displayWidth * t.scale;
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 += t.x;
428
- imgTop += t.y;
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((t.rotation * Math.PI) / 180);
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: number;
3
- y: number;
4
- scale: number;
5
- rotation: number;
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?: LayerTransform;
12
+ transform: LayerTransform;
13
13
  opacity?: number;
14
14
  visible?: boolean;
15
- effects: LayerEffect[];
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.9",
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": "bash -c 'set -a && [ -f ../.env ] && . ../.env; set +a && npm config set //registry.npmjs.org/:_authToken $NPM_TOKEN && npm 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(!(layer.visible ?? true))}
125
- aria-label={(layer.visible ?? true) ? 'Hide layer' : 'Show layer'}
123
+ onclick={() => onVisibilityChange(!layer.visible)}
124
+ aria-label={layer.visible ? 'Hide layer' : 'Show layer'}
126
125
  >
127
- {#if layer.visible ?? true}
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 ?? defaultTransform}
178
- opacity={layer.opacity ?? 1}
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
- {#if selectedLayerId}
450
- {@const selectedLayer = mockup.composition.layers.find((l) => l.id === selectedLayerId)}
451
- {#if selectedLayer}
452
- <LayerControls
453
- layer={selectedLayer}
454
- canvasWidth={mockup.composition.width}
455
- canvasHeight={mockup.composition.height}
456
- onTransformChange={(transform) => handleLayerTransform(selectedLayer.id, transform)}
457
- onOpacityChange={(opacity) => handleLayerOpacityChange(selectedLayer.id, opacity)}
458
- onVisibilityChange={(visible) => handleLayerVisibilityChange(selectedLayer.id, visible)}
459
- onEffectsChange={(effects) => handleLayerEffectsChange(selectedLayer.id, effects)}
460
- onNameChange={(name) => handleLayerNameChange(selectedLayer.id, name)}
461
- onSrcChange={(src) => handleLayerSrcChange(selectedLayer.id, src)}
462
- onDelete={() => handleDeleteLayer(selectedLayer.id)}
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
- bind:value={transform.rotation}
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
- bind:value={transform.rotation}
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: number;
48
- image: ImageOnSurface;
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 = omitDefaultsAlwaysInclude(effect, defaultResizeEffect, PX_SIZING_KEYS.resize);
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 = omitDefaultsAlwaysInclude(effect, defaultPositionEffect, PX_SIZING_KEYS.position);
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 = omitDefaultsAlwaysInclude(
141
- effect.cylinder,
142
- defaultCylinderWarp,
143
- PX_SIZING_KEYS.cylinder
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 = omitDefaultsAlwaysInclude(
155
- effect.sphere,
156
- defaultSphereWarp,
157
- PX_SIZING_KEYS.sphere
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 transform = layer.transform ?? defaultTransform;
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 ((layer.opacity ?? defaultLayer.opacity) !== defaultLayer.opacity) {
206
- normalized.opacity = layer.opacity ?? defaultLayer.opacity;
174
+ if (layer.opacity !== defaultLayer.opacity) {
175
+ normalized.opacity = layer.opacity;
207
176
  }
208
177
 
209
- if ((layer.visible ?? defaultLayer.visible) !== defaultLayer.visible) {
210
- normalized.visible = layer.visible ?? defaultLayer.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 LayerEffect[];
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
- normalized.width = composition.width;
229
- normalized.height = composition.height;
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
- if (Object.keys(compNormalized).length > 0) {
255
- normalized.composition = compNormalized as Composition;
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
- if (effect.width === undefined || effect.height === undefined) {
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
- if (
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 mergeDefaultsExcluding(effect, defaultSvgEffect, [
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
- if (shape === 'cylinder') {
364
- if (!effect.cylinder || effect.cylinder.diameter === undefined) {
365
- throw new Error('WarpEffect cylinder shape requires explicit cylinder.diameter (px sizing cannot use defaults)');
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
- } else if (shape === 'sphere') {
375
- if (!effect.sphere || effect.sphere.radius === undefined) {
376
- throw new Error('WarpEffect sphere shape requires explicit sphere.radius (px sizing cannot use defaults)');
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
- composition: denormalizeComposition(mockup.composition || {}),
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
- const t = l.transform ?? defaultTransform;
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 === false) continue;
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 t = layer.transform ?? defaultTransform;
364
- const scaledWidth = displayWidth * t.scale;
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(t.x),
397
- y: Number(t.y),
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(t.rotation),
403
- opacity: Number(layer.opacity ?? 1),
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 === false) continue;
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
- const t = layer.transform ?? defaultTransform;
464
- state.ctx.globalAlpha = layer.opacity ?? 1;
460
+ state.ctx.globalAlpha = layer.opacity;
465
461
 
466
- const scaledWidth = displayWidth * t.scale;
467
- const scaledHeight = displayHeight * t.scale;
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 += t.x;
483
- imgTop += t.y;
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((t.rotation * Math.PI) / 180);
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: number;
3
- y: number;
4
- scale: number;
5
- rotation: number;
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?: LayerTransform;
14
+ transform: LayerTransform;
15
15
  opacity?: number;
16
16
  visible?: boolean;
17
- effects: LayerEffect[];
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">