Files
TaroTime/app/ui-kabbalah-views.js
2026-03-07 13:38:13 -08:00

388 lines
12 KiB
JavaScript

(function () {
"use strict";
function resolvePathTarotImage(path) {
const cardName = String(path?.tarot?.card || "").trim();
if (!cardName || typeof window.TarotCardImages?.resolveTarotCardImage !== "function") {
return null;
}
return window.TarotCardImages.resolveTarotCardImage(cardName);
}
function getSvgImageHref(imageEl) {
if (!(imageEl instanceof SVGElement)) {
return "";
}
return String(
imageEl.getAttribute("href")
|| imageEl.getAttributeNS("http://www.w3.org/1999/xlink", "href")
|| ""
).trim();
}
function openTarotLightboxForPath(path, fallbackSrc = "") {
const openLightbox = window.TarotUiLightbox?.open;
if (typeof openLightbox !== "function") {
return false;
}
const cardName = String(path?.tarot?.card || "").trim();
const src = String(fallbackSrc || resolvePathTarotImage(path) || "").trim();
if (!src) {
return false;
}
const fallbackLabel = Number.isFinite(Number(path?.pathNumber))
? `Path ${path.pathNumber} tarot card`
: "Path tarot card";
openLightbox(src, cardName || fallbackLabel);
return true;
}
function getPathLabel(context, path) {
const glyph = String(path?.hebrewLetter?.char || "").trim();
const pathNumber = Number(path?.pathNumber);
const parts = [];
if (context.state.showPathLetters && glyph) {
parts.push(glyph);
}
if (context.state.showPathNumbers && Number.isFinite(pathNumber)) {
parts.push(String(pathNumber));
}
return parts.join(" ");
}
function svgEl(context, tag, attrs, text) {
const el = document.createElementNS(context.NS, tag);
for (const [key, value] of Object.entries(attrs || {})) {
el.setAttribute(key, String(value));
}
if (text != null) {
el.textContent = text;
}
return el;
}
function buildTreeSVG(context) {
const {
tree,
state,
NODE_POS,
SEPH_FILL,
DARK_TEXT,
DAAT,
PATH_LABEL_RADIUS,
PATH_LABEL_FONT_SIZE,
PATH_TAROT_WIDTH,
PATH_TAROT_HEIGHT,
PATH_LABEL_OFFSET_WITH_TAROT,
PATH_TAROT_OFFSET_WITH_LABEL,
PATH_TAROT_OFFSET_NO_LABEL,
R
} = context;
const svg = svgEl(context, "svg", {
viewBox: "0 0 240 470",
width: "100%",
role: "img",
"aria-label": "Kabbalah Tree of Life diagram",
class: "kab-svg"
});
svg.appendChild(svgEl(context, "rect", {
x: 113, y: 30, width: 14, height: 420,
rx: 7, fill: "#ffffff07", "pointer-events": "none"
}));
svg.appendChild(svgEl(context, "rect", {
x: 33, y: 88, width: 14, height: 255,
rx: 7, fill: "#ff220010", "pointer-events": "none"
}));
svg.appendChild(svgEl(context, "rect", {
x: 193, y: 88, width: 14, height: 255,
rx: 7, fill: "#2244ff10", "pointer-events": "none"
}));
[
{ x: 198, y: 73, text: "Mercy", anchor: "middle" },
{ x: 120, y: 17, text: "Balance", anchor: "middle" },
{ x: 42, y: 73, text: "Severity", anchor: "middle" }
].forEach(({ x, y, text, anchor }) => {
svg.appendChild(svgEl(context, "text", {
x, y, "text-anchor": anchor, "dominant-baseline": "auto",
fill: "#42425a", "font-size": "6", "pointer-events": "none"
}, text));
});
tree.paths.forEach((path) => {
const [x1, y1] = NODE_POS[path.connects.from];
const [x2, y2] = NODE_POS[path.connects.to];
const mx = (x1 + x2) / 2;
const my = (y1 + y2) / 2;
const tarotImage = state.showPathTarotCards ? resolvePathTarotImage(path) : null;
const hasTarotImage = Boolean(tarotImage);
const pathLabel = getPathLabel(context, path);
const hasLabel = Boolean(pathLabel);
const labelY = hasTarotImage && hasLabel ? my - PATH_LABEL_OFFSET_WITH_TAROT : my;
svg.appendChild(svgEl(context, "line", {
x1, y1, x2, y2,
class: "kab-path-line",
"data-path": path.pathNumber,
stroke: "#3c3c5c",
"stroke-width": "1.5",
"pointer-events": "none"
}));
svg.appendChild(svgEl(context, "line", {
x1, y1, x2, y2,
class: "kab-path-hit",
"data-path": path.pathNumber,
stroke: "transparent",
"stroke-width": String(12 * context.PATH_MARKER_SCALE),
role: "button",
tabindex: "0",
"aria-label": `Path ${path.pathNumber}: ${path.hebrewLetter?.transliteration || ""}${path.tarot?.card || ""}`,
style: "cursor:pointer"
}));
if (hasLabel) {
svg.appendChild(svgEl(context, "circle", {
cx: mx, cy: labelY, r: PATH_LABEL_RADIUS.toFixed(2),
fill: "#0d0d1c", opacity: "0.82",
"pointer-events": "none"
}));
svg.appendChild(svgEl(context, "text", {
x: mx, y: labelY + 1,
"text-anchor": "middle",
"dominant-baseline": "middle",
class: "kab-path-lbl",
"data-path": path.pathNumber,
fill: "#a8a8e0",
"font-size": PATH_LABEL_FONT_SIZE.toFixed(2),
"pointer-events": "none"
}, pathLabel));
}
if (hasTarotImage) {
const tarotY = hasLabel
? my + PATH_TAROT_OFFSET_WITH_LABEL
: my - PATH_TAROT_OFFSET_NO_LABEL;
svg.appendChild(svgEl(context, "image", {
href: tarotImage,
x: (mx - (PATH_TAROT_WIDTH / 2)).toFixed(2),
y: tarotY.toFixed(2),
width: PATH_TAROT_WIDTH.toFixed(2),
height: PATH_TAROT_HEIGHT.toFixed(2),
preserveAspectRatio: "xMidYMid meet",
class: "kab-path-tarot",
"data-path": path.pathNumber,
role: "button",
tabindex: "0",
"aria-label": `Path ${path.pathNumber} Tarot card ${path.tarot?.card || ""}`,
style: "cursor:pointer"
}));
}
});
svg.appendChild(svgEl(context, "circle", {
cx: DAAT[0], cy: DAAT[1], r: "9",
fill: "none", stroke: "#3c3c5c",
"stroke-dasharray": "3 2", "stroke-width": "1",
"pointer-events": "none"
}));
svg.appendChild(svgEl(context, "text", {
x: DAAT[0] + 13, y: DAAT[1] + 1,
"text-anchor": "start", "dominant-baseline": "middle",
fill: "#3c3c5c", "font-size": "6.5", "pointer-events": "none"
}, "Da'at"));
tree.sephiroth.forEach((seph) => {
const [cx, cy] = NODE_POS[seph.number];
const fill = SEPH_FILL[seph.number] || "#555";
const isLeft = cx < 80;
const isMid = cx === 120;
svg.appendChild(svgEl(context, "circle", {
cx, cy, r: "16",
fill, opacity: "0.12",
class: "kab-node-glow",
"data-sephira": seph.number,
"pointer-events": "none"
}));
svg.appendChild(svgEl(context, "circle", {
cx, cy, r: R,
fill, stroke: "#00000040", "stroke-width": "1",
class: "kab-node",
"data-sephira": seph.number,
role: "button",
tabindex: "0",
"aria-label": `Sephira ${seph.number}: ${seph.name}`,
style: "cursor:pointer"
}));
svg.appendChild(svgEl(context, "text", {
x: cx, y: cy + 0.5,
"text-anchor": "middle", "dominant-baseline": "middle",
fill: DARK_TEXT.has(seph.number) ? "#111" : "#fff",
"font-size": "8", "font-weight": "bold",
"pointer-events": "none"
}, String(seph.number)));
const lx = isLeft ? cx - R - 4 : cx + R + 4;
svg.appendChild(svgEl(context, "text", {
x: isMid ? cx : lx,
y: isMid ? cy + R + 8 : cy,
"text-anchor": isMid ? "middle" : (isLeft ? "end" : "start"),
"dominant-baseline": isMid ? "auto" : "middle",
fill: "#c0c0d4",
"font-size": "7.5", "pointer-events": "none",
class: "kab-node-lbl"
}, seph.name));
});
return svg;
}
function bindTreeInteractions(context, svg) {
const { tree, elements, renderSephiraDetail, renderPathDetail } = context;
svg.addEventListener("click", (event) => {
const clickTarget = event.target instanceof Element ? event.target : null;
const sephNum = clickTarget?.dataset?.sephira;
const pathNum = clickTarget?.dataset?.path;
if (pathNum != null && clickTarget?.classList?.contains("kab-path-tarot")) {
const path = tree.paths.find((entry) => entry.pathNumber === Number(pathNum));
if (path) {
openTarotLightboxForPath(path, getSvgImageHref(clickTarget));
renderPathDetail(path, tree, elements);
}
return;
}
if (sephNum != null) {
const seph = tree.sephiroth.find((entry) => entry.number === Number(sephNum));
if (seph) {
renderSephiraDetail(seph, tree, elements);
}
} else if (pathNum != null) {
const path = tree.paths.find((entry) => entry.pathNumber === Number(pathNum));
if (path) {
renderPathDetail(path, tree, elements);
}
}
});
svg.querySelectorAll(".kab-path-hit, .kab-path-tarot").forEach((element) => {
element.addEventListener("keydown", (event) => {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
const path = tree.paths.find((entry) => entry.pathNumber === Number(element.dataset.path));
if (path) {
if (element.classList.contains("kab-path-tarot")) {
openTarotLightboxForPath(path, getSvgImageHref(element));
}
renderPathDetail(path, tree, elements);
}
}
});
});
svg.querySelectorAll(".kab-node").forEach((element) => {
element.addEventListener("keydown", (event) => {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
const seph = tree.sephiroth.find((entry) => entry.number === Number(element.dataset.sephira));
if (seph) {
renderSephiraDetail(seph, tree, elements);
}
}
});
});
}
function bindRoseCrossInteractions(context, svg, roseElements) {
const { tree, renderPathDetail } = context;
if (!svg || !roseElements?.detailBodyEl) {
return;
}
const openPathFromTarget = (targetEl) => {
if (!(targetEl instanceof Element)) {
return;
}
const petal = targetEl.closest(".kab-rose-petal[data-path]");
if (!(petal instanceof SVGElement)) {
return;
}
const pathNumber = Number(petal.dataset.path);
if (!Number.isFinite(pathNumber)) {
return;
}
const path = tree.paths.find((entry) => entry.pathNumber === pathNumber);
if (path) {
renderPathDetail(path, tree, roseElements);
}
};
svg.addEventListener("click", (event) => {
openPathFromTarget(event.target);
});
svg.querySelectorAll(".kab-rose-petal[data-path]").forEach((petal) => {
petal.addEventListener("keydown", (event) => {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
openPathFromTarget(petal);
}
});
});
}
function renderRoseCross(context) {
const { state, elements, getRoseDetailElements } = context;
if (!state.tree || !elements?.roseCrossContainerEl) {
return;
}
const roseElements = getRoseDetailElements(elements);
if (!roseElements?.detailBodyEl) {
return;
}
const roseBuilder = window.KabbalahRosicrucianCross?.buildRosicrucianCrossSVG;
if (typeof roseBuilder !== "function") {
return;
}
const roseSvg = roseBuilder(state.tree);
elements.roseCrossContainerEl.innerHTML = "";
elements.roseCrossContainerEl.appendChild(roseSvg);
bindRoseCrossInteractions(context, roseSvg, roseElements);
}
function renderTree(context) {
const { state, elements } = context;
if (!state.tree || !elements?.treeContainerEl) {
return;
}
const svg = buildTreeSVG(context);
elements.treeContainerEl.innerHTML = "";
elements.treeContainerEl.appendChild(svg);
bindTreeInteractions(context, svg);
}
window.KabbalahViewsUi = {
renderTree,
renderRoseCross
};
})();