/* Cosmic Bagua compass — dark, glowing, with light-beam effects */
const COSMIC_DATA = {
trigrams: [
{ sym: "☰", name: "乾" }, { sym: "☱", name: "兑" },
{ sym: "☲", name: "离" }, { sym: "☳", name: "震" },
{ sym: "☴", name: "巽" }, { sym: "☵", name: "坎" },
{ sym: "☶", name: "艮" }, { sym: "☷", name: "坤" },
],
branches: ["子","丑","寅","卯","辰","巳","午","未","申","酉","戌","亥"],
solarTerms: [
"冬至","小寒","大寒","立春","雨水","惊蛰",
"春分","清明","谷雨","立夏","小满","芒种",
"夏至","小暑","大暑","立秋","处暑","白露",
"秋分","寒露","霜降","立冬","小雪","大雪"
],
mountains: [
"壬","子","癸","丑","艮","寅","甲","卯","乙","辰","巽","巳",
"丙","午","丁","未","坤","申","庚","酉","辛","戌","乾","亥"
],
hexagrams: [
"乾","坤","屯","蒙","需","讼","师","比",
"小畜","履","泰","否","同人","大有","谦","豫",
"随","蛊","临","观","噬嗑","贲","剥","复",
"无妄","大畜","颐","大过","坎","离","咸","恒",
"遁","大壮","晋","明夷","家人","睽","蹇","解",
"损","益","夬","姤","萃","升","困","井",
"革","鼎","震","艮","渐","归妹","丰","旅",
"巽","兑","涣","节","中孚","小过","既济","未济"
],
starSigils: [
"角","亢","氐","房","心","尾","箕",
"斗","牛","女","虚","危","室","壁",
"奎","娄","胃","昴","毕","觜","参",
"井","鬼","柳","星","张","翼","轸"
],
};
/* Cosmic Ring */
function CosmicRing({
items, innerR, outerR, rotation, onRotate, fontSize,
glowColor, textColor, divider, fillEven, fillOdd,
zIndex = 1, renderItem, accentIndices = [],
}) {
const ref = React.useRef(null);
const drag = React.useRef(null);
const n = items.length;
const step = 360 / n;
const cx = outerR, cy = outerR;
const onPointerDown = (e) => {
const rect = ref.current.getBoundingClientRect();
const ox = rect.left + rect.width / 2;
const oy = rect.top + rect.height / 2;
const a0 = Math.atan2(e.clientY - oy, e.clientX - ox) * 180 / Math.PI;
drag.current = { a0, r0: rotation, ox, oy };
e.currentTarget.setPointerCapture(e.pointerId);
};
const onPointerMove = (e) => {
if (!drag.current) return;
const { a0, r0 } = drag.current;
const a = Math.atan2(e.clientY - drag.current.oy, e.clientX - drag.current.ox) * 180 / Math.PI;
onRotate(r0 + (a - a0));
};
const onPointerUp = (e) => {
drag.current = null;
try { e.currentTarget.releasePointerCapture(e.pointerId); } catch {}
};
const sectors = [];
for (let i = 0; i < n; i++) {
const a1 = (i * step - 90) * Math.PI / 180;
const a2 = ((i + 1) * step - 90) * Math.PI / 180;
const x1o = cx + outerR * Math.cos(a1), y1o = cy + outerR * Math.sin(a1);
const x2o = cx + outerR * Math.cos(a2), y2o = cy + outerR * Math.sin(a2);
const x1i = cx + innerR * Math.cos(a1), y1i = cy + innerR * Math.sin(a1);
const x2i = cx + innerR * Math.cos(a2), y2i = cy + innerR * Math.sin(a2);
const large = step > 180 ? 1 : 0;
const d = `M ${x1o} ${y1o} A ${outerR} ${outerR} 0 ${large} 1 ${x2o} ${y2o} L ${x2i} ${y2i} A ${innerR} ${innerR} 0 ${large} 0 ${x1i} ${y1i} Z`;
sectors.push(
);
}
const midR = (innerR + outerR) / 2;
const labels = items.map((it, i) => {
const ang = i * step + step / 2 - 90;
const rad = ang * Math.PI / 180;
const x = cx + midR * Math.cos(rad);
const y = cy + midR * Math.sin(rad);
const rot = ang + 90;
const isAccent = accentIndices.includes(i);
const content = renderItem ? renderItem(it, i, isAccent) : it;
return (
{typeof content === "string" ? {content} : content}
);
});
return (
);
}
/* Glowing Taiji */
function CosmicTaiji({ size, rotation = 0, hue = "gold" }) {
const colors = hue === "cyan"
? { light: "#9ff5ff", dark: "#0a1a26", glow: "#5ed8ff" }
: { light: "#ffe9a8", dark: "#1a0e05", glow: "#ffce5c" };
return (
);
}
/* Vertical light beams rising and descending */
function LightBeams({ baseR, hue = "gold" }) {
const beams = React.useMemo(() => {
const arr = [];
const N = 24;
for (let i = 0; i < N; i++) {
const ang = (i * 360 / N) * Math.PI / 180;
const r = baseR + 20 + Math.random() * 30;
arr.push({
x: r * Math.cos(ang - Math.PI / 2),
y: r * Math.sin(ang - Math.PI / 2),
delay: Math.random() * 4,
dur: 2.5 + Math.random() * 2.5,
height: 80 + Math.random() * 220,
width: 1.5 + Math.random() * 2.5,
});
}
return arr;
}, [baseR]);
const beamColor = hue === "cyan" ? "#5ed8ff" : "#ffce5c";
return (
{beams.map((b, i) => (
))}
);
}
/* Constellation dots orbiting outside */
function OrbitingStars({ baseR, hue = "gold" }) {
const dots = React.useMemo(() => {
return Array.from({ length: 60 }, () => ({
ang: Math.random() * 360,
r: baseR + 60 + Math.random() * 200,
size: 1 + Math.random() * 2.5,
blink: Math.random() * 4,
blinkDur: 2 + Math.random() * 4,
}));
}, [baseR]);
const c = hue === "cyan" ? "#9ff5ff" : "#ffe9a8";
return (
{dots.map((d, i) => {
const rad = d.ang * Math.PI / 180;
return (
);
})}
);
}
/* The Cosmic Compass */
function CosmicCompass({ tweaks }) {
const baseR = 360;
const accentColors = tweaks.hue === "cyan"
? { glow: "#5ed8ff", text: "#cfeeff", divider: "rgba(94,216,255,0.55)", fillA: "rgba(20,60,90,0.55)", fillB: "rgba(8,28,48,0.65)" }
: { glow: "#ffce5c", text: "#f1dba0", divider: "rgba(255,206,92,0.5)", fillA: "rgba(80,40,8,0.55)", fillB: "rgba(40,20,4,0.65)" };
// 4 cardinal indices for mountains: 子 卯 午 酉 → indices 1, 7, 13, 19
const cardinalsMountains = [1, 7, 13, 19];
const ringSpec = [
{ key: "trigrams", inner: 80, outer: 115, items: COSMIC_DATA.trigrams.map(t => t.name), font: 18 },
{ key: "branches", inner: 115, outer: 150, items: COSMIC_DATA.branches, font: 20 },
{ key: "terms", inner: 150, outer: 190, items: COSMIC_DATA.solarTerms, font: 11,
renderItem: (s) => (
{s[0]}
{s[1]}
)
},
{ key: "mountains", inner: 190, outer: 232, items: COSMIC_DATA.mountains, font: 22, accentIndices: cardinalsMountains },
{ key: "hexagrams", inner: 232, outer: 295, items: COSMIC_DATA.hexagrams, font: 13,
renderItem: (s) => s.length === 1 ? {s} : (
{s[0]}
{s[1]}
)
},
{ key: "stars", inner: 295, outer: baseR, items: [...COSMIC_DATA.starSigils, ...COSMIC_DATA.starSigils.slice(0, 36)].slice(0, 36), font: 16 },
];
const [rotations, setRotations] = React.useState(
Object.fromEntries(ringSpec.map(r => [r.key, 0]))
);
const burstRef = React.useRef(null);
React.useEffect(() => {
const onBurst = (e) => {
const dur = (e.detail && e.detail.duration) || 30000;
burstRef.current = { start: performance.now(), duration: dur };
};
window.addEventListener('compass-burst', onBurst);
return () => window.removeEventListener('compass-burst', onBurst);
}, []);
React.useEffect(() => {
let raf, last = performance.now();
const tick = (t) => {
const dt = Math.min(0.05, (t - last) / 1000);
last = t;
const burst = burstRef.current;
let burstMult = 1;
if (burst) {
const elapsed = t - burst.start;
if (elapsed < burst.duration) {
const k = elapsed / burst.duration;
// Strong easing-out: starts ~80x, decays to 1x
burstMult = 1 + 80 * Math.pow(1 - k, 3.2);
} else {
burstRef.current = null;
}
}
const active = tweaks.autoSpin || burstRef.current;
if (active) {
setRotations(prev => {
const next = { ...prev };
ringSpec.forEach((r, i) => {
const dir = i % 2 === 0 ? 1 : -1;
const base = (tweaks.spinSpeed || 6) * (1 - i * 0.08);
const speed = base * burstMult;
next[r.key] = (prev[r.key] || 0) + dir * speed * dt;
});
return next;
});
}
raf = requestAnimationFrame(tick);
};
raf = requestAnimationFrame(tick);
return () => cancelAnimationFrame(raf);
}, [tweaks.autoSpin, tweaks.spinSpeed]);
// Pulsing aura
const [pulse, setPulse] = React.useState(0);
React.useEffect(() => {
let raf;
const tick = () => {
setPulse(performance.now() / 1000);
raf = requestAnimationFrame(tick);
};
raf = requestAnimationFrame(tick);
return () => cancelAnimationFrame(raf);
}, []);
const pulseScale = 1 + Math.sin(pulse * 1.4) * 0.04;
const pulseOpacity = 0.45 + Math.sin(pulse * 1.4) * 0.2;
return (
{/* Pulsing aura behind everything */}
{/* Orbiting starfield */}
{tweaks.showStars &&
}
{/* Light beams rising/descending */}
{tweaks.showBeams &&
}
{/* Outer glowing rings */}
{/* Tick marks */}
{ringSpec.map((r, i) => (
setRotations(p => ({ ...p, [r.key]: v }))}
fontSize={r.font}
glowColor={accentColors.glow}
textColor={accentColors.text}
divider={accentColors.divider}
fillEven={accentColors.fillA}
fillOdd={accentColors.fillB}
renderItem={r.renderItem}
accentIndices={r.accentIndices || []}
zIndex={10 + (ringSpec.length - i)}
/>
))}
{/* Cardinal cross */}
);
}
window.CosmicCompass = CosmicCompass;