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:
+10
@@ -0,0 +1,10 @@
|
||||
<script>
|
||||
export default {
|
||||
onCreate() {
|
||||
console.log('Hello Vela app created')
|
||||
},
|
||||
onDestroy() {
|
||||
console.log('Hello Vela app destroyed')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="192" height="192" viewBox="0 0 192 192">
|
||||
<rect width="192" height="192" rx="48" fill="#ff6900"/>
|
||||
<text x="96" y="116" text-anchor="middle" font-family="DejaVu Sans, sans-serif" font-size="72" font-weight="700" fill="#ffffff">Hi</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 293 B |
@@ -0,0 +1,91 @@
|
||||
// Solar position — NOAA solar calculator algorithm (Astronomical Algorithms, Meeus).
|
||||
// All functions pure; no Vela APIs. Exports: solarPosition, dayProgress, formatCountdown.
|
||||
const RAD = Math.PI / 180
|
||||
const DEG = 180 / Math.PI
|
||||
|
||||
function julianDay(date) {
|
||||
return date.getTime() / 86400000 + 2440587.5
|
||||
}
|
||||
|
||||
function sunVector(date) {
|
||||
const jd = julianDay(date)
|
||||
const n = jd - 2451545.0
|
||||
const L = (280.460 + 0.9856474 * n) % 360 // mean longitude
|
||||
const g = ((357.528 + 0.9856003 * n) % 360) * RAD // mean anomaly
|
||||
const lambda = (L + 1.915 * Math.sin(g) + 0.020 * Math.sin(2 * g)) * RAD // ecliptic longitude
|
||||
const eps = (23.439 - 0.0000004 * n) * RAD // obliquity
|
||||
const declination = Math.asin(Math.sin(eps) * Math.sin(lambda)) * DEG
|
||||
const rightAscension = Math.atan2(Math.cos(eps) * Math.sin(lambda), Math.cos(lambda)) * DEG
|
||||
const gmst = (18.697374558 + 24.06570982441908 * n) % 24 // sidereal hours
|
||||
return { declination, rightAscension: (rightAscension + 360) % 360, gmst: (gmst + 24) % 24 }
|
||||
}
|
||||
|
||||
// altitude/azimuth in degrees; azimuth 0=N, 90=E
|
||||
function solarPosition(date, latDeg, lonDeg) {
|
||||
const { declination, rightAscension, gmst } = sunVector(date)
|
||||
const lat = latDeg * RAD
|
||||
const hourAngle = ((gmst * 15 + lonDeg - rightAscension + 180) % 360 - 180) * RAD
|
||||
const sinAlt = Math.sin(lat) * Math.sin(declination * RAD) + Math.cos(lat) * Math.cos(declination * RAD) * Math.cos(hourAngle)
|
||||
const altitude = Math.asin(Math.max(-1, Math.min(1, sinAlt))) * DEG
|
||||
const az = Math.atan2(
|
||||
-Math.sin(hourAngle),
|
||||
Math.tan(lat) * Math.cos(declination * RAD) - Math.sin(declination * RAD) * Math.cos(hourAngle)
|
||||
) * DEG
|
||||
return { altitude, azimuth: (az + 360) % 360 }
|
||||
}
|
||||
|
||||
// fraction of daylight elapsed at `date` (0 sunrise → 1 sunset; 0 before, 1 after)
|
||||
function dayProgress(date, latDeg, lonDeg) {
|
||||
// scan the last 48h for the latest crossing at or before `date`
|
||||
let lastSunrise = NaN, lastSunset = NaN
|
||||
for (let h = 1; h <= 48; h++) {
|
||||
const t = date.getTime() - h * 3600e3
|
||||
const altT = solarPosition(new Date(t), latDeg, lonDeg).altitude
|
||||
const altNext = solarPosition(new Date(t + 3600e3), latDeg, lonDeg).altitude
|
||||
if (altT <= 0 && altNext > 0) { lastSunrise = t; break }
|
||||
}
|
||||
for (let h = 1; h <= 48; h++) {
|
||||
const t = date.getTime() - h * 3600e3
|
||||
const altT = solarPosition(new Date(t), latDeg, lonDeg).altitude
|
||||
const altNext = solarPosition(new Date(t + 3600e3), latDeg, lonDeg).altitude
|
||||
if (altT > 0 && altNext <= 0) { lastSunset = t; break }
|
||||
}
|
||||
// next sunset (forward scan)
|
||||
let nextSunset = NaN
|
||||
for (let h = 1; h <= 48; h++) {
|
||||
const t = date.getTime() + h * 3600e3
|
||||
const altT = solarPosition(new Date(t), latDeg, lonDeg).altitude
|
||||
const altNext = solarPosition(new Date(t + 3600e3), latDeg, lonDeg).altitude
|
||||
if (altT > 0 && altNext <= 0) { nextSunset = t + 3600e3; break }
|
||||
}
|
||||
if (Number.isNaN(lastSunrise)) return 0 // no sunrise in 48h → polar night
|
||||
// find next sunrise (forward scan) to disambiguate late-evening vs pre-dawn night
|
||||
let nextSunrise = NaN
|
||||
for (let h = 1; h <= 48; h++) {
|
||||
const t = date.getTime() + h * 3600e3
|
||||
const altT = solarPosition(new Date(t), latDeg, lonDeg).altitude
|
||||
const altNext = solarPosition(new Date(t + 3600e3), latDeg, lonDeg).altitude
|
||||
if (altT <= 0 && altNext > 0) { nextSunrise = t + 3600e3; break }
|
||||
}
|
||||
// night: past sunset (→1) until solar midnight, then pre-dawn (→0)
|
||||
if (!Number.isNaN(lastSunset) && lastSunset > lastSunrise) {
|
||||
if (Number.isNaN(nextSunrise)) return 1 // midnight sun edge
|
||||
const solarMidnight = lastSunset + (nextSunrise - lastSunset) / 2
|
||||
return date.getTime() < solarMidnight ? 1 : 0
|
||||
}
|
||||
if (Number.isNaN(nextSunset)) return 1 // midnight sun
|
||||
const span = nextSunset - lastSunrise
|
||||
if (span <= 0) return 1
|
||||
return Math.max(0, Math.min(1, (date.getTime() - lastSunrise) / span))
|
||||
}
|
||||
|
||||
function formatCountdown(seconds) {
|
||||
const s = Math.max(0, Math.round(seconds))
|
||||
if (s === 0) return '0m'
|
||||
if (s < 60) return '<1m'
|
||||
const m = Math.round(s / 60)
|
||||
if (m < 60) return m + 'm'
|
||||
return Math.floor(m / 60) + 'h ' + (m % 60) + 'm'
|
||||
}
|
||||
|
||||
module.exports = { solarPosition, dayProgress, formatCountdown }
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"package": "com.ilias.sundial",
|
||||
"name": "SUNDIAL",
|
||||
"icon": "/common/icon.png",
|
||||
"versionName": "0.3",
|
||||
"versionCode": 3,
|
||||
"minAPILevel": 1,
|
||||
"deviceTypeList": [
|
||||
"watch"
|
||||
],
|
||||
"config": {
|
||||
"logLevel": "debug",
|
||||
"designWidth": 466
|
||||
},
|
||||
"router": {
|
||||
"entry": "pages/index",
|
||||
"pages": {
|
||||
"pages/index": {
|
||||
"component": "index",
|
||||
"path": "/"
|
||||
}
|
||||
}
|
||||
},
|
||||
"display": {
|
||||
"backgroundColor": "#000000"
|
||||
},
|
||||
"features": [
|
||||
{
|
||||
"name": "system.router"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user