The file had two getColors() function declarations. The second one (hoisted later) wins and was missing the new keys. Both declarations updated.
1992 lines
95 KiB
HTML
1992 lines
95 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="de">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Background Vanta Clouds2</title>
|
|
<script src="config.js"></script>
|
|
<script src="i18n.js"></script>
|
|
<script>
|
|
(function() {
|
|
var theme = (window.CONFIG && window.CONFIG.theme) || 'ash';
|
|
var cssFile = window.THEMES && window.THEMES[theme];
|
|
var css = document.getElementById('theme-css');
|
|
if (css) css.href = cssFile || (window.THEMES && window.THEMES.ash) || 'ash.css';
|
|
})();
|
|
</script>
|
|
<link id="theme-css" rel="stylesheet" href="ash.css">
|
|
<script src="vendor/suncalc.js"></script>
|
|
<style>
|
|
html, body { margin: 0; padding: 0; overflow: hidden; width: 100%; height: 100%; font-family: monospace; }
|
|
#vanta { width: 100vw; height: 100vh; position: fixed; top: 0; left: 0; }
|
|
#debug-panel {
|
|
position: fixed;
|
|
bottom: 2vh;
|
|
left: 2vh;
|
|
width: 20vw;
|
|
min-width: 200px;
|
|
max-width: 320px;
|
|
background: rgba(28, 27, 31, 0.85);
|
|
border: 1px solid rgba(230, 223, 209, 0.3);
|
|
border-radius: 0.6vh;
|
|
padding: 1vh 1vw;
|
|
color: var(--bone);
|
|
font-family: monospace;
|
|
font-size: clamp(9px, 1.2vh, 14px);
|
|
line-height: 1.4;
|
|
z-index: 9999;
|
|
max-height: 90vh;
|
|
overflow-y: auto;
|
|
}
|
|
.debug-title {
|
|
font-size: 1.2em;
|
|
font-weight: bold;
|
|
color: var(--orange);
|
|
padding-bottom: 4px;
|
|
margin-bottom: 6px;
|
|
cursor: pointer;
|
|
user-select: none;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
.debug-title.expanded {
|
|
border-bottom: 1px solid rgba(230, 223, 209, 0.3);
|
|
margin-bottom: 6px;
|
|
}
|
|
.debug-title-toggle {
|
|
font-size: 0.8em;
|
|
color: var(--icon);
|
|
margin-left: 8px;
|
|
}
|
|
.debug-content.collapsed {
|
|
display: none;
|
|
}
|
|
.debug-controls {
|
|
display: flex;
|
|
gap: 5px;
|
|
margin-bottom: 8px;
|
|
}
|
|
.debug-select {
|
|
background: var(--dark);
|
|
color: var(--bone);
|
|
border: 1px solid var(--border);
|
|
border-radius: 2px;
|
|
padding: 2px 4px;
|
|
font-family: monospace;
|
|
font-size: 0.9em;
|
|
}
|
|
.debug-section {
|
|
font-size: 1em;
|
|
color: var(--icon);
|
|
margin-top: 6px;
|
|
margin-bottom: 2px;
|
|
}
|
|
.debug-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
padding: 1px 0;
|
|
}
|
|
.debug-label {
|
|
color: var(--icon);
|
|
min-width: 80px;
|
|
}
|
|
.libel {
|
|
color: var(--icon);
|
|
min-width: 50px;
|
|
}
|
|
.hex {
|
|
color: var(--bone);
|
|
font-size: 0.9em;
|
|
font-family: monospace;
|
|
}
|
|
.swatch {
|
|
width: 1.4em;
|
|
height: 1.4em;
|
|
border: 1px solid var(--stone);
|
|
border-radius: 2px;
|
|
}
|
|
.theme-table {
|
|
font-size: 0.9em;
|
|
margin-top: 4px;
|
|
}
|
|
.theme-table-row {
|
|
display: grid;
|
|
grid-template-columns: 5em 2.4em 2.4em 2.4em 2.4em 2.8em;
|
|
gap: 5px;
|
|
align-items: center;
|
|
padding: 1px 0;
|
|
}
|
|
.palette-table-row {
|
|
display: grid;
|
|
grid-template-columns: 7em 1.6em auto;
|
|
gap: 5px;
|
|
align-items: center;
|
|
padding: 1px 0;
|
|
}
|
|
.palette-table-row .theme-table-name {
|
|
text-align: right;
|
|
padding-right: 4px;
|
|
}
|
|
.palette-table-row .theme-table-speed {
|
|
text-align: left;
|
|
}
|
|
.theme-table-header {
|
|
color: var(--icon);
|
|
font-weight: bold;
|
|
border-bottom: 1px solid var(--border);
|
|
padding-bottom: 2px;
|
|
}
|
|
.theme-table-name {
|
|
color: var(--bone);
|
|
}
|
|
.theme-table-mini-swatch {
|
|
width: 1.3em;
|
|
height: 1.3em;
|
|
border: 1px solid var(--border);
|
|
border-radius: 1px;
|
|
}
|
|
.theme-table-speed {
|
|
color: var(--icon);
|
|
font-size: 0.85em;
|
|
}
|
|
@media (orientation: portrait) and (max-width: 700px) {
|
|
#debug-panel {
|
|
display: none;
|
|
}
|
|
}
|
|
#debug-panel.disabled {
|
|
display: none !important;
|
|
}
|
|
#info-panel {
|
|
position: fixed;
|
|
top: 2vh;
|
|
right: 2vh;
|
|
bottom: 2vh;
|
|
color: var(--bone);
|
|
background: rgba(28, 27, 31, 0.7);
|
|
padding: 1vh 1vw;
|
|
border-radius: 0.6vh;
|
|
z-index: 9999;
|
|
font-size: clamp(10px, 1.2vh, 14px);
|
|
line-height: 1.4;
|
|
width: clamp(260px, min(33vh, 28vw), 40vw);
|
|
display: flex;
|
|
flex-direction: column;
|
|
overflow-y: auto;
|
|
align-items: stretch;
|
|
}
|
|
|
|
@media (orientation: portrait) and (max-width: 700px) {
|
|
#info-panel {
|
|
width: 90vw;
|
|
}
|
|
}
|
|
#analog-clock {
|
|
width: 100%;
|
|
height: auto;
|
|
margin: 0 auto 0.4vh auto;
|
|
display: block;
|
|
flex-shrink: 0;
|
|
}
|
|
#digital-clock {
|
|
text-align: center;
|
|
font-size: 1.3em;
|
|
letter-spacing: 2px;
|
|
margin-bottom: 0.6vh;
|
|
padding-bottom: 0.5vh;
|
|
border-bottom: 1px solid rgba(230, 223, 209, 0.2);
|
|
white-space: pre;
|
|
}
|
|
.info-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.6vw;
|
|
padding: 0.05vh 0;
|
|
}
|
|
.info-icon {
|
|
width: 1.4em;
|
|
height: 1.4em;
|
|
flex-shrink: 0;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
.info-icon svg {
|
|
width: 100%;
|
|
height: 100%;
|
|
}
|
|
.info-content {
|
|
flex-grow: 1;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
gap: 0.6vw;
|
|
}
|
|
.info-label {
|
|
color: var(--icon);
|
|
}
|
|
.info-value {
|
|
color: var(--bone);
|
|
font-weight: bold;
|
|
}
|
|
.info-sub {
|
|
color: var(--stone);
|
|
font-size: 0.85em;
|
|
margin-left: 4px;
|
|
}
|
|
.weather-divider {
|
|
height: 1px;
|
|
background: rgba(230, 223, 209, 0.2);
|
|
margin: 0.5vh 0;
|
|
}
|
|
.weather-current {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.8vw;
|
|
padding: 0.8vh 0 1vh 0;
|
|
border-bottom: 1px solid rgba(230, 223, 209, 0.2);
|
|
margin-bottom: 1vh;
|
|
}
|
|
.weather-current-icon {
|
|
width: 3.5em;
|
|
height: 3.5em;
|
|
flex-shrink: 0;
|
|
}
|
|
.weather-current-icon svg { width: 100%; height: 100%; }
|
|
.weather-current-temp {
|
|
font-size: 2.4em;
|
|
font-weight: bold;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: baseline;
|
|
gap: 1vw;
|
|
}
|
|
.weather-location {
|
|
font-size: 0.45em;
|
|
font-weight: normal;
|
|
color: var(--icon);
|
|
text-align: right;
|
|
word-break: break-word;
|
|
max-width: 60%;
|
|
}
|
|
.weather-current-detail {
|
|
font-size: 1em;
|
|
color: var(--icon);
|
|
line-height: 1.3;
|
|
}
|
|
.weather-day {
|
|
margin-bottom: 0.6vh;
|
|
}
|
|
.weather-day-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: baseline;
|
|
padding: 0.2vh 0;
|
|
font-size: 1em;
|
|
}
|
|
.weather-day-name {
|
|
color: var(--bone);
|
|
font-weight: bold;
|
|
}
|
|
.weather-day-minmax {
|
|
color: var(--bone);
|
|
font-size: 0.85em;
|
|
}
|
|
.weather-day-slots {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
gap: 0.3vw;
|
|
font-size: 0.9em;
|
|
margin-top: 0.3vh;
|
|
}
|
|
.weather-slot {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
flex: 1;
|
|
gap: 0.3vh;
|
|
}
|
|
.weather-slot-icon {
|
|
width: 2em;
|
|
height: 2em;
|
|
}
|
|
.weather-slot-icon svg { width: 100%; height: 100%; }
|
|
.weather-slot-label {
|
|
color: var(--stone);
|
|
font-size: 0.8em;
|
|
}
|
|
.weather-slot-temp {
|
|
color: var(--bone);
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="vanta" style="background: #1c1b1f;"></div>
|
|
<div id="debug-panel">
|
|
<div class="debug-title expanded" onclick="toggleDebugPanel()">\n<span>DEBUG - Palette: <span id="dbg-palette-name">Ash</span></span><span class="debug-title-toggle" id="debug-toggle-icon">▼</span></div>
|
|
<div id="debug-content" class="debug-content">
|
|
<div class="debug-controls">
|
|
<select id="dbg-theme-select" class="debug-select" onchange="setDebugTheme(this.value)">
|
|
<!-- Populated dynamically by populateDebugSelects() -->
|
|
</select>
|
|
<select id="dbg-units-select" class="debug-select" onchange="setDebugUnits(this.value)">
|
|
<!-- Populated dynamically by populateDebugSelects() -->
|
|
</select>
|
|
<select id="dbg-language-select" class="debug-select" onchange="setDebugLanguage(this.value)">
|
|
<!-- Populated dynamically by populateDebugSelects() -->
|
|
</select>
|
|
</div>
|
|
<div class="debug-row"><span class="debug-label">Time:</span> <span id="dbg-time">--</span></div>
|
|
<div class="debug-row"><span class="debug-label">Day%:</span> <span id="dbg-day">--</span></div>
|
|
<div class="debug-row"><span class="debug-label">Weather:</span> <span id="dbg-weather">--</span></div>
|
|
<div class="debug-row"><span class="debug-label">Theme:</span> <span id="dbg-theme">--</span></div>
|
|
<div class="debug-row"><span class="debug-label">Speed:</span> <span id="dbg-speed">--</span></div>
|
|
<div class="debug-row"><span class="debug-label">Transition:</span> <span id="dbg-trans">--</span></div>
|
|
<div class="debug-section">Effective theme</div>
|
|
<div class="debug-row"><span class="libel">skyColor</span> <div class="swatch" id="dbg-t-sky"></div> <span class="hex" id="dbg-h-sky">--</span></div>
|
|
<div class="debug-row"><span class="libel">cloudColor</span> <div class="swatch" id="dbg-t-cloud"></div> <span class="hex" id="dbg-h-cloud">--</span></div>
|
|
<div class="debug-row"><span class="libel">lightColor</span> <div class="swatch" id="dbg-t-sunLight"></div> <span class="hex" id="dbg-h-sunLight">--</span></div>
|
|
<div class="debug-row"><span class="libel">bgColor</span> <div class="swatch" id="dbg-t-bg"></div> <span class="hex" id="dbg-h-bg">--</span></div>
|
|
<div class="debug-section">Vanta colors</div>
|
|
<div class="debug-row"><span class="libel">skyColor</span> <div class="swatch" id="dbg-v-sky"></div> <span class="hex" id="dbg-vh-sky">--</span></div>
|
|
<div class="debug-row"><span class="libel">cloudColor</span> <div class="swatch" id="dbg-v-cloud"></div> <span class="hex" id="dbg-vh-cloud">--</span></div>
|
|
<div class="debug-row"><span class="libel">lightColor</span> <div class="swatch" id="dbg-v-sunLight"></div> <span class="hex" id="dbg-vh-sunLight">--</span></div>
|
|
<div class="debug-row"><span class="libel">bgColor</span> <div class="swatch" id="dbg-v-bg"></div> <span class="hex" id="dbg-vh-bg">--</span></div>
|
|
<div class="debug-section">Theme table</div>
|
|
<div id="dbg-theme-table" class="theme-table"></div>
|
|
<div id="dbg-color-table" class="theme-table"></div>
|
|
<div class="debug-section">RPG angles (degrees)</div>
|
|
<div class="debug-row"><span class="libel">sun rise</span> <span id="dbg-sun-rise-angle">--</span></div>
|
|
<div class="debug-row"><span class="libel">sun set</span> <span id="dbg-sun-set-angle">--</span></div>
|
|
<div class="debug-row"><span class="libel">sun now</span> <span id="dbg-sun-now-angle">--</span></div>
|
|
<div class="debug-row"><span class="libel">moon rise</span> <span id="dbg-moon-rise-angle">--</span></div>
|
|
<div class="debug-row"><span class="libel">moon set</span> <span id="dbg-moon-set-angle">--</span></div>
|
|
<div class="debug-row"><span class="libel">moon now</span> <span id="dbg-moon-now-angle">--</span></div>
|
|
</div>
|
|
</div>
|
|
<div id="info-panel">
|
|
<svg id="analog-clock" viewBox="0 0 280 280">
|
|
<!-- Clock face -->
|
|
<circle cx="140" cy="140" r="135" fill="rgba(28,27,31,0.3)" stroke="var(--bone)" stroke-width="2"/>
|
|
|
|
<!-- Hour marks -->
|
|
<g id="hour-marks"></g>
|
|
|
|
<!-- Sun orbit (radius 65) -->
|
|
<circle cx="140" cy="140" r="65" stroke="var(--gray-dark)" stroke-width="1" fill="none" stroke-dasharray="2,3" opacity="0.6"/>
|
|
|
|
<!-- Moon orbit (radius 88) -->
|
|
<circle cx="140" cy="140" r="88" stroke="var(--gray-dark)" stroke-width="1" fill="none" stroke-dasharray="2,3" opacity="0.6"/>
|
|
|
|
<!-- Sun arc (from sunrise to sunset) -->
|
|
<path id="sun-arc" d="" stroke="var(--orange)" stroke-width="2.5" fill="none"/>
|
|
|
|
<!-- Moon arc (from moonrise to moonset) -->
|
|
<path id="moon-arc" d="" stroke="var(--icon)" stroke-width="2.5" fill="none"/>
|
|
|
|
<!-- Sun rise arrow (pointing in / up at sunrise) -->
|
|
<g id="sun-rise-arrow" fill="var(--orange)">
|
|
<polygon points="0,-9 8,0 -9,0"/>
|
|
</g>
|
|
|
|
<!-- Sun set arrow (pointing out / down at sunset) -->
|
|
<g id="sun-set-arrow" fill="var(--orange)">
|
|
<polygon points="0,9 8,0 -9,0"/>
|
|
</g>
|
|
|
|
<!-- Moon rise arrow -->
|
|
<g id="moon-rise-arrow" fill="var(--icon)">
|
|
<polygon points="0,-9 8,0 -9,0"/>
|
|
</g>
|
|
|
|
<!-- Moon set arrow -->
|
|
<g id="moon-set-arrow" fill="var(--icon)">
|
|
<polygon points="0,9 8,0 -9,0"/>
|
|
</g>
|
|
|
|
<!-- Sun zenith marker (diamond shape, small) -->
|
|
<g id="sun-zenith-marker" fill="var(--orange)">
|
|
<polygon points="0,-5 5,0 0,5 -5,0"/>
|
|
</g>
|
|
|
|
<!-- Moon zenith marker removed -->
|
|
|
|
<!-- Sun position (rendered after arcs/arrows so it's visible) -->
|
|
<circle id="rpg-sun-glow" cx="140" cy="140" r="14" fill="var(--orange)" opacity="0.4"/>
|
|
<circle id="rpg-sun" cx="140" cy="140" r="8" fill="var(--orange)"/>
|
|
<circle id="rpg-sun-core" cx="140" cy="140" r="3.5" fill="var(--chalk)"/>
|
|
|
|
<!-- Moon position (with phase) -->
|
|
<g id="rpg-moon">
|
|
<circle id="rpg-moon-bg" cx="0" cy="0" r="10" fill="var(--dark)" stroke="var(--stone)" stroke-width="1"/>
|
|
<path id="rpg-moon-lit" d="" fill="var(--bone)"/>
|
|
<path id="rpg-moon-dark" d="" fill="var(--gray)"/>
|
|
</g>
|
|
|
|
<!-- Clock hands (on top of everything) -->
|
|
<line id="hour-hand" x1="140" y1="140" x2="140" y2="49" stroke="var(--bone)" stroke-width="7" stroke-linecap="round" opacity="0.7"/>
|
|
<line id="minute-hand" x1="140" y1="140" x2="140" y2="28" stroke="var(--bone)" stroke-width="5" stroke-linecap="round" opacity="0.7"/>
|
|
<line id="second-hand" x1="140" y1="140" x2="140" y2="20" stroke="var(--red)" stroke-width="2.5" stroke-linecap="round"/>
|
|
<circle cx="140" cy="140" r="5" fill="var(--red)"/>
|
|
</svg>
|
|
<div id="digital-clock">00:00:00</div>
|
|
<div class="info-row">
|
|
<div class="info-icon">
|
|
<svg viewBox="0 0 24 24" fill="var(--orange)"><circle cx="12" cy="12" r="4"/><g stroke="var(--orange)" stroke-width="2" stroke-linecap="round"><line x1="12" y1="2" x2="12" y2="5"/><line x1="12" y1="19" x2="12" y2="22"/><line x1="2" y1="12" x2="5" y2="12"/><line x1="19" y1="12" x2="22" y2="12"/><line x1="4.9" y1="4.9" x2="6.9" y2="6.9"/><line x1="17.1" y1="17.1" x2="19.1" y2="19.1"/><line x1="4.9" y1="19.1" x2="6.9" y2="17.1"/><line x1="17.1" y1="6.9" x2="19.1" y2="4.9"/></g></svg>
|
|
</div>
|
|
<div class="info-content"><span class="info-label" data-i18n="Sunrise">Sunrise</span><span><span class="info-value" id="sunrise-value">--:--</span><span class="info-sub" id="sunrise-when"></span></span></div>
|
|
</div>
|
|
<div class="info-row">
|
|
<div class="info-icon">
|
|
<svg viewBox="0 0 24 24" fill="var(--orange)"><circle cx="12" cy="12" r="4"/><line x1="12" y1="12" x2="12" y2="3" stroke="var(--orange)" stroke-width="2"/></svg>
|
|
</div>
|
|
<div class="info-content"><span class="info-label" data-i18n="Sun Zenith">Sun Zenith</span><span><span class="info-value" id="sun-zenith-value">--:--</span><span class="info-sub" id="sun-zenith-when"></span></span></div>
|
|
</div>
|
|
<div class="info-row">
|
|
<div class="info-icon">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="var(--orange)" stroke-width="2"><path d="M3 12 A9 9 0 0 1 21 12 Z"/></svg>
|
|
</div>
|
|
<div class="info-content"><span class="info-label" data-i18n="Sunset">Sunset</span><span><span class="info-value" id="sunset-value">--:--</span><span class="info-sub" id="sunset-when"></span></span></div>
|
|
</div>
|
|
<div class="info-row">
|
|
<div class="info-icon">
|
|
<svg id="moon-phase-icon" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9" fill="var(--dark)"/></svg>
|
|
</div>
|
|
<div class="info-content"><span class="info-label" data-i18n="Moon Phase">Moon Phase</span><span class="info-value" id="moon-phase-text">--</span></div>
|
|
</div>
|
|
<div class="info-row">
|
|
<div class="info-icon">
|
|
<svg viewBox="0 0 24 24" fill="var(--icon)"><path d="M12 2 a10 10 0 1 0 0 20 a8 8 0 1 1 0 -16 a6 6 0 1 1 0 8"/></svg>
|
|
</div>
|
|
<div class="info-content"><span class="info-label" data-i18n="Moonrise">Moonrise</span><span><span class="info-value" id="moonrise-value">--:--</span><span class="info-sub" id="moonrise-when"></span></span></div>
|
|
</div>
|
|
<div class="info-row">
|
|
<div class="info-icon">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="var(--icon)" stroke-width="2"><path d="M3 12 A9 9 0 0 1 21 22 Z"/></svg>
|
|
</div>
|
|
<div class="info-content"><span class="info-label" data-i18n="Moonset">Moonset</span><span><span class="info-value" id="moonset-value">--:--</span><span class="info-sub" id="moonset-when"></span></span></div>
|
|
</div>
|
|
<div id="weather-divider" class="weather-divider"></div>
|
|
<div id="weather-current" class="weather-current"></div>
|
|
<div id="weather-forecast" class="weather-forecast"></div>
|
|
</div>
|
|
<script src="vendor/three.min.js"></script>
|
|
<script src="vendor/vanta.clouds.min.js"></script>
|
|
<script src="vendor/vanta.clouds2.min.js"></script>
|
|
<script src="vendor/clouds3d.js"></script>
|
|
<script src="vendor/clouds2d.js"></script>
|
|
<script>
|
|
window.__clouds2dStarted = true;
|
|
try {
|
|
var noiseDataUri = 'data:image/png;base64,PLACEHOLDER';
|
|
var renderer = (window.CONFIG && window.CONFIG.renderer) || 'vanta';
|
|
var useVanta = (renderer !== 'clouds2d' && renderer !== 'clouds3d');
|
|
var effect;
|
|
if (useVanta && typeof VANTA !== 'undefined') {
|
|
effect = (VANTA.CLOUDS2 || VANTA.CLOUDS)({
|
|
el: "#vanta",
|
|
mouseControls: false,
|
|
touchControls: false,
|
|
gyroControls: false,
|
|
minHeight: 200.00,
|
|
minWidth: 200.00,
|
|
scale: 1.00,
|
|
scaleMobile: 4,
|
|
speed: 1.0,
|
|
backgroundColor: 0x1c1b1f,
|
|
backgroundAlpha: 1,
|
|
skyColor: 0x3a4458,
|
|
cloudColor: 0x6d6a63,
|
|
lightColor: 0xe6dfd1,
|
|
texturePath: noiseDataUri
|
|
}) || { setOptions: function() {} };
|
|
} else {
|
|
// Pick the right renderer based on CONFIG.renderer
|
|
var _renderFn = (renderer === 'clouds3d' && window.clouds3dRender)
|
|
? window.clouds3dRender
|
|
: window.clouds2dRender;
|
|
effect = { setOptions: function() {} };
|
|
function c2dRender() {
|
|
var c2dCanvas = document.getElementById('vanta');
|
|
if (!c2dCanvas) return;
|
|
if (typeof _renderFn === 'function') {
|
|
_renderFn(c2dCanvas);
|
|
}
|
|
}
|
|
c2dRender();
|
|
window.addEventListener('resize', c2dRender);
|
|
// Continuously update animation via rAF
|
|
function c2dLoop() {
|
|
c2dRender();
|
|
window._clouds2dRaf = requestAnimationFrame(c2dLoop);
|
|
}
|
|
window._clouds2dRaf = requestAnimationFrame(c2dLoop);
|
|
}
|
|
|
|
function hexToRgb(h) {
|
|
return [(h >> 16) & 255, (h >> 8) & 255, h & 255];
|
|
}
|
|
function rgbToHex(r, g, b) {
|
|
return (Math.round(r) << 16) | (Math.round(g) << 8) | Math.round(b);
|
|
}
|
|
function lerp(a, b, t) {
|
|
return a + (b - a) * t;
|
|
}
|
|
function lerpColor(c1, c2, t) {
|
|
var a = hexToRgb(c1), b = hexToRgb(c2);
|
|
return rgbToHex(lerp(a[0], b[0], t), lerp(a[1], b[1], t), lerp(a[2], b[2], t));
|
|
}
|
|
function smooth(t) {
|
|
return t * t * (3 - 2 * t);
|
|
}
|
|
|
|
var CYCLE = 20000;
|
|
|
|
var PASSAU_LAT = (window.CONFIG && window.CONFIG.location && window.CONFIG.location.lat) || 48.5667;
|
|
var PASSAU_LON = (window.CONFIG && window.CONFIG.location && window.CONFIG.location.lon) || 13.4319;
|
|
|
|
var realDayProgress = 0;
|
|
|
|
// Read color palette from CSS variables
|
|
function getColors() {
|
|
var root = getComputedStyle(document.documentElement);
|
|
var keys = [
|
|
'void', 'panel', 'raised', 'border', 'mid', 'ash',
|
|
'icon', 'stone', 'bone', 'chalk',
|
|
'ember', 'gold', 'blood', 'rust', 'rot', 'moss',
|
|
'steel', 'steelLight', 'blue', 'blueLight', 'violet'
|
|
];
|
|
var map = {
|
|
void: 'black',
|
|
panel: 'dark',
|
|
raised: 'gray-darker',
|
|
border: 'gray-dark',
|
|
mid: 'gray',
|
|
ash: 'gray-light',
|
|
ember: 'orange',
|
|
gold: 'yellow',
|
|
blood: 'red',
|
|
rust: 'brown',
|
|
rot: 'pink',
|
|
moss: 'green',
|
|
steel: 'blue',
|
|
steelLight: 'blue',
|
|
blue: 'blue',
|
|
blueLight: 'blue-light',
|
|
violet: 'purple'
|
|
};
|
|
var result = {};
|
|
for (var i = 0; i < keys.length; i++) {
|
|
var ashKey = keys[i];
|
|
var cssKey = map[ashKey] || ashKey;
|
|
result[ashKey] = root.getPropertyValue('--' + cssKey).trim();
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function hexToInt(hex) {
|
|
return parseInt(hex.replace('#', ''), 16);
|
|
}
|
|
|
|
// Refreshable color cache - re-reads from CSS variables when theme changes
|
|
function getColors() {
|
|
var root = getComputedStyle(document.documentElement);
|
|
var keys = [
|
|
'void', 'panel', 'raised', 'border', 'mid', 'ash',
|
|
'icon', 'stone', 'bone', 'chalk',
|
|
'ember', 'gold', 'blood', 'rust', 'rot', 'moss',
|
|
'steel', 'steelLight', 'blue', 'blueLight', 'violet'
|
|
];
|
|
var map = {
|
|
void: 'black',
|
|
panel: 'dark',
|
|
raised: 'gray-darker',
|
|
border: 'gray-dark',
|
|
mid: 'gray',
|
|
ash: 'gray-light',
|
|
ember: 'orange',
|
|
gold: 'yellow',
|
|
blood: 'red',
|
|
rust: 'brown',
|
|
rot: 'pink',
|
|
moss: 'green',
|
|
steel: 'blue',
|
|
steelLight: 'blue',
|
|
blue: 'blue',
|
|
blueLight: 'blue-light',
|
|
violet: 'purple'
|
|
};
|
|
var result = {};
|
|
for (var i = 0; i < keys.length; i++) {
|
|
var ashKey = keys[i];
|
|
var cssKey = map[ashKey] || ashKey;
|
|
result[ashKey] = root.getPropertyValue('--' + cssKey).trim();
|
|
}
|
|
return result;
|
|
}
|
|
|
|
var ASH = getColors();
|
|
|
|
function rebuildFromColors() {
|
|
ASH = getColors();
|
|
// Rebuild palette
|
|
window.sunPalettes = [
|
|
{ alt: -Math.PI / 2, sky: hexToInt(ASH.void), cloud: hexToInt(ASH.void), light: hexToInt(ASH.stone), bg: hexToInt(ASH.void), cloudShadow: hexToInt(ASH.void), sun: hexToInt(ASH.stone), sunGlare: hexToInt(ASH.ash), sunLight: hexToInt(ASH.stone), sunVis: 0 },
|
|
{ alt: -0.262, sky: hexToInt(ASH.void), cloud: hexToInt(ASH.void), light: hexToInt(ASH.stone), bg: hexToInt(ASH.void), cloudShadow: hexToInt(ASH.void), sun: hexToInt(ASH.stone), sunGlare: hexToInt(ASH.ash), sunLight: hexToInt(ASH.stone), sunVis: 0 },
|
|
{ alt: -0.087, sky: hexToInt(ASH.panel), cloud: hexToInt(ASH.void), light: hexToInt(ASH.stone), bg: hexToInt(ASH.void), cloudShadow: hexToInt(ASH.panel), sun: hexToInt(ASH.stone), sunGlare: hexToInt(ASH.ash), sunLight: hexToInt(ASH.stone), sunVis: 0 },
|
|
{ alt: 0.0, sky: hexToInt(ASH.rust), cloud: hexToInt(ASH.border), light: hexToInt(ASH.gold), bg: hexToInt(ASH.rust), cloudShadow: hexToInt(ASH.raised), sun: hexToInt(ASH.ember), sunGlare: hexToInt(ASH.gold), sunLight: hexToInt(ASH.ember), sunVis: 0.8 },
|
|
{ alt: 0.087, sky: hexToInt(ASH.blood), cloud: hexToInt(ASH.border), light: hexToInt(ASH.ember), bg: hexToInt(ASH.blood), cloudShadow: hexToInt(ASH.raised), sun: hexToInt(ASH.ember), sunGlare: hexToInt(ASH.ember), sunLight: hexToInt(ASH.ember), sunVis: 0.7 },
|
|
{ alt: 0.4, sky: hexToInt(ASH.blue), cloud: hexToInt(ASH.border), light: hexToInt(ASH.bone), bg: hexToInt(ASH.blue), cloudShadow: hexToInt(ASH.steel), sun: hexToInt(ASH.chalk), sunGlare: hexToInt(ASH.bone), sunLight: hexToInt(ASH.chalk), sunVis: 1 },
|
|
{ alt: Math.PI / 2, sky: hexToInt(ASH.blueLight), cloud: hexToInt(ASH.border), light: hexToInt(ASH.chalk), bg: hexToInt(ASH.blueLight), cloudShadow: hexToInt(ASH.blue), sun: hexToInt(ASH.chalk), sunGlare: hexToInt(ASH.bone), sunLight: hexToInt(ASH.chalk), sunVis: 1 }
|
|
];
|
|
window.nightPaletteNew = { sky: hexToInt(ASH.void), cloud: hexToInt(ASH.raised), light: hexToInt(ASH.stone), bg: hexToInt(ASH.void), cloudShadow: hexToInt(ASH.void), sun: hexToInt(ASH.stone), sunGlare: hexToInt(ASH.ash), sunLight: hexToInt(ASH.stone) };
|
|
window.nightPaletteFull = { sky: hexToInt(ASH.panel), cloud: hexToInt(ASH.border), light: hexToInt(ASH.icon), bg: hexToInt(ASH.panel), cloudShadow: hexToInt(ASH.void), sun: hexToInt(ASH.icon), sunGlare: hexToInt(ASH.mid), sunLight: hexToInt(ASH.icon) };
|
|
window.weatherThemes = {
|
|
sunny: {
|
|
skyColor: hexToInt(ASH.blueLight),
|
|
cloudColor: hexToInt(ASH.stone),
|
|
lightColor: hexToInt(ASH.chalk),
|
|
backgroundColor: hexToInt(ASH.bone),
|
|
speed: 1.0
|
|
},
|
|
cloudy: {
|
|
skyColor: hexToInt(ASH.blue),
|
|
cloudColor: hexToInt(ASH.mid),
|
|
lightColor: hexToInt(ASH.bone),
|
|
backgroundColor: hexToInt(ASH.border),
|
|
speed: 1.2
|
|
},
|
|
stormy: {
|
|
skyColor: hexToInt(ASH.steel),
|
|
cloudColor: hexToInt(ASH.panel),
|
|
lightColor: hexToInt(ASH.stone),
|
|
backgroundColor: hexToInt(ASH.void),
|
|
speed: 2.5
|
|
},
|
|
night: {
|
|
skyColor: hexToInt(ASH.void),
|
|
cloudColor: hexToInt(ASH.raised),
|
|
lightColor: hexToInt(ASH.icon),
|
|
backgroundColor: hexToInt(ASH.void),
|
|
speed: 0.4
|
|
}
|
|
};
|
|
}
|
|
|
|
// Sun position palette (altitude-based) - all colors from Ash palette
|
|
// altitude in radians: -pi/2 (below) to pi/2 (zenith)
|
|
// Sun position palette (altitude-based) - all colors from current theme
|
|
// altitude in radians: -pi/2 (below) to pi/2 (zenith)
|
|
// Compressed twilight: dusk and dawn palette entries are now at
|
|
// -5deg / +5deg so the sunset transition completes within roughly
|
|
// an hour of clock time. -15deg is already full-night (void), so
|
|
// the sky is properly dark by ~1h after sunset.
|
|
window.sunPalettes = [
|
|
{ alt: -Math.PI / 2, sky: hexToInt(ASH.void), cloud: hexToInt(ASH.void), light: hexToInt(ASH.stone), bg: hexToInt(ASH.void), cloudShadow: hexToInt(ASH.void), sun: hexToInt(ASH.stone), sunGlare: hexToInt(ASH.ash), sunLight: hexToInt(ASH.stone), sunVis: 0 },
|
|
{ alt: -0.262, sky: hexToInt(ASH.void), cloud: hexToInt(ASH.void), light: hexToInt(ASH.stone), bg: hexToInt(ASH.void), cloudShadow: hexToInt(ASH.void), sun: hexToInt(ASH.stone), sunGlare: hexToInt(ASH.ash), sunLight: hexToInt(ASH.stone), sunVis: 0 },
|
|
{ alt: -0.087, sky: hexToInt(ASH.panel), cloud: hexToInt(ASH.void), light: hexToInt(ASH.stone), bg: hexToInt(ASH.void), cloudShadow: hexToInt(ASH.panel), sun: hexToInt(ASH.stone), sunGlare: hexToInt(ASH.ash), sunLight: hexToInt(ASH.stone), sunVis: 0 },
|
|
{ alt: 0.0, sky: hexToInt(ASH.rust), cloud: hexToInt(ASH.border), light: hexToInt(ASH.gold), bg: hexToInt(ASH.rust), cloudShadow: hexToInt(ASH.raised), sun: hexToInt(ASH.ember), sunGlare: hexToInt(ASH.gold), sunLight: hexToInt(ASH.ember), sunVis: 0.8 },
|
|
{ alt: 0.087, sky: hexToInt(ASH.blood), cloud: hexToInt(ASH.border), light: hexToInt(ASH.ember), bg: hexToInt(ASH.blood), cloudShadow: hexToInt(ASH.raised), sun: hexToInt(ASH.ember), sunGlare: hexToInt(ASH.ember), sunLight: hexToInt(ASH.ember), sunVis: 0.7 },
|
|
{ alt: 0.4, sky: hexToInt(ASH.blue), cloud: hexToInt(ASH.border), light: hexToInt(ASH.bone), bg: hexToInt(ASH.blue), cloudShadow: hexToInt(ASH.steel), sun: hexToInt(ASH.chalk), sunGlare: hexToInt(ASH.bone), sunLight: hexToInt(ASH.chalk), sunVis: 1 },
|
|
{ alt: Math.PI / 2, sky: hexToInt(ASH.blueLight), cloud: hexToInt(ASH.border), light: hexToInt(ASH.chalk), bg: hexToInt(ASH.blueLight), cloudShadow: hexToInt(ASH.blue), sun: hexToInt(ASH.chalk), sunGlare: hexToInt(ASH.bone), sunLight: hexToInt(ASH.chalk), sunVis: 1 }
|
|
];
|
|
|
|
// Night palette based on moon phase (0 = new, 0.5 = full, 1 = new) - Ash colors
|
|
window.nightPaletteNew = { sky: hexToInt(ASH.void), cloud: hexToInt(ASH.raised), light: hexToInt(ASH.stone), bg: hexToInt(ASH.void), cloudShadow: hexToInt(ASH.void), sun: hexToInt(ASH.stone), sunGlare: hexToInt(ASH.ash), sunLight: hexToInt(ASH.stone) };
|
|
window.nightPaletteFull = { sky: hexToInt(ASH.panel), cloud: hexToInt(ASH.border), light: hexToInt(ASH.icon), bg: hexToInt(ASH.panel), cloudShadow: hexToInt(ASH.void), sun: hexToInt(ASH.icon), sunGlare: hexToInt(ASH.mid), sunLight: hexToInt(ASH.icon) };
|
|
|
|
var MIN_NIGHT_BRIGHTNESS = 0.25; // minimum night brightness (even at new moon)
|
|
|
|
function getDayColor(sunAlt) {
|
|
// Find palette segment based on sun altitude
|
|
var palette;
|
|
if (sunAlt <= window.sunPalettes[0].alt) {
|
|
palette = window.sunPalettes[0];
|
|
} else if (sunAlt >= window.sunPalettes[window.sunPalettes.length - 1].alt) {
|
|
palette = window.sunPalettes[window.sunPalettes.length - 1];
|
|
} else {
|
|
for (var i = 0; i < window.sunPalettes.length - 1; i++) {
|
|
if (sunAlt >= window.sunPalettes[i].alt && sunAlt <= window.sunPalettes[i + 1].alt) {
|
|
var span = window.sunPalettes[i + 1].alt - window.sunPalettes[i].alt;
|
|
var localT = span === 0 ? 0 : (sunAlt - window.sunPalettes[i].alt) / span;
|
|
var s = smooth(localT);
|
|
palette = {
|
|
sky: lerpColor(window.sunPalettes[i].sky, window.sunPalettes[i + 1].sky, s),
|
|
cloud: lerpColor(window.sunPalettes[i].cloud, window.sunPalettes[i + 1].cloud, s),
|
|
light: lerpColor(window.sunPalettes[i].light, window.sunPalettes[i + 1].light, s),
|
|
bg: lerpColor(window.sunPalettes[i].bg, window.sunPalettes[i + 1].bg, s),
|
|
cloudShadow: lerpColor(window.sunPalettes[i].cloudShadow, window.sunPalettes[i + 1].cloudShadow, s),
|
|
sun: lerpColor(window.sunPalettes[i].sun, window.sunPalettes[i + 1].sun, s),
|
|
sunGlare: lerpColor(window.sunPalettes[i].sunGlare, window.sunPalettes[i + 1].sunGlare, s),
|
|
sunLight: lerpColor(window.sunPalettes[i].sunLight, window.sunPalettes[i + 1].sunLight, s),
|
|
sunVis: window.sunPalettes[i].sunVis + (window.sunPalettes[i + 1].sunVis - window.sunPalettes[i].sunVis) * s
|
|
};
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return palette;
|
|
}
|
|
|
|
function getNightColor(moonIllum) {
|
|
// moonIllum.fraction: 0 (new moon) to 1 (full moon)
|
|
// Interpolate between new and full moon palette based on fraction
|
|
var s = moonIllum.fraction;
|
|
var c = {
|
|
sky: lerpColor(window.nightPaletteNew.sky, window.nightPaletteFull.sky, s),
|
|
cloud: lerpColor(window.nightPaletteNew.cloud, window.nightPaletteFull.cloud, s),
|
|
light: lerpColor(window.nightPaletteNew.light, window.nightPaletteFull.light, s),
|
|
bg: lerpColor(window.nightPaletteNew.bg, window.nightPaletteFull.bg, s),
|
|
cloudShadow: lerpColor(window.nightPaletteNew.cloudShadow, window.nightPaletteFull.cloudShadow, s),
|
|
sun: lerpColor(window.nightPaletteNew.sun, window.nightPaletteFull.sun, s),
|
|
sunGlare: lerpColor(window.nightPaletteNew.sunGlare, window.nightPaletteFull.sunGlare, s),
|
|
sunLight: lerpColor(window.nightPaletteNew.sunLight, window.nightPaletteFull.sunLight, s)
|
|
};
|
|
// 'light' is moonlight shining ON clouds / stars — keep it
|
|
// bright. Only sky / bg / bg-adjacent get the new-moon
|
|
// darkening, and only partially (otherwise new-moon nights
|
|
// are pitch black with no cloud structure).
|
|
var darkness = 1.0 - (MIN_NIGHT_BRIGHTNESS + (1 - MIN_NIGHT_BRIGHTNESS) * s);
|
|
var shift = -Math.round(255 * darkness * 0.4);
|
|
return {
|
|
sky: shiftColor(c.sky, shift),
|
|
cloud: c.cloud, // keep contrast vs sky
|
|
light: c.light, // moonlight stays bright
|
|
bg: shiftColor(c.bg, shift),
|
|
cloudShadow: shiftColor(c.cloudShadow, Math.round(-shift * 0.5)),
|
|
sun: c.sun,
|
|
sunGlare: c.sunGlare,
|
|
sunLight: c.sunLight
|
|
};
|
|
}
|
|
|
|
function getColor(t) {
|
|
// t is no longer used - now uses real sun/moon
|
|
var now = new Date();
|
|
var sunPos = SunCalc.getPosition(now, PASSAU_LAT, PASSAU_LON);
|
|
var moonIllum = SunCalc.getMoonIllumination(now);
|
|
|
|
// The day palette covers sunAlt down to sunPalettes[0].alt
|
|
// (-pi/2, deep below horizon). Only below that do we hand off
|
|
// to the moon-based night palette. This makes twilight a
|
|
// smooth transition through the day palette entries.
|
|
if (sunPos.altitude > window.sunPalettes[0].alt) {
|
|
return getDayColor(sunPos.altitude);
|
|
} else {
|
|
return getNightColor(moonIllum);
|
|
}
|
|
}
|
|
|
|
function adjustColor(color, factor) {
|
|
var r = Math.min(255, Math.floor(((color >> 16) & 255) * factor));
|
|
var g = Math.min(255, Math.floor(((color >> 8) & 255) * factor));
|
|
var b = Math.min(255, Math.floor((color & 255) * factor));
|
|
return (r << 16) | (g << 8) | b;
|
|
}
|
|
|
|
function shiftColor(color, delta) {
|
|
var r = Math.max(0, Math.min(255, ((color >> 16) & 255) + delta));
|
|
var g = Math.max(0, Math.min(255, ((color >> 8) & 255) + delta));
|
|
var b = Math.max(0, Math.min(255, (color & 255) + delta));
|
|
return (r << 16) | (g << 8) | b;
|
|
}
|
|
|
|
function getWeatherModifiers(code) {
|
|
// Returns {cloudDark: factor, cloudLight: factor, skyShift: shift, bgShift: shift}
|
|
// code is WMO weather code from open-meteo
|
|
if (code === 0) {
|
|
// Clear sky - lighter clouds, brighter sky
|
|
return { cloudDark: 0.7, cloudLight: 1.3, skyShift: 15, bgShift: 15 };
|
|
} else if (code >= 1 && code <= 3) {
|
|
// Mainly clear to overcast - neutral
|
|
return { cloudDark: 0.95, cloudLight: 1.0, skyShift: 0, bgShift: -5 };
|
|
} else if (code === 45 || code === 48) {
|
|
// Fog - cool, washed out
|
|
return { cloudDark: 1.1, cloudLight: 0.85, skyShift: 10, bgShift: 10 };
|
|
} else if ((code >= 51 && code <= 57) || (code >= 61 && code <= 67)) {
|
|
// Drizzle / Rain - darker, slightly cooler
|
|
return { cloudDark: 1.3, cloudLight: 0.7, skyShift: -15, bgShift: -20 };
|
|
} else if ((code >= 71 && code <= 77) || (code >= 85 && code <= 86)) {
|
|
// Snow - brighter (light reflection)
|
|
return { cloudDark: 0.7, cloudLight: 1.5, skyShift: 25, bgShift: 25 };
|
|
} else if (code >= 80 && code <= 82) {
|
|
// Rain showers - darker
|
|
return { cloudDark: 1.4, cloudLight: 0.6, skyShift: -20, bgShift: -25 };
|
|
} else if (code >= 95) {
|
|
// Thunderstorm - very dark, dramatic
|
|
return { cloudDark: 1.6, cloudLight: 0.4, skyShift: -30, bgShift: -35 };
|
|
}
|
|
return { cloudDark: 1.0, cloudLight: 1.0, skyShift: 0, bgShift: 0 };
|
|
}
|
|
|
|
var currentWeatherCode = -1;
|
|
|
|
function applyWeatherModifiers(c, code) {
|
|
var m = getWeatherModifiers(code);
|
|
return {
|
|
sky: c.sky,
|
|
cloud: adjustColor(c.cloud, m.cloudLight),
|
|
cloudShadow: adjustColor(c.cloudShadow, m.cloudDark),
|
|
sun: adjustColor(c.sun, m.cloudLight),
|
|
sunGlare: adjustColor(c.sunGlare, m.cloudLight),
|
|
sunLight: adjustColor(c.sunLight, m.cloudLight),
|
|
bg: shiftColor(c.bg, m.bgShift),
|
|
sunVis: c.sunVis
|
|
};
|
|
}
|
|
|
|
// ============ Clock & Sun/Moon Calculations ============
|
|
|
|
var hourHand = document.getElementById('hour-hand');
|
|
var minuteHand = document.getElementById('minute-hand');
|
|
var secondHand = document.getElementById('second-hand');
|
|
var hourMarks = document.getElementById('hour-marks');
|
|
var rpgSunArc = document.getElementById('sun-arc');
|
|
var rpgMoonArc = document.getElementById('moon-arc');
|
|
var rpgSun = document.getElementById('rpg-sun');
|
|
var rpgSunGlow = document.getElementById('rpg-sun-glow');
|
|
var rpgSunCore = document.getElementById('rpg-sun-core');
|
|
var rpgMoon = document.getElementById('rpg-moon');
|
|
var rpgMoonLit = document.getElementById('rpg-moon-lit');
|
|
var rpgMoonDark = document.getElementById('rpg-moon-dark');
|
|
var sunRiseArrow = document.getElementById('sun-rise-arrow');
|
|
var sunSetArrow = document.getElementById('sun-set-arrow');
|
|
var moonRiseArrow = document.getElementById('moon-rise-arrow');
|
|
var moonsetArrow = document.getElementById('moon-set-arrow');
|
|
var sunZenithMarker = document.getElementById('sun-zenith-marker');
|
|
|
|
var SUN_ORBIT_RADIUS = 65;
|
|
var MOON_ORBIT_RADIUS = 88;
|
|
|
|
// Generate hour marks
|
|
for (var i = 0; i < 12; i++) {
|
|
var angle = (i * 30) * Math.PI / 180;
|
|
var isMain = i % 3 === 0;
|
|
var innerR = isMain ? 118 : 122;
|
|
var outerR = isMain ? 130 : 128;
|
|
var x1 = 140 + Math.sin(angle) * innerR;
|
|
var y1 = 140 - Math.cos(angle) * innerR;
|
|
var x2 = 140 + Math.sin(angle) * outerR;
|
|
var y2 = 140 - Math.cos(angle) * outerR;
|
|
var line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
|
|
line.setAttribute('x1', x1);
|
|
line.setAttribute('y1', y1);
|
|
line.setAttribute('x2', x2);
|
|
line.setAttribute('y2', y2);
|
|
line.setAttribute('stroke', ASH.bone);
|
|
line.setAttribute('stroke-width', isMain ? 5 : 3);
|
|
line.setAttribute('stroke-linecap', 'round');
|
|
hourMarks.appendChild(line);
|
|
}
|
|
|
|
function formatTimeHM(date) {
|
|
if (!date || isNaN(date.getTime())) return '--:--';
|
|
var h = date.getHours();
|
|
var m = date.getMinutes();
|
|
return (h < 10 ? '0' : '') + h + ':' + (m < 10 ? '0' : '') + m;
|
|
}
|
|
|
|
function formatTimeHMS(h, m, s) {
|
|
return (h < 10 ? '0' : '') + h + ':' + (m < 10 ? '0' : '') + m + ':' + (s < 10 ? '0' : '') + s;
|
|
}
|
|
|
|
function pad2(n) { return n < 10 ? '0' + n : '' + n; }
|
|
function pad3(n) { return n < 10 ? '00' + n : n < 100 ? '0' + n : '' + n; }
|
|
|
|
function formatDateTime(format, date) {
|
|
// Strict ISO 8601 format tokens (case-sensitive):
|
|
// YYYY 4-digit year (2026)
|
|
// MM month 01-12 (10) -- always month, never minute
|
|
// DD day 01-31 (07)
|
|
// HH hour 00-23 (14)
|
|
// mm minute 00-59 (35) -- lowercase
|
|
// ss second 00-59 (22) -- lowercase
|
|
// sss millisecond 000-999 (456)
|
|
// T literal 'T'
|
|
// Any other character is a literal (including whitespace).
|
|
//
|
|
// Non-standard extension tokens (lookup in i18n dict):
|
|
// DDD weekday short name (Mo., Tu., ...) from i18n
|
|
// dddd weekday full name (Monday, Tuesday,...) from i18n
|
|
// MMM month short name (Jan., Feb., ...) from i18n
|
|
// MMMM month full name (January, ...) from i18n
|
|
//
|
|
// Escape sequences are also evaluated:
|
|
// \n newline
|
|
// \t tab
|
|
// \\ literal backslash
|
|
var result = '';
|
|
var i = 0;
|
|
var weekdayKeys = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
|
var weekdayFull = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
|
|
var monthKeys = ['January', 'February', 'March', 'April', 'May', 'June',
|
|
'July', 'August', 'September', 'October', 'November', 'December'];
|
|
while (i < format.length) {
|
|
var ch = format.charAt(i);
|
|
// Handle escape sequences \n, \t, \\
|
|
if (ch === '\\' && i + 1 < format.length) {
|
|
var next = format.charAt(i + 1);
|
|
if (next === 'n') result += '\n';
|
|
else if (next === 't') result += '\t';
|
|
else if (next === '\\') result += '\\';
|
|
else result += ch + next;
|
|
i += 2;
|
|
continue;
|
|
}
|
|
if (ch === 'Y' && format.substr(i, 4) === 'YYYY') {
|
|
result += date.getFullYear();
|
|
i += 4;
|
|
} else if (ch === 'Y') {
|
|
result += date.getFullYear();
|
|
i += 1;
|
|
} else if (ch === 'M' && format.substr(i, 4) === 'MMMM') {
|
|
var tMonthFull = t(monthKeys[date.getMonth()]);
|
|
console.log('MMMM token', date.getMonth(), '->', tMonthFull, 'lang=', window.CONFIG && window.CONFIG.language);
|
|
result += tMonthFull;
|
|
i += 4;
|
|
} else if (ch === 'M' && format.substr(i, 3) === 'MMM') {
|
|
result += t(monthKeys[date.getMonth()].substring(0, 3));
|
|
i += 3;
|
|
} else if (ch === 'M' && format.substr(i, 2) === 'MM') {
|
|
result += pad2(date.getMonth() + 1);
|
|
i += 2;
|
|
} else if (ch === 'D' && format.substr(i, 4) === 'dddd') {
|
|
result += t(weekdayFull[date.getDay()]);
|
|
i += 4;
|
|
} else if (ch === 'D' && format.substr(i, 3) === 'DDD') {
|
|
result += t(weekdayKeys[date.getDay()]);
|
|
i += 3;
|
|
} else if (ch === 'D' && format.substr(i, 2) === 'DD') {
|
|
result += pad2(date.getDate());
|
|
i += 2;
|
|
} else if (ch === 'H' && format.substr(i, 2) === 'HH') {
|
|
result += pad2(date.getHours());
|
|
i += 2;
|
|
} else if (ch === 'm' && format.substr(i, 2) === 'mm') {
|
|
result += pad2(date.getMinutes());
|
|
i += 2;
|
|
} else if (ch === 's' && format.substr(i, 3) === 'sss') {
|
|
result += pad3(date.getMilliseconds());
|
|
i += 3;
|
|
} else if ((ch === 's' || ch === 'S') && format.substr(i, 2).toLowerCase() === 'ss') {
|
|
result += pad2(date.getSeconds());
|
|
i += 2;
|
|
} else if (ch === 'a' && format.substr(i, 4) === 'aaaa') {
|
|
// Full am/pm text (lowercase)
|
|
result += date.getHours() < 12 ? 'am' : 'pm';
|
|
i += 4;
|
|
} else if (ch === 'A' && format.substr(i, 4) === 'AAAA') {
|
|
// Full AM/PM text (uppercase)
|
|
result += date.getHours() < 12 ? 'AM' : 'PM';
|
|
i += 4;
|
|
} else if (ch === 'a') {
|
|
// am/pm (lowercase)
|
|
result += date.getHours() < 12 ? 'am' : 'pm';
|
|
i += 1;
|
|
} else if (ch === 'A') {
|
|
// AM/PM (uppercase)
|
|
result += date.getHours() < 12 ? 'AM' : 'PM';
|
|
i += 1;
|
|
} else {
|
|
result += ch;
|
|
i += 1;
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function formatDateShort(date) {
|
|
var d = date.getDate();
|
|
var mo = date.getMonth() + 1;
|
|
return (d < 10 ? '0' : '') + d + '.' + (mo < 10 ? '0' : '') + mo + '.';
|
|
}
|
|
|
|
function formatDateLong(date) {
|
|
var days = ['So.', 'Mo.', 'Di.', 'Mi.', 'Do.', 'Fr.', 'Sa.'];
|
|
var d = date.getDate();
|
|
var mo = date.getMonth() + 1;
|
|
return days[date.getDay()] + ' ' + (d < 10 ? '0' : '') + d + '.' + (mo < 10 ? '0' : '') + mo + '.';
|
|
}
|
|
|
|
function getNextEvent(todayEvent, now, lat, lon, type) {
|
|
if (!todayEvent || isNaN(todayEvent.getTime())) {
|
|
return { date: null, label: '' };
|
|
}
|
|
if (todayEvent > now) {
|
|
return { date: todayEvent, label: 'heute' };
|
|
}
|
|
for (var d = 1; d <= 7; d++) {
|
|
var nextDate = new Date(now);
|
|
nextDate.setDate(nextDate.getDate() + d);
|
|
var times = type.indexOf('moon') === 0 ? SunCalc.getMoonTimes(nextDate, lat, lon) : SunCalc.getTimes(nextDate, lat, lon);
|
|
var ev = type === 'moonrise' ? times.rise : type === 'moonset' ? times.set : times[type];
|
|
if (ev && !isNaN(ev.getTime())) {
|
|
var dayLabel = d === 1 ? t('tomorrow') : formatDateLong(nextDate);
|
|
return { date: ev, label: dayLabel };
|
|
}
|
|
}
|
|
return { date: null, label: '' };
|
|
}
|
|
|
|
function getMoonPhaseText(phase) {
|
|
var p = phase;
|
|
var keys = ['New Moon', 'Waxing Crescent', 'First Quarter', 'Waxing Gibbous',
|
|
'Full Moon', 'Waning Gibbous', 'Last Quarter', 'Waning Crescent'];
|
|
var idx;
|
|
if (p < 0.0625 || p >= 0.9375) idx = 0;
|
|
else if (p < 0.1875) idx = 1;
|
|
else if (p < 0.3125) idx = 2;
|
|
else if (p < 0.4375) idx = 3;
|
|
else if (p < 0.5625) idx = 4;
|
|
else if (p < 0.6875) idx = 5;
|
|
else if (p < 0.8125) idx = 6;
|
|
else idx = 7;
|
|
return t(keys[idx]);
|
|
}
|
|
|
|
var CENTER_X = 140;
|
|
var CENTER_Y = 140;
|
|
|
|
function angleForTime(date) {
|
|
// 0:00 = bottom, 6:00 = left, 12:00 = top, 18:00 = right
|
|
var hours = date.getHours() + date.getMinutes() / 60;
|
|
return ((hours / 24) * 360 + 90) * Math.PI / 180;
|
|
}
|
|
|
|
// The Moon advances ~12.19° per day relative to the Sun (synodic month
|
|
// ~29.53 days). Use SunCalc.getMoonIllumination().phase as the offset.
|
|
// phase=0: New moon (same direction as Sun)
|
|
// phase=0.5: Full moon (opposite the Sun)
|
|
function moonAngleForTime(date) {
|
|
var moonIllum = SunCalc.getMoonIllumination(date);
|
|
var hours = date.getHours() + date.getMinutes() / 60;
|
|
var hourAngle = (hours / 24) * 360 + 90;
|
|
// Moon is roughly at hourAngle + phase * 360
|
|
return ((hourAngle + moonIllum.phase * 360) % 360) * Math.PI / 180;
|
|
}
|
|
|
|
function pointOnCircle(date, radius) {
|
|
var angle = angleForTime(date);
|
|
return {
|
|
x: CENTER_X + Math.cos(angle) * radius,
|
|
y: CENTER_Y + Math.sin(angle) * radius
|
|
};
|
|
}
|
|
|
|
function arcBetween(startDate, endDate, radius) {
|
|
var sp = pointOnCircle(startDate, radius);
|
|
var ep = pointOnCircle(endDate, radius);
|
|
var startA = angleForTime(startDate) * 180 / Math.PI;
|
|
var endA = angleForTime(endDate) * 180 / Math.PI;
|
|
var diff = endA - startA;
|
|
if (diff < 0) diff += 360;
|
|
var largeArc = diff > 180 ? 1 : 0;
|
|
return 'M ' + sp.x + ' ' + sp.y + ' A ' + radius + ' ' + radius + ' 0 ' + largeArc + ' 1 ' + ep.x + ' ' + ep.y;
|
|
}
|
|
|
|
function getMoonPosition(now, moonTimes) {
|
|
try {
|
|
var rise = moonTimes.rise;
|
|
var set = moonTimes.set;
|
|
|
|
if (!rise || !set) {
|
|
return { point: pointOnCircle(now, MOON_ORBIT_RADIUS), isVisible: false };
|
|
}
|
|
|
|
var isVisible;
|
|
if (rise <= set) {
|
|
isVisible = (now >= rise && now <= set);
|
|
} else {
|
|
isVisible = (now >= rise || now <= set);
|
|
}
|
|
|
|
if (!isVisible) {
|
|
return { point: pointOnCircle(now, MOON_ORBIT_RADIUS), isVisible: false };
|
|
}
|
|
|
|
var riseAngle = angleForTime(rise);
|
|
var setAngle = angleForTime(set);
|
|
|
|
var diff = setAngle - riseAngle;
|
|
if (diff > Math.PI) diff -= 2 * Math.PI;
|
|
if (diff < -Math.PI) diff += 2 * Math.PI;
|
|
|
|
var progress;
|
|
if (rise <= set) {
|
|
progress = (now.getTime() - rise.getTime()) / (set.getTime() - rise.getTime());
|
|
} else {
|
|
if (now >= rise) {
|
|
var endMs = set.getTime() + 24 * 3600 * 1000;
|
|
progress = (now.getTime() - rise.getTime()) / (endMs - rise.getTime());
|
|
} else {
|
|
progress = (now.getTime() + 24 * 3600 * 1000 - rise.getTime()) /
|
|
(set.getTime() + 24 * 3600 * 1000 - rise.getTime());
|
|
}
|
|
}
|
|
|
|
var moonAngle = riseAngle + diff * progress;
|
|
return {
|
|
point: {
|
|
x: CENTER_X + Math.cos(moonAngle) * MOON_ORBIT_RADIUS,
|
|
y: CENTER_Y + Math.sin(moonAngle) * MOON_ORBIT_RADIUS
|
|
},
|
|
isVisible: true
|
|
};
|
|
} catch (e) {
|
|
return { point: pointOnCircle(now, MOON_ORBIT_RADIUS), isVisible: false };
|
|
}
|
|
}
|
|
|
|
function renderMoonPhaseIcon(phase, fraction) {
|
|
var icon = document.getElementById('moon-phase-icon');
|
|
icon.innerHTML = '';
|
|
|
|
var r = 9;
|
|
var bg = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
|
|
bg.setAttribute('cx', '12'); bg.setAttribute('cy', '12'); bg.setAttribute('r', r);
|
|
bg.setAttribute('fill', ASH.panel);
|
|
bg.setAttribute('stroke', ASH.stone);
|
|
bg.setAttribute('stroke-width', '0.5');
|
|
icon.appendChild(bg);
|
|
|
|
var rx = r * (1 - 2 * fraction);
|
|
|
|
var path;
|
|
if (phase < 0.5) {
|
|
if (rx > 0) {
|
|
path = 'M 12 3 A 9 9 0 0 0 12 21 A ' + rx + ' 9 0 0 1 12 3 Z';
|
|
} else {
|
|
path = 'M 12 3 A 9 9 0 0 0 12 21 A ' + Math.abs(rx) + ' 9 0 0 0 12 3 Z';
|
|
}
|
|
} else {
|
|
if (rx > 0) {
|
|
path = 'M 12 3 A 9 9 0 0 1 12 21 A ' + rx + ' 9 0 0 0 12 3 Z';
|
|
} else {
|
|
path = 'M 12 3 A 9 9 0 0 1 12 21 A ' + Math.abs(rx) + ' 9 0 0 1 12 3 Z';
|
|
}
|
|
}
|
|
|
|
var lit = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
lit.setAttribute('d', path);
|
|
lit.setAttribute('fill', ASH.bone);
|
|
icon.appendChild(lit);
|
|
}
|
|
|
|
function renderRpgMoon(phase, fraction) {
|
|
var r = 10;
|
|
rpgMoonDark.setAttribute('d', '');
|
|
|
|
var rx = r * (1 - 2 * fraction);
|
|
|
|
var path;
|
|
if (phase < 0.5) {
|
|
if (rx > 0) {
|
|
path = 'M 0 -10 A 10 10 0 0 0 0 10 A ' + rx + ' 10 0 0 1 0 -10 Z';
|
|
} else {
|
|
path = 'M 0 -10 A 10 10 0 0 0 0 10 A ' + Math.abs(rx) + ' 10 0 0 0 0 -10 Z';
|
|
}
|
|
} else {
|
|
if (rx > 0) {
|
|
path = 'M 0 -10 A 10 10 0 0 1 0 10 A ' + rx + ' 10 0 0 0 0 -10 Z';
|
|
} else {
|
|
path = 'M 0 -10 A 10 10 0 0 1 0 10 A ' + Math.abs(rx) + ' 10 0 0 1 0 -10 Z';
|
|
}
|
|
}
|
|
|
|
rpgMoonLit.setAttribute('d', path);
|
|
}
|
|
|
|
function updateClockAndEvents() {
|
|
var now = new Date();
|
|
|
|
// Calculate real day progress based on sun position
|
|
// 0% = midnight, 50% = noon, 100% = next midnight
|
|
var sunTimes = SunCalc.getTimes(now, PASSAU_LAT, PASSAU_LON);
|
|
var sunrise = sunTimes.sunrise;
|
|
var sunset = sunTimes.sunset;
|
|
if (now < sunrise) {
|
|
var prevSunset = new Date(sunset.getTime() - 24 * 3600 * 1000);
|
|
realDayProgress = (now.getTime() - prevSunset.getTime()) / (sunrise.getTime() - prevSunset.getTime());
|
|
} else if (now < sunset) {
|
|
realDayProgress = 0.5 + (now.getTime() - sunrise.getTime()) / (sunset.getTime() - sunrise.getTime()) * 0.5;
|
|
} else {
|
|
realDayProgress = 0.5 + (now.getTime() - sunset.getTime()) / (sunrise.getTime() + 24 * 3600 * 1000 - sunset.getTime()) * 0.5;
|
|
}
|
|
realDayProgress = Math.max(0, Math.min(1, realDayProgress));
|
|
|
|
// Real-time clock hands and digital
|
|
var h = now.getHours();
|
|
var m = now.getMinutes();
|
|
var s = now.getSeconds();
|
|
|
|
document.getElementById('digital-clock').textContent = formatDateTime((window.CONFIG && window.CONFIG.timeFormat) || 'HH:mm:ss', now);
|
|
|
|
var hourAngle = ((h % 12) + m / 60) * 30;
|
|
var minuteAngle = (m + s / 60) * 6;
|
|
var secondAngle = s * 6;
|
|
|
|
hourHand.setAttribute('transform', 'rotate(' + hourAngle + ' 140 140)');
|
|
minuteHand.setAttribute('transform', 'rotate(' + minuteAngle + ' 140 140)');
|
|
secondHand.setAttribute('transform', 'rotate(' + secondAngle + ' 140 140)');
|
|
|
|
// Sun times
|
|
var sunTimes = SunCalc.getTimes(now, PASSAU_LAT, PASSAU_LON);
|
|
var sunriseNext = getNextEvent(sunTimes.sunrise, now, PASSAU_LAT, PASSAU_LON, 'sunrise');
|
|
var sunsetNext = getNextEvent(sunTimes.sunset, now, PASSAU_LAT, PASSAU_LON, 'sunset');
|
|
|
|
document.getElementById('sunrise-value').textContent = formatTimeHM(sunriseNext.date);
|
|
document.getElementById('sunrise-when').textContent = sunriseNext.label ? '· ' + sunriseNext.label : '';
|
|
document.getElementById('sunset-value').textContent = formatTimeHM(sunsetNext.date);
|
|
document.getElementById('sunset-when').textContent = sunsetNext.label ? '· ' + sunsetNext.label : '';
|
|
|
|
// Sun zenith (solar noon) - find next occurrence
|
|
var sunZenith = sunTimes.solarNoon;
|
|
var sunZenithWhen = sunZenith > now ? t('today') : t('tomorrow');
|
|
if (sunZenith <= now) {
|
|
var tomorrow = new Date(now);
|
|
tomorrow.setDate(tomorrow.getDate() + 1);
|
|
sunZenith = SunCalc.getTimes(tomorrow, PASSAU_LAT, PASSAU_LON).solarNoon;
|
|
sunZenithWhen = formatDateLong(tomorrow);
|
|
}
|
|
document.getElementById('sun-zenith-value').textContent = formatTimeHM(sunZenith);
|
|
document.getElementById('sun-zenith-when').textContent = '· ' + sunZenithWhen;
|
|
|
|
// Moon times
|
|
var moonTimes = SunCalc.getMoonTimes(now, PASSAU_LAT, PASSAU_LON);
|
|
var moonriseNext = getNextEvent(moonTimes.rise, now, PASSAU_LAT, PASSAU_LON, 'moonrise');
|
|
var moonsetNext = getNextEvent(moonTimes.set, now, PASSAU_LAT, PASSAU_LON, 'moonset');
|
|
|
|
document.getElementById('moonrise-value').textContent = formatTimeHM(moonriseNext.date);
|
|
document.getElementById('moonrise-when').textContent = moonriseNext.label ? '· ' + moonriseNext.label : '';
|
|
document.getElementById('moonset-value').textContent = formatTimeHM(moonsetNext.date);
|
|
document.getElementById('moonset-when').textContent = moonsetNext.label ? '· ' + moonsetNext.label : '';
|
|
|
|
// Sun arc
|
|
if (sunTimes.sunrise && sunTimes.sunset) {
|
|
rpgSunArc.setAttribute('d', arcBetween(sunTimes.sunrise, sunTimes.sunset, SUN_ORBIT_RADIUS));
|
|
}
|
|
|
|
// Moon arc
|
|
if (moonTimes.rise && moonTimes.set) {
|
|
rpgMoonArc.setAttribute('d', arcBetween(moonTimes.rise, moonTimes.set, MOON_ORBIT_RADIUS));
|
|
}
|
|
|
|
// Sun position
|
|
var sunP = pointOnCircle(now, SUN_ORBIT_RADIUS);
|
|
rpgSun.setAttribute('cx', sunP.x);
|
|
rpgSun.setAttribute('cy', sunP.y);
|
|
rpgSunGlow.setAttribute('cx', sunP.x);
|
|
rpgSunGlow.setAttribute('cy', sunP.y);
|
|
rpgSunCore.setAttribute('cx', sunP.x);
|
|
rpgSunCore.setAttribute('cy', sunP.y);
|
|
|
|
var isDaytime = now >= sunTimes.sunrise && now <= sunTimes.sunset;
|
|
rpgSun.setAttribute('opacity', isDaytime ? '1' : '0.35');
|
|
rpgSunGlow.setAttribute('opacity', isDaytime ? '0.4' : '0.15');
|
|
rpgSunCore.setAttribute('opacity', isDaytime ? '1' : '0.35');
|
|
|
|
// Moon position - based on current time on the orbit
|
|
var moonAngle = moonAngleForTime(now);
|
|
var moonP = {
|
|
x: CENTER_X + Math.cos(moonAngle) * MOON_ORBIT_RADIUS,
|
|
y: CENTER_Y + Math.sin(moonAngle) * MOON_ORBIT_RADIUS
|
|
};
|
|
rpgMoon.setAttribute('transform', 'translate(' + moonP.x + ', ' + moonP.y + ')');
|
|
|
|
var moonVisible = false;
|
|
if (moonTimes.rise && moonTimes.set) {
|
|
if (moonTimes.rise < moonTimes.set) {
|
|
moonVisible = (now >= moonTimes.rise && now <= moonTimes.set);
|
|
} else {
|
|
moonVisible = (now >= moonTimes.rise || now <= moonTimes.set);
|
|
}
|
|
}
|
|
rpgMoon.setAttribute('opacity', moonVisible ? '1' : '0.6');
|
|
|
|
// Sun rise/set arrows
|
|
var srP = pointOnCircle(sunTimes.sunrise, SUN_ORBIT_RADIUS);
|
|
sunRiseArrow.setAttribute('transform', 'translate(' + srP.x + ', ' + srP.y + ')');
|
|
var ssP = pointOnCircle(sunTimes.sunset, SUN_ORBIT_RADIUS);
|
|
sunSetArrow.setAttribute('transform', 'translate(' + ssP.x + ', ' + ssP.y + ')');
|
|
|
|
// Sun zenith marker (next occurrence)
|
|
var sunZenithDate = sunTimes.solarNoon > now ? sunTimes.solarNoon : SunCalc.getTimes(new Date(now.getTime() + 86400000), PASSAU_LAT, PASSAU_LON).solarNoon;
|
|
var szP = pointOnCircle(sunZenithDate, SUN_ORBIT_RADIUS);
|
|
sunZenithMarker.setAttribute('transform', 'translate(' + szP.x + ', ' + szP.y + ')');
|
|
|
|
// Moon rise/set arrows
|
|
if (moonTimes.rise) {
|
|
var mrP = pointOnCircle(moonTimes.rise, MOON_ORBIT_RADIUS);
|
|
moonRiseArrow.setAttribute('transform', 'translate(' + mrP.x + ', ' + mrP.y + ')');
|
|
moonRiseArrow.setAttribute('opacity', '1');
|
|
} else {
|
|
moonRiseArrow.setAttribute('opacity', '0');
|
|
}
|
|
if (moonTimes.set) {
|
|
var msP = pointOnCircle(moonTimes.set, MOON_ORBIT_RADIUS);
|
|
moonsetArrow.setAttribute('transform', 'translate(' + msP.x + ', ' + msP.y + ')');
|
|
moonsetArrow.setAttribute('opacity', '1');
|
|
} else {
|
|
moonsetArrow.setAttribute('opacity', '0');
|
|
}
|
|
|
|
// Moon phase
|
|
var moonIllum = SunCalc.getMoonIllumination(now);
|
|
document.getElementById('moon-phase-text').textContent =
|
|
getMoonPhaseText(moonIllum.phase) + ' ' + Math.round(moonIllum.fraction * 100) + '%';
|
|
renderMoonPhaseIcon(moonIllum.phase, moonIllum.fraction);
|
|
renderRpgMoon(moonIllum.phase, moonIllum.fraction);
|
|
}
|
|
|
|
var startTime = Date.now();
|
|
|
|
window.weatherThemes = {
|
|
sunny: {
|
|
skyColor: hexToInt(ASH.blueLight),
|
|
cloudColor: hexToInt(ASH.stone),
|
|
lightColor: hexToInt(ASH.chalk),
|
|
backgroundColor: hexToInt(ASH.bone),
|
|
speed: 1.0
|
|
},
|
|
cloudy: {
|
|
skyColor: hexToInt(ASH.blue),
|
|
cloudColor: hexToInt(ASH.mid),
|
|
lightColor: hexToInt(ASH.bone),
|
|
backgroundColor: hexToInt(ASH.border),
|
|
speed: 1.2
|
|
},
|
|
stormy: {
|
|
skyColor: hexToInt(ASH.steel),
|
|
cloudColor: hexToInt(ASH.panel),
|
|
lightColor: hexToInt(ASH.stone),
|
|
backgroundColor: hexToInt(ASH.void),
|
|
speed: 2.5
|
|
},
|
|
night: {
|
|
skyColor: hexToInt(ASH.void),
|
|
cloudColor: hexToInt(ASH.raised),
|
|
lightColor: hexToInt(ASH.icon),
|
|
backgroundColor: hexToInt(ASH.void),
|
|
speed: 0.4
|
|
}
|
|
};
|
|
|
|
function getWeatherThemeName(code) {
|
|
if (code === 0) return 'sunny';
|
|
if (code >= 1 && code <= 3) return 'cloudy';
|
|
if (code >= 45 && code <= 48) return 'cloudy';
|
|
if ((code >= 51 && code <= 67) || (code >= 80 && code <= 82) || (code >= 71 && code <= 77) || (code >= 85 && code <= 86)) return 'stormy';
|
|
if (code >= 95) return 'stormy';
|
|
return 'cloudy';
|
|
}
|
|
|
|
function mixColor(color1, color2, factor) {
|
|
if (color1 === undefined || color2 === undefined) return color1 || color2;
|
|
var r1 = (color1 >> 16) & 255;
|
|
var g1 = (color1 >> 8) & 255;
|
|
var b1 = color1 & 255;
|
|
var r2 = (color2 >> 16) & 255;
|
|
var g2 = (color2 >> 8) & 255;
|
|
var b2 = color2 & 255;
|
|
var r = Math.round(r1 + (r2 - r1) * factor);
|
|
var g = Math.round(g1 + (g2 - g1) * factor);
|
|
var b = Math.round(b1 + (b2 - b1) * factor);
|
|
return (r << 16) | (g << 8) | b;
|
|
}
|
|
|
|
var prevThemeColors = null;
|
|
var targetThemeColors = null;
|
|
var transitionStartTime = 0;
|
|
var TRANSITION_DURATION = 5 * 60 * 1000;
|
|
var currentWeatherThemeName = null;
|
|
var currentThemeSpeed = 0.4;
|
|
|
|
function setWeatherTheme(themeName) {
|
|
if (currentWeatherThemeName !== themeName) {
|
|
prevThemeColors = getEffectiveThemeColors();
|
|
targetThemeColors = window.weatherThemes[themeName] || window.weatherThemes.cloudy;
|
|
transitionStartTime = Date.now();
|
|
currentWeatherThemeName = themeName;
|
|
}
|
|
}
|
|
|
|
function getEffectiveThemeColors() {
|
|
if (!targetThemeColors) return null;
|
|
|
|
var elapsed = Date.now() - transitionStartTime;
|
|
var smoothT;
|
|
if (!prevThemeColors || elapsed >= TRANSITION_DURATION) {
|
|
smoothT = 1;
|
|
} else {
|
|
var t = elapsed / TRANSITION_DURATION;
|
|
smoothT = t * t * (3 - 2 * t);
|
|
}
|
|
|
|
var result = {};
|
|
var keys = ['skyColor', 'cloudColor', 'lightColor', 'backgroundColor'];
|
|
for (var i = 0; i < keys.length; i++) {
|
|
var k = keys[i];
|
|
if (prevThemeColors && smoothT < 1) {
|
|
result[k] = mixColor(prevThemeColors[k], targetThemeColors[k], smoothT);
|
|
} else {
|
|
result[k] = targetThemeColors[k];
|
|
}
|
|
}
|
|
if (smoothT >= 1 && prevThemeColors !== targetThemeColors) {
|
|
prevThemeColors = targetThemeColors;
|
|
currentThemeSpeed = targetThemeColors.speed;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function applyWeatherTheme(c) {
|
|
var themeColors = getEffectiveThemeColors();
|
|
if (!themeColors) return c;
|
|
|
|
// At night, weather only tints slightly - we don't want a
|
|
// 'cloudy' sky colour washing over deep night sky and
|
|
// making it look bright.
|
|
var _sunPos = SunCalc.getPosition(new Date(), PASSAU_LAT, PASSAU_LON);
|
|
var nightFactor = Math.max(0, Math.min(1, -_sunPos.altitude / 0.4));
|
|
var mix = 0.6 * (1 - nightFactor * 0.7);
|
|
|
|
return {
|
|
sky: mixColor(c.sky, themeColors.skyColor, mix),
|
|
cloud: mixColor(c.cloud, themeColors.cloudColor, mix),
|
|
cloudShadow: c.cloudShadow !== undefined
|
|
? mixColor(c.cloudShadow, themeColors.cloudColor, mix * 0.8)
|
|
: mixColor(c.cloud, themeColors.cloudColor, mix * 0.8),
|
|
light: mixColor(c.sun, themeColors.lightColor, mix * 0.5),
|
|
bg: mixColor(c.bg, themeColors.backgroundColor, mix)
|
|
};
|
|
}
|
|
|
|
function getCurrentDayProgress() {
|
|
var now = new Date();
|
|
var startOfDay = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0);
|
|
return (now.getTime() - startOfDay.getTime()) / 86400000;
|
|
}
|
|
|
|
function toggleDebugPanel() {
|
|
var content = document.getElementById('debug-content');
|
|
var title = document.querySelector('.debug-title');
|
|
var icon = document.getElementById('debug-toggle-icon');
|
|
var isCollapsed = content.classList.contains('collapsed');
|
|
if (isCollapsed) {
|
|
content.classList.remove('collapsed');
|
|
title.classList.add('expanded');
|
|
icon.textContent = '▼';
|
|
} else {
|
|
content.classList.add('collapsed');
|
|
title.classList.remove('expanded');
|
|
icon.textContent = '▶';
|
|
}
|
|
}
|
|
|
|
var UNITS_OPTIONS = [
|
|
{ value: 'metric', label: '°C, km/h' },
|
|
{ value: 'imperial', label: '°F, mph' }
|
|
];
|
|
|
|
var LANGUAGE_OPTIONS = [
|
|
{ value: 'en', label: 'EN' },
|
|
{ value: 'de', label: 'DE' },
|
|
{ value: 'it', label: 'IT' },
|
|
{ value: 'es', label: 'ES' },
|
|
{ value: 'fr', label: 'FR' },
|
|
{ value: 'ru', label: 'RU' }
|
|
];
|
|
|
|
function applyI18n() {
|
|
var els = document.querySelectorAll('[data-i18n]');
|
|
for (var i = 0; i < els.length; i++) {
|
|
var key = els[i].getAttribute('data-i18n');
|
|
els[i].textContent = t(key);
|
|
}
|
|
// Trigger a weather refresh so dynamic texts (slot labels, current
|
|
// weather details) are re-rendered with the new language.
|
|
if (typeof fetchWeather === 'function') fetchWeather();
|
|
}
|
|
|
|
function setDebugLanguage(lang) {
|
|
window.CONFIG.language = lang;
|
|
applyI18n();
|
|
// Trigger weather refresh to update day labels
|
|
if (typeof fetchWeather === 'function') fetchWeather();
|
|
// Force a debug update so the digital clock shows the new language
|
|
if (typeof updateDebug === 'function') {
|
|
var now = new Date();
|
|
updateDebug(0, null,
|
|
{ sky: 0, cloud: 0, light: 0, bg: 0 },
|
|
currentWeatherThemeName, realDayProgress);
|
|
}
|
|
}
|
|
|
|
function populateDebugSelects() {
|
|
// Theme select (from window.THEMES)
|
|
var themeSelect = document.getElementById('dbg-theme-select');
|
|
themeSelect.innerHTML = '';
|
|
if (window.THEMES) {
|
|
for (var key in window.THEMES) {
|
|
if (window.THEMES.hasOwnProperty(key)) {
|
|
var opt = document.createElement('option');
|
|
opt.value = key;
|
|
opt.textContent = key.charAt(0).toUpperCase() + key.slice(1);
|
|
if (key === window.CONFIG.theme) opt.selected = true;
|
|
themeSelect.appendChild(opt);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Units select
|
|
var unitsSelect = document.getElementById('dbg-units-select');
|
|
unitsSelect.innerHTML = '';
|
|
for (var i = 0; i < UNITS_OPTIONS.length; i++) {
|
|
var opt2 = document.createElement('option');
|
|
opt2.value = UNITS_OPTIONS[i].value;
|
|
opt2.textContent = UNITS_OPTIONS[i].label;
|
|
if (UNITS_OPTIONS[i].value === window.CONFIG.units) opt2.selected = true;
|
|
unitsSelect.appendChild(opt2);
|
|
}
|
|
|
|
// Language select
|
|
var langSelect = document.getElementById('dbg-language-select');
|
|
langSelect.innerHTML = '';
|
|
for (var j = 0; j < LANGUAGE_OPTIONS.length; j++) {
|
|
var opt3 = document.createElement('option');
|
|
opt3.value = LANGUAGE_OPTIONS[j].value;
|
|
opt3.textContent = LANGUAGE_OPTIONS[j].label;
|
|
if (LANGUAGE_OPTIONS[j].value === window.CONFIG.language) opt3.selected = true;
|
|
langSelect.appendChild(opt3);
|
|
}
|
|
}
|
|
|
|
function setDebugTheme(theme) {
|
|
var cssFile = window.THEMES && window.THEMES[theme];
|
|
var css = document.getElementById('theme-css');
|
|
if (css && cssFile) css.href = cssFile;
|
|
setTimeout(function() {
|
|
rebuildFromColors();
|
|
document.getElementById('dbg-palette-name').textContent = theme.charAt(0).toUpperCase() + theme.slice(1);
|
|
}, 50);
|
|
window.CONFIG.theme = theme;
|
|
}
|
|
|
|
function setDebugUnits(units) {
|
|
window.CONFIG.units = units;
|
|
// Refresh weather to display new units
|
|
if (typeof fetchWeather === 'function') fetchWeather();
|
|
}
|
|
|
|
function updateDebug(t, palette, vantaColors, themeName, realDayProgress) {
|
|
var now = new Date();
|
|
var hh = now.getHours(), mm = now.getMinutes(), ss = now.getSeconds();
|
|
document.getElementById('dbg-time').textContent =
|
|
(hh < 10 ? '0' : '') + hh + ':' + (mm < 10 ? '0' : '') + mm + ':' + (ss < 10 ? '0' : '') + ss;
|
|
document.getElementById('dbg-day').textContent = realDayProgress != null ? (realDayProgress * 100).toFixed(1) + '%' : (t * 100).toFixed(1) + '%';
|
|
document.getElementById('dbg-weather').textContent =
|
|
currentWeatherCode + ' (' + (window.weatherThemes[themeName] ? themeName : '?') + ')';
|
|
|
|
// RPG angles
|
|
var sunTimes = SunCalc.getTimes(now, PASSAU_LAT, PASSAU_LON);
|
|
var moonTimes = SunCalc.getMoonTimes(now, PASSAU_LAT, PASSAU_LON);
|
|
document.getElementById('dbg-sun-rise-angle').textContent = (angleForTime(sunTimes.sunrise) * 180 / Math.PI).toFixed(1) + '°';
|
|
document.getElementById('dbg-sun-set-angle').textContent = (angleForTime(sunTimes.sunset) * 180 / Math.PI).toFixed(1) + '°';
|
|
document.getElementById('dbg-sun-now-angle').textContent = (angleForTime(now) * 180 / Math.PI).toFixed(1) + '°';
|
|
if (moonTimes.rise) {
|
|
document.getElementById('dbg-moon-rise-angle').textContent = (angleForTime(moonTimes.rise) * 180 / Math.PI).toFixed(1) + '°';
|
|
}
|
|
if (moonTimes.set) {
|
|
document.getElementById('dbg-moon-set-angle').textContent = (angleForTime(moonTimes.set) * 180 / Math.PI).toFixed(1) + '°';
|
|
}
|
|
document.getElementById('dbg-moon-now-angle').textContent = (moonAngleForTime(now) * 180 / Math.PI).toFixed(1) + '°';
|
|
document.getElementById('dbg-theme').textContent = themeName || '-';
|
|
document.getElementById('dbg-speed').textContent = currentThemeSpeed.toFixed(2);
|
|
|
|
var elapsed = Date.now() - transitionStartTime;
|
|
var transPct = targetThemeColors ? Math.min(100, (elapsed / TRANSITION_DURATION) * 100) : 100;
|
|
document.getElementById('dbg-trans').textContent = transPct.toFixed(0) + '%';
|
|
|
|
var themeColors = getEffectiveThemeColors();
|
|
if (themeColors) {
|
|
var keys = ['skyColor', 'cloudColor', 'lightColor', 'backgroundColor'];
|
|
var keyMap = {skyColor: 'sky', cloudColor: 'cloud', lightColor: 'sunLight', backgroundColor: 'bg'};
|
|
for (var i = 0; i < keys.length; i++) {
|
|
var k = keys[i];
|
|
var kk = keyMap[k];
|
|
var c = themeColors[k];
|
|
document.getElementById('dbg-t-' + kk).style.backgroundColor = '#' + ('00000' + c.toString(16)).slice(-6);
|
|
document.getElementById('dbg-h-' + kk).textContent = '#' + ('00000' + c.toString(16)).slice(-6);
|
|
}
|
|
}
|
|
|
|
if (vantaColors) {
|
|
var vKeys = ['sky', 'cloud', 'light', 'bg'];
|
|
var vKeyMap = {sky: 'sky', cloud: 'cloud', light: 'sunLight', bg: 'bg'};
|
|
for (var j = 0; j < vKeys.length; j++) {
|
|
var vk = vKeys[j];
|
|
var vc = vantaColors[vk];
|
|
var kk = vKeyMap[vk];
|
|
document.getElementById('dbg-v-' + kk).style.backgroundColor = '#' + ('00000' + vc.toString(16)).slice(-6);
|
|
document.getElementById('dbg-vh-' + kk).textContent = '#' + ('00000' + vc.toString(16)).slice(-6);
|
|
}
|
|
}
|
|
|
|
// Theme table
|
|
var themeNames = ['sunny', 'cloudy', 'stormy', 'night'];
|
|
var themeFields = [
|
|
{ key: 'skyColor', label: 'sky' },
|
|
{ key: 'cloudColor', label: 'cloud' },
|
|
{ key: 'lightColor', label: 'light' },
|
|
{ key: 'backgroundColor', label: 'bg' }
|
|
];
|
|
var tableHtml = '<div class="theme-table-row theme-table-header">';
|
|
tableHtml += '<span>Theme (Ash)</span>';
|
|
for (var t = 0; t < themeFields.length; t++) {
|
|
tableHtml += '<span title="' + themeFields[t].key + '">' + themeFields[t].label + '</span>';
|
|
}
|
|
tableHtml += '<span>speed</span>';
|
|
tableHtml += '</div>';
|
|
for (var n = 0; n < themeNames.length; n++) {
|
|
var tn = themeNames[n];
|
|
var th = window.weatherThemes[tn];
|
|
tableHtml += '<div class="theme-table-row">';
|
|
tableHtml += '<span class="theme-table-name">' + tn + '</span>';
|
|
for (var f = 0; f < themeFields.length; f++) {
|
|
var fc = th[themeFields[f].key];
|
|
tableHtml += '<div class="theme-table-mini-swatch" style="background:#' + ('00000' + fc.toString(16)).slice(-6) + '"></div>';
|
|
}
|
|
tableHtml += '<span class="theme-table-speed">' + th.speed + '</span>';
|
|
tableHtml += '</div>';
|
|
}
|
|
document.getElementById('dbg-theme-table').innerHTML = tableHtml;
|
|
|
|
// Color palette table - all colors from the palette
|
|
var colorKeys = [
|
|
'black', 'dark', 'gray-darker', 'gray-dark', 'gray', 'gray-light',
|
|
'icon', 'stone', 'bone', 'chalk',
|
|
'orange', 'yellow', 'red', 'brown', 'pink', 'green', 'blue', 'blue-light', 'purple'
|
|
];
|
|
var dbgRoot = getComputedStyle(document.documentElement);
|
|
var colorHtml = '<div class="theme-table-header">Palette</div>';
|
|
for (var k = 0; k < colorKeys.length; k++) {
|
|
var ck = colorKeys[k];
|
|
var cv = ASH[ck] || dbgRoot.getPropertyValue('--' + ck).trim();
|
|
colorHtml += '<div class="palette-table-row">' +
|
|
'<span class="theme-table-name">' + ck + '</span>' +
|
|
'<div class="theme-table-mini-swatch" style="background:' + cv + '"></div>' +
|
|
'<span class="theme-table-speed">' + cv + '</span>' +
|
|
'</div>';
|
|
}
|
|
document.getElementById('dbg-color-table').innerHTML = colorHtml;
|
|
}
|
|
|
|
function animate() {
|
|
var t = (Date.now() - startTime) / CYCLE % 1;
|
|
var c = getColor(t);
|
|
c = applyWeatherTheme(c);
|
|
// Expose current palette + weather info for the 2D renderer
|
|
window.__currentPalette = c;
|
|
window.__currentWeatherName = currentWeatherThemeName;
|
|
window.__currentDayProgress = realDayProgress;
|
|
window.__currentThemeSpeed = currentThemeSpeed;
|
|
// Sun altitude in radians. Negative = below horizon (night).
|
|
var _sunPos = SunCalc.getPosition(new Date(), PASSAU_LAT, PASSAU_LON);
|
|
window.__currentSunAlt = _sunPos.altitude;
|
|
effect.setOptions({
|
|
skyColor: c.sky,
|
|
cloudColor: c.cloud,
|
|
lightColor: c.light,
|
|
backgroundColor: c.bg,
|
|
backgroundAlpha: 0,
|
|
speed: currentThemeSpeed
|
|
});
|
|
document.body.style.backgroundColor = '#' + ('00000' + c.sky.toString(16)).slice(-6);
|
|
updateDebug(t, null, c, currentWeatherThemeName, realDayProgress);
|
|
requestAnimationFrame(animate);
|
|
}
|
|
|
|
function tick() {
|
|
try {
|
|
updateClockAndEvents();
|
|
} catch (e) {
|
|
console.error('Tick error:', e);
|
|
}
|
|
setTimeout(tick, 1000);
|
|
}
|
|
|
|
// ============ Weather ============
|
|
// Default location: London, UK
|
|
var DEFAULT_LAT = 51.5074;
|
|
var DEFAULT_LON = -0.1278;
|
|
|
|
var WEATHER_LAT = (window.CONFIG && window.CONFIG.location && window.CONFIG.location.lat) || DEFAULT_LAT;
|
|
var WEATHER_LON = (window.CONFIG && window.CONFIG.location && window.CONFIG.location.lon) || DEFAULT_LON;
|
|
|
|
// Try browser geolocation if no config location set
|
|
function initBrowserGeolocation() {
|
|
if (window.CONFIG && window.CONFIG.location && window.CONFIG.location.lat) return;
|
|
if (!navigator.geolocation) return;
|
|
navigator.geolocation.getCurrentPosition(
|
|
function(pos) {
|
|
WEATHER_LAT = pos.coords.latitude;
|
|
WEATHER_LON = pos.coords.longitude;
|
|
window.CONFIG.location = { lat: WEATHER_LAT, lon: WEATHER_LON };
|
|
// Reload weather with new coordinates
|
|
if (typeof fetchWeather === 'function') fetchWeather();
|
|
// Reverse geocode to get place name
|
|
reverseGeocode(WEATHER_LAT, WEATHER_LON);
|
|
},
|
|
function() { /* ignored - keep default */ },
|
|
{ timeout: 10000 }
|
|
);
|
|
}
|
|
|
|
// Reverse geocode lat/lon to a place name using Nominatim (OpenStreetMap).
|
|
// Free, no API key required. Has a 1 req/sec rate limit.
|
|
// Skipped if a custom label is already configured (luxury: user can
|
|
// set their own place name in CONFIG.location.label).
|
|
function reverseGeocode(lat, lon) {
|
|
if (window.CONFIG && window.CONFIG.location && window.CONFIG.location.label) return;
|
|
var url = 'https://nominatim.openstreetmap.org/reverse?format=json&lat=' +
|
|
lat + '&lon=' + lon + '&zoom=10&addressdetails=1';
|
|
fetch(url, {
|
|
headers: { 'Accept-Language': (window.CONFIG && window.CONFIG.language) || 'en' }
|
|
})
|
|
.then(function(r) { return r.ok ? r.json() : null; })
|
|
.then(function(data) {
|
|
if (data && data.address) {
|
|
var addr = data.address;
|
|
var name = addr.city || addr.town || addr.village || addr.hamlet ||
|
|
addr.suburb || addr.county || addr.state || data.display_name;
|
|
if (window.CONFIG && window.CONFIG.location) {
|
|
window.CONFIG.location.label = name;
|
|
}
|
|
}
|
|
})
|
|
.catch(function() { /* ignore errors */ });
|
|
}
|
|
|
|
// If config has lat/lon but no label, fetch the label
|
|
function fetchLocationLabel() {
|
|
if (!window.CONFIG || !window.CONFIG.location) return;
|
|
if (window.CONFIG.location.label) return;
|
|
if (!window.CONFIG.location.lat) return;
|
|
reverseGeocode(window.CONFIG.location.lat, window.CONFIG.location.lon);
|
|
}
|
|
|
|
// Get timezone offset based on user's local time
|
|
function getTimezoneOffsetMinutes() {
|
|
return new Date().getTimezoneOffset();
|
|
}
|
|
var weatherData = null;
|
|
|
|
var WMO_ICONS = {
|
|
0: 'sun',
|
|
1: 'sun-cloud',
|
|
2: 'cloud-sun',
|
|
3: 'cloud',
|
|
45: 'fog',
|
|
48: 'fog',
|
|
51: 'drizzle',
|
|
53: 'drizzle',
|
|
55: 'drizzle',
|
|
56: 'drizzle',
|
|
57: 'drizzle',
|
|
61: 'rain',
|
|
63: 'rain',
|
|
65: 'rain',
|
|
66: 'rain',
|
|
67: 'rain',
|
|
71: 'snow',
|
|
73: 'snow',
|
|
75: 'snow',
|
|
77: 'snow',
|
|
80: 'rain',
|
|
81: 'rain',
|
|
82: 'rain',
|
|
85: 'snow',
|
|
86: 'snow',
|
|
95: 'thunder',
|
|
96: 'thunder',
|
|
99: 'thunder'
|
|
};
|
|
|
|
function weatherSVG(code) {
|
|
var c = 'var(--bone)';
|
|
switch (code) {
|
|
case 0:
|
|
return '<svg viewBox="0 0 24 24" fill="' + c + '"><circle cx="12" cy="12" r="5"/><g stroke="' + c + '" stroke-width="2" stroke-linecap="round"><line x1="12" y1="2" x2="12" y2="4"/><line x1="12" y1="20" x2="12" y2="22"/><line x1="2" y1="12" x2="4" y2="12"/><line x1="20" y1="12" x2="22" y2="12"/><line x1="4.9" y1="4.9" x2="6.3" y2="6.3"/><line x1="17.7" y1="17.7" x2="19.1" y2="19.1"/><line x1="4.9" y1="19.1" x2="6.3" y2="17.7"/><line x1="17.7" y1="6.3" x2="19.1" y2="4.9"/></g></svg>';
|
|
case 1:
|
|
case 2:
|
|
return '<svg viewBox="0 0 24 24" fill="none" stroke="' + c + '" stroke-width="2"><circle cx="8" cy="8" r="3" fill="' + c + '"/><line x1="8" y1="2" x2="8" y2="3"/><line x1="2" y1="8" x2="3" y2="8"/><line x1="3.5" y1="3.5" x2="4.2" y2="4.2"/><line x1="12.5" y1="3.5" x2="11.8" y2="4.2"/><path d="M10 14 a4 4 0 0 1 4 -4 a4 4 0 0 1 4 4 a3 3 0 0 1 0 6 h-8 a3 3 0 0 1 0 -6 z" fill="var(--stone)" stroke="none"/></svg>';
|
|
case 3:
|
|
return '<svg viewBox="0 0 24 24" fill="var(--stone)"><path d="M6 18 a4 4 0 0 1 -2 -7 a5 5 0 0 1 9 -3 a4 4 0 0 1 7 1 a3 3 0 0 1 0 6 h-14 z"/></svg>';
|
|
case 45:
|
|
case 48:
|
|
return '<svg viewBox="0 0 24 24" fill="none" stroke="' + c + '" stroke-width="2"><path d="M6 18 a4 4 0 0 1 -2 -7 a5 5 0 0 1 9 -3 a4 4 0 0 1 7 1 a3 3 0 0 1 0 6 h-14 z" fill="var(--stone)" stroke="' + c + '"/><line x1="8" y1="20" x2="8" y2="22"/><line x1="12" y1="20" x2="12" y2="22"/><line x1="16" y1="20" x2="16" y2="22"/></svg>';
|
|
case 51:
|
|
case 55:
|
|
return '<svg viewBox="0 0 24 24" fill="var(--stone)"><path d="M6 14 a4 4 0 0 1 -2 -7 a5 5 0 0 1 9 -3 a4 4 0 0 1 7 1 a3 3 0 0 1 0 6 h-14 z"/><g stroke="var(--blue)" stroke-width="1.5" stroke-linecap="round"><line x1="8" y1="17" x2="7" y2="20"/><line x1="13" y1="17" x2="12" y2="20"/><line x1="17" y1="17" x2="16" y2="20"/></g></svg>';
|
|
case 53:
|
|
return '<svg viewBox="0 0 24 24" fill="var(--stone)"><path d="M6 14 a4 4 0 0 1 -2 -7 a5 5 0 0 1 9 -3 a4 4 0 0 1 7 1 a3 3 0 0 1 0 6 h-14 z"/><g stroke="var(--blue)" stroke-width="1.5" stroke-linecap="round"><line x1="9" y1="16" x2="8" y2="20"/><line x1="13" y1="16" x2="12" y2="20"/></g></svg>';
|
|
case 61:
|
|
case 80:
|
|
return '<svg viewBox="0 0 24 24" fill="var(--stone)"><path d="M6 13 a4 4 0 0 1 -2 -7 a5 5 0 0 1 9 -3 a4 4 0 0 1 7 1 a3 3 0 0 1 0 6 h-14 z"/><g stroke="var(--blue)" stroke-width="1.5" stroke-linecap="round"><line x1="8" y1="16" x2="7" y2="21"/><line x1="13" y1="16" x2="12" y2="21"/><line x1="17" y1="16" x2="16" y2="21"/></g></svg>';
|
|
case 63:
|
|
case 81:
|
|
return '<svg viewBox="0 0 24 24" fill="var(--stone)"><path d="M6 13 a4 4 0 0 1 -2 -7 a5 5 0 0 1 9 -3 a4 4 0 0 1 7 1 a3 3 0 0 1 0 6 h-14 z"/><g stroke="var(--blue)" stroke-width="1.5" stroke-linecap="round"><line x1="9" y1="15" x2="7" y2="21"/><line x1="13" y1="15" x2="11" y2="21"/><line x1="17" y1="15" x2="15" y2="21"/></g></svg>';
|
|
case 65:
|
|
case 82:
|
|
return '<svg viewBox="0 0 24 24" fill="var(--stone)"><path d="M6 13 a4 4 0 0 1 -2 -7 a5 5 0 0 1 9 -3 a4 4 0 0 1 7 1 a3 3 0 0 1 0 6 h-14 z"/><g stroke="var(--blue)" stroke-width="1.5" stroke-linecap="round"><line x1="7" y1="15" x2="4" y2="22"/><line x1="11" y1="15" x2="8" y2="22"/><line x1="15" y1="15" x2="12" y2="22"/><line x1="19" y1="15" x2="16" y2="22"/></g></svg>';
|
|
case 71:
|
|
case 73:
|
|
case 75:
|
|
case 77:
|
|
case 85:
|
|
case 86:
|
|
return '<svg viewBox="0 0 24 24" fill="var(--stone)"><path d="M6 13 a4 4 0 0 1 -2 -7 a5 5 0 0 1 9 -3 a4 4 0 0 1 7 1 a3 3 0 0 1 0 6 h-14 z"/><g fill="' + c + '"><circle cx="8" cy="18" r="1.5"/><circle cx="13" cy="19" r="1.5"/><circle cx="17" cy="18" r="1.5"/></g></svg>';
|
|
case 95:
|
|
case 96:
|
|
case 99:
|
|
return '<svg viewBox="0 0 24 24" fill="var(--stone)"><path d="M6 13 a4 4 0 0 1 -2 -7 a5 5 0 0 1 9 -3 a4 4 0 0 1 7 1 a3 3 0 0 1 0 6 h-14 z"/><path d="M12 2 L8 11 L12 11 L10 18 L16 9 L12 9 Z" fill="var(--yellow)"/></svg>';
|
|
default:
|
|
return '<svg viewBox="0 0 24 24" fill="var(--stone)"><circle cx="12" cy="12" r="6"/></svg>';
|
|
}
|
|
}
|
|
|
|
function fetchWeather() {
|
|
var url = 'https://api.open-meteo.com/v1/forecast?latitude=' + WEATHER_LAT +
|
|
'&longitude=' + WEATHER_LON +
|
|
'¤t=temperature_2m,relative_humidity_2m,weather_code,wind_speed_10m' +
|
|
'&hourly=temperature_2m,weather_code' +
|
|
'&daily=weather_code,temperature_2m_max,temperature_2m_min' +
|
|
'&timezone=Europe/Berlin&forecast_days=5';
|
|
|
|
fetch(url)
|
|
.then(function(response) { return response.json(); })
|
|
.then(function(data) {
|
|
weatherData = data;
|
|
currentWeatherCode = data.current.weather_code;
|
|
setWeatherTheme(getWeatherThemeName(currentWeatherCode));
|
|
updateWeatherDisplay();
|
|
})
|
|
.catch(function(err) {
|
|
console.log('Weather fetch failed:', err);
|
|
});
|
|
}
|
|
|
|
function findHourlyIndex(targetDate, hourlyTimeArray) {
|
|
var bestIdx = 0;
|
|
var bestDiff = Infinity;
|
|
for (var i = 0; i < hourlyTimeArray.length; i++) {
|
|
var t = new Date(hourlyTimeArray[i]);
|
|
var diff = Math.abs(t.getTime() - targetDate.getTime());
|
|
if (diff < bestDiff) {
|
|
bestDiff = diff;
|
|
bestIdx = i;
|
|
}
|
|
}
|
|
return bestIdx;
|
|
}
|
|
|
|
function updateWeatherDisplay() {
|
|
if (!weatherData) return;
|
|
|
|
var now = new Date();
|
|
var current = weatherData.current;
|
|
var hourly = weatherData.hourly;
|
|
var daily = weatherData.daily;
|
|
|
|
// Current
|
|
var currentUnits = (window.CONFIG && window.CONFIG.units) || 'metric';
|
|
var tempDisplay = currentUnits === 'imperial'
|
|
? Math.round(current.temperature_2m * 9 / 5 + 32) + '\u00b0F'
|
|
: Math.round(current.temperature_2m) + '\u00b0C';
|
|
var windDisplay = currentUnits === 'imperial'
|
|
? Math.round(current.wind_speed_10m * 0.621371) + ' mph'
|
|
: Math.round(current.wind_speed_10m) + ' km/h';
|
|
var currentHtml = '<div class="weather-current-icon">' + weatherSVG(current.weather_code) + '</div>' +
|
|
'<div><div class="weather-current-temp"><span>' + tempDisplay + '</span><span class="weather-location" id="weather-location">' +
|
|
((window.CONFIG && window.CONFIG.location && (window.CONFIG.location.label || (window.CONFIG.location.lat.toFixed(2) + ',' + window.CONFIG.location.lon.toFixed(2)))) || '') + '</span></div>' +
|
|
'<div class="weather-current-detail">' +
|
|
t('Humidity') + ' ' + Math.round(current.relative_humidity_2m) + '% · ' + t('Wind') + ' ' + windDisplay + '</div></div>';
|
|
document.getElementById('weather-current').innerHTML = currentHtml;
|
|
|
|
// Forecast
|
|
var forecastHtml = '';
|
|
for (var d = 0; d < 4; d++) {
|
|
var dayDate = new Date(daily.time[d]);
|
|
var dayLabel = d === 0 ? t('today') : t('tomorrow');
|
|
if (d > 1) {
|
|
var weekdayKey = ['Su.', 'Mo.', 'Tu.', 'We.', 'Th.', 'Fr.', 'Sa.'][dayDate.getDay()];
|
|
dayLabel = t(weekdayKey) + ' ' +
|
|
(dayDate.getDate() < 10 ? '0' : '') + dayDate.getDate() + '.' +
|
|
(dayDate.getMonth() + 1 < 10 ? '0' : '') + (dayDate.getMonth() + 1) + '.';
|
|
}
|
|
|
|
// Morning (8), Noon (14), Evening (20), Night (2)
|
|
var slots = [8, 14, 20, 2];
|
|
var slotsHtml = '';
|
|
for (var i = 0; i < slots.length; i++) {
|
|
var slotDate = new Date(dayDate);
|
|
slotDate.setHours(slots[i], 0, 0, 0);
|
|
if (i === 3 && d > 0) {
|
|
slotDate.setDate(slotDate.getDate() + 1);
|
|
} else if (i === 3 && d === 0) {
|
|
slotDate.setDate(slotDate.getDate() + 1);
|
|
}
|
|
var currentUnits = (window.CONFIG && window.CONFIG.units) || 'metric';
|
|
var convertTemp = function(c) {
|
|
return currentUnits === 'imperial'
|
|
? Math.round(c * 9 / 5 + 32) + '\u00b0F'
|
|
: Math.round(c) + '\u00b0C';
|
|
};
|
|
var slotIdx = findHourlyIndex(slotDate, hourly.time);
|
|
var slotCode = hourly.weather_code[slotIdx];
|
|
var slotTemp = hourly.temperature_2m[slotIdx];
|
|
var slotLabels = [t('Morning'), t('Noon'), t('Evening'), t('Night')];
|
|
slotsHtml += '<div class="weather-slot">' +
|
|
'<div class="weather-slot-icon">' + weatherSVG(slotCode) + '</div>' +
|
|
'<div class="weather-slot-label">' + slotLabels[i] + '</div>' +
|
|
'<div class="weather-slot-temp">' + convertTemp(slotTemp) + '</div>' +
|
|
'</div>';
|
|
}
|
|
|
|
forecastHtml += '<div class="weather-day">' +
|
|
'<div class="weather-day-header">' +
|
|
'<span class="weather-day-name">' + dayLabel + '</span>' +
|
|
'<span class="weather-day-minmax">' + convertTemp(daily.temperature_2m_min[d]) + ' / ' + convertTemp(daily.temperature_2m_max[d]) + '</span>' +
|
|
'</div>' +
|
|
'<div class="weather-day-slots">' + slotsHtml + '</div>' +
|
|
'</div>';
|
|
}
|
|
document.getElementById('weather-forecast').innerHTML = forecastHtml;
|
|
}
|
|
|
|
// Update weather every 15 minutes
|
|
applyI18n();
|
|
populateDebugSelects();
|
|
// Apply debug flag from CONFIG
|
|
var debugEl = document.getElementById('debug-panel');
|
|
if (debugEl && !(window.CONFIG && window.CONFIG.debug)) {
|
|
debugEl.style.display = 'none';
|
|
}
|
|
// Try browser geolocation if no explicit location configured
|
|
initBrowserGeolocation();
|
|
// Fetch location label from lat/lon if available
|
|
fetchLocationLabel();
|
|
animate();
|
|
try { tick(); } catch (e) { console.error('Initial tick error:', e); }
|
|
|
|
fetchWeather();
|
|
setInterval(fetchWeather, 15 * 60 * 1000);
|
|
tick();
|
|
} catch (e) {
|
|
window.__clouds2dError = e.message || String(e);
|
|
console.error('Background init error:', e);
|
|
}
|
|
</script>
|
|
</body>
|
|
</html> |