返回 presentation-ai
infographic-card-background.ts
根目录 / src / components / notebook / presentation / editor / utils / infographic-card-background.ts
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
124 lines TYPESCRIPT