| 1 | "use client"; |
| 2 | |
| 3 | import { useEffect, type RefObject } from "react"; |
| 4 | |
| 5 | const DARK_CHANNEL_THRESHOLD = 16; |
| 6 | const CARD_BACKGROUND_MARKER = "data-antv-card-background-fill"; |
| 7 | |
| 8 | function parseHexChannel(value: string): number { |
| 9 | return Number.parseInt(value, 16); |
| 10 | } |
| 11 | |
| 12 | function isNearBlackHex(color: string): boolean { |
| 13 | const shorthand = color.match(/^#([0-9a-f]{3})$/i); |
| 14 | if (shorthand) { |
| 15 | const [r, g, b] = shorthand[1]!.split(""); |
| 16 | return [r, g, b].every( |
| 17 | (channel) => |
| 18 | parseHexChannel(`${channel}${channel}`) <= DARK_CHANNEL_THRESHOLD, |
| 19 | ); |
| 20 | } |
| 21 | |
| 22 | const full = color.match(/^#([0-9a-f]{6})$/i); |
| 23 | if (!full) return false; |
| 24 | |
| 25 | const value = full[1]!; |
| 26 | return [value.slice(0, 2), value.slice(2, 4), value.slice(4, 6)].every( |
| 27 | (channel) => parseHexChannel(channel) <= DARK_CHANNEL_THRESHOLD, |
| 28 | ); |
| 29 | } |
| 30 | |
| 31 | function isNearBlackRgb(color: string): boolean { |
| 32 | const match = color.match( |
| 33 | /^rgba?\(\s*(\d+(?:\.\d+)?)\s*,\s*(\d+(?:\.\d+)?)\s*,\s*(\d+(?:\.\d+)?)(?:\s*,\s*(\d?(?:\.\d+)?))?\s*\)$/i, |
| 34 | ); |
| 35 | if (!match) return false; |
| 36 | |
| 37 | const alpha = match[4] === undefined ? 1 : Number.parseFloat(match[4]); |
| 38 | if (alpha <= 0) return false; |
| 39 | |
| 40 | return [match[1], match[2], match[3]].every( |
| 41 | (channel) => Number.parseFloat(channel!) <= DARK_CHANNEL_THRESHOLD, |
| 42 | ); |
| 43 | } |
| 44 | |
| 45 | function isNearBlackColor(color: string | null): boolean { |
| 46 | if (!color) return false; |
| 47 | |
| 48 | const normalized = color.trim().toLowerCase(); |
| 49 | if (normalized === "black") return true; |
| 50 | if (normalized.startsWith("#")) return isNearBlackHex(normalized); |
| 51 | return isNearBlackRgb(normalized); |
| 52 | } |
| 53 | |
| 54 | function resolvePresentationCardBackground(container: HTMLElement): string { |
| 55 | const probe = document.createElement("span"); |
| 56 | probe.style.color = "var(--presentation-card-background, hsl(var(--card)))"; |
| 57 | probe.style.display = "none"; |
| 58 | container.appendChild(probe); |
| 59 | const color = window.getComputedStyle(probe).color; |
| 60 | probe.remove(); |
| 61 | |
| 62 | return color || "hsl(var(--card))"; |
| 63 | } |
| 64 | |
| 65 | export function enforceInfographicCardBackground(container: HTMLElement): void { |
| 66 | const cardBackground = resolvePresentationCardBackground(container); |
| 67 | const rects = container.querySelectorAll<SVGRectElement>("svg rect"); |
| 68 | |
| 69 | for (const rect of rects) { |
| 70 | if (rect.closest('g[data-element-type="transient-container"]')) continue; |
| 71 | |
| 72 | const fill = rect.getAttribute("fill"); |
| 73 | const appliedCardBackground = rect.getAttribute(CARD_BACKGROUND_MARKER); |
| 74 | if (!isNearBlackColor(fill) && !appliedCardBackground) continue; |
| 75 | if (fill === cardBackground && appliedCardBackground === cardBackground) { |
| 76 | continue; |
| 77 | } |
| 78 | |
| 79 | rect.setAttribute("fill", cardBackground); |
| 80 | rect.setAttribute(CARD_BACKGROUND_MARKER, cardBackground); |
| 81 | |
| 82 | const stroke = rect.getAttribute("stroke"); |
| 83 | if (isNearBlackColor(stroke)) { |
| 84 | rect.setAttribute("stroke", cardBackground); |
| 85 | } |
| 86 | } |
| 87 | } |
| 88 | |
| 89 | export function useInfographicCardBackground( |
| 90 | containerRef: RefObject<HTMLElement | null>, |
| 91 | refreshKey: string, |
| 92 | ): void { |
| 93 | useEffect(() => { |
| 94 | const container = containerRef.current; |
| 95 | if (!container) return; |
| 96 | |
| 97 | let frameId: number | null = null; |
| 98 | const scheduleEnforcement = () => { |
| 99 | if (frameId !== null) return; |
| 100 | frameId = window.requestAnimationFrame(() => { |
| 101 | frameId = null; |
| 102 | enforceInfographicCardBackground(container); |
| 103 | }); |
| 104 | }; |
| 105 | |
| 106 | scheduleEnforcement(); |
| 107 | |
| 108 | const observer = new MutationObserver(scheduleEnforcement); |
| 109 | observer.observe(container, { |
| 110 | attributes: true, |
| 111 | attributeFilter: ["fill", "stroke"], |
| 112 | childList: true, |
| 113 | subtree: true, |
| 114 | }); |
| 115 | |
| 116 | return () => { |
| 117 | observer.disconnect(); |
| 118 | if (frameId !== null) { |
| 119 | window.cancelAnimationFrame(frameId); |
| 120 | } |
| 121 | }; |
| 122 | }, [containerRef, refreshKey]); |
| 123 | } |
| 124 |