Compare commits

...

5 Commits

Author SHA1 Message Date
Oleksandr Kozachuk 38e16687cb Make hands movement smooth on analag clock. 2025-07-10 14:37:56 +02:00
Kiyomichi Kosaka 2ade28e9cd Merge pull request #81 from ok2/codex/refactor-code-into-separate-files
Refactor JS into modules
2025-06-20 16:25:41 +02:00
Kiyomichi Kosaka a9b90729ff Refactor scripts into modules 2025-06-20 16:25:21 +02:00
Kiyomichi Kosaka ce17d2d5bf Merge pull request #80 from ok2/codex/refactor-code-to-reduce-size
Refactor JS helpers
2025-06-20 16:06:08 +02:00
Kiyomichi Kosaka 8f570ca4d3 refactor: reduce duplication via helpers 2025-06-20 16:05:51 +02:00
8 changed files with 318 additions and 358 deletions
+6 -4
View File
@@ -54,13 +54,15 @@ An interactive web app that visualizes the **CosmoChron Binary Epoch (CoBiE)** t
``` ```
├── index.html # Main HTML markup ├── index.html # Main HTML markup
├── analog.html # Analog clock interface
├── clock.js # Clock logic ├── clock.js # Clock logic
├── cobie.js # CoBiE time system utilities
├── utils.js # Generic helper functions
├── animate.js # Shared animations
├── events.js # Sample calendar events
├── style.css # Separated styles ├── style.css # Separated styles
├── script.js # JavaScript logic ├── script.js # Page interactions
├── README.md # This documentation ├── README.md # This documentation
└── assets/ # (Optional) images or external CSS/JS └── test/ # Unit tests
``` ```
## macOS Widget ## macOS Widget
+83
View File
@@ -0,0 +1,83 @@
(function(){
const lastAngles = {
handXeno: 0,
handQuantic: 0,
handChronon: 0,
handEonstrip: 0,
handMegasequence: 0
};
function rotateHand(id, angle) {
const el = document.getElementById(id);
if (!el) return;
const prev = lastAngles[id];
if (angle < prev) {
const target = angle + 360;
const handle = () => {
el.removeEventListener('transitionend', handle);
el.style.transition = 'none';
el.style.transform = `translateX(-50%) translateZ(0) rotate(${angle}deg)`;
void el.offsetWidth;
el.style.transition = '';
};
el.addEventListener('transitionend', handle, { once: true });
el.style.transform = `translateX(-50%) translateZ(0) rotate(${target}deg)`;
} else {
el.style.transform = `translateX(-50%) translateZ(0) rotate(${angle}deg)`;
}
lastAngles[id] = angle;
}
function animateSwipe(direction, onDone) {
const grid = document.getElementById('eonstripGrid');
if (!grid) { onDone(); return; }
grid.style.transition = 'none';
grid.style.transform = 'translateX(0)';
void grid.offsetWidth;
grid.style.transition = 'transform 0.3s ease';
grid.style.transform = `translateX(${direction > 0 ? '-100%' : '100%'})`;
function afterOut() {
grid.removeEventListener('transitionend', afterOut);
grid.style.transition = 'none';
grid.style.transform = `translateX(${direction > 0 ? '100%' : '-100%'})`;
onDone();
void grid.offsetWidth;
grid.style.transition = 'transform 0.3s ease';
grid.style.transform = 'translateX(0)';
}
grid.addEventListener('transitionend', afterOut);
}
function animateDetailSwipe(direction, onDone) {
const tl = document.getElementById('detailTimeline');
if (!tl) { onDone(); return; }
tl.style.transition = 'none';
tl.style.transform = 'translateX(0)';
void tl.offsetWidth;
tl.style.transition = 'transform 0.3s ease';
tl.style.transform = `translateX(${direction > 0 ? '-100%' : '100%'})`;
function afterOut() {
tl.removeEventListener('transitionend', afterOut);
tl.style.transition = 'none';
tl.style.transform = `translateX(${direction > 0 ? '100%' : '-100%'})`;
onDone();
void tl.offsetWidth;
tl.style.transition = 'transform 0.3s ease';
tl.style.transform = 'translateX(0)';
}
tl.addEventListener('transitionend', afterOut);
}
window.Animate = {
rotateHand,
animateSwipe,
animateDetailSwipe
};
})();
+5 -38
View File
@@ -114,39 +114,6 @@
}); });
} }
const lastAngles = {
handXeno: 0,
handQuantic: 0,
handChronon: 0,
handEonstrip: 0,
handMegasequence: 0
};
function rotateHand(id, angle) {
const el = document.getElementById(id);
if (!el) return;
const prev = lastAngles[id];
if (angle < prev) {
// When wrapping around (e.g. 15 → 0), animate to one full turn
// and then snap back to the new angle to avoid a jump.
const target = angle + 360;
const handle = () => {
el.removeEventListener('transitionend', handle);
// Snap back without animation
el.style.transition = 'none';
el.style.transform = `translateX(-50%) translateZ(0) rotate(${angle}deg)`;
void el.offsetWidth;
el.style.transition = '';
};
el.addEventListener('transitionend', handle, { once: true });
el.style.transform = `translateX(-50%) translateZ(0) rotate(${target}deg)`;
} else {
el.style.transform = `translateX(-50%) translateZ(0) rotate(${angle}deg)`;
}
lastAngles[id] = angle;
}
function renderClock(cob) { function renderClock(cob) {
// Use fractional progress within each unit so angles stay small // Use fractional progress within each unit so angles stay small
@@ -155,11 +122,11 @@
const cf = (cob % COBIE_UNITS.eonstrip) / COBIE_UNITS.eonstrip; const cf = (cob % COBIE_UNITS.eonstrip) / COBIE_UNITS.eonstrip;
const ef = (cob % COBIE_UNITS.megasequence) / COBIE_UNITS.megasequence; const ef = (cob % COBIE_UNITS.megasequence) / COBIE_UNITS.megasequence;
const mf = (cob % COBIE_UNITS.cosmocycle) / COBIE_UNITS.cosmocycle; const mf = (cob % COBIE_UNITS.cosmocycle) / COBIE_UNITS.cosmocycle;
rotateHand('handXeno', xf * 360); Animate.rotateHand('handXeno', xf * 360);
rotateHand('handQuantic', qf * 360); Animate.rotateHand('handQuantic', qf * 360);
rotateHand('handChronon', cf * 360); Animate.rotateHand('handChronon', cf * 360);
rotateHand('handEonstrip', ef * 360); Animate.rotateHand('handEonstrip', ef * 360);
rotateHand('handMegasequence', mf * 360); Animate.rotateHand('handMegasequence', mf * 360);
} }
function updateClock() { function updateClock() {
+17 -1
View File
@@ -19,6 +19,20 @@ const COBIE_UNITS = {
astralmillennia: 0x1000000000000000 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'
];
function floorDiv(a, b) { function floorDiv(a, b) {
return Math.trunc(a / b); return Math.trunc(a / b);
} }
@@ -172,7 +186,9 @@ const Cobie = {
toCobiets, toCobiets,
fromCobiets, fromCobiets,
formatCobieTimestamp, formatCobieTimestamp,
breakdownNonNeg breakdownNonNeg,
EONSTRIP_NAMES,
MEGASEQUENCE_NAMES
}; };
if (typeof module !== 'undefined' && module.exports) { if (typeof module !== 'undefined' && module.exports) {
+2
View File
@@ -167,6 +167,8 @@
</div> </div>
<script src="cobie.js"></script> <script src="cobie.js"></script>
<script src="utils.js"></script>
<script src="animate.js"></script>
<script src="events.js"></script> <script src="events.js"></script>
<script src="script.js"></script> <script src="script.js"></script>
<script src="clock.js"></script> <script src="clock.js"></script>
+104 -306
View File
@@ -20,19 +20,7 @@ const {
breakdownNonNeg breakdownNonNeg
} = window.Cobie; } = window.Cobie;
const EONSTRIP_NAMES = [ const { EONSTRIP_NAMES, MEGASEQUENCE_NAMES } = window.Cobie;
'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 currentOffset = 0;
let currentTimezone = 'UTC'; let currentTimezone = 'UTC';
@@ -44,41 +32,26 @@ let updateInterval;
let lastRenderedEonstrip = null; let lastRenderedEonstrip = null;
let currentDetailCob = null; let currentDetailCob = null;
function hexToRgba(hex, alpha) { // ── Utility color helpers (in utils.js) ───────────────────────────────────
if (!hex) return ''; const {
let c = hex.replace('#', ''); parseColor,
if (c.length === 3) { hexToRgba,
c = c.split('').map(x => x + x).join(''); getContrastColor,
} lightenColor,
const r = parseInt(c.substring(0,2),16); getHumanDiff
const g = parseInt(c.substring(2,4),16); } = window.Utils;
const b = parseInt(c.substring(4,6),16);
return `rgba(${r},${g},${b},${alpha})`;
}
function getContrastColor(hex) { const dateOptions = (long = true) => ({
if (!hex) return '#fff'; timeZone: currentTimezone === 'TAI' ? 'UTC' : currentTimezone,
let c = hex.replace('#',''); weekday: 'long',
if (c.length === 3) c = c.split('').map(x=>x+x).join(''); year: 'numeric',
const r = parseInt(c.substr(0,2),16); month: long ? 'long' : 'short',
const g = parseInt(c.substr(2,2),16); day: 'numeric',
const b = parseInt(c.substr(4,2),16); hour: '2-digit',
const yiq = (r*299 + g*587 + b*114) / 1000; minute: '2-digit',
return yiq >= 128 ? '#000' : '#fff'; second: '2-digit',
} hour12: false
});
function lightenColor(hex, percent) {
if (!hex) return '#fff';
let c = hex.replace('#','');
if (c.length === 3) c = c.split('').map(x=>x+x).join('');
let r = parseInt(c.substr(0,2),16);
let g = parseInt(c.substr(2,2),16);
let b = parseInt(c.substr(4,2),16);
r = Math.min(255, Math.round(r + (255 - r) * percent));
g = Math.min(255, Math.round(g + (255 - g) * percent));
b = Math.min(255, Math.round(b + (255 - b) * percent));
return '#' + [r,g,b].map(x=>x.toString(16).padStart(2,'0')).join('');
}
function applyEventColors(elem, color, alpha) { function applyEventColors(elem, color, alpha) {
if (!color || !elem) return; if (!color || !elem) return;
@@ -125,70 +98,45 @@ function formatSafeDate(rawDate, cobSeconds, intlOptions) {
// parseCobiets, floorDiv and other CoBiE helpers are provided by cobie.js // parseCobiets, floorDiv and other CoBiE helpers are provided by cobie.js
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 // ── Event utilities ──────────────────────────────────────────────────────
if (days < 0) { const normalizeEvent = ev => {
months--; const baseStart = parseCobiets(ev.start || ev.cobie);
// days in the month *before* `end`s month: if (baseStart === null) return null;
let prevMonthDays = new Date(Date.UTC( const tzShift = ev.shiftWithTimezone ?
end.getUTCFullYear(), getTimezoneOffsetSeconds(fromCobiets(baseStart)) : 0;
end.getUTCMonth(), 0 const startCob = baseStart - tzShift;
)).getUTCDate(); const endCob = ev.end ? parseCobiets(ev.end) - tzShift : Number.POSITIVE_INFINITY;
days += prevMonthDays; const unitVal = COBIE_UNITS[ev.unit] || COBIE_UNITS.cosmocycle;
} const interval = (ev.interval || 1) * unitVal;
// if month roll-under, borrow from year let duration = 0;
if (months < 0) { if (typeof ev.duration === 'string') {
years--; const d = parseCobiets(ev.duration);
months += 12; if (d !== null) duration = d;
} else if (typeof ev.duration === 'number') {
duration = ev.duration;
} }
return { startCob, endCob, interval, duration };
};
// 2) now handle hours/min/sec by “aligning” a Date at start+Y/M/D function collectEventOccurrences(start, end, predicate = () => true) {
let aligned = new Date(Date.UTC( const out = [];
start.getUTCFullYear() + years, if (!Array.isArray(window.SPECIAL_EVENTS)) return out;
start.getUTCMonth() + months, window.SPECIAL_EVENTS.forEach(ev => {
start.getUTCDate() + days, if (!predicate(ev)) return;
start.getUTCHours(), const meta = normalizeEvent(ev);
start.getUTCMinutes(), if (!meta || start > meta.endCob) return;
start.getUTCSeconds() let n = Math.floor((start - meta.startCob) / meta.interval);
)); if (n < 0) n = 0;
let diffMs = end.getTime() - aligned.getTime(); let occ = meta.startCob + n * meta.interval;
if (occ + meta.duration <= start) occ += meta.interval;
// if we overshot (negative), borrow one day while (occ < end && occ <= meta.endCob) {
if (diffMs < 0) { out.push({ event: ev, meta, occ });
// borrow 24 h occ += meta.interval;
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( return out;
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 };
} }
// getTAIOffsetAt, toCobiets, fromCobiets, breakdownNonNeg and // getTAIOffsetAt, toCobiets, fromCobiets, breakdownNonNeg and
@@ -206,17 +154,7 @@ function updateCurrentTime() {
const cobieElem = document.getElementById('cobieTime'); const cobieElem = document.getElementById('cobieTime');
if (cobieElem) cobieElem.textContent = formatCobieTimestamp(cobiets); if (cobieElem) cobieElem.textContent = formatCobieTimestamp(cobiets);
const options = { const options = 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
};
const taiOffset = getTAIOffsetAt(baseDate); const taiOffset = getTAIOffsetAt(baseDate);
let displayDate = baseDate; let displayDate = baseDate;
@@ -226,9 +164,9 @@ function updateCurrentTime() {
document.getElementById('regularTime').textContent = currentTimezone + ': ' + displayDate.toLocaleString('en-US', options); document.getElementById('regularTime').textContent = currentTimezone + ': ' + displayDate.toLocaleString('en-US', options);
options.timeZone = 'UTC'; const optionsUTC = { ...options, timeZone: 'UTC' };
const taiDate = new Date(baseDate.getTime() + taiOffset * 1000); const taiDate = new Date(baseDate.getTime() + taiOffset * 1000);
document.getElementById('taiTime').textContent = 'TAI UTC: ' + taiDate.toLocaleString('en-US', options) + ' (UTC + ' + taiOffset + 's)'; document.getElementById('taiTime').textContent = 'TAI UTC: ' + taiDate.toLocaleString('en-US', optionsUTC) + ' (UTC + ' + taiOffset + 's)';
const bd = breakdownNonNeg(Math.abs(cobiets)); const bd = breakdownNonNeg(Math.abs(cobiets));
@@ -265,17 +203,7 @@ function updateTimeBreakdown(cobiets) {
const eosEnd = eosStart + COBIE_UNITS.eonstrip - 1; const eosEnd = eosStart + COBIE_UNITS.eonstrip - 1;
// 4) Intl formatting options // 4) Intl formatting options
const dateOptions = { const optsLong = 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 ────────────── // ── Build the “core” units (always visible): Galactic Year → Second ──────────────
@@ -292,8 +220,8 @@ function updateTimeBreakdown(cobiets) {
const rawStart = fromCobiets(gyrStart); const rawStart = fromCobiets(gyrStart);
const rawEnd = fromCobiets(gyrEnd); const rawEnd = fromCobiets(gyrEnd);
return ` return `
<span>Started: ${formatSafeDate(rawStart, gyrStart, dateOptions)}</span><br> <span>Started: ${formatSafeDate(rawStart, gyrStart, optsLong)}</span><br>
<span>Ends: ${formatSafeDate(rawEnd, gyrEnd, dateOptions)}</span> <span>Ends: ${formatSafeDate(rawEnd, gyrEnd, optsLong)}</span>
`; `;
})()} })()}
</div> </div>
@@ -309,8 +237,8 @@ function updateTimeBreakdown(cobiets) {
const rawStart = fromCobiets(ccyStart); const rawStart = fromCobiets(ccyStart);
const rawEnd = fromCobiets(ccyEnd); const rawEnd = fromCobiets(ccyEnd);
return ` return `
<span>Started: ${formatSafeDate(rawStart, ccyStart, dateOptions)}</span><br> <span>Started: ${formatSafeDate(rawStart, ccyStart, optsLong)}</span><br>
<span>Ends: ${formatSafeDate(rawEnd, ccyEnd, dateOptions)}</span> <span>Ends: ${formatSafeDate(rawEnd, ccyEnd, optsLong)}</span>
`; `;
})()} })()}
</div> </div>
@@ -326,8 +254,8 @@ function updateTimeBreakdown(cobiets) {
const rawStart = fromCobiets(mqsStart); const rawStart = fromCobiets(mqsStart);
const rawEnd = fromCobiets(mqsEnd); const rawEnd = fromCobiets(mqsEnd);
return ` return `
<span>Started: ${formatSafeDate(rawStart, mqsStart, dateOptions)}</span><br> <span>Started: ${formatSafeDate(rawStart, mqsStart, optsLong)}</span><br>
<span>Ends: ${formatSafeDate(rawEnd, mqsEnd, dateOptions)}</span> <span>Ends: ${formatSafeDate(rawEnd, mqsEnd, optsLong)}</span>
`; `;
})()} })()}
</div> </div>
@@ -343,8 +271,8 @@ function updateTimeBreakdown(cobiets) {
const rawStart = fromCobiets(eosStart); const rawStart = fromCobiets(eosStart);
const rawEnd = fromCobiets(eosEnd); const rawEnd = fromCobiets(eosEnd);
return ` return `
<span>Started: ${formatSafeDate(rawStart, eosStart, dateOptions)}</span><br> <span>Started: ${formatSafeDate(rawStart, eosStart, optsLong)}</span><br>
<span>Ends: ${formatSafeDate(rawEnd, eosEnd, dateOptions)}</span> <span>Ends: ${formatSafeDate(rawEnd, eosEnd, optsLong)}</span>
`; `;
})()} })()}
</div> </div>
@@ -393,8 +321,8 @@ function updateTimeBreakdown(cobiets) {
const rawStart = fromCobiets(startAmt); const rawStart = fromCobiets(startAmt);
const rawEnd = fromCobiets(endAmt); const rawEnd = fromCobiets(endAmt);
return ` return `
<span>Started: ${formatSafeDate(rawStart, startAmt, dateOptions)}</span><br> <span>Started: ${formatSafeDate(rawStart, startAmt, optsLong)}</span><br>
<span>Ends: ${formatSafeDate(rawEnd, endAmt, dateOptions)}</span> <span>Ends: ${formatSafeDate(rawEnd, endAmt, optsLong)}</span>
`; `;
})()} })()}
</div> </div>
@@ -412,8 +340,8 @@ function updateTimeBreakdown(cobiets) {
const rawStart = fromCobiets(startAmt); const rawStart = fromCobiets(startAmt);
const rawEnd = fromCobiets(endAmt); const rawEnd = fromCobiets(endAmt);
return ` return `
<span>Started: ${formatSafeDate(rawStart, startAmt, dateOptions)}</span><br> <span>Started: ${formatSafeDate(rawStart, startAmt, optsLong)}</span><br>
<span>Ends: ${formatSafeDate(rawEnd, endAmt, dateOptions)}</span> <span>Ends: ${formatSafeDate(rawEnd, endAmt, optsLong)}</span>
`; `;
})()} })()}
</div> </div>
@@ -431,8 +359,8 @@ function updateTimeBreakdown(cobiets) {
const rawStart = fromCobiets(startAmt); const rawStart = fromCobiets(startAmt);
const rawEnd = fromCobiets(endAmt); const rawEnd = fromCobiets(endAmt);
return ` return `
<span>Started: ${formatSafeDate(rawStart, startAmt, dateOptions)}</span><br> <span>Started: ${formatSafeDate(rawStart, startAmt, optsLong)}</span><br>
<span>Ends: ${formatSafeDate(rawEnd, endAmt, dateOptions)}</span> <span>Ends: ${formatSafeDate(rawEnd, endAmt, optsLong)}</span>
`; `;
})()} })()}
</div> </div>
@@ -450,8 +378,8 @@ function updateTimeBreakdown(cobiets) {
const rawStart = fromCobiets(startAmt); const rawStart = fromCobiets(startAmt);
const rawEnd = fromCobiets(endAmt); const rawEnd = fromCobiets(endAmt);
return ` return `
<span>Started: ${formatSafeDate(rawStart, startAmt, dateOptions)}</span><br> <span>Started: ${formatSafeDate(rawStart, startAmt, optsLong)}</span><br>
<span>Ends: ${formatSafeDate(rawEnd, endAmt, dateOptions)}</span> <span>Ends: ${formatSafeDate(rawEnd, endAmt, optsLong)}</span>
`; `;
})()} })()}
</div> </div>
@@ -469,8 +397,8 @@ function updateTimeBreakdown(cobiets) {
const rawStart = fromCobiets(startAmt); const rawStart = fromCobiets(startAmt);
const rawEnd = fromCobiets(endAmt); const rawEnd = fromCobiets(endAmt);
return ` return `
<span>Started: ${formatSafeDate(rawStart, startAmt, dateOptions)}</span><br> <span>Started: ${formatSafeDate(rawStart, startAmt, optsLong)}</span><br>
<span>Ends: ${formatSafeDate(rawEnd, endAmt, dateOptions)}</span> <span>Ends: ${formatSafeDate(rawEnd, endAmt, optsLong)}</span>
`; `;
})()} })()}
</div> </div>
@@ -486,8 +414,8 @@ function updateTimeBreakdown(cobiets) {
const rawStart = fromCobiets(eocStart); const rawStart = fromCobiets(eocStart);
const rawEnd = fromCobiets(eocEnd); const rawEnd = fromCobiets(eocEnd);
return ` return `
<span>Started: ${formatSafeDate(rawStart, eocStart, dateOptions)}</span><br> <span>Started: ${formatSafeDate(rawStart, eocStart, optsLong)}</span><br>
<span>Ends: ${formatSafeDate(rawEnd, eocEnd, dateOptions)}</span> <span>Ends: ${formatSafeDate(rawEnd, eocEnd, optsLong)}</span>
`; `;
})()} })()}
</div> </div>
@@ -503,8 +431,8 @@ function updateTimeBreakdown(cobiets) {
const rawStart = fromCobiets(cerStart); const rawStart = fromCobiets(cerStart);
const rawEnd = fromCobiets(cerEnd); const rawEnd = fromCobiets(cerEnd);
return ` return `
<span>Started: ${formatSafeDate(rawStart, cerStart, dateOptions)}</span><br> <span>Started: ${formatSafeDate(rawStart, cerStart, optsLong)}</span><br>
<span>Ends: ${formatSafeDate(rawEnd, cerEnd, dateOptions)}</span> <span>Ends: ${formatSafeDate(rawEnd, cerEnd, optsLong)}</span>
`; `;
})()} })()}
</div> </div>
@@ -520,8 +448,8 @@ function updateTimeBreakdown(cobiets) {
const rawStart = fromCobiets(ueoStart); const rawStart = fromCobiets(ueoStart);
const rawEnd = fromCobiets(ueoEnd); const rawEnd = fromCobiets(ueoEnd);
return ` return `
<span>Started: ${formatSafeDate(rawStart, ueoStart, dateOptions)}</span><br> <span>Started: ${formatSafeDate(rawStart, ueoStart, optsLong)}</span><br>
<span>Ends: ${formatSafeDate(rawEnd, ueoEnd, dateOptions)}</span> <span>Ends: ${formatSafeDate(rawEnd, ueoEnd, optsLong)}</span>
`; `;
})()} })()}
</div> </div>
@@ -638,12 +566,7 @@ function updateCalendar() {
grid.innerHTML = ''; grid.innerHTML = '';
// reuse the same dateOpts you use elsewhere: // reuse the same dateOpts you use elsewhere:
const dateOpts = { const dateOpts = dateOptions(false);
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++) { for (let i = 0; i < 16; i++) {
const cellCob = baseCob + i * COBIE_UNITS.eonstrip; const cellCob = baseCob + i * COBIE_UNITS.eonstrip;
@@ -664,43 +587,16 @@ function updateCalendar() {
${startDate.toLocaleDateString('en-US', dateOpts)} ${startDate.toLocaleDateString('en-US', dateOpts)}
</div>`; </div>`;
if (Array.isArray(window.SPECIAL_EVENTS)) { collectEventOccurrences(
const cellStart = cellCob; cellCob,
const cellEnd = cellCob + COBIE_UNITS.eonstrip; cellCob + COBIE_UNITS.eonstrip,
window.SPECIAL_EVENTS.forEach(ev => { ev => ev.showMega !== false
if (ev.showMega === false) return; ).forEach(({ event }) => {
const baseStart = parseCobiets(ev.start || ev.cobie);
if (baseStart === null) return;
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;
}
if (cellStart > endCob) return;
let n = Math.floor((cellStart - startCob) / interval);
if (n < 0) n = 0;
let occ = startCob + n * interval;
if (occ + duration <= cellStart) {
occ += interval;
}
if (occ < cellEnd && occ + duration > cellStart && occ <= endCob) {
const tag = document.createElement('div'); const tag = document.createElement('div');
tag.className = 'event-tag'; tag.className = 'event-tag';
tag.textContent = ev.label; tag.textContent = event.label;
card.appendChild(tag); card.appendChild(tag);
}
}); });
}
const tooltip = document.createElement('div'); const tooltip = document.createElement('div');
tooltip.className = 'tooltip'; tooltip.className = 'tooltip';
tooltip.innerHTML = showEonstripDetails(i, cellCob, dateOpts); tooltip.innerHTML = showEonstripDetails(i, cellCob, dateOpts);
@@ -719,17 +615,7 @@ function showEonstripDetails(index, startCobiets, opts) {
const startDate = fromCobiets(startCobiets); const startDate = fromCobiets(startCobiets);
const endDate = fromCobiets(startCobiets + COBIE_UNITS.eonstrip - 1); const endDate = fromCobiets(startCobiets + COBIE_UNITS.eonstrip - 1);
const options = opts || { const options = opts || 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
};
return ` return `
<strong>${EONSTRIP_NAMES[index]} (0x${index.toString(16).toUpperCase()})</strong><br> <strong>${EONSTRIP_NAMES[index]} (0x${index.toString(16).toUpperCase()})</strong><br>
@@ -773,49 +659,19 @@ function showEonstripDetail(index, startCob) {
updateDetailCurrentTime(); updateDetailCurrentTime();
if (Array.isArray(window.SPECIAL_EVENTS)) { if (Array.isArray(window.SPECIAL_EVENTS)) {
const events = [];
const start = startCob; const start = startCob;
const end = startCob + COBIE_UNITS.eonstrip; const end = startCob + COBIE_UNITS.eonstrip;
window.SPECIAL_EVENTS.forEach(ev => { const events = collectEventOccurrences(start, end, ev => ev.showDetail !== false)
if (ev.showDetail === false) return; .map(({ event, meta, occ }) => ({
const baseStart = parseCobiets(ev.start || ev.cobie); label: event.label,
if (baseStart === null) return; color: event.color,
const tzShift = ev.shiftWithTimezone ? getTimezoneOffsetSeconds(fromCobiets(baseStart)) : 0; start: (occ - start) / COBIE_UNITS.eonstrip,
const startCobEv = baseStart - tzShift; end: (occ + meta.duration - start) / COBIE_UNITS.eonstrip,
const endCobEv = 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;
}
if (start > endCobEv) return;
let n = Math.floor((start - startCobEv) / interval);
if (n < 0) n = 0;
let occ = startCobEv + n * interval;
if (occ + duration <= start) occ += interval;
while (occ < end && occ <= endCobEv) {
const relStart = (occ - start) / COBIE_UNITS.eonstrip;
const relEnd = (occ + duration - start) / COBIE_UNITS.eonstrip;
events.push({
label: ev.label,
color: ev.color,
start: relStart,
end: relEnd,
cobStart: occ, cobStart: occ,
cobEnd: occ + duration, cobEnd: occ + meta.duration,
seriesStart: startCobEv, seriesStart: meta.startCob,
seriesEnd: endCobEv seriesEnd: meta.endCob
}); }));
occ += interval;
}
});
events.sort((a,b)=>a.start-b.start); events.sort((a,b)=>a.start-b.start);
const groups = []; const groups = [];
@@ -877,12 +733,7 @@ function showEonstripDetail(index, startCob) {
const tooltip = document.createElement('div'); const tooltip = document.createElement('div');
tooltip.className = 'tooltip'; tooltip.className = 'tooltip';
const optsShort = { const optsShort = dateOptions(false);
timeZone: currentTimezone === 'TAI' ? 'UTC' : currentTimezone,
year: 'numeric', month: 'short', day: 'numeric',
hour: '2-digit', minute: '2-digit',
hour12: false
};
const startStr = formatCobieTimestamp(ev.cobStart); const startStr = formatCobieTimestamp(ev.cobStart);
const endStr = formatCobieTimestamp(ev.cobEnd); const endStr = formatCobieTimestamp(ev.cobEnd);
@@ -919,14 +770,14 @@ function updateDetailCurrentTime() {
function detailPrev() { function detailPrev() {
if (currentDetailCob === null) return; if (currentDetailCob === null) return;
animateDetailSwipe(-1, () => { Animate.animateDetailSwipe(-1, () => {
showEonstripDetail(currentDetailCob - COBIE_UNITS.eonstrip); showEonstripDetail(currentDetailCob - COBIE_UNITS.eonstrip);
}); });
} }
function detailNext() { function detailNext() {
if (currentDetailCob === null) return; if (currentDetailCob === null) return;
animateDetailSwipe(1, () => { Animate.animateDetailSwipe(1, () => {
showEonstripDetail(currentDetailCob + COBIE_UNITS.eonstrip); showEonstripDetail(currentDetailCob + COBIE_UNITS.eonstrip);
}); });
} }
@@ -974,65 +825,12 @@ function navigatePeriod(evt, direction) {
exitDetailView(); exitDetailView();
} }
animateSwipe(direction, () => { Animate.animateSwipe(direction, () => {
currentOffset += direction * step; currentOffset += direction * step;
updateCalendar(); updateCalendar();
}); });
} }
function animateSwipe(direction, onDone) {
const grid = document.getElementById('eonstripGrid');
if (!grid) { onDone(); return; }
// Ensure a clean starting state when the grid was previously hidden
grid.style.transition = 'none';
grid.style.transform = 'translateX(0)';
void grid.offsetWidth; // force reflow
// 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 animateDetailSwipe(direction, onDone) {
const tl = document.getElementById('detailTimeline');
if (!tl) { onDone(); return; }
tl.style.transition = 'none';
tl.style.transform = 'translateX(0)';
void tl.offsetWidth;
tl.style.transition = 'transform 0.3s ease';
tl.style.transform = `translateX(${direction > 0 ? '-100%' : '100%'})`;
function afterOut() {
tl.removeEventListener('transitionend', afterOut);
tl.style.transition = 'none';
tl.style.transform = `translateX(${direction > 0 ? '100%' : '-100%'})`;
onDone();
void tl.offsetWidth;
tl.style.transition = 'transform 0.3s ease';
tl.style.transform = 'translateX(0)';
}
tl.addEventListener('transitionend', afterOut);
}
function goToNow() { function goToNow() {
manualMode = false; manualMode = false;
manualCobiets = 0; manualCobiets = 0;
+1 -1
View File
@@ -620,7 +620,7 @@ body.fullscreen-clock .clock-label {
left: 50%; left: 50%;
transform-origin: bottom center; transform-origin: bottom center;
transform: translateX(-50%); transform: translateX(-50%);
transition: transform 0.5s ease-in-out; transition: transform 1s linear;
border-radius: 2px; border-radius: 2px;
z-index: 1; z-index: 1;
} }
+92
View File
@@ -0,0 +1,92 @@
(function(){
function parseColor(hex) {
if (!hex) return [255, 255, 255];
let c = hex.replace('#', '');
if (c.length === 3) c = c.split('').map(x => x + x).join('');
const num = parseInt(c, 16);
return [(num >> 16) & 255, (num >> 8) & 255, num & 255];
}
const toHex = v => v.toString(16).padStart(2, '0');
function hexToRgba(hex, a = 1) {
const [r, g, b] = parseColor(hex);
return `rgba(${r},${g},${b},${a})`;
}
function getContrastColor(hex) {
const [r, g, b] = parseColor(hex);
const yiq = (r * 299 + g * 587 + b * 114) / 1000;
return yiq >= 128 ? '#000' : '#fff';
}
function lightenColor(hex, p) {
const [r, g, b] = parseColor(hex).map(v =>
Math.min(255, Math.round(v + (255 - v) * p))
);
return '#' + [r, g, b].map(toHex).join('');
}
function getHumanDiff(d1, d2) {
let start = d1 < d2 ? d1 : d2;
let end = d1 < d2 ? d2 : d1;
let years = end.getUTCFullYear() - start.getUTCFullYear();
let months = end.getUTCMonth() - start.getUTCMonth();
let days = end.getUTCDate() - start.getUTCDate();
if (days < 0) {
months--;
let prevMonthDays = new Date(Date.UTC(
end.getUTCFullYear(),
end.getUTCMonth(), 0
)).getUTCDate();
days += prevMonthDays;
}
if (months < 0) {
years--;
months += 12;
}
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 (diffMs < 0) {
diffMs += 24 * 3600e3;
if (days > 0) {
days--;
} else {
months--;
if (months < 0) {
years--;
months += 12;
}
days = new Date(Date.UTC(
end.getUTCFullYear(),
end.getUTCMonth(), 0
)).getUTCDate();
}
}
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 };
}
window.Utils = {
parseColor,
hexToRgba,
getContrastColor,
lightenColor,
getHumanDiff
};
})();