hyperframes 0.7.53 → 0.7.55

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.
@@ -796,6 +796,51 @@
796
796
  };
797
797
  }
798
798
 
799
+ // Text whose glyphs paint with an effectively transparent fill renders
800
+ // invisibly even though the element, its box, opacity and color all read as
801
+ // present — so geometry/occlusion/contrast audits miss it (contrast reads
802
+ // `color`, not the fill that actually paints). `-webkit-text-fill-color`
803
+ // overrides `color` for the glyph fill AND inherits, so a parent's
804
+ // `transparent` fill silently blanks descendant text that has its own opaque
805
+ // `color`. Its computed value already resolves to `color` when unset, so it
806
+ // is the effective fill directly. Clipped text (`background-clip: text`)
807
+ // legitimately uses a transparent fill — BUT only when a background actually
808
+ // paints the glyphs; a `background-clip: text` with no gradient/image and no
809
+ // opaque background-color paints nothing, so it stays reportable.
810
+ function invisibleTextIssue(element, time) {
811
+ const textRect = textRectFor(element);
812
+ if (!textRect) return null;
813
+ const text = textContentFor(element);
814
+ if (!text) return null;
815
+ const cs = getComputedStyle(element);
816
+ // Vendor computed-style props are read by property (camelCase), matching
817
+ // the rest of this script; `webkitTextFillColor` computes to `color` when
818
+ // unset, so it is the effective fill directly.
819
+ const fill = cs.webkitTextFillColor || cs.color;
820
+ if (colorAlpha(fill) > 0.05) return null;
821
+ const clip = cs.webkitBackgroundClip || cs.backgroundClip || "";
822
+ if (/text/i.test(clip)) {
823
+ const bgImage = cs.backgroundImage || "none";
824
+ const paintsGlyphs =
825
+ bgImage !== "none" || colorAlpha(cs.backgroundColor || "rgba(0, 0, 0, 0)") > 0.05;
826
+ // A usable clipped background fills the glyphs — legitimate gradient/solid
827
+ // clipped text. If nothing paints, fall through and report it.
828
+ if (paintsGlyphs) return null;
829
+ }
830
+ return {
831
+ code: "text_not_painted",
832
+ severity: "error",
833
+ time,
834
+ selector: selectorFor(element),
835
+ text,
836
+ message:
837
+ "Text paints with an effectively transparent fill (-webkit-text-fill-color / color), so its glyphs are invisible.",
838
+ rect: textRect,
839
+ fixHint:
840
+ "Set an explicit, opaque `color` on the text — and an explicit `-webkit-text-fill-color` if an ancestor makes the fill transparent. If the transparency is intentional gradient text, add `background-clip: text`.",
841
+ };
842
+ }
843
+
799
844
  function candidateAnchor(element) {
800
845
  const dataAttributes = {};
801
846
  for (const attribute of Array.from(element.attributes)) {
@@ -881,6 +926,8 @@
881
926
  issues.push(...textOverflowIssues(element, root, rootRect, time, tolerance));
882
927
  const occluded = occludedTextIssue(element, time);
883
928
  if (occluded) issues.push(occluded);
929
+ const invisible = invisibleTextIssue(element, time);
930
+ if (invisible) issues.push(invisible);
884
931
  }
885
932
 
886
933
  issues.push(...containerOverflowIssues(root, time, tolerance));
@@ -895,6 +942,31 @@
895
942
  // actually moved anything and the whole audit run is unreliable. Deliberately
896
943
  // a single opaque string (not a structured array) since Node only ever needs
897
944
  // equality, not per-element diffing.
945
+ // Pixel-only canvas motion (a 2D/WebGL canvas repainting without any element
946
+ // moving) is invisible to a geometry+opacity fingerprint and false-positived
947
+ // sweep_static (wild report: 4K WebGL comp needed a transparent moving DOM
948
+ // sentinel to pass). Downsample each visible canvas to 8x8 and fold its
949
+ // pixels into the fingerprint. Tainted/zero-sized/unreadable canvases hash
950
+ // to a constant — no worse than today, never a new false NEGATIVE for
951
+ // DOM-motion comps.
952
+ function canvasPixelHash(canvas) {
953
+ try {
954
+ if (!canvas.width || !canvas.height) return "x";
955
+ const off = document.createElement("canvas");
956
+ off.width = 8;
957
+ off.height = 8;
958
+ const ctx = off.getContext("2d");
959
+ if (!ctx) return "x";
960
+ ctx.drawImage(canvas, 0, 0, 8, 8);
961
+ const data = ctx.getImageData(0, 0, 8, 8).data;
962
+ let hash = 0;
963
+ for (let i = 0; i < data.length; i++) hash = (hash * 31 + data[i]) >>> 0;
964
+ return String(hash);
965
+ } catch {
966
+ return "x";
967
+ }
968
+ }
969
+
898
970
  window.__hyperframesLayoutGeometry = function collectLayoutGeometry() {
899
971
  const root =
900
972
  document.querySelector("[data-composition-id][data-width][data-height]") ||
@@ -903,12 +975,15 @@
903
975
  const elements = Array.from(root.querySelectorAll("*")).filter((element) =>
904
976
  isVisibleElement(element),
905
977
  );
906
- return elements
907
- .map((element) => {
908
- const rect = toRect(element.getBoundingClientRect());
909
- const opacity = round(opacityChain(element));
910
- return `${rect.left},${rect.top},${rect.width},${rect.height},${opacity}`;
911
- })
912
- .join("|");
978
+ const parts = elements.map((element) => {
979
+ const rect = toRect(element.getBoundingClientRect());
980
+ const opacity = round(opacityChain(element));
981
+ return `${rect.left},${rect.top},${rect.width},${rect.height},${opacity}`;
982
+ });
983
+ for (const canvas of root.querySelectorAll("canvas")) {
984
+ if (!isVisibleElement(canvas)) continue;
985
+ parts.push(`c:${canvasPixelHash(canvas)}`);
986
+ }
987
+ return parts.join("|");
913
988
  };
914
989
  })();