@overtone-art/canvas-editor-core 0.5.0 → 0.5.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
@@ -966,8 +966,10 @@ function computeTilePositions(config, targetW, targetH, baseW, baseH, origin) {
966
966
  }
967
967
  return placements;
968
968
  }
969
- function drawTiles(ctx, img, config, targetW, targetH, baseW, baseH, origin) {
969
+ function drawTiles(ctx, img, config, targetW, targetH, baseW, baseH, origin, draw) {
970
970
  const anchor = origin ?? { x: targetW / 2, y: targetH / 2 };
971
+ const drawW = draw && draw.width > 0 ? draw.width : baseW;
972
+ const drawH = draw && draw.height > 0 ? draw.height : baseH;
971
973
  ctx.save();
972
974
  ctx.translate(anchor.x, anchor.y);
973
975
  ctx.rotate(config.angle * Math.PI / 180);
@@ -975,7 +977,7 @@ function drawTiles(ctx, img, config, targetW, targetH, baseW, baseH, origin) {
975
977
  ctx.save();
976
978
  ctx.translate(tile.x, tile.y);
977
979
  ctx.rotate(tile.rotation);
978
- ctx.drawImage(img, -baseW / 2, -baseH / 2, baseW, baseH);
980
+ ctx.drawImage(img, -drawW / 2, -drawH / 2, drawW, drawH);
979
981
  ctx.restore();
980
982
  }
981
983
  ctx.restore();
@@ -997,6 +999,7 @@ function mod2(n) {
997
999
  var MAX_SNAPSHOT_PIXELS = 16e6;
998
1000
  var MAX_SNAPSHOT_SCALE = 8;
999
1001
  var SNAPSHOT_SHRINK_FACTOR = 2;
1002
+ var TILE_BLEED_DEVICE_PX = 2;
1000
1003
  var TiledPatternObject = class _TiledPatternObject extends import_fabric2.FabricObject {
1001
1004
  static type = "TiledPattern";
1002
1005
  /** The layer's real object. Never rendered directly (it is kept at opacity 0). */
@@ -1086,7 +1089,8 @@ var TiledPatternObject = class _TiledPatternObject extends import_fabric2.Fabric
1086
1089
  const { width, height } = this.area;
1087
1090
  if (width <= 0 || height <= 0) return;
1088
1091
  const { tileW, tileH } = this.liveTile();
1089
- const snapshot = this.ensureSnapshot(contextScale(ctx) * (tileW / Math.max(1, this.baseW())));
1092
+ const scale = contextScale(ctx);
1093
+ const snapshot = this.ensureSnapshot(scale * (tileW / Math.max(1, this.baseW())));
1090
1094
  if (!snapshot) return;
1091
1095
  const centre = this.getCenterPoint();
1092
1096
  const angle = this.liveAngle();
@@ -1097,7 +1101,17 @@ var TiledPatternObject = class _TiledPatternObject extends import_fabric2.Fabric
1097
1101
  const shift = this.shiftVector(tileW, tileH, angle);
1098
1102
  const origin = { x: centre.x - shift.x, y: centre.y - shift.y };
1099
1103
  const config = { ...this.config, angle, offsetX: 0, offsetY: 0 };
1100
- drawTiles(ctx, snapshot, config, width, height, tileW, tileH, origin);
1104
+ drawTiles(
1105
+ ctx,
1106
+ snapshot,
1107
+ config,
1108
+ width,
1109
+ height,
1110
+ tileW,
1111
+ tileH,
1112
+ origin,
1113
+ this.drawSize(tileW, tileH, TILE_BLEED_DEVICE_PX / scale)
1114
+ );
1101
1115
  ctx.restore();
1102
1116
  }
1103
1117
  /** Raster fallback for SVG export — one `<image>` covering the print area. */
@@ -1123,7 +1137,9 @@ var TiledPatternObject = class _TiledPatternObject extends import_fabric2.Fabric
1123
1137
  height,
1124
1138
  tileW,
1125
1139
  tileH,
1126
- { x: centre.x - shift.x, y: centre.y - shift.y }
1140
+ { x: centre.x - shift.x, y: centre.y - shift.y },
1141
+ // The fallback raster is built at 1:1, so a device pixel is a canvas unit.
1142
+ this.drawSize(tileW, tileH, TILE_BLEED_DEVICE_PX)
1127
1143
  );
1128
1144
  return [
1129
1145
  `<g transform="rotate(${-angle}) translate(${-centre.x} ${-centre.y})">`,
@@ -1163,18 +1179,64 @@ var TiledPatternObject = class _TiledPatternObject extends import_fabric2.Fabric
1163
1179
  });
1164
1180
  return Promise.resolve(copy);
1165
1181
  }
1182
+ /**
1183
+ * The source's tile footprint: its bounding box, minus a stroke it reserves
1184
+ * room for but never paints.
1185
+ *
1186
+ * fabric keeps `strokeWidth` inside an object's box whether or not a `stroke`
1187
+ * colour paints it, and the built-in shapes arrive with `strokeWidth: 1` and no
1188
+ * stroke. Stepping the grid by that box puts a transparent seam between every
1189
+ * pair of neighbours at zero spacing: the artwork is a pixel narrower than the
1190
+ * box it is stepped by.
1191
+ */
1192
+ sourceBox() {
1193
+ const rect = this.source.getBoundingRect();
1194
+ if (paintsStroke(this.source)) return { width: rect.width, height: rect.height };
1195
+ const dims = this.source._getTransformedDimensions({ strokeWidth: 0 });
1196
+ const radians = (this.source.angle ?? 0) * Math.PI / 180;
1197
+ const cos = Math.abs(Math.cos(radians));
1198
+ const sin = Math.abs(Math.sin(radians));
1199
+ return {
1200
+ width: dims.x * cos + dims.y * sin,
1201
+ height: dims.x * sin + dims.y * cos
1202
+ };
1203
+ }
1166
1204
  /** The source's on-canvas width, before the tile scale. */
1167
1205
  baseW() {
1168
- return Math.max(1, this.source.getBoundingRect().width);
1206
+ return Math.max(1, this.sourceBox().width);
1207
+ }
1208
+ /** The source's on-canvas height, before the tile scale. */
1209
+ baseH() {
1210
+ return Math.max(1, this.sourceBox().height);
1211
+ }
1212
+ /**
1213
+ * Size to paint the snapshot at, for a tile of `tileW × tileH`.
1214
+ *
1215
+ * The snapshot is NOT exactly the source's box: `toCanvasElement` rounds the
1216
+ * raster up to whole pixels and pads it further for a shadow. Painting it into
1217
+ * the tile step would squeeze the artwork inside that padding — every tile
1218
+ * shrinks by up to a pixel and the grid shows transparent seams at zero
1219
+ * spacing. So the bitmap is painted at its own footprint, scaled by the same
1220
+ * factor the tile is, and the step stays the source's box.
1221
+ */
1222
+ drawSize(tileW, tileH, bleed = 0) {
1223
+ const snapshot = this.snapshotEl;
1224
+ if (!snapshot || this.snapshotScale <= 0) {
1225
+ return { width: tileW + bleed, height: tileH + bleed };
1226
+ }
1227
+ return {
1228
+ width: snapshot.width / this.snapshotScale * (tileW / this.baseW()) + bleed,
1229
+ height: snapshot.height / this.snapshotScale * (tileH / this.baseH()) + bleed
1230
+ };
1169
1231
  }
1170
1232
  /** Tile size from the config alone, ignoring any in-flight gesture. */
1171
1233
  baseTile() {
1172
- const rect = this.source.getBoundingRect();
1234
+ const box = this.sourceBox();
1173
1235
  const scale = Math.max(1, this.config.scale ?? 100) / 100;
1174
1236
  const floor = this.tileFloor();
1175
1237
  return {
1176
- tileW: Math.max(floor, rect.width * scale),
1177
- tileH: Math.max(floor, rect.height * scale)
1238
+ tileW: Math.max(floor, box.width * scale),
1239
+ tileH: Math.max(floor, box.height * scale)
1178
1240
  };
1179
1241
  }
1180
1242
  /** Tile size as drawn right now, including a live scale gesture. */
@@ -1237,6 +1299,12 @@ var TiledPatternObject = class _TiledPatternObject extends import_fabric2.Fabric
1237
1299
  return Math.max(0.05, Math.min(requested, budgeted));
1238
1300
  }
1239
1301
  };
1302
+ function paintsStroke(object) {
1303
+ if (!object.strokeWidth || object.strokeWidth <= 0) return false;
1304
+ const { stroke } = object;
1305
+ if (typeof stroke === "string") return stroke !== "" && stroke !== "transparent";
1306
+ return stroke !== null && stroke !== void 0;
1307
+ }
1240
1308
  function clampPercent(value) {
1241
1309
  if (typeof value !== "number" || !Number.isFinite(value)) return 0;
1242
1310
  return Math.max(-100, Math.min(100, value));