1.9.3
Updated scope temperature arc to gradient Fix banner scaling Add Pulldown to scope
This commit is contained in:
+15
-2
@@ -287,7 +287,7 @@ export function UTCIForecast() {
|
||||
<div class="utci-fetch-time">Last update: ${fetchedAt.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}</div>`}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="scope-col">
|
||||
<${ScopeReticle}
|
||||
value=${currentRow?.utciAdj ?? null}
|
||||
cat=${currentCat}
|
||||
@@ -298,7 +298,20 @@ export function UTCIForecast() {
|
||||
activeEvent=${lensEvent}
|
||||
utciEnvShort=${UTCI_ENVIRONMENTS[utciEnv]?.shortLabel ?? null}
|
||||
/>
|
||||
|
||||
<div class="scope-env-row" data-env=${utciEnv}>
|
||||
<span class="scope-env-label">Surroundings</span>
|
||||
<${CustomSelect}
|
||||
value=${utciEnv}
|
||||
isOn=${true}
|
||||
noHide=${true}
|
||||
grpClass="grp-felt"
|
||||
options=${Object.entries(UTCI_ENVIRONMENTS).map(([k, v]) => ({
|
||||
value: k,
|
||||
label: v.label,
|
||||
}))}
|
||||
onChange=${(v) => setUtciEnv(v)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="header-right">
|
||||
|
||||
+44
-33
@@ -10,7 +10,7 @@
|
||||
// SkyScope({ elev, dt, glob, size }) small porthole per hour
|
||||
// WindVane({ bearing, size }) compass-arrow widget
|
||||
// CloudIcon({ category, size, elev, dt }) cloud/sun glyph
|
||||
// ScopeReticle({ value, cat, loading, elev, dt, glob, activeEvent })
|
||||
// ScopeReticle({ value, cat, loading, elev, dt, glob, activeEvent, utciEnvShort })
|
||||
// large UTCI dial
|
||||
// PrecipIcon({ precip, snow, size }) rain/snow icon
|
||||
// ------------------------------------------------------------------------
|
||||
@@ -92,6 +92,7 @@ export function CustomSelect({ value, options, onChange, hideLabel, hidingLabel,
|
||||
onClick=${(e) => { e.stopPropagation(); setOpen(o => !o); }}
|
||||
type="button"
|
||||
>
|
||||
<span class="cs-btn-overlay"></span>
|
||||
<span class="cs-btn-label">${btnLabel}</span>
|
||||
<span class="cs-arrow"></span>
|
||||
</button>
|
||||
@@ -454,32 +455,39 @@ export function ScopeReticle({ value, cat, loading, elev = 0, dt = new Date(), g
|
||||
major, medium,
|
||||
};
|
||||
});
|
||||
const stressBands = [
|
||||
{ min: -40, max: -27, color: '#5a9bdc' },
|
||||
{ min: -27, max: -13, color: '#5a9bdc' },
|
||||
{ min: -13, max: 0, color: '#6ca9e1' },
|
||||
{ min: 0, max: 9, color: '#93ccd9' },
|
||||
{ min: 9, max: 18, color: '#92d4a3' },
|
||||
{ min: 18, max: 26, color: '#dae15f' },
|
||||
{ min: 26, max: 32, color: '#f8a13f' },
|
||||
{ min: 32, max: 38, color: '#e15333' },
|
||||
{ min: 38, max: 46, color: '#ae181c' },
|
||||
{ min: 46, max: 50, color: '#8d0616' },
|
||||
];
|
||||
function fracToXY(frac, r) {
|
||||
const deg = 135 + frac * 270;
|
||||
const rad = (deg * Math.PI) / 180;
|
||||
return [cx + r * Math.cos(rad), cy + r * Math.sin(rad)];
|
||||
}
|
||||
function bandArcPath(band) {
|
||||
const f1 = Math.min(1, Math.max(0, (band.min + 10) / 60));
|
||||
const f2 = Math.min(1, Math.max(0, (band.max + 10) / 60));
|
||||
const arcR = R - 18;
|
||||
// Gradient colour stops: frac 0 = −10 °C, frac 1 = 50 °C
|
||||
const ARC_GRAD = [
|
||||
[0, [106, 169, 225]],
|
||||
[0.167, [147, 204, 217]],
|
||||
[0.317, [146, 212, 163]],
|
||||
[0.467, [218, 225, 95]],
|
||||
[0.600, [248, 161, 63]],
|
||||
[0.700, [225, 83, 51]],
|
||||
[0.800, [174, 24, 28]],
|
||||
[1.000, [141, 6, 22]],
|
||||
];
|
||||
function arcColorAt(f) {
|
||||
for (let i = 0; i < ARC_GRAD.length - 1; i++) {
|
||||
const [f1, c1] = ARC_GRAD[i], [f2, c2] = ARC_GRAD[i + 1];
|
||||
if (f <= f2) {
|
||||
const t = (f - f1) / (f2 - f1);
|
||||
return `rgb(${Math.round(c1[0]+(c2[0]-c1[0])*t)},${Math.round(c1[1]+(c2[1]-c1[1])*t)},${Math.round(c1[2]+(c2[2]-c1[2])*t)})`;
|
||||
}
|
||||
}
|
||||
return 'rgb(141,6,22)';
|
||||
}
|
||||
const arcR = R - 18;
|
||||
const gradArc = Array.from({ length: 90 }, (_, i) => {
|
||||
const f1 = i / 90, f2 = (i + 1) / 90;
|
||||
const [x1, y1] = fracToXY(f1, arcR);
|
||||
const [x2, y2] = fracToXY(f2, arcR);
|
||||
const large = (f2 - f1) * 270 > 180 ? 1 : 0;
|
||||
return `M ${x1} ${y1} A ${arcR} ${arcR} 0 ${large} 1 ${x2} ${y2}`;
|
||||
}
|
||||
return { d: `M ${x1} ${y1} A ${arcR} ${arcR} 0 0 1 ${x2} ${y2}`, color: arcColorAt((f1+f2)/2) };
|
||||
});
|
||||
let needleX = cx, needleY = cy + 54;
|
||||
if (value != null) {
|
||||
const frac = Math.min(1, Math.max(0, (value + 10) / 60));
|
||||
@@ -569,9 +577,9 @@ export function ScopeReticle({ value, cat, loading, elev = 0, dt = new Date(), g
|
||||
if (!overlaySVG) return null;
|
||||
return html`<g dangerouslySetInnerHTML=${{ __html: overlaySVG }} />`;
|
||||
})()}
|
||||
${stressBands.map((b, i) => html`
|
||||
<path key=${i} d=${bandArcPath(b)} fill="none"
|
||||
stroke=${b.color} stroke-width="4" stroke-linecap="butt" />`)}
|
||||
${gradArc.map((seg, i) => html`
|
||||
<path key=${i} d=${seg.d} fill="none"
|
||||
stroke=${seg.color} stroke-width="4" stroke-linecap="butt" />`)}
|
||||
<circle cx=${cx} cy=${cy} r=${R} fill="none" stroke="#c9b08a" stroke-width="1.5" />
|
||||
${ticks.map((t, i) => html`
|
||||
<line key=${i} x1=${t.x1} y1=${t.y1} x2=${t.x2} y2=${t.y2}
|
||||
@@ -621,23 +629,26 @@ export function ScopeReticle({ value, cat, loading, elev = 0, dt = new Date(), g
|
||||
font-family="Fraunces, serif" font-weight="700">
|
||||
${value.toFixed(1)}°
|
||||
</text>
|
||||
<text x=${cx} y=${cy + 15} text-anchor="middle"
|
||||
fill=${readoutMutedColor} font-size="6.5"
|
||||
<text x=${cx} y=${cy + 14} text-anchor="middle"
|
||||
fill=${readoutColor} font-size="8"
|
||||
font-family="Manrope, sans-serif" font-weight="700" letter-spacing="1.2">
|
||||
UTCI NOW
|
||||
SUNSOAK INDEX
|
||||
</text>
|
||||
<text x=${cx} y=${cy + 23} text-anchor="middle"
|
||||
fill=${glowColor} font-size="7"
|
||||
fill=${glowColor} font-size="9"
|
||||
font-family="Manrope, sans-serif" font-weight="700" letter-spacing="0.5">
|
||||
${cat.label.toUpperCase()}
|
||||
</text>
|
||||
${utciEnvShort && html`
|
||||
<text x=${cx} y="163" text-anchor="middle"
|
||||
fill=${readoutMutedColor} font-size="5.5"
|
||||
font-family="Manrope, sans-serif" font-weight="700" letter-spacing="0.8"
|
||||
opacity="0.85">
|
||||
${utciEnvShort.toUpperCase()} EST.
|
||||
</text>`}
|
||||
<${Fragment}>
|
||||
<rect x=${cx - 34} y="158" width="68" height="11" rx="3"
|
||||
fill=${glowColor} opacity="0.22" />
|
||||
<text x=${cx} y="166" text-anchor="middle"
|
||||
fill=${readoutColor} font-size="7"
|
||||
font-family="Manrope, sans-serif" font-weight="700" letter-spacing="0.6">
|
||||
${utciEnvShort.toUpperCase()}
|
||||
</text>
|
||||
</>`}
|
||||
</>`
|
||||
: html`<text x=${cx} y=${cy + 5} text-anchor="middle"
|
||||
fill=${readoutMutedColor} font-size="8"
|
||||
|
||||
@@ -485,36 +485,63 @@ export function useAppState() {
|
||||
if (bannerIndex >= activeEvents.length) setBannerIndex(0);
|
||||
}, [activeEvents.length]);
|
||||
|
||||
// ── BANNER HEIGHT → shell padding-top ────────────────────────────────
|
||||
// Floats the banner over the page. Padding only grows while open so
|
||||
// shorter slides never cause the page to jump up. Resets on close.
|
||||
// ── BANNER POSITION + SHELL PADDING ───────────────────────────────────
|
||||
// The banner is position:absolute so it floats over content. We (1) pin its
|
||||
// top to the bottom edge of the site-nav so it sits flush below the nav on
|
||||
// every screen size, and (2) add matching padding-top to the shell so the
|
||||
// page content sits just below the banner.
|
||||
// On close: keep the banner pinned to the nav and animate the padding back
|
||||
// to 0 so content slides up smoothly as the banner collapses.
|
||||
const bannerMaxHeightRef = useRef(0);
|
||||
|
||||
useEffect(() => {
|
||||
const shell = document.querySelector('.utci-shell');
|
||||
if (!shell) return;
|
||||
const wrap = document.querySelector('.event-banner-wrap');
|
||||
if (!shell || !wrap) return;
|
||||
|
||||
// The site-nav's outer height is the banner's resting top edge. The banner
|
||||
// lives inside the shell, so translate that into the shell's coordinate
|
||||
// space by subtracting the shell's own offset from the page top.
|
||||
const bannerTop = () => {
|
||||
const navEl = document.getElementById('site-nav');
|
||||
const navOuterHeight = navEl ? navEl.offsetHeight : 0;
|
||||
return navOuterHeight - shell.offsetTop;
|
||||
};
|
||||
|
||||
if (!bannerVisible) {
|
||||
bannerMaxHeightRef.current = 0;
|
||||
shell.style.paddingTop = '';
|
||||
return;
|
||||
// Keep the collapsing banner pinned to the nav's bottom edge, and animate
|
||||
// padding back to zero matching the banner collapse duration.
|
||||
wrap.style.top = bannerTop() + 'px';
|
||||
shell.style.transition = 'padding-top 0.45s cubic-bezier(0.4, 0, 0.2, 1)';
|
||||
shell.style.paddingTop = '0px';
|
||||
const tid = setTimeout(() => {
|
||||
shell.style.transition = '';
|
||||
shell.style.paddingTop = '';
|
||||
bannerMaxHeightRef.current = 0;
|
||||
}, 500);
|
||||
return () => clearTimeout(tid);
|
||||
}
|
||||
|
||||
const applyPadding = () => {
|
||||
const wrap = document.querySelector('.event-banner-wrap');
|
||||
if (!wrap) return;
|
||||
const h = wrap.getBoundingClientRect().height;
|
||||
if (h > bannerMaxHeightRef.current) {
|
||||
const applyPadding = (allowShrink = false) => {
|
||||
const top = bannerTop();
|
||||
wrap.style.top = top + 'px';
|
||||
const h = top + wrap.getBoundingClientRect().height;
|
||||
if (allowShrink || h > bannerMaxHeightRef.current) {
|
||||
bannerMaxHeightRef.current = h;
|
||||
shell.style.transition = ''; // no transition while open/resizing
|
||||
shell.style.paddingTop = h + 'px';
|
||||
}
|
||||
};
|
||||
|
||||
// Pin the banner immediately so it appears flush under the nav, then size
|
||||
// the content padding once the open animation settles.
|
||||
wrap.style.top = bannerTop() + 'px';
|
||||
const onResize = () => applyPadding(true);
|
||||
const tid = setTimeout(applyPadding, 460);
|
||||
window.addEventListener('resize', applyPadding);
|
||||
window.addEventListener('resize', onResize);
|
||||
return () => {
|
||||
clearTimeout(tid);
|
||||
window.removeEventListener('resize', applyPadding);
|
||||
window.removeEventListener('resize', onResize);
|
||||
};
|
||||
}, [bannerVisible, bannerIndex, activeEvents.length]);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user