Profile memory
Comment cleanup
This commit is contained in:
fraxle
2026-05-18 23:02:41 +01:00
parent dded468bec
commit 85c415d1f5
16 changed files with 504 additions and 581 deletions
+83 -83
View File
@@ -1,7 +1,7 @@
// ════════════════════════════════════════════════════════════════════════
// components.js Preact UI components (SVG widgets and controls).
// ------------------------------------------------------------------------
// components.js - Preact UI components (SVG widgets and controls).
//
// All components use the html`` tagged template from htm + Preact.
// All components use the html'' tagged template from htm + Preact.
// No external state; they depend only on utils.js and events.js.
//
// Exports (in order of appearance):
@@ -13,7 +13,7 @@
// ScopeReticle({ value, cat, loading, elev, dt, glob, activeEvent })
// large UTCI dial
// PrecipIcon({ precip, snow, size }) rain/snow icon
// ════════════════════════════════════════════════════════════════════════
// ------------------------------------------------------------------------
import { h, Fragment } from '../vendor/preact.js';
import { useState, useEffect, useRef } from '../vendor/preact-hooks.js';
@@ -26,19 +26,19 @@ import { getLensOverlaySVG } from './events.js';
const html = htm.bind(h);
// ═══════════════════════════════════════════════════════════════════
// CUSTOMSELECT cross-platform styled dropdown pill.
// ───────────────────────────────────────────────────────────────────
// -------------------------------------------------------------------
// CUSTOMSELECT - cross-platform styled dropdown pill.
// -------------------------------------------------------------------
// props:
// value current value string
// options array of { value, label, disabled }
// onChange fn(newValue)
// hideLabel label shown when value === 'off' (e.g. 'Burn')
// hidingLabel label shown for the 'off' option when active (e.g. 'Hide Burn')
// isOn bool whether the pill shows as active (brass)
// groupedLeft bool fuse right side with a VentPill
// isLastChild bool restore right border-radius when no sibling follows
// ═══════════════════════════════════════════════════════════════════
// value - current value string
// options - array of { value, label, disabled }
// onChange - fn(newValue)
// hideLabel - label shown when value === 'off' (e.g. 'Burn')
// hidingLabel - label shown for the 'off' option when active (e.g. 'Hide Burn')
// isOn - bool - whether the pill shows as active (brass)
// groupedLeft - bool - fuse right side with a VentPill
// isLastChild - bool - restore right border-radius when no sibling follows
// -------------------------------------------------------------------
export function CustomSelect({ value, options, onChange, hideLabel, hidingLabel, buttonLabel, isOn, noHide, groupedLeft, isLastChild }) {
const [open, setOpen] = useState(false);
const wrapRef = useRef(null);
@@ -111,15 +111,15 @@ export function CustomSelect({ value, options, onChange, hideLabel, hidingLabel,
</span>`;
}
// ═══════════════════════════════════════════════════════════════════
// VENTPILL the checkbox pill fused to the right of a CustomSelect.
// ───────────────────────────────────────────────────────────────────
// -------------------------------------------------------------------
// VENTPILL - the checkbox pill fused to the right of a CustomSelect.
// -------------------------------------------------------------------
// props:
// checked bool
// onChange fn()
// label string
// title tooltip string
// ═══════════════════════════════════════════════════════════════════
// checked - bool
// onChange - fn()
// label - string
// title - tooltip string
// -------------------------------------------------------------------
export function VentPill({ checked, onChange, label, title }) {
return html`
<label class=${`cs-vent${checked ? ' on' : ''}`} title=${title}>
@@ -134,23 +134,23 @@ export function VentPill({ checked, onChange, label, title }) {
</label>`;
}
// ═══════════════════════════════════════════════════════════════════
// SKYSCOPE the little porthole circle next to each hour.
// ───────────────────────────────────────────────────────────────────
// -------------------------------------------------------------------
// SKYSCOPE - the little porthole circle next to each hour.
// -------------------------------------------------------------------
// What gets drawn (back to front):
// 1. Sky disk colour from skyFillForElev() above
// 2. Horizon line faint dashed line at the middle
// 3. Sun OR moon sun positioned vertically by elevation;
// 1. Sky disk - colour from skyFillForElev() above
// 2. Horizon line - faint dashed line at the middle
// 3. Sun OR moon - sun positioned vertically by elevation;
// moon shows real phase (synodic period)
// 4. Brass ring the telescope/scope edge (#c8922a)
// 5. Lens highlight subtle inner glint
// 4. Brass ring - the telescope/scope edge (#c8922a)
// 5. Lens highlight - subtle inner glint
//
// Tweakable bits inside this function:
// size pass a different size= when calling for bigger/smaller
// innerR how thick the brass ring looks
// sunR the sun's drawn radius
// the stroke colour #c8922a is the brass change for a different metal
// ═══════════════════════════════════════════════════════════════════
// - size - pass a different size= when calling for bigger/smaller
// - innerR - how thick the brass ring looks
// - sunR - the sun's drawn radius
// - the stroke colour #c8922a is the brass - change for a different metal
// -------------------------------------------------------------------
export function SkyScope({ elev, dt, glob = 0, size = 26 }) {
const r = size / 2;
const innerR = r - 1.2;
@@ -165,14 +165,14 @@ export function SkyScope({ elev, dt, glob = 0, size = 26 }) {
const isTwil = elev > -8 && !isDay; // moon shown in twilight too
const isNight = elev <= -8;
// Sun position clamped so it never goes below the horizon line (y = r)
// Sun position - clamped so it never goes below the horizon line (y = r)
const elevClamped = Math.max(0, Math.min(90, elev));
const sunY = r - Math.sin((elevClamped * Math.PI) / 180) * (innerR - 2.5);
const sunR = Math.max(2.2, innerR * 0.32);
// Radial glow: strength driven by glob (W/m²). Uses a radialGradient
// Radial glow: strength driven by glob (W/m-). Uses a radialGradient
// so it's intense at the sun centre and fades smoothly to transparent.
const radFrac = Math.min(1, (glob || 0) / 900); // 0 (dark) 1 (blazing)
const radFrac = Math.min(1, (glob || 0) / 900); // 0 (dark) - 1 (blazing)
const horizFrac = Math.max(0, 1 - elev / 20); // extra warmth near horizon
const glowR = sunR + 4 + radFrac * 6; // total glow radius
const glowPeak = 0.35 + radFrac * 0.45 + horizFrac * 0.15; // centre opacity
@@ -253,20 +253,20 @@ export function SkyScope({ elev, dt, glob = 0, size = 26 }) {
</svg>`;
}
// ═══════════════════════════════════════════════════════════════════
// WINDVANE clean compass arrow on transparent background.
// ───────────────────────────────────────────────────────────────────
// -------------------------------------------------------------------
// WINDVANE - clean compass arrow on transparent background.
// -------------------------------------------------------------------
// Traditional weather-vane behaviour: the ARROWHEAD points INTO the
// wind (toward the source). Wind from the north head points north.
// wind (toward the source). Wind from the north - head points north.
//
// props:
// bearing degrees, 0 = wind FROM north
// size pixel diameter (default 30 to match SkyScope)
// bearing - degrees, 0 = wind FROM north
// size - pixel diameter (default 30 to match SkyScope)
//
// Tweakable bits:
// arrowColor / nMarkerColor line/fill colours
// Tiny N letter sits just above the arrow tail for orientation
// ═══════════════════════════════════════════════════════════════════
// - arrowColor / nMarkerColor - line/fill colours
// - Tiny N letter sits just above the arrow tail for orientation
// -------------------------------------------------------------------
export function WindVane({ bearing, size = 30 }) {
if (bearing == null || isNaN(bearing)) {
return html`<span style=${{ opacity: 0.5, fontSize: '11px' }}>—</span>`;
@@ -280,7 +280,7 @@ export function WindVane({ bearing, size = 30 }) {
const nColor = '#9a7d5a';
// Geometry of the single-ended arrow (drawn pointing DOWN by default).
// After rotation by (bearing + 180), the head lands on the bearing
// direction i.e. the side the wind is coming FROM.
// direction - i.e. the side the wind is coming FROM.
const tipY = r * 0.15; // arrowhead tip
const headBase = r * 0.58; // bottom of the triangle head
const headW = r * 0.42;
@@ -311,14 +311,14 @@ export function WindVane({ bearing, size = 30 }) {
</svg>`;
}
// ═══════════════════════════════════════════════════════════════════
// CLOUDICON Brass Line cloud / haze / overcast glyphs.
// -------------------------------------------------------------------
// CLOUDICON - Brass Line cloud / haze / overcast glyphs.
// No sun or moon here; the time-cell SkyScope owns day/night imagery.
// ───────────────────────────────────────────────────────────────────
// -------------------------------------------------------------------
// category: 'clear' | 'wispy' | 'scattered' | 'overcast'
// elev: solar elevation in degrees (negative = night)
// dt: Date used for moon phase
// ═══════════════════════════════════════════════════════════════════
// -------------------------------------------------------------------
export function CloudIcon({ category, size = 30, elev = 90, dt = new Date() }) {
const r = size / 2;
const brass = '#c8922a';
@@ -345,7 +345,7 @@ export function CloudIcon({ category, size = 30, elev = 90, dt = new Date() }) {
${line(size * 0.30, y0 + size * 0.18, size * 0.92, y0 + size * 0.18, ink, sw * 0.82, 0.72)}
</g>`;
// Sun shape circle + 8 rays, centred at (cx, cy)
// Sun shape - circle + 8 rays, centred at (cx, cy)
const sun = (cx, cy, cr) => {
const rays = Array.from({ length: 8 }, (_, k) => {
const angle = (k * Math.PI) / 4;
@@ -382,23 +382,23 @@ export function CloudIcon({ category, size = 30, elev = 90, dt = new Date() }) {
</svg>`;
}
// ═══════════════════════════════════════════════════════════════════
// SCOPERETICLE the big circular UTCI dial in the page header.
// ───────────────────────────────────────────────────────────────────
// -------------------------------------------------------------------
// SCOPERETICLE - the big circular UTCI dial in the page header.
// -------------------------------------------------------------------
// This is the one with tick marks, the swept colour band, and the
// needle pointing at the current felt-temperature.
//
// Tweakable bits:
// R = 86 outer ring radius (changes overall size)
// cx, cy = 100 centre point (leave alone unless you also
// - R = 86 outer ring radius (changes overall size)
// - cx, cy = 100 centre point (leave alone unless you also
// change the viewBox="0 0 200 200" below)
// { length: 36 } number of tick marks (1 every 10°)
// stressBands[] the colour ramp around the rim (matches UTCI)
// (value + 10) / 60 maps UTCI -10..50 onto the 270° sweep
// - { length: 36 } number of tick marks (1 every 10-)
// - stressBands[] the colour ramp around the rim (matches UTCI)
// - (value + 10) / 60 maps UTCI -10..50 onto the 270- sweep -
// widen the dial range by changing those numbers
// ═══════════════════════════════════════════════════════════════════
// -------------------------------------------------------------------
export function ScopeReticle({ value, cat, loading, elev = 0, dt = new Date(), glob = 0, activeEvent = null }) {
// ── Day/night scene behind the readout ─────────────────────────────
// -- Day/night scene behind the readout -----------------------------
// The whole back of the lens is a SkyScope-style scene: sky on top,
// grass on the bottom, sun positioned by elevation (or moon at night
// with its phase shadow). Mirrors the little hourly SkyScopes.
@@ -415,7 +415,7 @@ export function ScopeReticle({ value, cat, loading, elev = 0, dt = new Date(), g
const elevClamped = Math.max(0, Math.min(90, elev)); // clamp to above horizon
const sunY = cy - Math.sin((elevClamped * Math.PI) / 180) * (lensR - 18);
const sunDrawR = 14;
// Radial glow for the big dial sun is always at least faintly glowing
// Radial glow for the big dial - sun is always at least faintly glowing
// when above the horizon; scales up with radiation strength.
const radFrac = Math.min(1, (glob || 0) / 900);
const horizFrac = Math.max(0, 1 - elev / 20);
@@ -628,22 +628,22 @@ export function ScopeReticle({ value, cat, loading, elev = 0, dt = new Date(), g
</svg>`;
}
// ═══════════════════════════════════════════════════════════════════
// PRECIPICON rain drop or snowflake SVG for the precip cell.
// ───────────────────────────────────────────────────────────────────
// precip mm/h rainfall
// snow cm/h snowfall
// size pixel size (default 28 to match CloudIcon)
// -------------------------------------------------------------------
// PRECIPICON - rain drop or snowflake SVG for the precip cell.
// -------------------------------------------------------------------
// precip - mm/h rainfall
// snow - cm/h snowfall
// size - pixel size (default 28 to match CloudIcon)
//
// Intensity bands:
// dry faint dash
// light rain (< 1 mm/h) 1 drop
// moderate rain(< 4 mm/h) 2 drops
// heavy rain ( 4 mm/h) 3 drops
// light snow (< 0.5cm/h) 1 flake
// heavy snow ( 0.5cm/h) 2 flakes
// mixed 1 drop + 1 flake
// ═══════════════════════════════════════════════════════════════════
// dry - faint dash
// light rain (< 1 mm/h) - 1 drop
// moderate rain(< 4 mm/h) - 2 drops
// heavy rain (- 4 mm/h) - 3 drops
// light snow (< 0.5cm/h) - 1 flake
// heavy snow (- 0.5cm/h) - 2 flakes
// mixed - 1 drop + 1 flake
// -------------------------------------------------------------------
export function PrecipIcon({ precip = 0, snow = 0, size = 28 }) {
const r = size / 2;
const hasRain = precip > 0;
@@ -673,7 +673,7 @@ export function PrecipIcon({ precip = 0, snow = 0, size = 28 }) {
<line x1=${cx + fr * 0.68} y1=${cy - fr * 0.68} x2=${cx - fr * 0.68} y2=${cy + fr * 0.68} />
</g>`;
// Dry just a faint dash
// Dry - just a faint dash
if (!hasRain && !hasSnow) {
return html`
<svg width=${size} height=${size} viewBox=${`0 0 ${size} ${size}`}
@@ -683,9 +683,9 @@ export function PrecipIcon({ precip = 0, snow = 0, size = 28 }) {
</svg>`;
}
// Rain intensity number of drops
// Rain intensity - number of drops
const rainDrops = !hasRain ? 0 : precip < 1 ? 1 : precip < 4 ? 2 : 3;
// Snow intensity number of flakes
// Snow intensity - number of flakes
const snowFlakes = !hasSnow ? 0 : snow < 0.5 ? 1 : 2;
const fr = size * 0.08; // flake arm length