@nasser-sw/fabric 7.0.1-beta17 → 7.0.1-beta18

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.
Files changed (64) hide show
  1. package/.history/package_20251226051014.json +164 -0
  2. package/dist/fabric.d.ts +2 -0
  3. package/dist/fabric.d.ts.map +1 -1
  4. package/dist/fabric.min.mjs +1 -1
  5. package/dist/fabric.mjs +2 -0
  6. package/dist/fabric.mjs.map +1 -1
  7. package/dist/index.js +1742 -368
  8. package/dist/index.js.map +1 -1
  9. package/dist/index.min.js +1 -1
  10. package/dist/index.min.js.map +1 -1
  11. package/dist/index.min.mjs +1 -1
  12. package/dist/index.min.mjs.map +1 -1
  13. package/dist/index.mjs +1741 -369
  14. package/dist/index.mjs.map +1 -1
  15. package/dist/index.node.cjs +1742 -368
  16. package/dist/index.node.cjs.map +1 -1
  17. package/dist/index.node.mjs +1741 -369
  18. package/dist/index.node.mjs.map +1 -1
  19. package/dist/package.json.min.mjs +1 -1
  20. package/dist/package.json.mjs +1 -1
  21. package/dist/src/LayoutManager/LayoutStrategies/FrameLayout.d.ts +31 -0
  22. package/dist/src/LayoutManager/LayoutStrategies/FrameLayout.d.ts.map +1 -0
  23. package/dist/src/LayoutManager/LayoutStrategies/FrameLayout.min.mjs +2 -0
  24. package/dist/src/LayoutManager/LayoutStrategies/FrameLayout.min.mjs.map +1 -0
  25. package/dist/src/LayoutManager/LayoutStrategies/FrameLayout.mjs +81 -0
  26. package/dist/src/LayoutManager/LayoutStrategies/FrameLayout.mjs.map +1 -0
  27. package/dist/src/LayoutManager/index.d.ts +1 -0
  28. package/dist/src/LayoutManager/index.d.ts.map +1 -1
  29. package/dist/src/controls/commonControls.d.ts.map +1 -1
  30. package/dist/src/controls/commonControls.mjs +25 -6
  31. package/dist/src/controls/commonControls.mjs.map +1 -1
  32. package/dist/src/controls/controlRendering.d.ts +20 -0
  33. package/dist/src/controls/controlRendering.d.ts.map +1 -1
  34. package/dist/src/controls/controlRendering.mjs +63 -1
  35. package/dist/src/controls/controlRendering.mjs.map +1 -1
  36. package/dist/src/shapes/Frame.d.ts +298 -0
  37. package/dist/src/shapes/Frame.d.ts.map +1 -0
  38. package/dist/src/shapes/Frame.min.mjs +2 -0
  39. package/dist/src/shapes/Frame.min.mjs.map +1 -0
  40. package/dist/src/shapes/Frame.mjs +1236 -0
  41. package/dist/src/shapes/Frame.mjs.map +1 -0
  42. package/dist/src/shapes/Object/defaultValues.d.ts.map +1 -1
  43. package/dist/src/shapes/Object/defaultValues.mjs +8 -7
  44. package/dist/src/shapes/Object/defaultValues.mjs.map +1 -1
  45. package/dist-extensions/fabric.d.ts +2 -0
  46. package/dist-extensions/fabric.d.ts.map +1 -1
  47. package/dist-extensions/src/LayoutManager/LayoutStrategies/FrameLayout.d.ts +31 -0
  48. package/dist-extensions/src/LayoutManager/LayoutStrategies/FrameLayout.d.ts.map +1 -0
  49. package/dist-extensions/src/LayoutManager/index.d.ts +1 -0
  50. package/dist-extensions/src/LayoutManager/index.d.ts.map +1 -1
  51. package/dist-extensions/src/controls/commonControls.d.ts.map +1 -1
  52. package/dist-extensions/src/controls/controlRendering.d.ts +20 -0
  53. package/dist-extensions/src/controls/controlRendering.d.ts.map +1 -1
  54. package/dist-extensions/src/shapes/Frame.d.ts +298 -0
  55. package/dist-extensions/src/shapes/Frame.d.ts.map +1 -0
  56. package/dist-extensions/src/shapes/Object/defaultValues.d.ts.map +1 -1
  57. package/fabric.ts +8 -0
  58. package/package.json +1 -1
  59. package/src/LayoutManager/LayoutStrategies/FrameLayout.ts +80 -0
  60. package/src/LayoutManager/index.ts +1 -0
  61. package/src/controls/commonControls.ts +22 -0
  62. package/src/controls/controlRendering.ts +83 -0
  63. package/src/shapes/Frame.ts +1361 -0
  64. package/src/shapes/Object/defaultValues.ts +8 -7
@@ -0,0 +1,1236 @@
1
+ import { defineProperty as _defineProperty } from '../../_virtual/_rollupPluginBabelHelpers.mjs';
2
+ import { Group } from './Group.mjs';
3
+ import { Rect } from './Rect.mjs';
4
+ import { Circle } from './Circle.mjs';
5
+ import { Path } from './Path.mjs';
6
+ import { FabricImage } from './Image.mjs';
7
+ import { classRegistry } from '../ClassRegistry.mjs';
8
+ import { LayoutManager } from '../LayoutManager/LayoutManager.mjs';
9
+ import { FrameLayout } from '../LayoutManager/LayoutStrategies/FrameLayout.mjs';
10
+ import { enlivenObjects, enlivenObjectEnlivables } from '../util/misc/objectEnlive.mjs';
11
+ import { Control } from '../controls/Control.mjs';
12
+ import { getLocalPoint } from '../controls/util.mjs';
13
+ import { wrapWithFireEvent } from '../controls/wrapWithFireEvent.mjs';
14
+ import { wrapWithFixedAnchor } from '../controls/wrapWithFixedAnchor.mjs';
15
+ import { RESIZING } from '../constants.mjs';
16
+
17
+ /**
18
+ * Frame shape types supported out of the box
19
+ */
20
+
21
+ /**
22
+ * Frame metadata for persistence and state management
23
+ */
24
+
25
+ /**
26
+ * Frame-specific properties
27
+ */
28
+
29
+ const frameDefaultValues = {
30
+ frameWidth: 200,
31
+ frameHeight: 200,
32
+ frameShape: 'rect',
33
+ frameBorderRadius: 0,
34
+ isEditMode: false,
35
+ placeholderText: 'Drop image here',
36
+ placeholderColor: '#d0d0d0',
37
+ frameMeta: {
38
+ contentScale: 1,
39
+ contentOffsetX: 0,
40
+ contentOffsetY: 0
41
+ }
42
+ };
43
+
44
+ /**
45
+ * Frame class - A Canva-like frame container for images
46
+ *
47
+ * Features:
48
+ * - Fixed dimensions that don't change when content is added/removed
49
+ * - Multiple shape types (rect, circle, rounded-rect, custom SVG path)
50
+ * - Cover scaling: images fill the frame completely, overflow is clipped
51
+ * - Double-click edit mode: reposition/zoom content within frame
52
+ * - Drag & drop support for replacing images
53
+ * - Full serialization/deserialization support
54
+ *
55
+ * @example
56
+ * ```ts
57
+ * // Create a rectangular frame
58
+ * const frame = new Frame([], {
59
+ * frameWidth: 300,
60
+ * frameHeight: 200,
61
+ * frameShape: 'rect',
62
+ * left: 100,
63
+ * top: 100,
64
+ * });
65
+ *
66
+ * // Add image with cover scaling
67
+ * await frame.setImage('https://example.com/image.jpg');
68
+ *
69
+ * canvas.add(frame);
70
+ * ```
71
+ */
72
+ class Frame extends Group {
73
+ static getDefaults() {
74
+ return {
75
+ ...super.getDefaults(),
76
+ ...Frame.ownDefaults
77
+ };
78
+ }
79
+
80
+ /**
81
+ * Constructor
82
+ * @param objects - Initial objects (typically empty for frames)
83
+ * @param options - Frame configuration options
84
+ */
85
+ constructor() {
86
+ var _defaultMeta$contentS, _defaultMeta$contentO, _defaultMeta$contentO2;
87
+ let objects = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [];
88
+ let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
89
+ // Set up the frame layout manager before calling super
90
+ const frameLayoutManager = new LayoutManager(new FrameLayout());
91
+ super(objects, {
92
+ ...options,
93
+ layoutManager: frameLayoutManager
94
+ });
95
+
96
+ // Apply defaults
97
+ /**
98
+ * Reference to the content image
99
+ * @private
100
+ */
101
+ _defineProperty(this, "_contentImage", null);
102
+ /**
103
+ * Reference to the placeholder object
104
+ * @private
105
+ */
106
+ _defineProperty(this, "_placeholder", null);
107
+ /**
108
+ * Bound constraint handler references for cleanup
109
+ * @private
110
+ */
111
+ _defineProperty(this, "_boundConstrainMove", void 0);
112
+ _defineProperty(this, "_boundConstrainScale", void 0);
113
+ /**
114
+ * Stored clip path before edit mode
115
+ * @private
116
+ */
117
+ _defineProperty(this, "_editModeClipPath", void 0);
118
+ Object.assign(this, Frame.ownDefaults);
119
+
120
+ // Apply user options
121
+ this.setOptions(options);
122
+
123
+ // Ensure frameMeta is properly initialized with defaults
124
+ const defaultMeta = frameDefaultValues.frameMeta || {};
125
+ this.frameMeta = {
126
+ contentScale: (_defaultMeta$contentS = defaultMeta.contentScale) !== null && _defaultMeta$contentS !== void 0 ? _defaultMeta$contentS : 1,
127
+ contentOffsetX: (_defaultMeta$contentO = defaultMeta.contentOffsetX) !== null && _defaultMeta$contentO !== void 0 ? _defaultMeta$contentO : 0,
128
+ contentOffsetY: (_defaultMeta$contentO2 = defaultMeta.contentOffsetY) !== null && _defaultMeta$contentO2 !== void 0 ? _defaultMeta$contentO2 : 0,
129
+ ...options.frameMeta
130
+ };
131
+
132
+ // Set fixed dimensions
133
+ this.set({
134
+ width: this.frameWidth,
135
+ height: this.frameHeight
136
+ });
137
+
138
+ // Create clip path based on shape
139
+ this._updateClipPath();
140
+
141
+ // Create placeholder if no content
142
+ if (objects.length === 0) {
143
+ this._createPlaceholder();
144
+ }
145
+
146
+ // Set up custom resize controls (instead of scale controls)
147
+ this._setupResizeControls();
148
+ }
149
+
150
+ /**
151
+ * Sets up custom controls that resize instead of scale
152
+ * This is the key to Canva-like behavior - corners resize the frame dimensions
153
+ * instead of scaling the entire group (which would stretch the image)
154
+ * @private
155
+ */
156
+ _setupResizeControls() {
157
+ // Helper to change width (like changeObjectWidth but for frames)
158
+ // Note: wrapWithFixedAnchor sets origin to opposite corner, so localPoint.x IS the new width
159
+ const changeFrameWidth = (eventData, transform, x, y) => {
160
+ const target = transform.target;
161
+ const localPoint = getLocalPoint(transform, transform.originX, transform.originY, x, y);
162
+ const oldWidth = target.frameWidth;
163
+ // localPoint.x is distance from anchor (opposite side) to mouse = new width
164
+ const newWidth = Math.max(20, Math.abs(localPoint.x));
165
+ if (Math.abs(oldWidth - newWidth) < 1) return false;
166
+ target.frameWidth = newWidth;
167
+ target.width = newWidth;
168
+ target._updateClipPath();
169
+ target._adjustContentAfterResize();
170
+ return true;
171
+ };
172
+
173
+ // Helper to change height
174
+ const changeFrameHeight = (eventData, transform, x, y) => {
175
+ const target = transform.target;
176
+ const localPoint = getLocalPoint(transform, transform.originX, transform.originY, x, y);
177
+ const oldHeight = target.frameHeight;
178
+ const newHeight = Math.max(20, Math.abs(localPoint.y));
179
+ if (Math.abs(oldHeight - newHeight) < 1) return false;
180
+ target.frameHeight = newHeight;
181
+ target.height = newHeight;
182
+ target._updateClipPath();
183
+ target._adjustContentAfterResize();
184
+ return true;
185
+ };
186
+
187
+ // Helper to change both width and height (corners)
188
+ const changeFrameSize = (eventData, transform, x, y) => {
189
+ const target = transform.target;
190
+ const localPoint = getLocalPoint(transform, transform.originX, transform.originY, x, y);
191
+ const oldWidth = target.frameWidth;
192
+ const oldHeight = target.frameHeight;
193
+ const newWidth = Math.max(20, Math.abs(localPoint.x));
194
+ const newHeight = Math.max(20, Math.abs(localPoint.y));
195
+ if (Math.abs(oldWidth - newWidth) < 1 && Math.abs(oldHeight - newHeight) < 1) return false;
196
+ target.frameWidth = newWidth;
197
+ target.frameHeight = newHeight;
198
+ target.width = newWidth;
199
+ target.height = newHeight;
200
+ target._updateClipPath();
201
+ target._adjustContentAfterResize();
202
+ return true;
203
+ };
204
+
205
+ // Create wrapped handlers
206
+ const resizeFromCorner = wrapWithFireEvent(RESIZING, wrapWithFixedAnchor(changeFrameSize));
207
+ const resizeX = wrapWithFireEvent(RESIZING, wrapWithFixedAnchor(changeFrameWidth));
208
+ const resizeY = wrapWithFireEvent(RESIZING, wrapWithFixedAnchor(changeFrameHeight));
209
+
210
+ // Guard: ensure controls exist
211
+ if (!this.controls) {
212
+ console.warn('Frame: controls not initialized yet');
213
+ return;
214
+ }
215
+
216
+ // Override corner controls - use resize instead of scale
217
+ const cornerControls = ['tl', 'tr', 'bl', 'br'];
218
+ cornerControls.forEach(corner => {
219
+ const existing = this.controls[corner];
220
+ if (existing) {
221
+ this.controls[corner] = new Control({
222
+ x: existing.x,
223
+ y: existing.y,
224
+ cursorStyleHandler: existing.cursorStyleHandler,
225
+ actionHandler: resizeFromCorner,
226
+ actionName: 'resizing'
227
+ });
228
+ }
229
+ });
230
+
231
+ // Override side controls for horizontal resize
232
+ const horizontalControls = ['ml', 'mr'];
233
+ horizontalControls.forEach(corner => {
234
+ const existing = this.controls[corner];
235
+ if (existing) {
236
+ this.controls[corner] = new Control({
237
+ x: existing.x,
238
+ y: existing.y,
239
+ cursorStyleHandler: existing.cursorStyleHandler,
240
+ actionHandler: resizeX,
241
+ actionName: 'resizing',
242
+ render: existing.render,
243
+ // Keep the global pill renderer
244
+ sizeX: existing.sizeX,
245
+ sizeY: existing.sizeY
246
+ });
247
+ }
248
+ });
249
+
250
+ // Override side controls for vertical resize
251
+ const verticalControls = ['mt', 'mb'];
252
+ verticalControls.forEach(corner => {
253
+ const existing = this.controls[corner];
254
+ if (existing) {
255
+ this.controls[corner] = new Control({
256
+ x: existing.x,
257
+ y: existing.y,
258
+ cursorStyleHandler: existing.cursorStyleHandler,
259
+ actionHandler: resizeY,
260
+ actionName: 'resizing',
261
+ render: existing.render,
262
+ // Keep the global pill renderer
263
+ sizeX: existing.sizeX,
264
+ sizeY: existing.sizeY
265
+ });
266
+ }
267
+ });
268
+ }
269
+
270
+ /**
271
+ * Adjusts content after a resize operation (called from set override)
272
+ * @private
273
+ */
274
+ _adjustContentAfterResize() {
275
+ // Update placeholder if present (simple rect)
276
+ if (this._placeholder) {
277
+ this._placeholder.set({
278
+ width: this.frameWidth,
279
+ height: this.frameHeight
280
+ });
281
+ }
282
+
283
+ // Adjust content image (Canva-like behavior)
284
+ if (this._contentImage) {
285
+ var _ref, _this$frameMeta$origi, _ref2, _this$frameMeta$origi2, _img$scaleX, _img$left, _img$top;
286
+ const img = this._contentImage;
287
+ const originalWidth = (_ref = (_this$frameMeta$origi = this.frameMeta.originalWidth) !== null && _this$frameMeta$origi !== void 0 ? _this$frameMeta$origi : img.width) !== null && _ref !== void 0 ? _ref : 100;
288
+ const originalHeight = (_ref2 = (_this$frameMeta$origi2 = this.frameMeta.originalHeight) !== null && _this$frameMeta$origi2 !== void 0 ? _this$frameMeta$origi2 : img.height) !== null && _ref2 !== void 0 ? _ref2 : 100;
289
+
290
+ // Current image scale and position - preserve user's position
291
+ let currentScale = (_img$scaleX = img.scaleX) !== null && _img$scaleX !== void 0 ? _img$scaleX : 1;
292
+ let imgCenterX = (_img$left = img.left) !== null && _img$left !== void 0 ? _img$left : 0;
293
+ let imgCenterY = (_img$top = img.top) !== null && _img$top !== void 0 ? _img$top : 0;
294
+
295
+ // Check if current scale still covers the frame
296
+ const minScaleForCover = this._calculateCoverScale(originalWidth, originalHeight);
297
+ if (currentScale < minScaleForCover) {
298
+ // Image is too small to cover frame - scale up proportionally
299
+ // But try to keep the same visual center point
300
+ const scaleRatio = minScaleForCover / currentScale;
301
+
302
+ // Scale position proportionally to maintain visual anchor
303
+ imgCenterX = imgCenterX * scaleRatio;
304
+ imgCenterY = imgCenterY * scaleRatio;
305
+ currentScale = minScaleForCover;
306
+ img.set({
307
+ scaleX: currentScale,
308
+ scaleY: currentScale
309
+ });
310
+ this.frameMeta = {
311
+ ...this.frameMeta,
312
+ contentScale: currentScale
313
+ };
314
+ }
315
+
316
+ // Now constrain position only if needed to prevent empty space
317
+ const scaledImgHalfW = originalWidth * currentScale / 2;
318
+ const scaledImgHalfH = originalHeight * currentScale / 2;
319
+ const frameHalfW = this.frameWidth / 2;
320
+ const frameHalfH = this.frameHeight / 2;
321
+
322
+ // Calculate how much the image can move while still covering the frame
323
+ const maxOffsetX = Math.max(0, scaledImgHalfW - frameHalfW);
324
+ const maxOffsetY = Math.max(0, scaledImgHalfH - frameHalfH);
325
+
326
+ // Only constrain if position would show empty space
327
+ const needsConstraintX = Math.abs(imgCenterX) > maxOffsetX;
328
+ const needsConstraintY = Math.abs(imgCenterY) > maxOffsetY;
329
+ if (needsConstraintX) {
330
+ imgCenterX = Math.max(-maxOffsetX, Math.min(maxOffsetX, imgCenterX));
331
+ }
332
+ if (needsConstraintY) {
333
+ imgCenterY = Math.max(-maxOffsetY, Math.min(maxOffsetY, imgCenterY));
334
+ }
335
+ if (needsConstraintX || needsConstraintY) {
336
+ img.set({
337
+ left: imgCenterX,
338
+ top: imgCenterY
339
+ });
340
+ this.frameMeta = {
341
+ ...this.frameMeta,
342
+ contentOffsetX: imgCenterX,
343
+ contentOffsetY: imgCenterY
344
+ };
345
+ }
346
+ img.setCoords();
347
+ }
348
+ this.setCoords();
349
+ }
350
+
351
+ /**
352
+ * Updates the clip path based on the current frame shape
353
+ * @private
354
+ */
355
+ _updateClipPath() {
356
+ let clipPath;
357
+ switch (this.frameShape) {
358
+ case 'circle':
359
+ {
360
+ const radius = Math.min(this.frameWidth, this.frameHeight) / 2;
361
+ clipPath = new Circle({
362
+ radius,
363
+ originX: 'center',
364
+ originY: 'center',
365
+ left: 0,
366
+ top: 0
367
+ });
368
+ break;
369
+ }
370
+ case 'rounded-rect':
371
+ {
372
+ clipPath = new Rect({
373
+ width: this.frameWidth,
374
+ height: this.frameHeight,
375
+ rx: this.frameBorderRadius,
376
+ ry: this.frameBorderRadius,
377
+ originX: 'center',
378
+ originY: 'center',
379
+ left: 0,
380
+ top: 0
381
+ });
382
+ break;
383
+ }
384
+ case 'custom':
385
+ {
386
+ if (this.frameCustomPath) {
387
+ clipPath = new Path(this.frameCustomPath, {
388
+ originX: 'center',
389
+ originY: 'center',
390
+ left: 0,
391
+ top: 0
392
+ });
393
+ // Scale custom path to fit frame
394
+ const pathBounds = clipPath.getBoundingRect();
395
+ const scaleX = this.frameWidth / pathBounds.width;
396
+ const scaleY = this.frameHeight / pathBounds.height;
397
+ clipPath.set({
398
+ scaleX,
399
+ scaleY
400
+ });
401
+ } else {
402
+ // Fallback to rect if no custom path
403
+ clipPath = new Rect({
404
+ width: this.frameWidth,
405
+ height: this.frameHeight,
406
+ originX: 'center',
407
+ originY: 'center',
408
+ left: 0,
409
+ top: 0
410
+ });
411
+ }
412
+ break;
413
+ }
414
+ case 'rect':
415
+ default:
416
+ {
417
+ clipPath = new Rect({
418
+ width: this.frameWidth,
419
+ height: this.frameHeight,
420
+ originX: 'center',
421
+ originY: 'center',
422
+ left: 0,
423
+ top: 0
424
+ });
425
+ break;
426
+ }
427
+ }
428
+ this.clipPath = clipPath;
429
+ this.set('dirty', true);
430
+ }
431
+
432
+ /**
433
+ * Creates a placeholder element for empty frames
434
+ * Shows a colored rectangle - users can customize via placeholderColor
435
+ * @private
436
+ */
437
+ _createPlaceholder() {
438
+ // Remove existing placeholder if any
439
+ if (this._placeholder) {
440
+ super.remove(this._placeholder);
441
+ this._placeholder = null;
442
+ }
443
+
444
+ // Create placeholder background
445
+ const placeholder = new Rect({
446
+ width: this.frameWidth,
447
+ height: this.frameHeight,
448
+ fill: this.placeholderColor,
449
+ originX: 'center',
450
+ originY: 'center',
451
+ left: 0,
452
+ top: 0,
453
+ selectable: false,
454
+ evented: false
455
+ });
456
+ this._placeholder = placeholder;
457
+ super.add(placeholder);
458
+
459
+ // Ensure dimensions remain fixed
460
+ this._restoreFixedDimensions();
461
+ }
462
+
463
+ /**
464
+ * Removes the placeholder element
465
+ * @private
466
+ */
467
+ _removePlaceholder() {
468
+ if (this._placeholder) {
469
+ super.remove(this._placeholder);
470
+ this._placeholder = null;
471
+ }
472
+ }
473
+
474
+ /**
475
+ * Restores the fixed frame dimensions
476
+ * @private
477
+ */
478
+ _restoreFixedDimensions() {
479
+ this.set({
480
+ width: this.frameWidth,
481
+ height: this.frameHeight
482
+ });
483
+ }
484
+
485
+ /**
486
+ * Sets an image in the frame with cover scaling
487
+ *
488
+ * @param src - Image source URL
489
+ * @param options - Optional loading options
490
+ * @returns Promise that resolves when the image is loaded and set
491
+ *
492
+ * @example
493
+ * ```ts
494
+ * await frame.setImage('https://example.com/photo.jpg');
495
+ * canvas.renderAll();
496
+ * ```
497
+ */
498
+ async setImage(src) {
499
+ var _image$width, _image$height;
500
+ let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
501
+ const {
502
+ crossOrigin = 'anonymous',
503
+ signal
504
+ } = options;
505
+
506
+ // Load the image
507
+ const image = await FabricImage.fromURL(src, {
508
+ crossOrigin,
509
+ signal
510
+ });
511
+
512
+ // Get original dimensions
513
+ const originalWidth = (_image$width = image.width) !== null && _image$width !== void 0 ? _image$width : 100;
514
+ const originalHeight = (_image$height = image.height) !== null && _image$height !== void 0 ? _image$height : 100;
515
+
516
+ // Calculate cover scale
517
+ const scale = this._calculateCoverScale(originalWidth, originalHeight);
518
+
519
+ // Configure image for frame
520
+ image.set({
521
+ scaleX: scale,
522
+ scaleY: scale,
523
+ originX: 'center',
524
+ originY: 'center',
525
+ left: 0,
526
+ top: 0,
527
+ selectable: false,
528
+ evented: false
529
+ });
530
+
531
+ // Remove existing content
532
+ this._clearContent();
533
+
534
+ // Add new image
535
+ this._contentImage = image;
536
+ super.add(image);
537
+
538
+ // Force re-center the image after adding (layout might have moved it)
539
+ this._contentImage.set({
540
+ left: 0,
541
+ top: 0
542
+ });
543
+
544
+ // Update metadata
545
+ this.frameMeta = {
546
+ ...this.frameMeta,
547
+ contentScale: scale,
548
+ contentOffsetX: 0,
549
+ contentOffsetY: 0,
550
+ imageSrc: src,
551
+ originalWidth,
552
+ originalHeight
553
+ };
554
+
555
+ // Restore dimensions (in case Group recalculated them)
556
+ this._restoreFixedDimensions();
557
+
558
+ // Force recalculation of coordinates
559
+ this.setCoords();
560
+ this._contentImage.setCoords();
561
+ this.set('dirty', true);
562
+ }
563
+
564
+ /**
565
+ * Sets an image from an existing FabricImage object
566
+ *
567
+ * @param image - FabricImage instance
568
+ */
569
+ setImageObject(image) {
570
+ var _image$width2, _image$height2;
571
+ const originalWidth = (_image$width2 = image.width) !== null && _image$width2 !== void 0 ? _image$width2 : 100;
572
+ const originalHeight = (_image$height2 = image.height) !== null && _image$height2 !== void 0 ? _image$height2 : 100;
573
+
574
+ // Calculate cover scale
575
+ const scale = this._calculateCoverScale(originalWidth, originalHeight);
576
+
577
+ // Configure image for frame
578
+ image.set({
579
+ scaleX: scale,
580
+ scaleY: scale,
581
+ originX: 'center',
582
+ originY: 'center',
583
+ left: 0,
584
+ top: 0,
585
+ selectable: false,
586
+ evented: false
587
+ });
588
+
589
+ // Remove existing content
590
+ this._clearContent();
591
+
592
+ // Add new image
593
+ this._contentImage = image;
594
+ super.add(image);
595
+
596
+ // Update metadata
597
+ this.frameMeta = {
598
+ ...this.frameMeta,
599
+ contentScale: scale,
600
+ contentOffsetX: 0,
601
+ contentOffsetY: 0,
602
+ imageSrc: image.getSrc(),
603
+ originalWidth,
604
+ originalHeight
605
+ };
606
+
607
+ // Restore dimensions
608
+ this._restoreFixedDimensions();
609
+ this.set('dirty', true);
610
+ }
611
+
612
+ /**
613
+ * Calculates the cover scale factor for an image
614
+ * Cover scaling ensures the image fills the frame completely
615
+ *
616
+ * @param imageWidth - Original image width
617
+ * @param imageHeight - Original image height
618
+ * @returns Scale factor to apply
619
+ * @private
620
+ */
621
+ _calculateCoverScale(imageWidth, imageHeight) {
622
+ const scaleX = this.frameWidth / imageWidth;
623
+ const scaleY = this.frameHeight / imageHeight;
624
+ return Math.max(scaleX, scaleY);
625
+ }
626
+
627
+ /**
628
+ * Clears all content from the frame
629
+ * @private
630
+ */
631
+ _clearContent() {
632
+ // Remove placeholder
633
+ this._removePlaceholder();
634
+
635
+ // Remove content image
636
+ if (this._contentImage) {
637
+ super.remove(this._contentImage);
638
+ this._contentImage = null;
639
+ }
640
+
641
+ // Clear any other objects
642
+ const objects = this.getObjects();
643
+ objects.forEach(obj => super.remove(obj));
644
+ }
645
+
646
+ /**
647
+ * Clears the frame content and shows placeholder
648
+ */
649
+ clearContent() {
650
+ this._clearContent();
651
+ this._createPlaceholder();
652
+
653
+ // Reset metadata
654
+ this.frameMeta = {
655
+ contentScale: 1,
656
+ contentOffsetX: 0,
657
+ contentOffsetY: 0
658
+ };
659
+ this.set('dirty', true);
660
+ }
661
+
662
+ /**
663
+ * Checks if the frame has image content
664
+ */
665
+ hasContent() {
666
+ return this._contentImage !== null;
667
+ }
668
+
669
+ /**
670
+ * Gets the current content image
671
+ */
672
+ getContentImage() {
673
+ return this._contentImage;
674
+ }
675
+
676
+ /**
677
+ * Enters edit mode for repositioning content within the frame
678
+ * In edit mode, the content image can be dragged and scaled
679
+ */
680
+ enterEditMode() {
681
+ var _ref3, _this$frameMeta$origi3, _ref4, _this$frameMeta$origi4;
682
+ if (!this._contentImage || this.isEditMode) {
683
+ return;
684
+ }
685
+ this.isEditMode = true;
686
+
687
+ // Enable sub-target interaction so clicks go through to content
688
+ this.subTargetCheck = true;
689
+ this.interactive = true;
690
+
691
+ // Calculate minimum scale to cover frame
692
+ const originalWidth = (_ref3 = (_this$frameMeta$origi3 = this.frameMeta.originalWidth) !== null && _this$frameMeta$origi3 !== void 0 ? _this$frameMeta$origi3 : this._contentImage.width) !== null && _ref3 !== void 0 ? _ref3 : 100;
693
+ const originalHeight = (_ref4 = (_this$frameMeta$origi4 = this.frameMeta.originalHeight) !== null && _this$frameMeta$origi4 !== void 0 ? _this$frameMeta$origi4 : this._contentImage.height) !== null && _ref4 !== void 0 ? _ref4 : 100;
694
+ const minScale = this._calculateCoverScale(originalWidth, originalHeight);
695
+
696
+ // Make content image interactive with scale constraint
697
+ this._contentImage.set({
698
+ selectable: true,
699
+ evented: true,
700
+ hasControls: true,
701
+ hasBorders: true,
702
+ minScaleLimit: minScale,
703
+ lockScalingFlip: true
704
+ });
705
+
706
+ // Store clip path but keep rendering it for the overlay effect
707
+ if (this.clipPath) {
708
+ this._editModeClipPath = this.clipPath;
709
+ this.clipPath = undefined;
710
+ }
711
+
712
+ // Add constraint handlers for moving/scaling
713
+ this._setupEditModeConstraints();
714
+ this.set('dirty', true);
715
+
716
+ // Select the content image on the canvas
717
+ if (this.canvas) {
718
+ this.canvas.setActiveObject(this._contentImage);
719
+ this.canvas.renderAll();
720
+ }
721
+
722
+ // Fire custom event
723
+ this.fire('frame:editmode:enter', {
724
+ target: this
725
+ });
726
+ }
727
+ /**
728
+ * Sets up constraints for edit mode - prevents gaps
729
+ * @private
730
+ */
731
+ _setupEditModeConstraints() {
732
+ if (!this._contentImage || !this.canvas) return;
733
+ const frame = this;
734
+ const img = this._contentImage;
735
+
736
+ // Constrain movement to prevent gaps
737
+ this._boundConstrainMove = e => {
738
+ var _ref5, _frame$frameMeta$orig, _ref6, _frame$frameMeta$orig2, _img$scaleX2, _img$left2, _img$top2;
739
+ if (e.target !== img || !frame.isEditMode) return;
740
+ const originalWidth = (_ref5 = (_frame$frameMeta$orig = frame.frameMeta.originalWidth) !== null && _frame$frameMeta$orig !== void 0 ? _frame$frameMeta$orig : img.width) !== null && _ref5 !== void 0 ? _ref5 : 100;
741
+ const originalHeight = (_ref6 = (_frame$frameMeta$orig2 = frame.frameMeta.originalHeight) !== null && _frame$frameMeta$orig2 !== void 0 ? _frame$frameMeta$orig2 : img.height) !== null && _ref6 !== void 0 ? _ref6 : 100;
742
+ const currentScale = (_img$scaleX2 = img.scaleX) !== null && _img$scaleX2 !== void 0 ? _img$scaleX2 : 1;
743
+ const scaledImgHalfW = originalWidth * currentScale / 2;
744
+ const scaledImgHalfH = originalHeight * currentScale / 2;
745
+ const frameHalfW = frame.frameWidth / 2;
746
+ const frameHalfH = frame.frameHeight / 2;
747
+ const maxOffsetX = Math.max(0, scaledImgHalfW - frameHalfW);
748
+ const maxOffsetY = Math.max(0, scaledImgHalfH - frameHalfH);
749
+ let left = (_img$left2 = img.left) !== null && _img$left2 !== void 0 ? _img$left2 : 0;
750
+ let top = (_img$top2 = img.top) !== null && _img$top2 !== void 0 ? _img$top2 : 0;
751
+
752
+ // Constrain position
753
+ left = Math.max(-maxOffsetX, Math.min(maxOffsetX, left));
754
+ top = Math.max(-maxOffsetY, Math.min(maxOffsetY, top));
755
+ img.set({
756
+ left,
757
+ top
758
+ });
759
+ };
760
+
761
+ // Constrain scaling to prevent gaps
762
+ this._boundConstrainScale = e => {
763
+ var _ref7, _frame$frameMeta$orig3, _ref8, _frame$frameMeta$orig4, _img$scaleX3, _img$scaleY, _frame$_boundConstrai;
764
+ if (e.target !== img || !frame.isEditMode) return;
765
+ const originalWidth = (_ref7 = (_frame$frameMeta$orig3 = frame.frameMeta.originalWidth) !== null && _frame$frameMeta$orig3 !== void 0 ? _frame$frameMeta$orig3 : img.width) !== null && _ref7 !== void 0 ? _ref7 : 100;
766
+ const originalHeight = (_ref8 = (_frame$frameMeta$orig4 = frame.frameMeta.originalHeight) !== null && _frame$frameMeta$orig4 !== void 0 ? _frame$frameMeta$orig4 : img.height) !== null && _ref8 !== void 0 ? _ref8 : 100;
767
+ const minScale = frame._calculateCoverScale(originalWidth, originalHeight);
768
+ let scaleX = (_img$scaleX3 = img.scaleX) !== null && _img$scaleX3 !== void 0 ? _img$scaleX3 : 1;
769
+ let scaleY = (_img$scaleY = img.scaleY) !== null && _img$scaleY !== void 0 ? _img$scaleY : 1;
770
+
771
+ // Ensure uniform scaling and minimum scale
772
+ const scale = Math.max(minScale, Math.max(scaleX, scaleY));
773
+ img.set({
774
+ scaleX: scale,
775
+ scaleY: scale
776
+ });
777
+
778
+ // Also constrain position after scale
779
+ (_frame$_boundConstrai = frame._boundConstrainMove) === null || _frame$_boundConstrai === void 0 || _frame$_boundConstrai.call(frame, e);
780
+ };
781
+ this.canvas.on('object:moving', this._boundConstrainMove);
782
+ this.canvas.on('object:scaling', this._boundConstrainScale);
783
+ }
784
+
785
+ /**
786
+ * Removes edit mode constraint handlers
787
+ * @private
788
+ */
789
+ _removeEditModeConstraints() {
790
+ if (!this.canvas) return;
791
+ if (this._boundConstrainMove) {
792
+ this.canvas.off('object:moving', this._boundConstrainMove);
793
+ this._boundConstrainMove = undefined;
794
+ }
795
+ if (this._boundConstrainScale) {
796
+ this.canvas.off('object:scaling', this._boundConstrainScale);
797
+ this._boundConstrainScale = undefined;
798
+ }
799
+ }
800
+ /**
801
+ * Custom render to show edit mode overlay
802
+ * @override
803
+ */
804
+ render(ctx) {
805
+ super.render(ctx);
806
+
807
+ // Draw edit mode overlay if in edit mode
808
+ if (this.isEditMode && this._editModeClipPath) {
809
+ this._renderEditModeOverlay(ctx);
810
+ }
811
+ }
812
+
813
+ /**
814
+ * Renders the edit mode overlay - dims area outside frame, shows frame border
815
+ * @private
816
+ */
817
+ _renderEditModeOverlay(ctx) {
818
+ ctx.save();
819
+
820
+ // Apply the group's transform
821
+ const m = this.calcTransformMatrix();
822
+ ctx.transform(m[0], m[1], m[2], m[3], m[4], m[5]);
823
+
824
+ // Draw semi-transparent overlay on the OUTSIDE of the frame
825
+ // We do this by drawing a large rect and cutting out the frame shape
826
+ ctx.beginPath();
827
+
828
+ // Large outer rectangle (covers the whole image area)
829
+ const padding = 2000; // Large enough to cover any overflow
830
+ ctx.rect(-padding, -padding, padding * 2, padding * 2);
831
+
832
+ // Cut out the frame shape (counter-clockwise to create hole)
833
+ if (this.frameShape === 'circle') {
834
+ const radius = Math.min(this.frameWidth, this.frameHeight) / 2;
835
+ ctx.moveTo(radius, 0);
836
+ ctx.arc(0, 0, radius, 0, Math.PI * 2, true);
837
+ } else if (this.frameShape === 'rounded-rect') {
838
+ const w = this.frameWidth / 2;
839
+ const h = this.frameHeight / 2;
840
+ const r = Math.min(this.frameBorderRadius, w, h);
841
+ ctx.moveTo(w, h - r);
842
+ ctx.arcTo(w, -h, w - r, -h, r);
843
+ ctx.arcTo(-w, -h, -w, -h + r, r);
844
+ ctx.arcTo(-w, h, -w + r, h, r);
845
+ ctx.arcTo(w, h, w, h - r, r);
846
+ ctx.closePath();
847
+ } else {
848
+ // Rectangle
849
+ const w = this.frameWidth / 2;
850
+ const h = this.frameHeight / 2;
851
+ ctx.moveTo(w, -h);
852
+ ctx.lineTo(-w, -h);
853
+ ctx.lineTo(-w, h);
854
+ ctx.lineTo(w, h);
855
+ ctx.closePath();
856
+ }
857
+
858
+ // Fill with semi-transparent dark overlay
859
+ ctx.fillStyle = 'rgba(0, 0, 0, 0.5)';
860
+ ctx.fill('evenodd');
861
+
862
+ // Draw frame border
863
+ ctx.beginPath();
864
+ if (this.frameShape === 'circle') {
865
+ const radius = Math.min(this.frameWidth, this.frameHeight) / 2;
866
+ ctx.arc(0, 0, radius, 0, Math.PI * 2);
867
+ } else if (this.frameShape === 'rounded-rect') {
868
+ const w = this.frameWidth / 2;
869
+ const h = this.frameHeight / 2;
870
+ const r = Math.min(this.frameBorderRadius, w, h);
871
+ ctx.moveTo(w - r, -h);
872
+ ctx.arcTo(w, -h, w, -h + r, r);
873
+ ctx.arcTo(w, h, w - r, h, r);
874
+ ctx.arcTo(-w, h, -w, h - r, r);
875
+ ctx.arcTo(-w, -h, -w + r, -h, r);
876
+ ctx.closePath();
877
+ } else {
878
+ const w = this.frameWidth / 2;
879
+ const h = this.frameHeight / 2;
880
+ ctx.rect(-w, -h, this.frameWidth, this.frameHeight);
881
+ }
882
+ ctx.strokeStyle = 'rgba(255, 255, 255, 0.8)';
883
+ ctx.lineWidth = 2;
884
+ ctx.stroke();
885
+
886
+ // Draw subtle dashed line for frame boundary
887
+ ctx.setLineDash([5, 5]);
888
+ ctx.strokeStyle = 'rgba(0, 150, 255, 0.8)';
889
+ ctx.lineWidth = 1;
890
+ ctx.stroke();
891
+ ctx.restore();
892
+ }
893
+
894
+ /**
895
+ * Exits edit mode and saves the content position
896
+ */
897
+ exitEditMode() {
898
+ var _this$_contentImage$l, _this$_contentImage$t, _this$_contentImage$s, _this$_contentImage$s2, _ref9, _this$frameMeta$origi5, _ref0, _this$frameMeta$origi6;
899
+ if (!this._contentImage || !this.isEditMode) {
900
+ return;
901
+ }
902
+ this.isEditMode = false;
903
+
904
+ // Remove constraint handlers
905
+ this._removeEditModeConstraints();
906
+
907
+ // Disable sub-target interaction
908
+ this.subTargetCheck = false;
909
+ this.interactive = false;
910
+
911
+ // Get the current position of the content
912
+ const contentLeft = (_this$_contentImage$l = this._contentImage.left) !== null && _this$_contentImage$l !== void 0 ? _this$_contentImage$l : 0;
913
+ const contentTop = (_this$_contentImage$t = this._contentImage.top) !== null && _this$_contentImage$t !== void 0 ? _this$_contentImage$t : 0;
914
+ const contentScaleX = (_this$_contentImage$s = this._contentImage.scaleX) !== null && _this$_contentImage$s !== void 0 ? _this$_contentImage$s : 1;
915
+ const contentScaleY = (_this$_contentImage$s2 = this._contentImage.scaleY) !== null && _this$_contentImage$s2 !== void 0 ? _this$_contentImage$s2 : 1;
916
+
917
+ // Constrain position so image always covers the frame
918
+ const originalWidth = (_ref9 = (_this$frameMeta$origi5 = this.frameMeta.originalWidth) !== null && _this$frameMeta$origi5 !== void 0 ? _this$frameMeta$origi5 : this._contentImage.width) !== null && _ref9 !== void 0 ? _ref9 : 100;
919
+ const originalHeight = (_ref0 = (_this$frameMeta$origi6 = this.frameMeta.originalHeight) !== null && _this$frameMeta$origi6 !== void 0 ? _this$frameMeta$origi6 : this._contentImage.height) !== null && _ref0 !== void 0 ? _ref0 : 100;
920
+ const currentScale = Math.max(contentScaleX, contentScaleY);
921
+ const scaledImgHalfW = originalWidth * currentScale / 2;
922
+ const scaledImgHalfH = originalHeight * currentScale / 2;
923
+ const frameHalfW = this.frameWidth / 2;
924
+ const frameHalfH = this.frameHeight / 2;
925
+
926
+ // Ensure image covers frame (constrain position)
927
+ const maxOffsetX = Math.max(0, scaledImgHalfW - frameHalfW);
928
+ const maxOffsetY = Math.max(0, scaledImgHalfH - frameHalfH);
929
+ const constrainedLeft = Math.max(-maxOffsetX, Math.min(maxOffsetX, contentLeft));
930
+ const constrainedTop = Math.max(-maxOffsetY, Math.min(maxOffsetY, contentTop));
931
+
932
+ // Apply constrained position
933
+ this._contentImage.set({
934
+ left: constrainedLeft,
935
+ top: constrainedTop
936
+ });
937
+
938
+ // Update metadata with new offsets and scale
939
+ this.frameMeta = {
940
+ ...this.frameMeta,
941
+ contentOffsetX: constrainedLeft,
942
+ contentOffsetY: constrainedTop,
943
+ contentScale: currentScale
944
+ };
945
+
946
+ // Make content non-interactive again
947
+ this._contentImage.set({
948
+ selectable: false,
949
+ evented: false,
950
+ hasControls: false,
951
+ hasBorders: false
952
+ });
953
+
954
+ // Restore clip path
955
+ if (this._editModeClipPath) {
956
+ this.clipPath = this._editModeClipPath;
957
+ this._editModeClipPath = undefined;
958
+ } else {
959
+ this._updateClipPath();
960
+ }
961
+ this.set('dirty', true);
962
+
963
+ // Re-select the frame itself
964
+ if (this.canvas) {
965
+ this.canvas.setActiveObject(this);
966
+ this.canvas.renderAll();
967
+ }
968
+
969
+ // Fire custom event
970
+ this.fire('frame:editmode:exit', {
971
+ target: this
972
+ });
973
+ }
974
+
975
+ /**
976
+ * Toggles edit mode
977
+ */
978
+ toggleEditMode() {
979
+ if (this.isEditMode) {
980
+ this.exitEditMode();
981
+ } else {
982
+ this.enterEditMode();
983
+ }
984
+ }
985
+
986
+ /**
987
+ * Resizes the frame to new dimensions (Canva-like behavior)
988
+ *
989
+ * Canva behavior:
990
+ * - When frame shrinks: crops more of image (no scale change)
991
+ * - When frame grows: uncrops to show more, preserving position
992
+ * - Only scales up when image can't cover the frame anymore
993
+ *
994
+ * @param width - New frame width
995
+ * @param height - New frame height
996
+ * @param options - Resize options
997
+ */
998
+ resizeFrame(width, height) {
999
+ let options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
1000
+ const {
1001
+ maintainAspect = false
1002
+ } = options;
1003
+ if (maintainAspect) {
1004
+ const currentAspect = this.frameWidth / this.frameHeight;
1005
+ const newAspect = width / height;
1006
+ if (newAspect > currentAspect) {
1007
+ height = width / currentAspect;
1008
+ } else {
1009
+ width = height * currentAspect;
1010
+ }
1011
+ }
1012
+ this.frameWidth = width;
1013
+ this.frameHeight = height;
1014
+
1015
+ // Update dimensions using super.set to avoid re-triggering conversion
1016
+ super.set({
1017
+ width: this.frameWidth,
1018
+ height: this.frameHeight
1019
+ });
1020
+
1021
+ // Update clip path
1022
+ this._updateClipPath();
1023
+
1024
+ // Canva-like content adjustment
1025
+ this._adjustContentAfterResize();
1026
+ this.set('dirty', true);
1027
+ this.setCoords();
1028
+ }
1029
+
1030
+ /**
1031
+ * Sets the frame shape
1032
+ *
1033
+ * @param shape - Shape type
1034
+ * @param customPath - Custom SVG path for 'custom' shape type
1035
+ */
1036
+ setFrameShape(shape, customPath) {
1037
+ this.frameShape = shape;
1038
+ if (customPath) {
1039
+ this.frameCustomPath = customPath;
1040
+ }
1041
+ this._updateClipPath();
1042
+ this.set('dirty', true);
1043
+ }
1044
+
1045
+ /**
1046
+ * Sets the border radius for rounded-rect shape
1047
+ *
1048
+ * @param radius - Border radius in pixels
1049
+ */
1050
+ setBorderRadius(radius) {
1051
+ this.frameBorderRadius = radius;
1052
+ if (this.frameShape === 'rounded-rect') {
1053
+ this._updateClipPath();
1054
+ this.set('dirty', true);
1055
+ }
1056
+ }
1057
+
1058
+ /**
1059
+ * Override add to maintain fixed dimensions
1060
+ */
1061
+ add() {
1062
+ const size = super.add(...arguments);
1063
+ this._restoreFixedDimensions();
1064
+ return size;
1065
+ }
1066
+
1067
+ /**
1068
+ * Override remove to maintain fixed dimensions
1069
+ */
1070
+ remove() {
1071
+ const removed = super.remove(...arguments);
1072
+ this._restoreFixedDimensions();
1073
+ return removed;
1074
+ }
1075
+
1076
+ /**
1077
+ * Override insertAt to maintain fixed dimensions
1078
+ */
1079
+ insertAt(index) {
1080
+ for (var _len = arguments.length, objects = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
1081
+ objects[_key - 1] = arguments[_key];
1082
+ }
1083
+ const size = super.insertAt(index, ...objects);
1084
+ this._restoreFixedDimensions();
1085
+ return size;
1086
+ }
1087
+
1088
+ /**
1089
+ * Serializes the frame to a plain object
1090
+ */
1091
+ // @ts-ignore - Frame extends Group's toObject with additional properties
1092
+ toObject() {
1093
+ let propertiesToInclude = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [];
1094
+ return {
1095
+ ...super.toObject(propertiesToInclude),
1096
+ frameWidth: this.frameWidth,
1097
+ frameHeight: this.frameHeight,
1098
+ frameShape: this.frameShape,
1099
+ frameBorderRadius: this.frameBorderRadius,
1100
+ frameCustomPath: this.frameCustomPath,
1101
+ frameMeta: {
1102
+ ...this.frameMeta
1103
+ },
1104
+ isEditMode: false,
1105
+ // Always serialize as not in edit mode
1106
+ placeholderText: this.placeholderText,
1107
+ placeholderColor: this.placeholderColor
1108
+ };
1109
+ }
1110
+
1111
+ /**
1112
+ * Creates a Frame instance from a serialized object
1113
+ */
1114
+ static fromObject(object, abortable) {
1115
+ const {
1116
+ objects = [],
1117
+ layoutManager,
1118
+ frameWidth,
1119
+ frameHeight,
1120
+ frameShape,
1121
+ frameBorderRadius,
1122
+ frameCustomPath,
1123
+ frameMeta,
1124
+ placeholderText,
1125
+ placeholderColor,
1126
+ ...groupOptions
1127
+ } = object;
1128
+ return Promise.all([enlivenObjects(objects, abortable), enlivenObjectEnlivables(groupOptions, abortable)]).then(_ref1 => {
1129
+ var _frameMeta$contentSca, _frameMeta$contentOff, _frameMeta$contentOff2;
1130
+ let [enlivenedObjects, hydratedOptions] = _ref1;
1131
+ // Create frame with restored options
1132
+ const frame = new Frame([], {
1133
+ ...groupOptions,
1134
+ ...hydratedOptions,
1135
+ frameWidth,
1136
+ frameHeight,
1137
+ frameShape,
1138
+ frameBorderRadius,
1139
+ frameCustomPath,
1140
+ frameMeta: frameMeta ? {
1141
+ contentScale: (_frameMeta$contentSca = frameMeta.contentScale) !== null && _frameMeta$contentSca !== void 0 ? _frameMeta$contentSca : 1,
1142
+ contentOffsetX: (_frameMeta$contentOff = frameMeta.contentOffsetX) !== null && _frameMeta$contentOff !== void 0 ? _frameMeta$contentOff : 0,
1143
+ contentOffsetY: (_frameMeta$contentOff2 = frameMeta.contentOffsetY) !== null && _frameMeta$contentOff2 !== void 0 ? _frameMeta$contentOff2 : 0,
1144
+ ...frameMeta
1145
+ } : undefined,
1146
+ placeholderText,
1147
+ placeholderColor
1148
+ });
1149
+
1150
+ // If there was an image, restore it
1151
+ if (frameMeta !== null && frameMeta !== void 0 && frameMeta.imageSrc) {
1152
+ // Async restoration of image - caller should wait if needed
1153
+ frame.setImage(frameMeta.imageSrc).then(() => {
1154
+ // Restore content position from metadata
1155
+ if (frame._contentImage) {
1156
+ var _frameMeta$contentOff3, _frameMeta$contentOff4, _frameMeta$contentSca2, _frameMeta$contentSca3;
1157
+ frame._contentImage.set({
1158
+ left: (_frameMeta$contentOff3 = frameMeta.contentOffsetX) !== null && _frameMeta$contentOff3 !== void 0 ? _frameMeta$contentOff3 : 0,
1159
+ top: (_frameMeta$contentOff4 = frameMeta.contentOffsetY) !== null && _frameMeta$contentOff4 !== void 0 ? _frameMeta$contentOff4 : 0,
1160
+ scaleX: (_frameMeta$contentSca2 = frameMeta.contentScale) !== null && _frameMeta$contentSca2 !== void 0 ? _frameMeta$contentSca2 : 1,
1161
+ scaleY: (_frameMeta$contentSca3 = frameMeta.contentScale) !== null && _frameMeta$contentSca3 !== void 0 ? _frameMeta$contentSca3 : 1
1162
+ });
1163
+ }
1164
+ frame.set('dirty', true);
1165
+ }).catch(err => {
1166
+ console.warn('Failed to restore frame image:', err);
1167
+ });
1168
+ }
1169
+ return frame;
1170
+ });
1171
+ }
1172
+
1173
+ /**
1174
+ * Creates a Frame with a specific aspect ratio preset
1175
+ *
1176
+ * @param aspect - Aspect ratio preset (e.g., '16:9', '1:1', '4:5', '9:16')
1177
+ * @param size - Base size in pixels
1178
+ * @param options - Additional frame options
1179
+ */
1180
+ static createWithAspect(aspect) {
1181
+ var _defaultMeta$contentS2, _defaultMeta$contentO3, _defaultMeta$contentO4;
1182
+ let size = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 200;
1183
+ let options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
1184
+ let width;
1185
+ let height;
1186
+ switch (aspect) {
1187
+ case '16:9':
1188
+ width = size;
1189
+ height = size * (9 / 16);
1190
+ break;
1191
+ case '9:16':
1192
+ width = size * (9 / 16);
1193
+ height = size;
1194
+ break;
1195
+ case '4:5':
1196
+ width = size * (4 / 5);
1197
+ height = size;
1198
+ break;
1199
+ case '4:3':
1200
+ width = size;
1201
+ height = size * (3 / 4);
1202
+ break;
1203
+ case '3:4':
1204
+ width = size * (3 / 4);
1205
+ height = size;
1206
+ break;
1207
+ case '1:1':
1208
+ default:
1209
+ width = size;
1210
+ height = size;
1211
+ break;
1212
+ }
1213
+ const defaultMeta = frameDefaultValues.frameMeta || {};
1214
+ return new Frame([], {
1215
+ ...options,
1216
+ frameWidth: width,
1217
+ frameHeight: height,
1218
+ frameMeta: {
1219
+ contentScale: (_defaultMeta$contentS2 = defaultMeta.contentScale) !== null && _defaultMeta$contentS2 !== void 0 ? _defaultMeta$contentS2 : 1,
1220
+ contentOffsetX: (_defaultMeta$contentO3 = defaultMeta.contentOffsetX) !== null && _defaultMeta$contentO3 !== void 0 ? _defaultMeta$contentO3 : 0,
1221
+ contentOffsetY: (_defaultMeta$contentO4 = defaultMeta.contentOffsetY) !== null && _defaultMeta$contentO4 !== void 0 ? _defaultMeta$contentO4 : 0,
1222
+ aspect,
1223
+ ...options.frameMeta
1224
+ }
1225
+ });
1226
+ }
1227
+ }
1228
+
1229
+ // Register the Frame class with the class registry
1230
+ _defineProperty(Frame, "type", 'Frame');
1231
+ _defineProperty(Frame, "ownDefaults", frameDefaultValues);
1232
+ classRegistry.setClass(Frame);
1233
+ classRegistry.setClass(Frame, 'frame');
1234
+
1235
+ export { Frame, frameDefaultValues };
1236
+ //# sourceMappingURL=Frame.mjs.map