// ===================================================================== // shared utilities + hooks // ===================================================================== const { useState, useEffect, useRef, useMemo, useCallback, useLayoutEffect } = React; // Color mapping helpers function roleColor(role) { return ({ sheng: "#d4a24c", dan: "#e8c4b8", jing: "#3a5a6e", chou: "#b8945a" })[role] || "#aaa"; } function genreColor(g) { return ({ history: "#c8341c", family: "#d4a24c", court: "#5e8a6a" })[g] || "#aaa"; } function genreLabel(g) { return ({ history: "历史戏", family: "家庭戏", court: "公案戏" })[g] || g; } function roleLabel(r) { return ({ sheng: "生", dan: "旦", jing: "净", chou: "丑" })[r] || r; } function linkClassFromKind(k) { if (["foe","deceit"].includes(k)) return "foe"; if (["family","romantic"].includes(k)) return k === "romantic" ? "rom" : "fam"; if (["master","loyal","mercy","justice"].includes(k)) return "master"; return "ally"; } // hook: container size with ResizeObserver function useSize() { const ref = useRef(null); const [size, setSize] = useState({ w: 0, h: 0 }); useLayoutEffect(() => { if (!ref.current) return; const ro = new ResizeObserver(entries => { for (const e of entries) { const r = e.contentRect; setSize({ w: r.width, h: r.height }); } }); ro.observe(ref.current); return () => ro.disconnect(); }, []); return [ref, size]; } // simple seeded random for stable layout function mulberry32(a) { return function() { a |= 0; a = a + 0x6D2B79F5 | 0; let t = a; t = Math.imul(t ^ t >>> 15, t | 1); t ^= t + Math.imul(t ^ t >>> 7, t | 61); return ((t ^ t >>> 14) >>> 0) / 4294967296; }; } Object.assign(window, { roleColor, genreColor, genreLabel, roleLabel, linkClassFromKind, useSize, mulberry32, });