This commit is contained in:
2026-06-26 00:05:28 -04:00
parent 9b7b7f49a7
commit 1ee386949c
11 changed files with 521 additions and 326 deletions

View File

@@ -1,12 +1,10 @@
<script setup lang="ts">
import Loading from '@/components/Loading.vue';
import { inject, onMounted, ref, computed } from 'vue';
import { api } from '../services/api';
import {inject, computed } from 'vue';
import {useWeather} from '../services/api';
const data = ref<any>(null);
const openGraph = inject<(key: string) => void>('openGraph');
onMounted(async () => data.value = await api.current());
const {current: data} = useWeather();
const W = 320, H = 120, HORIZON = 45;
@@ -22,41 +20,42 @@ const arc = computed(() => {
const start = noon - 12 * 3600 * 1000;
const end = noon + 12 * 3600 * 1000;
const riseT = (rise - start) / (end - start);
const setT = (set - start) / (end - start);
const dayPct = Math.max(0, Math.min(1, (now - rise) / dayLen));
const riseT = (rise - start) / (end - start);
const setT = (set - start) / (end - start);
const dayPct = Math.max(0, Math.min(1, (now - rise) / dayLen));
const sunT = riseT + dayPct * (setT - riseT);
// After sunset, continue tracking sun below horizon
const totalPct = Math.max(0, Math.min(1, (now - start) / (end - start)));
const x1 = 0, x2 = W;
const totalW = x2 - x1;
const amplitude = H * 0.45;
const midY = H / 2;
const sineY = (t: number) => midY - Math.sin(t * Math.PI * 2 - Math.PI / 2) * amplitude;
const horizonY = sineY(riseT);
const pts = Array.from({ length: 201 }, (_, i) => {
const t = i / 200;
const x = x1 + totalW * t;
// Remap t so the arc peaks between riseT and setT only
const arcT = (t - riseT) / (setT - riseT);
const y = arcT >= 0 && arcT <= 1
? HORIZON - Math.sin(arcT * Math.PI) * HORIZON
: HORIZON + Math.abs(Math.sin(arcT * Math.PI)) * (HORIZON * 0.3); // subtle below-horizon dip
return { x, y };
return { x: x1 + totalW * t, y: sineY(t) };
});
const sunT = riseT + dayPct * (setT - riseT);
const splitIdx = Math.round(sunT * 200);
const { x: cx, y: cy } = pts[splitIdx];
const splitIdx = Math.round(totalPct * 200);
const { x: cx, y: cy } = pts[Math.min(splitIdx, 200)];
const traveledPts = pts.slice(0, splitIdx + 1);
const filledPath = traveledPts.map((p, i) => `${i === 0 ? 'M' : 'L'}${p.x.toFixed(1)},${p.y.toFixed(1)}`).join(' ')
+ ` L${traveledPts[traveledPts.length - 1].x.toFixed(1)},${HORIZON}`
+ ` L${x1},${HORIZON} Z`;
+ ` L${traveledPts[traveledPts.length - 1].x.toFixed(1)},${horizonY}`
+ ` L${x1},${horizonY} Z`;
const remainPts = pts.slice(splitIdx);
const remainPath = remainPts.map((p, i) => `${i === 0 ? 'M' : 'L'}${p.x.toFixed(1)},${p.y.toFixed(1)}`).join(' ');
const riseX = x1 + totalW * riseT;
const setX = x1 + totalW * setT;
const noonX = x1 + totalW * 0.5;
return { filledPath, remainPath, cx, cy, pct: dayPct, riseX, setX, noonX };
return { filledPath, remainPath, cx, cy, pct: dayPct, riseX, setX, horizonY };
});
</script>
@@ -64,28 +63,32 @@ const arc = computed(() => {
<div class="sun-card">
<div class="card-title">🌅 Sun</div>
<!-- Top labels -->
<div class="sun-top-labels" v-if="data">
<div>
<div class="stat-label">Sunrise</div>
<div class="stat-value">{{ new Date(data.sunrise).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) }}</div>
</div>
<div style="text-align:right">
<div class="stat-label">Sunset</div>
<div class="stat-value">{{ new Date(data.sunset).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) }}</div>
</div>
</div>
<div v-if="!data" class="w-100 pos-rel br-2 overflow-hidden mx-auto" style="width: 100%; aspect-ratio: 1.5"><Loading /></div>
<svg :viewBox="`0 0 ${W} ${H}`" preserveAspectRatio="none">
<!-- Filled traveled area -->
<path v-if="arc.filledPath" :d="arc.filledPath" fill="#3b82f6" opacity="0.35" />
<!-- Remaining curve stroke -->
<path v-if="arc.remainPath" :d="arc.remainPath" fill="none" stroke="#94a3b8" stroke-width="1.5" opacity="0.5" />
<!-- Horizon line -->
<line :x1="0" :y1="HORIZON" :x2="W" :y2="HORIZON" stroke="var(--border)" stroke-width="1" />
<!-- Sun dot -->
<circle v-if="data" :cx="arc.cx" :cy="arc.cy" r="8" fill="#f59e0b" class="sun-dot" />
</svg>
<template v-else>
<!-- Top labels -->
<div class="sun-top-labels" v-if="data">
<div>
<div class="stat-label">Sunrise</div>
<div class="stat-value">{{ new Date(data.sunrise).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) }}</div>
</div>
<div style="text-align:right">
<div class="stat-label">Sunset</div>
<div class="stat-value">{{ new Date(data.sunset).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) }}</div>
</div>
</div>
<svg :viewBox="`0 0 ${W} ${H}`" preserveAspectRatio="none">
<!-- Filled traveled area -->
<path v-if="arc.filledPath" :d="arc.filledPath" fill="#3b82f6" opacity="0.35" />
<!-- Remaining curve stroke -->
<path v-if="arc.remainPath" :d="arc.remainPath" fill="none" stroke="#94a3b8" stroke-width="1.5" opacity="0.5" />
<!-- Horizon line -->
<line :x1="0" :y1="arc.horizonY" :x2="W" :y2="arc.horizonY" stroke="var(--border)" stroke-width="1" />
<!-- Sun dot -->
<circle v-if="data" :cx="arc.cx" :cy="arc.cy" r="8" fill="#f59e0b" class="sun-dot" />
</svg>
</template>
<div class="divider" />