Compare commits

..

26 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
9 changed files with 479 additions and 346 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 ├── index.html # Main HTML markup
├── analog.html # Analog clock interface
├── clock.js # Clock logic ├── clock.js # Clock logic
├── cobie.js # CoBiE time system utilities
├── style.css # Separated styles ├── utils.js # Generic helper functions
├── script.js # JavaScript logic ├── animate.js # Shared animations
├── README.md # This documentation ├── events.js # Sample calendar events
── assets/ # (Optional) images or external CSS/JS ── style.css # Separated styles
├── script.js # Page interactions
├── README.md # This documentation
└── 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) {
+10 -8
View File
@@ -12,14 +12,16 @@
// duration - optional length of the event in seconds. // duration - optional length of the event in seconds.
// showMega - optional boolean, show label on the megasequence view (default true). // showMega - optional boolean, show label on the megasequence view (default true).
// showDetail - optional boolean, show event in the detail 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 = [ window.SPECIAL_EVENTS = [
{ cobie: '49f4.9332', label: 'Afina', unit: 'cosmocycle', interval: 1 }, { cobie: '49f4.9332', label: 'Afina', color: '#e57373', unit: 'cosmocycle', interval: 1 },
{ cobie: '11e5.f552', label: 'Oleks', unit: 'cosmocycle', interval: 1 }, { cobie: '11e5.f552', label: 'Oleks', color: '#64b5f6', unit: 'cosmocycle', interval: 1 },
{ cobie: '4d07.a2b2', label: 'Vincent', unit: 'cosmocycle', interval: 1 }, { cobie: '4d07.a2b2', label: 'Vincent', color: '#81c784', unit: 'cosmocycle', interval: 1 },
{ cobie: '3edc.d430', label: 'Hochzeitstag', unit: 'cosmocycle', interval: 1 }, { cobie: '3edc.d430', label: 'Hochzeitstag', color: '#ffb74d', unit: 'cosmocycle', interval: 1 },
{ cobie: '330d.d4ae', label: 'Zusammentag', unit: 'cosmocycle', interval: 1 }, { cobie: '330d.d4ae', label: 'Zusammentag', color: '#ba68c8', unit: 'cosmocycle', interval: 1 },
{ cobie: '11de.0c52', label: 'Anna', unit: 'cosmocycle', interval: 1 }, { cobie: '11de.0c52', label: 'Anna', color: '#4db6ac', unit: 'cosmocycle', interval: 1 },
{ cobie: '467f.ae61', label: 'Iris', unit: 'cosmocycle', interval: 1 }, { cobie: '467f.ae61', label: 'Iris', color: '#7986cb', unit: 'cosmocycle', interval: 1 },
{ cobie: '6854.7a75', label: 'Sleep', unit: 'second', interval: 86400, duration: 28800, showMega: false } { 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> </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>
+241 -281
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,6 +32,53 @@ let updateInterval;
let lastRenderedEonstrip = null; let lastRenderedEonstrip = null;
let currentDetailCob = 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) { function formatSafeDate(rawDate, cobSeconds, intlOptions) {
if (rawDate instanceof Date && !isNaN(rawDate.getTime())) { if (rawDate instanceof Date && !isNaN(rawDate.getTime())) {
// Date is valid: optionally shift for TAI vs UTC, then format: // 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 // 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;
// 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();
} }
} return { startCob, endCob, interval, duration };
};
// 3) extract h/m/s function collectEventOccurrences(start, end, predicate = () => true) {
let hours = Math.floor(diffMs / 3600e3); diffMs -= hours * 3600e3; const out = [];
let minutes = Math.floor(diffMs / 60e3); diffMs -= minutes * 60e3; if (!Array.isArray(window.SPECIAL_EVENTS)) return out;
let seconds = Math.floor(diffMs / 1e3); window.SPECIAL_EVENTS.forEach(ev => {
if (!predicate(ev)) return;
return { years, months, days, hours, minutes, seconds }; 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 // getTAIOffsetAt, toCobiets, fromCobiets, breakdownNonNeg and
@@ -144,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;
@@ -164,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));
@@ -177,6 +177,7 @@ function updateCurrentTime() {
} else { } else {
updateTimeBreakdown(cobiets); updateTimeBreakdown(cobiets);
} }
updateDetailCurrentTime();
} }
function updateTimeBreakdown(cobiets) { function updateTimeBreakdown(cobiets) {
@@ -202,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 ──────────────
@@ -229,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>
@@ -246,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>
@@ -263,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>
@@ -280,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>
@@ -330,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>
@@ -349,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>
@@ -368,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>
@@ -387,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>
@@ -406,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>
@@ -423,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>
@@ -440,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>
@@ -457,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>
@@ -575,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;
@@ -601,41 +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 startCob = parseCobiets(ev.start || ev.cobie); const tag = document.createElement('div');
if (startCob === null) return; tag.className = 'event-tag';
const endCob = ev.end ? parseCobiets(ev.end) : Number.POSITIVE_INFINITY; tag.textContent = event.label;
const unitVal = COBIE_UNITS[ev.unit] || COBIE_UNITS.cosmocycle; card.appendChild(tag);
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);
}
});
}
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);
@@ -654,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>
@@ -701,66 +652,51 @@ function showEonstripDetail(index, startCob) {
timeline.appendChild(block); timeline.appendChild(block);
} }
let markerCob = manualMode ? manualCobiets : toCobiets(new Date()); const line = document.createElement('div');
const rel = (markerCob - startCob) / COBIE_UNITS.eonstrip; line.className = 'current-time-line';
if (rel >= 0 && rel <= 1) { line.id = 'detailCurrentTime';
const line = document.createElement('div'); timeline.appendChild(line);
line.className = 'current-time-line'; updateDetailCurrentTime();
line.style.top = (rel * 100) + '%';
line.textContent = formatCobieTimestamp(markerCob);
timeline.appendChild(line);
}
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 startCobEv = parseCobiets(ev.start || ev.cobie); label: event.label,
if (startCobEv === null) return; color: event.color,
const endCobEv = ev.end ? parseCobiets(ev.end) : Number.POSITIVE_INFINITY; start: (occ - start) / COBIE_UNITS.eonstrip,
const unitVal = COBIE_UNITS[ev.unit] || COBIE_UNITS.cosmocycle; end: (occ + meta.duration - start) / COBIE_UNITS.eonstrip,
const interval = (ev.interval || 1) * unitVal; cobStart: occ,
let duration = 0; cobEnd: occ + meta.duration,
if (typeof ev.duration === 'string') { seriesStart: meta.startCob,
const d = parseCobiets(ev.duration); seriesEnd: meta.endCob
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,
cobStart: occ,
cobEnd: occ + duration
});
occ += interval;
}
});
events.sort((a,b)=>a.start-b.start); events.sort((a,b)=>a.start-b.start);
const columns = [];
const groups = [];
let active = [];
events.forEach(ev=>{ 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; let col=0;
while(columns[col] && columns[col] > ev.start) col++; while(g.columns[col] && g.columns[col] > ev.start) col++;
columns[col] = ev.end; g.columns[col] = ev.end;
ev.col = col; 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=>{ events.forEach(ev=>{
const left = ev.col * width; const left = ev.col * ev.width;
const displayStart = Math.max(0, ev.start); const displayStart = Math.max(0, ev.start);
const displayEnd = Math.min(1, ev.end); const displayEnd = Math.min(1, ev.end);
const elem = document.createElement('div'); const elem = document.createElement('div');
@@ -777,8 +713,10 @@ function showEonstripDetail(index, startCob) {
elem.className = 'event-line'; elem.className = 'event-line';
} }
elem.style.top = (displayStart * 100) + '%'; elem.style.top = (displayStart * 100) + '%';
elem.style.left = left + '%'; elem.style.left = `calc(var(--scale-width) + ${left}%)`;
elem.style.width = `calc(${width}% - 2px)`; 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') { if (elem.classList.contains('small-event') || elem.className === 'event-line') {
const label = document.createElement('span'); const label = document.createElement('span');
@@ -788,41 +726,60 @@ function showEonstripDetail(index, startCob) {
} }
label.textContent = ev.label; label.textContent = ev.label;
elem.appendChild(label); elem.appendChild(label);
if (ev.color) applyEventColors(label, ev.color, 0.5);
} else { } else {
elem.textContent = ev.label; elem.textContent = ev.label;
} }
const tooltip = document.createElement('div'); const tooltip = document.createElement('div');
tooltip.className = 'tooltip'; tooltip.className = 'tooltip';
const opts = { const optsShort = dateOptions(false);
timeZone: currentTimezone === 'TAI' ? 'UTC' : currentTimezone,
year: 'numeric', month: 'short', day: 'numeric', const startStr = formatCobieTimestamp(ev.cobStart);
hour: '2-digit', minute: '2-digit', second: '2-digit', const endStr = formatCobieTimestamp(ev.cobEnd);
hour12: false const startDate = fromCobiets(ev.cobStart).toLocaleString('en-US', optsShort);
}; const endDate = fromCobiets(ev.cobEnd).toLocaleString('en-US', optsShort);
const startStr = formatCobieTimestamp(ev.cobStart); const seriesStart = formatCobieTimestamp(ev.seriesStart);
const endStr = formatCobieTimestamp(ev.cobEnd); const seriesEnd = isFinite(ev.seriesEnd) ? formatCobieTimestamp(ev.seriesEnd) : '∞';
const startDate = fromCobiets(ev.cobStart).toLocaleString('en-US', opts);
const endDate = fromCobiets(ev.cobEnd).toLocaleString('en-US', opts);
tooltip.innerHTML = tooltip.innerHTML =
`<strong>${ev.label}</strong><br>` + `<strong>${ev.label}</strong><br>` +
`Start: ${startStr}<br>` + `Start: ${startStr} (${startDate})<br>` +
`End: ${endStr}<br>` + (ev.cobEnd > ev.cobStart ? `End: ${endStr} (${endDate})<br>` : '') +
`${startDate} ${endDate}`; `Series: ${seriesStart} ${seriesEnd}`;
elem.appendChild(tooltip); elem.appendChild(tooltip);
timeline.appendChild(elem); 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() { function detailPrev() {
if (currentDetailCob === null) return; if (currentDetailCob === null) return;
showEonstripDetail(currentDetailCob - COBIE_UNITS.eonstrip); Animate.animateDetailSwipe(-1, () => {
showEonstripDetail(currentDetailCob - COBIE_UNITS.eonstrip);
});
} }
function detailNext() { function detailNext() {
if (currentDetailCob === null) return; if (currentDetailCob === null) return;
showEonstripDetail(currentDetailCob + COBIE_UNITS.eonstrip); Animate.animateDetailSwipe(1, () => {
showEonstripDetail(currentDetailCob + COBIE_UNITS.eonstrip);
});
} }
function detailNow() { function detailNow() {
@@ -868,41 +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 goToNow() { function goToNow() {
manualMode = false; manualMode = false;
manualCobiets = 0; manualCobiets = 0;
@@ -992,6 +920,9 @@ document.getElementById('timezone').addEventListener('change', (e) => {
currentTimezone = e.target.value; currentTimezone = e.target.value;
updateCurrentTime(); updateCurrentTime();
updateCalendar(); updateCalendar();
if (currentDetailCob !== null) {
showEonstripDetail(currentDetailCob);
}
}); });
// Set default timezone based on user's locale // Set default timezone based on user's locale
@@ -1037,11 +968,12 @@ document.getElementById('toggleExtended').addEventListener('click', () => {
}); });
// ── Swipe & Wheel Navigation ──────────────────────────────────────────────── // ── Swipe & Wheel Navigation ────────────────────────────────────────────────
let swipeStartX = null; let swipeStartX = null;
let swipeStartY = null; let swipeStartY = null;
let swipeMods = { altKey: false, shiftKey: false, ctrlKey: false }; let swipeMods = { altKey: false, shiftKey: false, ctrlKey: false };
let isSwiping = false; let isSwiping = false;
let swipeGrid = null; let swipeGrid = null;
let swipeContext = 'calendar';
function swipeStart(e) { function swipeStart(e) {
const touch = e.touches ? e.touches[0] : e; const touch = e.touches ? e.touches[0] : e;
@@ -1052,7 +984,17 @@ function swipeStart(e) {
shiftKey: e.shiftKey || false, shiftKey: e.shiftKey || false,
ctrlKey: e.ctrlKey || 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) { if (swipeGrid) {
swipeGrid.style.transition = 'none'; swipeGrid.style.transition = 'none';
} }
@@ -1089,9 +1031,19 @@ function swipeEnd(e) {
// prepare opposite side // prepare opposite side
swipeGrid.style.transition = 'none'; swipeGrid.style.transition = 'none';
swipeGrid.style.transform = `translateX(${dx < 0 ? width : -width}px)`; swipeGrid.style.transform = `translateX(${dx < 0 ? width : -width}px)`;
const step = getStep(swipeMods);
currentOffset += direction * step; if (swipeContext === 'calendar') {
updateCalendar(); 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; void swipeGrid.offsetWidth;
swipeGrid.style.transition = 'transform 0.3s ease'; swipeGrid.style.transition = 'transform 0.3s ease';
swipeGrid.style.transform = 'translateX(0)'; swipeGrid.style.transform = 'translateX(0)';
@@ -1116,7 +1068,15 @@ document.addEventListener('mouseup', swipeEnd);
function wheelNavigate(e) { function wheelNavigate(e) {
if (Math.abs(e.deltaX) > Math.abs(e.deltaY) && Math.abs(e.deltaX) > 10) { if (Math.abs(e.deltaX) > Math.abs(e.deltaY) && Math.abs(e.deltaX) > 10) {
const direction = e.deltaX > 0 ? 1 : -1; const direction = e.deltaX > 0 ? 1 : -1;
navigatePeriod(e, direction); if (currentDetailCob !== null) {
if (direction === 1) {
detailNext();
} else {
detailPrev();
}
} else {
navigatePeriod(e, direction);
}
} }
} }
+21 -12
View File
@@ -400,6 +400,7 @@
.detail-timeline { .detail-timeline {
position: relative; position: relative;
--scale-width: 24px;
height: 400px; height: 400px;
border-left: 3px solid #00ffff; border-left: 3px solid #00ffff;
margin-right: 40px; margin-right: 40px;
@@ -425,8 +426,8 @@
.current-time-line { .current-time-line {
position: absolute; position: absolute;
left: 0; left: var(--scale-width);
width: calc(100% + 40px); width: calc(100% + 40px - var(--scale-width));
border-top: 2px solid #ff00ff; border-top: 2px solid #ff00ff;
color: #ff00ff; color: #ff00ff;
font-size: 0.9em; font-size: 0.9em;
@@ -434,21 +435,28 @@
padding-top: 2px; padding-top: 2px;
text-shadow: 0 0 4px #ff00ff; text-shadow: 0 0 4px #ff00ff;
pointer-events: none; pointer-events: none;
z-index: 6; z-index: 2;
} }
.event-box, .event-line { .event-box, .event-line {
position: absolute; position: absolute;
left: 0; left: var(--scale-width);
background: rgba(255,0,255,0.4); background: var(--bg-color, rgba(255,0,255,0.4));
border: 1px solid rgba(0,255,255,0.7); border: 1px solid var(--border-color, rgba(0,255,255,0.7));
border-radius: 4px; border-radius: 4px;
padding: 2px 4px; padding: 2px 4px;
color: #fff; color: var(--text-color, #fff);
font-size: 0.75em; font-size: 0.75em;
overflow: visible; overflow: visible;
white-space: nowrap; white-space: nowrap;
z-index: 3; 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 { .event-line {
@@ -469,16 +477,17 @@
left: 50%; left: 50%;
transform: translate(-50%, -2px); transform: translate(-50%, -2px);
margin-bottom: 2px; margin-bottom: 2px;
background: rgba(255,0,255,0.5); background: var(--bg-color, rgba(255,0,255,0.5));
border: 1px solid rgba(0,255,255,0.8); border: 1px solid var(--border-color, rgba(0,255,255,0.8));
border-radius: 4px; border-radius: 4px;
padding: 2px 6px; padding: 2px 6px;
white-space: nowrap; white-space: nowrap;
color: #fff; color: var(--text-color, #fff);
font-size: 0.75em; font-size: 0.75em;
font-weight: 600; font-weight: 600;
text-shadow: 0 0 4px #ff00ff; text-shadow: 0 0 4px var(--border-color, #ff00ff);
z-index: 4; z-index: 4;
box-shadow: 0 0 4px var(--border-color, rgba(0,255,255,0.9));
} }
.event-line .event-label.below, .event-line .event-label.below,
@@ -611,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
};
})();