@overtone-art/canvas-editor-core 0.8.4 → 0.8.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.js CHANGED
@@ -4083,10 +4083,127 @@ var MaskController = class {
4083
4083
  };
4084
4084
 
4085
4085
  // src/masks/manager.ts
4086
- var import_fabric20 = require("fabric");
4086
+ var import_fabric21 = require("fabric");
4087
4087
 
4088
4088
  // src/masks/compose.ts
4089
+ var import_fabric16 = require("fabric");
4090
+
4091
+ // src/gradient.ts
4089
4092
  var import_fabric15 = require("fabric");
4093
+ var DEFAULT_GRADIENT_CONFIG = {
4094
+ kind: "linear",
4095
+ angle: 90,
4096
+ center: { x: 0.5, y: 0.5 },
4097
+ radius: 0.5,
4098
+ stops: [
4099
+ { color: "#ffffff", opacity: 1, position: 0 },
4100
+ { color: "#000000", opacity: 1, position: 1 }
4101
+ ]
4102
+ };
4103
+ var MIN_RADIUS = 1e-3;
4104
+ var MAX_GRADIENT_STOPS = 256;
4105
+ var FALLBACK_STOP_COLOR = DEFAULT_GRADIENT_CONFIG.stops[0].color;
4106
+ function linearCoords(angle, box) {
4107
+ const radians = angle * Math.PI / 180;
4108
+ const sin = Math.sin(radians);
4109
+ const cos = Math.cos(radians);
4110
+ const vx = box.width * sin;
4111
+ const vy = -box.height * cos;
4112
+ const length = Math.abs(box.width * sin) + Math.abs(box.height * cos);
4113
+ const scale = length / (vx * vx + vy * vy || 1);
4114
+ return {
4115
+ x1: 0.5 - scale * vx / 2,
4116
+ y1: 0.5 - scale * vy / 2,
4117
+ x2: 0.5 + scale * vx / 2,
4118
+ y2: 0.5 + scale * vy / 2
4119
+ };
4120
+ }
4121
+ function angleFromCoords(coords, box) {
4122
+ const dx = (coords.x2 - coords.x1) / (box.width || 1);
4123
+ const dy = (coords.y2 - coords.y1) / (box.height || 1);
4124
+ const degrees = Math.atan2(dx, -dy) * 180 / Math.PI;
4125
+ return (degrees + 360) % 360;
4126
+ }
4127
+ function stopColor(stop) {
4128
+ const source = isCssColor(stop.color) ? stop.color : FALLBACK_STOP_COLOR;
4129
+ const color = new import_fabric15.Color(source);
4130
+ const opacity = Number.isFinite(stop.opacity) ? stop.opacity : 1;
4131
+ color.setAlpha(clamp(opacity, 0, 1));
4132
+ return color.toRgba();
4133
+ }
4134
+ function toFabricGradient(config, box) {
4135
+ const colorStops = [...config.stops].sort((a, b) => a.position - b.position).slice(0, MAX_GRADIENT_STOPS).map((stop) => {
4136
+ const position = Number.isFinite(stop.position) ? stop.position : 0;
4137
+ return { offset: clamp(position, 0, 1), color: stopColor(stop) };
4138
+ });
4139
+ if (config.kind === "radial") {
4140
+ const radius = Math.max(MIN_RADIUS, config.radius);
4141
+ return new import_fabric15.Gradient({
4142
+ type: "radial",
4143
+ gradientUnits: "percentage",
4144
+ coords: {
4145
+ x1: config.center.x,
4146
+ y1: config.center.y,
4147
+ r1: 0,
4148
+ x2: config.center.x,
4149
+ y2: config.center.y,
4150
+ r2: radius
4151
+ },
4152
+ colorStops
4153
+ });
4154
+ }
4155
+ return new import_fabric15.Gradient({
4156
+ type: "linear",
4157
+ gradientUnits: "percentage",
4158
+ coords: linearCoords(config.angle, box),
4159
+ colorStops
4160
+ });
4161
+ }
4162
+ function toFabricMaskGradient(config, box) {
4163
+ const gradient = toFabricGradient(config, box);
4164
+ gradient.colorStops = gradient.colorStops.map((stop) => {
4165
+ const color = new import_fabric15.Color(stop.color);
4166
+ const [red, green, blue] = color.getSource();
4167
+ const luminance = (0.2126 * red + 0.7152 * green + 0.0722 * blue) / 255;
4168
+ color.setSource([255, 255, 255, clamp(luminance * color.getAlpha(), 0, 1)]);
4169
+ return { offset: stop.offset, color: color.toRgba() };
4170
+ });
4171
+ return gradient;
4172
+ }
4173
+ function readGradientConfig(object) {
4174
+ const fill = object.fill;
4175
+ if (!fill || typeof fill === "string" || !(fill instanceof import_fabric15.Gradient)) return null;
4176
+ const stops = fill.colorStops.map((stop) => {
4177
+ const source = isCssColor(stop.color) ? stop.color : FALLBACK_STOP_COLOR;
4178
+ const color = new import_fabric15.Color(source);
4179
+ const offset = Number.isFinite(stop.offset) ? stop.offset : 0;
4180
+ return {
4181
+ color: `#${color.toHex().toLowerCase()}`,
4182
+ opacity: color.getAlpha(),
4183
+ position: clamp(offset, 0, 1)
4184
+ };
4185
+ });
4186
+ if (fill.type === "radial") {
4187
+ const { x1, y1, r2 } = fill.coords;
4188
+ return {
4189
+ kind: "radial",
4190
+ angle: DEFAULT_GRADIENT_CONFIG.angle,
4191
+ center: { x: x1, y: y1 },
4192
+ radius: r2,
4193
+ stops
4194
+ };
4195
+ }
4196
+ const box = { width: object.width || 1, height: object.height || 1 };
4197
+ return {
4198
+ kind: "linear",
4199
+ angle: angleFromCoords(fill.coords, box),
4200
+ center: { ...DEFAULT_GRADIENT_CONFIG.center },
4201
+ radius: DEFAULT_GRADIENT_CONFIG.radius,
4202
+ stops
4203
+ };
4204
+ }
4205
+
4206
+ // src/masks/compose.ts
4090
4207
  var MODE_OPERATION = {
4091
4208
  add: "source-over",
4092
4209
  subtract: "destination-out",
@@ -4096,7 +4213,7 @@ function neutralize(child) {
4096
4213
  child.set({ opacity: 0, globalCompositeOperation: "source-over" });
4097
4214
  }
4098
4215
  function baseRect(box) {
4099
- return new import_fabric15.Rect({
4216
+ return new import_fabric16.Rect({
4100
4217
  left: box.left,
4101
4218
  top: box.top,
4102
4219
  width: Math.max(1, box.width),
@@ -4116,14 +4233,22 @@ function composeMaskGroup(children, entries, options) {
4116
4233
  neutralize(child);
4117
4234
  return;
4118
4235
  }
4236
+ if (entry.gradient) {
4237
+ child.set({
4238
+ fill: toFabricMaskGradient(entry.gradient, {
4239
+ width: Math.max(1, child.width ?? 1),
4240
+ height: Math.max(1, child.height ?? 1)
4241
+ })
4242
+ });
4243
+ }
4119
4244
  child.set({
4120
4245
  opacity: entry.opacity,
4121
4246
  globalCompositeOperation: MODE_OPERATION[entry.mode] ?? "source-over"
4122
4247
  });
4123
4248
  });
4124
4249
  const first = entries.find((entry) => entry.visible);
4125
- const withBase = first && first.mode !== "add" ? [baseRect(options.box), ...children] : [...children];
4126
- return new import_fabric15.Group(withBase, {
4250
+ const withBase = !first || first.mode !== "add" ? [baseRect(options.box), ...children] : [...children];
4251
+ return new import_fabric16.Group(withBase, {
4127
4252
  absolutePositioned: options.absolute,
4128
4253
  // Cached, so the children's compositing operations resolve against each
4129
4254
  // other instead of against the page underneath the mask.
@@ -4137,11 +4262,105 @@ function needsAbsoluteSpace(entries) {
4137
4262
  }
4138
4263
 
4139
4264
  // src/masks/edit.ts
4140
- var import_fabric16 = require("fabric");
4265
+ var import_fabric17 = require("fabric");
4266
+
4267
+ // src/masks/edit-viewport.ts
4268
+ var MIN_PADDING = 48;
4269
+ var MAX_PADDING = 512;
4270
+ var PADDING_FRACTION = 0.35;
4271
+ var OUTSIDE_SHADE = "rgba(15, 15, 15, 0.55)";
4272
+ var MaskEditViewport = class {
4273
+ constructor(canvas) {
4274
+ this.canvas = canvas;
4275
+ }
4276
+ canvas;
4277
+ state = null;
4278
+ open(handle) {
4279
+ if (this.state) return;
4280
+ const width = this.canvas.getWidth();
4281
+ const height = this.canvas.getHeight();
4282
+ const base = Math.min(
4283
+ MAX_PADDING,
4284
+ Math.max(MIN_PADDING, Math.max(width, height) * PADDING_FRACTION)
4285
+ );
4286
+ handle.setCoords();
4287
+ const bounds = handle.getBoundingRect();
4288
+ const left = Math.min(MAX_PADDING, Math.max(base, -bounds.left + MIN_PADDING));
4289
+ const top = Math.min(MAX_PADDING, Math.max(base, -bounds.top + MIN_PADDING));
4290
+ const right = Math.min(
4291
+ MAX_PADDING,
4292
+ Math.max(base, bounds.left + bounds.width - width + MIN_PADDING)
4293
+ );
4294
+ const bottom = Math.min(
4295
+ MAX_PADDING,
4296
+ Math.max(base, bounds.top + bounds.height - height + MIN_PADDING)
4297
+ );
4298
+ const viewport = [...this.canvas.viewportTransform];
4299
+ const wrapper = this.canvas.wrapperEl;
4300
+ this.state = {
4301
+ width,
4302
+ height,
4303
+ left,
4304
+ top,
4305
+ viewport,
4306
+ wrapperCss: wrapper.style.cssText
4307
+ };
4308
+ this.canvas.setDimensions({ width: width + left + right, height: height + top + bottom });
4309
+ const paddedViewport = [...viewport];
4310
+ paddedViewport[4] += left;
4311
+ paddedViewport[5] += top;
4312
+ this.canvas.setViewportTransform(paddedViewport);
4313
+ wrapper.style.position = "absolute";
4314
+ wrapper.style.left = `${-left}px`;
4315
+ wrapper.style.top = `${-top}px`;
4316
+ wrapper.style.zIndex = "1";
4317
+ this.canvas.on("after:render", this.onAfterRender);
4318
+ this.canvas.calcOffset();
4319
+ this.canvas.requestRenderAll();
4320
+ }
4321
+ close() {
4322
+ const state = this.state;
4323
+ if (!state) return;
4324
+ this.canvas.off("after:render", this.onAfterRender);
4325
+ this.canvas.setViewportTransform(state.viewport);
4326
+ this.canvas.setDimensions({ width: state.width, height: state.height });
4327
+ this.canvas.wrapperEl.style.cssText = state.wrapperCss;
4328
+ this.canvas.calcOffset();
4329
+ this.state = null;
4330
+ }
4331
+ /** Save with real document dimensions, then reopen the same editing view. */
4332
+ checkpoint(handle, save) {
4333
+ this.close();
4334
+ try {
4335
+ save();
4336
+ } finally {
4337
+ this.open(handle);
4338
+ }
4339
+ }
4340
+ onAfterRender = ({ ctx }) => {
4341
+ const state = this.state;
4342
+ if (!state || ctx !== this.canvas.getContext()) return;
4343
+ const fullWidth = this.canvas.getWidth();
4344
+ const fullHeight = this.canvas.getHeight();
4345
+ const right = state.left + state.width;
4346
+ const bottom = state.top + state.height;
4347
+ ctx.save();
4348
+ ctx.fillStyle = OUTSIDE_SHADE;
4349
+ ctx.fillRect(0, 0, fullWidth, state.top);
4350
+ ctx.fillRect(0, bottom, fullWidth, fullHeight - bottom);
4351
+ ctx.fillRect(0, state.top, state.left, state.height);
4352
+ ctx.fillRect(right, state.top, fullWidth - right, state.height);
4353
+ ctx.restore();
4354
+ this.canvas.drawControls(ctx);
4355
+ };
4356
+ };
4357
+
4358
+ // src/masks/edit.ts
4141
4359
  var MaskEditController = class {
4142
4360
  constructor(canvas, onCommit) {
4143
4361
  this.canvas = canvas;
4144
4362
  this.onCommit = onCommit;
4363
+ this.viewport = new MaskEditViewport(canvas);
4145
4364
  }
4146
4365
  canvas;
4147
4366
  onCommit;
@@ -4149,6 +4368,7 @@ var MaskEditController = class {
4149
4368
  editing = null;
4150
4369
  child = null;
4151
4370
  group = null;
4371
+ viewport;
4152
4372
  active() {
4153
4373
  return this.editing;
4154
4374
  }
@@ -4179,6 +4399,7 @@ var MaskEditController = class {
4179
4399
  this.group = group;
4180
4400
  this.canvas.add(handle);
4181
4401
  this.canvas.setActiveObject(handle);
4402
+ this.viewport.open(handle);
4182
4403
  this.canvas.on("object:moving", this.onTransform);
4183
4404
  this.canvas.on("object:scaling", this.onTransform);
4184
4405
  this.canvas.on("object:rotating", this.onTransform);
@@ -4193,6 +4414,7 @@ var MaskEditController = class {
4193
4414
  this.canvas.off("object:scaling", this.onTransform);
4194
4415
  this.canvas.off("object:rotating", this.onTransform);
4195
4416
  this.canvas.off("object:modified", this.onModified);
4417
+ this.viewport.close();
4196
4418
  if (this.canvas.getActiveObject() === this.handle) this.canvas.discardActiveObject();
4197
4419
  this.canvas.remove(this.handle);
4198
4420
  this.handle = null;
@@ -4211,15 +4433,15 @@ var MaskEditController = class {
4211
4433
  onModified = (event) => {
4212
4434
  if (!this.handle || event.target !== this.handle) return;
4213
4435
  this.write();
4214
- this.onCommit();
4436
+ this.viewport.checkpoint(this.handle, this.onCommit);
4215
4437
  };
4216
4438
  /** Handle transform (canvas space) → clip child transform (group-relative). */
4217
4439
  write() {
4218
4440
  if (!this.handle || !this.child || !this.group) return;
4219
4441
  applyMatrix(
4220
4442
  this.child,
4221
- import_fabric16.util.multiplyTransformMatrices(
4222
- import_fabric16.util.invertTransform(matrixOf(this.group)),
4443
+ import_fabric17.util.multiplyTransformMatrices(
4444
+ import_fabric17.util.invertTransform(matrixOf(this.group)),
4223
4445
  matrixOf(this.handle)
4224
4446
  )
4225
4447
  );
@@ -4230,15 +4452,15 @@ var MaskEditController = class {
4230
4452
  };
4231
4453
 
4232
4454
  // src/masks/store.ts
4233
- var import_fabric19 = require("fabric");
4455
+ var import_fabric20 = require("fabric");
4234
4456
 
4235
4457
  // src/masks/host.ts
4236
- var import_fabric17 = require("fabric");
4458
+ var import_fabric18 = require("fabric");
4237
4459
  function findCanvasHost(layers) {
4238
4460
  return layers.getAll().find((layer) => layer.meta.canvasMask);
4239
4461
  }
4240
4462
  function createCanvasHost(canvas, layers) {
4241
- const rect = new import_fabric17.Rect({
4463
+ const rect = new import_fabric18.Rect({
4242
4464
  left: 0,
4243
4465
  top: 0,
4244
4466
  width: canvas.getWidth(),
@@ -4287,9 +4509,9 @@ function hostBoxOf(canvas, host, absolute) {
4287
4509
  }
4288
4510
 
4289
4511
  // src/masks/install.ts
4290
- var import_fabric18 = require("fabric");
4512
+ var import_fabric19 = require("fabric");
4291
4513
  function convertSpace(host, sources, absolute) {
4292
- const wasAbsolute = host.clipPath instanceof import_fabric18.Group ? host.clipPath.absolutePositioned : absolute;
4514
+ const wasAbsolute = host.clipPath instanceof import_fabric19.Group ? host.clipPath.absolutePositioned : absolute;
4293
4515
  if (absolute === wasAbsolute) return;
4294
4516
  for (const source of sources) {
4295
4517
  if (absolute) toCanvasSpace(source, host);
@@ -4342,6 +4564,29 @@ var MaskStackStore = class {
4342
4564
  get(target, maskId) {
4343
4565
  return this.list(target).find((entry) => entry.id === maskId);
4344
4566
  }
4567
+ /** Repaint a gradient mask without replacing or re-fitting its geometry. */
4568
+ setGradient(target, maskId, gradient, options = {}) {
4569
+ const host = this.host(target);
4570
+ if (!host) return false;
4571
+ const entries = [...this.list(target)];
4572
+ const index = entries.findIndex((entry) => entry.id === maskId);
4573
+ if (index === -1) return false;
4574
+ const sources = this.unwrap(target, host);
4575
+ const source = sources[index];
4576
+ if (!source) return false;
4577
+ source.set({
4578
+ fill: toFabricGradient(gradient, {
4579
+ width: Math.max(1, source.width ?? 1),
4580
+ height: Math.max(1, source.height ?? 1)
4581
+ })
4582
+ });
4583
+ entries[index] = {
4584
+ ...entries[index],
4585
+ ...options.meta ? { meta: options.meta } : {}
4586
+ };
4587
+ this.commit(target, host, entries, sources, options.save ?? true);
4588
+ return true;
4589
+ }
4345
4590
  /** Every target that currently carries at least one mask. */
4346
4591
  targets() {
4347
4592
  return this.layers.getAll().filter((layer) => (layer.meta.maskStack ?? []).length > 0).map((layer) => layer.meta.canvasMask ? CANVAS_MASK_TARGET : layer.id);
@@ -4402,14 +4647,14 @@ var MaskStackStore = class {
4402
4647
  const entries = this.list(target);
4403
4648
  const meta = this.hostLayer(target)?.meta;
4404
4649
  if (entries.length === 0 && this.holdsBoxClip(meta)) return [];
4405
- if (entries.length === 0 || !(clip instanceof import_fabric19.Group)) {
4650
+ if (entries.length === 0 || !(clip instanceof import_fabric20.Group)) {
4406
4651
  const source = asObject(clip);
4407
4652
  if (meta?.overflow === "hidden") source.clipPath = void 0;
4408
- return [source];
4653
+ return restoreGradientPaint(entries, [source]);
4409
4654
  }
4410
4655
  const children = unwrapGroup(clip);
4411
4656
  const extra = children.length - entries.length;
4412
- return extra > 0 ? children.slice(extra) : children;
4657
+ return restoreGradientPaint(entries, extra > 0 ? children.slice(extra) : children);
4413
4658
  }
4414
4659
  /**
4415
4660
  * Entries for a stack, adopting a pre-stack clip as the first one. Without
@@ -4442,7 +4687,8 @@ var MaskStackStore = class {
4442
4687
  if (!layer) return;
4443
4688
  const absolute = forceAbsolute || target === CANVAS_MASK_TARGET || needsAbsoluteSpace(entries);
4444
4689
  convertSpace(host, sources, absolute);
4445
- const next = withRelativeTransforms(entries, sources, host, absolute);
4690
+ const painted = captureGradientPaint(entries, sources);
4691
+ const next = withRelativeTransforms(painted, sources, host, absolute);
4446
4692
  installClip(host, next, sources, this.hostBox(target, absolute), absolute);
4447
4693
  if (next.length === 0) {
4448
4694
  delete layer.meta.maskStack;
@@ -4456,12 +4702,41 @@ var MaskStackStore = class {
4456
4702
  if (save) this.history.save();
4457
4703
  }
4458
4704
  };
4705
+ function restoreGradientPaint(entries, sources) {
4706
+ entries.forEach((entry, index) => {
4707
+ const source = sources[index];
4708
+ if (!source || !entry.gradient) return;
4709
+ const box = {
4710
+ width: Math.max(1, source.width ?? 1),
4711
+ height: Math.max(1, source.height ?? 1)
4712
+ };
4713
+ const fill = source.fill;
4714
+ const rendered = toFabricMaskGradient(entry.gradient, box);
4715
+ if (fill instanceof import_fabric20.Gradient && JSON.stringify(fill.toObject()) !== JSON.stringify(rendered.toObject())) {
4716
+ return;
4717
+ }
4718
+ source.set({
4719
+ fill: toFabricGradient(entry.gradient, box)
4720
+ });
4721
+ });
4722
+ return sources;
4723
+ }
4724
+ function captureGradientPaint(entries, sources) {
4725
+ return entries.map((entry, index) => {
4726
+ const gradient = sources[index] ? readGradientConfig(sources[index]) : null;
4727
+ if (gradient) return { ...entry, gradient };
4728
+ if (!entry.gradient) return entry;
4729
+ const next = { ...entry };
4730
+ delete next.gradient;
4731
+ return next;
4732
+ });
4733
+ }
4459
4734
 
4460
4735
  // src/masks/manager.ts
4461
4736
  var LayerMaskManager = class extends MaskStackStore {
4462
4737
  // Committing mid-drag would recompose the group the handle writes into and
4463
4738
  // leave it pointing at a discarded object; the geometry is settled on endEdit.
4464
- edits = new MaskEditController(this.canvas, () => this.history.save());
4739
+ edits = new MaskEditController(this.canvas, () => this.history.saveImmediate());
4465
4740
  onLayersChanged = () => this.pin();
4466
4741
  // Selecting a layer means the user has moved on from the mask they had open;
4467
4742
  // leaving both selected would show mask controls for an unrelated layer.
@@ -4506,7 +4781,7 @@ var LayerMaskManager = class extends MaskStackStore {
4506
4781
  if (index === -1) return false;
4507
4782
  this.endEdit(false);
4508
4783
  this.commit(target, host, [...entries], this.unwrap(target, host), false, true);
4509
- const group = host.clipPath instanceof import_fabric20.Group ? host.clipPath : null;
4784
+ const group = host.clipPath instanceof import_fabric21.Group ? host.clipPath : null;
4510
4785
  if (!group) return false;
4511
4786
  const children = group.getObjects();
4512
4787
  const child = children[children.length - entries.length + index];
@@ -4848,110 +5123,6 @@ function layerTypeOf(object) {
4848
5123
  return "shape";
4849
5124
  }
4850
5125
 
4851
- // src/gradient.ts
4852
- var import_fabric21 = require("fabric");
4853
- var DEFAULT_GRADIENT_CONFIG = {
4854
- kind: "linear",
4855
- angle: 90,
4856
- center: { x: 0.5, y: 0.5 },
4857
- radius: 0.5,
4858
- stops: [
4859
- { color: "#ffffff", opacity: 1, position: 0 },
4860
- { color: "#000000", opacity: 1, position: 1 }
4861
- ]
4862
- };
4863
- var MIN_RADIUS = 1e-3;
4864
- var MAX_GRADIENT_STOPS = 256;
4865
- var FALLBACK_STOP_COLOR = DEFAULT_GRADIENT_CONFIG.stops[0].color;
4866
- function linearCoords(angle, box) {
4867
- const radians = angle * Math.PI / 180;
4868
- const sin = Math.sin(radians);
4869
- const cos = Math.cos(radians);
4870
- const vx = box.width * sin;
4871
- const vy = -box.height * cos;
4872
- const length = Math.abs(box.width * sin) + Math.abs(box.height * cos);
4873
- const scale = length / (vx * vx + vy * vy || 1);
4874
- return {
4875
- x1: 0.5 - scale * vx / 2,
4876
- y1: 0.5 - scale * vy / 2,
4877
- x2: 0.5 + scale * vx / 2,
4878
- y2: 0.5 + scale * vy / 2
4879
- };
4880
- }
4881
- function angleFromCoords(coords, box) {
4882
- const dx = (coords.x2 - coords.x1) / (box.width || 1);
4883
- const dy = (coords.y2 - coords.y1) / (box.height || 1);
4884
- const degrees = Math.atan2(dx, -dy) * 180 / Math.PI;
4885
- return (degrees + 360) % 360;
4886
- }
4887
- function stopColor(stop) {
4888
- const source = isCssColor(stop.color) ? stop.color : FALLBACK_STOP_COLOR;
4889
- const color = new import_fabric21.Color(source);
4890
- const opacity = Number.isFinite(stop.opacity) ? stop.opacity : 1;
4891
- color.setAlpha(clamp(opacity, 0, 1));
4892
- return color.toRgba();
4893
- }
4894
- function toFabricGradient(config, box) {
4895
- const colorStops = [...config.stops].sort((a, b) => a.position - b.position).slice(0, MAX_GRADIENT_STOPS).map((stop) => {
4896
- const position = Number.isFinite(stop.position) ? stop.position : 0;
4897
- return { offset: clamp(position, 0, 1), color: stopColor(stop) };
4898
- });
4899
- if (config.kind === "radial") {
4900
- const radius = Math.max(MIN_RADIUS, config.radius);
4901
- return new import_fabric21.Gradient({
4902
- type: "radial",
4903
- gradientUnits: "percentage",
4904
- coords: {
4905
- x1: config.center.x,
4906
- y1: config.center.y,
4907
- r1: 0,
4908
- x2: config.center.x,
4909
- y2: config.center.y,
4910
- r2: radius
4911
- },
4912
- colorStops
4913
- });
4914
- }
4915
- return new import_fabric21.Gradient({
4916
- type: "linear",
4917
- gradientUnits: "percentage",
4918
- coords: linearCoords(config.angle, box),
4919
- colorStops
4920
- });
4921
- }
4922
- function readGradientConfig(object) {
4923
- const fill = object.fill;
4924
- if (!fill || typeof fill === "string" || !(fill instanceof import_fabric21.Gradient)) return null;
4925
- const stops = fill.colorStops.map((stop) => {
4926
- const source = isCssColor(stop.color) ? stop.color : FALLBACK_STOP_COLOR;
4927
- const color = new import_fabric21.Color(source);
4928
- const offset = Number.isFinite(stop.offset) ? stop.offset : 0;
4929
- return {
4930
- color: `#${color.toHex().toLowerCase()}`,
4931
- opacity: color.getAlpha(),
4932
- position: clamp(offset, 0, 1)
4933
- };
4934
- });
4935
- if (fill.type === "radial") {
4936
- const { x1, y1, r2 } = fill.coords;
4937
- return {
4938
- kind: "radial",
4939
- angle: DEFAULT_GRADIENT_CONFIG.angle,
4940
- center: { x: x1, y: y1 },
4941
- radius: r2,
4942
- stops
4943
- };
4944
- }
4945
- const box = { width: object.width || 1, height: object.height || 1 };
4946
- return {
4947
- kind: "linear",
4948
- angle: angleFromCoords(fill.coords, box),
4949
- center: { ...DEFAULT_GRADIENT_CONFIG.center },
4950
- radius: DEFAULT_GRADIENT_CONFIG.radius,
4951
- stops
4952
- };
4953
- }
4954
-
4955
5126
  // src/editor.ts
4956
5127
  var MIN_ZOOM = 0.01;
4957
5128
  var MAX_ZOOM = 8;