388 lines
12 KiB
JavaScript
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
|
|
};
|
|
})(); |