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

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,110 @@ 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 MAX_GRADIENT_STOPS = 256;
4787
+ var FALLBACK_STOP_COLOR = DEFAULT_GRADIENT_CONFIG.stops[0].color;
4788
+ function linearCoords(angle, box) {
4789
+ const radians = angle * Math.PI / 180;
4790
+ const sin = Math.sin(radians);
4791
+ const cos = Math.cos(radians);
4792
+ const vx = box.width * sin;
4793
+ const vy = -box.height * cos;
4794
+ const length = Math.abs(box.width * sin) + Math.abs(box.height * cos);
4795
+ const scale = length / (vx * vx + vy * vy || 1);
4796
+ return {
4797
+ x1: 0.5 - scale * vx / 2,
4798
+ y1: 0.5 - scale * vy / 2,
4799
+ x2: 0.5 + scale * vx / 2,
4800
+ y2: 0.5 + scale * vy / 2
4801
+ };
4802
+ }
4803
+ function angleFromCoords(coords, box) {
4804
+ const dx = (coords.x2 - coords.x1) / (box.width || 1);
4805
+ const dy = (coords.y2 - coords.y1) / (box.height || 1);
4806
+ const degrees = Math.atan2(dx, -dy) * 180 / Math.PI;
4807
+ return (degrees + 360) % 360;
4808
+ }
4809
+ function stopColor(stop) {
4810
+ const source = isCssColor(stop.color) ? stop.color : FALLBACK_STOP_COLOR;
4811
+ const color = new import_fabric21.Color(source);
4812
+ const opacity = Number.isFinite(stop.opacity) ? stop.opacity : 1;
4813
+ color.setAlpha(clamp(opacity, 0, 1));
4814
+ return color.toRgba();
4815
+ }
4816
+ function toFabricGradient(config, box) {
4817
+ const colorStops = [...config.stops].sort((a, b) => a.position - b.position).slice(0, MAX_GRADIENT_STOPS).map((stop) => {
4818
+ const position = Number.isFinite(stop.position) ? stop.position : 0;
4819
+ return { offset: clamp(position, 0, 1), color: stopColor(stop) };
4820
+ });
4821
+ if (config.kind === "radial") {
4822
+ const radius = Math.max(MIN_RADIUS, config.radius);
4823
+ return new import_fabric21.Gradient({
4824
+ type: "radial",
4825
+ gradientUnits: "percentage",
4826
+ coords: {
4827
+ x1: config.center.x,
4828
+ y1: config.center.y,
4829
+ r1: 0,
4830
+ x2: config.center.x,
4831
+ y2: config.center.y,
4832
+ r2: radius
4833
+ },
4834
+ colorStops
4835
+ });
4836
+ }
4837
+ return new import_fabric21.Gradient({
4838
+ type: "linear",
4839
+ gradientUnits: "percentage",
4840
+ coords: linearCoords(config.angle, box),
4841
+ colorStops
4842
+ });
4843
+ }
4844
+ function readGradientConfig(object) {
4845
+ const fill = object.fill;
4846
+ if (!fill || typeof fill === "string" || !(fill instanceof import_fabric21.Gradient)) return null;
4847
+ const stops = fill.colorStops.map((stop) => {
4848
+ const source = isCssColor(stop.color) ? stop.color : FALLBACK_STOP_COLOR;
4849
+ const color = new import_fabric21.Color(source);
4850
+ const offset = Number.isFinite(stop.offset) ? stop.offset : 0;
4851
+ return {
4852
+ color: `#${color.toHex().toLowerCase()}`,
4853
+ opacity: color.getAlpha(),
4854
+ position: clamp(offset, 0, 1)
4855
+ };
4856
+ });
4857
+ if (fill.type === "radial") {
4858
+ const { x1, y1, r2 } = fill.coords;
4859
+ return {
4860
+ kind: "radial",
4861
+ angle: DEFAULT_GRADIENT_CONFIG.angle,
4862
+ center: { x: x1, y: y1 },
4863
+ radius: r2,
4864
+ stops
4865
+ };
4866
+ }
4867
+ const box = { width: object.width || 1, height: object.height || 1 };
4868
+ return {
4869
+ kind: "linear",
4870
+ angle: angleFromCoords(fill.coords, box),
4871
+ center: { ...DEFAULT_GRADIENT_CONFIG.center },
4872
+ radius: DEFAULT_GRADIENT_CONFIG.radius,
4873
+ stops
4874
+ };
4875
+ }
4876
+
4705
4877
  // src/editor.ts
4706
4878
  var MIN_ZOOM = 0.1;
4707
4879
  var MAX_ZOOM = 8;
@@ -4746,7 +4918,7 @@ var CanvasEditor = class {
4746
4918
  const widthPx = this.units.toPixels(config.width);
4747
4919
  const heightPx = this.units.toPixels(config.height);
4748
4920
  this.designBackground = config.backgroundColor ?? "#ffffff";
4749
- this.canvas = new import_fabric21.Canvas(canvasElement, {
4921
+ this.canvas = new import_fabric22.Canvas(canvasElement, {
4750
4922
  width: widthPx,
4751
4923
  height: heightPx,
4752
4924
  backgroundColor: this.designBackground,
@@ -4781,7 +4953,7 @@ var CanvasEditor = class {
4781
4953
  // ─── Layer Operations ────────────────────────────────
4782
4954
  async addImage(url, options) {
4783
4955
  try {
4784
- const img = await import_fabric21.FabricImage.fromURL(
4956
+ const img = await import_fabric22.FabricImage.fromURL(
4785
4957
  url,
4786
4958
  {},
4787
4959
  { originX: "left", originY: "top", ...options }
@@ -4807,7 +4979,7 @@ var CanvasEditor = class {
4807
4979
  if (this.crop.activeLayerId() === layerId) this.crop.cancel();
4808
4980
  const previous = layer.fabricObject;
4809
4981
  try {
4810
- const replacement = await import_fabric21.FabricImage.fromURL(url, {}, { originX: "left", originY: "top" });
4982
+ const replacement = await import_fabric22.FabricImage.fromURL(url, {}, { originX: "left", originY: "top" });
4811
4983
  replacement.set({
4812
4984
  left: previous.left,
4813
4985
  top: previous.top,
@@ -4836,7 +5008,7 @@ var CanvasEditor = class {
4836
5008
  }
4837
5009
  }
4838
5010
  addText(text, options) {
4839
- const textbox = new import_fabric21.Textbox(text, {
5011
+ const textbox = new import_fabric22.Textbox(text, {
4840
5012
  fontSize: 32,
4841
5013
  fontFamily: "Arial",
4842
5014
  fill: "#000000",
@@ -4858,6 +5030,65 @@ var CanvasEditor = class {
4858
5030
  this.history.save();
4859
5031
  return layer;
4860
5032
  }
5033
+ /**
5034
+ * Insert a gradient layer. It is an ordinary `Rect` with a gradient fill —
5035
+ * which is exactly why the Node print compositor needs no code for it.
5036
+ */
5037
+ addGradient(config = DEFAULT_GRADIENT_CONFIG, box) {
5038
+ const width = box?.width ?? this.canvas.getWidth();
5039
+ const height = box?.height ?? this.canvas.getHeight();
5040
+ const object = new import_fabric22.Rect({
5041
+ left: box?.left ?? 0,
5042
+ top: box?.top ?? 0,
5043
+ originX: "left",
5044
+ originY: "top",
5045
+ width,
5046
+ height,
5047
+ // A gradient fill IS the artwork; an outline is not part of this
5048
+ // layer's design, only its own gradient (unlike a plain shape, where a
5049
+ // stroke is a legitimate look).
5050
+ strokeWidth: 0,
5051
+ fill: toFabricGradient(config, { width, height })
5052
+ });
5053
+ const layer = this.layers.add("gradient", object, "Gradient");
5054
+ this.layers.select(layer.id);
5055
+ this.history.save();
5056
+ return layer;
5057
+ }
5058
+ /**
5059
+ * Repaint a layer's gradient. `save: false` while a slider is being dragged —
5060
+ * the settled value is the one worth an undo step.
5061
+ */
5062
+ setGradient(layerId, config, save = true) {
5063
+ const layer = this.layers.get(layerId);
5064
+ if (!layer) return;
5065
+ const object = layer.fabricObject;
5066
+ object.set({
5067
+ fill: toFabricGradient(config, {
5068
+ width: object.width || 1,
5069
+ height: object.height || 1
5070
+ })
5071
+ });
5072
+ object.dirty = true;
5073
+ this.canvas.requestRenderAll();
5074
+ this.events.emit("layer:modified", { layerId });
5075
+ if (save) this.history.save();
5076
+ }
5077
+ /** Set a layer's blend mode. Applied to the render proxy too, like opacity. */
5078
+ setBlend(layerId, mode) {
5079
+ const layer = this.layers.get(layerId);
5080
+ if (!layer) return;
5081
+ const operation = blendOperation(mode);
5082
+ layer.fabricObject.set({ globalCompositeOperation: operation });
5083
+ layer.fabricObject.dirty = true;
5084
+ if (layer.renderProxy) {
5085
+ layer.renderProxy.set({ globalCompositeOperation: operation });
5086
+ layer.renderProxy.dirty = true;
5087
+ }
5088
+ this.canvas.requestRenderAll();
5089
+ this.events.emit("layer:modified", { layerId });
5090
+ this.history.save();
5091
+ }
4861
5092
  async addTemplate(template, params) {
4862
5093
  const values = {};
4863
5094
  for (const parameter of template.parameters) {
@@ -4967,10 +5198,10 @@ var CanvasEditor = class {
4967
5198
  const next = { ...previous, ...adjustments };
4968
5199
  const clampAdjustment = (value, min = -1) => clamp(value ?? 0, min, 1);
4969
5200
  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) })
5201
+ new import_fabric22.filters.Brightness({ brightness: clampAdjustment(next.brightness) }),
5202
+ new import_fabric22.filters.Contrast({ contrast: clampAdjustment(next.contrast) }),
5203
+ new import_fabric22.filters.Saturation({ saturation: clampAdjustment(next.saturation) }),
5204
+ new import_fabric22.filters.Blur({ blur: clampAdjustment(next.blur, 0) })
4974
5205
  ];
4975
5206
  layer.meta.imageAdjustments = next;
4976
5207
  image.applyFilters();
@@ -5011,7 +5242,7 @@ var CanvasEditor = class {
5011
5242
  this.layers.detach(layer.id);
5012
5243
  }
5013
5244
  }
5014
- const group = new import_fabric21.Group(objects);
5245
+ const group = new import_fabric22.Group(objects);
5015
5246
  const grouped = this.layers.add("group", group, name);
5016
5247
  this.layers.adoptChildren(grouped, children);
5017
5248
  this.layers.reorder(grouped.id, insertIndex);
@@ -5073,7 +5304,7 @@ var CanvasEditor = class {
5073
5304
  const layer = this.layers.get(id);
5074
5305
  if (!layer) throw new Error(`Layer not found: ${id}`);
5075
5306
  try {
5076
- if (options.resolution === "source" && layer.fabricObject instanceof import_fabric21.FabricImage) {
5307
+ if (options.resolution === "source" && layer.fabricObject instanceof import_fabric22.FabricImage) {
5077
5308
  const image = await layer.fabricObject.clone();
5078
5309
  image.set({
5079
5310
  left: 0,
@@ -5349,7 +5580,7 @@ var CanvasEditor = class {
5349
5580
  return;
5350
5581
  }
5351
5582
  try {
5352
- const image = await import_fabric21.FabricImage.fromURL(
5583
+ const image = await import_fabric22.FabricImage.fromURL(
5353
5584
  url,
5354
5585
  { crossOrigin: options.crossOrigin ?? null, signal: options.signal },
5355
5586
  { originX: "left", originY: "top" }
@@ -5766,10 +5997,13 @@ var AnnotationOverlay = class {
5766
5997
  // Annotate the CommonJS export names for ESM import in node:
5767
5998
  0 && (module.exports = {
5768
5999
  AnnotationOverlay,
6000
+ BLEND_GROUPS,
6001
+ BLEND_MODES,
5769
6002
  CANVAS_MASK_TARGET,
5770
6003
  CANVAS_SIZE_PRESETS,
5771
6004
  CanvasEditor,
5772
6005
  CropController,
6006
+ DEFAULT_GRADIENT_CONFIG,
5773
6007
  DEFAULT_LAYER_SHADOW,
5774
6008
  DEFAULT_PATTERN_CONFIG,
5775
6009
  DEFAULT_SELECTION_STYLE,
@@ -5795,11 +6029,15 @@ var AnnotationOverlay = class {
5795
6029
  TextWrapManager,
5796
6030
  TiledPatternObject,
5797
6031
  UnitConverter,
6032
+ angleFromCoords,
5798
6033
  applyAspectLock,
5799
6034
  applyLayerShadow,
5800
6035
  applyObjectSelectionStyle,
5801
6036
  applySelectionStyle,
5802
6037
  applyTextWrapToObject,
6038
+ blendModeLabel,
6039
+ blendModeOf,
6040
+ blendOperation,
5803
6041
  buildCurveLinePaths,
5804
6042
  buildCurvePathData,
5805
6043
  clamp,
@@ -5825,9 +6063,11 @@ var AnnotationOverlay = class {
5825
6063
  isMaskPresetId,
5826
6064
  isShapeMaskId,
5827
6065
  isTextureMaskId,
6066
+ linearCoords,
5828
6067
  needsAbsoluteSpace,
5829
6068
  normalizeTextCurve,
5830
6069
  preWrapWordSplit,
6070
+ readGradientConfig,
5831
6071
  readLayerShadow,
5832
6072
  readTextWrap,
5833
6073
  renderTextureMask,
@@ -5840,6 +6080,7 @@ var AnnotationOverlay = class {
5840
6080
  textPathCurve,
5841
6081
  textPathSpec,
5842
6082
  toCanvasSpace,
6083
+ toFabricGradient,
5843
6084
  toHostSpace,
5844
6085
  unwrapGroup
5845
6086
  });