// 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) const COBIE_EPOCH = 0; // Unix TAI epoch in seconds const COBIE_UNITS = { second: 1, xenocycle: 0x10, quantic: 0x100, chronon: 0x1000, eonstrip: 0x10000, megasequence: 0x100000, cosmocycle: 0x1000000, galactic_year: 0x10000000, universal_eon: 0x100000000, celestial_era: 0x1000000000, epoch_of_cosmos: 0x10000000000, cosmic_aeon: 0x100000000000, metaepoch: 0x1000000000000, eternum: 0x10000000000000, infinitum: 0x100000000000000, astralmillennia: 0x1000000000000000 }; const EONSTRIP_NAMES = [ 'Solprime', 'Lunex', 'Terros', 'Aquarion', 'Ventaso', 'Ignisar', 'Crystalos', 'Floraen', 'Faunor', 'Nebulus', 'Astraeus', 'Umbranox', 'Electros', 'Chronar', 'Radiantae', 'Etherion' ]; const MEGASEQUENCE_NAMES = [ 'Azurean Tide', 'Sable Gleam', 'Verdanth Starfall', 'Crimson Dusk', 'Cobalt Frost', 'Amber Blaze', 'Viridian Bloom', 'Argent Veil', 'Helian Rise', 'Nocturne Shade', 'Celestine Aura', 'Pyralis Light', 'Zephyrine Whisper', 'Lustran Bounty', 'Umbral Echo', 'Mythran Epoch' ]; let currentOffset = 0; let currentTimezone = 'UTC'; let manualMode = false; let manualCobiets = 0; let compareManualMode = false; let compareCobiets = 0; let updateInterval; let lastRenderedEonstrip = null; function fmt(d, o) { // shift if TAI, then format const shifted = currentTimezone==='TAI' ? new Date(d.getTime() + getTAIOffsetAt(d)*1000) : d; return shifted.toLocaleString('en-US', o); } 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')}`; } } function parseCobiets(str) { const m = /^([+-]?)([0-9A-Fa-f]+)\.([0-9A-Fa-f]{1,})$/.exec(str.trim()); if (!m) return null; const sign = m[1] === '-' ? -1 : 1; // All date‐units in descending “size” order, exactly as in COBIE_UNITS const allDateKeys = [ 'astralmillennia', 'infinitum', 'eternum', 'metaepoch', 'cosmic_aeon', 'epoch_of_cosmos', 'celestial_era', 'universal_eon', 'galactic_year', 'cosmocycle', 'megasequence', 'eonstrip' ]; // Raw hex‐string on the left of “.” let rawDateHex = m[2]; // Pad on the left with zeros if shorter than the number of allDateKeys if (rawDateHex.length < allDateKeys.length) { rawDateHex = rawDateHex.padStart(allDateKeys.length, '0'); } // If user provided MORE hex‐digits than allDateKeys.length, we treat each extra // digit as an even higher‐order “super‐unit” whose weight is: // 16^( position_from_right_of_eonstrip + 1 ) * COBIE_UNITS['eonstrip'] let dateKeys = [...allDateKeys]; if (rawDateHex.length > allDateKeys.length) { const extraCount = rawDateHex.length - allDateKeys.length; for (let i = 0; i < extraCount; i++) { // Insert null placeholders at the front for “beyond astralmillennia” dateKeys.unshift(null); } } // Time portion (always exactly 4 hex digits → chronon, quantic, xenocycle, second) const timeHexRaw = m[3]; const timeHex = timeHexRaw.padStart(4, '0'); const timeKeys = ['chronon', 'quantic', 'xenocycle', 'second']; let total = 0; // 1) Process every date‐hex digit for (let i = 0; i < rawDateHex.length; i++) { const digit = parseInt(rawDateHex[i], 16); const key = dateKeys[i]; if (key === null) { // “Super‐unit” weight = (16 ^ power) * (size of eonstrip), // where power = (#digits_left_of_eonstrip) − (index_of_this_digit) − 1 const power = rawDateHex.length - 1 - i; total += digit * Math.pow(16, power) * COBIE_UNITS['eonstrip']; } else { total += digit * COBIE_UNITS[key]; } } // 2) Add time‐portion timeHex.split('').forEach((h, i) => { total += parseInt(h, 16) * COBIE_UNITS[timeKeys[i]]; }); return sign * total; } function floorDiv(a, b) { return Math.trunc(a / b); } function getCurrentTAIOffset() { return getTAIOffsetAt(new Date()); } function getHumanDiff(d1, d2) { // make sure start ≤ end let start = d1 < d2 ? d1 : d2; let end = d1 < d2 ? d2 : d1; // 1) year/month/day difference let years = end.getUTCFullYear() - start.getUTCFullYear(); let months = end.getUTCMonth() - start.getUTCMonth(); let days = end.getUTCDate() - start.getUTCDate(); // if day roll-under, borrow from month if (days < 0) { months--; // days in the month *before* `end`’s month: let prevMonthDays = new Date(Date.UTC( end.getUTCFullYear(), end.getUTCMonth(), 0 )).getUTCDate(); days += prevMonthDays; } // if month roll-under, borrow from year if (months < 0) { years--; months += 12; } // 2) now handle hours/min/sec by “aligning” a Date at start+Y/M/D let aligned = new Date(Date.UTC( start.getUTCFullYear() + years, start.getUTCMonth() + months, start.getUTCDate() + days, start.getUTCHours(), start.getUTCMinutes(), start.getUTCSeconds() )); let diffMs = end.getTime() - aligned.getTime(); // if we overshot (negative), borrow one day if (diffMs < 0) { // borrow 24 h diffMs += 24 * 3600e3; if (days > 0) { days--; } else { // days was zero, so borrow a month months--; if (months < 0) { years--; months += 12; } // set days to length of the previous month of `end` days = new Date(Date.UTC( end.getUTCFullYear(), end.getUTCMonth(), 0 )).getUTCDate(); } } // 3) extract h/m/s let hours = Math.floor(diffMs / 3600e3); diffMs -= hours * 3600e3; let minutes = Math.floor(diffMs / 60e3); diffMs -= minutes * 60e3; let seconds = Math.floor(diffMs / 1e3); return { years, months, days, hours, minutes, seconds }; } function getTAIOffsetAt(date) { const taiEpoch = new Date('1958-01-01T00:00:00Z'); if (date < taiEpoch) { return 0; } const leapSeconds = [ { date: '1972-01-01T00:00:00Z', offset: 10 }, { date: '1972-07-01T00:00:00Z', offset: 11 }, { date: '1973-01-01T00:00:00Z', offset: 12 }, { date: '1974-01-01T00:00:00Z', offset: 13 }, { date: '1975-01-01T00:00:00Z', offset: 14 }, { date: '1976-01-01T00:00:00Z', offset: 15 }, { date: '1977-01-01T00:00:00Z', offset: 16 }, { date: '1978-01-01T00:00:00Z', offset: 17 }, { date: '1979-01-01T00:00:00Z', offset: 18 }, { date: '1980-01-01T00:00:00Z', offset: 19 }, { date: '1981-07-01T00:00:00Z', offset: 20 }, { date: '1982-07-01T00:00:00Z', offset: 21 }, { date: '1983-07-01T00:00:00Z', offset: 22 }, { date: '1985-07-01T00:00:00Z', offset: 23 }, { date: '1988-01-01T00:00:00Z', offset: 24 }, { date: '1990-01-01T00:00:00Z', offset: 25 }, { date: '1991-01-01T00:00:00Z', offset: 26 }, { date: '1992-07-01T00:00:00Z', offset: 27 }, { date: '1993-07-01T00:00:00Z', offset: 28 }, { date: '1994-07-01T00:00:00Z', offset: 29 }, { date: '1996-01-01T00:00:00Z', offset: 30 }, { date: '1997-07-01T00:00:00Z', offset: 31 }, { date: '1999-01-01T00:00:00Z', offset: 32 }, { date: '2006-01-01T00:00:00Z', offset: 33 }, { date: '2009-01-01T00:00:00Z', offset: 34 }, { date: '2012-07-01T00:00:00Z', offset: 35 }, { date: '2015-07-01T00:00:00Z', offset: 36 }, { date: '2017-01-01T00:00:00Z', offset: 37 } ]; for (let i = 0; i < leapSeconds.length; i++) { const leapDate = new Date(leapSeconds[i].date); if (date < leapDate) { return i === 0 ? 10 : leapSeconds[i - 1].offset; } } return 37; // Most recent known offset } function toCobiets(date) { const utcSec = floorDiv(date.getTime(), 1000); const taiSec = utcSec + getTAIOffsetAt(date); return taiSec - COBIE_EPOCH; } function fromCobiets(cobiets) { const taiSeconds = cobiets + COBIE_EPOCH; const taiMs = taiSeconds * 1000; let utcMs = taiMs for (let i = 0; i < 3; i++) { const off = getTAIOffsetAt(new Date(utcMs)); utcMs = taiMs - off * 1000; } return new Date(utcMs); } const UNIT_KEYS = [ 'astralmillennia', 'infinitum', 'eternum', 'metaepoch', 'cosmic_aeon', 'epoch_of_cosmos', 'celestial_era', 'universal_eon', 'galactic_year', 'cosmocycle', 'megasequence', 'eonstrip', 'chronon', 'quantic', 'xenocycle', 'second' ]; function breakdownNonNeg(cob) { let rem = cob, bd = {}; for (let key of UNIT_KEYS) { bd[key] = floorDiv(rem, COBIE_UNITS[key]); rem %= COBIE_UNITS[key]; } return bd; } function formatCobieTimestamp(cobiets) { const sign = cobiets < 0 ? '-' : '+'; const absCob = Math.abs(cobiets); const bd = breakdownNonNeg(absCob); // All date‐units in descending “size” order const dateUnits = [ 'astralmillennia', 'infinitum', 'eternum', 'metaepoch', 'cosmic_aeon', 'epoch_of_cosmos', 'celestial_era', 'universal_eon', 'galactic_year', 'cosmocycle', 'megasequence', 'eonstrip' ]; // Build a raw hex‐string by concatenating each unit’s hex let rawDateHex = dateUnits .map(key => bd[key].toString(16)) .join(''); // Trim leading zeros but leave at least one “0” if everything is zero rawDateHex = rawDateHex.replace(/^0+/, ''); if (rawDateHex === '') rawDateHex = '0'; // Time portion: exactly 4 hex digits (chronon → second) const timeHex = [ bd.chronon, bd.quantic, bd.xenocycle, bd.second ].map(n => n.toString(16)).join(''); const paddedTimeHex = timeHex.padStart(4, '0'); return sign + rawDateHex + '.' + paddedTimeHex; } 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 = { timeZone: currentTimezone === 'TAI' ? 'UTC' : currentTimezone, weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }; 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); options.timeZone = 'UTC'; const taiDate = new Date(baseDate.getTime() + taiOffset * 1000); document.getElementById('taiTime').textContent = 'TAI UTC: ' + taiDate.toLocaleString('en-US', options) + ' (UTC + ' + taiOffset + 's)'; const bd = breakdownNonNeg(Math.abs(cobiets)); const currentStrip = bd.eonstrip; if (currentStrip !== lastRenderedEonstrip) { updateCalendar(); lastRenderedEonstrip = currentStrip; } else { updateTimeBreakdown(cobiets); } } function updateTimeBreakdown(cobiets) { // 1) Break down absolute CoBiE seconds into each unit const breakdown = breakdownNonNeg(Math.abs(cobiets)); // 2) Compute the “start” CoBiE‐seconds 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 dateOptions = { timeZone: currentTimezone === 'TAI' ? 'UTC' : currentTimezone, weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }; // // ── Build the “core” units (always visible): Galactic Year → Second ────────────── // const coreHtml = `
Galactic Year 0x${breakdown.galactic_year.toString(16).toUpperCase()} (${breakdown.galactic_year})
${(() => { const rawStart = fromCobiets(gyrStart); const rawEnd = fromCobiets(gyrEnd); return ` Started: ${formatSafeDate(rawStart, gyrStart, dateOptions)}
Ends: ${formatSafeDate(rawEnd, gyrEnd, dateOptions)} `; })()}
Cosmocycle 0x${breakdown.cosmocycle.toString(16).toUpperCase()} (${breakdown.cosmocycle})
${(() => { const rawStart = fromCobiets(ccyStart); const rawEnd = fromCobiets(ccyEnd); return ` Started: ${formatSafeDate(rawStart, ccyStart, dateOptions)}
Ends: ${formatSafeDate(rawEnd, ccyEnd, dateOptions)} `; })()}
Megasequence 0x${breakdown.megasequence.toString(16).toUpperCase()} (${breakdown.megasequence}) – ${MEGASEQUENCE_NAMES[breakdown.megasequence] || 'Unknown'}
${(() => { const rawStart = fromCobiets(mqsStart); const rawEnd = fromCobiets(mqsEnd); return ` Started: ${formatSafeDate(rawStart, mqsStart, dateOptions)}
Ends: ${formatSafeDate(rawEnd, mqsEnd, dateOptions)} `; })()}
Eonstrip 0x${breakdown.eonstrip.toString(16).toUpperCase()} (${breakdown.eonstrip}) – ${EONSTRIP_NAMES[breakdown.eonstrip]}
${(() => { const rawStart = fromCobiets(eosStart); const rawEnd = fromCobiets(eosEnd); return ` Started: ${formatSafeDate(rawStart, eosStart, dateOptions)}
Ends: ${formatSafeDate(rawEnd, eosEnd, dateOptions)} `; })()}
Chronon 0x${breakdown.chronon.toString(16).toUpperCase()} (${breakdown.chronon})
Quantic 0x${breakdown.quantic.toString(16).toUpperCase()} (${breakdown.quantic})
Xenocycle 0x${breakdown.xenocycle.toString(16).toUpperCase()} (${breakdown.xenocycle})
Second 0x${breakdown.second.toString(16).toUpperCase()} (${breakdown.second})
`; document.getElementById('coreBreakdown').innerHTML = coreHtml; // // ── Build the “extended” units (hidden by default) in order: Astralmillennia → Universal Eon ─── // const extendedHtml = `
Astralmillennia 0x${breakdown.astralmillennia.toString(16).toUpperCase()} (${breakdown.astralmillennia})
${(() => { const startAmt = breakdown.astralmillennia * COBIE_UNITS.astralmillennia; const endAmt = startAmt + (COBIE_UNITS.astralmillennia - 1); const rawStart = fromCobiets(startAmt); const rawEnd = fromCobiets(endAmt); return ` Started: ${formatSafeDate(rawStart, startAmt, dateOptions)}
Ends: ${formatSafeDate(rawEnd, endAmt, dateOptions)} `; })()}
Infinitum 0x${breakdown.infinitum.toString(16).toUpperCase()} (${breakdown.infinitum})
${(() => { const startAmt = breakdown.infinitum * COBIE_UNITS.infinitum; const endAmt = startAmt + (COBIE_UNITS.infinitum - 1); const rawStart = fromCobiets(startAmt); const rawEnd = fromCobiets(endAmt); return ` Started: ${formatSafeDate(rawStart, startAmt, dateOptions)}
Ends: ${formatSafeDate(rawEnd, endAmt, dateOptions)} `; })()}
Eternum 0x${breakdown.eternum.toString(16).toUpperCase()} (${breakdown.eternum})
${(() => { const startAmt = breakdown.eternum * COBIE_UNITS.eternum; const endAmt = startAmt + (COBIE_UNITS.eternum - 1); const rawStart = fromCobiets(startAmt); const rawEnd = fromCobiets(endAmt); return ` Started: ${formatSafeDate(rawStart, startAmt, dateOptions)}
Ends: ${formatSafeDate(rawEnd, endAmt, dateOptions)} `; })()}
Metaepoch 0x${breakdown.metaepoch.toString(16).toUpperCase()} (${breakdown.metaepoch})
${(() => { const startAmt = breakdown.metaepoch * COBIE_UNITS.metaepoch; const endAmt = startAmt + (COBIE_UNITS.metaepoch - 1); const rawStart = fromCobiets(startAmt); const rawEnd = fromCobiets(endAmt); return ` Started: ${formatSafeDate(rawStart, startAmt, dateOptions)}
Ends: ${formatSafeDate(rawEnd, endAmt, dateOptions)} `; })()}
Cosmic Aeon 0x${breakdown.cosmic_aeon.toString(16).toUpperCase()} (${breakdown.cosmic_aeon})
${(() => { 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 ` Started: ${formatSafeDate(rawStart, startAmt, dateOptions)}
Ends: ${formatSafeDate(rawEnd, endAmt, dateOptions)} `; })()}
Epoch of Cosmos 0x${breakdown.epoch_of_cosmos.toString(16).toUpperCase()} (${breakdown.epoch_of_cosmos})
${(() => { const rawStart = fromCobiets(eocStart); const rawEnd = fromCobiets(eocEnd); return ` Started: ${formatSafeDate(rawStart, eocStart, dateOptions)}
Ends: ${formatSafeDate(rawEnd, eocEnd, dateOptions)} `; })()}
Celestial Era 0x${breakdown.celestial_era.toString(16).toUpperCase()} (${breakdown.celestial_era})
${(() => { const rawStart = fromCobiets(cerStart); const rawEnd = fromCobiets(cerEnd); return ` Started: ${formatSafeDate(rawStart, cerStart, dateOptions)}
Ends: ${formatSafeDate(rawEnd, cerEnd, dateOptions)} `; })()}
Universal Eon 0x${breakdown.universal_eon.toString(16).toUpperCase()} (${breakdown.universal_eon})
${(() => { const rawStart = fromCobiets(ueoStart); const rawEnd = fromCobiets(ueoEnd); return ` Started: ${formatSafeDate(rawStart, ueoStart, dateOptions)}
Ends: ${formatSafeDate(rawEnd, ueoEnd, dateOptions)} `; })()}
`; 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 = `
Compare ${formatCobieTimestamp(baseCob)}
Difference (CoBiE) ${formatCobieTimestamp(diffCob)}
Difference ${human.years}y ${human.months}m ${human.days}d ${human.hours}h ${human.minutes}m ${human.seconds}s
`; const extDiv = document.getElementById('extendedBreakdown'); const coreDiv = document.getElementById('coreBreakdown'); if (extDiv.style.display === 'block') { extDiv.insertAdjacentHTML('beforeend', diffHtml); } else { coreDiv.insertAdjacentHTML('beforeend', diffHtml); } // Re‐attach the “edit‐on‐click” 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 = { timeZone: currentTimezone==='TAI' ? 'UTC' : currentTimezone, year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: 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 = `
${EONSTRIP_NAMES[i]}
${sign}${cellBd.galactic_year.toString(16)}${cellBd.cosmocycle.toString(16)}${cellBd.megasequence.toString(16)}${i.toString(16)}
${startDate.toLocaleDateString('en-US', dateOpts)}
`; if (Array.isArray(window.SPECIAL_EVENTS)) { const offsetStart = ((cellCob % COBIE_UNITS.cosmocycle) + COBIE_UNITS.cosmocycle) % COBIE_UNITS.cosmocycle; const offsetEnd = offsetStart + COBIE_UNITS.eonstrip; window.SPECIAL_EVENTS.forEach(ev => { const evCob = parseCobiets(ev.cobie); if (evCob === null) return; const evOffset = ((evCob % COBIE_UNITS.cosmocycle) + COBIE_UNITS.cosmocycle) % COBIE_UNITS.cosmocycle; if (evOffset >= offsetStart && evOffset < offsetEnd) { const tag = document.createElement('div'); tag.className = 'event-tag'; tag.textContent = ev.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) { card.addEventListener('click', () => { currentOffset = 0; manualMode = true; manualCobiets = cob; clearInterval(updateInterval); document.querySelector('.current-time').classList.add('manual'); updateCurrentTime(); }); })(cellCob + currentTime); } updateTimeBreakdown(currentCob); } function showEonstripDetails(index, startCobiets, opts) { const startDate = fromCobiets(startCobiets); const endDate = fromCobiets(startCobiets + COBIE_UNITS.eonstrip - 1); const options = opts || { timeZone: currentTimezone === 'TAI' ? 'UTC' : currentTimezone, weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }; return ` ${EONSTRIP_NAMES[index]} (0x${index.toString(16).toUpperCase()})
Start: ${startDate.toLocaleString('en-US', options)}
End: ${endDate.toLocaleString('en-US', options)}
Duration: ${COBIE_UNITS.eonstrip} sec (~${(COBIE_UNITS.eonstrip/3600).toFixed(2)} h) `; } function navigatePeriod(evt, direction) { // base step = 1 megasequence let step = 1; if (evt.altKey && evt.shiftKey && evt.ctrlKey) { // Epoch of Cosmos = 16⁵ MS step = 16**5; } else if (evt.altKey && evt.ctrlKey) { // Celestial Era = 16⁴ MS step = 16**4; } else if (evt.altKey && evt.shiftKey) { // Universal Eon = 16³ MS step = 16**3; } else if (evt.altKey) { // Galactic Year = 16² MS step = 16**2; } else if (evt.shiftKey) { // Cosmocycle = 16¹ MS step = 16**1; } animateSwipe(direction, () => { currentOffset += direction * step; updateCalendar(); }); } function animateSwipe(direction, onDone) { const grid = document.getElementById('eonstripGrid'); if (!grid) { onDone(); return; } // slide out grid.style.transition = 'transform 0.3s ease'; grid.style.transform = `translateX(${direction > 0 ? '-100%' : '100%'})`; function afterOut() { grid.removeEventListener('transitionend', afterOut); // prepare new position off-screen on the other side grid.style.transition = 'none'; grid.style.transform = `translateX(${direction > 0 ? '100%' : '-100%'})`; onDone(); // force reflow to apply position instantly void grid.offsetWidth; // slide in with transition grid.style.transition = 'transform 0.3s ease'; grid.style.transform = 'translateX(0)'; } grid.addEventListener('transitionend', afterOut); } function goToNow() { manualMode = false; manualCobiets = 0; compareManualMode = false; currentOffset = 0; updateCurrentTime(); updateCalendar(); clearInterval(updateInterval); updateInterval = setInterval(updateCurrentTime, 1000); document.querySelector('.current-time').classList.remove('manual'); } 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 = '12ch'; 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'); } // swap elements input.replaceWith(span); span.addEventListener('click', enterEdit); } // Timezone change handler document.getElementById('timezone').addEventListener('change', (e) => { currentTimezone = e.target.value; updateCurrentTime(); updateCalendar(); }); // 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; } 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; function swipeStart(e) { const touch = e.touches ? e.touches[0] : e; swipeStartX = touch.clientX; swipeStartY = touch.clientY; } function swipeEnd(e) { if (swipeStartX === null || swipeStartY === null) return; const touch = e.changedTouches ? e.changedTouches[0] : e; const dx = touch.clientX - swipeStartX; const dy = touch.clientY - swipeStartY; if (Math.abs(dx) > 40 && Math.abs(dx) > Math.abs(dy)) { const direction = dx < 0 ? 1 : -1; // left → next, right → prev navigatePeriod({ altKey: e.altKey || false, shiftKey: e.shiftKey || false, ctrlKey: e.ctrlKey || false }, direction); } swipeStartX = swipeStartY = null; } document.addEventListener('touchstart', swipeStart, {passive: true}); document.addEventListener('touchend', swipeEnd); document.addEventListener('mousedown', swipeStart); 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; navigatePeriod(e, direction); } } document.addEventListener('wheel', wheelNavigate);