577 lines
19 KiB
JavaScript
577 lines
19 KiB
JavaScript
(function () {
|
||
"use strict";
|
||
|
||
// ─── SVG tree layout constants ──────────────────────────────────────────────
|
||
const NS = "http://www.w3.org/2000/svg";
|
||
const R = 11; // sephira circle radius
|
||
|
||
// Standard Hermetic GD Tree of Life positions in a 240×470 viewBox
|
||
const NODE_POS = {
|
||
1: [120, 30], // Kether — crown of middle pillar
|
||
2: [200, 88], // Chokmah — right pillar
|
||
3: [40, 88], // Binah — left pillar
|
||
4: [200, 213], // Chesed — right pillar
|
||
5: [40, 213], // Geburah — left pillar
|
||
6: [120, 273], // Tiphareth — middle pillar
|
||
7: [200, 343], // Netzach — right pillar
|
||
8: [40, 343], // Hod — left pillar
|
||
9: [120, 398], // Yesod — middle pillar
|
||
10: [120, 448], // Malkuth — bottom of middle pillar
|
||
};
|
||
|
||
// King-scale fill colours
|
||
const SEPH_FILL = {
|
||
1: "#e8e8e8", // Kether — white brilliance
|
||
2: "#87ceeb", // Chokmah — soft sky blue
|
||
3: "#7d2b5a", // Binah — dark crimson
|
||
4: "#1a56e0", // Chesed — deep blue
|
||
5: "#d44014", // Geburah — scarlet
|
||
6: "#d4a017", // Tiphareth — gold
|
||
7: "#22aa60", // Netzach — emerald
|
||
8: "#cc5500", // Hod — orange
|
||
9: "#6030c0", // Yesod — violet
|
||
10: "#c8b000", // Malkuth — citrine / amber
|
||
};
|
||
|
||
// Nodes with light-ish fills get dark number text; others get white
|
||
const DARK_TEXT = new Set([1, 2, 6, 10]);
|
||
|
||
// Da'at – phantom sephira drawn as a dashed circle, not clickable
|
||
const DAAT = [120, 148];
|
||
const PATH_MARKER_SCALE = 1.33;
|
||
const PATH_LABEL_RADIUS = 9 * PATH_MARKER_SCALE;
|
||
const PATH_LABEL_FONT_SIZE = 8.8 * PATH_MARKER_SCALE;
|
||
const PATH_TAROT_WIDTH = 16 * PATH_MARKER_SCALE;
|
||
const PATH_TAROT_HEIGHT = 24 * PATH_MARKER_SCALE;
|
||
const PATH_LABEL_OFFSET_WITH_TAROT = 11 * PATH_MARKER_SCALE;
|
||
const PATH_TAROT_OFFSET_WITH_LABEL = 1 * PATH_MARKER_SCALE;
|
||
const PATH_TAROT_OFFSET_NO_LABEL = 12 * PATH_MARKER_SCALE;
|
||
|
||
// ─── state ──────────────────────────────────────────────────────────────────
|
||
const state = {
|
||
initialized: false,
|
||
tree: null,
|
||
godsData: {},
|
||
hebrewLetterIdByToken: {},
|
||
fourWorldLayers: [],
|
||
showPathLetters: true,
|
||
showPathNumbers: true,
|
||
showPathTarotCards: false,
|
||
selectedSephiraNumber: null,
|
||
selectedPathNumber: null
|
||
};
|
||
|
||
const kabbalahDetailUi = window.KabbalahDetailUi || {};
|
||
const kabbalahViewsUi = window.KabbalahViewsUi || {};
|
||
|
||
if (
|
||
typeof kabbalahViewsUi.renderTree !== "function"
|
||
|| typeof kabbalahViewsUi.renderRoseCross !== "function"
|
||
) {
|
||
throw new Error("KabbalahViewsUi module must load before ui-kabbalah.js");
|
||
}
|
||
|
||
const PLANET_NAME_TO_ID = {
|
||
saturn: "saturn",
|
||
jupiter: "jupiter",
|
||
mars: "mars",
|
||
sol: "sol",
|
||
sun: "sol",
|
||
venus: "venus",
|
||
mercury: "mercury",
|
||
luna: "luna",
|
||
moon: "luna"
|
||
};
|
||
|
||
const ZODIAC_NAME_TO_ID = {
|
||
aries: "aries",
|
||
taurus: "taurus",
|
||
gemini: "gemini",
|
||
cancer: "cancer",
|
||
leo: "leo",
|
||
virgo: "virgo",
|
||
libra: "libra",
|
||
scorpio: "scorpio",
|
||
sagittarius: "sagittarius",
|
||
capricorn: "capricorn",
|
||
aquarius: "aquarius",
|
||
pisces: "pisces"
|
||
};
|
||
|
||
const HEBREW_LETTER_ALIASES = {
|
||
aleph: "alef",
|
||
alef: "alef",
|
||
beth: "bet",
|
||
bet: "bet",
|
||
gimel: "gimel",
|
||
daleth: "dalet",
|
||
dalet: "dalet",
|
||
he: "he",
|
||
vav: "vav",
|
||
zayin: "zayin",
|
||
cheth: "het",
|
||
chet: "het",
|
||
het: "het",
|
||
teth: "tet",
|
||
tet: "tet",
|
||
yod: "yod",
|
||
kaph: "kaf",
|
||
kaf: "kaf",
|
||
lamed: "lamed",
|
||
mem: "mem",
|
||
nun: "nun",
|
||
samekh: "samekh",
|
||
ayin: "ayin",
|
||
pe: "pe",
|
||
tzaddi: "tsadi",
|
||
tzadi: "tsadi",
|
||
tsadi: "tsadi",
|
||
qoph: "qof",
|
||
qof: "qof",
|
||
resh: "resh",
|
||
shin: "shin",
|
||
tav: "tav"
|
||
};
|
||
|
||
const DEFAULT_FOUR_QABALISTIC_WORLD_LAYERS = [
|
||
{
|
||
slot: "Yod",
|
||
letterChar: "י",
|
||
hebrewToken: "yod",
|
||
world: "Atziluth",
|
||
worldLayer: "Archetypal World (God’s Will)",
|
||
worldDescription: "World of gods or specific facets or divine qualities.",
|
||
soulLayer: "Chiah",
|
||
soulTitle: "Life Force",
|
||
soulDescription: "The Chiah is the Life Force itself and our true identity as reflection of Supreme Consciousness."
|
||
},
|
||
{
|
||
slot: "Heh",
|
||
letterChar: "ה",
|
||
hebrewToken: "he",
|
||
world: "Briah",
|
||
worldLayer: "Creative World (God’s Love)",
|
||
worldDescription: "World of archangels, executors of divine qualities.",
|
||
soulLayer: "Neshamah",
|
||
soulTitle: "Soul-Intuition",
|
||
soulDescription: "The Neshamah is the part of our soul that transcends the thinking process."
|
||
},
|
||
{
|
||
slot: "Vav",
|
||
letterChar: "ו",
|
||
hebrewToken: "vav",
|
||
world: "Yetzirah",
|
||
worldLayer: "Formative World (God’s Mind)",
|
||
worldDescription: "World of angels who work under archangelic direction.",
|
||
soulLayer: "Ruach",
|
||
soulTitle: "Intellect",
|
||
soulDescription: "The Ruach is the thinking mind that often dominates attention and identity."
|
||
},
|
||
{
|
||
slot: "Heh (final)",
|
||
letterChar: "ה",
|
||
hebrewToken: "he",
|
||
world: "Assiah",
|
||
worldLayer: "Material World (God’s Creation)",
|
||
worldDescription: "World of spirits that infuse matter and energy through specialized duties.",
|
||
soulLayer: "Nephesh",
|
||
soulTitle: "Animal Soul",
|
||
soulDescription: "The Nephesh is instinctive consciousness expressed through appetite, emotion, sex drive, and survival."
|
||
}
|
||
];
|
||
|
||
function titleCase(value) {
|
||
return String(value || "")
|
||
.split(/[\s-_]+/)
|
||
.filter(Boolean)
|
||
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
||
.join(" ");
|
||
}
|
||
|
||
function normalizeSoulId(value) {
|
||
return String(value || "")
|
||
.trim()
|
||
.toLowerCase()
|
||
.replace(/[^a-z]/g, "");
|
||
}
|
||
|
||
function buildFourWorldLayersFromDataset(magickDataset) {
|
||
const worlds = magickDataset?.grouped?.kabbalah?.fourWorlds;
|
||
const souls = magickDataset?.grouped?.kabbalah?.souls;
|
||
if (!worlds || typeof worlds !== "object") {
|
||
return [...DEFAULT_FOUR_QABALISTIC_WORLD_LAYERS];
|
||
}
|
||
|
||
const worldOrder = ["atzilut", "briah", "yetzirah", "assiah"];
|
||
const soulAliases = {
|
||
chiah: "chaya",
|
||
chaya: "chaya",
|
||
neshamah: "neshama",
|
||
neshama: "neshama",
|
||
ruach: "ruach",
|
||
nephesh: "nephesh"
|
||
};
|
||
|
||
return worldOrder.map((worldId, index) => {
|
||
const fallback = DEFAULT_FOUR_QABALISTIC_WORLD_LAYERS[index] || {};
|
||
const worldEntry = worlds?.[worldId] || null;
|
||
if (!worldEntry || typeof worldEntry !== "object") {
|
||
return fallback;
|
||
}
|
||
|
||
const tetragrammaton = worldEntry?.tetragrammaton && typeof worldEntry.tetragrammaton === "object"
|
||
? worldEntry.tetragrammaton
|
||
: {};
|
||
|
||
const rawSoulId = normalizeSoulId(worldEntry?.soulId);
|
||
const soulId = soulAliases[rawSoulId] || rawSoulId;
|
||
const soulEntry = souls?.[soulId] && typeof souls[soulId] === "object"
|
||
? souls[soulId]
|
||
: null;
|
||
|
||
const soulLayer = soulEntry?.name?.roman || fallback.soulLayer || titleCase(rawSoulId || soulId);
|
||
const soulTitle = soulEntry?.title?.en || fallback.soulTitle || titleCase(soulEntry?.name?.en || "");
|
||
const soulDescription = soulEntry?.desc?.en || fallback.soulDescription || "";
|
||
|
||
return {
|
||
slot: tetragrammaton?.isFinal
|
||
? `${String(tetragrammaton?.slot || fallback.slot || "Heh")} (final)`
|
||
: String(tetragrammaton?.slot || fallback.slot || ""),
|
||
letterChar: String(tetragrammaton?.letterChar || fallback.letterChar || ""),
|
||
hebrewToken: String(tetragrammaton?.hebrewLetterId || fallback.hebrewToken || "").toLowerCase(),
|
||
world: String(worldEntry?.name?.roman || fallback.world || titleCase(worldEntry?.id || worldId)),
|
||
worldLayer: String(worldEntry?.worldLayer?.en || fallback.worldLayer || worldEntry?.desc?.en || ""),
|
||
worldDescription: String(worldEntry?.worldDescription?.en || fallback.worldDescription || ""),
|
||
soulLayer: String(soulLayer || ""),
|
||
soulTitle: String(soulTitle || ""),
|
||
soulDescription: String(soulDescription || "")
|
||
};
|
||
}).filter(Boolean);
|
||
}
|
||
|
||
// ─── element references ─────────────────────────────────────────────────────
|
||
function getElements() {
|
||
return {
|
||
treeContainerEl: document.getElementById("kab-tree-container"),
|
||
detailNameEl: document.getElementById("kab-detail-name"),
|
||
detailSubEl: document.getElementById("kab-detail-sub"),
|
||
detailBodyEl: document.getElementById("kab-detail-body"),
|
||
pathLetterToggleEl: document.getElementById("kab-path-letter-toggle"),
|
||
pathNumberToggleEl: document.getElementById("kab-path-number-toggle"),
|
||
pathTarotToggleEl: document.getElementById("kab-path-tarot-toggle"),
|
||
roseCrossContainerEl: document.getElementById("kab-rose-cross-container"),
|
||
roseDetailNameEl: document.getElementById("kab-rose-detail-name"),
|
||
roseDetailSubEl: document.getElementById("kab-rose-detail-sub"),
|
||
roseDetailBodyEl: document.getElementById("kab-rose-detail-body"),
|
||
};
|
||
}
|
||
|
||
function getRoseDetailElements(elements) {
|
||
if (!elements) {
|
||
return null;
|
||
}
|
||
|
||
return {
|
||
detailNameEl: elements.roseDetailNameEl,
|
||
detailSubEl: elements.roseDetailSubEl,
|
||
detailBodyEl: elements.roseDetailBodyEl
|
||
};
|
||
}
|
||
|
||
function normalizeText(value) {
|
||
return String(value || "").trim().toLowerCase();
|
||
}
|
||
|
||
function normalizeLetterToken(value) {
|
||
return String(value || "")
|
||
.trim()
|
||
.toLowerCase()
|
||
.replace(/[^a-z]/g, "");
|
||
}
|
||
|
||
function buildHebrewLetterLookup(magickDataset) {
|
||
const letters = magickDataset?.grouped?.hebrewLetters;
|
||
const lookup = {};
|
||
|
||
if (!letters || typeof letters !== "object") {
|
||
return lookup;
|
||
}
|
||
|
||
Object.entries(letters).forEach(([letterId, entry]) => {
|
||
const entryId = String(entry?.id || letterId || "");
|
||
|
||
const idToken = normalizeLetterToken(letterId);
|
||
const canonicalIdToken = HEBREW_LETTER_ALIASES[idToken] || idToken;
|
||
if (canonicalIdToken && !lookup[canonicalIdToken]) {
|
||
lookup[canonicalIdToken] = entryId;
|
||
}
|
||
|
||
const nameToken = normalizeLetterToken(entry?.letter?.name);
|
||
const canonicalNameToken = HEBREW_LETTER_ALIASES[nameToken] || nameToken;
|
||
if (canonicalNameToken && !lookup[canonicalNameToken]) {
|
||
lookup[canonicalNameToken] = entryId;
|
||
}
|
||
});
|
||
|
||
return lookup;
|
||
}
|
||
|
||
function resolveHebrewLetterId(value) {
|
||
const token = normalizeLetterToken(value);
|
||
if (!token) return null;
|
||
|
||
const canonical = HEBREW_LETTER_ALIASES[token] || token;
|
||
return state.hebrewLetterIdByToken[canonical] || state.hebrewLetterIdByToken[token] || null;
|
||
}
|
||
|
||
function resolvePlanetId(value) {
|
||
const text = normalizeText(value);
|
||
if (!text) return null;
|
||
|
||
for (const [key, planetId] of Object.entries(PLANET_NAME_TO_ID)) {
|
||
if (text === key || text.includes(key)) {
|
||
return planetId;
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
function resolveZodiacId(value) {
|
||
const text = normalizeText(value);
|
||
if (!text) return null;
|
||
for (const [name, zodiacId] of Object.entries(ZODIAC_NAME_TO_ID)) {
|
||
if (text === name || text.includes(name)) {
|
||
return zodiacId;
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
function findPathByHebrewToken(tree, hebrewToken) {
|
||
const canonicalToken = HEBREW_LETTER_ALIASES[normalizeLetterToken(hebrewToken)] || normalizeLetterToken(hebrewToken);
|
||
if (!canonicalToken) {
|
||
return null;
|
||
}
|
||
|
||
const paths = Array.isArray(tree?.paths) ? tree.paths : [];
|
||
return paths.find((path) => {
|
||
const letterToken = normalizeLetterToken(path?.hebrewLetter?.transliteration || path?.hebrewLetter?.char);
|
||
const canonicalLetterToken = HEBREW_LETTER_ALIASES[letterToken] || letterToken;
|
||
return canonicalLetterToken === canonicalToken;
|
||
}) || null;
|
||
}
|
||
|
||
function getDetailRenderContext(tree, elements, extra = {}) {
|
||
return {
|
||
tree,
|
||
elements,
|
||
godsData: state.godsData,
|
||
fourWorldLayers: state.fourWorldLayers,
|
||
resolvePlanetId,
|
||
resolveZodiacId,
|
||
resolveHebrewLetterId,
|
||
findPathByHebrewToken,
|
||
...extra
|
||
};
|
||
}
|
||
|
||
function clearHighlights() {
|
||
document.querySelectorAll(".kab-node, .kab-node-glow")
|
||
.forEach(el => el.classList.remove("kab-node-active"));
|
||
document.querySelectorAll(".kab-path-hit, .kab-path-line, .kab-path-lbl, .kab-path-tarot, .kab-rose-petal")
|
||
.forEach(el => el.classList.remove("kab-path-active"));
|
||
}
|
||
|
||
function renderSephiraDetail(seph, tree, elements) {
|
||
state.selectedSephiraNumber = Number(seph?.number);
|
||
state.selectedPathNumber = null;
|
||
|
||
clearHighlights();
|
||
document.querySelectorAll(`.kab-node[data-sephira="${seph.number}"], .kab-node-glow[data-sephira="${seph.number}"]`)
|
||
.forEach(el => el.classList.add("kab-node-active"));
|
||
|
||
if (typeof kabbalahDetailUi.renderSephiraDetail === "function") {
|
||
kabbalahDetailUi.renderSephiraDetail(getDetailRenderContext(tree, elements, {
|
||
seph,
|
||
onPathSelect: (path) => renderPathDetail(path, tree, elements)
|
||
}));
|
||
}
|
||
}
|
||
|
||
function renderPathDetail(path, tree, elements) {
|
||
state.selectedPathNumber = Number(path?.pathNumber);
|
||
state.selectedSephiraNumber = null;
|
||
|
||
clearHighlights();
|
||
document.querySelectorAll(`[data-path="${path.pathNumber}"]`)
|
||
.forEach(el => el.classList.add("kab-path-active"));
|
||
|
||
if (typeof kabbalahDetailUi.renderPathDetail === "function") {
|
||
kabbalahDetailUi.renderPathDetail(getDetailRenderContext(tree, elements, {
|
||
path,
|
||
activeHebrewToken: normalizeLetterToken(path?.hebrewLetter?.transliteration || path?.hebrewLetter?.char || "")
|
||
}));
|
||
}
|
||
}
|
||
|
||
|
||
function renderRoseLandingIntro(roseElements) {
|
||
if (typeof kabbalahDetailUi.renderRoseLandingIntro === "function") {
|
||
kabbalahDetailUi.renderRoseLandingIntro(roseElements);
|
||
}
|
||
}
|
||
|
||
function getViewRenderContext(elements) {
|
||
return {
|
||
state,
|
||
tree: state.tree,
|
||
elements,
|
||
getRoseDetailElements,
|
||
renderSephiraDetail,
|
||
renderPathDetail,
|
||
NS,
|
||
R,
|
||
NODE_POS,
|
||
SEPH_FILL,
|
||
DARK_TEXT,
|
||
DAAT,
|
||
PATH_MARKER_SCALE,
|
||
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
|
||
};
|
||
}
|
||
|
||
function renderRoseCurrentSelection(elements) {
|
||
if (!state.tree) {
|
||
return;
|
||
}
|
||
|
||
const roseElements = getRoseDetailElements(elements);
|
||
if (!roseElements?.detailBodyEl) {
|
||
return;
|
||
}
|
||
|
||
if (Number.isFinite(Number(state.selectedPathNumber))) {
|
||
const selectedPath = state.tree.paths.find((entry) => entry.pathNumber === Number(state.selectedPathNumber));
|
||
if (selectedPath) {
|
||
renderPathDetail(selectedPath, state.tree, roseElements);
|
||
return;
|
||
}
|
||
}
|
||
|
||
renderRoseLandingIntro(roseElements);
|
||
}
|
||
|
||
function renderRoseCross(elements) {
|
||
kabbalahViewsUi.renderRoseCross(getViewRenderContext(elements));
|
||
}
|
||
|
||
function renderTree(elements) {
|
||
kabbalahViewsUi.renderTree(getViewRenderContext(elements));
|
||
}
|
||
|
||
function renderCurrentSelection(elements) {
|
||
if (!state.tree) {
|
||
return;
|
||
}
|
||
|
||
if (Number.isFinite(Number(state.selectedPathNumber))) {
|
||
const selectedPath = state.tree.paths.find((entry) => entry.pathNumber === Number(state.selectedPathNumber));
|
||
if (selectedPath) {
|
||
renderPathDetail(selectedPath, state.tree, elements);
|
||
return;
|
||
}
|
||
}
|
||
|
||
if (Number.isFinite(Number(state.selectedSephiraNumber))) {
|
||
const selectedSephira = state.tree.sephiroth.find((entry) => entry.number === Number(state.selectedSephiraNumber));
|
||
if (selectedSephira) {
|
||
renderSephiraDetail(selectedSephira, state.tree, elements);
|
||
return;
|
||
}
|
||
}
|
||
|
||
renderSephiraDetail(state.tree.sephiroth[0], state.tree, elements);
|
||
}
|
||
|
||
// ─── initialise section ──────────────────────────────────────────────────────
|
||
function init(magickDataset, elements) {
|
||
const tree = magickDataset?.grouped?.kabbalah?.["kabbalah-tree"];
|
||
if (!tree) {
|
||
if (elements.detailNameEl) {
|
||
elements.detailNameEl.textContent = "Kabbalah data unavailable";
|
||
elements.detailSubEl.textContent = "Could not load kabbalah-tree.json";
|
||
}
|
||
return;
|
||
}
|
||
state.tree = tree;
|
||
state.godsData = magickDataset?.grouped?.["gods"]?.byPath || {};
|
||
state.hebrewLetterIdByToken = buildHebrewLetterLookup(magickDataset);
|
||
state.fourWorldLayers = buildFourWorldLayersFromDataset(magickDataset);
|
||
|
||
const bindPathDisplayToggle = (toggleEl, stateKey) => {
|
||
if (!toggleEl) {
|
||
return;
|
||
}
|
||
|
||
toggleEl.checked = Boolean(state[stateKey]);
|
||
if (toggleEl.dataset.bound) {
|
||
return;
|
||
}
|
||
|
||
toggleEl.addEventListener("change", () => {
|
||
state[stateKey] = Boolean(toggleEl.checked);
|
||
renderTree(elements);
|
||
renderCurrentSelection(elements);
|
||
});
|
||
|
||
toggleEl.dataset.bound = "true";
|
||
};
|
||
|
||
bindPathDisplayToggle(elements.pathLetterToggleEl, "showPathLetters");
|
||
bindPathDisplayToggle(elements.pathNumberToggleEl, "showPathNumbers");
|
||
bindPathDisplayToggle(elements.pathTarotToggleEl, "showPathTarotCards");
|
||
|
||
renderTree(elements);
|
||
renderCurrentSelection(elements);
|
||
renderRoseCross(elements);
|
||
renderRoseCurrentSelection(elements);
|
||
}
|
||
|
||
function selectPathByNumber(pathNumber) {
|
||
if (!state.initialized || !state.tree) return;
|
||
const el = getElements();
|
||
const path = state.tree.paths.find(p => p.pathNumber === pathNumber);
|
||
if (path) renderPathDetail(path, state.tree, el);
|
||
}
|
||
|
||
function selectSephiraByNumber(n) {
|
||
if (!state.initialized || !state.tree) return;
|
||
const el = getElements();
|
||
const seph = state.tree.sephiroth.find(s => s.number === n);
|
||
if (seph) renderSephiraDetail(seph, state.tree, el);
|
||
}
|
||
|
||
// select sephirah (1-10) or path (11+) by a single number
|
||
function selectNode(n) {
|
||
if (n >= 1 && n <= 10) selectSephiraByNumber(n);
|
||
else selectPathByNumber(n);
|
||
}
|
||
|
||
// ─── public API ────────────────────────────────────────────────────────
|
||
function ensureKabbalahSection(magickDataset) {
|
||
if (state.initialized) return;
|
||
state.initialized = true;
|
||
const elements = getElements();
|
||
init(magickDataset, elements);
|
||
}
|
||
|
||
window.KabbalahSectionUi = { ensureKabbalahSection, selectPathByNumber, selectSephiraByNumber, selectNode };
|
||
})();
|