Refactor scripts into modules

This commit is contained in:
Kiyomichi Kosaka 2025-06-20 16:25:21 +02:00
parent ce17d2d5bf
commit a9b90729ff
7 changed files with 219 additions and 206 deletions

View File

@ -54,13 +54,15 @@ An interactive web app that visualizes the **CosmoChron Binary Epoch (CoBiE)** t
``` ```
├── index.html # Main HTML markup ├── index.html # Main HTML markup
├── analog.html # Analog clock interface
├── clock.js # Clock logic ├── clock.js # Clock logic
├── cobie.js # CoBiE time system utilities
├── style.css # Separated styles ├── utils.js # Generic helper functions
├── script.js # JavaScript logic ├── animate.js # Shared animations
├── README.md # This documentation ├── events.js # Sample calendar events
└── assets/ # (Optional) images or external CSS/JS ├── style.css # Separated styles
├── script.js # Page interactions
├── README.md # This documentation
└── test/ # Unit tests
``` ```
## macOS Widget ## macOS Widget

83
animate.js Normal file
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
};
})();

View File

@ -114,39 +114,6 @@
}); });
} }
const lastAngles = {
handXeno: 0,
handQuantic: 0,
handChronon: 0,
handEonstrip: 0,
handMegasequence: 0
};
function rotateHand(id, angle) {
const el = document.getElementById(id);
if (!el) return;
const prev = lastAngles[id];
if (angle < prev) {
// When wrapping around (e.g. 15 → 0), animate to one full turn
// and then snap back to the new angle to avoid a jump.
const target = angle + 360;
const handle = () => {
el.removeEventListener('transitionend', handle);
// Snap back without animation
el.style.transition = 'none';
el.style.transform = `translateX(-50%) translateZ(0) rotate(${angle}deg)`;
void el.offsetWidth;
el.style.transition = '';
};
el.addEventListener('transitionend', handle, { once: true });
el.style.transform = `translateX(-50%) translateZ(0) rotate(${target}deg)`;
} else {
el.style.transform = `translateX(-50%) translateZ(0) rotate(${angle}deg)`;
}
lastAngles[id] = angle;
}
function renderClock(cob) { function renderClock(cob) {
// Use fractional progress within each unit so angles stay small // Use fractional progress within each unit so angles stay small
@ -155,11 +122,11 @@
const cf = (cob % COBIE_UNITS.eonstrip) / COBIE_UNITS.eonstrip; const cf = (cob % COBIE_UNITS.eonstrip) / COBIE_UNITS.eonstrip;
const ef = (cob % COBIE_UNITS.megasequence) / COBIE_UNITS.megasequence; const ef = (cob % COBIE_UNITS.megasequence) / COBIE_UNITS.megasequence;
const mf = (cob % COBIE_UNITS.cosmocycle) / COBIE_UNITS.cosmocycle; const mf = (cob % COBIE_UNITS.cosmocycle) / COBIE_UNITS.cosmocycle;
rotateHand('handXeno', xf * 360); Animate.rotateHand('handXeno', xf * 360);
rotateHand('handQuantic', qf * 360); Animate.rotateHand('handQuantic', qf * 360);
rotateHand('handChronon', cf * 360); Animate.rotateHand('handChronon', cf * 360);
rotateHand('handEonstrip', ef * 360); Animate.rotateHand('handEonstrip', ef * 360);
rotateHand('handMegasequence', mf * 360); Animate.rotateHand('handMegasequence', mf * 360);
} }
function updateClock() { function updateClock() {

View File

@ -19,6 +19,20 @@ const COBIE_UNITS = {
astralmillennia: 0x1000000000000000 astralmillennia: 0x1000000000000000
}; };
const EONSTRIP_NAMES = [
'Solprime', 'Lunex', 'Terros', 'Aquarion',
'Ventaso', 'Ignisar', 'Crystalos', 'Floraen',
'Faunor', 'Nebulus', 'Astraeus', 'Umbranox',
'Electros', 'Chronar', 'Radiantae', 'Etherion'
];
const MEGASEQUENCE_NAMES = [
'Azurean Tide', 'Sable Gleam', 'Verdanth Starfall', 'Crimson Dusk',
'Cobalt Frost', 'Amber Blaze', 'Viridian Bloom', 'Argent Veil',
'Helian Rise', 'Nocturne Shade', 'Celestine Aura', 'Pyralis Light',
'Zephyrine Whisper', 'Lustran Bounty', 'Umbral Echo', 'Mythran Epoch'
];
function floorDiv(a, b) { function floorDiv(a, b) {
return Math.trunc(a / b); return Math.trunc(a / b);
} }
@ -172,7 +186,9 @@ const Cobie = {
toCobiets, toCobiets,
fromCobiets, fromCobiets,
formatCobieTimestamp, formatCobieTimestamp,
breakdownNonNeg breakdownNonNeg,
EONSTRIP_NAMES,
MEGASEQUENCE_NAMES
}; };
if (typeof module !== 'undefined' && module.exports) { if (typeof module !== 'undefined' && module.exports) {

View File

@ -167,6 +167,8 @@
</div> </div>
<script src="cobie.js"></script> <script src="cobie.js"></script>
<script src="utils.js"></script>
<script src="animate.js"></script>
<script src="events.js"></script> <script src="events.js"></script>
<script src="script.js"></script> <script src="script.js"></script>
<script src="clock.js"></script> <script src="clock.js"></script>

173
script.js
View File

@ -20,19 +20,7 @@ const {
breakdownNonNeg breakdownNonNeg
} = window.Cobie; } = window.Cobie;
const EONSTRIP_NAMES = [ const { EONSTRIP_NAMES, MEGASEQUENCE_NAMES } = window.Cobie;
'Solprime', 'Lunex', 'Terros', 'Aquarion',
'Ventaso', 'Ignisar', 'Crystalos', 'Floraen',
'Faunor', 'Nebulus', 'Astraeus', 'Umbranox',
'Electros', 'Chronar', 'Radiantae', 'Etherion'
];
const MEGASEQUENCE_NAMES = [
'Azurean Tide', 'Sable Gleam', 'Verdanth Starfall', 'Crimson Dusk',
'Cobalt Frost', 'Amber Blaze', 'Viridian Bloom', 'Argent Veil',
'Helian Rise', 'Nocturne Shade', 'Celestine Aura', 'Pyralis Light',
'Zephyrine Whisper', 'Lustran Bounty', 'Umbral Echo', 'Mythran Epoch'
];
let currentOffset = 0; let currentOffset = 0;
let currentTimezone = 'UTC'; let currentTimezone = 'UTC';
@ -44,34 +32,14 @@ let updateInterval;
let lastRenderedEonstrip = null; let lastRenderedEonstrip = null;
let currentDetailCob = null; let currentDetailCob = null;
// ── Utility color helpers ──────────────────────────────────────────────── // ── Utility color helpers (in utils.js) ───────────────────────────────────
function parseColor(hex) { const {
if (!hex) return [255, 255, 255]; parseColor,
let c = hex.replace('#', ''); hexToRgba,
if (c.length === 3) c = c.split('').map(x => x + x).join(''); getContrastColor,
const num = parseInt(c, 16); lightenColor,
return [(num >> 16) & 255, (num >> 8) & 255, num & 255]; getHumanDiff
} } = window.Utils;
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('');
};
const dateOptions = (long = true) => ({ const dateOptions = (long = true) => ({
timeZone: currentTimezone === 'TAI' ? 'UTC' : currentTimezone, 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 // 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 ────────────────────────────────────────────────────── // ── Event utilities ──────────────────────────────────────────────────────
const normalizeEvent = ev => { const normalizeEvent = ev => {
@ -866,14 +770,14 @@ function updateDetailCurrentTime() {
function detailPrev() { function detailPrev() {
if (currentDetailCob === null) return; if (currentDetailCob === null) return;
animateDetailSwipe(-1, () => { Animate.animateDetailSwipe(-1, () => {
showEonstripDetail(currentDetailCob - COBIE_UNITS.eonstrip); showEonstripDetail(currentDetailCob - COBIE_UNITS.eonstrip);
}); });
} }
function detailNext() { function detailNext() {
if (currentDetailCob === null) return; if (currentDetailCob === null) return;
animateDetailSwipe(1, () => { Animate.animateDetailSwipe(1, () => {
showEonstripDetail(currentDetailCob + COBIE_UNITS.eonstrip); showEonstripDetail(currentDetailCob + COBIE_UNITS.eonstrip);
}); });
} }
@ -921,65 +825,12 @@ function navigatePeriod(evt, direction) {
exitDetailView(); exitDetailView();
} }
animateSwipe(direction, () => { Animate.animateSwipe(direction, () => {
currentOffset += direction * step; currentOffset += direction * step;
updateCalendar(); updateCalendar();
}); });
} }
function animateSwipe(direction, onDone) {
const grid = document.getElementById('eonstripGrid');
if (!grid) { onDone(); return; }
// Ensure a clean starting state when the grid was previously hidden
grid.style.transition = 'none';
grid.style.transform = 'translateX(0)';
void grid.offsetWidth; // force reflow
// slide out
grid.style.transition = 'transform 0.3s ease';
grid.style.transform = `translateX(${direction > 0 ? '-100%' : '100%'})`;
function afterOut() {
grid.removeEventListener('transitionend', afterOut);
// prepare new position off-screen on the other side
grid.style.transition = 'none';
grid.style.transform = `translateX(${direction > 0 ? '100%' : '-100%'})`;
onDone();
// force reflow to apply position instantly
void grid.offsetWidth;
// slide in with transition
grid.style.transition = 'transform 0.3s ease';
grid.style.transform = 'translateX(0)';
}
grid.addEventListener('transitionend', afterOut);
}
function 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() { function goToNow() {
manualMode = false; manualMode = false;
manualCobiets = 0; manualCobiets = 0;

92
utils.js Normal file
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
};
})();