Compare commits

...

85 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
Oleksandr Kozachuk 93fe6bcdd2 Simplify events.js 2025-06-20 07:51:19 +02:00
Kiyomichi Kosaka 456f0744c4 Merge pull request #63 from ok2/codex/add-properties-to-control-event-visibility 2025-06-20 07:49:53 +02:00
Kiyomichi Kosaka 7bfee30e71 feat(events): configurable visibility 2025-06-20 07:49:34 +02:00
Kiyomichi Kosaka 2e6d870c0f Merge pull request #62 from ok2/codex/fix-visibility-of-event-labels-and-line-thickness 2025-06-20 07:47:54 +02:00
Kiyomichi Kosaka 88c37b566a Fix detail view labels for small events 2025-06-20 07:47:31 +02:00
Kiyomichi Kosaka 47d4a66b0f Merge pull request #61 from ok2/codex/fix-event-overflow-in-detail-view 2025-06-20 07:42:01 +02:00
Kiyomichi Kosaka 38a7de5ef8 fix(detail): clamp long events 2025-06-20 07:41:46 +02:00
Kiyomichi Kosaka 5e2ec31e4f Merge pull request #60 from ok2/codex/fix-bugs-in-analog-clock-and-megasequence 2025-06-20 07:35:29 +02:00
Kiyomichi Kosaka ec8f238a05 Fix detail view navigation and full screen 2025-06-20 07:35:04 +02:00
Kiyomichi Kosaka 01f06b84b2 Merge pull request #59 from ok2/codex/fix-missing-event-and-prev/next-buttons 2025-06-20 00:55:06 +02:00
Kiyomichi Kosaka c991a58296 Add daily Sleep event and restore detail navigation 2025-06-20 00:54:28 +02:00
Kiyomichi Kosaka 13dd3ec664 Merge pull request #58 from ok2/codex/add-interactive-view-for-eonstrip-events 2025-06-20 00:34:07 +02:00
Kiyomichi Kosaka 7e060760e7 Add vertical eonstrip detail view 2025-06-20 00:33:33 +02:00
Kiyomichi Kosaka d32b672289 Merge pull request #57 from ok2/codex/enhance-event-periodicity-support 2025-06-20 00:26:59 +02:00
Kiyomichi Kosaka 5e703813d7 Add flexible recurring events 2025-06-20 00:26:11 +02:00
Oleksandr Kozachuk d11b8cf19f Remove unneeded code. 2025-06-20 00:15:15 +02:00
Oleksandr Kozachuk 51aaa52112 Bigger marker. 2025-06-20 00:14:49 +02:00
Kiyomichi Kosaka 3a2df03eb3 Merge pull request #56 from ok2/codex/fix-analog-clock-markers-positioning 2025-06-20 00:06:04 +02:00
Kiyomichi Kosaka 3ea7f1e69f Set marker font size relative to radius 2025-06-20 00:05:48 +02:00
Oleksandr Kozachuk bc39eb3169 Simplify label. 2025-06-19 23:57:18 +02:00
Kiyomichi Kosaka ebbfc215b1 Merge pull request #55 from ok2/codex/find-issue-with-analog-clock-widget
Fix macOS widget resource bundling
2025-06-16 15:18:56 +02:00
Kiyomichi Kosaka 65e8457359 Add widget resource files 2025-06-16 15:18:43 +02:00
Kiyomichi Kosaka f4d4f518c6 Merge pull request #54 from ok2/codex/fix-widget-display-issue-on-macos
Fix macOS widget docs
2025-06-16 14:45:43 +02:00
Kiyomichi Kosaka 1d887cf2ca Update macOS widget instructions 2025-06-16 14:45:32 +02:00
Oleksandr Kozachuk 80e58578fc Fix widget. 2025-06-16 14:42:03 +02:00
Kiyomichi Kosaka 9256d5ecb7 Merge pull request #53 from ok2/codex/fix-bugs-in-cobie-analog-clock-widget
Fix WKWebView setup in widget
2025-06-16 14:39:09 +02:00
Kiyomichi Kosaka 4b8b29a657 Fix web view compilation issues 2025-06-16 14:38:54 +02:00
Kiyomichi Kosaka b749cfe5f4 Merge pull request #52 from ok2/codex/implement-cobie-analog-clock-widget
Implement CoBiE analog clock widget
2025-06-16 14:32:02 +02:00
Kiyomichi Kosaka 342e5c9c72 Add HTML-based analog clock widget 2025-06-16 14:31:47 +02:00
Oleksandr Kozachuk a486b97a2a Add empty Xcode project (remove old unsupported widget) 2025-06-16 14:25:00 +02:00
Kiyomichi Kosaka 81c93710ed Merge pull request #51 from ok2/codex/fix-widget-install-issue-on-mac-os-x
Fix macOS widget packaging
2025-06-16 13:00:58 +02:00
Kiyomichi Kosaka db3b93fc1b Fix macOS widget packaging 2025-06-16 12:58:21 +02:00
Kiyomichi Kosaka 9031736f7a Merge pull request #50 from ok2/codex/prevent-clock-size-change-on-font-scaling
Fix analog clock scaling
2025-06-15 15:23:19 +02:00
Kiyomichi Kosaka a6375d4148 Fix analog clock marker scaling 2025-06-15 15:23:09 +02:00
Kiyomichi Kosaka 7e4e4af00e Merge pull request #49 from ok2/codex/create-shell-script-for-macos-widget
Create script to build macOS widget
2025-06-15 15:03:17 +02:00
Kiyomichi Kosaka 1e1a961dc2 Add build script and remove widget duplicates 2025-06-15 15:02:12 +02:00
Kiyomichi Kosaka 356e904abc Merge pull request #48 from ok2/codex/implement-analog-clock-widget-for-macos
Add macOS dashboard widget
2025-06-15 14:57:07 +02:00
Kiyomichi Kosaka a1f987ff59 Add macOS dashboard widget 2025-06-15 14:56:16 +02:00
Oleksandr Kozachuk 8789b96ada Fix central point. 2025-06-15 14:50:26 +02:00
Kiyomichi Kosaka d8874f1164 Merge pull request #47 from ok2/codex/remove-white-border-from-clock-center
Remove border from analog clock center
2025-06-15 14:45:05 +02:00
Kiyomichi Kosaka fb5cec7837 Remove central clock border 2025-06-15 14:44:53 +02:00
Kiyomichi Kosaka 979acd4483 Merge pull request #46 from ok2/codex/reduce-central-point-size-on-big-screens
Fix clock center size and jitter
2025-06-15 14:43:23 +02:00
Kiyomichi Kosaka d7ba0479f4 Reduce center size and fix jitter 2025-06-15 14:43:13 +02:00
Kiyomichi Kosaka 794631dcba Merge pull request #45 from ok2/codex/fix-central-point-movement-and-size-adaptation
Fix analog clock center dot
2025-06-15 14:38:11 +02:00
Kiyomichi Kosaka fe8fa11f91 Fix center dot jitter and adjust size 2025-06-15 14:37:53 +02:00
11 changed files with 775 additions and 281 deletions
Vendored
BIN
View File
Binary file not shown.
+3
View File
@@ -0,0 +1,3 @@
# Build artifacts
CoBiEClock.wdgt
build-widget/
+20 -6
View File
@@ -54,15 +54,29 @@ 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
The repository provides a SwiftUI widget inside the Xcode project
located at `CoBiE/CoBiE.xcodeproj`.
1. Open the project in Xcode.
2. Select the `CoBiE Analog Clock` scheme.
3. Build and run to install the widget on macOS 13 or later.
The widget displays the analog CoBiE clock directly on your desktop or
in Notification Center.
## Contributing
1. Fork the repository.
+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
};
})();
+8 -40
View File
@@ -83,9 +83,10 @@
const outerR = baseRadius - 2; // just inside the border
// Distance from center for the marker digits so they sit just inside big ticks
// Distance from center so marker's outer edge sits just inside the big tick
const markerSize = markers[0] ? markers[0].offsetWidth : 0;
const markerRadius = outerR - lenBig - markerSize * getMarkerOffset(clock.offsetWidth);
const inset = baseRadius * 0.001; // 0.1% of the radius
const markerRadius = outerR - lenBig - inset + markerSize / 2;
markers.forEach((m, i) => {
const angle = (i / 16) * 2 * Math.PI;
@@ -113,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%) rotate(${angle}deg)`;
void el.offsetWidth;
el.style.transition = '';
};
el.addEventListener('transitionend', handle, { once: true });
el.style.transform = `translateX(-50%) rotate(${target}deg)`;
} else {
el.style.transform = `translateX(-50%) rotate(${angle}deg)`;
}
lastAngles[id] = angle;
}
function renderClock(cob) {
// Use fractional progress within each unit so angles stay small
@@ -154,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) {
+23 -10
View File
@@ -1,14 +1,27 @@
// Configuration of periodic events in CoBiE time
// Each event repeats every cosmocycle.
// "cobie" is the timestamp within the cosmocycle when the event occurs.
// "tag" is a short label that will be displayed on the calendar.
// Each object describes when the event occurs and how often it repeats.
//
// Fields:
// start - CoBiE timestamp when the first occurrence happens.
// end - optional CoBiE timestamp after which the event stops.
// unit - the unit of the recurrence ("second", "xenocycle", "quantic",
// "chronon", "eonstrip", "megasequence", "cosmocycle", ...).
// interval - how many units between occurrences (1 = every unit,
// 2 = every second unit, ...).
// label - short description displayed on the calendar.
// 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', megasequence: 'Mythran Epoch', eonstrip: 'Ventaso' },
{ cobie: '11e5.f552', label: 'Oleks', megasequence: 'Umbral Echo', eonstrip: 'Ignisar' },
{ cobie: '4d07.a2b2', label: 'Vincent', megasequence: 'Azurean Tide', eonstrip: 'Floraen' },
{ cobie: '3edc.d430', label: 'Hochzeitstag', megasequence: 'Lustran Bounty', eonstrip: 'Electros' },
{ cobie: '330d.d4ae', label: 'Zusammentag', megasequence: 'Azurean Tide', eonstrip: 'Chronar' },
{ cobie: '11de.0c52', label: 'Anna', megasequence: 'Lustran Bounty', eonstrip: 'Radiantae' },
{ cobie: '467f.ae61', label: 'Iris', megasequence: 'Argent Veil', eonstrip: 'Etherion' }
{ 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 }
];
+18 -2
View File
@@ -31,7 +31,8 @@
<div class="hand chronon" id="handChronon"></div>
<div class="hand quantic" id="handQuantic"></div>
<div class="hand xeno" id="handXeno"></div>
<div class="clock-label">CoBiE Time</div>
<div class="clock-center"></div>
<div class="clock-label">CoBiE</div>
</div>
</div>
</div>
@@ -60,11 +61,24 @@
<button onclick="goToNow()">Now</button>
</div>
<div class="calendar-view">
<div class="calendar-view" id="calendarView">
<div class="calendar-header" id="calendarHeader">Loading...</div>
<div class="eonstrip-grid" id="eonstripGrid"></div>
</div>
<div id="eonstripDetailView" class="detail-view" style="display:none;">
<div class="detail-header">
<button id="backToCalendar" class="back-btn">Back</button>
<div class="detail-nav">
<button id="detailPrev" class="back-btn"></button>
<button id="detailNow" class="back-btn">Now</button>
<button id="detailNext" class="back-btn"></button>
</div>
<span id="detailTitle"></span>
</div>
<div class="detail-timeline" id="detailTimeline"></div>
</div>
<div class="time-details">
<h3 style="text-align: center; margin-bottom: 20px; color: #00ffff;">Time Breakdown</h3>
@@ -153,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>
+353 -209
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';
@@ -42,13 +30,53 @@ let compareManualMode = false;
let compareCobiets = 0;
let updateInterval;
let lastRenderedEonstrip = null;
let currentDetailCob = null;
function fmt(d, o) {
// shift if TAI, then format
const shifted = currentTimezone==='TAI'
? new Date(d.getTime() + getTAIOffsetAt(d)*1000)
: d;
return shifted.toLocaleString('en-US', o);
// ── 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) {
@@ -70,74 +98,45 @@ function formatSafeDate(rawDate, cobSeconds, intlOptions) {
// parseCobiets, floorDiv and other CoBiE helpers are provided by cobie.js
function getCurrentTAIOffset() {
return getTAIOffsetAt(new Date());
}
function getHumanDiff(d1, d2) {
// make sure start ≤ end
let start = d1 < d2 ? d1 : d2;
let end = d1 < d2 ? d2 : d1;
// 1) year/month/day difference
let years = end.getUTCFullYear() - start.getUTCFullYear();
let months = end.getUTCMonth() - start.getUTCMonth();
let days = end.getUTCDate() - start.getUTCDate();
// if day roll-under, borrow from month
if (days < 0) {
months--;
// days in the month *before* `end`s month:
let prevMonthDays = new Date(Date.UTC(
end.getUTCFullYear(),
end.getUTCMonth(), 0
)).getUTCDate();
days += prevMonthDays;
}
// if month roll-under, borrow from year
if (months < 0) {
years--;
months += 12;
}
// 2) now handle hours/min/sec by “aligning” a Date at start+Y/M/D
let aligned = new Date(Date.UTC(
start.getUTCFullYear() + years,
start.getUTCMonth() + months,
start.getUTCDate() + days,
start.getUTCHours(),
start.getUTCMinutes(),
start.getUTCSeconds()
));
let diffMs = end.getTime() - aligned.getTime();
// if we overshot (negative), borrow one day
if (diffMs < 0) {
// borrow 24 h
diffMs += 24 * 3600e3;
if (days > 0) {
days--;
} else {
// days was zero, so borrow a month
months--;
if (months < 0) {
years--;
months += 12;
}
// set days to length of the previous month of `end`
days = new Date(Date.UTC(
end.getUTCFullYear(),
end.getUTCMonth(), 0
)).getUTCDate();
// ── 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
@@ -155,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;
@@ -175,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));
@@ -188,6 +177,7 @@ function updateCurrentTime() {
} else {
updateTimeBreakdown(cobiets);
}
updateDetailCurrentTime();
}
function updateTimeBreakdown(cobiets) {
@@ -213,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 ──────────────
@@ -240,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>
@@ -257,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>
@@ -274,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>
@@ -291,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>
@@ -341,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>
@@ -360,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>
@@ -379,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>
@@ -398,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>
@@ -417,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>
@@ -434,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>
@@ -451,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>
@@ -468,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>
@@ -586,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;
@@ -612,39 +587,26 @@ function updateCalendar() {
${startDate.toLocaleDateString('en-US', dateOpts)}
</div>`;
if (Array.isArray(window.SPECIAL_EVENTS)) {
const offsetStart = ((cellCob % COBIE_UNITS.cosmocycle) + COBIE_UNITS.cosmocycle) % COBIE_UNITS.cosmocycle;
const offsetEnd = offsetStart + COBIE_UNITS.eonstrip;
window.SPECIAL_EVENTS.forEach(ev => {
const evCob = parseCobiets(ev.cobie);
if (evCob === null) return;
const evOffset = ((evCob % COBIE_UNITS.cosmocycle) + COBIE_UNITS.cosmocycle) % COBIE_UNITS.cosmocycle;
if (evOffset >= offsetStart && evOffset < offsetEnd) {
const tag = document.createElement('div');
tag.className = 'event-tag';
tag.textContent = ev.label;
card.appendChild(tag);
}
});
}
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);
card.appendChild(tooltip);
grid.appendChild(card);
(function(cob) {
(function(cob, idx) {
card.addEventListener('click', () => {
currentOffset = 0;
manualMode = true;
manualCobiets = cob;
clearInterval(updateInterval);
document.querySelector('.current-time').classList.add('manual');
updateCurrentTime();
if (window.CobieClock) {
window.CobieClock.showTime(manualCobiets);
}
showEonstripDetail(idx, cob);
});
})(cellCob + currentTime);
})(cellCob, i);
}
updateTimeBreakdown(currentCob);
}
@@ -653,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>
@@ -673,6 +625,175 @@ function showEonstripDetails(index, startCobiets, opts) {
`;
}
function showEonstripDetail(index, startCob) {
if (startCob === undefined) {
startCob = index;
const bdTmp = breakdownNonNeg(Math.abs(startCob));
index = bdTmp.eonstrip;
}
currentDetailCob = startCob;
const calendar = document.getElementById('calendarView');
const detail = document.getElementById('eonstripDetailView');
const timeline = document.getElementById('detailTimeline');
const title = document.getElementById('detailTitle');
calendar.style.display = 'none';
detail.style.display = 'block';
timeline.innerHTML = '';
const bd = breakdownNonNeg(Math.abs(startCob));
const sign = startCob < 0 ? '-' : '+';
title.textContent = `${sign}${bd.galactic_year.toString(16)}${bd.cosmocycle.toString(16)}${bd.megasequence.toString(16)}${index.toString(16)} ${EONSTRIP_NAMES[index]}`;
for (let c = 0; c <= 16; c++) {
const block = document.createElement('div');
block.className = 'timeline-block';
block.style.top = (c / 16 * 100) + '%';
if (c < 16) block.textContent = c.toString(16).toUpperCase();
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 start = startCob;
const end = startCob + COBIE_UNITS.eonstrip;
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 = [];
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(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);
});
groups.forEach(g=>{
const width = 100/(g.maxCols||1);
g.events.forEach(ev=>ev.width=width);
});
events.forEach(ev=>{
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');
if (ev.end > ev.start) {
elem.className = 'event-box';
const h = (displayEnd - displayStart) * 100;
if (h < 2) {
elem.classList.add('small-event');
elem.style.height = '4px';
} else {
elem.style.height = (h > 0 ? h : 0) + '%';
}
} else {
elem.className = 'event-line';
}
elem.style.top = (displayStart * 100) + '%';
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;
}
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;
Animate.animateDetailSwipe(-1, () => {
showEonstripDetail(currentDetailCob - COBIE_UNITS.eonstrip);
});
}
function detailNext() {
if (currentDetailCob === null) return;
Animate.animateDetailSwipe(1, () => {
showEonstripDetail(currentDetailCob + COBIE_UNITS.eonstrip);
});
}
function detailNow() {
const now = toCobiets(new Date());
const start = now - (now % COBIE_UNITS.eonstrip);
showEonstripDetail(start);
}
function exitDetailView() {
document.getElementById('eonstripDetailView').style.display = 'none';
document.getElementById('calendarView').style.display = 'block';
currentDetailCob = null;
}
function getStep(mods) {
// base step = 1 megasequence
let step = 1;
@@ -700,41 +821,24 @@ function getStep(mods) {
function navigatePeriod(evt, direction) {
const step = getStep(evt);
animateSwipe(direction, () => {
if (currentDetailCob !== null) {
exitDetailView();
}
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;
compareManualMode = false;
currentOffset = 0;
if (currentDetailCob !== null) {
exitDetailView();
}
updateCurrentTime();
updateCalendar();
clearInterval(updateInterval);
@@ -816,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
@@ -828,6 +935,11 @@ if (matchingOption) {
currentTimezone = userTimezone;
}
document.getElementById('backToCalendar').addEventListener('click', exitDetailView);
document.getElementById('detailPrev').addEventListener('click', detailPrev);
document.getElementById('detailNext').addEventListener('click', detailNext);
document.getElementById('detailNow').addEventListener('click', detailNow);
updateCurrentTime();
updateCalendar();
updateInterval = setInterval(updateCurrentTime, 1000);
@@ -856,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;
@@ -871,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';
}
@@ -908,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)';
@@ -935,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);
}
}
}
@@ -951,4 +1092,7 @@ if (document.readyState === 'loading') {
window.navigatePeriod = navigatePeriod;
window.goToNow = goToNow;
window.detailPrev = detailPrev;
window.detailNext = detailNext;
window.detailNow = detailNow;
})();
+158 -13
View File
@@ -368,6 +368,142 @@
will-change: transform;
}
.detail-view {
margin-top: 20px;
background: rgba(255, 255, 255, 0.05);
border-radius: 10px;
padding: 20px;
}
.detail-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.detail-nav {
display: flex;
gap: 10px;
align-items: center;
}
.back-btn {
background: linear-gradient(45deg, #00ffff, #0080ff);
border: none;
border-radius: 5px;
color: #fff;
padding: 6px 12px;
cursor: pointer;
font-size: 0.9em;
}
.detail-timeline {
position: relative;
--scale-width: 24px;
height: 400px;
border-left: 3px solid #00ffff;
margin-right: 40px;
margin-left: 0;
overflow: visible;
}
.timeline-block {
position: absolute;
left: 0;
width: calc(100% + 40px);
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: 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: 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 {
height: 4px;
padding: 0;
overflow: visible;
}
.event-box.small-event {
overflow: visible;
padding: 0;
}
.event-line .event-label,
.event-box.small-event .event-label {
position: absolute;
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 6px;
white-space: nowrap;
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 */
.time-display {
--clock-size: 40vmin;
@@ -398,19 +534,26 @@
box-shadow: 0 0 25px rgba(0, 255, 255, 0.2), inset 0 0 40px rgba(255, 0, 255, 0.2);
}
#clock::after {
content: '';
.clock-center {
position: absolute;
top: 50%;
left: 50%;
width: 1.2em;
height: 1.2em;
transform: translate(-50%, -50%);
width: calc(var(--clock-size) * 0.13);
height: calc(var(--clock-size) * 0.13);
transform: translate(-50%, -50%) translateZ(0);
background: url('logo.svg') center/contain no-repeat;
background-color: #ffffff;
background-color: transparent;
border-radius: 50%;
box-shadow: 0 0 8px rgba(0, 255, 255, 0.8);
/* box-shadow: 0 0 8px rgba(0, 255, 255, 0.8); */
z-index: 2;
pointer-events: none;
}
@media screen and (min-width: 1200px) {
.clock-center {
width: calc(var(--clock-size) * 0.085);
height: calc(var(--clock-size) * 0.085);
}
}
.clock-label {
@@ -432,13 +575,14 @@ body.fullscreen-clock .clock-label {
.marker {
position: absolute;
width: 2em;
height: 2em;
width: calc(var(--clock-size) * 0.13);
height: calc(var(--clock-size) * 0.13);
text-align: center;
line-height: 2em;
line-height: calc(var(--clock-size) * 0.13);
/* Use a futuristic font for the clock markers */
font-family: 'Orbitron', 'Trebuchet MS', 'Lucida Sans', Arial, sans-serif;
font-size: 1.2em;
/* 1% of the clock radius */
font-size: calc(var(--clock-size) * 0.05);
font-weight: 600;
color: #00ffff;
background: none;
@@ -476,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;
}
@@ -529,9 +673,10 @@ body.fullscreen-clock .current-time,
body.fullscreen-clock .timezone-selector,
body.fullscreen-clock .calendar-controls,
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
};
})();