854 lines
40 KiB
HTML
854 lines
40 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="de">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Vanta Clouds</title>
|
|
<script src="https://cdn.jsdelivr.net/npm/suncalc@1.9.0/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; }
|
|
#info-panel {
|
|
position: fixed;
|
|
bottom: 20px;
|
|
right: 20px;
|
|
color: #e6dfd1;
|
|
background: rgba(28, 27, 31, 0.7);
|
|
padding: 14px 18px;
|
|
border-radius: 8px;
|
|
z-index: 9999;
|
|
font-size: 13px;
|
|
line-height: 1.6;
|
|
min-width: 220px;
|
|
}
|
|
#analog-clock {
|
|
width: 200px;
|
|
height: 200px;
|
|
margin: 0 auto 10px auto;
|
|
display: block;
|
|
}
|
|
#digital-clock {
|
|
text-align: center;
|
|
font-size: 16px;
|
|
letter-spacing: 2px;
|
|
margin-bottom: 12px;
|
|
padding-bottom: 10px;
|
|
border-bottom: 1px solid rgba(230, 223, 209, 0.2);
|
|
}
|
|
.info-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
padding: 3px 0;
|
|
}
|
|
.info-icon {
|
|
width: 18px;
|
|
height: 18px;
|
|
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: 10px;
|
|
}
|
|
.info-label {
|
|
color: #b0a89a;
|
|
}
|
|
.info-value {
|
|
color: #e6dfd1;
|
|
font-weight: bold;
|
|
}
|
|
.info-sub {
|
|
color: #999187;
|
|
font-size: 11px;
|
|
margin-left: 4px;
|
|
}
|
|
.weather-divider {
|
|
height: 1px;
|
|
background: rgba(230, 223, 209, 0.2);
|
|
margin: 8px 0;
|
|
}
|
|
.weather-current {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
padding: 4px 0 8px 0;
|
|
border-bottom: 1px solid rgba(230, 223, 209, 0.2);
|
|
margin-bottom: 8px;
|
|
}
|
|
.weather-current-icon {
|
|
width: 36px;
|
|
height: 36px;
|
|
flex-shrink: 0;
|
|
}
|
|
.weather-current-icon svg { width: 100%; height: 100%; }
|
|
.weather-current-temp {
|
|
font-size: 22px;
|
|
font-weight: bold;
|
|
}
|
|
.weather-current-detail {
|
|
font-size: 11px;
|
|
color: #b0a89a;
|
|
line-height: 1.4;
|
|
}
|
|
.weather-day {
|
|
margin-bottom: 6px;
|
|
}
|
|
.weather-day-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: baseline;
|
|
padding: 2px 0;
|
|
font-size: 12px;
|
|
}
|
|
.weather-day-name {
|
|
color: #e6dfd1;
|
|
font-weight: bold;
|
|
}
|
|
.weather-day-minmax {
|
|
color: #999187;
|
|
font-size: 11px;
|
|
}
|
|
.weather-day-slots {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
gap: 4px;
|
|
font-size: 11px;
|
|
}
|
|
.weather-slot {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
flex: 1;
|
|
gap: 2px;
|
|
}
|
|
.weather-slot-icon {
|
|
width: 22px;
|
|
height: 22px;
|
|
}
|
|
.weather-slot-icon svg { width: 100%; height: 100%; }
|
|
.weather-slot-label {
|
|
color: #999187;
|
|
font-size: 9px;
|
|
}
|
|
.weather-slot-temp {
|
|
color: #e6dfd1;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="vanta"></div>
|
|
<div id="info-panel">
|
|
<svg id="analog-clock" viewBox="0 0 200 200">
|
|
<!-- Clock face -->
|
|
<circle cx="100" cy="100" r="95" fill="rgba(28,27,31,0.3)" stroke="#e6dfd1" stroke-width="2"/>
|
|
|
|
<!-- Hour marks -->
|
|
<g id="hour-marks"></g>
|
|
|
|
<!-- Sun orbit (radius 55) -->
|
|
<circle cx="100" cy="100" r="55" stroke="#3a383d" stroke-width="1" fill="none" stroke-dasharray="2,3" opacity="0.6"/>
|
|
|
|
<!-- Moon orbit (radius 75) -->
|
|
<circle cx="100" cy="100" r="75" stroke="#3a383d" stroke-width="1" fill="none" stroke-dasharray="2,3" opacity="0.6"/>
|
|
|
|
<!-- Sun arc (from sunrise to sunset) -->
|
|
<path id="sun-arc" d="" stroke="#e0914f" stroke-width="2.5" fill="none"/>
|
|
|
|
<!-- Moon arc (from moonrise to moonset) -->
|
|
<path id="moon-arc" d="" stroke="#b0a89a" stroke-width="2.5" fill="none"/>
|
|
|
|
<!-- Sun rise arrow (pointing in / up at sunrise) -->
|
|
<g id="sun-rise-arrow" fill="#e0914f">
|
|
<polygon points="0,-7 6,3 -6,3"/>
|
|
</g>
|
|
|
|
<!-- Sun set arrow (pointing out / down at sunset) -->
|
|
<g id="sun-set-arrow" fill="#e0914f">
|
|
<polygon points="0,7 6,-3 -6,-3"/>
|
|
</g>
|
|
|
|
<!-- Moon rise arrow -->
|
|
<g id="moon-rise-arrow" fill="#b0a89a">
|
|
<polygon points="0,-7 6,3 -6,3"/>
|
|
</g>
|
|
|
|
<!-- Moon set arrow -->
|
|
<g id="moon-set-arrow" fill="#b0a89a">
|
|
<polygon points="0,7 6,-3 -6,-3"/>
|
|
</g>
|
|
|
|
<!-- Sun position (rendered after arcs/arrows so it's visible) -->
|
|
<circle id="rpg-sun-glow" cx="100" cy="100" r="12" fill="#e0914f" opacity="0.4"/>
|
|
<circle id="rpg-sun" cx="100" cy="100" r="7" fill="#e0914f"/>
|
|
<circle id="rpg-sun-core" cx="100" cy="100" r="3" fill="#f3ede1"/>
|
|
|
|
<!-- Moon position (with phase) -->
|
|
<g id="rpg-moon">
|
|
<circle id="rpg-moon-bg" cx="0" cy="0" r="9" fill="#b0a89a" stroke="#e6dfd1" stroke-width="1"/>
|
|
<path id="rpg-moon-lit" d="" fill="#e6dfd1"/>
|
|
<path id="rpg-moon-dark" d="" fill="#54514f"/>
|
|
</g>
|
|
|
|
<!-- Clock hands (on top of everything) -->
|
|
<line id="hour-hand" x1="100" y1="100" x2="100" y2="35" stroke="#e6dfd1" stroke-width="4" stroke-linecap="round" opacity="0.7"/>
|
|
<line id="minute-hand" x1="100" y1="100" x2="100" y2="20" stroke="#e6dfd1" stroke-width="3" stroke-linecap="round" opacity="0.7"/>
|
|
<line id="second-hand" x1="100" y1="100" x2="100" y2="14" stroke="#e0914f" stroke-width="1.5" stroke-linecap="round"/>
|
|
<circle cx="100" cy="100" r="4" fill="#e0914f"/>
|
|
</svg>
|
|
<div id="digital-clock">00:00:00</div>
|
|
<div class="info-row">
|
|
<div class="info-icon">
|
|
<svg viewBox="0 0 24 24" fill="#e0914f"><circle cx="12" cy="12" r="4"/><g stroke="#e0914f" 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">Aufgang</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="none" stroke="#e0914f" 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">Untergang</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="#e6dfd1"/></svg>
|
|
</div>
|
|
<div class="info-content"><span class="info-label">Mondphase</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="#b0a89a"><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">Mond auf</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="#b0a89a" 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">Mond unter</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="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/vanta@0.5.21/dist/vanta.clouds.min.js"></script>
|
|
<script>
|
|
var effect = VANTA.CLOUDS({
|
|
el: "#vanta",
|
|
mouseControls: false,
|
|
touchControls: false,
|
|
gyroControls: false,
|
|
minHeight: 200.00,
|
|
minWidth: 200.00,
|
|
scale: 1.00,
|
|
scaleMobile: 4,
|
|
speed: 0.4,
|
|
backgroundColor: 0x0,
|
|
backgroundAlpha: 0,
|
|
skyColor: 0x3a4458,
|
|
cloudColor: 0x6d6a63,
|
|
cloudShadowColor: 0x2a3450,
|
|
sunColor: 0xf3ede1,
|
|
sunGlareColor: 0xe6dfd1,
|
|
sunlightColor: 0xf3ede1
|
|
}) || { setOptions: function() {} };
|
|
|
|
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 palette = [
|
|
{ t: 0.00, sky: 0x0a0a14, cloud: 0x2a2822, light: 0x999187, bg: 0x0a0a14, cloudShadow: 0x100f12, sun: 0x999187, sunGlare: 0x6d6a63, sunLight: 0x999187, sunVis: 0 },
|
|
{ t: 0.08, sky: 0x141428, cloud: 0x2a2822, light: 0x999187, bg: 0x1a1428, cloudShadow: 0x141316, sun: 0x999187, sunGlare: 0x6d6a63, sunLight: 0x999187, sunVis: 0 },
|
|
{ t: 0.18, sky: 0x3a2a28, cloud: 0x4a2e1c, light: 0xa85f1d, bg: 0x4a2820, cloudShadow: 0x1f1814, sun: 0xc56a2b, sunGlare: 0xa85f1d, sunLight: 0xc56a2b, sunVis: 0.3 },
|
|
{ t: 0.28, sky: 0x6a4030, cloud: 0x5a3a28, light: 0xd6824a, bg: 0x7a4530, cloudShadow: 0x2a1f17, sun: 0xe0914f, sunGlare: 0xd6824a, sunLight: 0xe0914f, sunVis: 0.8 },
|
|
{ t: 0.42, sky: 0x4a5060, cloud: 0x6d6a63, light: 0xe0d4be, bg: 0x506080, cloudShadow: 0x3a4458, sun: 0xf3ede1, sunGlare: 0xe6dfd1, sunLight: 0xf3ede1, sunVis: 1 },
|
|
{ t: 0.62, sky: 0x3a4458, cloud: 0x6d6a63, light: 0xe6dfd1, bg: 0x445878, cloudShadow: 0x2a3450, sun: 0xf3ede1, sunGlare: 0xe6dfd1, sunLight: 0xf3ede1, sunVis: 1 },
|
|
{ t: 0.66, sky: 0x4a4a55, cloud: 0x6d5a52, light: 0xe6c89a, bg: 0x5a554e, cloudShadow: 0x3a3428, sun: 0xe6c89a, sunGlare: 0xd6b46a, sunLight: 0xe6c89a, sunVis: 0.95 },
|
|
{ t: 0.69, sky: 0x6a5040, cloud: 0x6d4a3a, light: 0xe6a872, bg: 0x7a5540, cloudShadow: 0x4a3025, sun: 0xe6a872, sunGlare: 0xd6824a, sunLight: 0xe6a872, sunVis: 0.85 },
|
|
{ t: 0.72, sky: 0x7a4530, cloud: 0x6d3a28, light: 0xe0914f, bg: 0x7a3825, cloudShadow: 0x4a2818, sun: 0xe0914f, sunGlare: 0xa86030, sunLight: 0xe0914f, sunVis: 0.7 },
|
|
{ t: 0.78, sky: 0x5a3030, cloud: 0x4a2e22, light: 0xa86030, bg: 0x5a2820, cloudShadow: 0x2a1818, sun: 0xa86030, sunGlare: 0x6b4423, sunLight: 0xa86030, sunVis: 0.4 },
|
|
{ t: 0.85, sky: 0x3a2030, cloud: 0x3a2a2a, light: 0x6a4030, bg: 0x4a2030, cloudShadow: 0x1f1814, sun: 0x6a4030, sunGlare: 0x54514f, sunLight: 0x6a4030, sunVis: 0.1 },
|
|
{ t: 0.92, sky: 0x1a1428, cloud: 0x2a2822, light: 0x999187, bg: 0x1a1428, cloudShadow: 0x141316, sun: 0x999187, sunGlare: 0x6d6a63, sunLight: 0x999187, sunVis: 0 },
|
|
{ t: 1.00, sky: 0x0a0a14, cloud: 0x2a2822, light: 0x999187, bg: 0x0a0a14, cloudShadow: 0x100f12, sun: 0x999187, sunGlare: 0x6d6a63, sunLight: 0x999187, sunVis: 0 }
|
|
];
|
|
|
|
function getColor(t) {
|
|
t = t % 1;
|
|
for (var i = 0; i < palette.length - 1; i++) {
|
|
if (t >= palette[i].t && t <= palette[i + 1].t) {
|
|
var span = palette[i + 1].t - palette[i].t;
|
|
var localT = span === 0 ? 0 : (t - palette[i].t) / span;
|
|
var s = smooth(localT);
|
|
return {
|
|
sky: lerpColor(palette[i].sky, palette[i + 1].sky, s),
|
|
cloud: lerpColor(palette[i].cloud, palette[i + 1].cloud, s),
|
|
light: lerpColor(palette[i].light, palette[i + 1].light, s),
|
|
bg: lerpColor(palette[i].bg, palette[i + 1].bg, s),
|
|
cloudShadow: lerpColor(palette[i].cloudShadow, palette[i + 1].cloudShadow, s),
|
|
sun: lerpColor(palette[i].sun, palette[i + 1].sun, s),
|
|
sunGlare: lerpColor(palette[i].sunGlare, palette[i + 1].sunGlare, s),
|
|
sunLight: lerpColor(palette[i].sunLight, palette[i + 1].sunLight, s),
|
|
sunVis: palette[i].sunVis + (palette[i + 1].sunVis - palette[i].sunVis) * s
|
|
};
|
|
}
|
|
}
|
|
return { sky: palette[0].sky, cloud: palette[0].cloud, light: palette[0].light, bg: palette[0].bg, cloudShadow: palette[0].cloudShadow, sun: palette[0].sun, sunGlare: palette[0].sunGlare, sunLight: palette[0].sunLight, sunVis: 0 };
|
|
}
|
|
|
|
// ============ Clock & Sun/Moon Calculations ============
|
|
var PASSAU_LAT = 48.5667;
|
|
var PASSAU_LON = 13.4319;
|
|
|
|
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 SUN_ORBIT_RADIUS = 55;
|
|
var MOON_ORBIT_RADIUS = 75;
|
|
|
|
// Generate hour marks
|
|
for (var i = 0; i < 12; i++) {
|
|
var angle = (i * 30) * Math.PI / 180;
|
|
var x1 = 100 + Math.sin(angle) * 85;
|
|
var y1 = 100 - Math.cos(angle) * 85;
|
|
var x2 = 100 + Math.sin(angle) * 90;
|
|
var y2 = 100 - Math.cos(angle) * 90;
|
|
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', '#e6dfd1');
|
|
line.setAttribute('stroke-width', i % 3 === 0 ? 3 : 1);
|
|
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 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 ? 'morgen' : formatDateLong(nextDate);
|
|
return { date: ev, label: dayLabel };
|
|
}
|
|
}
|
|
return { date: null, label: '' };
|
|
}
|
|
|
|
function getMoonPhaseText(phase) {
|
|
var p = phase;
|
|
if (p < 0.0625 || p >= 0.9375) return 'Neumond';
|
|
if (p < 0.1875) return 'Zunehm. Sichel';
|
|
if (p < 0.3125) return 'Erstes Viertel';
|
|
if (p < 0.4375) return 'Zunehm. Gibbous';
|
|
if (p < 0.5625) return 'Vollmond';
|
|
if (p < 0.6875) return 'Abnehm. Gibbous';
|
|
if (p < 0.8125) return 'Letztes Viertel';
|
|
return 'Abnehm. Sichel';
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
function pointOnCircle(date, radius) {
|
|
var angle = angleForTime(date);
|
|
return {
|
|
x: 100 + Math.cos(angle) * radius,
|
|
y: 100 + 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;
|
|
// Use shorter arc (largeArc=0) unless the difference exceeds 180
|
|
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 };
|
|
}
|
|
|
|
// Determine if moon is visible (above horizon)
|
|
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 };
|
|
}
|
|
|
|
// Above horizon - interpolate between moonrise and moonset
|
|
var riseAngle = angleForTime(rise);
|
|
var setAngle = angleForTime(set);
|
|
|
|
// Calculate the angular difference (could be negative if crossing midnight)
|
|
var diff = setAngle - riseAngle;
|
|
if (diff > Math.PI) diff -= 2 * Math.PI;
|
|
if (diff < -Math.PI) diff += 2 * Math.PI;
|
|
|
|
// Calculate progress (0 at rise, 1 at set)
|
|
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: 100 + Math.cos(moonAngle) * MOON_ORBIT_RADIUS,
|
|
y: 100 + Math.sin(moonAngle) * MOON_ORBIT_RADIUS
|
|
},
|
|
isVisible: true
|
|
};
|
|
} catch (e) {
|
|
return { point: pointOnCircle(now, MOON_ORBIT_RADIUS), isVisible: false };
|
|
}
|
|
}
|
|
|
|
function renderMoonPhaseIcon(phase) {
|
|
var icon = document.getElementById('moon-phase-icon');
|
|
icon.innerHTML = '';
|
|
var bg = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
|
|
bg.setAttribute('cx', '12'); bg.setAttribute('cy', '12'); bg.setAttribute('r', '9');
|
|
bg.setAttribute('fill', '#b0a89a');
|
|
bg.setAttribute('stroke', '#e6dfd1');
|
|
bg.setAttribute('stroke-width', '0.5');
|
|
icon.appendChild(bg);
|
|
if (phase < 0.5) {
|
|
var lit = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
var r = 9;
|
|
var phaseAngle = phase * 360;
|
|
var rx = r * Math.abs(Math.cos(phaseAngle * Math.PI / 180));
|
|
var sweepFlag = phase < 0.25 ? 0 : 1;
|
|
lit.setAttribute('d', 'M 12 3 A 9 9 0 0 1 12 21 A ' + rx + ' 9 0 0 ' + sweepFlag + ' 12 3 Z');
|
|
lit.setAttribute('fill', '#e6dfd1');
|
|
icon.appendChild(lit);
|
|
} else {
|
|
var lit = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
var r = 9;
|
|
var phaseAngle = (1 - phase) * 360;
|
|
var rx = r * Math.abs(Math.cos(phaseAngle * Math.PI / 180));
|
|
var sweepFlag = phase > 0.75 ? 0 : 1;
|
|
lit.setAttribute('d', 'M 12 3 A 9 9 0 0 0 12 21 A ' + rx + ' 9 0 0 ' + sweepFlag + ' 12 3 Z');
|
|
lit.setAttribute('fill', '#e6dfd1');
|
|
icon.appendChild(lit);
|
|
}
|
|
}
|
|
|
|
function renderRpgMoon(phase) {
|
|
var r = 9;
|
|
rpgMoonDark.setAttribute('d', '');
|
|
if (phase < 0.5) {
|
|
var phaseAngle = phase * 360;
|
|
var rx = r * Math.abs(Math.cos(phaseAngle * Math.PI / 180));
|
|
var sweepFlag = phase < 0.25 ? 0 : 1;
|
|
rpgMoonLit.setAttribute('d', 'M 0 -9 A 9 9 0 0 1 0 9 A ' + rx + ' 9 0 0 ' + sweepFlag + ' 0 -9 Z');
|
|
} else {
|
|
var phaseAngle = (1 - phase) * 360;
|
|
var rx = r * Math.abs(Math.cos(phaseAngle * Math.PI / 180));
|
|
var sweepFlag = phase > 0.75 ? 0 : 1;
|
|
rpgMoonLit.setAttribute('d', 'M 0 -9 A 9 9 0 0 0 0 9 A ' + rx + ' 9 0 0 ' + sweepFlag + ' 0 -9 Z');
|
|
}
|
|
}
|
|
|
|
function updateClockAndEvents() {
|
|
var now = new Date();
|
|
|
|
// Real-time clock hands and digital
|
|
var h = now.getHours();
|
|
var m = now.getMinutes();
|
|
var s = now.getSeconds();
|
|
|
|
document.getElementById('digital-clock').textContent = formatTimeHMS(h, m, s);
|
|
|
|
var hourAngle = ((h % 12) + m / 60) * 30;
|
|
var minuteAngle = (m + s / 60) * 6;
|
|
var secondAngle = s * 6;
|
|
|
|
hourHand.setAttribute('transform', 'rotate(' + hourAngle + ' 100 100)');
|
|
minuteHand.setAttribute('transform', 'rotate(' + minuteAngle + ' 100 100)');
|
|
secondHand.setAttribute('transform', 'rotate(' + secondAngle + ' 100 100)');
|
|
|
|
// 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 : '';
|
|
|
|
// 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 moonP = pointOnCircle(now, 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 + ')');
|
|
|
|
// 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);
|
|
renderRpgMoon(moonIllum.phase);
|
|
}
|
|
|
|
var startTime = Date.now();
|
|
|
|
function animate() {
|
|
var elapsed = (Date.now() - startTime) % CYCLE;
|
|
var t = elapsed / CYCLE;
|
|
var c = getColor(t);
|
|
effect.setOptions({
|
|
skyColor: c.sky,
|
|
cloudColor: c.cloud,
|
|
cloudShadowColor: c.cloudShadow,
|
|
sunColor: c.sun,
|
|
sunGlareColor: c.sunGlare,
|
|
sunlightColor: c.sunLight,
|
|
backgroundColor: c.bg
|
|
});
|
|
document.body.style.backgroundColor = '#' + ('00000' + c.bg.toString(16)).slice(-6);
|
|
requestAnimationFrame(animate);
|
|
}
|
|
|
|
function tick() {
|
|
updateClockAndEvents();
|
|
setTimeout(tick, 1000);
|
|
}
|
|
|
|
// ============ Weather ============
|
|
var WEATHER_LAT = 48.5667;
|
|
var WEATHER_LON = 13.4319;
|
|
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 = '#e6dfd1';
|
|
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="#999187" stroke="none"/></svg>';
|
|
case 3:
|
|
return '<svg viewBox="0 0 24 24" fill="#999187"><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="#999187" 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="#999187"><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="#7c9aa1" 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="#999187"><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="#7c9aa1" 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="#999187"><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="#7c9aa1" 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="#999187"><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="#7c9aa1" 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="#999187"><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="#7c9aa1" 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="#999187"><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="#999187"><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="#d6b46a"/></svg>';
|
|
default:
|
|
return '<svg viewBox="0 0 24 24" fill="#999187"><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;
|
|
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 currentHtml = '<div class="weather-current-icon">' + weatherSVG(current.weather_code) + '</div>' +
|
|
'<div><div class="weather-current-temp">' + Math.round(current.temperature_2m) + '°C</div>' +
|
|
'<div class="weather-current-detail">' +
|
|
'Feuchte ' + Math.round(current.relative_humidity_2m) + '% · Wind ' + Math.round(current.wind_speed_10m) + ' km/h</div></div>';
|
|
document.getElementById('weather-current').innerHTML = currentHtml;
|
|
|
|
// Forecast
|
|
var forecastHtml = '';
|
|
var dayNames = ['Heute', 'Morgen'];
|
|
for (var d = 0; d < 4; d++) {
|
|
var dayDate = new Date(daily.time[d]);
|
|
var dayLabel = d === 0 ? 'Heute' : dayNames[1] || dayDate.toLocaleDateString('de-DE', { weekday: 'short' });
|
|
if (d > 1) {
|
|
dayLabel = dayDate.toLocaleDateString('de-DE', { weekday: 'short' }) + ' ' +
|
|
(dayDate.getDate() < 10 ? '0' : '') + dayDate.getDate() + '.' +
|
|
(dayDate.getMonth() + 1 < 10 ? '0' : '') + (dayDate.getMonth() + 1) + '.';
|
|
}
|
|
|
|
// Morning (8), Noon (14), Evening (20)
|
|
var slots = [8, 14, 20];
|
|
var slotsHtml = '';
|
|
for (var i = 0; i < slots.length; i++) {
|
|
var slotDate = new Date(dayDate);
|
|
slotDate.setHours(slots[i], 0, 0, 0);
|
|
var slotIdx = findHourlyIndex(slotDate, hourly.time);
|
|
var slotCode = hourly.weather_code[slotIdx];
|
|
var slotTemp = hourly.temperature_2m[slotIdx];
|
|
var slotLabels = ['M', 'Mi', 'A'];
|
|
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">' + Math.round(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">' + Math.round(daily.temperature_2m_min[d]) + '° / ' + Math.round(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
|
|
fetchWeather();
|
|
setInterval(fetchWeather, 15 * 60 * 1000);
|
|
|
|
animate();
|
|
tick();
|
|
</script>
|
|
</body>
|
|
</html> |