@overtone-art/canvas-editor-core 0.6.1 → 0.6.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.mjs CHANGED
@@ -209,6 +209,19 @@ var LayerManager = class {
209
209
  this.layers = this.layers.filter((l) => l.id !== group.id);
210
210
  this.syncZOrder();
211
211
  }
212
+ /**
213
+ * Move existing layer records under a group, and announce the new tree.
214
+ * Assigning `group.children` directly emits nothing, and the reorder that
215
+ * follows a grouping is a no-op whenever the group already sits where it
216
+ * belongs — leaving `layers:changed` describing a childless group, so a panel
217
+ * built on it renders the group with nothing inside.
218
+ */
219
+ adoptChildren(group, children) {
220
+ for (const child of children) child.parentId = group.id;
221
+ group.children = children;
222
+ this.emitChanged();
223
+ this.onPropertyChanged?.();
224
+ }
212
225
  /**
213
226
  * Re-attach child records to an enlivened group. `data[i]` describes
214
227
  * `group._objects[i]` — the order fabric serializes and restores them in. A
@@ -1130,7 +1143,60 @@ var STROKE2 = "#22c55e";
1130
1143
  import { util } from "fabric";
1131
1144
 
1132
1145
  // src/pattern/tiled-pattern-object.ts
1133
- import { FabricObject, Point } from "fabric";
1146
+ import { FabricObject, Point as Point2 } from "fabric";
1147
+
1148
+ // src/text-fit.ts
1149
+ import { Point } from "fabric";
1150
+ var TEXT_FIT_SLACK = 0.5;
1151
+ var MEASURE_WIDTH = 1e5;
1152
+ function unwrappedWidth(text) {
1153
+ const authored = text.width;
1154
+ try {
1155
+ text.set({ width: MEASURE_WIDTH });
1156
+ text.initDimensions?.();
1157
+ const measured = text.calcTextWidth?.() ?? authored;
1158
+ return Number.isFinite(measured) && measured > 0 ? measured : authored;
1159
+ } finally {
1160
+ text.set({ width: authored });
1161
+ text.initDimensions?.();
1162
+ }
1163
+ }
1164
+ function asText(object) {
1165
+ if (!object) return null;
1166
+ const text = object;
1167
+ return typeof text.text === "string" && typeof text.calcTextWidth === "function" ? text : null;
1168
+ }
1169
+ function textInk(text) {
1170
+ const boxWidth = Math.max(0, text.width ?? 0);
1171
+ const measured = text.calcTextWidth?.() ?? boxWidth;
1172
+ const width = Math.max(1, Math.min(boxWidth, Number.isFinite(measured) ? measured : boxWidth));
1173
+ const slack = (boxWidth - width) / 2;
1174
+ const align = text.textAlign ?? "left";
1175
+ if (align.includes("center")) return { width, dx: 0 };
1176
+ const flip = text.flipX ? -1 : 1;
1177
+ return { width, dx: flip * (align.includes("right") ? slack : -slack) };
1178
+ }
1179
+ function fitTextWidth(object) {
1180
+ const text = asText(object);
1181
+ if (!text || text.path) return false;
1182
+ const before = textInk(text);
1183
+ const fitted = unwrappedWidth(text) + TEXT_FIT_SLACK;
1184
+ if (Math.abs(fitted - text.width) < TEXT_FIT_SLACK) return false;
1185
+ const centre = text.getCenterPoint();
1186
+ text.set({ width: fitted });
1187
+ text.initDimensions?.();
1188
+ const after = textInk(text);
1189
+ const shift = (before.dx - after.dx) * (text.scaleX ?? 1);
1190
+ const radians = (text.angle ?? 0) * Math.PI / 180;
1191
+ const moved = new Point(
1192
+ centre.x + shift * Math.cos(radians),
1193
+ centre.y + shift * Math.sin(radians)
1194
+ );
1195
+ text.setPositionByOrigin(moved, "center", "center");
1196
+ text.setCoords();
1197
+ text.dirty = true;
1198
+ return true;
1199
+ }
1134
1200
 
1135
1201
  // src/pattern/tile-geometry.ts
1136
1202
  var MAX_TILES_PER_AXIS = 200;
@@ -1143,13 +1209,13 @@ function computeTilePositions(config, targetW, targetH, baseW, baseH, origin) {
1143
1209
  const tileH = Math.max(minTile, baseH * (1 + config.verticalSpacing / 100));
1144
1210
  const cols = Math.ceil(span / tileW) + 2;
1145
1211
  const rows = Math.ceil(span / tileH) + 2;
1146
- const halfCols = Math.ceil(cols / 2);
1147
- const halfRows = Math.ceil(rows / 2);
1212
+ const [colFrom, colTo] = axisRange(config.horizontalLimit, Math.ceil(cols / 2));
1213
+ const [rowFrom, rowTo] = axisRange(config.verticalLimit, Math.ceil(rows / 2));
1148
1214
  const shiftX = tileW * (clampOffset(config.offsetX) / 100);
1149
1215
  const shiftY = tileH * (clampOffset(config.offsetY) / 100);
1150
1216
  const placements = [];
1151
- for (let j = -halfRows; j <= halfRows; j++) {
1152
- for (let i = -halfCols; i <= halfCols; i++) {
1217
+ for (let j = rowFrom; j <= rowTo; j++) {
1218
+ for (let i = colFrom; i <= colTo; i++) {
1153
1219
  let x = i * tileW + shiftX;
1154
1220
  let y = j * tileH + shiftY;
1155
1221
  if (config.mode === "brick-horizontal" && mod2(j) === 1) {
@@ -1179,6 +1245,12 @@ function drawTiles(ctx, img, config, targetW, targetH, baseW, baseH, origin, dra
1179
1245
  }
1180
1246
  ctx.restore();
1181
1247
  }
1248
+ function axisRange(limit, half) {
1249
+ const count = Number.isFinite(limit) ? Math.floor(limit) : 0;
1250
+ if (!count || count <= 0) return [-half, half];
1251
+ const from = -Math.floor((count - 1) / 2);
1252
+ return [from, from + count - 1];
1253
+ }
1182
1254
  function cornerRadius(origin, w, h) {
1183
1255
  const dx = Math.max(Math.abs(origin.x), Math.abs(w - origin.x));
1184
1256
  const dy = Math.max(Math.abs(origin.y), Math.abs(h - origin.y));
@@ -1250,7 +1322,7 @@ var TiledPatternObject = class _TiledPatternObject extends FabricObject {
1250
1322
  */
1251
1323
  syncBox() {
1252
1324
  const { tileW, tileH } = this.baseTile();
1253
- const origin = this.source.getCenterPoint();
1325
+ const origin = this.source.getCenterPoint().add(this.inkOffset());
1254
1326
  const anchor = this.anchorFromOrigin(origin, tileW, tileH, this.config.angle);
1255
1327
  this.set({
1256
1328
  left: anchor.x,
@@ -1280,7 +1352,8 @@ var TiledPatternObject = class _TiledPatternObject extends FabricObject {
1280
1352
  const { tileW, tileH } = this.liveTile();
1281
1353
  const centre = this.getCenterPoint();
1282
1354
  const shift = this.shiftVector(tileW, tileH, this.liveAngle());
1283
- return new Point(centre.x - shift.x, centre.y - shift.y);
1355
+ const ink = this.inkOffset();
1356
+ return new Point2(centre.x - shift.x - ink.x, centre.y - shift.y - ink.y);
1284
1357
  }
1285
1358
  _render(ctx) {
1286
1359
  const { width, height } = this.area;
@@ -1295,8 +1368,7 @@ var TiledPatternObject = class _TiledPatternObject extends FabricObject {
1295
1368
  ctx.scale(1 / (this.scaleX || 1), 1 / (this.scaleY || 1));
1296
1369
  ctx.rotate(-angle * Math.PI / 180);
1297
1370
  ctx.translate(-centre.x, -centre.y);
1298
- const shift = this.shiftVector(tileW, tileH, angle);
1299
- const origin = { x: centre.x - shift.x, y: centre.y - shift.y };
1371
+ const origin = { x: centre.x, y: centre.y };
1300
1372
  const config = { ...this.config, angle, offsetX: 0, offsetY: 0 };
1301
1373
  drawTiles(
1302
1374
  ctx,
@@ -1325,7 +1397,6 @@ var TiledPatternObject = class _TiledPatternObject extends FabricObject {
1325
1397
  if (!snapshot) return [];
1326
1398
  const centre = this.getCenterPoint();
1327
1399
  const angle = this.liveAngle();
1328
- const shift = this.shiftVector(tileW, tileH, angle);
1329
1400
  drawTiles(
1330
1401
  ctx,
1331
1402
  snapshot,
@@ -1334,7 +1405,7 @@ var TiledPatternObject = class _TiledPatternObject extends FabricObject {
1334
1405
  height,
1335
1406
  tileW,
1336
1407
  tileH,
1337
- { x: centre.x - shift.x, y: centre.y - shift.y },
1408
+ { x: centre.x, y: centre.y },
1338
1409
  // The fallback raster is built at 1:1, so a device pixel is a canvas unit.
1339
1410
  this.drawSize(tileW, tileH, TILE_BLEED_DEVICE_PX)
1340
1411
  );
@@ -1377,7 +1448,7 @@ var TiledPatternObject = class _TiledPatternObject extends FabricObject {
1377
1448
  return Promise.resolve(copy);
1378
1449
  }
1379
1450
  /**
1380
- * The source's tile footprint: its bounding box, minus a stroke it reserves
1451
+ * The source's tile footprint: the art it paints, minus a stroke it reserves
1381
1452
  * room for but never paints.
1382
1453
  *
1383
1454
  * fabric keeps `strokeWidth` inside an object's box whether or not a `stroke`
@@ -1385,11 +1456,18 @@ var TiledPatternObject = class _TiledPatternObject extends FabricObject {
1385
1456
  * stroke. Stepping the grid by that box puts a transparent seam between every
1386
1457
  * pair of neighbours at zero spacing: the artwork is a pixel narrower than the
1387
1458
  * box it is stepped by.
1459
+ *
1460
+ * Text is the same problem an order of magnitude larger — see {@link textInk}.
1388
1461
  */
1389
1462
  sourceBox() {
1463
+ const text = asText(this.source);
1464
+ const paints = paintsStroke(this.source);
1390
1465
  const rect = this.source.getBoundingRect();
1391
- if (paintsStroke(this.source)) return { width: rect.width, height: rect.height };
1392
- const dims = this.source._getTransformedDimensions({ strokeWidth: 0 });
1466
+ if (paints && !text) return { width: rect.width, height: rect.height };
1467
+ const options = {};
1468
+ if (!paints) options.strokeWidth = 0;
1469
+ if (text) options.width = textInk(text).width;
1470
+ const dims = this.source._getTransformedDimensions(options);
1393
1471
  const radians = (this.source.angle ?? 0) * Math.PI / 180;
1394
1472
  const cos = Math.abs(Math.cos(radians));
1395
1473
  const sin = Math.abs(Math.sin(radians));
@@ -1398,6 +1476,22 @@ var TiledPatternObject = class _TiledPatternObject extends FabricObject {
1398
1476
  height: dims.x * sin + dims.y * cos
1399
1477
  };
1400
1478
  }
1479
+ /**
1480
+ * Vector from the source's centre to the centre of the art it paints.
1481
+ *
1482
+ * Zero for everything except text that does not fill its box: the tile has to
1483
+ * sit on the glyphs, or the frame the user drags frames empty space and the
1484
+ * grid registers off the run by half the padding.
1485
+ */
1486
+ inkOffset() {
1487
+ const text = asText(this.source);
1488
+ if (!text) return new Point2(0, 0);
1489
+ const { dx } = textInk(text);
1490
+ if (!dx) return new Point2(0, 0);
1491
+ const scaled = dx * (this.source.scaleX ?? 1);
1492
+ const radians = (this.source.angle ?? 0) * Math.PI / 180;
1493
+ return new Point2(scaled * Math.cos(radians), scaled * Math.sin(radians));
1494
+ }
1401
1495
  /** The source's on-canvas width, before the tile scale. */
1402
1496
  baseW() {
1403
1497
  return Math.max(1, this.sourceBox().width);
@@ -1457,11 +1551,11 @@ var TiledPatternObject = class _TiledPatternObject extends FabricObject {
1457
1551
  const radians = angle * Math.PI / 180;
1458
1552
  const cos = Math.cos(radians);
1459
1553
  const sin = Math.sin(radians);
1460
- return new Point(dx * cos - dy * sin, dx * sin + dy * cos);
1554
+ return new Point2(dx * cos - dy * sin, dx * sin + dy * cos);
1461
1555
  }
1462
1556
  anchorFromOrigin(origin, tileW, tileH, angle) {
1463
1557
  const shift = this.shiftVector(tileW, tileH, angle);
1464
- return new Point(origin.x + shift.x, origin.y + shift.y);
1558
+ return new Point2(origin.x + shift.x, origin.y + shift.y);
1465
1559
  }
1466
1560
  /**
1467
1561
  * Snapshot the source at (at least) `scale`, reusing the cached one while it
@@ -1474,7 +1568,15 @@ var TiledPatternObject = class _TiledPatternObject extends FabricObject {
1474
1568
  const source = this.source;
1475
1569
  const opacity = source.opacity;
1476
1570
  source.opacity = 1;
1571
+ const text = asText(source);
1572
+ const authoredWidth = text?.width;
1573
+ const fitted = text ? textInk(text).width + TEXT_FIT_SLACK : 0;
1574
+ const hug = text !== null && authoredWidth !== void 0 && fitted < authoredWidth;
1477
1575
  try {
1576
+ if (hug && text) {
1577
+ text.set({ width: fitted });
1578
+ text.initDimensions?.();
1579
+ }
1478
1580
  const el = source.toCanvasElement({ multiplier: wanted, enableRetinaScaling: false });
1479
1581
  if (!el.width || !el.height) return null;
1480
1582
  this.snapshotEl = el;
@@ -1483,6 +1585,10 @@ var TiledPatternObject = class _TiledPatternObject extends FabricObject {
1483
1585
  return this.snapshotEl;
1484
1586
  } finally {
1485
1587
  source.opacity = opacity;
1588
+ if (hug && text && authoredWidth !== void 0) {
1589
+ text.set({ width: authoredWidth });
1590
+ text.initDimensions?.();
1591
+ }
1486
1592
  source.dirty = false;
1487
1593
  }
1488
1594
  return this.snapshotEl;
@@ -1859,17 +1965,55 @@ function buildCurvePathData(config, width, fontSize) {
1859
1965
  }
1860
1966
 
1861
1967
  // src/text-curve.ts
1862
- var MEASURE_WIDTH = 1e5;
1968
+ var MEASURE_WIDTH2 = 1e5;
1863
1969
  var PATH_SLACK = 0.06;
1864
1970
  var FALLBACK_LINE_HEIGHT = 1.16;
1865
1971
  var patches = /* @__PURE__ */ new WeakMap();
1972
+ var dimensionPatches = /* @__PURE__ */ new WeakMap();
1973
+ var authoredCaching = /* @__PURE__ */ new WeakMap();
1974
+ var GLYPH_HALF_HEIGHT = 0.7;
1975
+ function curvedInkSize(text) {
1976
+ const lineCount = text._textLines?.length ?? 0;
1977
+ for (let index = 0; index < lineCount; index += 1) text.getLineWidth?.(index);
1978
+ const lines = text.__charBounds;
1979
+ if (!lines?.length) return null;
1980
+ let halfWidth = 0;
1981
+ let halfHeight = 0;
1982
+ for (const line of lines) {
1983
+ for (const box of line ?? []) {
1984
+ const { renderLeft, renderTop } = box;
1985
+ if (typeof renderLeft !== "number" || typeof renderTop !== "number") continue;
1986
+ const halfGlyphWidth = (box.kernedWidth ?? box.width ?? 0) / 2;
1987
+ const halfGlyphHeight = (box.height ?? text.fontSize) * GLYPH_HALF_HEIGHT;
1988
+ const angle = box.angle ?? 0;
1989
+ const cos = Math.abs(Math.cos(angle));
1990
+ const sin = Math.abs(Math.sin(angle));
1991
+ halfWidth = Math.max(
1992
+ halfWidth,
1993
+ Math.abs(renderLeft) + halfGlyphWidth * cos + halfGlyphHeight * sin
1994
+ );
1995
+ halfHeight = Math.max(
1996
+ halfHeight,
1997
+ Math.abs(renderTop) + halfGlyphWidth * sin + halfGlyphHeight * cos
1998
+ );
1999
+ }
2000
+ }
2001
+ if (halfWidth === 0 && halfHeight === 0) return null;
2002
+ return { width: halfWidth * 2, height: halfHeight * 2 };
2003
+ }
2004
+ function growBoxToInk(text) {
2005
+ const ink = curvedInkSize(text);
2006
+ if (!ink) return;
2007
+ if (ink.width > (text.width ?? 0)) text.width = ink.width;
2008
+ if (ink.height > (text.height ?? 0)) text.height = ink.height;
2009
+ }
1866
2010
  function isCurvable(object) {
1867
2011
  return !!object && typeof object.text === "string";
1868
2012
  }
1869
2013
  function measureText(text) {
1870
2014
  const authored = text.width;
1871
2015
  try {
1872
- text.set({ width: MEASURE_WIDTH });
2016
+ text.set({ width: MEASURE_WIDTH2 });
1873
2017
  text.initDimensions?.();
1874
2018
  return Math.max(1, text.calcTextWidth?.() ?? text.width ?? 1);
1875
2019
  } finally {
@@ -1894,11 +2038,29 @@ var TextCurveManager = class {
1894
2038
  this.layers = layers;
1895
2039
  this.history = history;
1896
2040
  this.events = events;
2041
+ this.canvas.on("text:changed", this.onTextChanged);
1897
2042
  }
1898
2043
  canvas;
1899
2044
  layers;
1900
2045
  history;
1901
2046
  events;
2047
+ /**
2048
+ * Typing on the canvas changes the run the paths were built for. Fabric drops
2049
+ * a glyph whose centre falls past the end of the path, so without rebuilding
2050
+ * here a longer replacement simply vanishes — select-all-and-retype rendered
2051
+ * an empty box — and a shorter one piles up on a path built for the old text.
2052
+ *
2053
+ * No history entry: fabric records the edit when editing exits, and one save
2054
+ * per keystroke would bury every earlier step under the typing.
2055
+ */
2056
+ onTextChanged = (event) => {
2057
+ const layer = event.target ? this.layers.findByObject(event.target) : void 0;
2058
+ if (!layer?.meta.curve) return;
2059
+ this.refresh(layer.id, false);
2060
+ };
2061
+ dispose() {
2062
+ this.canvas.off("text:changed", this.onTextChanged);
2063
+ }
1902
2064
  /** Curve parameters for a layer, or null when it is not curved text. */
1903
2065
  get(layerId) {
1904
2066
  const layer = this.layers.get(layerId);
@@ -1983,6 +2145,8 @@ var TextCurveManager = class {
1983
2145
  * outside the layer's box — where object caching clips it away.
1984
2146
  */
1985
2147
  attach(text, curves) {
2148
+ if (!authoredCaching.has(text)) authoredCaching.set(text, text.objectCaching);
2149
+ text.set({ objectCaching: false });
1986
2150
  const paths = curves.map(toFabricPath);
1987
2151
  const union = paths.length === 1 ? paths[0] : toFabricPath({ data: curves.map((c) => c.data).join(" "), length: 0 });
1988
2152
  text.set({
@@ -1994,6 +2158,35 @@ var TextCurveManager = class {
1994
2158
  pathStartOffset: 0
1995
2159
  });
1996
2160
  this.patchLineMeasure(text, paths, union);
2161
+ this.patchDimensions(text);
2162
+ }
2163
+ /**
2164
+ * Re-apply {@link growBoxToInk} after every one of fabric's own dimension
2165
+ * passes. Typing, a font change, a style edit and a state restore all call
2166
+ * `initDimensions`, and each one puts the box back to the path's bounding box
2167
+ * — so growing it once, here, would hold only until the next keystroke.
2168
+ */
2169
+ patchDimensions(text) {
2170
+ if (dimensionPatches.has(text)) return;
2171
+ const original = text.initDimensions;
2172
+ if (typeof original !== "function") return;
2173
+ const installed = function() {
2174
+ original.call(this);
2175
+ growBoxToInk(this);
2176
+ };
2177
+ const ownedOriginal = Object.prototype.hasOwnProperty.call(text, "initDimensions");
2178
+ text.initDimensions = installed;
2179
+ dimensionPatches.set(text, { original, installed, ownedOriginal });
2180
+ }
2181
+ /** Restore fabric's own dimension pass, if this manager replaced it. */
2182
+ unpatchDimensions(text) {
2183
+ const patch = dimensionPatches.get(text);
2184
+ if (!patch) return;
2185
+ if (text.initDimensions === patch.installed) {
2186
+ if (patch.ownedOriginal) text.initDimensions = patch.original;
2187
+ else delete text.initDimensions;
2188
+ }
2189
+ dimensionPatches.delete(text);
1997
2190
  }
1998
2191
  /**
1999
2192
  * Fabric lays every line of a text object along `this.path`, from one
@@ -2045,6 +2238,12 @@ var TextCurveManager = class {
2045
2238
  }
2046
2239
  detach(text, authoredWidth) {
2047
2240
  this.unpatchLineMeasure(text);
2241
+ this.unpatchDimensions(text);
2242
+ const caching = authoredCaching.get(text);
2243
+ if (caching !== void 0) {
2244
+ text.set({ objectCaching: caching });
2245
+ authoredCaching.delete(text);
2246
+ }
2048
2247
  text.set({ path: null, pathStartOffset: 0 });
2049
2248
  if (authoredWidth !== void 0 && authoredWidth > 0) text.set({ width: authoredWidth });
2050
2249
  }
@@ -3581,8 +3780,8 @@ var LayerMaskManager = class extends MaskStackStore {
3581
3780
  onLayersChanged = () => this.pin();
3582
3781
  // Selecting a layer means the user has moved on from the mask they had open;
3583
3782
  // leaving both selected would show mask controls for an unrelated layer.
3584
- onLayerSelected = () => {
3585
- if (this.selected) this.select(null, null);
3783
+ onLayerSelected = ({ layerId }) => {
3784
+ if (layerId && this.selected) this.select(null, null);
3586
3785
  };
3587
3786
  onObjectModified = (event) => {
3588
3787
  const object = event.target;
@@ -3642,6 +3841,7 @@ var LayerMaskManager = class extends MaskStackStore {
3642
3841
  }
3643
3842
  select(target, maskId) {
3644
3843
  this.selected = target && maskId ? { target, maskId } : null;
3844
+ if (this.selected) this.layers.select(null);
3645
3845
  this.events.emit("mask:selected", { target, maskId: this.selected ? maskId : null });
3646
3846
  }
3647
3847
  // ─── Mutations ───────────────────────────────────────
@@ -4194,8 +4394,7 @@ var CanvasEditor = class {
4194
4394
  }
4195
4395
  const group = new Group7(objects);
4196
4396
  const grouped = this.layers.add("group", group, name);
4197
- for (const child of children) child.parentId = grouped.id;
4198
- grouped.children = children;
4397
+ this.layers.adoptChildren(grouped, children);
4199
4398
  this.layers.reorder(grouped.id, insertIndex);
4200
4399
  this.layers.select(grouped.id);
4201
4400
  this.history.save();
@@ -4804,6 +5003,7 @@ var CanvasEditor = class {
4804
5003
  this.crop.dispose();
4805
5004
  this.history.dispose();
4806
5005
  this.patterns.dispose();
5006
+ this.curves.dispose();
4807
5007
  this.events.removeAllListeners();
4808
5008
  this.canvas.dispose();
4809
5009
  }
@@ -4863,11 +5063,15 @@ var DEFAULT_PATTERN_CONFIG = {
4863
5063
  horizontalSpacing: 0,
4864
5064
  verticalSpacing: 0,
4865
5065
  angle: 0,
4866
- horizontalOffset: 0,
5066
+ // Half a tile: the offset only bites in the brick modes, and a brick course
5067
+ // that is not half-shifted is just a grid with extra steps.
5068
+ horizontalOffset: 50,
4867
5069
  offsetX: 0,
4868
5070
  offsetY: 0,
4869
5071
  rotationStepH: 0,
4870
- rotationStepV: 0
5072
+ rotationStepV: 0,
5073
+ horizontalLimit: 0,
5074
+ verticalLimit: 0
4871
5075
  };
4872
5076
 
4873
5077
  // src/presets.ts
@@ -4972,6 +5176,7 @@ export {
4972
5176
  exportPNG,
4973
5177
  exportPrintArea,
4974
5178
  exportSVG,
5179
+ fitTextWidth,
4975
5180
  fitToBox,
4976
5181
  generateId,
4977
5182
  isCssColor,