5.0.8
Day tab colour update Cleanup event banner and add close
This commit is contained in:
@@ -30,3 +30,4 @@ dist/
|
|||||||
*.ps1
|
*.ps1
|
||||||
*.py
|
*.py
|
||||||
secrets.local.php
|
secrets.local.php
|
||||||
|
/data
|
||||||
|
|||||||
+81
-3
@@ -272,15 +272,27 @@ body {
|
|||||||
.event-note-slide {
|
.event-note-slide {
|
||||||
grid-area: 1 / 1; /* stack all slides on top of each other */
|
grid-area: 1 / 1; /* stack all slides on top of each other */
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: baseline;
|
flex-direction: column;
|
||||||
flex-wrap: wrap;
|
gap: 2px;
|
||||||
gap: 4px 10px;
|
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
opacity: 0; /* only .is-active is visible */
|
opacity: 0; /* only .is-active is visible */
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
user-select: none; /* keeps hidden copy out of a drag-selection */
|
user-select: none; /* keeps hidden copy out of a drag-selection */
|
||||||
transition: opacity 0.35s ease;
|
transition: opacity 0.35s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.event-note-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-note-body {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 10px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
.event-note-slide.is-active {
|
.event-note-slide.is-active {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
@@ -351,6 +363,63 @@ body {
|
|||||||
transform: scale(1.3);
|
transform: scale(1.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Close control — a small brass "Close ×" pill in the top-right corner. It
|
||||||
|
sits at the end of the strip's flex row rather than being absolutely
|
||||||
|
positioned, so the dates line can never run underneath it. */
|
||||||
|
.event-note-close {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
align-self: flex-start;
|
||||||
|
margin: -2px -6px 0 2px; /* pull into the padding so it hugs the corner */
|
||||||
|
padding: 3px 7px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: transparent;
|
||||||
|
font-family: Manrope, sans-serif;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: #9a7d5a;
|
||||||
|
line-height: 1;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s, color 0.2s, border-color 0.2s;
|
||||||
|
}
|
||||||
|
.event-note-close:hover {
|
||||||
|
background: rgba(200, 146, 42, 0.14);
|
||||||
|
border-color: rgba(200, 146, 42, 0.45);
|
||||||
|
color: #7a5a1e;
|
||||||
|
}
|
||||||
|
.event-note-close:focus-visible {
|
||||||
|
outline: 2px solid #c8922a;
|
||||||
|
outline-offset: 1px;
|
||||||
|
}
|
||||||
|
.event-note.is-priority .event-note-close {
|
||||||
|
color: #a06e18;
|
||||||
|
}
|
||||||
|
.event-note.is-tinted .event-note-close {
|
||||||
|
color: #56452c;
|
||||||
|
}
|
||||||
|
.event-note.is-tinted .event-note-close:hover {
|
||||||
|
background: rgba(0, 0, 0, 0.09);
|
||||||
|
border-color: rgba(0, 0, 0, 0.22);
|
||||||
|
color: #2a1a08;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Collapse on the way out — height and vertical spacing animate to zero so
|
||||||
|
the page closes the gap smoothly instead of snapping upward. */
|
||||||
|
.event-note.is-closing {
|
||||||
|
animation: event-note-out 0.26s ease forwards;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
@keyframes event-note-out {
|
||||||
|
from { opacity: 1; transform: translateY(0); max-height: 200px; }
|
||||||
|
to { opacity: 0; transform: translateY(-4px); max-height: 0;
|
||||||
|
margin-bottom: 0; padding-top: 0; padding-bottom: 0; border-width: 0; }
|
||||||
|
}
|
||||||
|
|
||||||
/* Above true-mobile: the nav drops into the flow beneath the note. Its own
|
/* Above true-mobile: the nav drops into the flow beneath the note. Its own
|
||||||
32px side padding is dropped so the links line up with the note and the
|
32px side padding is dropped so the links line up with the note and the
|
||||||
content rather than sitting 32px inside them. */
|
content rather than sitting 32px inside them. */
|
||||||
@@ -376,6 +445,15 @@ body {
|
|||||||
.event-note-dates {
|
.event-note-dates {
|
||||||
margin-left: 0; /* no room to right-align on a phone */
|
margin-left: 0; /* no room to right-align on a phone */
|
||||||
}
|
}
|
||||||
|
/* Phone: the cross alone, pushed to the right-hand end of the row and
|
||||||
|
padded out to a ~26px touch target. */
|
||||||
|
.event-note-close {
|
||||||
|
margin: -4px -6px 0 auto;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
.event-note-close-label {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Nav logo — hidden on desktop, shown on mobile */
|
/* Nav logo — hidden on desktop, shown on mobile */
|
||||||
|
|||||||
+49
-7
@@ -276,6 +276,31 @@ export function UTCIForecast() {
|
|||||||
setTimeout(() => { setNoteIndex(next); setNoteFading(false); }, NOTE_FADE_MS);
|
setTimeout(() => { setNoteIndex(next); setNoteFading(false); }, NOTE_FADE_MS);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ── Dismissal ────────────────────────────────────────────────────────
|
||||||
|
// Closing the strip hides it for the rest of the browser session, but only
|
||||||
|
// for the events that were showing at the time: the dismissal is stored
|
||||||
|
// against a signature of the active ids, so a new event (or a fresh weather
|
||||||
|
// warning) brings the strip back rather than staying silently suppressed.
|
||||||
|
const NOTE_CLOSE_KEY = 'sunscope_event_note_closed';
|
||||||
|
const NOTE_CLOSE_MS = 260;
|
||||||
|
const noteSig = activeEvents.map(e => e.id).sort().join('|');
|
||||||
|
const [noteClosedSig, setNoteClosedSig] = useState(() => {
|
||||||
|
try { return sessionStorage.getItem(NOTE_CLOSE_KEY) || ''; } catch (e) { return ''; }
|
||||||
|
});
|
||||||
|
// Kept mounted for the collapse animation, then dropped.
|
||||||
|
const [noteClosing, setNoteClosing] = useState(false);
|
||||||
|
const noteDismissed = !!noteSig && noteSig === noteClosedSig;
|
||||||
|
|
||||||
|
const noteClose = () => {
|
||||||
|
if (noteClosing) return;
|
||||||
|
setNoteClosing(true);
|
||||||
|
setTimeout(() => {
|
||||||
|
try { sessionStorage.setItem(NOTE_CLOSE_KEY, noteSig); } catch (e) { /* ignore */ }
|
||||||
|
setNoteClosedSig(noteSig);
|
||||||
|
setNoteClosing(false);
|
||||||
|
}, NOTE_CLOSE_MS);
|
||||||
|
};
|
||||||
|
|
||||||
// Keep the index in range if the event list shrinks between forecasts.
|
// Keep the index in range if the event list shrinks between forecasts.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (noteIndex >= activeEvents.length) setNoteIndex(0);
|
if (noteIndex >= activeEvents.length) setNoteIndex(0);
|
||||||
@@ -669,8 +694,8 @@ export function UTCIForecast() {
|
|||||||
// • "utci-about" — the explainer paragraphs at the bottom
|
// • "utci-about" — the explainer paragraphs at the bottom
|
||||||
// • "utci-footer" — the "reading the table" note
|
// • "utci-footer" — the "reading the table" note
|
||||||
return html`
|
return html`
|
||||||
<div class=${`utci-app${activeEvents.length > 0 ? ' has-event-note' : ''}`}>
|
<div class=${`utci-app${activeEvents.length > 0 && !noteDismissed ? ' has-event-note' : ''}`}>
|
||||||
${activeEvents.length > 0 && (() => {
|
${activeEvents.length > 0 && !noteDismissed && (() => {
|
||||||
const ev = activeEvents[noteIndex] || activeEvents[0];
|
const ev = activeEvents[noteIndex] || activeEvents[0];
|
||||||
const isPriority = PRIORITY_WEATHER_IDS.has(ev.id);
|
const isPriority = PRIORITY_WEATHER_IDS.has(ev.id);
|
||||||
const fmtDate = (iso) => iso
|
const fmtDate = (iso) => iso
|
||||||
@@ -703,7 +728,7 @@ export function UTCIForecast() {
|
|||||||
};
|
};
|
||||||
})() : undefined;
|
})() : undefined;
|
||||||
return html`
|
return html`
|
||||||
<div class=${`event-note${isPriority ? ' is-priority' : ''}${noteTintStyle ? ' is-tinted' : ''}`} style=${noteTintStyle}>
|
<div class=${`event-note${isPriority ? ' is-priority' : ''}${noteTintStyle ? ' is-tinted' : ''}${noteClosing ? ' is-closing' : ''}`} style=${noteTintStyle}>
|
||||||
<span class="event-note-accent" style=${{ background: ev.color === '#fdf8ee' ? '#c8922a' : ev.color }} />
|
<span class="event-note-accent" style=${{ background: ev.color === '#fdf8ee' ? '#c8922a' : ev.color }} />
|
||||||
${/* Every slide is rendered, all stacked in one CSS grid cell, so the
|
${/* Every slide is rendered, all stacked in one CSS grid cell, so the
|
||||||
strip is always as tall as the LONGEST event's copy. Rotating to
|
strip is always as tall as the LONGEST event's copy. Rotating to
|
||||||
@@ -720,10 +745,14 @@ export function UTCIForecast() {
|
|||||||
class=${`event-note-slide${isActive ? ' is-active' : ''}`}
|
class=${`event-note-slide${isActive ? ' is-active' : ''}`}
|
||||||
aria-hidden=${isActive ? undefined : 'true'}
|
aria-hidden=${isActive ? undefined : 'true'}
|
||||||
>
|
>
|
||||||
<span class="event-note-emoji">${e.emoji}</span>
|
<div class="event-note-header">
|
||||||
<span class="event-note-title">${e.title}</span>
|
<span class="event-note-emoji">${e.emoji}</span>
|
||||||
<span class="event-note-msg">${e.message}</span>
|
<span class="event-note-title">${e.title}</span>
|
||||||
${dateLine && html`<span class="event-note-dates">${dateLine}</span>`}
|
</div>
|
||||||
|
<div class="event-note-body">
|
||||||
|
<span class="event-note-msg">${e.message}</span>
|
||||||
|
${dateLine && html`<span class="event-note-dates">${dateLine}</span>`}
|
||||||
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
@@ -738,6 +767,19 @@ export function UTCIForecast() {
|
|||||||
onClick=${() => noteGoTo(i)}
|
onClick=${() => noteGoTo(i)}
|
||||||
/>`)}
|
/>`)}
|
||||||
</div>`}
|
</div>`}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="event-note-close"
|
||||||
|
aria-label="Close event banner"
|
||||||
|
title="Close"
|
||||||
|
onClick=${noteClose}
|
||||||
|
>
|
||||||
|
<span class="event-note-close-label">Close</span>
|
||||||
|
<svg viewBox="0 0 12 12" width="10" height="10" aria-hidden="true">
|
||||||
|
<path d="M1.5 1.5 L10.5 10.5 M10.5 1.5 L1.5 10.5"
|
||||||
|
fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
</div>`;
|
</div>`;
|
||||||
})()}
|
})()}
|
||||||
<nav class="utci-topnav" id="site-nav">
|
<nav class="utci-topnav" id="site-nav">
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
|
|
||||||
import { h, Fragment } from '../../vendor/preact.js';
|
import { h, Fragment } from '../../vendor/preact.js';
|
||||||
import htm from '../../vendor/htm.js';
|
import htm from '../../vendor/htm.js';
|
||||||
import { confidenceBand, utciCategory, petCategory, hexToRgb, mixRgb, rainTint, cloudTint, snowTint, VEHICLE_TYPES, BUILDING_TYPES, FUR_COLORS } from '../utils.js';
|
import { confidenceBand, utciCategory, petCategory, UTCI_BANDS, PET_BANDS, bandRampRgb, hexToRgb, mixRgb, rainTint, cloudTint, snowTint, VEHICLE_TYPES, BUILDING_TYPES, FUR_COLORS } from '../utils.js';
|
||||||
import { FREE_DAYS, UTCI_ENVIRONMENTS, deriveProfileMain, FILTER_PROFILES, variantIcons } from '../config.js';
|
import { FREE_DAYS, UTCI_ENVIRONMENTS, deriveProfileMain, FILTER_PROFILES, variantIcons } from '../config.js';
|
||||||
import { CloudIcon, PrecipIcon, HouseIcon, CarIcon, FogIcon, IceIcon } from '../components.js';
|
import { CloudIcon, PrecipIcon, HouseIcon, CarIcon, FogIcon, IceIcon } from '../components.js';
|
||||||
import { SubscribeModal } from './SubscribeModal.js';
|
import { SubscribeModal } from './SubscribeModal.js';
|
||||||
@@ -292,6 +292,14 @@ export function DayTabs({
|
|||||||
// event banner can colour itself from exactly the same source.
|
// event banner can colour itself from exactly the same source.
|
||||||
const catFn = isPetsProfile ? petCategory : utciCategory;
|
const catFn = isPetsProfile ? petCategory : utciCategory;
|
||||||
const tBand = dayHi !== null ? catFn(dayHi) : { bg: '#f8e554' };
|
const tBand = dayHi !== null ? catFn(dayHi) : { bg: '#f8e554' };
|
||||||
|
// Band colour, but ramped: bandRampRgb() interpolates between the
|
||||||
|
// this band's colour and the next one's, so 30° isn't the same
|
||||||
|
// swatch as 27° while 32° still lands on the Extreme orange
|
||||||
|
// (see BAND COLOUR RAMP in utils.js). Danger stays flat — it's an
|
||||||
|
// alarm colour, not a point on the gradient.
|
||||||
|
const tBandRgb = tBand.solid
|
||||||
|
? hexToRgb(tBand.bg)
|
||||||
|
: bandRampRgb(dayHi, isPetsProfile ? PET_BANDS : UTCI_BANDS);
|
||||||
|
|
||||||
const ccVals = repRows.map(r => r.cc).filter(v => isFinite(v));
|
const ccVals = repRows.map(r => r.cc).filter(v => isFinite(v));
|
||||||
const avgCloud = ccVals.length ? ccVals.reduce((s, v) => s + v, 0) / ccVals.length : 0;
|
const avgCloud = ccVals.length ? ccVals.reduce((s, v) => s + v, 0) / ccVals.length : 0;
|
||||||
@@ -314,7 +322,7 @@ export function DayTabs({
|
|||||||
|
|
||||||
let rgb;
|
let rgb;
|
||||||
if (tempOnly) {
|
if (tempOnly) {
|
||||||
rgb = hexToRgb(tBand.bg);
|
rgb = tBandRgb;
|
||||||
} else if (isHot) {
|
} else if (isHot) {
|
||||||
// Keep the heat hue; cloud only mutes it a touch (orange/red never
|
// Keep the heat hue; cloud only mutes it a touch (orange/red never
|
||||||
// greens) and rain does not override heat.
|
// greens) and rain does not override heat.
|
||||||
@@ -322,7 +330,7 @@ export function DayTabs({
|
|||||||
? (dayHi >= 49 ? 0.15 : dayHi >= 43 ? 0.30 : 0.45)
|
? (dayHi >= 49 ? 0.15 : dayHi >= 43 ? 0.30 : 0.45)
|
||||||
: (dayHi >= 39 ? 0.15 : dayHi >= 34 ? 0.30 : 0.45);
|
: (dayHi >= 39 ? 0.15 : dayHi >= 34 ? 0.30 : 0.45);
|
||||||
const t = Math.max(0, Math.min(1, (avgCloud - 30) / 70)) * heatFactor;
|
const t = Math.max(0, Math.min(1, (avgCloud - 30) / 70)) * heatFactor;
|
||||||
rgb = mixRgb(hexToRgb(tBand.bg), [170, 162, 152], t);
|
rgb = mixRgb(tBandRgb, [170, 162, 152], t);
|
||||||
} else if (daySnow >= 0.1) {
|
} else if (daySnow >= 0.1) {
|
||||||
// Snow: pale → icy blue with depth.
|
// Snow: pale → icy blue with depth.
|
||||||
rgb = snowTint(daySnow);
|
rgb = snowTint(daySnow);
|
||||||
@@ -334,7 +342,7 @@ export function DayTabs({
|
|||||||
rgb = cloudTint(avgCloud);
|
rgb = cloudTint(avgCloud);
|
||||||
} else if (dayHi !== null && dayHi < (isPetsProfile ? 2 : 10)) {
|
} else if (dayHi !== null && dayHi < (isPetsProfile ? 2 : 10)) {
|
||||||
// Clear & cold: keep the cold temperature blue.
|
// Clear & cold: keep the cold temperature blue.
|
||||||
rgb = hexToRgb(tBand.bg);
|
rgb = tBandRgb;
|
||||||
} else {
|
} else {
|
||||||
// Clear & mild/warm: a plain sunny yellow.
|
// Clear & mild/warm: a plain sunny yellow.
|
||||||
rgb = hexToRgb('#f8e554');
|
rgb = hexToRgb('#f8e554');
|
||||||
@@ -350,16 +358,29 @@ export function DayTabs({
|
|||||||
}
|
}
|
||||||
const [wR, wG, wB] = rgb;
|
const [wR, wG, wB] = rgb;
|
||||||
|
|
||||||
const wBgNeutral = isDanger
|
// How pastel the tab is drawn also carries heat: the tint is blended
|
||||||
? weatherGradientNeutral(wR, wG, wB, 0.15, 0.72)
|
// toward white, and the hotter the day the less white goes in. This
|
||||||
: isExtreme
|
// used to step at the band edges (0.50 for everything up to Caution,
|
||||||
? weatherGradientNeutral(wR, wG, wB, 0.20, 0.66)
|
// then 0.20 at Extreme), which made 30° look like a washed-out yellow
|
||||||
: weatherGradientNeutral(wR, wG, wB);
|
// while 32° snapped to a solid orange. Ramp the blend continuously
|
||||||
const wBgActive = isDanger
|
// instead — the endpoints still land on the old Extreme (heatT = 1)
|
||||||
? weatherGradientActive(wR, wG, wB, 0.12, 0.72)
|
// and Danger (both = 1) values, so only the in-between days change.
|
||||||
: isExtreme
|
const heatFloor = isPetsProfile ? 35 : 27; // start of the hot half
|
||||||
? weatherGradientActive(wR, wG, wB, 0.16, 0.66)
|
const heatMid = isPetsProfile ? 40 : 32; // Extreme threshold
|
||||||
: weatherGradientActive(wR, wG, wB);
|
const heatTop = isPetsProfile ? 52 : 41; // Danger threshold
|
||||||
|
const clamp01 = (v) => Math.max(0, Math.min(1, v));
|
||||||
|
const heatT = dayHi === null ? 0 : clamp01((dayHi - heatFloor) / (heatMid - heatFloor));
|
||||||
|
const overT = dayHi === null ? 0 : clamp01((dayHi - heatMid) / (heatTop - heatMid));
|
||||||
|
const wBgNeutral = weatherGradientNeutral(
|
||||||
|
wR, wG, wB,
|
||||||
|
0.50 - 0.30 * heatT - 0.05 * overT, // edge: 0.50 → 0.20 → 0.15
|
||||||
|
0.69 - 0.03 * heatT + 0.06 * overT, // centre: 0.69 → 0.66 → 0.72
|
||||||
|
);
|
||||||
|
const wBgActive = weatherGradientActive(
|
||||||
|
wR, wG, wB,
|
||||||
|
0.42 - 0.26 * heatT - 0.04 * overT, // edge: 0.42 → 0.16 → 0.12
|
||||||
|
0.64 + 0.02 * heatT + 0.06 * overT, // centre: 0.64 → 0.66 → 0.72
|
||||||
|
);
|
||||||
const wText = '#2a1d10';
|
const wText = '#2a1d10';
|
||||||
// Active-tab outline: the day's weather colour, 20% darker.
|
// Active-tab outline: the day's weather colour, 20% darker.
|
||||||
const wOutline = `rgb(${Math.round(wR * 0.8)},${Math.round(wG * 0.8)},${Math.round(wB * 0.8)})`;
|
const wOutline = `rgb(${Math.round(wR * 0.8)},${Math.round(wG * 0.8)},${Math.round(wB * 0.8)})`;
|
||||||
|
|||||||
@@ -56,6 +56,58 @@ export function utciCategory(u) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------
|
||||||
|
// BAND COLOUR RAMP - continuous version of the banded palette.
|
||||||
|
// -------------------------------------------------------------------
|
||||||
|
// utciCategory()/petCategory() hand back ONE flat colour per band, which
|
||||||
|
// is right for table cells (a cell states which band you are in) but wrong
|
||||||
|
// for anything that should read as a temperature gradient: every day from
|
||||||
|
// 27 -C to 31.9 -C is the same "Caution" swatch, so the only visible change
|
||||||
|
// is the hard jump at the next threshold.
|
||||||
|
//
|
||||||
|
// bandRampRgb() anchors each band's colour at the BOTTOM of that band's
|
||||||
|
// range and interpolates toward the next band's colour across the band. So
|
||||||
|
// a temperature sitting exactly on a threshold still gets that band's own
|
||||||
|
// published colour (32 -C is still the Extreme orange), and every degree
|
||||||
|
// above it slides proportionally toward the next tier - 30 -C lands 60% of
|
||||||
|
// the way from Caution to Extreme instead of being identical to 27 -C.
|
||||||
|
//
|
||||||
|
// The solid Danger band is left out of the ramp: it's a flat stop-everything
|
||||||
|
// alarm colour, not part of the gradient, so the ramp clamps at the last
|
||||||
|
// graded band and callers keep handling Danger separately.
|
||||||
|
// -------------------------------------------------------------------
|
||||||
|
const rampAnchorCache = new WeakMap();
|
||||||
|
|
||||||
|
function rampAnchors(bands) {
|
||||||
|
let anchors = rampAnchorCache.get(bands);
|
||||||
|
if (anchors) return anchors;
|
||||||
|
anchors = [];
|
||||||
|
let lower = null;
|
||||||
|
for (const band of bands) {
|
||||||
|
if (band.solid) break;
|
||||||
|
// The first band is open-ended downward; give it a nominal 10 -C span so
|
||||||
|
// it still has an anchor below its own threshold.
|
||||||
|
anchors.push({ at: lower === null ? band.max - 10 : lower, rgb: hexToRgb(band.bg) });
|
||||||
|
if (band.max === undefined) break;
|
||||||
|
lower = band.max;
|
||||||
|
}
|
||||||
|
rampAnchorCache.set(bands, anchors);
|
||||||
|
return anchors;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function bandRampRgb(t, bands) {
|
||||||
|
const anchors = rampAnchors(bands);
|
||||||
|
if (t === null || !isFinite(t)) return anchors[0].rgb.slice();
|
||||||
|
if (t <= anchors[0].at) return anchors[0].rgb.slice();
|
||||||
|
const last = anchors[anchors.length - 1];
|
||||||
|
if (t >= last.at) return last.rgb.slice();
|
||||||
|
for (let i = 1; i < anchors.length; i++) {
|
||||||
|
const a = anchors[i - 1], b = anchors[i];
|
||||||
|
if (t <= b.at) return mixRgb(a.rgb, b.rgb, (t - a.at) / (b.at - a.at));
|
||||||
|
}
|
||||||
|
return last.rgb.slice();
|
||||||
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------
|
// -------------------------------------------------------------------
|
||||||
// WEATHER TINT PALETTE - single source of truth for "what colour is
|
// WEATHER TINT PALETTE - single source of truth for "what colour is
|
||||||
// this weather", shared by the day tabs (components/DayTabs.js) and the
|
// this weather", shared by the day tabs (components/DayTabs.js) and the
|
||||||
|
|||||||
Reference in New Issue
Block a user