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
+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) {
+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>
+110 -312
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,41 +32,26 @@ let updateInterval;
let lastRenderedEonstrip = null;
let currentDetailCob = null;
function hexToRgba(hex, alpha) {
if (!hex) return '';
let c = hex.replace('#', '');
if (c.length === 3) {
c = c.split('').map(x => x + x).join('');
}
const r = parseInt(c.substring(0,2),16);
const g = parseInt(c.substring(2,4),16);
const b = parseInt(c.substring(4,6),16);
return `rgba(${r},${g},${b},${alpha})`;
}
// ── Utility color helpers (in utils.js) ───────────────────────────────────
const {
parseColor,
hexToRgba,
getContrastColor,
lightenColor,
getHumanDiff
} = window.Utils;
function getContrastColor(hex) {
if (!hex) return '#fff';
let c = hex.replace('#','');
if (c.length === 3) c = c.split('').map(x=>x+x).join('');
const r = parseInt(c.substr(0,2),16);
const g = parseInt(c.substr(2,2),16);
const b = parseInt(c.substr(4,2),16);
const yiq = (r*299 + g*587 + b*114) / 1000;
return yiq >= 128 ? '#000' : '#fff';
}
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('');
}
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;
@@ -125,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
@@ -206,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;
@@ -226,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));
@@ -265,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 ──────────────
@@ -292,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>
@@ -309,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>
@@ -326,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>
@@ -343,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>
@@ -393,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>
@@ -412,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>
@@ -431,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>
@@ -450,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>
@@ -469,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>
@@ -486,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>
@@ -503,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>
@@ -520,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>
@@ -638,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;
@@ -664,43 +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 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');
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);
@@ -719,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>
@@ -773,49 +659,19 @@ function showEonstripDetail(index, startCob) {
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 baseStart = parseCobiets(ev.start || ev.cobie);
if (baseStart === null) return;
const tzShift = ev.shiftWithTimezone ? getTimezoneOffsetSeconds(fromCobiets(baseStart)) : 0;
const startCobEv = baseStart - tzShift;
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,
cobEnd: occ + duration,
seriesStart: startCobEv,
seriesEnd: endCobEv
});
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 groups = [];
@@ -877,12 +733,7 @@ function showEonstripDetail(index, startCob) {
const tooltip = document.createElement('div');
tooltip.className = 'tooltip';
const optsShort = {
timeZone: currentTimezone === 'TAI' ? 'UTC' : currentTimezone,
year: 'numeric', month: 'short', day: 'numeric',
hour: '2-digit', minute: '2-digit',
hour12: false
};
const optsShort = dateOptions(false);
const startStr = formatCobieTimestamp(ev.cobStart);
const endStr = formatCobieTimestamp(ev.cobEnd);
@@ -919,14 +770,14 @@ function updateDetailCurrentTime() {
function detailPrev() {
if (currentDetailCob === null) return;
animateDetailSwipe(-1, () => {
Animate.animateDetailSwipe(-1, () => {
showEonstripDetail(currentDetailCob - COBIE_UNITS.eonstrip);
});
}
function detailNext() {
if (currentDetailCob === null) return;
animateDetailSwipe(1, () => {
Animate.animateDetailSwipe(1, () => {
showEonstripDetail(currentDetailCob + COBIE_UNITS.eonstrip);
});
}
@@ -974,65 +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; }
// 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() {
manualMode = false;
manualCobiets = 0;
+1 -1
View File
@@ -620,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;
}
+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
};
})();