SUNDIAL solar dial v0.3: round screen, NOAA solar math, day/night arc

- 466px round layout, designWidth 466 (S4 41mm 466x466)
- solar position + dayProgress in src/common/solar.js (pure, TDD'd)
- sunrise/sunset sweep, night moon, countdown, 30s refresh
- tests: solar math, page logic, circle geometry/contrast
This commit is contained in:
2026-09-09 15:09:14 +01:00
commit 1a3a2566e3
16 changed files with 10991 additions and 0 deletions
+207
View File
@@ -0,0 +1,207 @@
<template>
<div class="page">
<stack class="dial">
<!-- outer bezel ring -->
<div class="bezel"></div>
<!-- sky disc: upper half = day sky -->
<div class="sky"></div>
<!-- lower half = night -->
<div class="nightHalf"></div>
<!-- sun glow halo -->
<div class="sunHalo {{ isDay ? '' : 'moonHalo' }}" style="{{ haloStyle }}"></div>
<!-- sun / moon marker -->
<div class="sunDot {{ isDay ? '' : 'moon' }}" style="{{ markerStyle }}"></div>
<!-- horizon line -->
<div class="horizon"></div>
<!-- hour tick marks around the rim -->
<div class="tick" style="left:230px;top:20px"></div>
<div class="tick" style="left:335px;top:48px"></div>
<div class="tick" style="left:412px;top:125px"></div>
<div class="tick" style="left:440px;top:230px"></div>
<div class="tick" style="left:412px;top:335px"></div>
<div class="tick" style="left:335px;top:412px"></div>
<div class="tick" style="left:230px;top:440px"></div>
<div class="tick" style="left:125px;top:412px"></div>
<div class="tick" style="left:48px;top:335px"></div>
<div class="tick" style="left:20px;top:230px"></div>
<div class="tick" style="left:48px;top:125px"></div>
<div class="tick" style="left:125px;top:48px"></div>
<!-- noon marker -->
<div class="tick noon" style="left:230px;top:20px"></div>
<!-- center readouts -->
<div class="center">
<text class="big">{{ clock }}</text>
<text class="phase">{{ phaseLabel }}</text>
<text class="count">{{ countdown }}</text>
</div>
</stack>
</div>
</template>
<script>
import solar from '../../common/solar.js'
export default {
private: {
display: '',
clock: '--:--',
phaseLabel: '',
countdown: '',
sunAngle: 0,
isDay: true,
orbitR: 150,
markerStyle: { left: '211px', top: '73px' },
haloStyle: { left: '205px', top: '67px' }
},
onInit() {
this.LAT = 48.8566
this.LON = 2.3522
},
onShow() {
this.update()
this.timer = setInterval(() => this.update(), 30000) // 30s cadence — gentle on battery
},
onHide() {
clearInterval(this.timer)
},
onDestroy() {
clearInterval(this.timer)
},
update() {
const now = new Date()
const hh = String(now.getHours()).padStart(2, '0')
const mm = String(now.getMinutes()).padStart(2, '0')
this.clock = hh + ':' + mm
const pos = solar.solarPosition(now, this.LAT, this.LON)
this.isDay = pos.altitude > 0
const prog = solar.dayProgress(now, this.LAT, this.LON)
let frac = prog
if (!this.isDay) {
frac = this.nightFrac(now)
}
// -90° (sunrise, left horizon) → 0° (noon, top) → +90° (sunset, right)
this.sunAngle = this.isDay ? -90 + prog * 180 : -90 - (1 - frac) * 180
this.positionMarker()
this.phaseLabel = this.isDay ? 'DAYLIGHT' : 'NIGHT'
const cd = this.nextTransitionSeconds(now)
this.countdown = this.isDay ? ('sunset in ' + solar.formatCountdown(cd)) : ('sunrise in ' + solar.formatCountdown(cd))
},
positionMarker() {
const angle = this.sunAngle * Math.PI / 180
const cx = 233 + this.orbitR * Math.sin(angle)
const cy = 233 - this.orbitR * Math.cos(angle)
this.markerStyle = {
left: Math.round(cx - 22) + 'px',
top: Math.round(cy - 22) + 'px'
}
this.haloStyle = {
left: Math.round(cx - 28) + 'px',
top: Math.round(cy - 28) + 'px'
}
},
nightFrac(now) {
const secs = this.nextTransitionSeconds(now)
const NIGHT = 12 * 3600
return Math.max(0, Math.min(1, 1 - secs / NIGHT))
},
nextTransitionSeconds(now) {
const isDayNow = solar.solarPosition(now, this.LAT, this.LON).altitude > 0
for (let m = 5; m <= 48 * 60; m += 5) {
const t = new Date(now.getTime() + m * 60e3)
const day = solar.solarPosition(t, this.LAT, this.LON).altitude > 0
if (day !== isDayNow) return m * 60
}
return 0
}
}
</script>
<style>
.page {
width: 466px; height: 466px;
background-color: #000000;
align-items: center;
justify-content: center;
}
.dial {
width: 466px; height: 466px;
}
.bezel {
position: absolute; left: 12px; top: 12px;
width: 438px; height: 438px;
border-radius: 220px;
border-width: 2px;
border-color: #2a3040;
}
.sky {
position: absolute; left: 33px; top: 33px;
width: 400px; height: 400px;
border-radius: 200px;
background-color: #0e1f3a;
}
.nightHalf {
position: absolute; left: 33px; top: 233px;
width: 400px; height: 200px;
border-radius: 0px 0px 200px 200px;
background-color: #060a14;
}
.horizon {
position: absolute; left: 33px; top: 233px;
width: 400px; height: 4px;
background-color: #4a5a7a;
}
.sunHalo {
position: absolute;
width: 56px; height: 56px;
border-radius: 28px;
background-color: #ffc93c;
opacity: 0.25;
}
.moonHalo {
background-color: #cfd6e4;
opacity: 0.15;
}
.sunDot {
position: absolute;
width: 44px; height: 44px;
border-radius: 22px;
background-color: #ffc93c;
}
.moon {
background-color: #cfd6e4;
}
.tick {
position: absolute;
width: 6px; height: 6px;
border-radius: 3px;
background-color: #3a4052;
}
.tick.noon {
background-color: #f3a633;
}
.center {
position: absolute; left: 0px; top: 170px;
width: 466px;
flex-direction: column;
align-items: center;
}
.big {
height: 84px;
color: #ffffff;
font-size: 66px;
text-align: center;
}
.phase {
height: 32px;
color: #f3a633;
font-size: 24px;
text-align: center;
}
.count {
height: 36px;
color: #9aa0aa;
font-size: 22px;
text-align: center;
}
</style>