CoBiE/script.js
2025-06-20 16:25:21 +02:00

1099 lines
39 KiB
JavaScript
Raw Permalink 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.

// CoBiE Time System Implementation
// Using Unix TAI epoch (January 1, 1970, 00:00:00 TAI)
// Note: TAI differs from UTC by leap seconds (37 seconds as of 2025)
(function() {
if (!window.Cobie) {
console.error('cobie.js not loaded');
return;
}
const {
COBIE_EPOCH,
COBIE_UNITS,
floorDiv,
parseCobiets,
getTAIOffsetAt,
toCobiets,
fromCobiets,
formatCobieTimestamp,
breakdownNonNeg
} = window.Cobie;
const { EONSTRIP_NAMES, MEGASEQUENCE_NAMES } = window.Cobie;
let currentOffset = 0;
let currentTimezone = 'UTC';
let manualMode = false;
let manualCobiets = 0;
let compareManualMode = false;
let compareCobiets = 0;
let updateInterval;
let lastRenderedEonstrip = null;
let currentDetailCob = null;
// ── Utility color helpers (in utils.js) ───────────────────────────────────
const {
parseColor,
hexToRgba,
getContrastColor,
lightenColor,
getHumanDiff
} = window.Utils;
const dateOptions = (long = true) => ({
timeZone: currentTimezone === 'TAI' ? 'UTC' : currentTimezone,
weekday: 'long',
year: 'numeric',
month: long ? 'long' : 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false
});
function applyEventColors(elem, color, alpha) {
if (!color || !elem) return;
elem.style.setProperty('--bg-color', hexToRgba(color, alpha));
// Use a lighter shade for the border so it stands out even for dark colors
elem.style.setProperty('--border-color', lightenColor(color, 0.4));
elem.style.setProperty('--text-color', getContrastColor(color));
}
function getTimezoneOffsetSeconds(date) {
if (currentTimezone === 'UTC') return 0;
if (currentTimezone === 'TAI') return getTAIOffsetAt(date);
const dtf = new Intl.DateTimeFormat('en-US', {
timeZone: currentTimezone,
year: 'numeric', month: '2-digit', day: '2-digit',
hour: '2-digit', minute: '2-digit', second: '2-digit',
hour12: false
});
const parts = dtf.formatToParts(date).reduce((acc, p) => {
if (p.type !== 'literal') acc[p.type] = parseInt(p.value, 10);
return acc;
}, {});
const utcTime = Date.UTC(parts.year, parts.month - 1, parts.day,
parts.hour, parts.minute, parts.second);
return (utcTime - date.getTime()) / 1000;
}
function formatSafeDate(rawDate, cobSeconds, intlOptions) {
if (rawDate instanceof Date && !isNaN(rawDate.getTime())) {
// Date is valid: optionally shift for TAI vs UTC, then format:
const shifted = currentTimezone === 'TAI'
? new Date(rawDate.getTime() + getTAIOffsetAt(rawDate) * 1000)
: rawDate;
return shifted.toLocaleString('en-US', intlOptions);
} else {
// Invalid Date: compute an approximate calendar year
// Seconds in a tropical year ≈ 365.2425 days
const secondsPerYear = 3600 * 24 * 365.2425;
const yearsSince1970 = cobSeconds / secondsPerYear;
const approxYear = Math.round(1970 + yearsSince1970);
return `≈ Year ${approxYear.toLocaleString('en-US')}`;
}
}
// parseCobiets, floorDiv and other CoBiE helpers are provided by cobie.js
// ── Event utilities ──────────────────────────────────────────────────────
const normalizeEvent = ev => {
const baseStart = parseCobiets(ev.start || ev.cobie);
if (baseStart === null) return null;
const tzShift = ev.shiftWithTimezone ?
getTimezoneOffsetSeconds(fromCobiets(baseStart)) : 0;
const startCob = baseStart - tzShift;
const endCob = ev.end ? parseCobiets(ev.end) - tzShift : Number.POSITIVE_INFINITY;
const unitVal = COBIE_UNITS[ev.unit] || COBIE_UNITS.cosmocycle;
const interval = (ev.interval || 1) * unitVal;
let duration = 0;
if (typeof ev.duration === 'string') {
const d = parseCobiets(ev.duration);
if (d !== null) duration = d;
} else if (typeof ev.duration === 'number') {
duration = ev.duration;
}
return { startCob, endCob, interval, duration };
};
function collectEventOccurrences(start, end, predicate = () => true) {
const out = [];
if (!Array.isArray(window.SPECIAL_EVENTS)) return out;
window.SPECIAL_EVENTS.forEach(ev => {
if (!predicate(ev)) return;
const meta = normalizeEvent(ev);
if (!meta || start > meta.endCob) return;
let n = Math.floor((start - meta.startCob) / meta.interval);
if (n < 0) n = 0;
let occ = meta.startCob + n * meta.interval;
if (occ + meta.duration <= start) occ += meta.interval;
while (occ < end && occ <= meta.endCob) {
out.push({ event: ev, meta, occ });
occ += meta.interval;
}
});
return out;
}
// getTAIOffsetAt, toCobiets, fromCobiets, breakdownNonNeg and
// formatCobieTimestamp come from cobie.js
function updateCurrentTime() {
let cobiets, baseDate;
if (manualMode) {
cobiets = manualCobiets;
baseDate = fromCobiets(cobiets);
} else {
baseDate = new Date();
cobiets = toCobiets(baseDate);
}
const cobieElem = document.getElementById('cobieTime');
if (cobieElem) cobieElem.textContent = formatCobieTimestamp(cobiets);
const options = dateOptions();
const taiOffset = getTAIOffsetAt(baseDate);
let displayDate = baseDate;
if (currentTimezone === 'TAI') {
displayDate = new Date(baseDate.getTime() + taiOffset * 1000);
}
document.getElementById('regularTime').textContent = currentTimezone + ': ' + displayDate.toLocaleString('en-US', options);
const optionsUTC = { ...options, timeZone: 'UTC' };
const taiDate = new Date(baseDate.getTime() + taiOffset * 1000);
document.getElementById('taiTime').textContent = 'TAI UTC: ' + taiDate.toLocaleString('en-US', optionsUTC) + ' (UTC + ' + taiOffset + 's)';
const bd = breakdownNonNeg(Math.abs(cobiets));
const currentStrip = bd.eonstrip;
if (currentStrip !== lastRenderedEonstrip) {
updateCalendar();
lastRenderedEonstrip = currentStrip;
} else {
updateTimeBreakdown(cobiets);
}
updateDetailCurrentTime();
}
function updateTimeBreakdown(cobiets) {
// 1) Break down absolute CoBiE seconds into each unit
const breakdown = breakdownNonNeg(Math.abs(cobiets));
// 2) Compute the “start” CoBiEseconds for each unit (in ascending order)
const eocStart = breakdown.epoch_of_cosmos * COBIE_UNITS.epoch_of_cosmos;
const cerStart = eocStart + breakdown.celestial_era * COBIE_UNITS.celestial_era;
const ueoStart = cerStart + breakdown.universal_eon * COBIE_UNITS.universal_eon;
const gyrStart = ueoStart + breakdown.galactic_year * COBIE_UNITS.galactic_year;
const ccyStart = gyrStart + breakdown.cosmocycle * COBIE_UNITS.cosmocycle;
const mqsStart = ccyStart + breakdown.megasequence * COBIE_UNITS.megasequence;
const eosStart = mqsStart + breakdown.eonstrip * COBIE_UNITS.eonstrip;
// 3) Compute each “end” by adding (unitSize 1)
const eocEnd = eocStart + COBIE_UNITS.epoch_of_cosmos - 1;
const cerEnd = cerStart + COBIE_UNITS.celestial_era - 1;
const ueoEnd = ueoStart + COBIE_UNITS.universal_eon - 1;
const gyrEnd = gyrStart + COBIE_UNITS.galactic_year - 1;
const ccyEnd = ccyStart + COBIE_UNITS.cosmocycle - 1;
const mqsEnd = mqsStart + COBIE_UNITS.megasequence - 1;
const eosEnd = eosStart + COBIE_UNITS.eonstrip - 1;
// 4) Intl formatting options
const optsLong = dateOptions();
//
// ── Build the “core” units (always visible): Galactic Year → Second ──────────────
//
const coreHtml = `
<div class="time-unit">
<span class="unit-name">Galactic Year</span>
<span class="unit-value">
0x${breakdown.galactic_year.toString(16).toUpperCase()} (${breakdown.galactic_year})
</span>
</div>
<div class="time-unit" style="padding-left: 20px; font-size: 0.9em; color: #888;">
${(() => {
const rawStart = fromCobiets(gyrStart);
const rawEnd = fromCobiets(gyrEnd);
return `
<span>Started: ${formatSafeDate(rawStart, gyrStart, optsLong)}</span><br>
<span>Ends: ${formatSafeDate(rawEnd, gyrEnd, optsLong)}</span>
`;
})()}
</div>
<div class="time-unit">
<span class="unit-name">Cosmocycle</span>
<span class="unit-value">
0x${breakdown.cosmocycle.toString(16).toUpperCase()} (${breakdown.cosmocycle})
</span>
</div>
<div class="time-unit" style="padding-left: 20px; font-size: 0.9em; color: #888;">
${(() => {
const rawStart = fromCobiets(ccyStart);
const rawEnd = fromCobiets(ccyEnd);
return `
<span>Started: ${formatSafeDate(rawStart, ccyStart, optsLong)}</span><br>
<span>Ends: ${formatSafeDate(rawEnd, ccyEnd, optsLong)}</span>
`;
})()}
</div>
<div class="time-unit">
<span class="unit-name">Megasequence</span>
<span class="unit-value">
0x${breakdown.megasequence.toString(16).toUpperCase()} (${breakdown.megasequence}) ${MEGASEQUENCE_NAMES[breakdown.megasequence] || 'Unknown'}
</span>
</div>
<div class="time-unit" style="padding-left: 20px; font-size: 0.9em; color: #888;">
${(() => {
const rawStart = fromCobiets(mqsStart);
const rawEnd = fromCobiets(mqsEnd);
return `
<span>Started: ${formatSafeDate(rawStart, mqsStart, optsLong)}</span><br>
<span>Ends: ${formatSafeDate(rawEnd, mqsEnd, optsLong)}</span>
`;
})()}
</div>
<div class="time-unit">
<span class="unit-name">Eonstrip</span>
<span class="unit-value">
0x${breakdown.eonstrip.toString(16).toUpperCase()} (${breakdown.eonstrip}) ${EONSTRIP_NAMES[breakdown.eonstrip]}
</span>
</div>
<div class="time-unit" style="padding-left: 20px; font-size: 0.9em; color: #888;">
${(() => {
const rawStart = fromCobiets(eosStart);
const rawEnd = fromCobiets(eosEnd);
return `
<span>Started: ${formatSafeDate(rawStart, eosStart, optsLong)}</span><br>
<span>Ends: ${formatSafeDate(rawEnd, eosEnd, optsLong)}</span>
`;
})()}
</div>
<div class="time-unit">
<span class="unit-name">Chronon</span>
<span class="unit-value">
0x${breakdown.chronon.toString(16).toUpperCase()} (${breakdown.chronon})
</span>
</div>
<div class="time-unit">
<span class="unit-name">Quantic</span>
<span class="unit-value">
0x${breakdown.quantic.toString(16).toUpperCase()} (${breakdown.quantic})
</span>
</div>
<div class="time-unit">
<span class="unit-name">Xenocycle</span>
<span class="unit-value">
0x${breakdown.xenocycle.toString(16).toUpperCase()} (${breakdown.xenocycle})
</span>
</div>
<div class="time-unit">
<span class="unit-name">Second</span>
<span class="unit-value">
0x${breakdown.second.toString(16).toUpperCase()} (${breakdown.second})
</span>
</div>
`;
document.getElementById('coreBreakdown').innerHTML = coreHtml;
//
// ── Build the “extended” units (hidden by default) in order: Astralmillennia → Universal Eon ───
//
const extendedHtml = `
<div class="time-unit">
<span class="unit-name">Astralmillennia</span>
<span class="unit-value">
0x${breakdown.astralmillennia.toString(16).toUpperCase()} (${breakdown.astralmillennia})
</span>
</div>
<div class="time-unit" style="padding-left: 20px; font-size: 0.9em; color: #888;">
${(() => {
const startAmt = breakdown.astralmillennia * COBIE_UNITS.astralmillennia;
const endAmt = startAmt + (COBIE_UNITS.astralmillennia - 1);
const rawStart = fromCobiets(startAmt);
const rawEnd = fromCobiets(endAmt);
return `
<span>Started: ${formatSafeDate(rawStart, startAmt, optsLong)}</span><br>
<span>Ends: ${formatSafeDate(rawEnd, endAmt, optsLong)}</span>
`;
})()}
</div>
<div class="time-unit">
<span class="unit-name">Infinitum</span>
<span class="unit-value">
0x${breakdown.infinitum.toString(16).toUpperCase()} (${breakdown.infinitum})
</span>
</div>
<div class="time-unit" style="padding-left: 20px; font-size: 0.9em; color: #888;">
${(() => {
const startAmt = breakdown.infinitum * COBIE_UNITS.infinitum;
const endAmt = startAmt + (COBIE_UNITS.infinitum - 1);
const rawStart = fromCobiets(startAmt);
const rawEnd = fromCobiets(endAmt);
return `
<span>Started: ${formatSafeDate(rawStart, startAmt, optsLong)}</span><br>
<span>Ends: ${formatSafeDate(rawEnd, endAmt, optsLong)}</span>
`;
})()}
</div>
<div class="time-unit">
<span class="unit-name">Eternum</span>
<span class="unit-value">
0x${breakdown.eternum.toString(16).toUpperCase()} (${breakdown.eternum})
</span>
</div>
<div class="time-unit" style="padding-left: 20px; font-size: 0.9em; color: #888;">
${(() => {
const startAmt = breakdown.eternum * COBIE_UNITS.eternum;
const endAmt = startAmt + (COBIE_UNITS.eternum - 1);
const rawStart = fromCobiets(startAmt);
const rawEnd = fromCobiets(endAmt);
return `
<span>Started: ${formatSafeDate(rawStart, startAmt, optsLong)}</span><br>
<span>Ends: ${formatSafeDate(rawEnd, endAmt, optsLong)}</span>
`;
})()}
</div>
<div class="time-unit">
<span class="unit-name">Metaepoch</span>
<span class="unit-value">
0x${breakdown.metaepoch.toString(16).toUpperCase()} (${breakdown.metaepoch})
</span>
</div>
<div class="time-unit" style="padding-left: 20px; font-size: 0.9em; color: #888;">
${(() => {
const startAmt = breakdown.metaepoch * COBIE_UNITS.metaepoch;
const endAmt = startAmt + (COBIE_UNITS.metaepoch - 1);
const rawStart = fromCobiets(startAmt);
const rawEnd = fromCobiets(endAmt);
return `
<span>Started: ${formatSafeDate(rawStart, startAmt, optsLong)}</span><br>
<span>Ends: ${formatSafeDate(rawEnd, endAmt, optsLong)}</span>
`;
})()}
</div>
<div class="time-unit">
<span class="unit-name">Cosmic Aeon</span>
<span class="unit-value">
0x${breakdown.cosmic_aeon.toString(16).toUpperCase()} (${breakdown.cosmic_aeon})
</span>
</div>
<div class="time-unit" style="padding-left: 20px; font-size: 0.9em; color: #888;">
${(() => {
const startAmt = breakdown.cosmic_aeon * COBIE_UNITS.cosmic_aeon;
const endAmt = startAmt + (COBIE_UNITS.cosmic_aeon - 1);
const rawStart = fromCobiets(startAmt);
const rawEnd = fromCobiets(endAmt);
return `
<span>Started: ${formatSafeDate(rawStart, startAmt, optsLong)}</span><br>
<span>Ends: ${formatSafeDate(rawEnd, endAmt, optsLong)}</span>
`;
})()}
</div>
<div class="time-unit">
<span class="unit-name">Epoch of Cosmos</span>
<span class="unit-value">
0x${breakdown.epoch_of_cosmos.toString(16).toUpperCase()} (${breakdown.epoch_of_cosmos})
</span>
</div>
<div class="time-unit" style="padding-left: 20px; font-size: 0.9em; color: #888;">
${(() => {
const rawStart = fromCobiets(eocStart);
const rawEnd = fromCobiets(eocEnd);
return `
<span>Started: ${formatSafeDate(rawStart, eocStart, optsLong)}</span><br>
<span>Ends: ${formatSafeDate(rawEnd, eocEnd, optsLong)}</span>
`;
})()}
</div>
<div class="time-unit">
<span class="unit-name">Celestial Era</span>
<span class="unit-value">
0x${breakdown.celestial_era.toString(16).toUpperCase()} (${breakdown.celestial_era})
</span>
</div>
<div class="time-unit" style="padding-left: 20px; font-size: 0.9em; color: #888;">
${(() => {
const rawStart = fromCobiets(cerStart);
const rawEnd = fromCobiets(cerEnd);
return `
<span>Started: ${formatSafeDate(rawStart, cerStart, optsLong)}</span><br>
<span>Ends: ${formatSafeDate(rawEnd, cerEnd, optsLong)}</span>
`;
})()}
</div>
<div class="time-unit">
<span class="unit-name">Universal Eon</span>
<span class="unit-value">
0x${breakdown.universal_eon.toString(16).toUpperCase()} (${breakdown.universal_eon})
</span>
</div>
<div class="time-unit" style="padding-left: 20px; font-size: 0.9em; color: #888;">
${(() => {
const rawStart = fromCobiets(ueoStart);
const rawEnd = fromCobiets(ueoEnd);
return `
<span>Started: ${formatSafeDate(rawStart, ueoStart, optsLong)}</span><br>
<span>Ends: ${formatSafeDate(rawEnd, ueoEnd, optsLong)}</span>
`;
})()}
</div>
`;
document.getElementById('extendedBreakdown').innerHTML = extendedHtml;
//
// ── If “manual mode” is active, append the Compare/Difference rows beneath whichever section is visible ──
//
if (manualMode) {
const baseCob = compareManualMode ? compareCobiets : toCobiets(new Date());
const baseDate = fromCobiets(baseCob);
const manualDate = fromCobiets(manualCobiets);
const human = getHumanDiff(baseDate, manualDate);
const diffCob = manualCobiets - baseCob;
const diffHtml = `
<div class="time-unit">
<span class="unit-name">Compare</span>
<span class="unit-value" id="diffField">${formatCobieTimestamp(baseCob)}</span>
</div>
<div class="time-unit">
<span class="unit-name">Difference (CoBiE)</span>
<span class="unit-value">${formatCobieTimestamp(diffCob)}</span>
</div>
<div class="time-unit">
<span class="unit-name">Difference</span>
<span class="unit-value">
${human.years}y ${human.months}m ${human.days}d ${human.hours}h ${human.minutes}m ${human.seconds}s
</span>
</div>
`;
const extDiv = document.getElementById('extendedBreakdown');
const coreDiv = document.getElementById('coreBreakdown');
if (extDiv.style.display === 'block') {
extDiv.insertAdjacentHTML('beforeend', diffHtml);
} else {
coreDiv.insertAdjacentHTML('beforeend', diffHtml);
}
// Reattach the “editonclick” listener for the Compare field
document.getElementById('diffField').addEventListener('click', enterDiffEdit);
}
}
function enterDiffEdit() {
const span = document.getElementById('diffField');
const val = span.textContent;
const input = document.createElement('input');
input.id = 'diffInput';
input.value = val;
input.style.background = 'transparent';
input.style.border = 'none';
input.style.color = 'inherit';
input.style.width = '12ch';
span.replaceWith(input);
input.focus();
input.setSelectionRange(1, val.length);
input.addEventListener('keydown', e => { if (e.key === 'Enter') commitDiff(); });
input.addEventListener('blur', commitDiff);
}
function commitDiff() {
const input = document.getElementById('diffInput');
const parsed = parseCobiets(input.value);
const span = document.createElement('span');
span.id = 'diffField';
span.className = 'unit-value';
if (parsed !== null) {
compareManualMode = true;
compareCobiets = parsed;
span.textContent = formatCobieTimestamp(parsed);
} else {
span.textContent = input.defaultValue;
}
input.replaceWith(span);
span.addEventListener('click', enterDiffEdit);
// refresh breakdown display with new comparison
updateCurrentTime();
}
function updateCalendar() {
let currentCob, baseCob, currentBd, currentTime;
if (manualMode) {
currentCob = manualCobiets;
currentBd = breakdownNonNeg(Math.abs(manualCobiets));
currentTime = currentCob % COBIE_UNITS.eonstrip;
const curEonstrip = currentCob % COBIE_UNITS.megasequence;
const msStartCob = currentCob - curEonstrip + (currentOffset * COBIE_UNITS.megasequence);
baseCob = msStartCob;
} else {
const now = new Date();
currentCob = toCobiets(now);
currentBd = breakdownNonNeg(Math.abs(currentCob));
currentTime = currentCob % COBIE_UNITS.eonstrip;
const curEonstrip = currentCob % COBIE_UNITS.megasequence;
const msStartCob = currentCob - curEonstrip + (currentOffset * COBIE_UNITS.megasequence);
baseCob = msStartCob;
}
const baseBd = breakdownNonNeg(Math.abs(baseCob));
const sign = baseCob < 0 ? '-' : '+';
// Header
const msName = MEGASEQUENCE_NAMES[ baseBd.megasequence ] || 'Unknown';
document.getElementById('calendarHeader').textContent =
`${sign} ${baseBd.megasequence.toString(16)}: ${msName}`;
document.getElementById('currentPeriod').textContent =
`${sign}${baseBd.galactic_year.toString(16)}${baseBd.cosmocycle.toString(16)}${baseBd.megasequence.toString(16)}`;
// Grid
const grid = document.getElementById('eonstripGrid');
grid.innerHTML = '';
// reuse the same dateOpts you use elsewhere:
const dateOpts = dateOptions(false);
for (let i = 0; i < 16; i++) {
const cellCob = baseCob + i * COBIE_UNITS.eonstrip;
const cellBd = breakdownNonNeg(Math.abs(cellCob));
const startDate = fromCobiets(cellCob);
const card = document.createElement('div');
card.className = 'eonstrip-card';
if (currentOffset === 0 && i === currentBd.eonstrip)
card.classList.add('current');
card.innerHTML = `
<div class="eonstrip-name">${EONSTRIP_NAMES[i]}</div>
<div class="eonstrip-hex">
${sign}${cellBd.galactic_year.toString(16)}${cellBd.cosmocycle.toString(16)}${cellBd.megasequence.toString(16)}${i.toString(16)}
</div>
<div class="eonstrip-dates">
${startDate.toLocaleDateString('en-US', dateOpts)}
</div>`;
collectEventOccurrences(
cellCob,
cellCob + COBIE_UNITS.eonstrip,
ev => ev.showMega !== false
).forEach(({ event }) => {
const tag = document.createElement('div');
tag.className = 'event-tag';
tag.textContent = event.label;
card.appendChild(tag);
});
const tooltip = document.createElement('div');
tooltip.className = 'tooltip';
tooltip.innerHTML = showEonstripDetails(i, cellCob, dateOpts);
card.appendChild(tooltip);
grid.appendChild(card);
(function(cob, idx) {
card.addEventListener('click', () => {
showEonstripDetail(idx, cob);
});
})(cellCob, i);
}
updateTimeBreakdown(currentCob);
}
function showEonstripDetails(index, startCobiets, opts) {
const startDate = fromCobiets(startCobiets);
const endDate = fromCobiets(startCobiets + COBIE_UNITS.eonstrip - 1);
const options = opts || dateOptions();
return `
<strong>${EONSTRIP_NAMES[index]} (0x${index.toString(16).toUpperCase()})</strong><br>
Start: ${startDate.toLocaleString('en-US', options)}<br>
End: ${endDate.toLocaleString('en-US', options)}<br>
Duration: ${COBIE_UNITS.eonstrip} sec (~${(COBIE_UNITS.eonstrip/3600).toFixed(2)} h)
`;
}
function showEonstripDetail(index, startCob) {
if (startCob === undefined) {
startCob = index;
const bdTmp = breakdownNonNeg(Math.abs(startCob));
index = bdTmp.eonstrip;
}
currentDetailCob = startCob;
const calendar = document.getElementById('calendarView');
const detail = document.getElementById('eonstripDetailView');
const timeline = document.getElementById('detailTimeline');
const title = document.getElementById('detailTitle');
calendar.style.display = 'none';
detail.style.display = 'block';
timeline.innerHTML = '';
const bd = breakdownNonNeg(Math.abs(startCob));
const sign = startCob < 0 ? '-' : '+';
title.textContent = `${sign}${bd.galactic_year.toString(16)}${bd.cosmocycle.toString(16)}${bd.megasequence.toString(16)}${index.toString(16)} ${EONSTRIP_NAMES[index]}`;
for (let c = 0; c <= 16; c++) {
const block = document.createElement('div');
block.className = 'timeline-block';
block.style.top = (c / 16 * 100) + '%';
if (c < 16) block.textContent = c.toString(16).toUpperCase();
timeline.appendChild(block);
}
const line = document.createElement('div');
line.className = 'current-time-line';
line.id = 'detailCurrentTime';
timeline.appendChild(line);
updateDetailCurrentTime();
if (Array.isArray(window.SPECIAL_EVENTS)) {
const start = startCob;
const end = startCob + COBIE_UNITS.eonstrip;
const events = collectEventOccurrences(start, end, ev => ev.showDetail !== false)
.map(({ event, meta, occ }) => ({
label: event.label,
color: event.color,
start: (occ - start) / COBIE_UNITS.eonstrip,
end: (occ + meta.duration - start) / COBIE_UNITS.eonstrip,
cobStart: occ,
cobEnd: occ + meta.duration,
seriesStart: meta.startCob,
seriesEnd: meta.endCob
}));
events.sort((a,b)=>a.start-b.start);
const groups = [];
let active = [];
events.forEach(ev=>{
active = active.filter(a=>a.end>ev.start);
if (active.length===0) {
groups.push({events:[],columns:[],maxCols:0});
}
const g = groups[groups.length-1];
let col=0;
while(g.columns[col] && g.columns[col] > ev.start) col++;
g.columns[col] = ev.end;
ev.col = col;
g.maxCols = Math.max(g.maxCols, col+1);
g.events.push(ev);
active.push(ev);
});
groups.forEach(g=>{
const width = 100/(g.maxCols||1);
g.events.forEach(ev=>ev.width=width);
});
events.forEach(ev=>{
const left = ev.col * ev.width;
const displayStart = Math.max(0, ev.start);
const displayEnd = Math.min(1, ev.end);
const elem = document.createElement('div');
if (ev.end > ev.start) {
elem.className = 'event-box';
const h = (displayEnd - displayStart) * 100;
if (h < 2) {
elem.classList.add('small-event');
elem.style.height = '4px';
} else {
elem.style.height = (h > 0 ? h : 0) + '%';
}
} else {
elem.className = 'event-line';
}
elem.style.top = (displayStart * 100) + '%';
elem.style.left = `calc(var(--scale-width) + ${left}%)`;
elem.style.width = `calc(${ev.width}% - 2px)`;
if (ev.color) applyEventColors(elem, ev.color, 0.4);
if (elem.classList.contains('small-event') || elem.className === 'event-line') {
const label = document.createElement('span');
label.className = 'event-label';
if (displayStart < 0.05) {
label.classList.add('below');
}
label.textContent = ev.label;
elem.appendChild(label);
if (ev.color) applyEventColors(label, ev.color, 0.5);
} else {
elem.textContent = ev.label;
}
const tooltip = document.createElement('div');
tooltip.className = 'tooltip';
const optsShort = dateOptions(false);
const startStr = formatCobieTimestamp(ev.cobStart);
const endStr = formatCobieTimestamp(ev.cobEnd);
const startDate = fromCobiets(ev.cobStart).toLocaleString('en-US', optsShort);
const endDate = fromCobiets(ev.cobEnd).toLocaleString('en-US', optsShort);
const seriesStart = formatCobieTimestamp(ev.seriesStart);
const seriesEnd = isFinite(ev.seriesEnd) ? formatCobieTimestamp(ev.seriesEnd) : '∞';
tooltip.innerHTML =
`<strong>${ev.label}</strong><br>` +
`Start: ${startStr} (${startDate})<br>` +
(ev.cobEnd > ev.cobStart ? `End: ${endStr} (${endDate})<br>` : '') +
`Series: ${seriesStart} ${seriesEnd}`;
elem.appendChild(tooltip);
timeline.appendChild(elem);
});
}
}
function updateDetailCurrentTime() {
if (currentDetailCob === null) return;
const line = document.getElementById('detailCurrentTime');
if (!line) return;
const nowCob = manualMode ? manualCobiets : toCobiets(new Date());
const rel = (nowCob - currentDetailCob) / COBIE_UNITS.eonstrip;
if (rel >= 0 && rel <= 1) {
line.style.display = 'block';
line.style.top = (rel * 100) + '%';
line.textContent = formatCobieTimestamp(nowCob);
} else {
line.style.display = 'none';
}
}
function detailPrev() {
if (currentDetailCob === null) return;
Animate.animateDetailSwipe(-1, () => {
showEonstripDetail(currentDetailCob - COBIE_UNITS.eonstrip);
});
}
function detailNext() {
if (currentDetailCob === null) return;
Animate.animateDetailSwipe(1, () => {
showEonstripDetail(currentDetailCob + COBIE_UNITS.eonstrip);
});
}
function detailNow() {
const now = toCobiets(new Date());
const start = now - (now % COBIE_UNITS.eonstrip);
showEonstripDetail(start);
}
function exitDetailView() {
document.getElementById('eonstripDetailView').style.display = 'none';
document.getElementById('calendarView').style.display = 'block';
currentDetailCob = null;
}
function getStep(mods) {
// base step = 1 megasequence
let step = 1;
if (mods.altKey && mods.shiftKey && mods.ctrlKey) {
// Epoch of Cosmos = 16⁵ MS
step = 16 ** 5;
} else if (mods.altKey && mods.ctrlKey) {
// Celestial Era = 16⁴ MS
step = 16 ** 4;
} else if (mods.altKey && mods.shiftKey) {
// Universal Eon = 16³ MS
step = 16 ** 3;
} else if (mods.altKey) {
// Galactic Year = 16² MS
step = 16 ** 2;
} else if (mods.shiftKey) {
// Cosmocycle = 16¹ MS
step = 16 ** 1;
}
return step;
}
function navigatePeriod(evt, direction) {
const step = getStep(evt);
if (currentDetailCob !== null) {
exitDetailView();
}
Animate.animateSwipe(direction, () => {
currentOffset += direction * step;
updateCalendar();
});
}
function goToNow() {
manualMode = false;
manualCobiets = 0;
compareManualMode = false;
currentOffset = 0;
if (currentDetailCob !== null) {
exitDetailView();
}
updateCurrentTime();
updateCalendar();
clearInterval(updateInterval);
updateInterval = setInterval(updateCurrentTime, 1000);
document.querySelector('.current-time').classList.remove('manual');
if (window.CobieClock) {
window.CobieClock.start();
}
}
function enterEdit() {
const span = document.getElementById('cobieTime');
const val = span.textContent;
const input = document.createElement('input');
input.id = 'cobieInput';
input.value = val;
input.className = 'cobie-time';
input.style.background = 'transparent';
input.style.border = 'none';
input.style.color = 'inherit';
input.style.textAlign = 'center';
input.style.width = '100%';
input.style.boxSizing = 'border-box';
// keep the input visually centered like the original span
input.style.display = 'block';
input.style.margin = '10px 0';
span.replaceWith(input);
input.focus();
input.setSelectionRange(1, val.length);
input.addEventListener('keydown', e => { if (e.key === 'Enter') commitInput(); });
input.addEventListener('blur', commitInput);
}
function commitInput() {
const input = document.getElementById('cobieInput');
const str = input.value;
const parsed = parseCobiets(str);
// remember what the old numeric value was
// if we weren't already in manualMode,
// this will be "now"
const oldCob = manualMode
? manualCobiets
: toCobiets(new Date());
// build the new span
const span = document.createElement('div');
span.id = 'cobieTime';
span.className = 'cobie-time';
if (parsed === null) {
// invalid: restore the old numeric value
span.textContent = formatCobieTimestamp(oldCob);
// keep manualMode exactly as it was
manualCobiets = oldCob;
// manualMode = true;
} else {
// valid: switch into manual mode at the new value
manualMode = true;
manualCobiets = parsed;
clearInterval(updateInterval);
span.textContent = formatCobieTimestamp(parsed);
updateCurrentTime();
updateCalendar();
document.querySelector('.current-time').classList.add('manual');
if (window.CobieClock) {
window.CobieClock.showTime(manualCobiets);
}
}
// swap elements
input.replaceWith(span);
span.addEventListener('click', enterEdit);
}
function init() {
// Timezone change handler
document.getElementById('timezone').addEventListener('change', (e) => {
currentTimezone = e.target.value;
updateCurrentTime();
updateCalendar();
if (currentDetailCob !== null) {
showEonstripDetail(currentDetailCob);
}
});
// Set default timezone based on user's locale
const userTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
const timezoneSelect = document.getElementById('timezone');
const options = Array.from(timezoneSelect.options);
const matchingOption = options.find(opt => opt.value === userTimezone);
if (matchingOption) {
timezoneSelect.value = userTimezone;
currentTimezone = userTimezone;
}
document.getElementById('backToCalendar').addEventListener('click', exitDetailView);
document.getElementById('detailPrev').addEventListener('click', detailPrev);
document.getElementById('detailNext').addEventListener('click', detailNext);
document.getElementById('detailNow').addEventListener('click', detailNow);
updateCurrentTime();
updateCalendar();
updateInterval = setInterval(updateCurrentTime, 1000);
document.getElementById('cobieTime').addEventListener('click', enterEdit);
document.getElementById('toggleExtended').addEventListener('click', () => {
const extDiv = document.getElementById('extendedBreakdown');
const arrow = document.querySelector('#toggleExtended .arrow-icon');
const text = document.querySelector('#toggleExtended .btn-text');
if (extDiv.style.display === 'block') {
// hide it
extDiv.style.display = 'none';
arrow.style.transform = 'rotate(0deg)';
text.textContent = 'Show Cosmic Units';
} else {
// show it
extDiv.style.display = 'block';
arrow.style.transform = 'rotate(180deg)';
text.textContent = 'Hide Cosmic Units';
}
// Because “manual mode” might have appended diff rows, we need to rerun updateTimeBreakdown
// to ensure diff rows end up in the correct section
updateTimeBreakdown(manualMode ? manualCobiets : toCobiets(new Date()));
});
// ── Swipe & Wheel Navigation ────────────────────────────────────────────────
let swipeStartX = null;
let swipeStartY = null;
let swipeMods = { altKey: false, shiftKey: false, ctrlKey: false };
let isSwiping = false;
let swipeGrid = null;
let swipeContext = 'calendar';
function swipeStart(e) {
const touch = e.touches ? e.touches[0] : e;
swipeStartX = touch.clientX;
swipeStartY = touch.clientY;
swipeMods = {
altKey: e.altKey || false,
shiftKey: e.shiftKey || false,
ctrlKey: e.ctrlKey || false
};
const detailView = document.getElementById('eonstripDetailView');
const detailOpen = detailView && detailView.style.display === 'block';
if (detailOpen) {
swipeGrid = document.getElementById('detailTimeline');
swipeContext = 'detail';
} else {
swipeGrid = document.getElementById('eonstripGrid');
swipeContext = 'calendar';
}
if (swipeGrid) {
swipeGrid.style.transition = 'none';
}
isSwiping = true;
}
function swipeMove(e) {
if (!isSwiping || !swipeGrid) return;
const touch = e.touches ? e.touches[0] : e;
const dx = touch.clientX - swipeStartX;
const dy = touch.clientY - swipeStartY;
if (Math.abs(dx) > Math.abs(dy)) {
swipeGrid.style.transform = `translateX(${dx}px)`;
}
}
function swipeEnd(e) {
if (!isSwiping || swipeStartX === null || swipeStartY === null) {
return;
}
const touch = e.changedTouches ? e.changedTouches[0] : e;
const dx = touch.clientX - swipeStartX;
const dy = touch.clientY - swipeStartY;
if (swipeGrid) {
const width = swipeGrid.clientWidth;
if (Math.abs(dx) > 40 && Math.abs(dx) > Math.abs(dy)) {
const direction = dx < 0 ? 1 : -1; // left → next
swipeGrid.style.transition = 'transform 0.2s ease';
swipeGrid.style.transform = `translateX(${dx < 0 ? -width : width}px)`;
swipeGrid.addEventListener('transitionend', function after() {
swipeGrid.removeEventListener('transitionend', after);
// prepare opposite side
swipeGrid.style.transition = 'none';
swipeGrid.style.transform = `translateX(${dx < 0 ? width : -width}px)`;
if (swipeContext === 'calendar') {
const step = getStep(swipeMods);
currentOffset += direction * step;
updateCalendar();
} else if (swipeContext === 'detail') {
if (direction === 1) {
currentDetailCob !== null && showEonstripDetail(currentDetailCob + COBIE_UNITS.eonstrip);
} else {
currentDetailCob !== null && showEonstripDetail(currentDetailCob - COBIE_UNITS.eonstrip);
}
}
void swipeGrid.offsetWidth;
swipeGrid.style.transition = 'transform 0.3s ease';
swipeGrid.style.transform = 'translateX(0)';
});
} else {
swipeGrid.style.transition = 'transform 0.2s ease';
swipeGrid.style.transform = 'translateX(0)';
}
}
swipeStartX = swipeStartY = null;
isSwiping = false;
}
document.addEventListener('touchstart', swipeStart, { passive: true });
document.addEventListener('touchmove', swipeMove, { passive: true });
document.addEventListener('touchend', swipeEnd);
document.addEventListener('mousedown', swipeStart);
document.addEventListener('mousemove', swipeMove);
document.addEventListener('mouseup', swipeEnd);
function wheelNavigate(e) {
if (Math.abs(e.deltaX) > Math.abs(e.deltaY) && Math.abs(e.deltaX) > 10) {
const direction = e.deltaX > 0 ? 1 : -1;
if (currentDetailCob !== null) {
if (direction === 1) {
detailNext();
} else {
detailPrev();
}
} else {
navigatePeriod(e, direction);
}
}
}
document.addEventListener('wheel', wheelNavigate);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
window.navigatePeriod = navigatePeriod;
window.goToNow = goToNow;
window.detailPrev = detailPrev;
window.detailNext = detailNext;
window.detailNow = detailNow;
})();