@leapfrog/interactive-canvas 2.0.25-beta-5 → 2.0.25-beta-6

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.
@@ -340,6 +340,7 @@ export declare abstract class AbstractInteractiveCanvas implements IInteractiveC
340
340
  * @inheritDoc
341
341
  */
342
342
  cloneObject(sourceObject: FabricObject, objectData: CanvasObjectData): Promise<void>;
343
+ private redrawScheduled;
343
344
  /**
344
345
  * Redraw the canvas.
345
346
  */
@@ -232,9 +232,18 @@ class ResizeCanvasLayoutFunction {
232
232
  const targetHeight = increase.withSize(increase.width, baseDimensions.height + increase.height);
233
233
  if (currentDimensions.height !== targetHeight.height) {
234
234
  canvas.setDimensions(targetHeight);
235
- canvas.getObjectsInternal().forEach((object) => object.setCoords());
235
+ // Only update coords for objects affected by height change (pinned to bottom, etc.)
236
+ // This is much faster than updating all objects on slow machines
237
+ canvas.getObjectsInternal().forEach((object) => {
238
+ var _a;
239
+ const canvasObj = canvas.getObjectsByTypeInternal(object.type).find(o => o.getId() === object.getId());
240
+ // Only update coords for objects that are positioned relative to canvas bottom or that might be affected
241
+ if (canvasObj && (((_a = canvasObj.isPinToBottom) === null || _a === void 0 ? void 0 : _a.call(canvasObj)) || object.top > currentDimensions.heightPx * 0.5)) {
242
+ object.setCoords();
243
+ }
244
+ });
245
+ canvas.redraw();
236
246
  }
237
- canvas.redraw();
238
247
  }
239
248
  }
240
249
 
@@ -262,13 +271,22 @@ class AdvancedLayoutManager extends AbstractLayoutManager {
262
271
  this.repositionCutoffs = new RepositionCutoffsLayoutFunction();
263
272
  this.repositionBackgrounds = new RepositionBackgroundsLayoutFunction();
264
273
  this.resizeCanvas = new ResizeCanvasLayoutFunction();
274
+ this.dimensionsLayoutScheduled = false;
265
275
  }
266
276
  onCanvasDimensionsChange() {
267
- this.pinToBottom.execute(this.canvas);
268
- this.repositionCutoffs.execute(this.canvas);
269
- this.repositionBorders.execute(this.canvas);
270
- this.repositionBackgrounds.execute(this.canvas);
271
- this.redrawAll.execute(this.canvas);
277
+ // Batch dimension changes to avoid redundant layout passes
278
+ if (this.dimensionsLayoutScheduled) {
279
+ return;
280
+ }
281
+ this.dimensionsLayoutScheduled = true;
282
+ Promise.resolve().then(() => {
283
+ this.pinToBottom.execute(this.canvas);
284
+ this.repositionCutoffs.execute(this.canvas);
285
+ this.repositionBorders.execute(this.canvas);
286
+ this.repositionBackgrounds.execute(this.canvas);
287
+ this.redrawAll.execute(this.canvas);
288
+ this.dimensionsLayoutScheduled = false;
289
+ });
272
290
  }
273
291
  onTextChange() {
274
292
  this.resizeCanvas.execute(this.canvas);
@@ -296,7 +314,9 @@ class StackObjectsLayoutFunction {
296
314
  const footerObjects = objects[0];
297
315
  const bodyObjects = objects[1];
298
316
  const bodyWidth = canvasDimensions.widthPx - canvasMargin.right - canvasMargin.left;
299
- canvas.redraw();
317
+ //canvas.redraw();
318
+ // No need to redraw here - it will be called by the layout manager after all layout functions complete
319
+ // This reduces redundant render passes significantly
300
320
  //Make one pass and set the widths of all elements. This will force objects with dynamic heights (like text) to take on their necessary height before trying to lay them out.
301
321
  this.applyWidths(allObjects, bodyWidth);
302
322
  const bodyRows = this.getRows(bodyObjects);
@@ -383,7 +403,11 @@ class StackObjectsLayoutFunction {
383
403
  object.setPosition({
384
404
  width: actualWidth
385
405
  });
386
- object.updateDimensions();
406
+ _(objects).forEach((object) => {
407
+ if (object.getType() !== ObjectType.VERTICAL_RULE) {
408
+ object.updateDimensions();
409
+ }
410
+ });
387
411
  });
388
412
  }
389
413
  adjustCanvasSize(canvas, requiredHeight) {
@@ -456,6 +480,7 @@ class StandardLayoutManager extends AbstractLayoutManager {
456
480
  this.repositionBackgrounds = new RepositionBackgroundsLayoutFunction();
457
481
  this.stackObjects = new StackObjectsLayoutFunction();
458
482
  this.updateObjectDimensions = new UpdateObjectDimensionsLayoutFunction();
483
+ this.layoutBatchScheduled = false;
459
484
  }
460
485
  /**
461
486
  * @override
@@ -497,12 +522,21 @@ class StandardLayoutManager extends AbstractLayoutManager {
497
522
  this.stackAndRedraw();
498
523
  }
499
524
  stackAndRedraw() {
500
- this.stackObjects.execute(this.canvas);
501
- this.repositionCutoffs.execute(this.canvas);
502
- this.repositionBorders.execute(this.canvas);
503
- this.repositionBackgrounds.execute(this.canvas);
504
- this.updateObjectDimensions.execute(this.canvas);
505
- this.redrawAll.execute(this.canvas);
525
+ // Batch multiple layout requests into a single pass
526
+ if (this.layoutBatchScheduled) {
527
+ return;
528
+ }
529
+ this.layoutBatchScheduled = true;
530
+ // Use microtask to batch synchronous calls, but still execute before next frame
531
+ Promise.resolve().then(() => {
532
+ this.stackObjects.execute(this.canvas);
533
+ this.repositionCutoffs.execute(this.canvas);
534
+ this.repositionBorders.execute(this.canvas);
535
+ this.repositionBackgrounds.execute(this.canvas);
536
+ this.updateObjectDimensions.execute(this.canvas);
537
+ this.redrawAll.execute(this.canvas);
538
+ this.layoutBatchScheduled = false;
539
+ });
506
540
  }
507
541
  }
508
542
 
@@ -1780,6 +1814,9 @@ var ObjectType;
1780
1814
  class Text extends AbstractCanvasObject {
1781
1815
  constructor(canvas, fabricObject, persistedField) {
1782
1816
  super(ObjectType.TEXT, canvas, fabricObject, persistedField);
1817
+ //private notifyCanvasTimeout: NodeJS.Timeout | null = null;
1818
+ this.notifyCanvasTimeout = null;
1819
+ this.NOTIFY_DELAY_MS = 150; // Debounce canvas notifications
1783
1820
  this.canvas = canvas;
1784
1821
  this.fontLibrary = canvas.getFontLibrary();
1785
1822
  this.baseComponentHeight = this.fabricObject.height;
@@ -2527,9 +2564,15 @@ class Text extends AbstractCanvasObject {
2527
2564
  onTextChange() {
2528
2565
  this.data.userText = this.fabricObject.text;
2529
2566
  this.text$.next(this.fabricObject.text);
2530
- if (this.getBoundingBox().width > this.canvas.getDimensions().widthPx) {
2531
- this.fabricObject.set({ left: 0, width: this.canvas.getDimensions().widthPx });
2532
- this.redraw();
2567
+ // Only check width bounds if text is close to canvas edge (performance optimization)
2568
+ const currentLeft = this.fabricObject.left || 0;
2569
+ const canvasWidth = this.canvas.getDimensions().widthPx;
2570
+ // Skip expensive getBoundingBox if object is clearly within bounds
2571
+ if (currentLeft + (this.fabricObject.width || 0) > canvasWidth * 0.9) {
2572
+ if (this.getBoundingBox().width > canvasWidth) {
2573
+ this.fabricObject.set({ left: 0, width: canvasWidth });
2574
+ this.redraw();
2575
+ }
2533
2576
  }
2534
2577
  }
2535
2578
  onDelete() {
@@ -2550,7 +2593,13 @@ class Text extends AbstractCanvasObject {
2550
2593
  }
2551
2594
  }
2552
2595
  notifyCanvasOfTextChanges() {
2553
- this.canvas.notifyTextChanged();
2596
+ if (this.notifyCanvasTimeout) {
2597
+ clearTimeout(this.notifyCanvasTimeout);
2598
+ }
2599
+ this.notifyCanvasTimeout = setTimeout(() => {
2600
+ this.canvas.notifyTextChanged();
2601
+ this.notifyCanvasTimeout = null;
2602
+ }, this.NOTIFY_DELAY_MS);
2554
2603
  }
2555
2604
  }
2556
2605
  Text.NEW_LINE = "\n";
@@ -2716,6 +2765,7 @@ class AbstractInteractiveCanvas {
2716
2765
  this.devicePixelRatio = devicePixelRatio;
2717
2766
  this.fontLibrary = new FontLibrary();
2718
2767
  this.dimensionChangeRejections$ = new Subject();
2768
+ this.redrawScheduled = false;
2719
2769
  this.IMAGE_CAPTURE_COEFFICIENT = this.calculateImageCaptureCoefficient();
2720
2770
  /**
2721
2771
  * Fabric defaults to only 2 decimal places when saving, which causes havoc on the scaling of large images.
@@ -3581,7 +3631,16 @@ class AbstractInteractiveCanvas {
3581
3631
  * Redraw the canvas.
3582
3632
  */
3583
3633
  redraw() {
3584
- this.fabricCanvas.requestRenderAll();
3634
+ //this.fabricCanvas.requestRenderAll();
3635
+ if (this.redrawScheduled) {
3636
+ return; // Already scheduled, don't schedule again
3637
+ }
3638
+ this.redrawScheduled = true;
3639
+ // Use requestAnimationFrame for smoother rendering and automatic batching
3640
+ requestAnimationFrame(() => {
3641
+ this.fabricCanvas.requestRenderAll();
3642
+ this.redrawScheduled = false;
3643
+ });
3585
3644
  }
3586
3645
  syncObjectList() {
3587
3646
  const newObjectList = _(this.fabricCanvas.getObjects())
@@ -253,9 +253,18 @@ class ResizeCanvasLayoutFunction {
253
253
  const targetHeight = increase.withSize(increase.width, baseDimensions.height + increase.height);
254
254
  if (currentDimensions.height !== targetHeight.height) {
255
255
  canvas.setDimensions(targetHeight);
256
- canvas.getObjectsInternal().forEach((object) => object.setCoords());
256
+ // Only update coords for objects affected by height change (pinned to bottom, etc.)
257
+ // This is much faster than updating all objects on slow machines
258
+ canvas.getObjectsInternal().forEach((object) => {
259
+ var _a;
260
+ const canvasObj = canvas.getObjectsByTypeInternal(object.type).find(o => o.getId() === object.getId());
261
+ // Only update coords for objects that are positioned relative to canvas bottom or that might be affected
262
+ if (canvasObj && (((_a = canvasObj.isPinToBottom) === null || _a === void 0 ? void 0 : _a.call(canvasObj)) || object.top > currentDimensions.heightPx * 0.5)) {
263
+ object.setCoords();
264
+ }
265
+ });
266
+ canvas.redraw();
257
267
  }
258
- canvas.redraw();
259
268
  }
260
269
  }
261
270
 
@@ -283,13 +292,22 @@ class AdvancedLayoutManager extends AbstractLayoutManager {
283
292
  this.repositionCutoffs = new RepositionCutoffsLayoutFunction();
284
293
  this.repositionBackgrounds = new RepositionBackgroundsLayoutFunction();
285
294
  this.resizeCanvas = new ResizeCanvasLayoutFunction();
295
+ this.dimensionsLayoutScheduled = false;
286
296
  }
287
297
  onCanvasDimensionsChange() {
288
- this.pinToBottom.execute(this.canvas);
289
- this.repositionCutoffs.execute(this.canvas);
290
- this.repositionBorders.execute(this.canvas);
291
- this.repositionBackgrounds.execute(this.canvas);
292
- this.redrawAll.execute(this.canvas);
298
+ // Batch dimension changes to avoid redundant layout passes
299
+ if (this.dimensionsLayoutScheduled) {
300
+ return;
301
+ }
302
+ this.dimensionsLayoutScheduled = true;
303
+ Promise.resolve().then(() => {
304
+ this.pinToBottom.execute(this.canvas);
305
+ this.repositionCutoffs.execute(this.canvas);
306
+ this.repositionBorders.execute(this.canvas);
307
+ this.repositionBackgrounds.execute(this.canvas);
308
+ this.redrawAll.execute(this.canvas);
309
+ this.dimensionsLayoutScheduled = false;
310
+ });
293
311
  }
294
312
  onTextChange() {
295
313
  this.resizeCanvas.execute(this.canvas);
@@ -317,7 +335,9 @@ class StackObjectsLayoutFunction {
317
335
  const footerObjects = objects[0];
318
336
  const bodyObjects = objects[1];
319
337
  const bodyWidth = canvasDimensions.widthPx - canvasMargin.right - canvasMargin.left;
320
- canvas.redraw();
338
+ //canvas.redraw();
339
+ // No need to redraw here - it will be called by the layout manager after all layout functions complete
340
+ // This reduces redundant render passes significantly
321
341
  //Make one pass and set the widths of all elements. This will force objects with dynamic heights (like text) to take on their necessary height before trying to lay them out.
322
342
  this.applyWidths(allObjects, bodyWidth);
323
343
  const bodyRows = this.getRows(bodyObjects);
@@ -404,7 +424,11 @@ class StackObjectsLayoutFunction {
404
424
  object.setPosition({
405
425
  width: actualWidth
406
426
  });
407
- object.updateDimensions();
427
+ _(objects).forEach((object) => {
428
+ if (object.getType() !== exports.ObjectType.VERTICAL_RULE) {
429
+ object.updateDimensions();
430
+ }
431
+ });
408
432
  });
409
433
  }
410
434
  adjustCanvasSize(canvas, requiredHeight) {
@@ -477,6 +501,7 @@ class StandardLayoutManager extends AbstractLayoutManager {
477
501
  this.repositionBackgrounds = new RepositionBackgroundsLayoutFunction();
478
502
  this.stackObjects = new StackObjectsLayoutFunction();
479
503
  this.updateObjectDimensions = new UpdateObjectDimensionsLayoutFunction();
504
+ this.layoutBatchScheduled = false;
480
505
  }
481
506
  /**
482
507
  * @override
@@ -518,12 +543,21 @@ class StandardLayoutManager extends AbstractLayoutManager {
518
543
  this.stackAndRedraw();
519
544
  }
520
545
  stackAndRedraw() {
521
- this.stackObjects.execute(this.canvas);
522
- this.repositionCutoffs.execute(this.canvas);
523
- this.repositionBorders.execute(this.canvas);
524
- this.repositionBackgrounds.execute(this.canvas);
525
- this.updateObjectDimensions.execute(this.canvas);
526
- this.redrawAll.execute(this.canvas);
546
+ // Batch multiple layout requests into a single pass
547
+ if (this.layoutBatchScheduled) {
548
+ return;
549
+ }
550
+ this.layoutBatchScheduled = true;
551
+ // Use microtask to batch synchronous calls, but still execute before next frame
552
+ Promise.resolve().then(() => {
553
+ this.stackObjects.execute(this.canvas);
554
+ this.repositionCutoffs.execute(this.canvas);
555
+ this.repositionBorders.execute(this.canvas);
556
+ this.repositionBackgrounds.execute(this.canvas);
557
+ this.updateObjectDimensions.execute(this.canvas);
558
+ this.redrawAll.execute(this.canvas);
559
+ this.layoutBatchScheduled = false;
560
+ });
527
561
  }
528
562
  }
529
563
 
@@ -1801,6 +1835,9 @@ exports.ObjectType = void 0;
1801
1835
  class Text extends AbstractCanvasObject {
1802
1836
  constructor(canvas, fabricObject, persistedField) {
1803
1837
  super(exports.ObjectType.TEXT, canvas, fabricObject, persistedField);
1838
+ //private notifyCanvasTimeout: NodeJS.Timeout | null = null;
1839
+ this.notifyCanvasTimeout = null;
1840
+ this.NOTIFY_DELAY_MS = 150; // Debounce canvas notifications
1804
1841
  this.canvas = canvas;
1805
1842
  this.fontLibrary = canvas.getFontLibrary();
1806
1843
  this.baseComponentHeight = this.fabricObject.height;
@@ -2548,9 +2585,15 @@ class Text extends AbstractCanvasObject {
2548
2585
  onTextChange() {
2549
2586
  this.data.userText = this.fabricObject.text;
2550
2587
  this.text$.next(this.fabricObject.text);
2551
- if (this.getBoundingBox().width > this.canvas.getDimensions().widthPx) {
2552
- this.fabricObject.set({ left: 0, width: this.canvas.getDimensions().widthPx });
2553
- this.redraw();
2588
+ // Only check width bounds if text is close to canvas edge (performance optimization)
2589
+ const currentLeft = this.fabricObject.left || 0;
2590
+ const canvasWidth = this.canvas.getDimensions().widthPx;
2591
+ // Skip expensive getBoundingBox if object is clearly within bounds
2592
+ if (currentLeft + (this.fabricObject.width || 0) > canvasWidth * 0.9) {
2593
+ if (this.getBoundingBox().width > canvasWidth) {
2594
+ this.fabricObject.set({ left: 0, width: canvasWidth });
2595
+ this.redraw();
2596
+ }
2554
2597
  }
2555
2598
  }
2556
2599
  onDelete() {
@@ -2571,7 +2614,13 @@ class Text extends AbstractCanvasObject {
2571
2614
  }
2572
2615
  }
2573
2616
  notifyCanvasOfTextChanges() {
2574
- this.canvas.notifyTextChanged();
2617
+ if (this.notifyCanvasTimeout) {
2618
+ clearTimeout(this.notifyCanvasTimeout);
2619
+ }
2620
+ this.notifyCanvasTimeout = setTimeout(() => {
2621
+ this.canvas.notifyTextChanged();
2622
+ this.notifyCanvasTimeout = null;
2623
+ }, this.NOTIFY_DELAY_MS);
2575
2624
  }
2576
2625
  }
2577
2626
  Text.NEW_LINE = "\n";
@@ -2737,6 +2786,7 @@ class AbstractInteractiveCanvas {
2737
2786
  this.devicePixelRatio = devicePixelRatio;
2738
2787
  this.fontLibrary = new FontLibrary();
2739
2788
  this.dimensionChangeRejections$ = new rxjs.Subject();
2789
+ this.redrawScheduled = false;
2740
2790
  this.IMAGE_CAPTURE_COEFFICIENT = this.calculateImageCaptureCoefficient();
2741
2791
  /**
2742
2792
  * Fabric defaults to only 2 decimal places when saving, which causes havoc on the scaling of large images.
@@ -3602,7 +3652,16 @@ class AbstractInteractiveCanvas {
3602
3652
  * Redraw the canvas.
3603
3653
  */
3604
3654
  redraw() {
3605
- this.fabricCanvas.requestRenderAll();
3655
+ //this.fabricCanvas.requestRenderAll();
3656
+ if (this.redrawScheduled) {
3657
+ return; // Already scheduled, don't schedule again
3658
+ }
3659
+ this.redrawScheduled = true;
3660
+ // Use requestAnimationFrame for smoother rendering and automatic batching
3661
+ requestAnimationFrame(() => {
3662
+ this.fabricCanvas.requestRenderAll();
3663
+ this.redrawScheduled = false;
3664
+ });
3606
3665
  }
3607
3666
  syncObjectList() {
3608
3667
  const newObjectList = _(this.fabricCanvas.getObjects())
@@ -9,6 +9,7 @@ export declare class AdvancedLayoutManager extends AbstractLayoutManager {
9
9
  private repositionCutoffs;
10
10
  private repositionBackgrounds;
11
11
  private resizeCanvas;
12
+ private dimensionsLayoutScheduled;
12
13
  constructor(canvas: IInteractiveCanvas);
13
14
  onCanvasDimensionsChange(): void;
14
15
  onTextChange(): void;
@@ -36,5 +36,6 @@ export declare class StandardLayoutManager extends AbstractLayoutManager {
36
36
  * @override
37
37
  */
38
38
  onMarginChange(): void;
39
+ private layoutBatchScheduled;
39
40
  private stackAndRedraw;
40
41
  }
@@ -63,6 +63,8 @@ export declare class Text extends AbstractCanvasObject<ExtendedFabricTextbox> im
63
63
  private readonly text$;
64
64
  private readonly textSelection$;
65
65
  static readonly NEW_LINE: string;
66
+ private notifyCanvasTimeout;
67
+ private readonly NOTIFY_DELAY_MS;
66
68
  constructor(canvas: IInteractiveCanvas, fabricObject: fabric.Textbox, persistedField?: CanvasObjectData);
67
69
  /**
68
70
  * @override
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leapfrog/interactive-canvas",
3
- "version": "2.0.25-beta-5",
3
+ "version": "2.0.25-beta-6",
4
4
  "description": "Leapfrog interactive canvas",
5
5
  "files": [
6
6
  "dist"