From a9b90729ff09ca7beb96dce0e64248f7a6e0feba Mon Sep 17 00:00:00 2001 From: Kiyomichi Kosaka Date: Fri, 20 Jun 2025 16:25:21 +0200 Subject: [PATCH] Refactor scripts into modules --- README.md | 14 +++-- animate.js | 83 +++++++++++++++++++++++++ clock.js | 43 ++----------- cobie.js | 18 +++++- index.html | 2 + script.js | 173 ++++------------------------------------------------- utils.js | 92 ++++++++++++++++++++++++++++ 7 files changed, 219 insertions(+), 206 deletions(-) create mode 100644 animate.js create mode 100644 utils.js diff --git a/README.md b/README.md index 6f76c6e..6e7e4b6 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/animate.js b/animate.js new file mode 100644 index 0000000..316088a --- /dev/null +++ b/animate.js @@ -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 + }; +})(); diff --git a/clock.js b/clock.js index 163ac76..3aa0946 100644 --- a/clock.js +++ b/clock.js @@ -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() { diff --git a/cobie.js b/cobie.js index 37fceef..7fb19f2 100644 --- a/cobie.js +++ b/cobie.js @@ -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) { diff --git a/index.html b/index.html index af18f18..672005b 100644 --- a/index.html +++ b/index.html @@ -167,6 +167,8 @@ + + diff --git a/script.js b/script.js index dcc2ef7..6460678 100644 --- a/script.js +++ b/script.js @@ -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,34 +32,14 @@ let updateInterval; let lastRenderedEonstrip = null; let currentDetailCob = null; -// ── Utility color helpers ──────────────────────────────────────────────── -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'); - -const hexToRgba = (hex, a = 1) => { - const [r, g, b] = parseColor(hex); - return `rgba(${r},${g},${b},${a})`; -}; - -const getContrastColor = hex => { - const [r, g, b] = parseColor(hex); - const yiq = (r * 299 + g * 587 + b * 114) / 1000; - return yiq >= 128 ? '#000' : '#fff'; -}; - -const 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(''); -}; +// ── Utility color helpers (in utils.js) ─────────────────────────────────── +const { + parseColor, + hexToRgba, + getContrastColor, + lightenColor, + getHumanDiff +} = window.Utils; const dateOptions = (long = true) => ({ timeZone: currentTimezone === 'TAI' ? 'UTC' : currentTimezone, @@ -130,71 +98,7 @@ 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(); - } - } - - // 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 }; -} // ── Event utilities ────────────────────────────────────────────────────── const normalizeEvent = ev => { @@ -866,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); }); } @@ -921,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; diff --git a/utils.js b/utils.js new file mode 100644 index 0000000..a4b070c --- /dev/null +++ b/utils.js @@ -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 + }; +})();