Compare commits

...

40 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
Kiyomichi Kosaka da56bfce7a Merge pull request #79 from ok2/codex/implement-swipe-mechanism-for-eonstrips
Enable swipe navigation in detail view
2025-06-20 14:04:19 +02:00
Kiyomichi Kosaka f38f4af4ca Add swipe navigation for detail view 2025-06-20 14:03:59 +02:00
Kiyomichi Kosaka ff5d2a7816 Merge pull request #78 from ok2/codex/improve-border-visibility-in-detail-view
Improve event border visibility
2025-06-20 13:59:21 +02:00
Kiyomichi Kosaka 3961bc3345 Improve visibility of event borders 2025-06-20 13:59:03 +02:00
Kiyomichi Kosaka 3974237f42 Merge pull request #77 from ok2/codex/ensure-visible-borders-for-event-boxes
Improve event box borders
2025-06-20 13:49:46 +02:00
Kiyomichi Kosaka 3822b0cdf7 Enhance detail event box visibility 2025-06-20 13:49:32 +02:00
Kiyomichi Kosaka 8451fa9f54 Merge pull request #76 from ok2/codex/fix-event-width-and-color-handling
Improve event overlap display
2025-06-20 13:42:02 +02:00
Kiyomichi Kosaka 26cfa8b868 Improve detail event display 2025-06-20 13:41:48 +02:00
Oleksandr Kozachuk 3f7a343b17 Improve tooltip. 2025-06-20 13:32:58 +02:00
Oleksandr Kozachuk 217c0b0028 Add Sleep. 2025-06-20 13:22:33 +02:00
Oleksandr Kozachuk a63ed16871 Move night 2 hours later. 2025-06-20 13:22:33 +02:00
Kiyomichi Kosaka 6e45dca49a Merge pull request #75 from ok2/codex/improve-tooltip-for-event-details
Improve event tooltip info
2025-06-20 13:21:47 +02:00
Kiyomichi Kosaka 1ec09e1b76 Improve event tooltip with series info 2025-06-20 13:21:32 +02:00
Kiyomichi Kosaka cf30e830a0 Merge pull request #74 from ok2/codex/update-eonstrips-detail-view-on-timezone-change 2025-06-20 10:31:21 +02:00
Kiyomichi Kosaka 65b85f1575 Update detail view on timezone change 2025-06-20 10:31:06 +02:00
Kiyomichi Kosaka 49b30a69e9 Merge pull request #73 from ok2/codex/rename-sleep-to-night-and-add-timezone-shift-attribute 2025-06-20 10:23:26 +02:00
Kiyomichi Kosaka 6c75140533 Rename Sleep event and add timezone shifting 2025-06-20 10:10:11 +02:00
Kiyomichi Kosaka 1002456a0c Merge pull request #72 from ok2/codex/update-event-boxes-and-current-time-line-layout 2025-06-20 08:42:08 +02:00
Kiyomichi Kosaka f6f1502f1a Adjust detail view timeline layout and auto-update 2025-06-20 08:41:44 +02:00
Kiyomichi Kosaka 615a14b5a8 Merge pull request #71 from ok2/codex/fix-layout-issues-in-detail-view 2025-06-20 08:35:03 +02:00
Kiyomichi Kosaka 08a4ef346a fix detail timeline visuals 2025-06-20 08:34:44 +02:00
Kiyomichi Kosaka 1fab01ffb9 Merge pull request #70 from ok2/codex/fix-event-box-label-positioning-and-toolbox-visibility 2025-06-20 08:27:43 +02:00
Kiyomichi Kosaka 441eec4e0c Adjust detail timeline layout and event label positions 2025-06-20 08:27:23 +02:00
Kiyomichi Kosaka d671e64c85 Merge pull request #69 from ok2/codex/refactor-event-box-labels-and-cronon-scale 2025-06-20 08:20:30 +02:00
Kiyomichi Kosaka 6c868f3768 Improve event labels and tooltips 2025-06-20 08:20:13 +02:00
Kiyomichi Kosaka 7a8a463169 Merge pull request #68 from ok2/codex/fix-analog-clock-background-visibility-in-eonstrip-view 2025-06-20 08:11:46 +02:00
Kiyomichi Kosaka 0f7f83c618 Fix fullscreen clock not hiding detail view 2025-06-20 08:11:28 +02:00
Kiyomichi Kosaka bf04c9569a Merge pull request #67 from ok2/codex/improve-timeline-and-event-label-visibility 2025-06-20 08:10:07 +02:00
Kiyomichi Kosaka 483b20e13d Improve detail timeline visibility 2025-06-20 08:09:50 +02:00
Kiyomichi Kosaka 757aa60ec4 Merge pull request #66 from ok2/codex/fix-chronon-numbers-visibility-and-add-timestamp-line 2025-06-20 08:03:04 +02:00
Kiyomichi Kosaka ead5d58d21 Fix detail view timeline 2025-06-20 08:02:40 +02:00
Kiyomichi Kosaka 3a591be7dc Merge pull request #65 from ok2/codex/fix-stuck-animation-issue-in-eonstript-detail-view 2025-06-20 08:00:28 +02:00
Kiyomichi Kosaka 7f706f12cd fix grid animation when navigating from detail 2025-06-20 08:00:10 +02:00
Kiyomichi Kosaka b2d8754c7e Merge pull request #64 from ok2/codex/fix-visibility-of-labels-in-detail-view 2025-06-20 07:55:49 +02:00
Kiyomichi Kosaka 1a61d7b3cc Allow detail labels to overflow 2025-06-20 07:55:34 +02:00
9 changed files with 540 additions and 325 deletions
+8 -6
View File
@@ -54,13 +54,15 @@ An interactive web app that visualizes the **CosmoChron Binary Epoch (CoBiE)** t
```
├── index.html # Main HTML markup
├── analog.html # Analog clock interface
├── clock.js # Clock logic
├── style.css # Separated styles
├── script.js # JavaScript logic
├── README.md # This documentation
── assets/ # (Optional) images or external CSS/JS
├── cobie.js # CoBiE time system utilities
├── utils.js # Generic helper functions
├── animate.js # Shared animations
├── events.js # Sample calendar events
── style.css # Separated styles
├── script.js # Page interactions
├── README.md # This documentation
└── test/ # Unit tests
```
## 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) {
// Use fractional progress within each unit so angles stay small
@@ -155,11 +122,11 @@
const cf = (cob % COBIE_UNITS.eonstrip) / COBIE_UNITS.eonstrip;
const ef = (cob % COBIE_UNITS.megasequence) / COBIE_UNITS.megasequence;
const mf = (cob % COBIE_UNITS.cosmocycle) / COBIE_UNITS.cosmocycle;
rotateHand('handXeno', xf * 360);
rotateHand('handQuantic', qf * 360);
rotateHand('handChronon', cf * 360);
rotateHand('handEonstrip', ef * 360);
rotateHand('handMegasequence', mf * 360);
Animate.rotateHand('handXeno', xf * 360);
Animate.rotateHand('handQuantic', qf * 360);
Animate.rotateHand('handChronon', cf * 360);
Animate.rotateHand('handEonstrip', ef * 360);
Animate.rotateHand('handMegasequence', mf * 360);
}
function updateClock() {
+17 -1
View File
@@ -19,6 +19,20 @@ const COBIE_UNITS = {
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) {
return Math.trunc(a / b);
}
@@ -172,7 +186,9 @@ const Cobie = {
toCobiets,
fromCobiets,
formatCobieTimestamp,
breakdownNonNeg
breakdownNonNeg,
EONSTRIP_NAMES,
MEGASEQUENCE_NAMES
};
if (typeof module !== 'undefined' && module.exports) {
+10 -8
View File
@@ -12,14 +12,16 @@
// duration - optional length of the event in seconds.
// showMega - optional boolean, show label on the megasequence view (default true).
// showDetail - optional boolean, show event in the detail view (default true).
// color - optional hex color for the event (used in detail view)
window.SPECIAL_EVENTS = [
{ cobie: '49f4.9332', label: 'Afina', unit: 'cosmocycle', interval: 1 },
{ cobie: '11e5.f552', label: 'Oleks', unit: 'cosmocycle', interval: 1 },
{ cobie: '4d07.a2b2', label: 'Vincent', unit: 'cosmocycle', interval: 1 },
{ cobie: '3edc.d430', label: 'Hochzeitstag', unit: 'cosmocycle', interval: 1 },
{ cobie: '330d.d4ae', label: 'Zusammentag', unit: 'cosmocycle', interval: 1 },
{ cobie: '11de.0c52', label: 'Anna', unit: 'cosmocycle', interval: 1 },
{ cobie: '467f.ae61', label: 'Iris', unit: 'cosmocycle', interval: 1 },
{ cobie: '6854.7a75', label: 'Sleep', unit: 'second', interval: 86400, duration: 28800, showMega: false }
{ cobie: '49f4.9332', label: 'Afina', color: '#e57373', unit: 'cosmocycle', interval: 1 },
{ cobie: '11e5.f552', label: 'Oleks', color: '#64b5f6', unit: 'cosmocycle', interval: 1 },
{ cobie: '4d07.a2b2', label: 'Vincent', color: '#81c784', unit: 'cosmocycle', interval: 1 },
{ cobie: '3edc.d430', label: 'Hochzeitstag', color: '#ffb74d', unit: 'cosmocycle', interval: 1 },
{ cobie: '330d.d4ae', label: 'Zusammentag', color: '#ba68c8', unit: 'cosmocycle', interval: 1 },
{ cobie: '11de.0c52', label: 'Anna', color: '#4db6ac', unit: 'cosmocycle', interval: 1 },
{ cobie: '467f.ae61', label: 'Iris', color: '#7986cb', unit: 'cosmocycle', interval: 1 },
{ cobie: '6854.7a75', label: 'Sleep', color: '#546e7a', unit: 'second', interval: 86400, duration: 28800, showMega: false },
{ cobie: '6854.9695', label: 'Night', color: '#212121', unit: 'second', interval: 86400, duration: 28800, showMega: false, shiftWithTimezone: true }
];
+2
View File
@@ -167,6 +167,8 @@
</div>
<script src="cobie.js"></script>
<script src="utils.js"></script>
<script src="animate.js"></script>
<script src="events.js"></script>
<script src="script.js"></script>
<script src="clock.js"></script>
+251 -248
View File
@@ -20,19 +20,7 @@ const {
breakdownNonNeg
} = window.Cobie;
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'
];
const { EONSTRIP_NAMES, MEGASEQUENCE_NAMES } = window.Cobie;
let currentOffset = 0;
let currentTimezone = 'UTC';
@@ -44,6 +32,53 @@ 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:
@@ -63,70 +98,45 @@ function formatSafeDate(rawDate, cobSeconds, intlOptions) {
// 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
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();
// ── 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 };
};
// 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 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
@@ -144,17 +154,7 @@ function updateCurrentTime() {
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 options = dateOptions();
const taiOffset = getTAIOffsetAt(baseDate);
let displayDate = baseDate;
@@ -164,9 +164,9 @@ function updateCurrentTime() {
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);
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));
@@ -177,6 +177,7 @@ function updateCurrentTime() {
} else {
updateTimeBreakdown(cobiets);
}
updateDetailCurrentTime();
}
function updateTimeBreakdown(cobiets) {
@@ -202,17 +203,7 @@ function updateTimeBreakdown(cobiets) {
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
};
const optsLong = dateOptions();
//
// ── Build the “core” units (always visible): Galactic Year → Second ──────────────
@@ -229,8 +220,8 @@ function updateTimeBreakdown(cobiets) {
const rawStart = fromCobiets(gyrStart);
const rawEnd = fromCobiets(gyrEnd);
return `
<span>Started: ${formatSafeDate(rawStart, gyrStart, dateOptions)}</span><br>
<span>Ends: ${formatSafeDate(rawEnd, gyrEnd, dateOptions)}</span>
<span>Started: ${formatSafeDate(rawStart, gyrStart, optsLong)}</span><br>
<span>Ends: ${formatSafeDate(rawEnd, gyrEnd, optsLong)}</span>
`;
})()}
</div>
@@ -246,8 +237,8 @@ function updateTimeBreakdown(cobiets) {
const rawStart = fromCobiets(ccyStart);
const rawEnd = fromCobiets(ccyEnd);
return `
<span>Started: ${formatSafeDate(rawStart, ccyStart, dateOptions)}</span><br>
<span>Ends: ${formatSafeDate(rawEnd, ccyEnd, dateOptions)}</span>
<span>Started: ${formatSafeDate(rawStart, ccyStart, optsLong)}</span><br>
<span>Ends: ${formatSafeDate(rawEnd, ccyEnd, optsLong)}</span>
`;
})()}
</div>
@@ -263,8 +254,8 @@ function updateTimeBreakdown(cobiets) {
const rawStart = fromCobiets(mqsStart);
const rawEnd = fromCobiets(mqsEnd);
return `
<span>Started: ${formatSafeDate(rawStart, mqsStart, dateOptions)}</span><br>
<span>Ends: ${formatSafeDate(rawEnd, mqsEnd, dateOptions)}</span>
<span>Started: ${formatSafeDate(rawStart, mqsStart, optsLong)}</span><br>
<span>Ends: ${formatSafeDate(rawEnd, mqsEnd, optsLong)}</span>
`;
})()}
</div>
@@ -280,8 +271,8 @@ function updateTimeBreakdown(cobiets) {
const rawStart = fromCobiets(eosStart);
const rawEnd = fromCobiets(eosEnd);
return `
<span>Started: ${formatSafeDate(rawStart, eosStart, dateOptions)}</span><br>
<span>Ends: ${formatSafeDate(rawEnd, eosEnd, dateOptions)}</span>
<span>Started: ${formatSafeDate(rawStart, eosStart, optsLong)}</span><br>
<span>Ends: ${formatSafeDate(rawEnd, eosEnd, optsLong)}</span>
`;
})()}
</div>
@@ -330,8 +321,8 @@ function updateTimeBreakdown(cobiets) {
const rawStart = fromCobiets(startAmt);
const rawEnd = fromCobiets(endAmt);
return `
<span>Started: ${formatSafeDate(rawStart, startAmt, dateOptions)}</span><br>
<span>Ends: ${formatSafeDate(rawEnd, endAmt, dateOptions)}</span>
<span>Started: ${formatSafeDate(rawStart, startAmt, optsLong)}</span><br>
<span>Ends: ${formatSafeDate(rawEnd, endAmt, optsLong)}</span>
`;
})()}
</div>
@@ -349,8 +340,8 @@ function updateTimeBreakdown(cobiets) {
const rawStart = fromCobiets(startAmt);
const rawEnd = fromCobiets(endAmt);
return `
<span>Started: ${formatSafeDate(rawStart, startAmt, dateOptions)}</span><br>
<span>Ends: ${formatSafeDate(rawEnd, endAmt, dateOptions)}</span>
<span>Started: ${formatSafeDate(rawStart, startAmt, optsLong)}</span><br>
<span>Ends: ${formatSafeDate(rawEnd, endAmt, optsLong)}</span>
`;
})()}
</div>
@@ -368,8 +359,8 @@ function updateTimeBreakdown(cobiets) {
const rawStart = fromCobiets(startAmt);
const rawEnd = fromCobiets(endAmt);
return `
<span>Started: ${formatSafeDate(rawStart, startAmt, dateOptions)}</span><br>
<span>Ends: ${formatSafeDate(rawEnd, endAmt, dateOptions)}</span>
<span>Started: ${formatSafeDate(rawStart, startAmt, optsLong)}</span><br>
<span>Ends: ${formatSafeDate(rawEnd, endAmt, optsLong)}</span>
`;
})()}
</div>
@@ -387,8 +378,8 @@ function updateTimeBreakdown(cobiets) {
const rawStart = fromCobiets(startAmt);
const rawEnd = fromCobiets(endAmt);
return `
<span>Started: ${formatSafeDate(rawStart, startAmt, dateOptions)}</span><br>
<span>Ends: ${formatSafeDate(rawEnd, endAmt, dateOptions)}</span>
<span>Started: ${formatSafeDate(rawStart, startAmt, optsLong)}</span><br>
<span>Ends: ${formatSafeDate(rawEnd, endAmt, optsLong)}</span>
`;
})()}
</div>
@@ -406,8 +397,8 @@ function updateTimeBreakdown(cobiets) {
const rawStart = fromCobiets(startAmt);
const rawEnd = fromCobiets(endAmt);
return `
<span>Started: ${formatSafeDate(rawStart, startAmt, dateOptions)}</span><br>
<span>Ends: ${formatSafeDate(rawEnd, endAmt, dateOptions)}</span>
<span>Started: ${formatSafeDate(rawStart, startAmt, optsLong)}</span><br>
<span>Ends: ${formatSafeDate(rawEnd, endAmt, optsLong)}</span>
`;
})()}
</div>
@@ -423,8 +414,8 @@ function updateTimeBreakdown(cobiets) {
const rawStart = fromCobiets(eocStart);
const rawEnd = fromCobiets(eocEnd);
return `
<span>Started: ${formatSafeDate(rawStart, eocStart, dateOptions)}</span><br>
<span>Ends: ${formatSafeDate(rawEnd, eocEnd, dateOptions)}</span>
<span>Started: ${formatSafeDate(rawStart, eocStart, optsLong)}</span><br>
<span>Ends: ${formatSafeDate(rawEnd, eocEnd, optsLong)}</span>
`;
})()}
</div>
@@ -440,8 +431,8 @@ function updateTimeBreakdown(cobiets) {
const rawStart = fromCobiets(cerStart);
const rawEnd = fromCobiets(cerEnd);
return `
<span>Started: ${formatSafeDate(rawStart, cerStart, dateOptions)}</span><br>
<span>Ends: ${formatSafeDate(rawEnd, cerEnd, dateOptions)}</span>
<span>Started: ${formatSafeDate(rawStart, cerStart, optsLong)}</span><br>
<span>Ends: ${formatSafeDate(rawEnd, cerEnd, optsLong)}</span>
`;
})()}
</div>
@@ -457,8 +448,8 @@ function updateTimeBreakdown(cobiets) {
const rawStart = fromCobiets(ueoStart);
const rawEnd = fromCobiets(ueoEnd);
return `
<span>Started: ${formatSafeDate(rawStart, ueoStart, dateOptions)}</span><br>
<span>Ends: ${formatSafeDate(rawEnd, ueoEnd, dateOptions)}</span>
<span>Started: ${formatSafeDate(rawStart, ueoStart, optsLong)}</span><br>
<span>Ends: ${formatSafeDate(rawEnd, ueoEnd, optsLong)}</span>
`;
})()}
</div>
@@ -575,12 +566,7 @@ function updateCalendar() {
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
};
const dateOpts = dateOptions(false);
for (let i = 0; i < 16; i++) {
const cellCob = baseCob + i * COBIE_UNITS.eonstrip;
@@ -601,41 +587,16 @@ function updateCalendar() {
${startDate.toLocaleDateString('en-US', dateOpts)}
</div>`;
if (Array.isArray(window.SPECIAL_EVENTS)) {
const cellStart = cellCob;
const cellEnd = cellCob + COBIE_UNITS.eonstrip;
window.SPECIAL_EVENTS.forEach(ev => {
if (ev.showMega === false) return;
const startCob = parseCobiets(ev.start || ev.cobie);
if (startCob === null) return;
const endCob = ev.end ? parseCobiets(ev.end) : 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');
tag.className = 'event-tag';
tag.textContent = ev.label;
card.appendChild(tag);
}
});
}
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);
@@ -654,17 +615,7 @@ 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
};
const options = opts || dateOptions();
return `
<strong>${EONSTRIP_NAMES[index]} (0x${index.toString(16).toUpperCase()})</strong><br>
@@ -701,50 +652,51 @@ function showEonstripDetail(index, startCob) {
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 events = [];
const start = startCob;
const end = startCob + COBIE_UNITS.eonstrip;
window.SPECIAL_EVENTS.forEach(ev => {
if (ev.showDetail === false) return;
const startCobEv = parseCobiets(ev.start || ev.cobie);
if (startCobEv === null) return;
const endCobEv = ev.end ? parseCobiets(ev.end) : 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, start: relStart, end: relEnd });
occ += interval;
}
});
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 columns = [];
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(columns[col] && columns[col] > ev.start) col++;
columns[col] = ev.end;
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);
});
const width = 100 / (columns.length || 1);
groups.forEach(g=>{
const width = 100/(g.maxCols||1);
g.events.forEach(ev=>ev.width=width);
});
events.forEach(ev=>{
const left = ev.col * width;
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');
@@ -761,30 +713,73 @@ function showEonstripDetail(index, startCob) {
elem.className = 'event-line';
}
elem.style.top = (displayStart * 100) + '%';
elem.style.left = left + '%';
elem.style.width = `calc(${width}% - 2px)`;
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;
}
timeline.appendChild(elem);
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;
showEonstripDetail(currentDetailCob - COBIE_UNITS.eonstrip);
Animate.animateDetailSwipe(-1, () => {
showEonstripDetail(currentDetailCob - COBIE_UNITS.eonstrip);
});
}
function detailNext() {
if (currentDetailCob === null) return;
showEonstripDetail(currentDetailCob + COBIE_UNITS.eonstrip);
Animate.animateDetailSwipe(1, () => {
showEonstripDetail(currentDetailCob + COBIE_UNITS.eonstrip);
});
}
function detailNow() {
@@ -830,36 +825,12 @@ function navigatePeriod(evt, direction) {
exitDetailView();
}
animateSwipe(direction, () => {
Animate.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;
@@ -949,6 +920,9 @@ 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
@@ -994,11 +968,12 @@ document.getElementById('toggleExtended').addEventListener('click', () => {
});
// ── Swipe & Wheel Navigation ────────────────────────────────────────────────
let swipeStartX = null;
let swipeStartY = null;
let swipeMods = { altKey: false, shiftKey: false, ctrlKey: false };
let isSwiping = false;
let swipeGrid = null;
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;
@@ -1009,7 +984,17 @@ function swipeStart(e) {
shiftKey: e.shiftKey || false,
ctrlKey: e.ctrlKey || false
};
swipeGrid = document.getElementById('eonstripGrid');
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';
}
@@ -1046,9 +1031,19 @@ function swipeEnd(e) {
// prepare opposite side
swipeGrid.style.transition = 'none';
swipeGrid.style.transform = `translateX(${dx < 0 ? width : -width}px)`;
const step = getStep(swipeMods);
currentOffset += direction * step;
updateCalendar();
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)';
@@ -1073,7 +1068,15 @@ 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);
if (currentDetailCob !== null) {
if (direction === 1) {
detailNext();
} else {
detailPrev();
}
} else {
navigatePeriod(e, direction);
}
}
}
+72 -24
View File
@@ -400,33 +400,63 @@
.detail-timeline {
position: relative;
--scale-width: 24px;
height: 400px;
border-left: 2px solid #00ffff;
margin-left: 40px;
overflow: hidden;
border-left: 3px solid #00ffff;
margin-right: 40px;
margin-left: 0;
overflow: visible;
}
.timeline-block {
position: absolute;
left: -40px;
left: 0;
width: calc(100% + 40px);
border-top: 1px dashed rgba(255,255,255,0.2);
color: #aaa;
font-size: 0.8em;
text-align: left;
padding-left: 4px;
border-top: 1px dashed rgba(0,255,255,0.5);
color: #00ffff;
font-size: 0.9em;
font-weight: 600;
padding-top: 2px;
text-shadow: 0 0 4px #00ffff;
pointer-events: none;
z-index: 5;
}
.current-time-line {
position: absolute;
left: var(--scale-width);
width: calc(100% + 40px - var(--scale-width));
border-top: 2px solid #ff00ff;
color: #ff00ff;
font-size: 0.9em;
font-weight: 600;
padding-top: 2px;
text-shadow: 0 0 4px #ff00ff;
pointer-events: none;
z-index: 2;
}
.event-box, .event-line {
position: absolute;
left: 0;
background: rgba(255,0,255,0.3);
border: 1px solid rgba(0,255,255,0.5);
left: var(--scale-width);
background: var(--bg-color, rgba(255,0,255,0.4));
border: 1px solid var(--border-color, rgba(0,255,255,0.7));
border-radius: 4px;
padding: 2px 4px;
color: #fff;
font-size: 0.7em;
overflow: hidden;
color: var(--text-color, #fff);
font-size: 0.75em;
overflow: visible;
white-space: nowrap;
z-index: 3;
box-shadow: 0 0 4px var(--border-color, rgba(0,255,255,0.9));
}
.event-box {
display: flex;
align-items: center;
justify-content: center;
text-align: center;
}
.event-line {
@@ -443,17 +473,35 @@
.event-line .event-label,
.event-box.small-event .event-label {
position: absolute;
left: 100%;
top: 50%;
transform: translateY(-50%);
margin-left: 4px;
background: rgba(255,0,255,0.3);
border: 1px solid rgba(0,255,255,0.5);
bottom: 100%;
left: 50%;
transform: translate(-50%, -2px);
margin-bottom: 2px;
background: var(--bg-color, rgba(255,0,255,0.5));
border: 1px solid var(--border-color, rgba(0,255,255,0.8));
border-radius: 4px;
padding: 2px 4px;
padding: 2px 6px;
white-space: nowrap;
color: #fff;
font-size: 0.7em;
color: var(--text-color, #fff);
font-size: 0.75em;
font-weight: 600;
text-shadow: 0 0 4px var(--border-color, #ff00ff);
z-index: 4;
box-shadow: 0 0 4px var(--border-color, rgba(0,255,255,0.9));
}
.event-line .event-label.below,
.event-box.small-event .event-label.below {
bottom: auto;
top: 100%;
transform: translate(-50%, 2px);
margin-bottom: 0;
margin-top: 2px;
}
.event-box:hover .tooltip,
.event-line:hover .tooltip {
opacity: 1;
}
/* Layout combining current time and analog clock */
@@ -572,7 +620,7 @@ body.fullscreen-clock .clock-label {
left: 50%;
transform-origin: bottom center;
transform: translateX(-50%);
transition: transform 0.5s ease-in-out;
transition: transform 1s linear;
border-radius: 2px;
z-index: 1;
}
@@ -628,7 +676,7 @@ body.fullscreen-clock .calendar-view,
body.fullscreen-clock .detail-view,
body.fullscreen-clock .time-details,
body.fullscreen-clock .explanations {
display: none;
display: none !important;
}
body.fullscreen-clock .time-display {
+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
};
})();