@overtone-art/canvas-editor-core 0.2.1 → 0.2.3

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
@@ -81,6 +81,12 @@ interface PrintifyPositioning {
81
81
  type TileMode = 'grid' | 'brick-horizontal' | 'brick-vertical';
82
82
  interface PatternConfig {
83
83
  mode: TileMode;
84
+ /**
85
+ * Tile size as a % of the source image's on-canvas size (100 = natural).
86
+ * Smaller values pack more repeats across the print area; larger values make
87
+ * fewer, bigger tiles. Independent of how the source layer was placed.
88
+ */
89
+ scale: number;
84
90
  /** Extra horizontal gap between tiles, as % of tile width. */
85
91
  horizontalSpacing: number;
86
92
  /** Extra vertical gap between tiles, as % of tile height. */
@@ -99,6 +105,14 @@ interface PatternState {
99
105
  config: PatternConfig;
100
106
  /** Original (pre-pattern) image source, as a durable data URL when possible. */
101
107
  originalSrc: string;
108
+ /**
109
+ * Serialized `clipPath` the image carried before it became a pattern, or null
110
+ * if it had none. A pattern must fill the whole print area, so any pre-pattern
111
+ * clip (e.g. a template crop sized to the original box) is stripped while the
112
+ * pattern is on and re-applied when it is turned off. Persisted so it survives
113
+ * serialization / reload / undo.
114
+ */
115
+ originalClip?: Record<string, unknown> | null;
102
116
  /** Original fabric transform, restored when the pattern is turned off. */
103
117
  original: {
104
118
  left: number;
@@ -330,6 +344,25 @@ declare class PatternManager {
330
344
  private enqueue;
331
345
  private renderLayer;
332
346
  }
347
+ /**
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.
356
+ */
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
+ };
333
366
  /** Clear the decoded-image cache (e.g. on teardown). Exported for completeness. */
334
367
  declare function clearPatternImageCache(): void;
335
368
  /**
@@ -460,4 +493,4 @@ declare function round2(v: number): number;
460
493
  declare function serializeEditor(editor: CanvasEditor): EditorState;
461
494
  declare function deserializeEditor(editor: CanvasEditor, state: EditorState): Promise<void>;
462
495
 
463
- 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 };
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 };
package/dist/index.d.ts CHANGED
@@ -81,6 +81,12 @@ interface PrintifyPositioning {
81
81
  type TileMode = 'grid' | 'brick-horizontal' | 'brick-vertical';
82
82
  interface PatternConfig {
83
83
  mode: TileMode;
84
+ /**
85
+ * Tile size as a % of the source image's on-canvas size (100 = natural).
86
+ * Smaller values pack more repeats across the print area; larger values make
87
+ * fewer, bigger tiles. Independent of how the source layer was placed.
88
+ */
89
+ scale: number;
84
90
  /** Extra horizontal gap between tiles, as % of tile width. */
85
91
  horizontalSpacing: number;
86
92
  /** Extra vertical gap between tiles, as % of tile height. */
@@ -99,6 +105,14 @@ interface PatternState {
99
105
  config: PatternConfig;
100
106
  /** Original (pre-pattern) image source, as a durable data URL when possible. */
101
107
  originalSrc: string;
108
+ /**
109
+ * Serialized `clipPath` the image carried before it became a pattern, or null
110
+ * if it had none. A pattern must fill the whole print area, so any pre-pattern
111
+ * clip (e.g. a template crop sized to the original box) is stripped while the
112
+ * pattern is on and re-applied when it is turned off. Persisted so it survives
113
+ * serialization / reload / undo.
114
+ */
115
+ originalClip?: Record<string, unknown> | null;
102
116
  /** Original fabric transform, restored when the pattern is turned off. */
103
117
  original: {
104
118
  left: number;
@@ -330,6 +344,25 @@ declare class PatternManager {
330
344
  private enqueue;
331
345
  private renderLayer;
332
346
  }
347
+ /**
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.
356
+ */
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
+ };
333
366
  /** Clear the decoded-image cache (e.g. on teardown). Exported for completeness. */
334
367
  declare function clearPatternImageCache(): void;
335
368
  /**
@@ -460,4 +493,4 @@ declare function round2(v: number): number;
460
493
  declare function serializeEditor(editor: CanvasEditor): EditorState;
461
494
  declare function deserializeEditor(editor: CanvasEditor, state: EditorState): Promise<void>;
462
495
 
463
- 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 };
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 };
package/dist/index.js CHANGED
@@ -50,13 +50,14 @@ __export(index_exports, {
50
50
  exportPNG: () => exportPNG,
51
51
  exportSVG: () => exportSVG,
52
52
  generateId: () => generateId,
53
+ resolvePatternPlacement: () => resolvePatternPlacement,
53
54
  round2: () => round2,
54
55
  serializeEditor: () => serializeEditor
55
56
  });
56
57
  module.exports = __toCommonJS(index_exports);
57
58
 
58
59
  // src/editor.ts
59
- var import_fabric3 = require("fabric");
60
+ var import_fabric4 = require("fabric");
60
61
 
61
62
  // src/events.ts
62
63
  var EventEmitter = class {
@@ -598,6 +599,8 @@ var CropController = class {
598
599
  var STROKE2 = "#22c55e";
599
600
 
600
601
  // src/pattern.ts
602
+ var import_fabric2 = require("fabric");
603
+ var MAX_TILES_PER_AXIS = 200;
601
604
  var PatternManager = class {
602
605
  constructor(canvas, layers, history) {
603
606
  this.canvas = canvas;
@@ -626,9 +629,11 @@ var PatternManager = class {
626
629
  const image = layer.fabricObject;
627
630
  const firstEnable = !layer.meta.pattern;
628
631
  if (!layer.meta.pattern) {
632
+ const clip = image.clipPath;
629
633
  layer.meta.pattern = {
630
634
  config,
631
635
  originalSrc: elementToDataURL(image) ?? image.getSrc(),
636
+ originalClip: clip ? clip.toObject() : null,
632
637
  original: {
633
638
  left: image.left ?? 0,
634
639
  top: image.top ?? 0,
@@ -645,7 +650,7 @@ var PatternManager = class {
645
650
  layer.meta.pattern.config = config;
646
651
  }
647
652
  try {
648
- await this.renderLayer(layer);
653
+ await this.renderLayer(layer, firstEnable);
649
654
  } catch (err) {
650
655
  if (firstEnable) delete layer.meta.pattern;
651
656
  throw err;
@@ -672,6 +677,7 @@ var PatternManager = class {
672
677
  cropY: state.original.cropY,
673
678
  angle: state.original.angle
674
679
  });
680
+ image.clipPath = state.originalClip ? (await import_fabric2.util.enlivenObjects([state.originalClip]))[0] : void 0;
675
681
  image.setCoords();
676
682
  delete layer.meta.pattern;
677
683
  this.canvas.requestRenderAll();
@@ -688,14 +694,18 @@ var PatternManager = class {
688
694
  );
689
695
  return next;
690
696
  }
691
- async renderLayer(layer) {
697
+ async renderLayer(layer, pin) {
692
698
  const state = layer.meta.pattern;
693
699
  if (!state) return;
694
700
  const image = layer.fabricObject;
695
701
  const cw = this.canvas.getWidth();
696
702
  const ch = this.canvas.getHeight();
697
- const tileW = state.original.width * state.original.scaleX;
698
- const tileH = state.original.height * state.original.scaleY;
703
+ const { left, top, angle } = resolvePatternPlacement(pin, image);
704
+ const scale = Math.max(1, state.config.scale ?? 100) / 100;
705
+ const diag = Math.sqrt(cw * cw + ch * ch);
706
+ const minTile = Math.max(2, diag / MAX_TILES_PER_AXIS);
707
+ const tileW = Math.max(minTile, state.original.width * state.original.scaleX * scale);
708
+ const tileH = Math.max(minTile, state.original.height * state.original.scaleY * scale);
699
709
  const dataUrl = await buildPatternDataURL(
700
710
  state.originalSrc,
701
711
  state.config,
@@ -705,11 +715,30 @@ var PatternManager = class {
705
715
  tileH
706
716
  );
707
717
  await image.setSrc(dataUrl);
708
- image.set({ left: 0, top: 0, scaleX: 1, scaleY: 1, cropX: 0, cropY: 0, angle: 0 });
718
+ image.set({
719
+ left,
720
+ top,
721
+ scaleX: 1,
722
+ scaleY: 1,
723
+ width: cw,
724
+ height: ch,
725
+ cropX: 0,
726
+ cropY: 0,
727
+ angle
728
+ });
729
+ image.clipPath = void 0;
709
730
  image.setCoords();
710
731
  this.canvas.requestRenderAll();
711
732
  }
712
733
  };
734
+ function resolvePatternPlacement(pin, current) {
735
+ if (pin) return { left: 0, top: 0, angle: 0 };
736
+ return {
737
+ left: current.left ?? 0,
738
+ top: current.top ?? 0,
739
+ angle: current.angle ?? 0
740
+ };
741
+ }
713
742
  function elementToDataURL(image) {
714
743
  try {
715
744
  const el = image.getElement();
@@ -770,9 +799,10 @@ async function buildPatternDataURL(src, config, targetW, targetH, baseW, baseH)
770
799
  return off.toDataURL("image/png");
771
800
  }
772
801
  function computeTilePositions(config, targetW, targetH, baseW, baseH) {
773
- const tileW = Math.max(1, baseW * (1 + config.horizontalSpacing / 100));
774
- const tileH = Math.max(1, baseH * (1 + config.verticalSpacing / 100));
775
802
  const diag = Math.sqrt(targetW * targetW + targetH * targetH);
803
+ const minTile = Math.max(1, diag / MAX_TILES_PER_AXIS);
804
+ const tileW = Math.max(minTile, baseW * (1 + config.horizontalSpacing / 100));
805
+ const tileH = Math.max(minTile, baseH * (1 + config.verticalSpacing / 100));
776
806
  const cols = Math.ceil(diag / tileW) + 2;
777
807
  const rows = Math.ceil(diag / tileH) + 2;
778
808
  const halfCols = Math.ceil(cols / 2);
@@ -856,7 +886,7 @@ var UnitConverter = class {
856
886
  };
857
887
 
858
888
  // src/serialization.ts
859
- var import_fabric2 = require("fabric");
889
+ var import_fabric3 = require("fabric");
860
890
  var VERSION = "1.0.0";
861
891
  function serializeEditor(editor) {
862
892
  return {
@@ -887,7 +917,7 @@ async function deserializeEditor(editor, state) {
887
917
  editor.canvas.requestRenderAll();
888
918
  }
889
919
  async function restoreLayer(editor, serialized) {
890
- const objects = await import_fabric2.util.enlivenObjects([serialized.fabricObject]);
920
+ const objects = await import_fabric3.util.enlivenObjects([serialized.fabricObject]);
891
921
  const fabricObject = objects[0];
892
922
  const layer = editor.layers.add(serialized.type, fabricObject, serialized.name, serialized.id);
893
923
  if (serialized.meta) {
@@ -949,7 +979,7 @@ var CanvasEditor = class {
949
979
  const widthPx = this.units.toPixels(config.width);
950
980
  const heightPx = this.units.toPixels(config.height);
951
981
  this.designBackground = config.backgroundColor ?? "#ffffff";
952
- this.canvas = new import_fabric3.Canvas(canvasElement, {
982
+ this.canvas = new import_fabric4.Canvas(canvasElement, {
953
983
  width: widthPx,
954
984
  height: heightPx,
955
985
  backgroundColor: this.designBackground,
@@ -975,13 +1005,13 @@ var CanvasEditor = class {
975
1005
  }
976
1006
  // ─── Layer Operations ────────────────────────────────
977
1007
  async addImage(url, options) {
978
- const img = await import_fabric3.FabricImage.fromURL(url, {}, { originX: "left", originY: "top", ...options });
1008
+ const img = await import_fabric4.FabricImage.fromURL(url, {}, { originX: "left", originY: "top", ...options });
979
1009
  const layer = this.layers.add("image", img);
980
1010
  this.history.save();
981
1011
  return layer;
982
1012
  }
983
1013
  addText(text, options) {
984
- const textbox = new import_fabric3.Textbox(text, {
1014
+ const textbox = new import_fabric4.Textbox(text, {
985
1015
  fontSize: 32,
986
1016
  fontFamily: "Arial",
987
1017
  fill: "#000000",
@@ -1256,6 +1286,7 @@ var CanvasEditor = class {
1256
1286
  // src/types.ts
1257
1287
  var DEFAULT_PATTERN_CONFIG = {
1258
1288
  mode: "grid",
1289
+ scale: 100,
1259
1290
  horizontalSpacing: 0,
1260
1291
  verticalSpacing: 0,
1261
1292
  angle: 0,
@@ -1285,6 +1316,7 @@ var DEFAULT_PATTERN_CONFIG = {
1285
1316
  exportPNG,
1286
1317
  exportSVG,
1287
1318
  generateId,
1319
+ resolvePatternPlacement,
1288
1320
  round2,
1289
1321
  serializeEditor
1290
1322
  });