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,
|
2026-03-12 21:01:32 -07:00
|
|
|
|
selectedPathNumber: null,
|
|
|
|
|
|
exportInProgress: false,
|
|
|
|
|
|
exportFormat: ""
|
2026-03-07 01:09:00 -08:00
|
|
|
|
};
|
2026-03-12 21:01:32 -07:00
|
|
|
|
const TREE_EXPORT_FORMATS = {
|
|
|
|
|
|
webp: {
|
|
|
|
|
|
mimeType: "image/webp",
|
|
|
|
|
|
extension: "webp",
|
|
|
|
|
|
quality: 0.98
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
const TREE_EXPORT_BACKGROUND = "#02030a";
|
2026-03-07 01:09:00 -08:00
|
|
|
|
|
2026-03-07 05:17:50 -08:00
|
|
|
|
const kabbalahDetailUi = window.KabbalahDetailUi || {};
|
2026-03-07 13:38:13 -08:00
|
|
|
|
const kabbalahViewsUi = window.KabbalahViewsUi || {};
|
2026-03-12 21:01:32 -07:00
|
|
|
|
let webpExportSupported = null;
|
2026-03-07 13:38:13 -08:00
|
|
|
|
|
|
|
|
|
|
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 (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"),
|
2026-03-12 21:01:32 -07:00
|
|
|
|
treeExportWebpEl: document.getElementById("kab-tree-export-webp"),
|
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
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 21:01:32 -07:00
|
|
|
|
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());
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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");
|
|
|
|
|
|
|
2026-03-12 21:01:32 -07:00
|
|
|
|
syncExportControls(elements);
|
|
|
|
|
|
if (elements.treeExportWebpEl && !elements.treeExportWebpEl.dataset.bound) {
|
|
|
|
|
|
elements.treeExportWebpEl.addEventListener("click", () => {
|
|
|
|
|
|
void exportTreeView("webp");
|
|
|
|
|
|
});
|
|
|
|
|
|
elements.treeExportWebpEl.dataset.bound = "true";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-07 01:09:00 -08:00
|
|
|
|
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 };
|
|
|
|
|
|
})();
|