@overtone-art/canvas-editor-core 0.2.3 → 0.2.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.
package/dist/index.d.mts CHANGED
@@ -95,11 +95,29 @@ interface PatternConfig {
95
95
  angle: number;
96
96
  /** Brick row/column shift, as % of tile size. */
97
97
  horizontalOffset: number;
98
+ /**
99
+ * Phase shift of the whole tile grid inside the print area, as % of tile
100
+ * width. The pattern object itself is pinned to the print area (it can't be
101
+ * dragged — that would expose bare edges), so this is how the tiling is
102
+ * nudged into alignment. Clamped to ±100% so coverage is never broken.
103
+ */
104
+ offsetX: number;
105
+ /** Phase shift of the tile grid, as % of tile height. See {@link offsetX}. */
106
+ offsetY: number;
98
107
  /** Added rotation per horizontal step, in degrees. */
99
108
  rotationStepH: number;
100
109
  /** Added rotation per vertical step, in degrees. */
101
110
  rotationStepV: number;
102
111
  }
112
+ /** Fabric interaction flags frozen while a layer is rendered as a pattern. */
113
+ interface PatternLocks {
114
+ lockMovementX: boolean;
115
+ lockMovementY: boolean;
116
+ lockScalingX: boolean;
117
+ lockScalingY: boolean;
118
+ lockRotation: boolean;
119
+ hasControls: boolean;
120
+ }
103
121
  /** Persisted on a layer's meta while it is rendered as a repeating pattern. */
104
122
  interface PatternState {
105
123
  config: PatternConfig;
@@ -113,6 +131,13 @@ interface PatternState {
113
131
  * serialization / reload / undo.
114
132
  */
115
133
  originalClip?: Record<string, unknown> | null;
134
+ /**
135
+ * Interaction flags the object carried before it became a pattern. A pattern
136
+ * fills the whole print area, so while it is on the object is locked in place
137
+ * (a drag/resize would slide the tiled bitmap off the area and leave a bare
138
+ * band); the captured flags are restored when the pattern is turned off.
139
+ */
140
+ originalLocks?: PatternLocks;
116
141
  /** Original fabric transform, restored when the pattern is turned off. */
117
142
  original: {
118
143
  left: number;
@@ -338,31 +363,31 @@ declare class PatternManager {
338
363
  getConfig(layerId: string): PatternConfig | null;
339
364
  /** Turn a plain image layer into a pattern, or update an existing one. */
340
365
  apply(layerId: string, config: PatternConfig): Promise<void>;
366
+ /**
367
+ * Stretch every restored pattern layer back over the full print area and
368
+ * re-freeze it, without re-rasterising: the baked bitmap keeps whatever size
369
+ * it was saved at, so it is scaled (not re-tiled) to the current canvas. Used
370
+ * after a state restore, where the canvas may be a different display size
371
+ * than when the pattern was baked — and to repair states saved while a
372
+ * pattern could still be dragged out of the print area.
373
+ */
374
+ repinAll(): void;
341
375
  /** Restore the original image and drop the pattern. */
342
376
  disable(layerId: string): Promise<void>;
343
377
  /** Run `task` after any in-flight work for this layer, regardless of outcome. */
344
378
  private enqueue;
345
379
  private renderLayer;
346
380
  }
381
+ /** Interaction flags to restore when a pattern is turned off. */
382
+ declare function captureLocks(obj: FabricObject): PatternLocks;
383
+ /** Pin a pattern object: no move, scale or rotate while the pattern is on. */
384
+ declare function applyPatternLocks(obj: FabricObject): void;
347
385
  /**
348
- * Object placement for a pattern re-render.
349
- *
350
- * The FIRST enable pins the pattern to the print-area origin (0,0, unrotated) so
351
- * it fills the whole area. A later config update (scale/angle/spacing slider)
352
- * preserves the object's current placement — otherwise tweaking a slider would
353
- * snap a pattern the user had dragged/rotated back to the top-left corner.
354
- *
355
- * Exported for unit testing.
386
+ * Restore pre-pattern interaction flags. States saved before locking existed
387
+ * carry no snapshot — fall back to a fully interactive object rather than
388
+ * leaving it frozen forever.
356
389
  */
357
- declare function resolvePatternPlacement(pin: boolean, current: {
358
- left?: number;
359
- top?: number;
360
- angle?: number;
361
- }): {
362
- left: number;
363
- top: number;
364
- angle: number;
365
- };
390
+ declare function restoreLocks(obj: FabricObject, locks: PatternLocks | undefined): void;
366
391
  /** Clear the decoded-image cache (e.g. on teardown). Exported for completeness. */
367
392
  declare function clearPatternImageCache(): void;
368
393
  /**
@@ -493,4 +518,4 @@ declare function round2(v: number): number;
493
518
  declare function serializeEditor(editor: CanvasEditor): EditorState;
494
519
  declare function deserializeEditor(editor: CanvasEditor, state: EditorState): Promise<void>;
495
520
 
496
- export { CanvasEditor, CropController, DEFAULT_PATTERN_CONFIG, type EditorConfig, type EditorEvents, type EditorState, EventEmitter, type ExportFormat, type FileAdapter, HistoryManager, type ImageProvider, type ImageProviderResult, Layer, type LayerData, LayerManager, type LayerMeta, type LayerType, type MockupConfig, type MockupPrintArea, type PatternConfig, PatternManager, type PatternState, type PngExportOptions, type PrintifyPositioning, type SerializedLayer, type ShapePlugin, SnapManager, type TemplateDefinition, type TemplateParameter, type TileMode, type TilePlacement, type Unit, UnitConverter, buildPatternDataURL, clamp, clearPatternImageCache, computeTilePositions, deserializeEditor, drawTiles, exportDataURL, exportPNG, exportSVG, generateId, resolvePatternPlacement, round2, serializeEditor };
521
+ export { CanvasEditor, CropController, DEFAULT_PATTERN_CONFIG, type EditorConfig, type EditorEvents, type EditorState, EventEmitter, type ExportFormat, type FileAdapter, HistoryManager, type ImageProvider, type ImageProviderResult, Layer, type LayerData, LayerManager, type LayerMeta, type LayerType, type MockupConfig, type MockupPrintArea, type PatternConfig, type PatternLocks, PatternManager, type PatternState, type PngExportOptions, type PrintifyPositioning, type SerializedLayer, type ShapePlugin, SnapManager, type TemplateDefinition, type TemplateParameter, type TileMode, type TilePlacement, type Unit, UnitConverter, applyPatternLocks, buildPatternDataURL, captureLocks, clamp, clearPatternImageCache, computeTilePositions, deserializeEditor, drawTiles, exportDataURL, exportPNG, exportSVG, generateId, restoreLocks, round2, serializeEditor };
package/dist/index.d.ts CHANGED
@@ -95,11 +95,29 @@ interface PatternConfig {
95
95
  angle: number;
96
96
  /** Brick row/column shift, as % of tile size. */
97
97
  horizontalOffset: number;
98
+ /**
99
+ * Phase shift of the whole tile grid inside the print area, as % of tile
100
+ * width. The pattern object itself is pinned to the print area (it can't be
101
+ * dragged — that would expose bare edges), so this is how the tiling is
102
+ * nudged into alignment. Clamped to ±100% so coverage is never broken.
103
+ */
104
+ offsetX: number;
105
+ /** Phase shift of the tile grid, as % of tile height. See {@link offsetX}. */
106
+ offsetY: number;
98
107
  /** Added rotation per horizontal step, in degrees. */
99
108
  rotationStepH: number;
100
109
  /** Added rotation per vertical step, in degrees. */
101
110
  rotationStepV: number;
102
111
  }
112
+ /** Fabric interaction flags frozen while a layer is rendered as a pattern. */
113
+ interface PatternLocks {
114
+ lockMovementX: boolean;
115
+ lockMovementY: boolean;
116
+ lockScalingX: boolean;
117
+ lockScalingY: boolean;
118
+ lockRotation: boolean;
119
+ hasControls: boolean;
120
+ }
103
121
  /** Persisted on a layer's meta while it is rendered as a repeating pattern. */
104
122
  interface PatternState {
105
123
  config: PatternConfig;
@@ -113,6 +131,13 @@ interface PatternState {
113
131
  * serialization / reload / undo.
114
132
  */
115
133
  originalClip?: Record<string, unknown> | null;
134
+ /**
135
+ * Interaction flags the object carried before it became a pattern. A pattern
136
+ * fills the whole print area, so while it is on the object is locked in place
137
+ * (a drag/resize would slide the tiled bitmap off the area and leave a bare
138
+ * band); the captured flags are restored when the pattern is turned off.
139
+ */
140
+ originalLocks?: PatternLocks;
116
141
  /** Original fabric transform, restored when the pattern is turned off. */
117
142
  original: {
118
143
  left: number;
@@ -338,31 +363,31 @@ declare class PatternManager {
338
363
  getConfig(layerId: string): PatternConfig | null;
339
364
  /** Turn a plain image layer into a pattern, or update an existing one. */
340
365
  apply(layerId: string, config: PatternConfig): Promise<void>;
366
+ /**
367
+ * Stretch every restored pattern layer back over the full print area and
368
+ * re-freeze it, without re-rasterising: the baked bitmap keeps whatever size
369
+ * it was saved at, so it is scaled (not re-tiled) to the current canvas. Used
370
+ * after a state restore, where the canvas may be a different display size
371
+ * than when the pattern was baked — and to repair states saved while a
372
+ * pattern could still be dragged out of the print area.
373
+ */
374
+ repinAll(): void;
341
375
  /** Restore the original image and drop the pattern. */
342
376
  disable(layerId: string): Promise<void>;
343
377
  /** Run `task` after any in-flight work for this layer, regardless of outcome. */
344
378
  private enqueue;
345
379
  private renderLayer;
346
380
  }
381
+ /** Interaction flags to restore when a pattern is turned off. */
382
+ declare function captureLocks(obj: FabricObject): PatternLocks;
383
+ /** Pin a pattern object: no move, scale or rotate while the pattern is on. */
384
+ declare function applyPatternLocks(obj: FabricObject): void;
347
385
  /**
348
- * Object placement for a pattern re-render.
349
- *
350
- * The FIRST enable pins the pattern to the print-area origin (0,0, unrotated) so
351
- * it fills the whole area. A later config update (scale/angle/spacing slider)
352
- * preserves the object's current placement — otherwise tweaking a slider would
353
- * snap a pattern the user had dragged/rotated back to the top-left corner.
354
- *
355
- * Exported for unit testing.
386
+ * Restore pre-pattern interaction flags. States saved before locking existed
387
+ * carry no snapshot — fall back to a fully interactive object rather than
388
+ * leaving it frozen forever.
356
389
  */
357
- declare function resolvePatternPlacement(pin: boolean, current: {
358
- left?: number;
359
- top?: number;
360
- angle?: number;
361
- }): {
362
- left: number;
363
- top: number;
364
- angle: number;
365
- };
390
+ declare function restoreLocks(obj: FabricObject, locks: PatternLocks | undefined): void;
366
391
  /** Clear the decoded-image cache (e.g. on teardown). Exported for completeness. */
367
392
  declare function clearPatternImageCache(): void;
368
393
  /**
@@ -493,4 +518,4 @@ declare function round2(v: number): number;
493
518
  declare function serializeEditor(editor: CanvasEditor): EditorState;
494
519
  declare function deserializeEditor(editor: CanvasEditor, state: EditorState): Promise<void>;
495
520
 
496
- export { CanvasEditor, CropController, DEFAULT_PATTERN_CONFIG, type EditorConfig, type EditorEvents, type EditorState, EventEmitter, type ExportFormat, type FileAdapter, HistoryManager, type ImageProvider, type ImageProviderResult, Layer, type LayerData, LayerManager, type LayerMeta, type LayerType, type MockupConfig, type MockupPrintArea, type PatternConfig, PatternManager, type PatternState, type PngExportOptions, type PrintifyPositioning, type SerializedLayer, type ShapePlugin, SnapManager, type TemplateDefinition, type TemplateParameter, type TileMode, type TilePlacement, type Unit, UnitConverter, buildPatternDataURL, clamp, clearPatternImageCache, computeTilePositions, deserializeEditor, drawTiles, exportDataURL, exportPNG, exportSVG, generateId, resolvePatternPlacement, round2, serializeEditor };
521
+ export { CanvasEditor, CropController, DEFAULT_PATTERN_CONFIG, type EditorConfig, type EditorEvents, type EditorState, EventEmitter, type ExportFormat, type FileAdapter, HistoryManager, type ImageProvider, type ImageProviderResult, Layer, type LayerData, LayerManager, type LayerMeta, type LayerType, type MockupConfig, type MockupPrintArea, type PatternConfig, type PatternLocks, PatternManager, type PatternState, type PngExportOptions, type PrintifyPositioning, type SerializedLayer, type ShapePlugin, SnapManager, type TemplateDefinition, type TemplateParameter, type TileMode, type TilePlacement, type Unit, UnitConverter, applyPatternLocks, buildPatternDataURL, captureLocks, clamp, clearPatternImageCache, computeTilePositions, deserializeEditor, drawTiles, exportDataURL, exportPNG, exportSVG, generateId, restoreLocks, round2, serializeEditor };
package/dist/index.js CHANGED
@@ -40,7 +40,9 @@ __export(index_exports, {
40
40
  PatternManager: () => PatternManager,
41
41
  SnapManager: () => SnapManager,
42
42
  UnitConverter: () => UnitConverter,
43
+ applyPatternLocks: () => applyPatternLocks,
43
44
  buildPatternDataURL: () => buildPatternDataURL,
45
+ captureLocks: () => captureLocks,
44
46
  clamp: () => clamp,
45
47
  clearPatternImageCache: () => clearPatternImageCache,
46
48
  computeTilePositions: () => computeTilePositions,
@@ -50,7 +52,7 @@ __export(index_exports, {
50
52
  exportPNG: () => exportPNG,
51
53
  exportSVG: () => exportSVG,
52
54
  generateId: () => generateId,
53
- resolvePatternPlacement: () => resolvePatternPlacement,
55
+ restoreLocks: () => restoreLocks,
54
56
  round2: () => round2,
55
57
  serializeEditor: () => serializeEditor
56
58
  });
@@ -634,6 +636,7 @@ var PatternManager = class {
634
636
  config,
635
637
  originalSrc: elementToDataURL(image) ?? image.getSrc(),
636
638
  originalClip: clip ? clip.toObject() : null,
639
+ originalLocks: captureLocks(image),
637
640
  original: {
638
641
  left: image.left ?? 0,
639
642
  top: image.top ?? 0,
@@ -650,7 +653,7 @@ var PatternManager = class {
650
653
  layer.meta.pattern.config = config;
651
654
  }
652
655
  try {
653
- await this.renderLayer(layer, firstEnable);
656
+ await this.renderLayer(layer);
654
657
  } catch (err) {
655
658
  if (firstEnable) delete layer.meta.pattern;
656
659
  throw err;
@@ -658,6 +661,34 @@ var PatternManager = class {
658
661
  this.history.save();
659
662
  });
660
663
  }
664
+ /**
665
+ * Stretch every restored pattern layer back over the full print area and
666
+ * re-freeze it, without re-rasterising: the baked bitmap keeps whatever size
667
+ * it was saved at, so it is scaled (not re-tiled) to the current canvas. Used
668
+ * after a state restore, where the canvas may be a different display size
669
+ * than when the pattern was baked — and to repair states saved while a
670
+ * pattern could still be dragged out of the print area.
671
+ */
672
+ repinAll() {
673
+ const cw = this.canvas.getWidth();
674
+ const ch = this.canvas.getHeight();
675
+ let changed = false;
676
+ for (const layer of this.layers.getAll()) {
677
+ if (!layer.meta.pattern || layer.type !== "image") continue;
678
+ const image = layer.fabricObject;
679
+ image.set({
680
+ left: 0,
681
+ top: 0,
682
+ angle: 0,
683
+ scaleX: cw / (image.width || cw),
684
+ scaleY: ch / (image.height || ch)
685
+ });
686
+ applyPatternLocks(image);
687
+ image.setCoords();
688
+ changed = true;
689
+ }
690
+ if (changed) this.canvas.requestRenderAll();
691
+ }
661
692
  /** Restore the original image and drop the pattern. */
662
693
  disable(layerId) {
663
694
  return this.enqueue(layerId, async () => {
@@ -678,6 +709,7 @@ var PatternManager = class {
678
709
  angle: state.original.angle
679
710
  });
680
711
  image.clipPath = state.originalClip ? (await import_fabric2.util.enlivenObjects([state.originalClip]))[0] : void 0;
712
+ restoreLocks(image, state.originalLocks);
681
713
  image.setCoords();
682
714
  delete layer.meta.pattern;
683
715
  this.canvas.requestRenderAll();
@@ -694,13 +726,12 @@ var PatternManager = class {
694
726
  );
695
727
  return next;
696
728
  }
697
- async renderLayer(layer, pin) {
729
+ async renderLayer(layer) {
698
730
  const state = layer.meta.pattern;
699
731
  if (!state) return;
700
732
  const image = layer.fabricObject;
701
733
  const cw = this.canvas.getWidth();
702
734
  const ch = this.canvas.getHeight();
703
- const { left, top, angle } = resolvePatternPlacement(pin, image);
704
735
  const scale = Math.max(1, state.config.scale ?? 100) / 100;
705
736
  const diag = Math.sqrt(cw * cw + ch * ch);
706
737
  const minTile = Math.max(2, diag / MAX_TILES_PER_AXIS);
@@ -716,29 +747,54 @@ var PatternManager = class {
716
747
  );
717
748
  await image.setSrc(dataUrl);
718
749
  image.set({
719
- left,
720
- top,
750
+ left: 0,
751
+ top: 0,
721
752
  scaleX: 1,
722
753
  scaleY: 1,
723
754
  width: cw,
724
755
  height: ch,
725
756
  cropX: 0,
726
757
  cropY: 0,
727
- angle
758
+ angle: 0
728
759
  });
729
760
  image.clipPath = void 0;
761
+ applyPatternLocks(image);
730
762
  image.setCoords();
731
763
  this.canvas.requestRenderAll();
732
764
  }
733
765
  };
734
- function resolvePatternPlacement(pin, current) {
735
- if (pin) return { left: 0, top: 0, angle: 0 };
766
+ function captureLocks(obj) {
736
767
  return {
737
- left: current.left ?? 0,
738
- top: current.top ?? 0,
739
- angle: current.angle ?? 0
768
+ lockMovementX: obj.lockMovementX ?? false,
769
+ lockMovementY: obj.lockMovementY ?? false,
770
+ lockScalingX: obj.lockScalingX ?? false,
771
+ lockScalingY: obj.lockScalingY ?? false,
772
+ lockRotation: obj.lockRotation ?? false,
773
+ hasControls: obj.hasControls ?? true
740
774
  };
741
775
  }
776
+ function applyPatternLocks(obj) {
777
+ obj.set({
778
+ lockMovementX: true,
779
+ lockMovementY: true,
780
+ lockScalingX: true,
781
+ lockScalingY: true,
782
+ lockRotation: true,
783
+ hasControls: false
784
+ });
785
+ }
786
+ function restoreLocks(obj, locks) {
787
+ obj.set(
788
+ locks ?? {
789
+ lockMovementX: false,
790
+ lockMovementY: false,
791
+ lockScalingX: false,
792
+ lockScalingY: false,
793
+ lockRotation: false,
794
+ hasControls: true
795
+ }
796
+ );
797
+ }
742
798
  function elementToDataURL(image) {
743
799
  try {
744
800
  const el = image.getElement();
@@ -807,11 +863,13 @@ function computeTilePositions(config, targetW, targetH, baseW, baseH) {
807
863
  const rows = Math.ceil(diag / tileH) + 2;
808
864
  const halfCols = Math.ceil(cols / 2);
809
865
  const halfRows = Math.ceil(rows / 2);
866
+ const shiftX = tileW * (clampOffset(config.offsetX) / 100);
867
+ const shiftY = tileH * (clampOffset(config.offsetY) / 100);
810
868
  const placements = [];
811
869
  for (let j = -halfRows; j <= halfRows; j++) {
812
870
  for (let i = -halfCols; i <= halfCols; i++) {
813
- let x = i * tileW;
814
- let y = j * tileH;
871
+ let x = i * tileW + shiftX;
872
+ let y = j * tileH + shiftY;
815
873
  if (config.mode === "brick-horizontal" && mod2(j) === 1) {
816
874
  x += tileW * (config.horizontalOffset / 100);
817
875
  } else if (config.mode === "brick-vertical" && mod2(i) === 1) {
@@ -836,6 +894,10 @@ function drawTiles(ctx, img, config, targetW, targetH, baseW, baseH) {
836
894
  }
837
895
  ctx.restore();
838
896
  }
897
+ function clampOffset(value) {
898
+ if (typeof value !== "number" || !Number.isFinite(value)) return 0;
899
+ return Math.max(-100, Math.min(100, value));
900
+ }
839
901
  function mod2(n) {
840
902
  return (n % 2 + 2) % 2;
841
903
  }
@@ -1097,6 +1159,7 @@ var CanvasEditor = class {
1097
1159
  }
1098
1160
  async fromJSON(state) {
1099
1161
  await deserializeEditor(this, state);
1162
+ this.patterns.repinAll();
1100
1163
  }
1101
1164
  // ─── Export ──────────────────────────────────────────
1102
1165
  async toPNG(options) {
@@ -1291,6 +1354,8 @@ var DEFAULT_PATTERN_CONFIG = {
1291
1354
  verticalSpacing: 0,
1292
1355
  angle: 0,
1293
1356
  horizontalOffset: 0,
1357
+ offsetX: 0,
1358
+ offsetY: 0,
1294
1359
  rotationStepH: 0,
1295
1360
  rotationStepV: 0
1296
1361
  };
@@ -1306,7 +1371,9 @@ var DEFAULT_PATTERN_CONFIG = {
1306
1371
  PatternManager,
1307
1372
  SnapManager,
1308
1373
  UnitConverter,
1374
+ applyPatternLocks,
1309
1375
  buildPatternDataURL,
1376
+ captureLocks,
1310
1377
  clamp,
1311
1378
  clearPatternImageCache,
1312
1379
  computeTilePositions,
@@ -1316,7 +1383,7 @@ var DEFAULT_PATTERN_CONFIG = {
1316
1383
  exportPNG,
1317
1384
  exportSVG,
1318
1385
  generateId,
1319
- resolvePatternPlacement,
1386
+ restoreLocks,
1320
1387
  round2,
1321
1388
  serializeEditor
1322
1389
  });