Replace Daira glyphs with normalized weather icons
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { Component, createSignal, For, Show, Signal } from "solid-js";
|
||||
import { weatherIcons } from "./iconConsts";
|
||||
import "../../css/weatherIcons.css";
|
||||
import { WeatherGlyph } from "./WeatherGlyph";
|
||||
import { WeatherIcon } from "./WeatherIcon";
|
||||
|
||||
export const IconInputs: Component<{cities: string[]; weatherIconsSignal: Signal<Record<string, string>>}> = (props) => {
|
||||
const [icons, setIcons] = props.weatherIconsSignal;
|
||||
@@ -11,9 +11,9 @@ export const IconInputs: Component<{cities: string[]; weatherIconsSignal: Signal
|
||||
const applyAll = () => setIcons(Object.fromEntries(props.cities.map(city => [city, selected()])));
|
||||
|
||||
return <section class="symbolEditor">
|
||||
<div class="symbolEditorHeading"><div><strong>Weather symbols</strong><p class="fieldHint">Choose one symbol, apply it broadly, then update the exceptions below.</p></div><div class="bulkSymbol"><span class="selectedPreview"><WeatherGlyph glyph={selected()} size={40} tone="inverse"/></span><button type="button" onClick={applyAll}>Apply to all cities</button></div></div>
|
||||
<div class="symbolPalette" role="radiogroup" aria-label="Choose weather symbol"><For each={weatherIcons}>{icon => <button type="button" role="radio" aria-checked={selected() === icon} aria-label={`Weather symbol ${icon}`} class="weatherIconButton" classList={{selected: selected() === icon}} onClick={() => setSelected(icon)}><WeatherGlyph glyph={icon} size={40} tone={selected() === icon ? "inverse" : "default"}/></button>}</For></div>
|
||||
<div class="symbolEditorHeading"><div><strong>Weather symbols</strong><p class="fieldHint">Choose one symbol, apply it broadly, then update the exceptions below.</p></div><div class="bulkSymbol"><span class="selectedPreview"><WeatherIcon code={selected()} size={40} tone="inverse"/></span><button type="button" onClick={applyAll}>Apply to all cities</button></div></div>
|
||||
<div class="symbolPalette" role="radiogroup" aria-label="Choose weather symbol"><For each={weatherIcons}>{icon => <button type="button" role="radio" aria-checked={selected() === icon} aria-label={`Weather symbol ${icon}`} class="weatherIconButton" classList={{selected: selected() === icon}} onClick={() => setSelected(icon)}><WeatherIcon code={icon} size={40} tone={selected() === icon ? "inverse" : "default"}/></button>}</For></div>
|
||||
<div class="assignmentHeading"><strong>City assignments</strong><span>Use the selected symbol for each exception.</span></div>
|
||||
<div class="citySymbols"><For each={props.cities}>{city => <div class="citySymbolRow"><span class="cityName">{city}</span><span class="currentSymbol" classList={{empty: !icons()[city]}}><Show when={icons()[city]} fallback="—">{icon => <WeatherGlyph glyph={icon()} size={40}/>}</Show></span><button type="button" class="assignAction" onClick={() => assign(city)}>Use selected</button><button type="button" class="clearSymbol" disabled={!icons()[city]} onClick={() => clear(city)} aria-label={`Clear ${city} symbol`}>×</button></div>}</For></div>
|
||||
<div class="citySymbols"><For each={props.cities}>{city => <div class="citySymbolRow"><span class="cityName">{city}</span><span class="currentSymbol" classList={{empty: !icons()[city]}}><Show when={icons()[city]} fallback="—">{icon => <WeatherIcon code={icon()} size={40}/>}</Show></span><button type="button" class="assignAction" onClick={() => assign(city)}>Use selected</button><button type="button" class="clearSymbol" disabled={!icons()[city]} onClick={() => clear(city)} aria-label={`Clear ${city} symbol`}>×</button></div>}</For></div>
|
||||
</section>;
|
||||
};
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import { Component, createEffect, onMount } from "solid-js";
|
||||
|
||||
export const WeatherGlyph: Component<{glyph: string; size?: number; tone?: "default" | "inverse"}> = (props) => {
|
||||
let canvas!: HTMLCanvasElement;
|
||||
const boxSize = () => props.size ?? 40;
|
||||
const draw = async () => {
|
||||
const size = boxSize();
|
||||
await document.fonts.load(`${Math.round(size * 0.82)}px "Daira by LTV grafika"`);
|
||||
canvas.width = size; canvas.height = size;
|
||||
const ctx = canvas.getContext("2d")!;
|
||||
ctx.clearRect(0, 0, size, size);
|
||||
ctx.font = `normal ${Math.round(size * 0.82)}px "Daira by LTV grafika"`;
|
||||
ctx.fillStyle = props.tone === "inverse" ? "#ffffff" : "#17212d";
|
||||
ctx.textAlign = "left"; ctx.textBaseline = "alphabetic";
|
||||
const metrics = ctx.measureText(props.glyph);
|
||||
const x = size / 2 + (metrics.actualBoundingBoxLeft - metrics.actualBoundingBoxRight) / 2;
|
||||
const y = size / 2 + (metrics.actualBoundingBoxAscent - metrics.actualBoundingBoxDescent) / 2;
|
||||
ctx.fillText(props.glyph, x, y);
|
||||
};
|
||||
onMount(draw);
|
||||
createEffect(() => { props.glyph; props.tone; boxSize(); if (canvas) void draw(); });
|
||||
return <canvas ref={canvas} class="weatherGlyph" style={{width: `${boxSize()}px`, height: `${boxSize()}px`}} aria-hidden="true"/>;
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Component } from "solid-js";
|
||||
import { getWeatherIconUrl } from "./weatherIconAssets";
|
||||
|
||||
export const WeatherIcon: Component<{code: string; size?: number; tone?: "default" | "inverse"}> = (props) => {
|
||||
const boxSize = () => props.size ?? 40;
|
||||
return <img
|
||||
src={getWeatherIconUrl(props.code)}
|
||||
class="weatherIconAsset"
|
||||
classList={{inverse: props.tone === "inverse"}}
|
||||
style={{width: `${boxSize()}px`, height: `${boxSize()}px`}}
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
/>;
|
||||
};
|
||||
@@ -1,8 +0,0 @@
|
||||
import { Component } from "solid-js";
|
||||
|
||||
/* Put this in page so browser actually downloads ltv font */
|
||||
export const WeatherIconStub: Component<{}> = () => {
|
||||
return (
|
||||
<div class="weatherIconStub">ABCDEF</div>
|
||||
)
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
/* font-family: 'Daira by LTV grafika'; */
|
||||
export const weatherIcons = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "a", "b", "c", "d", "e", "f", "g", "h", "i"]
|
||||
export const weatherIconCities = ['Ainaži', 'Alūksne', 'Bauska', 'Daugavpils', 'Liepāja', 'Madona', 'Priekuļi', 'Rēzekne', 'Rīga', 'Saldus', 'Stende', 'Ventspils', 'Zīlāni']
|
||||
|
||||
@@ -179,4 +178,4 @@ export const codeIconMap: { [key: number]: string } = {
|
||||
2617: 'd',
|
||||
2618: 'd',
|
||||
2619: 'f',
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { weatherIcons } from "./iconConsts";
|
||||
|
||||
const iconModules = import.meta.glob("../../assets/weather-icons/*.png", {
|
||||
eager: true,
|
||||
query: "?url",
|
||||
import: "default",
|
||||
}) as Record<string, string>;
|
||||
|
||||
const imageCache = new Map<string, HTMLImageElement>();
|
||||
|
||||
function assetFilename(code: string): string {
|
||||
return code === code.toLowerCase() ? `${code}${code}.png` : `${code}.png`;
|
||||
}
|
||||
|
||||
export function getWeatherIconUrl(code: string): string {
|
||||
const filename = assetFilename(code);
|
||||
const entry = Object.entries(iconModules).find(([path]) => path.endsWith(`/${filename}`));
|
||||
if (!entry) throw new Error(`Missing weather icon asset: ${filename}`);
|
||||
return entry[1];
|
||||
}
|
||||
|
||||
export function loadWeatherIcon(code: string): Promise<HTMLImageElement> {
|
||||
const cached = imageCache.get(code);
|
||||
if (cached?.complete && cached.naturalWidth > 0) return Promise.resolve(cached);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const image = cached ?? new Image();
|
||||
imageCache.set(code, image);
|
||||
image.onload = () => resolve(image);
|
||||
image.onerror = () => reject(new Error(`Failed to load weather icon: ${code}`));
|
||||
if (!image.src) image.src = getWeatherIconUrl(code);
|
||||
});
|
||||
}
|
||||
|
||||
export function preloadWeatherIcons(codes: readonly string[] = weatherIcons): Promise<HTMLImageElement[]> {
|
||||
return Promise.all(codes.map(loadWeatherIcon));
|
||||
}
|
||||
|
||||
export function getLoadedWeatherIcon(code: string): HTMLImageElement | undefined {
|
||||
const image = imageCache.get(code);
|
||||
return image?.complete && image.naturalWidth > 0 ? image : undefined;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user