canvu-react 0.3.33 → 0.3.35

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
@@ -1043,153 +1043,203 @@ function svgNumber(value) {
1043
1043
  const rounded = Math.round(value * 100) / 100;
1044
1044
  return Number.isInteger(rounded) ? String(rounded) : String(rounded);
1045
1045
  }
1046
- function architecturalCloudScallopCount(perimeter, amplitude) {
1047
- const targetScallopLength = Math.max(18, amplitude * 2.45);
1048
- let count = Math.max(12, Math.round(perimeter / targetScallopLength));
1049
- if (count % 2 === 1) count += 1;
1050
- return count;
1051
- }
1052
- function roundedRectMetrics(width, height, inset, radius) {
1053
- const left = inset;
1054
- const top = inset;
1055
- const right = width - inset;
1056
- const bottom = height - inset;
1057
- const rectWidth = Math.max(0, right - left);
1058
- const rectHeight = Math.max(0, bottom - top);
1059
- const normalizedRadius = Math.max(
1060
- 0,
1061
- Math.min(radius, rectWidth / 2, rectHeight / 2)
1046
+ var ARCHITECTURAL_CLOUD_RADIUS = 38;
1047
+ var ARCHITECTURAL_CLOUD_TARGET_SPACING = 50;
1048
+ var ARCHITECTURAL_CLOUD_FLAT_RADIUS_RATIO = 0.38;
1049
+ var ARCHITECTURAL_CLOUD_ROUNDED_RADIUS = 72;
1050
+ var ARCHITECTURAL_CLOUD_ROUNDED_MIN_RADIUS = 44;
1051
+ var ARCHITECTURAL_CLOUD_ROUNDED_TARGET_SPACING = 98;
1052
+ function architecturalCloudCenterCount(edgeLength, radius) {
1053
+ const targetSpacing = Math.min(
1054
+ ARCHITECTURAL_CLOUD_TARGET_SPACING,
1055
+ Math.max(1, radius * 1.3)
1062
1056
  );
1063
- const centerX = width / 2;
1064
- const topHalfLength = Math.max(0, right - normalizedRadius - centerX);
1065
- const horizontalLength = Math.max(0, rectWidth - normalizedRadius * 2);
1066
- const verticalLength = Math.max(0, rectHeight - normalizedRadius * 2);
1067
- const arcLength = normalizedRadius * (Math.PI / 2);
1057
+ return Math.max(2, Math.round(edgeLength / targetSpacing) + 1);
1058
+ }
1059
+ function distributeRange(start, end, count) {
1060
+ if (count <= 1) return [start];
1061
+ const step = (end - start) / (count - 1);
1062
+ return Array.from({ length: count }, (_, index) => start + step * index);
1063
+ }
1064
+ function lineCloudPathSegment(start, end) {
1065
+ const dx = end[0] - start[0];
1066
+ const dy = end[1] - start[1];
1067
+ const length = Math.hypot(dx, dy);
1068
1068
  return {
1069
- left,
1070
- top,
1071
- right,
1072
- bottom,
1073
- radius: normalizedRadius,
1074
- centerX,
1075
- topHalfLength,
1076
- horizontalLength,
1077
- verticalLength,
1078
- arcLength,
1079
- perimeter: horizontalLength * 2 + verticalLength * 2 + Math.PI * 2 * normalizedRadius
1069
+ length,
1070
+ pointAt: (t) => [start[0] + dx * t, start[1] + dy * t]
1080
1071
  };
1081
1072
  }
1082
- function pointOnLine(startX, startY, endX, endY, t) {
1083
- return [startX + (endX - startX) * t, startY + (endY - startY) * t];
1073
+ function ellipsePoint(centerX, centerY, radiusX, radiusY, angle) {
1074
+ return [centerX + Math.cos(angle) * radiusX, centerY + Math.sin(angle) * radiusY];
1084
1075
  }
1085
- function pointOnArc(centerX, centerY, radius, startAngle, endAngle, t) {
1086
- const theta = startAngle + (endAngle - startAngle) * t;
1087
- return [centerX + Math.cos(theta) * radius, centerY + Math.sin(theta) * radius];
1076
+ function approximateEllipseArcLength(radiusX, radiusY, startAngle, endAngle) {
1077
+ const steps = Math.max(
1078
+ 4,
1079
+ Math.ceil(Math.abs(endAngle - startAngle) / (Math.PI / 16))
1080
+ );
1081
+ let length = 0;
1082
+ let previous = ellipsePoint(0, 0, radiusX, radiusY, startAngle);
1083
+ for (let index = 1; index <= steps; index += 1) {
1084
+ const angle = startAngle + (endAngle - startAngle) * index / steps;
1085
+ const next = ellipsePoint(0, 0, radiusX, radiusY, angle);
1086
+ length += Math.hypot(next[0] - previous[0], next[1] - previous[1]);
1087
+ previous = next;
1088
+ }
1089
+ return length;
1088
1090
  }
1089
- function pointOnRoundedRectPath(metrics, distance) {
1090
- if (metrics.perimeter <= 0) return [metrics.centerX, metrics.top];
1091
- let remaining = (distance % metrics.perimeter + metrics.perimeter) % metrics.perimeter;
1092
- const consume = (length) => {
1093
- if (length <= 1e-9) return null;
1094
- if (remaining <= length) return remaining / length;
1095
- remaining -= length;
1096
- return null;
1091
+ function ellipseCloudPathSegment(centerX, centerY, radiusX, radiusY, startAngle, endAngle) {
1092
+ return {
1093
+ length: approximateEllipseArcLength(radiusX, radiusY, startAngle, endAngle),
1094
+ pointAt: (t) => ellipsePoint(
1095
+ centerX,
1096
+ centerY,
1097
+ radiusX,
1098
+ radiusY,
1099
+ startAngle + (endAngle - startAngle) * t
1100
+ )
1097
1101
  };
1098
- let t = consume(metrics.topHalfLength);
1099
- if (t != null) {
1100
- return pointOnLine(
1101
- metrics.centerX,
1102
- metrics.top,
1103
- metrics.right - metrics.radius,
1104
- metrics.top,
1105
- t
1106
- );
1102
+ }
1103
+ function cloudPathPerimeter(segments) {
1104
+ const usableSegments = segments.filter((segment) => segment.length > 1e-9);
1105
+ return usableSegments.reduce((sum, segment) => sum + segment.length, 0);
1106
+ }
1107
+ function pointOnCloudPath(segments, distance) {
1108
+ const perimeter = cloudPathPerimeter(segments);
1109
+ if (perimeter <= 0) return [0, 0];
1110
+ let remaining = (distance % perimeter + perimeter) % perimeter;
1111
+ for (const segment of segments) {
1112
+ if (segment.length <= 1e-9) continue;
1113
+ if (remaining <= segment.length) {
1114
+ const t = remaining / segment.length;
1115
+ return segment.pointAt(t);
1116
+ }
1117
+ remaining -= segment.length;
1107
1118
  }
1108
- t = consume(metrics.arcLength);
1109
- if (t != null) {
1110
- return pointOnArc(
1111
- metrics.right - metrics.radius,
1112
- metrics.top + metrics.radius,
1113
- metrics.radius,
1119
+ const fallback = segments.find((segment) => segment.length > 1e-9);
1120
+ return fallback?.pointAt(0) ?? [0, 0];
1121
+ }
1122
+ function buildRoundedCapsulePathSegments(width, height, inset) {
1123
+ const left = inset;
1124
+ const top = inset;
1125
+ const right = width - inset;
1126
+ const bottom = height - inset;
1127
+ const capsuleWidth = Math.max(0, right - left);
1128
+ const capsuleHeight = Math.max(0, bottom - top);
1129
+ const radius = Math.min(capsuleWidth, capsuleHeight) / 2;
1130
+ if (radius <= 0) return [];
1131
+ const leftCenterX = left + radius;
1132
+ const rightCenterX = right - radius;
1133
+ const topCenterY = top + radius;
1134
+ const bottomCenterY = bottom - radius;
1135
+ return [
1136
+ lineCloudPathSegment([leftCenterX, top], [rightCenterX, top]),
1137
+ ellipseCloudPathSegment(
1138
+ rightCenterX,
1139
+ topCenterY,
1140
+ radius,
1141
+ radius,
1114
1142
  -Math.PI / 2,
1143
+ 0
1144
+ ),
1145
+ lineCloudPathSegment([right, topCenterY], [right, bottomCenterY]),
1146
+ ellipseCloudPathSegment(
1147
+ rightCenterX,
1148
+ bottomCenterY,
1149
+ radius,
1150
+ radius,
1115
1151
  0,
1116
- t
1117
- );
1118
- }
1119
- t = consume(metrics.verticalLength);
1120
- if (t != null) {
1121
- return pointOnLine(
1122
- metrics.right,
1123
- metrics.top + metrics.radius,
1124
- metrics.right,
1125
- metrics.bottom - metrics.radius,
1126
- t
1127
- );
1128
- }
1129
- t = consume(metrics.arcLength);
1130
- if (t != null) {
1131
- return pointOnArc(
1132
- metrics.right - metrics.radius,
1133
- metrics.bottom - metrics.radius,
1134
- metrics.radius,
1135
- 0,
1152
+ Math.PI / 2
1153
+ ),
1154
+ lineCloudPathSegment([rightCenterX, bottom], [leftCenterX, bottom]),
1155
+ ellipseCloudPathSegment(
1156
+ leftCenterX,
1157
+ bottomCenterY,
1158
+ radius,
1159
+ radius,
1136
1160
  Math.PI / 2,
1137
- t
1138
- );
1139
- }
1140
- t = consume(metrics.horizontalLength);
1141
- if (t != null) {
1142
- return pointOnLine(
1143
- metrics.right - metrics.radius,
1144
- metrics.bottom,
1145
- metrics.left + metrics.radius,
1146
- metrics.bottom,
1147
- t
1148
- );
1149
- }
1150
- t = consume(metrics.arcLength);
1151
- if (t != null) {
1152
- return pointOnArc(
1153
- metrics.left + metrics.radius,
1154
- metrics.bottom - metrics.radius,
1155
- metrics.radius,
1156
- Math.PI / 2,
1157
- Math.PI,
1158
- t
1159
- );
1160
- }
1161
- t = consume(metrics.verticalLength);
1162
- if (t != null) {
1163
- return pointOnLine(
1164
- metrics.left,
1165
- metrics.bottom - metrics.radius,
1166
- metrics.left,
1167
- metrics.top + metrics.radius,
1168
- t
1169
- );
1170
- }
1171
- t = consume(metrics.arcLength);
1172
- if (t != null) {
1173
- return pointOnArc(
1174
- metrics.left + metrics.radius,
1175
- metrics.top + metrics.radius,
1176
- metrics.radius,
1161
+ Math.PI
1162
+ ),
1163
+ lineCloudPathSegment([left, bottomCenterY], [left, topCenterY]),
1164
+ ellipseCloudPathSegment(
1165
+ leftCenterX,
1166
+ topCenterY,
1167
+ radius,
1168
+ radius,
1177
1169
  Math.PI,
1178
- Math.PI * 1.5,
1179
- t
1180
- );
1181
- }
1182
- t = consume(metrics.topHalfLength);
1183
- if (t != null) {
1184
- return pointOnLine(
1185
- metrics.left + metrics.radius,
1186
- metrics.top,
1187
- metrics.centerX,
1188
- metrics.top,
1189
- t
1170
+ Math.PI * 1.5
1171
+ )
1172
+ ];
1173
+ }
1174
+ function buildRoundedArcCloudPathD(cloudWidth, cloudHeight, center) {
1175
+ const minDimension = Math.min(cloudWidth, cloudHeight);
1176
+ const radius = Math.min(
1177
+ ARCHITECTURAL_CLOUD_ROUNDED_RADIUS,
1178
+ Math.max(ARCHITECTURAL_CLOUD_ROUNDED_MIN_RADIUS, minDimension * 0.16)
1179
+ );
1180
+ const centerPath = buildRoundedCapsulePathSegments(
1181
+ cloudWidth,
1182
+ cloudHeight,
1183
+ radius
1184
+ );
1185
+ const centerPerimeter = cloudPathPerimeter(centerPath);
1186
+ if (centerPerimeter <= 0) return "";
1187
+ const lobeCount = Math.max(
1188
+ 8,
1189
+ Math.round(centerPerimeter / ARCHITECTURAL_CLOUD_ROUNDED_TARGET_SPACING)
1190
+ );
1191
+ const centers = Array.from(
1192
+ { length: lobeCount },
1193
+ (_, index) => pointOnCloudPath(centerPath, centerPerimeter * index / lobeCount)
1194
+ );
1195
+ const points = centers.map((point, index) => {
1196
+ const previous = centers[(index - 1 + centers.length) % centers.length] ?? point;
1197
+ return cloudCircleIntersection(previous, point, radius, center);
1198
+ });
1199
+ const [startX, startY] = points[0] ?? [0, 0];
1200
+ const segments = [`M${svgNumber(startX)} ${svgNumber(startY)}`];
1201
+ for (const [endX, endY] of points.slice(1)) {
1202
+ segments.push(
1203
+ `A ${svgNumber(radius)} ${svgNumber(radius)} 0 0 1 ${svgNumber(endX)} ${svgNumber(endY)}`
1190
1204
  );
1191
1205
  }
1192
- return [metrics.centerX, metrics.top];
1206
+ segments.push(
1207
+ `A ${svgNumber(radius)} ${svgNumber(radius)} 0 0 1 ${svgNumber(startX)} ${svgNumber(startY)}`
1208
+ );
1209
+ segments.push("Z");
1210
+ return segments.join(" ");
1211
+ }
1212
+ function cloudCircleIntersection(a, b, radius, center) {
1213
+ const midX = (a[0] + b[0]) / 2;
1214
+ const midY = (a[1] + b[1]) / 2;
1215
+ const dx = b[0] - a[0];
1216
+ const dy = b[1] - a[1];
1217
+ const distance = Math.hypot(dx, dy);
1218
+ if (distance <= 1e-9) return [midX, midY];
1219
+ const halfDistance = distance / 2;
1220
+ const offset = Math.sqrt(
1221
+ Math.max(0, radius * radius - halfDistance * halfDistance)
1222
+ );
1223
+ const normalX = -dy / distance;
1224
+ const normalY = dx / distance;
1225
+ const first = [midX + normalX * offset, midY + normalY * offset];
1226
+ const second = [
1227
+ midX - normalX * offset,
1228
+ midY - normalY * offset
1229
+ ];
1230
+ const firstDistance = (first[0] - center[0]) * (first[0] - center[0]) + (first[1] - center[1]) * (first[1] - center[1]);
1231
+ const secondDistance = (second[0] - center[0]) * (second[0] - center[0]) + (second[1] - center[1]) * (second[1] - center[1]);
1232
+ return firstDistance >= secondDistance ? first : second;
1233
+ }
1234
+ function cloudEllipseIntersection(a, b, radiusX, radiusY, center) {
1235
+ const scaleY = radiusX / radiusY;
1236
+ const [x, y] = cloudCircleIntersection(
1237
+ [a[0], a[1] * scaleY],
1238
+ [b[0], b[1] * scaleY],
1239
+ radiusX,
1240
+ [center[0], center[1] * scaleY]
1241
+ );
1242
+ return [x, y / scaleY];
1193
1243
  }
1194
1244
  function buildRectSvg(width, height, style = DEFAULT_STROKE_STYLE) {
1195
1245
  return `<rect width="${width}" height="${height}" fill="none" stroke="${style.stroke}" stroke-width="${style.strokeWidth}" rx="4"${strokeOpacityAttr(style)} />`;
@@ -1203,39 +1253,63 @@ function buildArchitecturalCloudPathD(width, height, strokeWidth = DEFAULT_STROK
1203
1253
  const w = Math.max(0, width);
1204
1254
  const h = Math.max(0, height);
1205
1255
  if (w <= 0 || h <= 0) return "";
1206
- const inset = Math.max(0.5, strokeWidth / 2);
1207
- const outerWidth = Math.max(0, w - inset * 2);
1208
- const outerHeight = Math.max(0, h - inset * 2);
1209
- if (outerWidth <= 0 || outerHeight <= 0) return "";
1210
- const amplitude = Math.min(
1211
- outerWidth * 0.12,
1212
- outerHeight * 0.12,
1213
- Math.max(5, Math.min(14, Math.min(w, h) * 0.07))
1256
+ const padding = Math.max(0, strokeWidth * 2);
1257
+ const cloudWidth = Math.max(0, w - padding);
1258
+ const cloudHeight = Math.max(0, h - padding);
1259
+ if (cloudWidth <= 0 || cloudHeight <= 0) return "";
1260
+ const radiusX = Math.min(
1261
+ ARCHITECTURAL_CLOUD_RADIUS,
1262
+ cloudWidth * ARCHITECTURAL_CLOUD_FLAT_RADIUS_RATIO
1214
1263
  );
1215
- const radius = Math.min(
1216
- outerWidth / 2,
1217
- outerHeight / 2,
1218
- Math.max(amplitude * 2.5, outerHeight * 0.43)
1264
+ const radiusY = Math.min(
1265
+ ARCHITECTURAL_CLOUD_RADIUS,
1266
+ cloudHeight * ARCHITECTURAL_CLOUD_FLAT_RADIUS_RATIO
1219
1267
  );
1220
- const outer = roundedRectMetrics(w, h, inset, radius);
1221
- const inner = roundedRectMetrics(
1222
- w,
1223
- h,
1224
- inset + amplitude,
1225
- Math.max(0, radius - amplitude)
1268
+ if (radiusX <= 0 || radiusY <= 0) return "";
1269
+ const center = [cloudWidth / 2, cloudHeight / 2];
1270
+ const leftCenterX = radiusX;
1271
+ const rightCenterX = cloudWidth - radiusX;
1272
+ const topCenterY = radiusY;
1273
+ const bottomCenterY = cloudHeight - radiusY;
1274
+ const horizontalCenters = distributeRange(
1275
+ leftCenterX,
1276
+ rightCenterX,
1277
+ architecturalCloudCenterCount(Math.max(0, rightCenterX - leftCenterX), radiusX)
1226
1278
  );
1227
- const scallopCount = architecturalCloudScallopCount(outer.perimeter, amplitude);
1228
- const [startX, startY] = pointOnRoundedRectPath(inner, 0);
1279
+ const verticalCenters = distributeRange(
1280
+ topCenterY,
1281
+ bottomCenterY,
1282
+ architecturalCloudCenterCount(Math.max(0, bottomCenterY - topCenterY), radiusY)
1283
+ );
1284
+ if (horizontalCenters.length > 3 && verticalCenters.length > 3) {
1285
+ const roundedArcCloudPath = buildRoundedArcCloudPathD(
1286
+ cloudWidth,
1287
+ cloudHeight,
1288
+ center
1289
+ );
1290
+ if (roundedArcCloudPath !== "") return roundedArcCloudPath;
1291
+ }
1292
+ const rectangularCenters = [
1293
+ ...horizontalCenters.map((x) => [x, topCenterY]),
1294
+ ...verticalCenters.slice(1).map((y) => [rightCenterX, y]),
1295
+ ...horizontalCenters.slice(0, -1).reverse().map((x) => [x, bottomCenterY]),
1296
+ ...verticalCenters.slice(1, -1).reverse().map((y) => [leftCenterX, y])
1297
+ ];
1298
+ const centers = rectangularCenters;
1299
+ const points = centers.map((point, index) => {
1300
+ const previous = centers[(index - 1 + centers.length) % centers.length] ?? point;
1301
+ return cloudEllipseIntersection(previous, point, radiusX, radiusY, center);
1302
+ });
1303
+ const [startX, startY] = points[0] ?? [0, 0];
1229
1304
  const segments = [`M${svgNumber(startX)} ${svgNumber(startY)}`];
1230
- for (let index = 0; index < scallopCount; index += 1) {
1231
- const controlDistance = (index + 0.5) / scallopCount * outer.perimeter;
1232
- const endDistance = (index + 1) / scallopCount * inner.perimeter;
1233
- const [controlX, controlY] = pointOnRoundedRectPath(outer, controlDistance);
1234
- const [endX, endY] = pointOnRoundedRectPath(inner, endDistance);
1305
+ for (const [endX, endY] of points.slice(1)) {
1235
1306
  segments.push(
1236
- `Q${svgNumber(controlX)} ${svgNumber(controlY)} ${svgNumber(endX)} ${svgNumber(endY)}`
1307
+ `A ${svgNumber(radiusX)} ${svgNumber(radiusY)} 0 0 1 ${svgNumber(endX)} ${svgNumber(endY)}`
1237
1308
  );
1238
1309
  }
1310
+ segments.push(
1311
+ `A ${svgNumber(radiusX)} ${svgNumber(radiusY)} 0 0 1 ${svgNumber(startX)} ${svgNumber(startY)}`
1312
+ );
1239
1313
  segments.push("Z");
1240
1314
  return segments.join(" ");
1241
1315
  }
@@ -2525,20 +2599,25 @@ function markImageAsManaged(item) {
2525
2599
  };
2526
2600
  }
2527
2601
  function restackManagedImages(items) {
2528
- let anchor;
2602
+ let anchorAabbY = Infinity;
2603
+ let anchorCenterX = 0;
2529
2604
  for (const item of items) {
2530
2605
  if (!isManagedImage(item)) continue;
2531
- if (!anchor || item.bounds.y < anchor.bounds.y) anchor = item;
2606
+ const aabb = boundsAabbForRotatedItem(item);
2607
+ if (aabb.y < anchorAabbY) {
2608
+ anchorAabbY = aabb.y;
2609
+ anchorCenterX = aabb.x + aabb.width / 2;
2610
+ }
2532
2611
  }
2533
- if (!anchor) return [...items];
2534
- const anchorCenterX = anchor.bounds.x + anchor.bounds.width / 2;
2535
- const anchorTopY = anchor.bounds.y;
2536
- let cursorY = anchorTopY;
2612
+ if (!Number.isFinite(anchorAabbY)) return [...items];
2613
+ let cursorY = anchorAabbY;
2537
2614
  return items.map((item) => {
2538
2615
  if (!isManagedImage(item)) return item;
2616
+ const aabb = boundsAabbForRotatedItem(item);
2617
+ const centerY = cursorY + aabb.height / 2;
2539
2618
  const newX = anchorCenterX - item.bounds.width / 2;
2540
- const newY = cursorY;
2541
- cursorY = newY + item.bounds.height + STACK_GAP_WORLD;
2619
+ const newY = centerY - item.bounds.height / 2;
2620
+ cursorY += aabb.height + STACK_GAP_WORLD;
2542
2621
  if (item.bounds.x === newX && item.bounds.y === newY) return item;
2543
2622
  return {
2544
2623
  ...item,
@@ -2561,8 +2640,10 @@ function copyManagedImage(items, id) {
2561
2640
  return restackManagedImages(inserted);
2562
2641
  }
2563
2642
  function rotateManagedImage(items, id) {
2564
- return items.map(
2565
- (i) => i.id === id ? { ...i, rotation: ((i.rotation ?? 0) + Math.PI / 2) % (Math.PI * 2) } : i
2643
+ return restackManagedImages(
2644
+ items.map(
2645
+ (i) => i.id === id ? { ...i, rotation: ((i.rotation ?? 0) + Math.PI / 2) % (Math.PI * 2) } : i
2646
+ )
2566
2647
  );
2567
2648
  }
2568
2649
  function deleteManagedImage(items, id) {