@overtone-art/canvas-editor-core 0.7.0 → 0.8.0

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.js CHANGED
@@ -21,10 +21,13 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
23
  AnnotationOverlay: () => AnnotationOverlay,
24
+ BLEND_GROUPS: () => BLEND_GROUPS,
25
+ BLEND_MODES: () => BLEND_MODES,
24
26
  CANVAS_MASK_TARGET: () => CANVAS_MASK_TARGET,
25
27
  CANVAS_SIZE_PRESETS: () => CANVAS_SIZE_PRESETS,
26
28
  CanvasEditor: () => CanvasEditor,
27
29
  CropController: () => CropController,
30
+ DEFAULT_GRADIENT_CONFIG: () => DEFAULT_GRADIENT_CONFIG,
28
31
  DEFAULT_LAYER_SHADOW: () => DEFAULT_LAYER_SHADOW,
29
32
  DEFAULT_PATTERN_CONFIG: () => DEFAULT_PATTERN_CONFIG,
30
33
  DEFAULT_SELECTION_STYLE: () => DEFAULT_SELECTION_STYLE,
@@ -50,11 +53,15 @@ __export(index_exports, {
50
53
  TextWrapManager: () => TextWrapManager,
51
54
  TiledPatternObject: () => TiledPatternObject,
52
55
  UnitConverter: () => UnitConverter,
56
+ angleFromCoords: () => angleFromCoords,
53
57
  applyAspectLock: () => applyAspectLock,
54
58
  applyLayerShadow: () => applyLayerShadow,
55
59
  applyObjectSelectionStyle: () => applyObjectSelectionStyle,
56
60
  applySelectionStyle: () => applySelectionStyle,
57
61
  applyTextWrapToObject: () => applyTextWrapToObject,
62
+ blendModeLabel: () => blendModeLabel,
63
+ blendModeOf: () => blendModeOf,
64
+ blendOperation: () => blendOperation,
58
65
  buildCurveLinePaths: () => buildCurveLinePaths,
59
66
  buildCurvePathData: () => buildCurvePathData,
60
67
  clamp: () => clamp,
@@ -80,9 +87,11 @@ __export(index_exports, {
80
87
  isMaskPresetId: () => isMaskPresetId,
81
88
  isShapeMaskId: () => isShapeMaskId,
82
89
  isTextureMaskId: () => isTextureMaskId,
90
+ linearCoords: () => linearCoords,
83
91
  needsAbsoluteSpace: () => needsAbsoluteSpace,
84
92
  normalizeTextCurve: () => normalizeTextCurve,
85
93
  preWrapWordSplit: () => preWrapWordSplit,
94
+ readGradientConfig: () => readGradientConfig,
86
95
  readLayerShadow: () => readLayerShadow,
87
96
  readTextWrap: () => readTextWrap,
88
97
  renderTextureMask: () => renderTextureMask,
@@ -95,13 +104,14 @@ __export(index_exports, {
95
104
  textPathCurve: () => textPathCurve,
96
105
  textPathSpec: () => textPathSpec,
97
106
  toCanvasSpace: () => toCanvasSpace,
107
+ toFabricGradient: () => toFabricGradient,
98
108
  toHostSpace: () => toHostSpace,
99
109
  unwrapGroup: () => unwrapGroup
100
110
  });
101
111
  module.exports = __toCommonJS(index_exports);
102
112
 
103
113
  // src/editor.ts
104
- var import_fabric21 = require("fabric");
114
+ var import_fabric22 = require("fabric");
105
115
 
106
116
  // src/events.ts
107
117
  var EventEmitter = class {
@@ -145,6 +155,53 @@ var import_fabric = require("fabric");
145
155
  var import_nanoid = require("nanoid");
146
156
  var generateId = () => (0, import_nanoid.nanoid)(12);
147
157
 
158
+ // src/blend.ts
159
+ var BLEND_MODES = [
160
+ "normal",
161
+ "darken",
162
+ "multiply",
163
+ "color-burn",
164
+ "lighten",
165
+ "screen",
166
+ "color-dodge",
167
+ "linear-dodge",
168
+ "overlay",
169
+ "soft-light",
170
+ "hard-light",
171
+ "difference",
172
+ "exclusion",
173
+ "hue",
174
+ "saturation",
175
+ "color",
176
+ "luminosity"
177
+ ];
178
+ var OPERATION_EXCEPTIONS = {
179
+ normal: "source-over",
180
+ "linear-dodge": "lighter"
181
+ };
182
+ function blendOperation(mode) {
183
+ return OPERATION_EXCEPTIONS[mode] ?? mode;
184
+ }
185
+ function blendModeOf(operation) {
186
+ if (!operation) return "normal";
187
+ for (const mode of BLEND_MODES) {
188
+ if (blendOperation(mode) === operation) return mode;
189
+ }
190
+ return "normal";
191
+ }
192
+ var BLEND_GROUPS = [
193
+ { label: "Normal", modes: ["normal"] },
194
+ { label: "Darken", modes: ["darken", "multiply", "color-burn"] },
195
+ { label: "Lighten", modes: ["lighten", "screen", "color-dodge", "linear-dodge"] },
196
+ { label: "Contrast", modes: ["overlay", "soft-light", "hard-light"] },
197
+ { label: "Comparative", modes: ["difference", "exclusion"] },
198
+ { label: "Composite", modes: ["hue", "saturation", "color", "luminosity"] }
199
+ ];
200
+ function blendModeLabel(mode) {
201
+ if (mode === "linear-dodge") return "Linear Dodge (Add)";
202
+ return mode.split("-").map((word) => (word[0] ?? "").toUpperCase() + word.slice(1)).join(" ");
203
+ }
204
+
148
205
  // src/layer.ts
149
206
  var Layer = class {
150
207
  id;
@@ -188,6 +245,7 @@ var Layer = class {
188
245
  return Object.keys(this.meta).length > 0;
189
246
  }
190
247
  toData() {
248
+ const blend = blendModeOf(this.fabricObject.globalCompositeOperation);
191
249
  return {
192
250
  id: this.id,
193
251
  type: this.type,
@@ -195,6 +253,7 @@ var Layer = class {
195
253
  visible: this.visible,
196
254
  locked: this.locked,
197
255
  opacity: this.opacity,
256
+ ...blend !== "normal" ? { blend } : {},
198
257
  ...this.hasMeta() ? { meta: this.meta } : {},
199
258
  ...this.children.length > 0 ? { children: this.children.map((c) => c.toData()) } : {}
200
259
  };
@@ -356,7 +415,11 @@ var LayerManager = class {
356
415
  visible: layer.visible,
357
416
  opacity: layer.opacity,
358
417
  selectable: !layer.locked,
359
- evented: !layer.locked
418
+ evented: !layer.locked,
419
+ // Compositing lives only on the fabric object (see `blendModeOf`); carry
420
+ // it from the object being replaced or a blend set before this call
421
+ // silently reverts to normal.
422
+ globalCompositeOperation: previous.globalCompositeOperation
360
423
  });
361
424
  this.canvas.insertAt(Math.max(0, stackIndex), fabricObject);
362
425
  if (layer.renderProxy) {
@@ -386,7 +449,12 @@ var LayerManager = class {
386
449
  visible: layer.visible,
387
450
  opacity: layer.opacity,
388
451
  selectable: !layer.locked,
389
- evented: !layer.locked
452
+ evented: !layer.locked,
453
+ // The proxy is what actually draws once a layer has one (see
454
+ // `LayerManager`'s class doc); a blend set before the proxy existed
455
+ // must not silently stop applying. Read off the layer's own object —
456
+ // that stays the single source of truth (see `blendModeOf`).
457
+ globalCompositeOperation: layer.fabricObject.globalCompositeOperation
390
458
  });
391
459
  this.canvas.add(proxy);
392
460
  this.syncZOrder();
@@ -4702,6 +4770,109 @@ var LayerMaskManager = class extends MaskStackStore {
4702
4770
  }
4703
4771
  };
4704
4772
 
4773
+ // src/gradient.ts
4774
+ var import_fabric21 = require("fabric");
4775
+ var DEFAULT_GRADIENT_CONFIG = {
4776
+ kind: "linear",
4777
+ angle: 90,
4778
+ center: { x: 0.5, y: 0.5 },
4779
+ radius: 0.5,
4780
+ stops: [
4781
+ { color: "#ffffff", opacity: 1, position: 0 },
4782
+ { color: "#000000", opacity: 1, position: 1 }
4783
+ ]
4784
+ };
4785
+ var MIN_RADIUS = 1e-3;
4786
+ var FALLBACK_STOP_COLOR = DEFAULT_GRADIENT_CONFIG.stops[0].color;
4787
+ function linearCoords(angle, box) {
4788
+ const radians = angle * Math.PI / 180;
4789
+ const sin = Math.sin(radians);
4790
+ const cos = Math.cos(radians);
4791
+ const vx = box.width * sin;
4792
+ const vy = -box.height * cos;
4793
+ const length = Math.abs(box.width * sin) + Math.abs(box.height * cos);
4794
+ const scale = length / (vx * vx + vy * vy || 1);
4795
+ return {
4796
+ x1: 0.5 - scale * vx / 2,
4797
+ y1: 0.5 - scale * vy / 2,
4798
+ x2: 0.5 + scale * vx / 2,
4799
+ y2: 0.5 + scale * vy / 2
4800
+ };
4801
+ }
4802
+ function angleFromCoords(coords, box) {
4803
+ const dx = (coords.x2 - coords.x1) / (box.width || 1);
4804
+ const dy = (coords.y2 - coords.y1) / (box.height || 1);
4805
+ const degrees = Math.atan2(dx, -dy) * 180 / Math.PI;
4806
+ return (degrees + 360) % 360;
4807
+ }
4808
+ function stopColor(stop) {
4809
+ const source = isCssColor(stop.color) ? stop.color : FALLBACK_STOP_COLOR;
4810
+ const color = new import_fabric21.Color(source);
4811
+ const opacity = Number.isFinite(stop.opacity) ? stop.opacity : 1;
4812
+ color.setAlpha(clamp(opacity, 0, 1));
4813
+ return color.toRgba();
4814
+ }
4815
+ function toFabricGradient(config, box) {
4816
+ const colorStops = [...config.stops].sort((a, b) => a.position - b.position).map((stop) => {
4817
+ const position = Number.isFinite(stop.position) ? stop.position : 0;
4818
+ return { offset: clamp(position, 0, 1), color: stopColor(stop) };
4819
+ });
4820
+ if (config.kind === "radial") {
4821
+ const radius = Math.max(MIN_RADIUS, config.radius);
4822
+ return new import_fabric21.Gradient({
4823
+ type: "radial",
4824
+ gradientUnits: "percentage",
4825
+ coords: {
4826
+ x1: config.center.x,
4827
+ y1: config.center.y,
4828
+ r1: 0,
4829
+ x2: config.center.x,
4830
+ y2: config.center.y,
4831
+ r2: radius
4832
+ },
4833
+ colorStops
4834
+ });
4835
+ }
4836
+ return new import_fabric21.Gradient({
4837
+ type: "linear",
4838
+ gradientUnits: "percentage",
4839
+ coords: linearCoords(config.angle, box),
4840
+ colorStops
4841
+ });
4842
+ }
4843
+ function readGradientConfig(object) {
4844
+ const fill = object.fill;
4845
+ if (!fill || typeof fill === "string" || !(fill instanceof import_fabric21.Gradient)) return null;
4846
+ const stops = fill.colorStops.map((stop) => {
4847
+ const source = isCssColor(stop.color) ? stop.color : FALLBACK_STOP_COLOR;
4848
+ const color = new import_fabric21.Color(source);
4849
+ const offset = Number.isFinite(stop.offset) ? stop.offset : 0;
4850
+ return {
4851
+ color: `#${color.toHex().toLowerCase()}`,
4852
+ opacity: color.getAlpha(),
4853
+ position: clamp(offset, 0, 1)
4854
+ };
4855
+ });
4856
+ if (fill.type === "radial") {
4857
+ const { x1, y1, r2 } = fill.coords;
4858
+ return {
4859
+ kind: "radial",
4860
+ angle: DEFAULT_GRADIENT_CONFIG.angle,
4861
+ center: { x: x1, y: y1 },
4862
+ radius: r2,
4863
+ stops
4864
+ };
4865
+ }
4866
+ const box = { width: object.width || 1, height: object.height || 1 };
4867
+ return {
4868
+ kind: "linear",
4869
+ angle: angleFromCoords(fill.coords, box),
4870
+ center: { ...DEFAULT_GRADIENT_CONFIG.center },
4871
+ radius: DEFAULT_GRADIENT_CONFIG.radius,
4872
+ stops
4873
+ };
4874
+ }
4875
+
4705
4876
  // src/editor.ts
4706
4877
  var MIN_ZOOM = 0.1;
4707
4878
  var MAX_ZOOM = 8;
@@ -4746,7 +4917,7 @@ var CanvasEditor = class {
4746
4917
  const widthPx = this.units.toPixels(config.width);
4747
4918
  const heightPx = this.units.toPixels(config.height);
4748
4919
  this.designBackground = config.backgroundColor ?? "#ffffff";
4749
- this.canvas = new import_fabric21.Canvas(canvasElement, {
4920
+ this.canvas = new import_fabric22.Canvas(canvasElement, {
4750
4921
  width: widthPx,
4751
4922
  height: heightPx,
4752
4923
  backgroundColor: this.designBackground,
@@ -4781,7 +4952,7 @@ var CanvasEditor = class {
4781
4952
  // ─── Layer Operations ────────────────────────────────
4782
4953
  async addImage(url, options) {
4783
4954
  try {
4784
- const img = await import_fabric21.FabricImage.fromURL(
4955
+ const img = await import_fabric22.FabricImage.fromURL(
4785
4956
  url,
4786
4957
  {},
4787
4958
  { originX: "left", originY: "top", ...options }
@@ -4807,7 +4978,7 @@ var CanvasEditor = class {
4807
4978
  if (this.crop.activeLayerId() === layerId) this.crop.cancel();
4808
4979
  const previous = layer.fabricObject;
4809
4980
  try {
4810
- const replacement = await import_fabric21.FabricImage.fromURL(url, {}, { originX: "left", originY: "top" });
4981
+ const replacement = await import_fabric22.FabricImage.fromURL(url, {}, { originX: "left", originY: "top" });
4811
4982
  replacement.set({
4812
4983
  left: previous.left,
4813
4984
  top: previous.top,
@@ -4836,7 +5007,7 @@ var CanvasEditor = class {
4836
5007
  }
4837
5008
  }
4838
5009
  addText(text, options) {
4839
- const textbox = new import_fabric21.Textbox(text, {
5010
+ const textbox = new import_fabric22.Textbox(text, {
4840
5011
  fontSize: 32,
4841
5012
  fontFamily: "Arial",
4842
5013
  fill: "#000000",
@@ -4858,6 +5029,65 @@ var CanvasEditor = class {
4858
5029
  this.history.save();
4859
5030
  return layer;
4860
5031
  }
5032
+ /**
5033
+ * Insert a gradient layer. It is an ordinary `Rect` with a gradient fill —
5034
+ * which is exactly why the Node print compositor needs no code for it.
5035
+ */
5036
+ addGradient(config = DEFAULT_GRADIENT_CONFIG, box) {
5037
+ const width = box?.width ?? this.canvas.getWidth();
5038
+ const height = box?.height ?? this.canvas.getHeight();
5039
+ const object = new import_fabric22.Rect({
5040
+ left: box?.left ?? 0,
5041
+ top: box?.top ?? 0,
5042
+ originX: "left",
5043
+ originY: "top",
5044
+ width,
5045
+ height,
5046
+ // A gradient fill IS the artwork; an outline is not part of this
5047
+ // layer's design, only its own gradient (unlike a plain shape, where a
5048
+ // stroke is a legitimate look).
5049
+ strokeWidth: 0,
5050
+ fill: toFabricGradient(config, { width, height })
5051
+ });
5052
+ const layer = this.layers.add("gradient", object, "Gradient");
5053
+ this.layers.select(layer.id);
5054
+ this.history.save();
5055
+ return layer;
5056
+ }
5057
+ /**
5058
+ * Repaint a layer's gradient. `save: false` while a slider is being dragged —
5059
+ * the settled value is the one worth an undo step.
5060
+ */
5061
+ setGradient(layerId, config, save = true) {
5062
+ const layer = this.layers.get(layerId);
5063
+ if (!layer) return;
5064
+ const object = layer.fabricObject;
5065
+ object.set({
5066
+ fill: toFabricGradient(config, {
5067
+ width: object.width || 1,
5068
+ height: object.height || 1
5069
+ })
5070
+ });
5071
+ object.dirty = true;
5072
+ this.canvas.requestRenderAll();
5073
+ this.events.emit("layer:modified", { layerId });
5074
+ if (save) this.history.save();
5075
+ }
5076
+ /** Set a layer's blend mode. Applied to the render proxy too, like opacity. */
5077
+ setBlend(layerId, mode) {
5078
+ const layer = this.layers.get(layerId);
5079
+ if (!layer) return;
5080
+ const operation = blendOperation(mode);
5081
+ layer.fabricObject.set({ globalCompositeOperation: operation });
5082
+ layer.fabricObject.dirty = true;
5083
+ if (layer.renderProxy) {
5084
+ layer.renderProxy.set({ globalCompositeOperation: operation });
5085
+ layer.renderProxy.dirty = true;
5086
+ }
5087
+ this.canvas.requestRenderAll();
5088
+ this.events.emit("layer:modified", { layerId });
5089
+ this.history.save();
5090
+ }
4861
5091
  async addTemplate(template, params) {
4862
5092
  const values = {};
4863
5093
  for (const parameter of template.parameters) {
@@ -4967,10 +5197,10 @@ var CanvasEditor = class {
4967
5197
  const next = { ...previous, ...adjustments };
4968
5198
  const clampAdjustment = (value, min = -1) => clamp(value ?? 0, min, 1);
4969
5199
  image.filters = [
4970
- new import_fabric21.filters.Brightness({ brightness: clampAdjustment(next.brightness) }),
4971
- new import_fabric21.filters.Contrast({ contrast: clampAdjustment(next.contrast) }),
4972
- new import_fabric21.filters.Saturation({ saturation: clampAdjustment(next.saturation) }),
4973
- new import_fabric21.filters.Blur({ blur: clampAdjustment(next.blur, 0) })
5200
+ new import_fabric22.filters.Brightness({ brightness: clampAdjustment(next.brightness) }),
5201
+ new import_fabric22.filters.Contrast({ contrast: clampAdjustment(next.contrast) }),
5202
+ new import_fabric22.filters.Saturation({ saturation: clampAdjustment(next.saturation) }),
5203
+ new import_fabric22.filters.Blur({ blur: clampAdjustment(next.blur, 0) })
4974
5204
  ];
4975
5205
  layer.meta.imageAdjustments = next;
4976
5206
  image.applyFilters();
@@ -5011,7 +5241,7 @@ var CanvasEditor = class {
5011
5241
  this.layers.detach(layer.id);
5012
5242
  }
5013
5243
  }
5014
- const group = new import_fabric21.Group(objects);
5244
+ const group = new import_fabric22.Group(objects);
5015
5245
  const grouped = this.layers.add("group", group, name);
5016
5246
  this.layers.adoptChildren(grouped, children);
5017
5247
  this.layers.reorder(grouped.id, insertIndex);
@@ -5073,7 +5303,7 @@ var CanvasEditor = class {
5073
5303
  const layer = this.layers.get(id);
5074
5304
  if (!layer) throw new Error(`Layer not found: ${id}`);
5075
5305
  try {
5076
- if (options.resolution === "source" && layer.fabricObject instanceof import_fabric21.FabricImage) {
5306
+ if (options.resolution === "source" && layer.fabricObject instanceof import_fabric22.FabricImage) {
5077
5307
  const image = await layer.fabricObject.clone();
5078
5308
  image.set({
5079
5309
  left: 0,
@@ -5349,7 +5579,7 @@ var CanvasEditor = class {
5349
5579
  return;
5350
5580
  }
5351
5581
  try {
5352
- const image = await import_fabric21.FabricImage.fromURL(
5582
+ const image = await import_fabric22.FabricImage.fromURL(
5353
5583
  url,
5354
5584
  { crossOrigin: options.crossOrigin ?? null, signal: options.signal },
5355
5585
  { originX: "left", originY: "top" }
@@ -5766,10 +5996,13 @@ var AnnotationOverlay = class {
5766
5996
  // Annotate the CommonJS export names for ESM import in node:
5767
5997
  0 && (module.exports = {
5768
5998
  AnnotationOverlay,
5999
+ BLEND_GROUPS,
6000
+ BLEND_MODES,
5769
6001
  CANVAS_MASK_TARGET,
5770
6002
  CANVAS_SIZE_PRESETS,
5771
6003
  CanvasEditor,
5772
6004
  CropController,
6005
+ DEFAULT_GRADIENT_CONFIG,
5773
6006
  DEFAULT_LAYER_SHADOW,
5774
6007
  DEFAULT_PATTERN_CONFIG,
5775
6008
  DEFAULT_SELECTION_STYLE,
@@ -5795,11 +6028,15 @@ var AnnotationOverlay = class {
5795
6028
  TextWrapManager,
5796
6029
  TiledPatternObject,
5797
6030
  UnitConverter,
6031
+ angleFromCoords,
5798
6032
  applyAspectLock,
5799
6033
  applyLayerShadow,
5800
6034
  applyObjectSelectionStyle,
5801
6035
  applySelectionStyle,
5802
6036
  applyTextWrapToObject,
6037
+ blendModeLabel,
6038
+ blendModeOf,
6039
+ blendOperation,
5803
6040
  buildCurveLinePaths,
5804
6041
  buildCurvePathData,
5805
6042
  clamp,
@@ -5825,9 +6062,11 @@ var AnnotationOverlay = class {
5825
6062
  isMaskPresetId,
5826
6063
  isShapeMaskId,
5827
6064
  isTextureMaskId,
6065
+ linearCoords,
5828
6066
  needsAbsoluteSpace,
5829
6067
  normalizeTextCurve,
5830
6068
  preWrapWordSplit,
6069
+ readGradientConfig,
5831
6070
  readLayerShadow,
5832
6071
  readTextWrap,
5833
6072
  renderTextureMask,
@@ -5840,6 +6079,7 @@ var AnnotationOverlay = class {
5840
6079
  textPathCurve,
5841
6080
  textPathSpec,
5842
6081
  toCanvasSpace,
6082
+ toFabricGradient,
5843
6083
  toHostSpace,
5844
6084
  unwrapGroup
5845
6085
  });