@overtone-art/canvas-editor-core 0.2.2 → 0.2.5

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,12 +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;
385
+ /**
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.
389
+ */
390
+ declare function restoreLocks(obj: FabricObject, locks: PatternLocks | undefined): void;
347
391
  /** Clear the decoded-image cache (e.g. on teardown). Exported for completeness. */
348
392
  declare function clearPatternImageCache(): void;
349
393
  /**
@@ -474,4 +518,4 @@ declare function round2(v: number): number;
474
518
  declare function serializeEditor(editor: CanvasEditor): EditorState;
475
519
  declare function deserializeEditor(editor: CanvasEditor, state: EditorState): Promise<void>;
476
520
 
477
- 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, 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,12 +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;
385
+ /**
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.
389
+ */
390
+ declare function restoreLocks(obj: FabricObject, locks: PatternLocks | undefined): void;
347
391
  /** Clear the decoded-image cache (e.g. on teardown). Exported for completeness. */
348
392
  declare function clearPatternImageCache(): void;
349
393
  /**
@@ -474,4 +518,4 @@ declare function round2(v: number): number;
474
518
  declare function serializeEditor(editor: CanvasEditor): EditorState;
475
519
  declare function deserializeEditor(editor: CanvasEditor, state: EditorState): Promise<void>;
476
520
 
477
- 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, 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,6 +52,7 @@ __export(index_exports, {
50
52
  exportPNG: () => exportPNG,
51
53
  exportSVG: () => exportSVG,
52
54
  generateId: () => generateId,
55
+ restoreLocks: () => restoreLocks,
53
56
  round2: () => round2,
54
57
  serializeEditor: () => serializeEditor
55
58
  });
@@ -633,6 +636,7 @@ var PatternManager = class {
633
636
  config,
634
637
  originalSrc: elementToDataURL(image) ?? image.getSrc(),
635
638
  originalClip: clip ? clip.toObject() : null,
639
+ originalLocks: captureLocks(image),
636
640
  original: {
637
641
  left: image.left ?? 0,
638
642
  top: image.top ?? 0,
@@ -657,6 +661,34 @@ var PatternManager = class {
657
661
  this.history.save();
658
662
  });
659
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
+ }
660
692
  /** Restore the original image and drop the pattern. */
661
693
  disable(layerId) {
662
694
  return this.enqueue(layerId, async () => {
@@ -677,6 +709,7 @@ var PatternManager = class {
677
709
  angle: state.original.angle
678
710
  });
679
711
  image.clipPath = state.originalClip ? (await import_fabric2.util.enlivenObjects([state.originalClip]))[0] : void 0;
712
+ restoreLocks(image, state.originalLocks);
680
713
  image.setCoords();
681
714
  delete layer.meta.pattern;
682
715
  this.canvas.requestRenderAll();
@@ -725,10 +758,43 @@ var PatternManager = class {
725
758
  angle: 0
726
759
  });
727
760
  image.clipPath = void 0;
761
+ applyPatternLocks(image);
728
762
  image.setCoords();
729
763
  this.canvas.requestRenderAll();
730
764
  }
731
765
  };
766
+ function captureLocks(obj) {
767
+ return {
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
774
+ };
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
+ }
732
798
  function elementToDataURL(image) {
733
799
  try {
734
800
  const el = image.getElement();
@@ -797,11 +863,13 @@ function computeTilePositions(config, targetW, targetH, baseW, baseH) {
797
863
  const rows = Math.ceil(diag / tileH) + 2;
798
864
  const halfCols = Math.ceil(cols / 2);
799
865
  const halfRows = Math.ceil(rows / 2);
866
+ const shiftX = tileW * (clampOffset(config.offsetX) / 100);
867
+ const shiftY = tileH * (clampOffset(config.offsetY) / 100);
800
868
  const placements = [];
801
869
  for (let j = -halfRows; j <= halfRows; j++) {
802
870
  for (let i = -halfCols; i <= halfCols; i++) {
803
- let x = i * tileW;
804
- let y = j * tileH;
871
+ let x = i * tileW + shiftX;
872
+ let y = j * tileH + shiftY;
805
873
  if (config.mode === "brick-horizontal" && mod2(j) === 1) {
806
874
  x += tileW * (config.horizontalOffset / 100);
807
875
  } else if (config.mode === "brick-vertical" && mod2(i) === 1) {
@@ -826,6 +894,10 @@ function drawTiles(ctx, img, config, targetW, targetH, baseW, baseH) {
826
894
  }
827
895
  ctx.restore();
828
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
+ }
829
901
  function mod2(n) {
830
902
  return (n % 2 + 2) % 2;
831
903
  }
@@ -1087,6 +1159,7 @@ var CanvasEditor = class {
1087
1159
  }
1088
1160
  async fromJSON(state) {
1089
1161
  await deserializeEditor(this, state);
1162
+ this.patterns.repinAll();
1090
1163
  }
1091
1164
  // ─── Export ──────────────────────────────────────────
1092
1165
  async toPNG(options) {
@@ -1281,6 +1354,8 @@ var DEFAULT_PATTERN_CONFIG = {
1281
1354
  verticalSpacing: 0,
1282
1355
  angle: 0,
1283
1356
  horizontalOffset: 0,
1357
+ offsetX: 0,
1358
+ offsetY: 0,
1284
1359
  rotationStepH: 0,
1285
1360
  rotationStepV: 0
1286
1361
  };
@@ -1296,7 +1371,9 @@ var DEFAULT_PATTERN_CONFIG = {
1296
1371
  PatternManager,
1297
1372
  SnapManager,
1298
1373
  UnitConverter,
1374
+ applyPatternLocks,
1299
1375
  buildPatternDataURL,
1376
+ captureLocks,
1300
1377
  clamp,
1301
1378
  clearPatternImageCache,
1302
1379
  computeTilePositions,
@@ -1306,6 +1383,7 @@ var DEFAULT_PATTERN_CONFIG = {
1306
1383
  exportPNG,
1307
1384
  exportSVG,
1308
1385
  generateId,
1386
+ restoreLocks,
1309
1387
  round2,
1310
1388
  serializeEditor
1311
1389
  });