Optically center weather font glyphs
This commit is contained in:
@@ -49,7 +49,7 @@ export function drawOnMap(
|
||||
ctx.fillStyle = "#FFFFFF";
|
||||
ctx.font = `normal ${props.fontSize * 5}px Daira by LTV grafika`;
|
||||
const iconSize = ctx.measureText(icon);
|
||||
ctx.fillText(icon, localX + props.boxSize/2 + 10 + iconSize.width/2, localY);
|
||||
drawOpticallyCenteredGlyph(ctx, icon, localX + props.boxSize/2 + 10 + iconSize.width/2, localY);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -81,6 +81,15 @@ export function drawOnMap(
|
||||
ctx.fillText("M/S", windProp.offsetX + boxMiddleX + gustsWidth / 2 - 22, (windProp.offsetY + 805)*windProp.scaleY);
|
||||
}
|
||||
|
||||
function drawOpticallyCenteredGlyph(ctx: CanvasRenderingContext2D, glyph: string, centerX: number, centerY: number): void {
|
||||
const metrics = ctx.measureText(glyph);
|
||||
ctx.textAlign = "left";
|
||||
ctx.textBaseline = "alphabetic";
|
||||
const x = centerX + (metrics.actualBoundingBoxLeft - metrics.actualBoundingBoxRight) / 2;
|
||||
const y = centerY + (metrics.actualBoundingBoxAscent - metrics.actualBoundingBoxDescent) / 2;
|
||||
ctx.fillText(glyph, x, y);
|
||||
}
|
||||
|
||||
function drawTitleOverlay(ctx: CanvasRenderingContext2D, title: string, source: string, canvasWidth: number): void {
|
||||
if (!title.trim() && !source.trim()) return;
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Component, createSignal, For, Show, Signal } from "solid-js";
|
||||
import { weatherIcons } from "./iconConsts";
|
||||
import "../../css/weatherIcons.css";
|
||||
import { WeatherGlyph } from "./WeatherGlyph";
|
||||
|
||||
export const IconInputs: Component<{cities: string[]; weatherIconsSignal: Signal<Record<string, string>>}> = (props) => {
|
||||
const [icons, setIcons] = props.weatherIconsSignal;
|
||||
@@ -10,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">{selected()}</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)}>{icon}</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"><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="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="—">{icons()[city]}</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 => <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>
|
||||
</section>;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
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"/>;
|
||||
};
|
||||
@@ -2,7 +2,7 @@
|
||||
font-family: 'Daira by LTV grafika';
|
||||
font-size: 30px;
|
||||
display:inline-flex;align-items:center;justify-content:center;
|
||||
width:46px;height:46px;min-height:46px;
|
||||
width:50px;height:50px;min-height:50px;
|
||||
background:#f4f6f8;border:1px solid #d9e0e8;border-radius:10px;
|
||||
padding:0;margin:0;
|
||||
cursor: pointer;
|
||||
@@ -24,3 +24,5 @@
|
||||
}
|
||||
.weatherIconButton.selected{background:#17212d;border-color:#17212d;color:#fff;box-shadow:0 0 0 3px var(--accent-pale)}
|
||||
.weatherIconButton:focus-visible{outline:3px solid var(--accent-soft);outline-offset:2px}
|
||||
.weatherGlyph{display:block;flex:none;color:inherit;pointer-events:none}
|
||||
.symbolPalette{grid-template-columns:repeat(auto-fill,50px)!important}.selectedPreview{width:46px!important;height:46px!important}.selectedPreview,.currentSymbol{font-family:inherit!important}
|
||||
|
||||
Reference in New Issue
Block a user