Files
TaroTime/app/ui-kabbalah.js
T

1784 lines
57 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
(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,
selectedWorldLayerIndex: 0,
selectedSephiraNumber: null,
selectedPathNumber: null,
activeNodeKey: "",
exportInProgress: false,
exportFormat: ""
};
let detailNavigator = null;
let browserDetailNavigator = null;
let worldsDetailNavigator = null;
let pathsDetailNavigator = null;
let roseDetailNavigator = null;
const TREE_EXPORT_FORMATS = {
webp: {
mimeType: "image/webp",
extension: "webp",
quality: 0.98
}
};
const TREE_EXPORT_BACKGROUND = "#02030a";
const DAATH_SEPHIRA = Object.freeze({
number: 0,
displayNumber: "Daath",
sephiraId: "daath",
name: "Daath",
nameHebrew: "דעת",
translation: "Knowledge",
planet: "Abyss / Hidden Sephirah",
intelligence: "Invisible Sephirah of Knowledge",
tarot: "No fixed trump attribution",
description: "Daath is the hidden or invisible sephirah placed beneath Kether and between Chokmah and Binah. In Hermetic Qabalah it often marks the threshold of the Abyss rather than a stable emanation like the ten manifest sephiroth."
});
const kabbalahDetailUi = window.KabbalahDetailUi || {};
const kabbalahViewsUi = window.KabbalahViewsUi || {};
let webpExportSupported = null;
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 (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 {
browserSectionEl: document.getElementById("kabbalah-section"),
browserListEl: document.getElementById("kab-browser-list"),
browserCountEl: document.getElementById("kab-browser-count"),
browserDetailNameEl: document.getElementById("kab-browser-detail-name"),
browserDetailSubEl: document.getElementById("kab-browser-detail-sub"),
browserDetailBodyEl: document.getElementById("kab-browser-detail-body"),
browserDetailPrevEl: document.getElementById("kab-browser-detail-prev"),
browserDetailPositionEl: document.getElementById("kab-browser-detail-position"),
browserDetailNextEl: document.getElementById("kab-browser-detail-next"),
worldsSectionEl: document.getElementById("kabbalah-worlds-section"),
worldsListEl: document.getElementById("kab-worlds-list"),
worldsCountEl: document.getElementById("kab-worlds-count"),
worldsDetailNameEl: document.getElementById("kab-worlds-detail-name"),
worldsDetailSubEl: document.getElementById("kab-worlds-detail-sub"),
worldsDetailBodyEl: document.getElementById("kab-worlds-detail-body"),
worldsDetailPrevEl: document.getElementById("kab-worlds-detail-prev"),
worldsDetailPositionEl: document.getElementById("kab-worlds-detail-position"),
worldsDetailNextEl: document.getElementById("kab-worlds-detail-next"),
pathsSectionEl: document.getElementById("kabbalah-paths-section"),
pathsListEl: document.getElementById("kab-paths-list"),
pathsCountEl: document.getElementById("kab-paths-count"),
pathsDetailNameEl: document.getElementById("kab-paths-detail-name"),
pathsDetailSubEl: document.getElementById("kab-paths-detail-sub"),
pathsDetailBodyEl: document.getElementById("kab-paths-detail-body"),
pathsDetailPrevEl: document.getElementById("kab-paths-detail-prev"),
pathsDetailPositionEl: document.getElementById("kab-paths-detail-position"),
pathsDetailNextEl: document.getElementById("kab-paths-detail-next"),
crossSectionEl: document.getElementById("kabbalah-cross-section"),
sectionEl: document.getElementById("kabbalah-tree-section"),
treeContainerEl: document.getElementById("kab-tree-container"),
detailNameEl: document.getElementById("kab-detail-name"),
detailSubEl: document.getElementById("kab-detail-sub"),
detailBodyEl: document.getElementById("kab-detail-body"),
detailPrevEl: document.getElementById("kab-detail-prev"),
detailPositionEl: document.getElementById("kab-detail-position"),
detailNextEl: document.getElementById("kab-detail-next"),
pathLetterToggleEl: document.getElementById("kab-path-letter-toggle"),
pathNumberToggleEl: document.getElementById("kab-path-number-toggle"),
pathTarotToggleEl: document.getElementById("kab-path-tarot-toggle"),
treeExportWebpEl: document.getElementById("kab-tree-export-webp"),
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"),
roseDetailPrevEl: document.getElementById("kab-rose-detail-prev"),
roseDetailPositionEl: document.getElementById("kab-rose-detail-position"),
roseDetailNextEl: document.getElementById("kab-rose-detail-next"),
};
}
function getTreeDetailElements(elements) {
if (!elements) {
return null;
}
return {
detailNameEl: elements.detailNameEl,
detailSubEl: elements.detailSubEl,
detailBodyEl: elements.detailBodyEl
};
}
function getRoseDetailElements(elements) {
if (!elements) {
return null;
}
return {
detailNameEl: elements.roseDetailNameEl,
detailSubEl: elements.roseDetailSubEl,
detailBodyEl: elements.roseDetailBodyEl
};
}
function getPathDetailElements(elements) {
if (!elements) {
return null;
}
return {
detailNameEl: elements.pathsDetailNameEl,
detailSubEl: elements.pathsDetailSubEl,
detailBodyEl: elements.pathsDetailBodyEl
};
}
function getBrowserDetailElements(elements) {
if (!elements) {
return null;
}
return {
detailNameEl: elements.browserDetailNameEl,
detailSubEl: elements.browserDetailSubEl,
detailBodyEl: elements.browserDetailBodyEl
};
}
function getWorldDetailElements(elements) {
if (!elements) {
return null;
}
return {
detailNameEl: elements.worldsDetailNameEl,
detailSubEl: elements.worldsDetailSubEl,
detailBodyEl: elements.worldsDetailBodyEl
};
}
function normalizeDetailElements(elements) {
if (!elements) {
return null;
}
return {
detailNameEl: elements.detailNameEl || null,
detailSubEl: elements.detailSubEl || null,
detailBodyEl: elements.detailBodyEl || null
};
}
function getDetailRenderTargets(primaryElements) {
const elements = getElements();
const candidates = [
normalizeDetailElements(primaryElements),
getTreeDetailElements(elements),
getBrowserDetailElements(elements)
];
const seen = new Set();
return candidates.filter((target) => {
const bodyEl = target?.detailBodyEl;
if (!(bodyEl instanceof Element) || seen.has(bodyEl)) {
return false;
}
seen.add(bodyEl);
return true;
});
}
function hasFiniteSelectionNumber(value) {
if (value === null || value === undefined || value === "") {
return false;
}
return Number.isFinite(Number(value));
}
function isDaathToken(value) {
return String(value || "").trim().toLowerCase() === "daath";
}
function buildSephiraKey(value) {
if (isDaathToken(value) || Number(value) === 0) {
return "sephira:daath";
}
return hasFiniteSelectionNumber(value) ? `sephira:${Number(value)}` : "";
}
function buildPathKey(value) {
return hasFiniteSelectionNumber(value) ? `path:${Number(value)}` : "";
}
function buildWorldKey(value) {
return hasFiniteSelectionNumber(value) ? `world:${Number(value)}` : "";
}
function getSelectedSephiraKey() {
return buildSephiraKey(state.selectedSephiraNumber);
}
function getSelectedPathKey() {
return buildPathKey(state.selectedPathNumber);
}
function getSelectedWorldKey() {
return buildWorldKey(state.selectedWorldLayerIndex);
}
function getSephiraByNumber(number) {
if (isDaathToken(number) || Number(number) === 0) {
return DAATH_SEPHIRA;
}
if (!state.tree) {
return null;
}
return state.tree.sephiroth.find((entry) => Number(entry?.number) === Number(number)) || null;
}
function getPathByNumber(number) {
if (!state.tree) {
return null;
}
return state.tree.paths.find((entry) => Number(entry?.pathNumber) === Number(number)) || null;
}
function getWorldLayerByIndex(index) {
if (!Array.isArray(state.fourWorldLayers)) {
return null;
}
const targetIndex = Number(index);
return Number.isInteger(targetIndex) && targetIndex >= 0 && targetIndex < state.fourWorldLayers.length
? state.fourWorldLayers[targetIndex]
: null;
}
function getSephirotSequenceEntries() {
if (!state.tree) {
return [];
}
const entries = Array.isArray(state.tree.sephiroth)
? [...state.tree.sephiroth]
.sort((left, right) => Number(left?.number || 0) - Number(right?.number || 0))
.map((entry) => ({
key: buildSephiraKey(entry?.number),
type: "sephira",
number: Number(entry?.number)
}))
: [];
const daathEntry = {
key: buildSephiraKey(0),
type: "sephira",
number: 0
};
const insertIndex = entries.findIndex((entry) => entry.number === 3);
if (insertIndex >= 0) {
entries.splice(insertIndex + 1, 0, daathEntry);
} else {
entries.push(daathEntry);
}
return entries;
}
function getPathSequenceEntries() {
if (!state.tree) {
return [];
}
return Array.isArray(state.tree.paths)
? [...state.tree.paths]
.sort((left, right) => Number(left?.pathNumber || 0) - Number(right?.pathNumber || 0))
.map((entry) => ({
key: buildPathKey(entry?.pathNumber),
type: "path",
number: Number(entry?.pathNumber)
}))
: [];
}
function getWorldSequenceEntries() {
return Array.isArray(state.fourWorldLayers)
? state.fourWorldLayers.map((layer, index) => ({
key: buildWorldKey(index),
type: "world",
index,
world: String(layer?.world || "")
}))
: [];
}
function getNodeSequenceEntries() {
if (!state.tree) {
return [];
}
const sephiroth = Array.isArray(state.tree.sephiroth)
? [...state.tree.sephiroth]
.sort((left, right) => Number(left?.number || 0) - Number(right?.number || 0))
.map((entry) => ({
key: buildSephiraKey(entry?.number),
type: "sephira",
number: Number(entry?.number)
}))
: [];
const paths = getPathSequenceEntries();
return [...sephiroth, ...paths];
}
function getSelectedNodeKey() {
return String(state.activeNodeKey || "").trim() || getSelectedPathKey() || getSelectedSephiraKey();
}
function buildSequenceState(entries, currentKey) {
const currentIndex = entries.findIndex((entry) => entry.key === currentKey);
return {
total: entries.length,
currentIndex,
previousKey: currentIndex > 0 ? entries[currentIndex - 1].key : "",
nextKey: currentIndex >= 0 && currentIndex < entries.length - 1 ? entries[currentIndex + 1].key : ""
};
}
function getNodeSequenceState() {
return buildSequenceState(getNodeSequenceEntries(), getSelectedNodeKey());
}
function getSephirotSequenceState() {
return buildSequenceState(getSephirotSequenceEntries(), getSelectedSephiraKey());
}
function getPathSequenceState() {
return buildSequenceState(getPathSequenceEntries(), getSelectedPathKey());
}
function getWorldSequenceState() {
return buildSequenceState(getWorldSequenceEntries(), getSelectedWorldKey());
}
function getBrowserListItemMeta(entry) {
const seph = getSephiraByNumber(entry.number);
const displayNumber = String(seph?.displayNumber || entry.number || "").trim();
return {
title: displayNumber ? `${displayNumber} · ${seph?.name || "Sephirah"}` : `${seph?.name || "Sephirah"}`,
meta: [seph?.nameHebrew, seph?.translation, seph?.planet].filter(Boolean).join(" · ") || "Sephirah"
};
}
function getWorldListItemMeta(entry) {
const layer = getWorldLayerByIndex(entry.index);
return {
title: String(layer?.world || `World ${entry.index + 1}`),
meta: [
layer?.slot ? `${layer.slot}: ${layer.letterChar || ""}`.trim() : "",
layer?.soulLayer
].filter(Boolean).join(" · ") || "Qabalistic World"
};
}
function getPathListItemMeta(entry) {
const path = getPathByNumber(entry.number);
const fromName = getSephiraByNumber(path?.connects?.from)?.name || `Node ${path?.connects?.from || "?"}`;
const toName = getSephiraByNumber(path?.connects?.to)?.name || `Node ${path?.connects?.to || "?"}`;
const letterLabel = [path?.hebrewLetter?.char, path?.hebrewLetter?.transliteration].filter(Boolean).join(" ").trim();
return {
title: `Path ${entry.number}${letterLabel ? ` · ${letterLabel}` : ""}`,
meta: [
`${fromName} -> ${toName}`,
String(path?.tarot?.card || "").trim()
].filter(Boolean).join(" · ") || "Path"
};
}
function syncBrowserListSelection(elements = getElements()) {
if (!elements?.browserListEl) {
return;
}
const selectedKey = getSelectedSephiraKey();
elements.browserListEl.querySelectorAll(".planet-list-item[data-node-key]").forEach((button) => {
const isSelected = button.dataset.nodeKey === selectedKey;
button.classList.toggle("is-selected", isSelected);
button.setAttribute("aria-selected", isSelected ? "true" : "false");
});
}
function renderBrowserList(elements = getElements()) {
if (!elements?.browserListEl) {
return;
}
const entries = getSephirotSequenceEntries();
elements.browserListEl.innerHTML = "";
entries.forEach((entry) => {
const button = document.createElement("button");
const { title, meta } = getBrowserListItemMeta(entry);
button.type = "button";
button.className = "planet-list-item";
button.setAttribute("role", "option");
button.dataset.nodeKey = entry.key;
button.innerHTML = `
<div class="planet-list-name">${title}</div>
<div class="planet-list-meta">${meta}</div>
`;
elements.browserListEl.appendChild(button);
});
if (elements.browserCountEl) {
elements.browserCountEl.textContent = `${entries.length} sephiroth`;
}
syncBrowserListSelection(elements);
}
function syncWorldListSelection(elements = getElements()) {
if (!elements?.worldsListEl) {
return;
}
const selectedKey = getSelectedWorldKey();
elements.worldsListEl.querySelectorAll(".planet-list-item[data-world-key]").forEach((button) => {
const isSelected = button.dataset.worldKey === selectedKey;
button.classList.toggle("is-selected", isSelected);
button.setAttribute("aria-selected", isSelected ? "true" : "false");
});
}
function syncPathsListSelection(elements = getElements()) {
if (!elements?.pathsListEl) {
return;
}
const selectedKey = getSelectedPathKey();
elements.pathsListEl.querySelectorAll(".planet-list-item[data-path-key]").forEach((button) => {
const isSelected = button.dataset.pathKey === selectedKey;
button.classList.toggle("is-selected", isSelected);
button.setAttribute("aria-selected", isSelected ? "true" : "false");
});
}
function renderWorldsList(elements = getElements()) {
if (!elements?.worldsListEl) {
return;
}
const entries = getWorldSequenceEntries();
elements.worldsListEl.innerHTML = "";
entries.forEach((entry) => {
const button = document.createElement("button");
const { title, meta } = getWorldListItemMeta(entry);
button.type = "button";
button.className = "planet-list-item";
button.setAttribute("role", "option");
button.dataset.worldKey = entry.key;
button.innerHTML = `
<div class="planet-list-name">${title}</div>
<div class="planet-list-meta">${meta}</div>
`;
elements.worldsListEl.appendChild(button);
});
if (elements.worldsCountEl) {
elements.worldsCountEl.textContent = `${entries.length} worlds`;
}
syncWorldListSelection(elements);
}
function renderPathsList(elements = getElements()) {
if (!elements?.pathsListEl) {
return;
}
const entries = getPathSequenceEntries();
elements.pathsListEl.innerHTML = "";
entries.forEach((entry) => {
const button = document.createElement("button");
const { title, meta } = getPathListItemMeta(entry);
button.type = "button";
button.className = "planet-list-item";
button.setAttribute("role", "option");
button.dataset.pathKey = entry.key;
button.innerHTML = `
<div class="planet-list-name">${title}</div>
<div class="planet-list-meta">${meta}</div>
`;
elements.pathsListEl.appendChild(button);
});
if (elements.pathsCountEl) {
elements.pathsCountEl.textContent = `${entries.length} paths`;
}
syncPathsListSelection(elements);
}
function bindBrowserList(elements = getElements()) {
if (!elements?.browserListEl || elements.browserListEl.dataset.bound) {
return;
}
elements.browserListEl.addEventListener("click", (event) => {
const target = event.target instanceof Element
? event.target.closest(".planet-list-item[data-node-key]")
: null;
if (!(target instanceof HTMLButtonElement)) {
return;
}
const targetKey = String(target.dataset.nodeKey || "").trim();
if (!targetKey) {
return;
}
selectNodeBySequenceKey(targetKey, getBrowserDetailElements(getElements()));
});
elements.browserListEl.dataset.bound = "true";
}
function bindWorldList(elements = getElements()) {
if (!elements?.worldsListEl || elements.worldsListEl.dataset.bound) {
return;
}
elements.worldsListEl.addEventListener("click", (event) => {
const target = event.target instanceof Element
? event.target.closest(".planet-list-item[data-world-key]")
: null;
if (!(target instanceof HTMLButtonElement)) {
return;
}
const targetKey = String(target.dataset.worldKey || "").trim();
if (!targetKey) {
return;
}
selectNodeBySequenceKey(targetKey, getWorldDetailElements(getElements()));
});
elements.worldsListEl.dataset.bound = "true";
}
function bindPathsList(elements = getElements()) {
if (!elements?.pathsListEl || elements.pathsListEl.dataset.bound) {
return;
}
elements.pathsListEl.addEventListener("click", (event) => {
const target = event.target instanceof Element
? event.target.closest(".planet-list-item[data-path-key]")
: null;
if (!(target instanceof HTMLButtonElement)) {
return;
}
const targetKey = String(target.dataset.pathKey || "").trim();
if (!targetKey) {
return;
}
selectNodeBySequenceKey(targetKey, getPathDetailElements(getElements()));
});
elements.pathsListEl.dataset.bound = "true";
}
function selectNodeBySequenceKey(targetKey, elements = getElements()) {
if (!state.tree) {
return false;
}
const [type, rawToken] = String(targetKey || "").split(":");
if (type === "sephira") {
const seph = getSephiraByNumber(isDaathToken(rawToken) ? 0 : Number(rawToken));
if (!seph) {
return false;
}
renderSephiraDetail(seph, state.tree, elements);
return true;
}
if (type === "path") {
const path = getPathByNumber(Number(rawToken));
if (!path) {
return false;
}
renderPathDetail(path, state.tree, elements);
return true;
}
if (type === "world") {
const worldLayer = getWorldLayerByIndex(Number(rawToken));
if (!worldLayer) {
return false;
}
renderWorldLayerDetail(worldLayer, Number(rawToken), state.tree, elements);
return true;
}
return false;
}
function getDetailNavigator() {
if (detailNavigator || typeof window.TarotSequenceNav?.createSequenceNavigator !== "function") {
return detailNavigator;
}
detailNavigator = window.TarotSequenceNav.createSequenceNavigator({
getElements,
isActive: (elements) => Boolean(elements?.sectionEl && elements.sectionEl.hidden === false),
getSequenceState: getNodeSequenceState,
getPrevButton: (elements) => elements?.detailPrevEl,
getNextButton: (elements) => elements?.detailNextEl,
getPositionEl: (elements) => elements?.detailPositionEl,
formatPositionText: ({ total, currentIndex }) => {
if (total > 0 && currentIndex >= 0) {
return `${currentIndex + 1} of ${total} nodes`;
}
return total > 0 ? `${total} nodes` : "No nodes";
},
selectTarget: (targetKey, elements) => selectNodeBySequenceKey(targetKey, elements)
});
return detailNavigator;
}
function getBrowserDetailNavigator() {
if (browserDetailNavigator || typeof window.TarotSequenceNav?.createSequenceNavigator !== "function") {
return browserDetailNavigator;
}
browserDetailNavigator = window.TarotSequenceNav.createSequenceNavigator({
getElements,
isActive: (elements) => Boolean(elements?.browserSectionEl && elements.browserSectionEl.hidden === false),
getSequenceState: getSephirotSequenceState,
getPrevButton: (elements) => elements?.browserDetailPrevEl,
getNextButton: (elements) => elements?.browserDetailNextEl,
getPositionEl: (elements) => elements?.browserDetailPositionEl,
formatPositionText: ({ total, currentIndex }) => {
if (total > 0 && currentIndex >= 0) {
return `${currentIndex + 1} of ${total} sephiroth`;
}
return total > 0 ? `${total} sephiroth` : "No sephiroth";
},
selectTarget: (targetKey) => selectNodeBySequenceKey(targetKey, getBrowserDetailElements(getElements()))
});
return browserDetailNavigator;
}
function getPathsDetailNavigator() {
if (pathsDetailNavigator || typeof window.TarotSequenceNav?.createSequenceNavigator !== "function") {
return pathsDetailNavigator;
}
pathsDetailNavigator = window.TarotSequenceNav.createSequenceNavigator({
getElements,
isActive: (elements) => Boolean(elements?.pathsSectionEl && elements.pathsSectionEl.hidden === false),
getSequenceState: getPathSequenceState,
getPrevButton: (elements) => elements?.pathsDetailPrevEl,
getNextButton: (elements) => elements?.pathsDetailNextEl,
getPositionEl: (elements) => elements?.pathsDetailPositionEl,
formatPositionText: ({ total, currentIndex }) => {
if (total > 0 && currentIndex >= 0) {
return `${currentIndex + 1} of ${total} paths`;
}
return total > 0 ? `${total} paths` : "No paths";
},
selectTarget: (targetKey) => selectNodeBySequenceKey(targetKey, getPathDetailElements(getElements()))
});
return pathsDetailNavigator;
}
function getRoseDetailNavigator() {
if (roseDetailNavigator || typeof window.TarotSequenceNav?.createSequenceNavigator !== "function") {
return roseDetailNavigator;
}
roseDetailNavigator = window.TarotSequenceNav.createSequenceNavigator({
getElements,
isActive: (elements) => Boolean(elements?.crossSectionEl && elements.crossSectionEl.hidden === false),
getSequenceState: getPathSequenceState,
getPrevButton: (elements) => elements?.roseDetailPrevEl,
getNextButton: (elements) => elements?.roseDetailNextEl,
getPositionEl: (elements) => elements?.roseDetailPositionEl,
formatPositionText: ({ total, currentIndex }) => {
if (total > 0 && currentIndex >= 0) {
return `${currentIndex + 1} of ${total} paths`;
}
return total > 0 ? `${total} paths` : "No paths";
},
selectTarget: (targetKey) => selectNodeBySequenceKey(targetKey, getRoseDetailElements(getElements()))
});
return roseDetailNavigator;
}
function getWorldDetailNavigator() {
if (worldsDetailNavigator || typeof window.TarotSequenceNav?.createSequenceNavigator !== "function") {
return worldsDetailNavigator;
}
worldsDetailNavigator = window.TarotSequenceNav.createSequenceNavigator({
getElements,
isActive: (elements) => Boolean(elements?.worldsSectionEl && elements.worldsSectionEl.hidden === false),
getSequenceState: getWorldSequenceState,
getPrevButton: (elements) => elements?.worldsDetailPrevEl,
getNextButton: (elements) => elements?.worldsDetailNextEl,
getPositionEl: (elements) => elements?.worldsDetailPositionEl,
formatPositionText: ({ total, currentIndex }) => {
if (total > 0 && currentIndex >= 0) {
return `${currentIndex + 1} of ${total} worlds`;
}
return total > 0 ? `${total} worlds` : "No worlds";
},
selectTarget: (targetKey) => selectNodeBySequenceKey(targetKey, getWorldDetailElements(getElements()))
});
return worldsDetailNavigator;
}
function syncDetailNavigation(elements = getElements()) {
getDetailNavigator()?.sync(elements);
}
function syncBrowserDetailNavigation(elements = getElements()) {
getBrowserDetailNavigator()?.sync(elements);
}
function syncPathsDetailNavigation(elements = getElements()) {
getPathsDetailNavigator()?.sync(elements);
}
function syncRoseDetailNavigation(elements = getElements()) {
getRoseDetailNavigator()?.sync(elements);
}
function syncWorldDetailNavigation(elements = getElements()) {
getWorldDetailNavigator()?.sync(elements);
}
function bindDetailNavigation(elements = getElements()) {
getDetailNavigator()?.bind(elements);
}
function bindBrowserDetailNavigation(elements = getElements()) {
getBrowserDetailNavigator()?.bind(elements);
}
function bindPathsDetailNavigation(elements = getElements()) {
getPathsDetailNavigator()?.bind(elements);
}
function bindRoseDetailNavigation(elements = getElements()) {
getRoseDetailNavigator()?.bind(elements);
}
function bindWorldDetailNavigation(elements = getElements()) {
getWorldDetailNavigator()?.bind(elements);
}
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 renderSephiraDetailIntoElements(seph, tree, elements, options = {}) {
if (typeof kabbalahDetailUi.renderSephiraDetail === "function") {
kabbalahDetailUi.renderSephiraDetail(getDetailRenderContext(tree, elements, {
seph,
onPathSelect: typeof options.onPathSelect === "function"
? options.onPathSelect
: null
}));
}
}
function renderPathDetailIntoElements(path, tree, elements) {
if (typeof kabbalahDetailUi.renderPathDetail === "function") {
kabbalahDetailUi.renderPathDetail(getDetailRenderContext(tree, elements, {
path,
activeHebrewToken: normalizeLetterToken(path?.hebrewLetter?.transliteration || path?.hebrewLetter?.char || "")
}));
}
}
function renderWorldLayerDetailIntoElements(worldLayer, tree, elements, worldIndex) {
if (typeof kabbalahDetailUi.renderWorldLayerDetail === "function") {
kabbalahDetailUi.renderWorldLayerDetail(getDetailRenderContext(tree, elements, {
worldLayer,
worldIndex
}));
}
}
function renderSephiraDetail(seph, tree, elements) {
state.selectedSephiraNumber = Number(seph?.number);
if (buildSephiraKey(seph?.number) !== "sephira:daath") {
state.activeNodeKey = buildSephiraKey(seph?.number);
}
clearHighlights();
document.querySelectorAll(`.kab-node[data-sephira="${seph.number}"], .kab-node-glow[data-sephira="${seph.number}"]`)
.forEach(el => el.classList.add("kab-node-active"));
const allElements = getElements();
const treeElements = getTreeDetailElements(allElements);
const browserElements = getBrowserDetailElements(allElements);
if (treeElements?.detailBodyEl) {
renderSephiraDetailIntoElements(seph, tree, treeElements, {
onPathSelect: (path) => renderPathDetail(path, tree, treeElements)
});
}
if (browserElements?.detailBodyEl) {
renderSephiraDetailIntoElements(seph, tree, browserElements, {
onPathSelect: (path) => {
document.dispatchEvent(new CustomEvent("nav:kabbalah-path", {
detail: { pathNo: Number(path?.pathNumber) }
}));
}
});
}
syncDetailNavigation();
syncBrowserDetailNavigation();
syncBrowserListSelection();
}
function renderPathDetail(path, tree, elements) {
state.selectedPathNumber = Number(path?.pathNumber);
state.activeNodeKey = buildPathKey(path?.pathNumber);
clearHighlights();
document.querySelectorAll(`[data-path="${path.pathNumber}"]`)
.forEach(el => el.classList.add("kab-path-active"));
const allElements = getElements();
const treeElements = getTreeDetailElements(allElements);
const pathElements = getPathDetailElements(allElements);
const roseElements = getRoseDetailElements(allElements);
if (treeElements?.detailBodyEl) {
renderPathDetailIntoElements(path, tree, treeElements);
}
if (pathElements?.detailBodyEl) {
renderPathDetailIntoElements(path, tree, pathElements);
}
if (roseElements?.detailBodyEl) {
renderPathDetailIntoElements(path, tree, roseElements);
}
syncDetailNavigation();
syncPathsDetailNavigation();
syncRoseDetailNavigation();
syncPathsListSelection();
syncBrowserListSelection();
}
function renderWorldLayerDetail(worldLayer, worldIndex, tree, elements) {
state.selectedWorldLayerIndex = Number(worldIndex);
const worldElements = getWorldDetailElements(getElements());
if (worldElements?.detailBodyEl) {
renderWorldLayerDetailIntoElements(worldLayer, tree, worldElements, worldIndex);
}
syncWorldDetailNavigation();
syncWorldListSelection();
}
function renderRoseLandingIntro(roseElements) {
if (typeof kabbalahDetailUi.renderRoseLandingIntro === "function") {
kabbalahDetailUi.renderRoseLandingIntro(roseElements);
}
}
function renderBrowserCurrentSelection(elements) {
if (!state.tree) {
return;
}
const browserElements = getBrowserDetailElements(elements);
if (!browserElements?.detailBodyEl) {
return;
}
const selectedSephira = getSephiraByNumber(state.selectedSephiraNumber);
if (selectedSephira) {
renderSephiraDetail(selectedSephira, state.tree, browserElements);
return;
}
const fallbackSephira = getSephiraByNumber(getSephirotSequenceEntries()[0]?.number);
if (fallbackSephira) {
renderSephiraDetail(fallbackSephira, state.tree, browserElements);
}
}
function renderPathsCurrentSelection(elements) {
if (!state.tree) {
return;
}
const pathElements = getPathDetailElements(elements);
if (!pathElements?.detailBodyEl) {
return;
}
if (hasFiniteSelectionNumber(state.selectedPathNumber)) {
const selectedPath = getPathByNumber(state.selectedPathNumber);
if (selectedPath) {
renderPathDetail(selectedPath, state.tree, pathElements);
return;
}
}
const fallbackPath = getPathByNumber(getPathSequenceEntries()[0]?.number);
if (fallbackPath) {
renderPathDetail(fallbackPath, state.tree, pathElements);
}
}
function renderWorldCurrentSelection(elements) {
const worldElements = getWorldDetailElements(elements);
if (!worldElements?.detailBodyEl) {
return;
}
const selectedWorld = getWorldLayerByIndex(state.selectedWorldLayerIndex);
if (selectedWorld) {
renderWorldLayerDetail(selectedWorld, state.selectedWorldLayerIndex, state.tree, worldElements);
return;
}
const fallbackWorld = getWorldLayerByIndex(0);
if (fallbackWorld) {
renderWorldLayerDetail(fallbackWorld, 0, state.tree, worldElements);
}
}
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 (hasFiniteSelectionNumber(state.selectedPathNumber)) {
const selectedPath = getPathByNumber(state.selectedPathNumber);
if (selectedPath) {
renderPathDetail(selectedPath, state.tree, roseElements);
return;
}
}
renderRoseLandingIntro(roseElements);
syncRoseDetailNavigation(elements);
}
function renderRoseCross(elements) {
kabbalahViewsUi.renderRoseCross(getViewRenderContext(elements));
}
function renderTree(elements) {
kabbalahViewsUi.renderTree(getViewRenderContext(elements));
}
function isExportFormatSupported(format) {
const exportFormat = TREE_EXPORT_FORMATS[format];
if (!exportFormat) {
return false;
}
if (format === "webp" && typeof webpExportSupported === "boolean") {
return webpExportSupported;
}
const probeCanvas = document.createElement("canvas");
const dataUrl = probeCanvas.toDataURL(exportFormat.mimeType);
const isSupported = dataUrl.startsWith(`data:${exportFormat.mimeType}`);
if (format === "webp") {
webpExportSupported = isSupported;
}
return isSupported;
}
function syncExportControls(elements) {
if (!(elements?.treeExportWebpEl instanceof HTMLButtonElement)) {
return;
}
const supportsWebp = isExportFormatSupported("webp");
elements.treeExportWebpEl.hidden = !supportsWebp;
elements.treeExportWebpEl.disabled = Boolean(state.exportInProgress) || !supportsWebp;
elements.treeExportWebpEl.textContent = state.exportInProgress && state.exportFormat === "webp"
? "Exporting..."
: "Export WebP";
if (supportsWebp) {
elements.treeExportWebpEl.title = "Download the current Tree of Life view as a WebP image.";
}
}
function copyComputedStyles(sourceEl, targetEl) {
if (!(sourceEl instanceof Element) || !(targetEl instanceof Element)) {
return;
}
const computedStyle = window.getComputedStyle(sourceEl);
Array.from(computedStyle).forEach((propertyName) => {
targetEl.style.setProperty(
propertyName,
computedStyle.getPropertyValue(propertyName),
computedStyle.getPropertyPriority(propertyName)
);
});
targetEl.style.setProperty("animation", "none");
targetEl.style.setProperty("transition", "none");
}
function inlineSvgStyles(sourceNode, targetNode) {
if (!(sourceNode instanceof Element) || !(targetNode instanceof Element)) {
return;
}
copyComputedStyles(sourceNode, targetNode);
const sourceChildren = Array.from(sourceNode.children);
const targetChildren = Array.from(targetNode.children);
const childCount = Math.min(sourceChildren.length, targetChildren.length);
for (let index = 0; index < childCount; index += 1) {
inlineSvgStyles(sourceChildren[index], targetChildren[index]);
}
}
function absolutizeSvgImageLinks(svgEl) {
if (!(svgEl instanceof SVGSVGElement)) {
return;
}
svgEl.querySelectorAll("image").forEach((imageEl) => {
const href = imageEl.getAttribute("href")
|| imageEl.getAttributeNS("http://www.w3.org/1999/xlink", "href");
if (!href) {
return;
}
const absoluteHref = new URL(href, document.baseURI).href;
imageEl.setAttribute("href", absoluteHref);
imageEl.setAttributeNS("http://www.w3.org/1999/xlink", "href", absoluteHref);
});
}
function prepareSvgMarkupForExport(svgEl) {
if (!(svgEl instanceof SVGSVGElement)) {
throw new Error("Tree view is not ready to export yet.");
}
const bounds = svgEl.getBoundingClientRect();
const viewBox = svgEl.viewBox?.baseVal || null;
const width = Math.max(
240,
Math.round(bounds.width),
Number.isFinite(viewBox?.width) ? Math.round(viewBox.width) : 0
);
const height = Math.max(
470,
Math.round(bounds.height),
Number.isFinite(viewBox?.height) ? Math.round(viewBox.height) : 0
);
const clone = svgEl.cloneNode(true);
if (!(clone instanceof SVGSVGElement)) {
throw new Error("Tree export could not clone the current SVG view.");
}
clone.setAttribute("xmlns", "http://www.w3.org/2000/svg");
clone.setAttribute("xmlns:xlink", "http://www.w3.org/1999/xlink");
clone.setAttribute("width", String(width));
clone.setAttribute("height", String(height));
clone.setAttribute("preserveAspectRatio", "xMidYMid meet");
inlineSvgStyles(svgEl, clone);
absolutizeSvgImageLinks(clone);
const backgroundRect = document.createElementNS(NS, "rect");
backgroundRect.setAttribute("x", "0");
backgroundRect.setAttribute("y", "0");
backgroundRect.setAttribute("width", "100%");
backgroundRect.setAttribute("height", "100%");
backgroundRect.setAttribute("fill", TREE_EXPORT_BACKGROUND);
backgroundRect.setAttribute("pointer-events", "none");
clone.insertBefore(backgroundRect, clone.firstChild);
return {
width,
height,
markup: new XMLSerializer().serializeToString(clone)
};
}
function loadSvgImage(markup) {
return new Promise((resolve, reject) => {
const svgBlob = new Blob([markup], { type: "image/svg+xml;charset=utf-8" });
const svgUrl = URL.createObjectURL(svgBlob);
const image = new Image();
image.decoding = "async";
image.onload = () => {
URL.revokeObjectURL(svgUrl);
resolve(image);
};
image.onerror = () => {
URL.revokeObjectURL(svgUrl);
reject(new Error("Tree export renderer could not load the current SVG view."));
};
image.src = svgUrl;
});
}
function canvasToBlobByFormat(canvas, format) {
const exportFormat = TREE_EXPORT_FORMATS[format];
if (!exportFormat) {
return Promise.reject(new Error("Unsupported export format."));
}
return new Promise((resolve, reject) => {
canvas.toBlob((blob) => {
if (blob) {
resolve(blob);
return;
}
reject(new Error("Canvas export failed."));
}, exportFormat.mimeType, exportFormat.quality);
});
}
async function exportTreeView(format = "webp") {
const exportFormat = TREE_EXPORT_FORMATS[format];
if (!exportFormat || state.exportInProgress) {
return;
}
const elements = getElements();
const svgEl = elements.treeContainerEl?.querySelector("svg.kab-svg");
if (!(svgEl instanceof SVGSVGElement)) {
window.alert("Tree view is not ready to export yet.");
return;
}
state.exportInProgress = true;
state.exportFormat = format;
syncExportControls(elements);
try {
const { width, height, markup } = prepareSvgMarkupForExport(svgEl);
const image = await loadSvgImage(markup);
const scale = Math.max(2, Math.min(4, Number(window.devicePixelRatio) || 1));
const canvas = document.createElement("canvas");
canvas.width = Math.max(1, Math.ceil(width * scale));
canvas.height = Math.max(1, Math.ceil(height * scale));
const context = canvas.getContext("2d");
if (!context) {
throw new Error("Canvas context is unavailable.");
}
context.scale(scale, scale);
context.imageSmoothingEnabled = true;
context.imageSmoothingQuality = "high";
context.fillStyle = TREE_EXPORT_BACKGROUND;
context.fillRect(0, 0, width, height);
context.drawImage(image, 0, 0, width, height);
const blob = await canvasToBlobByFormat(canvas, format);
const blobUrl = URL.createObjectURL(blob);
const downloadLink = document.createElement("a");
const stamp = new Date().toISOString().slice(0, 10);
downloadLink.href = blobUrl;
downloadLink.download = `tree-of-life-${stamp}.${exportFormat.extension}`;
document.body.appendChild(downloadLink);
downloadLink.click();
downloadLink.remove();
setTimeout(() => URL.revokeObjectURL(blobUrl), 1000);
} catch (error) {
window.alert(error instanceof Error ? error.message : "Unable to export the current Tree of Life view.");
} finally {
state.exportInProgress = false;
state.exportFormat = "";
syncExportControls(getElements());
}
}
function renderCurrentSelection(elements) {
if (!state.tree) {
return;
}
const activeNodeKey = String(state.activeNodeKey || "").trim();
if (activeNodeKey.startsWith("path:")) {
const selectedPath = getPathByNumber(Number(activeNodeKey.split(":")[1]));
if (selectedPath) {
renderPathDetail(selectedPath, state.tree, elements);
return;
}
}
if (activeNodeKey.startsWith("sephira:") && !activeNodeKey.endsWith(":daath")) {
const selectedSephira = getSephiraByNumber(Number(activeNodeKey.split(":")[1]));
if (selectedSephira) {
renderSephiraDetail(selectedSephira, state.tree, elements);
return;
}
}
renderSephiraDetail(state.tree.sephiroth[0], state.tree, elements);
}
function renderVisibleKabbalahViews(elements = getElements()) {
renderBrowserList(elements);
renderWorldsList(elements);
renderPathsList(elements);
if (elements.browserSectionEl && elements.browserSectionEl.hidden === false) {
renderBrowserCurrentSelection(elements);
} else {
syncBrowserListSelection(elements);
syncBrowserDetailNavigation(elements);
}
if (elements.worldsSectionEl && elements.worldsSectionEl.hidden === false) {
renderWorldCurrentSelection(elements);
} else {
syncWorldListSelection(elements);
syncWorldDetailNavigation(elements);
}
if (elements.pathsSectionEl && elements.pathsSectionEl.hidden === false) {
renderPathsCurrentSelection(elements);
} else {
syncPathsListSelection(elements);
syncPathsDetailNavigation(elements);
}
if (elements.crossSectionEl && elements.crossSectionEl.hidden === false) {
renderRoseCross(elements);
renderRoseCurrentSelection(elements);
} else {
syncRoseDetailNavigation(elements);
}
if (elements.sectionEl && elements.sectionEl.hidden === false) {
renderTree(elements);
renderCurrentSelection(elements);
} else {
syncDetailNavigation(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);
if (!hasFiniteSelectionNumber(state.selectedWorldLayerIndex)
|| Number(state.selectedWorldLayerIndex) < 0
|| Number(state.selectedWorldLayerIndex) >= state.fourWorldLayers.length) {
state.selectedWorldLayerIndex = 0;
}
if (!hasFiniteSelectionNumber(state.selectedSephiraNumber)) {
state.selectedSephiraNumber = Number(state.tree.sephiroth[0]?.number || 1);
}
if (!String(state.activeNodeKey || "").trim()) {
state.activeNodeKey = buildSephiraKey(state.selectedSephiraNumber);
}
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");
bindDetailNavigation(elements);
bindBrowserDetailNavigation(elements);
bindPathsDetailNavigation(elements);
bindWorldDetailNavigation(elements);
bindRoseDetailNavigation(elements);
bindBrowserList(elements);
bindWorldList(elements);
bindPathsList(elements);
syncExportControls(elements);
if (elements.treeExportWebpEl && !elements.treeExportWebpEl.dataset.bound) {
elements.treeExportWebpEl.addEventListener("click", () => {
void exportTreeView("webp");
});
elements.treeExportWebpEl.dataset.bound = "true";
}
renderVisibleKabbalahViews(elements);
}
function selectPathByNumber(pathNumber) {
if (!state.initialized || !state.tree) return;
const el = getElements();
const path = getPathByNumber(pathNumber);
if (path) renderPathDetail(path, state.tree, el);
}
function selectSephiraByNumber(n) {
if (!state.initialized || !state.tree) return;
const el = getElements();
const seph = getSephiraByNumber(n);
if (seph) renderSephiraDetail(seph, state.tree, el);
}
// select sephirah (1-10) or path (11+) by a single number
function selectNode(n) {
if (isDaathToken(n) || Number(n) === 0) selectSephiraByNumber(0);
else if (n >= 1 && n <= 10) selectSephiraByNumber(n);
else selectPathByNumber(n);
}
// ─── public API ────────────────────────────────────────────────────────
function ensureKabbalahSection(magickDataset) {
const elements = getElements();
if (state.initialized) {
renderVisibleKabbalahViews(elements);
return;
}
state.initialized = true;
init(magickDataset, elements);
}
window.KabbalahSectionUi = { ensureKabbalahSection, selectPathByNumber, selectSephiraByNumber, selectNode };
})();