Optically center weather font glyphs
This commit is contained in:
@@ -49,7 +49,7 @@ export function drawOnMap(
|
|||||||
ctx.fillStyle = "#FFFFFF";
|
ctx.fillStyle = "#FFFFFF";
|
||||||
ctx.font = `normal ${props.fontSize * 5}px Daira by LTV grafika`;
|
ctx.font = `normal ${props.fontSize * 5}px Daira by LTV grafika`;
|
||||||
const iconSize = ctx.measureText(icon);
|
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);
|
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 {
|
function drawTitleOverlay(ctx: CanvasRenderingContext2D, title: string, source: string, canvasWidth: number): void {
|
||||||
if (!title.trim() && !source.trim()) return;
|
if (!title.trim() && !source.trim()) return;
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Component, createSignal, For, Show, Signal } from "solid-js";
|
import { Component, createSignal, For, Show, Signal } from "solid-js";
|
||||||
import { weatherIcons } from "./iconConsts";
|
import { weatherIcons } from "./iconConsts";
|
||||||
import "../../css/weatherIcons.css";
|
import "../../css/weatherIcons.css";
|
||||||
|
import { WeatherGlyph } from "./WeatherGlyph";
|
||||||
|
|
||||||
export const IconInputs: Component<{cities: string[]; weatherIconsSignal: Signal<Record<string, string>>}> = (props) => {
|
export const IconInputs: Component<{cities: string[]; weatherIconsSignal: Signal<Record<string, string>>}> = (props) => {
|
||||||
const [icons, setIcons] = props.weatherIconsSignal;
|
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()])));
|
const applyAll = () => setIcons(Object.fromEntries(props.cities.map(city => [city, selected()])));
|
||||||
|
|
||||||
return <section class="symbolEditor">
|
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="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)}>{icon}</button>}</For></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="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>;
|
</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-family: 'Daira by LTV grafika';
|
||||||
font-size: 30px;
|
font-size: 30px;
|
||||||
display:inline-flex;align-items:center;justify-content:center;
|
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;
|
background:#f4f6f8;border:1px solid #d9e0e8;border-radius:10px;
|
||||||
padding:0;margin:0;
|
padding:0;margin:0;
|
||||||
cursor: pointer;
|
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.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}
|
.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