hyperframes 0.8.73 → 0.8.74

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.
@@ -1274,6 +1274,121 @@
1274
1274
  return issues;
1275
1275
  }
1276
1276
 
1277
+ // Pixels drawn into a <canvas> have no DOM box, so text the frame edge cuts is invisible to
1278
+ // canvas_overflow. Read a thin band just inside each covered frame edge and count sharp steps.
1279
+ const CANVAS_EDGE_STEP = 96;
1280
+ const CANVAS_EDGE_MIN_STEPS = 2;
1281
+ // Monospace and bitmap glyph cells end in a gap, so the outermost column alone can read empty.
1282
+ const CANVAS_EDGE_BAND_PX = 4;
1283
+
1284
+ function coveredFrameEdges(rect, rootRect, tolerance) {
1285
+ const top = Math.max(rect.top, rootRect.top);
1286
+ const bottom = Math.min(rect.bottom, rootRect.bottom);
1287
+ const left = Math.max(rect.left, rootRect.left);
1288
+ const right = Math.min(rect.right, rootRect.right);
1289
+ if (bottom - top < CANVAS_EDGE_BAND_PX || right - left < CANVAS_EDGE_BAND_PX) return [];
1290
+ const band = CANVAS_EDGE_BAND_PX;
1291
+ const rows = { y: top, height: bottom - top, width: band, vertical: true };
1292
+ const cols = { x: left, width: right - left, height: band, vertical: false };
1293
+ const edges = [];
1294
+ if (rect.left <= rootRect.left + tolerance) edges.push({ side: "left", x: left, ...rows });
1295
+ if (rect.right >= rootRect.right - tolerance)
1296
+ edges.push({ side: "right", x: right - band, ...rows });
1297
+ if (rect.top <= rootRect.top + tolerance) edges.push({ side: "top", y: top, ...cols });
1298
+ if (rect.bottom >= rootRect.bottom - tolerance)
1299
+ edges.push({ side: "bottom", y: bottom - band, ...cols });
1300
+ return edges;
1301
+ }
1302
+
1303
+ // Draw the band into a scratch canvas (never getContext on the film's own canvas, which would
1304
+ // claim it as 2D). A WebGL canvas without preserveDrawingBuffer reads blank: no finding.
1305
+ function canvasEdgeBand(canvas, rect, edge) {
1306
+ const scaleX = canvas.width / rect.width;
1307
+ const scaleY = canvas.height / rect.height;
1308
+ const off = document.createElement("canvas");
1309
+ off.width = Math.max(1, Math.round(edge.width * scaleX));
1310
+ off.height = Math.max(1, Math.round(edge.height * scaleY));
1311
+ const ctx = off.getContext("2d");
1312
+ if (!ctx) return null;
1313
+ const sx = Math.max(0, Math.floor((edge.x - rect.left) * scaleX));
1314
+ const sy = Math.max(0, Math.floor((edge.y - rect.top) * scaleY));
1315
+ ctx.drawImage(canvas, sx, sy, off.width, off.height, 0, 0, off.width, off.height);
1316
+ return {
1317
+ data: ctx.getImageData(0, 0, off.width, off.height).data,
1318
+ width: off.width,
1319
+ height: off.height,
1320
+ };
1321
+ }
1322
+
1323
+ // Collapse the band across its thickness (brightest wins), then count steps along the edge:
1324
+ // luma over black plus alpha, compared two pixels back so anti-aliased glyph edges still step.
1325
+ function sharpSteps(band, vertical) {
1326
+ const length = vertical ? band.height : band.width;
1327
+ const thickness = vertical ? band.width : band.height;
1328
+ const levels = [];
1329
+ for (let along = 0; along < length; along++) {
1330
+ let luma = 0;
1331
+ let alpha = 0;
1332
+ for (let across = 0; across < thickness; across++) {
1333
+ const i = 4 * (vertical ? along * band.width + across : across * band.width + along);
1334
+ const a = band.data[i + 3];
1335
+ const l =
1336
+ (0.299 * band.data[i] + 0.587 * band.data[i + 1] + 0.114 * band.data[i + 2]) * (a / 255);
1337
+ luma = Math.max(luma, l);
1338
+ alpha = Math.max(alpha, a);
1339
+ }
1340
+ levels.push([luma, alpha]);
1341
+ }
1342
+ let steps = 0;
1343
+ for (let i = 2; i < levels.length; i++) {
1344
+ const [luma, alpha] = levels[i];
1345
+ const [backLuma, backAlpha] = levels[i - 2];
1346
+ if (Math.max(Math.abs(luma - backLuma), Math.abs(alpha - backAlpha)) < CANVAS_EDGE_STEP)
1347
+ continue;
1348
+ steps++;
1349
+ i += 2;
1350
+ }
1351
+ return steps;
1352
+ }
1353
+
1354
+ function canvasEdgeHasContent(canvas, rect, edge) {
1355
+ try {
1356
+ const band = canvasEdgeBand(canvas, rect, edge);
1357
+ return !!band && sharpSteps(band, edge.vertical) >= CANVAS_EDGE_MIN_STEPS;
1358
+ } catch {
1359
+ return false;
1360
+ }
1361
+ }
1362
+
1363
+ // ponytail: maps through the bounding box, exact for scale and translate; a rotated or skewed
1364
+ // canvas reads the wrong pixels. Add a matrix inverse if that bites.
1365
+ function canvasEdgeIssues(root, rootRect, time, tolerance) {
1366
+ const issues = [];
1367
+ for (const canvas of Array.from(root.querySelectorAll("canvas"))) {
1368
+ if (!isVisibleElement(canvas) || hasAllowOverflowFlag(canvas)) continue;
1369
+ if (!canvas.width || !canvas.height) continue;
1370
+ const rect = toRect(canvas.getBoundingClientRect());
1371
+ if (!rect.width || !rect.height) continue;
1372
+ const sides = coveredFrameEdges(rect, rootRect, tolerance)
1373
+ .filter((edge) => canvasEdgeHasContent(canvas, rect, edge))
1374
+ .map((edge) => edge.side);
1375
+ if (sides.length === 0) continue;
1376
+ issues.push({
1377
+ code: "canvas_content_at_edge",
1378
+ severity: "warning",
1379
+ time,
1380
+ selector: selectorFor(canvas),
1381
+ containerSelector: selectorFor(root),
1382
+ message: `Canvas content touches the frame edge (${sides.join(", ")}).`,
1383
+ rect,
1384
+ containerRect: rootRect,
1385
+ fixHint:
1386
+ "Keep drawn text inside the visible part of the canvas, or mark intentional full-bleed art with data-layout-allow-overflow.",
1387
+ });
1388
+ }
1389
+ return issues;
1390
+ }
1391
+
1277
1392
  // Soft prior only — the counterfactual attach test (below) is what makes detachment a finding.
1278
1393
  const CONNECTOR_NAME = /\b(conn(ector)?|arrow|edge|link|flow|wire)\b/i;
1279
1394
  const CONNECTOR_SKIP_CONTAINERS = "defs, marker, clipPath, mask, symbol, pattern";
@@ -1646,6 +1761,7 @@
1646
1761
  const escaped = escapedContainerIssues(root, time);
1647
1762
  issues.push(...escaped.issues);
1648
1763
  issues.push(...panelOutOfCanvasIssues(root, rootRect, time, tolerance, escaped.flagged));
1764
+ issues.push(...canvasEdgeIssues(root, rootRect, time, tolerance));
1649
1765
  issues.push(...connectorDetachmentIssues(root, rootRect, time));
1650
1766
  issues.push(...connectorOrphanIssues(root, rootRect, time));
1651
1767
  return issues;