Files
TaroTime/app/ui-kabbalah.js

577 lines
19 KiB
JavaScript
Raw Normal View History

2026-03-07 01:09:00 -08:00
(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
};
2026-03-07 05:17:50 -08:00
const kabbalahDetailUi = window.KabbalahDetailUi || {};
2026-03-07 13:38:13 -08:00
const kabbalahViewsUi = window.KabbalahViewsUi || {};
if (
typeof kabbalahViewsUi.renderTree !== "function"
|| typeof kabbalahViewsUi.renderRoseCross !== "function"
) {
throw new Error("KabbalahViewsUi module must load before ui-kabbalah.js");
}
2026-03-07 05:17:50 -08:00
2026-03-07 01:09:00 -08:00
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 (Gods 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 (Gods 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 (Gods 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 (Gods 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"),
2026-03-07 05:17:50 -08:00
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
2026-03-07 01:09:00 -08:00
};
}
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;
}
2026-03-07 05:17:50 -08:00
function getDetailRenderContext(tree, elements, extra = {}) {
return {
tree,
elements,
godsData: state.godsData,
fourWorldLayers: state.fourWorldLayers,
resolvePlanetId,
resolveZodiacId,
resolveHebrewLetterId,
findPathByHebrewToken,
...extra
};
2026-03-07 01:09:00 -08:00
}
function clearHighlights() {
document.querySelectorAll(".kab-node, .kab-node-glow")
.forEach(el => el.classList.remove("kab-node-active"));
2026-03-07 05:17:50 -08:00
document.querySelectorAll(".kab-path-hit, .kab-path-line, .kab-path-lbl, .kab-path-tarot, .kab-rose-petal")
2026-03-07 01:09:00 -08:00
.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"));
2026-03-07 05:17:50 -08:00
if (typeof kabbalahDetailUi.renderSephiraDetail === "function") {
kabbalahDetailUi.renderSephiraDetail(getDetailRenderContext(tree, elements, {
seph,
onPathSelect: (path) => renderPathDetail(path, tree, elements)
}));
2026-03-07 01:09:00 -08:00
}
}
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"));
2026-03-07 05:17:50 -08:00
if (typeof kabbalahDetailUi.renderPathDetail === "function") {
kabbalahDetailUi.renderPathDetail(getDetailRenderContext(tree, elements, {
path,
activeHebrewToken: normalizeLetterToken(path?.hebrewLetter?.transliteration || path?.hebrewLetter?.char || "")
}));
2026-03-07 01:09:00 -08:00
}
}
2026-03-07 05:17:50 -08:00
function renderRoseLandingIntro(roseElements) {
if (typeof kabbalahDetailUi.renderRoseLandingIntro === "function") {
kabbalahDetailUi.renderRoseLandingIntro(roseElements);
}
}
2026-03-07 13:38:13 -08:00
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
};
2026-03-07 05:17:50 -08:00
}
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);
}
2026-03-07 13:38:13 -08:00
function renderRoseCross(elements) {
kabbalahViewsUi.renderRoseCross(getViewRenderContext(elements));
}
2026-03-07 01:09:00 -08:00
2026-03-07 13:38:13 -08:00
function renderTree(elements) {
kabbalahViewsUi.renderTree(getViewRenderContext(elements));
2026-03-07 01:09:00 -08:00
}
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);
2026-03-07 05:17:50 -08:00
renderRoseCross(elements);
renderRoseCurrentSelection(elements);
2026-03-07 01:09:00 -08:00
}
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 };
})();