2026-03-07 13:38:13 -08:00
|
|
|
/* ui-now-helpers.js — Lightbox and astronomy/countdown helpers for the Now panel */
|
|
|
|
|
(function () {
|
|
|
|
|
"use strict";
|
|
|
|
|
|
|
|
|
|
const { DAY_IN_MS, getMoonPhaseName, getDecanForDate } = window.TarotCalc || {};
|
|
|
|
|
const { resolveTarotCardImage, getTarotCardDisplayName } = window.TarotCardImages || {};
|
|
|
|
|
|
|
|
|
|
let nowLightboxOverlayEl = null;
|
|
|
|
|
let nowLightboxImageEl = null;
|
|
|
|
|
let nowLightboxZoomed = false;
|
|
|
|
|
|
|
|
|
|
const LIGHTBOX_ZOOM_SCALE = 6.66;
|
|
|
|
|
|
|
|
|
|
const PLANETARY_BODIES = [
|
|
|
|
|
{ id: "sol", astronomyBody: "Sun", fallbackName: "Sun", fallbackSymbol: "☉︎" },
|
|
|
|
|
{ id: "luna", astronomyBody: "Moon", fallbackName: "Moon", fallbackSymbol: "☾︎" },
|
|
|
|
|
{ id: "mercury", astronomyBody: "Mercury", fallbackName: "Mercury", fallbackSymbol: "☿︎" },
|
|
|
|
|
{ id: "venus", astronomyBody: "Venus", fallbackName: "Venus", fallbackSymbol: "♀︎" },
|
|
|
|
|
{ id: "mars", astronomyBody: "Mars", fallbackName: "Mars", fallbackSymbol: "♂︎" },
|
|
|
|
|
{ id: "jupiter", astronomyBody: "Jupiter", fallbackName: "Jupiter", fallbackSymbol: "♃︎" },
|
|
|
|
|
{ id: "saturn", astronomyBody: "Saturn", fallbackName: "Saturn", fallbackSymbol: "♄︎" },
|
|
|
|
|
{ id: "uranus", astronomyBody: "Uranus", fallbackName: "Uranus", fallbackSymbol: "♅︎" },
|
|
|
|
|
{ id: "neptune", astronomyBody: "Neptune", fallbackName: "Neptune", fallbackSymbol: "♆︎" },
|
|
|
|
|
{ id: "pluto", astronomyBody: "Pluto", fallbackName: "Pluto", fallbackSymbol: "♇︎" }
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
function resetNowLightboxZoom() {
|
|
|
|
|
if (!nowLightboxImageEl) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
nowLightboxZoomed = false;
|
|
|
|
|
nowLightboxImageEl.style.transform = "scale(1)";
|
|
|
|
|
nowLightboxImageEl.style.transformOrigin = "center center";
|
|
|
|
|
nowLightboxImageEl.style.cursor = "zoom-in";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function updateNowLightboxZoomOrigin(clientX, clientY) {
|
|
|
|
|
if (!nowLightboxZoomed || !nowLightboxImageEl) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const rect = nowLightboxImageEl.getBoundingClientRect();
|
|
|
|
|
if (!rect.width || !rect.height) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const x = Math.min(100, Math.max(0, ((clientX - rect.left) / rect.width) * 100));
|
|
|
|
|
const y = Math.min(100, Math.max(0, ((clientY - rect.top) / rect.height) * 100));
|
|
|
|
|
nowLightboxImageEl.style.transformOrigin = `${x}% ${y}%`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function isNowLightboxPointOnCard(clientX, clientY) {
|
|
|
|
|
if (!nowLightboxImageEl) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const rect = nowLightboxImageEl.getBoundingClientRect();
|
|
|
|
|
const naturalWidth = nowLightboxImageEl.naturalWidth;
|
|
|
|
|
const naturalHeight = nowLightboxImageEl.naturalHeight;
|
|
|
|
|
|
|
|
|
|
if (!rect.width || !rect.height || !naturalWidth || !naturalHeight) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const frameAspect = rect.width / rect.height;
|
|
|
|
|
const imageAspect = naturalWidth / naturalHeight;
|
|
|
|
|
|
|
|
|
|
let renderWidth = rect.width;
|
|
|
|
|
let renderHeight = rect.height;
|
|
|
|
|
if (imageAspect > frameAspect) {
|
|
|
|
|
renderHeight = rect.width / imageAspect;
|
|
|
|
|
} else {
|
|
|
|
|
renderWidth = rect.height * imageAspect;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const left = rect.left + (rect.width - renderWidth) / 2;
|
|
|
|
|
const top = rect.top + (rect.height - renderHeight) / 2;
|
|
|
|
|
const right = left + renderWidth;
|
|
|
|
|
const bottom = top + renderHeight;
|
|
|
|
|
|
|
|
|
|
return clientX >= left && clientX <= right && clientY >= top && clientY <= bottom;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function ensureNowImageLightbox() {
|
|
|
|
|
if (nowLightboxOverlayEl && nowLightboxImageEl) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
nowLightboxOverlayEl = document.createElement("div");
|
|
|
|
|
nowLightboxOverlayEl.setAttribute("aria-hidden", "true");
|
|
|
|
|
nowLightboxOverlayEl.style.position = "fixed";
|
|
|
|
|
nowLightboxOverlayEl.style.inset = "0";
|
|
|
|
|
nowLightboxOverlayEl.style.background = "rgba(0, 0, 0, 0.82)";
|
|
|
|
|
nowLightboxOverlayEl.style.display = "none";
|
|
|
|
|
nowLightboxOverlayEl.style.alignItems = "center";
|
|
|
|
|
nowLightboxOverlayEl.style.justifyContent = "center";
|
|
|
|
|
nowLightboxOverlayEl.style.zIndex = "9999";
|
|
|
|
|
nowLightboxOverlayEl.style.padding = "0";
|
|
|
|
|
|
|
|
|
|
const image = document.createElement("img");
|
|
|
|
|
image.alt = "Now card enlarged image";
|
|
|
|
|
image.style.maxWidth = "100vw";
|
|
|
|
|
image.style.maxHeight = "100vh";
|
|
|
|
|
image.style.width = "100vw";
|
|
|
|
|
image.style.height = "100vh";
|
|
|
|
|
image.style.objectFit = "contain";
|
|
|
|
|
image.style.borderRadius = "0";
|
|
|
|
|
image.style.boxShadow = "none";
|
|
|
|
|
image.style.border = "none";
|
|
|
|
|
image.style.cursor = "zoom-in";
|
|
|
|
|
image.style.transform = "scale(1)";
|
|
|
|
|
image.style.transformOrigin = "center center";
|
|
|
|
|
image.style.transition = "transform 120ms ease-out";
|
|
|
|
|
image.style.userSelect = "none";
|
|
|
|
|
|
|
|
|
|
nowLightboxImageEl = image;
|
|
|
|
|
nowLightboxOverlayEl.appendChild(image);
|
|
|
|
|
|
|
|
|
|
const closeLightbox = () => {
|
|
|
|
|
if (!nowLightboxOverlayEl || !nowLightboxImageEl) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
nowLightboxOverlayEl.style.display = "none";
|
|
|
|
|
nowLightboxOverlayEl.setAttribute("aria-hidden", "true");
|
|
|
|
|
nowLightboxImageEl.removeAttribute("src");
|
|
|
|
|
resetNowLightboxZoom();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
nowLightboxOverlayEl.addEventListener("click", (event) => {
|
|
|
|
|
if (event.target === nowLightboxOverlayEl) {
|
|
|
|
|
closeLightbox();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
nowLightboxImageEl.addEventListener("click", (event) => {
|
|
|
|
|
event.stopPropagation();
|
|
|
|
|
if (!isNowLightboxPointOnCard(event.clientX, event.clientY)) {
|
|
|
|
|
closeLightbox();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (!nowLightboxZoomed) {
|
|
|
|
|
nowLightboxZoomed = true;
|
|
|
|
|
nowLightboxImageEl.style.transform = `scale(${LIGHTBOX_ZOOM_SCALE})`;
|
|
|
|
|
nowLightboxImageEl.style.cursor = "zoom-out";
|
|
|
|
|
updateNowLightboxZoomOrigin(event.clientX, event.clientY);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
resetNowLightboxZoom();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
nowLightboxImageEl.addEventListener("mousemove", (event) => {
|
|
|
|
|
updateNowLightboxZoomOrigin(event.clientX, event.clientY);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
nowLightboxImageEl.addEventListener("mouseleave", () => {
|
|
|
|
|
if (nowLightboxZoomed) {
|
|
|
|
|
nowLightboxImageEl.style.transformOrigin = "center center";
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
document.addEventListener("keydown", (event) => {
|
|
|
|
|
if (event.key === "Escape") {
|
|
|
|
|
closeLightbox();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
document.body.appendChild(nowLightboxOverlayEl);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function openNowImageLightbox(src, altText) {
|
|
|
|
|
if (!src) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ensureNowImageLightbox();
|
|
|
|
|
if (!nowLightboxOverlayEl || !nowLightboxImageEl) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
nowLightboxImageEl.src = src;
|
|
|
|
|
nowLightboxImageEl.alt = altText || "Now card enlarged image";
|
|
|
|
|
resetNowLightboxZoom();
|
|
|
|
|
nowLightboxOverlayEl.style.display = "flex";
|
|
|
|
|
nowLightboxOverlayEl.setAttribute("aria-hidden", "false");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getDisplayTarotName(cardName, trumpNumber) {
|
|
|
|
|
if (!cardName) {
|
|
|
|
|
return "";
|
|
|
|
|
}
|
|
|
|
|
if (typeof getTarotCardDisplayName !== "function") {
|
|
|
|
|
return cardName;
|
|
|
|
|
}
|
|
|
|
|
if (Number.isFinite(Number(trumpNumber))) {
|
|
|
|
|
return getTarotCardDisplayName(cardName, { trumpNumber: Number(trumpNumber) }) || cardName;
|
|
|
|
|
}
|
|
|
|
|
return getTarotCardDisplayName(cardName) || cardName;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function bindNowCardLightbox(imageEl) {
|
|
|
|
|
if (!(imageEl instanceof HTMLImageElement) || imageEl.dataset.lightboxBound === "true") {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
imageEl.dataset.lightboxBound = "true";
|
|
|
|
|
imageEl.style.cursor = "zoom-in";
|
|
|
|
|
imageEl.title = "Click to enlarge";
|
|
|
|
|
imageEl.addEventListener("click", () => {
|
|
|
|
|
const src = imageEl.getAttribute("src");
|
|
|
|
|
if (!src || imageEl.style.display === "none") {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
openNowImageLightbox(src, imageEl.alt || "Now card enlarged image");
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function normalizeLongitude(value) {
|
|
|
|
|
const numeric = Number(value);
|
|
|
|
|
if (!Number.isFinite(numeric)) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return ((numeric % 360) + 360) % 360;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getSortedSigns(signs) {
|
|
|
|
|
if (!Array.isArray(signs)) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return [...signs].sort((a, b) => (a.order || 0) - (b.order || 0));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getSignForLongitude(longitude, signs) {
|
|
|
|
|
const normalized = normalizeLongitude(longitude);
|
|
|
|
|
if (normalized === null) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const sortedSigns = getSortedSigns(signs);
|
|
|
|
|
if (!sortedSigns.length) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const signIndex = Math.min(sortedSigns.length - 1, Math.floor(normalized / 30));
|
|
|
|
|
const sign = sortedSigns[signIndex] || null;
|
|
|
|
|
if (!sign) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
sign,
|
|
|
|
|
degreeInSign: normalized - signIndex * 30,
|
|
|
|
|
absoluteLongitude: normalized
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getSabianSymbolForLongitude(longitude, sabianSymbols) {
|
|
|
|
|
const normalized = normalizeLongitude(longitude);
|
|
|
|
|
if (normalized === null || !Array.isArray(sabianSymbols) || !sabianSymbols.length) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const absoluteDegree = Math.floor(normalized) + 1;
|
|
|
|
|
return sabianSymbols.find((entry) => Number(entry?.absoluteDegree) === absoluteDegree) || null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function calculatePlanetPositions(referenceData, now) {
|
|
|
|
|
if (!window.Astronomy || !referenceData) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const positions = [];
|
|
|
|
|
|
|
|
|
|
PLANETARY_BODIES.forEach((body) => {
|
|
|
|
|
try {
|
|
|
|
|
const geoVector = window.Astronomy.GeoVector(body.astronomyBody, now, true);
|
|
|
|
|
const ecliptic = window.Astronomy.Ecliptic(geoVector);
|
|
|
|
|
const signInfo = getSignForLongitude(ecliptic?.elon, referenceData.signs);
|
|
|
|
|
if (!signInfo?.sign) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const planetInfo = referenceData.planets?.[body.id] || null;
|
|
|
|
|
const symbol = planetInfo?.symbol || body.fallbackSymbol;
|
|
|
|
|
const name = planetInfo?.name || body.fallbackName;
|
|
|
|
|
|
|
|
|
|
positions.push({
|
|
|
|
|
id: body.id,
|
|
|
|
|
symbol,
|
|
|
|
|
name,
|
|
|
|
|
longitude: signInfo.absoluteLongitude,
|
|
|
|
|
sign: signInfo.sign,
|
|
|
|
|
degreeInSign: signInfo.degreeInSign,
|
|
|
|
|
label: `${symbol} ${name}: ${signInfo.sign.symbol} ${signInfo.sign.name} ${signInfo.degreeInSign.toFixed(1)}°`
|
|
|
|
|
});
|
|
|
|
|
} catch {
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return positions;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function updateNowStats(referenceData, elements, now) {
|
|
|
|
|
const planetPositions = calculatePlanetPositions(referenceData, now);
|
|
|
|
|
|
|
|
|
|
if (elements.nowStatsPlanetsEl) {
|
|
|
|
|
elements.nowStatsPlanetsEl.replaceChildren();
|
|
|
|
|
|
|
|
|
|
if (!planetPositions.length) {
|
|
|
|
|
elements.nowStatsPlanetsEl.textContent = "--";
|
|
|
|
|
} else {
|
|
|
|
|
planetPositions.forEach((position) => {
|
|
|
|
|
const item = document.createElement("div");
|
|
|
|
|
item.className = "now-stats-planet";
|
|
|
|
|
item.textContent = position.label;
|
|
|
|
|
elements.nowStatsPlanetsEl.appendChild(item);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (elements.nowStatsSabianEl) {
|
|
|
|
|
const sunPosition = planetPositions.find((entry) => entry.id === "sol") || null;
|
|
|
|
|
const moonPosition = planetPositions.find((entry) => entry.id === "luna") || null;
|
|
|
|
|
const sunSabianSymbol = sunPosition
|
|
|
|
|
? getSabianSymbolForLongitude(sunPosition.longitude, referenceData.sabianSymbols)
|
|
|
|
|
: null;
|
|
|
|
|
const moonSabianSymbol = moonPosition
|
|
|
|
|
? getSabianSymbolForLongitude(moonPosition.longitude, referenceData.sabianSymbols)
|
|
|
|
|
: null;
|
|
|
|
|
|
|
|
|
|
const sunLine = sunSabianSymbol?.phrase
|
|
|
|
|
? `Sun Sabian ${sunSabianSymbol.absoluteDegree}: ${sunSabianSymbol.phrase}`
|
|
|
|
|
: "Sun Sabian: --";
|
|
|
|
|
const moonLine = moonSabianSymbol?.phrase
|
|
|
|
|
? `Moon Sabian ${moonSabianSymbol.absoluteDegree}: ${moonSabianSymbol.phrase}`
|
|
|
|
|
: "Moon Sabian: --";
|
|
|
|
|
|
|
|
|
|
elements.nowStatsSabianEl.textContent = `${sunLine}\n${moonLine}`;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function formatCountdown(ms, mode) {
|
|
|
|
|
if (!Number.isFinite(ms) || ms <= 0) {
|
|
|
|
|
if (mode === "hours") {
|
|
|
|
|
return "0.0 hours";
|
|
|
|
|
}
|
|
|
|
|
if (mode === "seconds") {
|
|
|
|
|
return "0s";
|
|
|
|
|
}
|
|
|
|
|
return "0m";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (mode === "hours") {
|
|
|
|
|
return `${(ms / 3600000).toFixed(1)} hours`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (mode === "seconds") {
|
|
|
|
|
return `${Math.floor(ms / 1000)}s`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return `${Math.floor(ms / 60000)}m`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function parseMonthDay(monthDay) {
|
|
|
|
|
const [month, day] = String(monthDay || "").split("-").map(Number);
|
|
|
|
|
return { month, day };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getCurrentPhaseName(date) {
|
|
|
|
|
return getMoonPhaseName(window.SunCalc.getMoonIllumination(date).phase);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function findNextMoonPhaseTransition(now) {
|
|
|
|
|
const currentPhase = getCurrentPhaseName(now);
|
|
|
|
|
const stepMs = 15 * 60 * 1000;
|
|
|
|
|
const maxMs = 40 * DAY_IN_MS;
|
|
|
|
|
|
|
|
|
|
let previousTime = now.getTime();
|
|
|
|
|
let previousPhase = currentPhase;
|
|
|
|
|
|
|
|
|
|
for (let t = previousTime + stepMs; t <= previousTime + maxMs; t += stepMs) {
|
|
|
|
|
const phaseName = getCurrentPhaseName(new Date(t));
|
|
|
|
|
if (phaseName !== previousPhase) {
|
|
|
|
|
let low = previousTime;
|
|
|
|
|
let high = t;
|
|
|
|
|
while (high - low > 1000) {
|
|
|
|
|
const mid = Math.floor((low + high) / 2);
|
|
|
|
|
const midPhase = getCurrentPhaseName(new Date(mid));
|
|
|
|
|
if (midPhase === currentPhase) {
|
|
|
|
|
low = mid;
|
|
|
|
|
} else {
|
|
|
|
|
high = mid;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const transitionAt = new Date(high);
|
|
|
|
|
const nextPhase = getCurrentPhaseName(new Date(high + 1000));
|
|
|
|
|
return {
|
|
|
|
|
fromPhase: currentPhase,
|
|
|
|
|
nextPhase,
|
|
|
|
|
changeAt: transitionAt
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
previousTime = t;
|
|
|
|
|
previousPhase = phaseName;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getSignStartDate(now, sign) {
|
|
|
|
|
const { month: startMonth, day: startDay } = parseMonthDay(sign.start);
|
|
|
|
|
const { month: endMonth } = parseMonthDay(sign.end);
|
|
|
|
|
const wrapsYear = startMonth > endMonth;
|
|
|
|
|
|
|
|
|
|
let year = now.getFullYear();
|
|
|
|
|
const nowMonth = now.getMonth() + 1;
|
|
|
|
|
const nowDay = now.getDate();
|
|
|
|
|
|
|
|
|
|
if (wrapsYear && (nowMonth < startMonth || (nowMonth === startMonth && nowDay < startDay))) {
|
|
|
|
|
year -= 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return new Date(year, startMonth - 1, startDay);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getNextSign(signs, currentSign) {
|
|
|
|
|
const sorted = [...signs].sort((a, b) => (a.order || 0) - (b.order || 0));
|
|
|
|
|
const index = sorted.findIndex((entry) => entry.id === currentSign.id);
|
|
|
|
|
if (index < 0) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
return sorted[(index + 1) % sorted.length] || null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getDecanByIndex(decansBySign, signId, index) {
|
|
|
|
|
const signDecans = decansBySign[signId] || [];
|
|
|
|
|
return signDecans.find((entry) => entry.index === index) || null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function findNextDecanTransition(now, signs, decansBySign) {
|
|
|
|
|
const currentInfo = getDecanForDate(now, signs, decansBySign);
|
|
|
|
|
if (!currentInfo?.sign) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const currentIndex = currentInfo.decan?.index || 1;
|
|
|
|
|
const signStart = getSignStartDate(now, currentInfo.sign);
|
|
|
|
|
|
|
|
|
|
if (currentIndex < 3) {
|
|
|
|
|
const changeAt = new Date(signStart.getTime() + currentIndex * 10 * DAY_IN_MS);
|
|
|
|
|
const nextDecan = getDecanByIndex(decansBySign, currentInfo.sign.id, currentIndex + 1);
|
|
|
|
|
const nextLabel = nextDecan?.tarotMinorArcana || `${currentInfo.sign.name} Decan ${currentIndex + 1}`;
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
key: `${currentInfo.sign.id}-${currentIndex}`,
|
|
|
|
|
changeAt,
|
|
|
|
|
nextLabel
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const nextSign = getNextSign(signs, currentInfo.sign);
|
|
|
|
|
if (!nextSign) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const { month: nextMonth, day: nextDay } = parseMonthDay(nextSign.start);
|
|
|
|
|
let year = now.getFullYear();
|
|
|
|
|
let changeAt = new Date(year, nextMonth - 1, nextDay);
|
|
|
|
|
if (changeAt.getTime() <= now.getTime()) {
|
|
|
|
|
changeAt = new Date(year + 1, nextMonth - 1, nextDay);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const nextDecan = getDecanByIndex(decansBySign, nextSign.id, 1);
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
key: `${currentInfo.sign.id}-${currentIndex}`,
|
|
|
|
|
changeAt,
|
|
|
|
|
nextLabel: nextDecan?.tarotMinorArcana || `${nextSign.name} Decan 1`
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function setNowCardImage(imageEl, cardName, fallbackLabel, trumpNumber) {
|
|
|
|
|
if (!imageEl) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bindNowCardLightbox(imageEl);
|
|
|
|
|
|
|
|
|
|
if (!cardName || typeof resolveTarotCardImage !== "function") {
|
|
|
|
|
imageEl.style.display = "none";
|
|
|
|
|
imageEl.removeAttribute("src");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const src = resolveTarotCardImage(cardName);
|
|
|
|
|
if (!src) {
|
|
|
|
|
imageEl.style.display = "none";
|
|
|
|
|
imageEl.removeAttribute("src");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
imageEl.src = src;
|
|
|
|
|
const displayName = getDisplayTarotName(cardName, trumpNumber);
|
|
|
|
|
imageEl.alt = `${fallbackLabel}: ${displayName}`;
|
|
|
|
|
imageEl.style.display = "block";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
window.NowUiHelpers = {
|
2026-03-07 14:15:09 -08:00
|
|
|
getSignStartDate,
|
2026-03-07 13:38:13 -08:00
|
|
|
findNextDecanTransition,
|
|
|
|
|
findNextMoonPhaseTransition,
|
|
|
|
|
formatCountdown,
|
|
|
|
|
getDisplayTarotName,
|
|
|
|
|
setNowCardImage,
|
|
|
|
|
updateNowStats
|
|
|
|
|
};
|
|
|
|
|
})();
|