Add warning map border overlays and feathered fills

This commit is contained in:
b0txec
2026-08-22 20:31:04 +03:00
parent bc5bee171c
commit 13115f014d
5 changed files with 53 additions and 16 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

+20 -11
View File
@@ -1,4 +1,4 @@
import { createEffect, createMemo, createResource, createSignal, For, onMount, Show } from 'solid-js'; import { createEffect, createMemo, createResource, createSignal, For, Show } from 'solid-js';
import { apiHost } from '../../consts'; import { apiHost } from '../../consts';
import { download } from '../../helpers/download'; import { download } from '../../helpers/download';
import { LoadingSpinner } from '../../components/spinner/LoadingSpinner'; import { LoadingSpinner } from '../../components/spinner/LoadingSpinner';
@@ -19,7 +19,7 @@ export function Warnings() {
const [title, setTitle] = createSignal(''); const [title, setTitle] = createSignal('');
const [selectedDate, setSelectedDate] = createSignal('all'); const [selectedDate, setSelectedDate] = createSignal('all');
const [phenomenon, setPhenomenon] = createSignal('all'); const [phenomenon, setPhenomenon] = createSignal('all');
const [background, setBackground] = createSignal<HTMLImageElement>(); const [artwork, setArtwork] = createSignal<{ background: HTMLImageElement; border: HTMLImageElement }>();
const [data, { refetch }] = createResource(fetchWarnings); const [data, { refetch }] = createResource(fetchWarnings);
let canvas!: HTMLCanvasElement; let canvas!: HTMLCanvasElement;
@@ -33,24 +33,33 @@ export function Warnings() {
return dateMatches && phenomenonMatches; return dateMatches && phenomenonMatches;
})); }));
const loadBackground = () => { const loadArtwork = () => {
const image = new Image(); const current = template();
image.onload = () => setBackground(image); setArtwork(undefined);
image.src = template().map; const background = new Image();
const border = new Image();
let loaded = 0;
const finish = () => {
loaded += 1;
if (loaded === 2 && template() === current) setArtwork({ background, border });
};
background.onload = finish;
border.onload = finish;
background.src = current.map;
border.src = current.border;
}; };
onMount(loadBackground); createEffect(() => { output(); loadArtwork(); });
createEffect(() => { output(); loadBackground(); });
createEffect(() => { createEffect(() => {
const image = background(); const images = artwork();
const current = template(); const current = template();
const warnings = filteredWarnings(); const warnings = filteredWarnings();
const currentTitle = title(); const currentTitle = title();
if (!canvas || !image) return; if (!canvas || !images) return;
if (canvas.width !== current.width) canvas.width = current.width; if (canvas.width !== current.width) canvas.width = current.width;
if (canvas.height !== current.height) canvas.height = current.height; if (canvas.height !== current.height) canvas.height = current.height;
void document.fonts.load(`700 ${current.title.fontSize}px Monda`).then(() => void document.fonts.load(`700 ${current.title.fontSize}px Monda`).then(() =>
drawWarningMap(canvas.getContext('2d')!, image, current, warnings, currentTitle), drawWarningMap(canvas.getContext('2d')!, images.background, images.border, current, warnings, currentTitle),
); );
}); });
+25 -5
View File
@@ -33,6 +33,7 @@ export function warningSeverity(value: string): Severity {
export function drawWarningMap( export function drawWarningMap(
ctx: CanvasRenderingContext2D, ctx: CanvasRenderingContext2D,
background: HTMLImageElement, background: HTMLImageElement,
border: HTMLImageElement,
template: WarningTemplate, template: WarningTemplate,
warnings: WeatherWarning[], warnings: WeatherWarning[],
title: string, title: string,
@@ -40,9 +41,23 @@ export function drawWarningMap(
ctx.clearRect(0, 0, template.width, template.height); ctx.clearRect(0, 0, template.width, template.height);
ctx.drawImage(background, 0, 0, template.width, template.height); ctx.drawImage(background, 0, 0, template.width, template.height);
const warningLayer = document.createElement('canvas');
warningLayer.width = template.width;
warningLayer.height = template.height;
const warningContext = warningLayer.getContext('2d')!;
[...warnings] [...warnings]
.sort((a, b) => severityRank(warningSeverity(a.intensity)) - severityRank(warningSeverity(b.intensity))) .sort((a, b) => severityRank(warningSeverity(a.intensity)) - severityRank(warningSeverity(b.intensity)))
.forEach(warning => drawWarning(ctx, template, warning)); .forEach(warning => drawWarning(warningContext, template, warning));
ctx.save();
ctx.filter = `blur(${template.feather}px)`;
ctx.drawImage(warningLayer, 0, 0);
ctx.restore();
// The newsroom-authored outline is deliberately last so it hides small
// projection differences and remains crisp above the softened data layer.
ctx.drawImage(border, 0, 0, template.width, template.height);
drawTitle(ctx, template, title); drawTitle(ctx, template, title);
drawLegend(ctx, template, warnings); drawLegend(ctx, template, warnings);
@@ -62,10 +77,6 @@ function drawWarning(ctx: CanvasRenderingContext2D, template: WarningTemplate, w
ctx.closePath(); ctx.closePath();
ctx.fillStyle = style.fill; ctx.fillStyle = style.fill;
ctx.fill(); ctx.fill();
ctx.strokeStyle = style.stroke;
ctx.lineWidth = template.width === 1920 ? 3 : 5;
ctx.lineJoin = 'round';
ctx.stroke();
}); });
} }
@@ -96,6 +107,15 @@ function drawTitle(ctx: CanvasRenderingContext2D, template: WarningTemplate, val
ctx.textAlign = 'left'; ctx.textAlign = 'left';
ctx.textBaseline = 'middle'; ctx.textBaseline = 'middle';
ctx.fillText(text, x + layout.horizontalPadding, layout.top + layout.height / 2); ctx.fillText(text, x + layout.horizontalPadding, layout.top + layout.height / 2);
const sourceHeight = layout.height * .52;
const sourceWidth = Math.max(layout.height * 2.45, width * .38);
ctx.fillStyle = 'rgba(245, 245, 245, .94)';
ctx.fillRect(x + width - sourceWidth, layout.top + layout.height, sourceWidth, sourceHeight);
ctx.fillStyle = '#111';
ctx.font = `700 ${fontSize * .55}px Monda`;
ctx.textAlign = 'center';
ctx.fillText('LVĢMC', x + width - sourceWidth / 2, layout.top + layout.height + sourceHeight / 2);
} }
function drawLegend(ctx: CanvasRenderingContext2D, template: WarningTemplate, warnings: WeatherWarning[]) { function drawLegend(ctx: CanvasRenderingContext2D, template: WarningTemplate, warnings: WeatherWarning[]) {
@@ -1,5 +1,7 @@
import map1920 from '../../assets/map_1920x1080.webp'; import map1920 from '../../assets/map_1920x1080.webp';
import map3840 from '../../assets/map_3840x1440.webp'; import map3840 from '../../assets/map_3840x1440.webp';
import border1920 from '../../assets/production/BRIDINAJUMI_BORDER_1920.png';
import border3840 from '../../assets/production/BRIDINAJUMI_BORDER_3840.png';
export type WarningOutput = '1920x1080' | '3840x1440'; export type WarningOutput = '1920x1080' | '3840x1440';
@@ -7,6 +9,8 @@ export type WarningTemplate = {
width: number; width: number;
height: number; height: number;
map: string; map: string;
border: string;
feather: number;
geo: { west: number; east: number; north: number; south: number; left: number; right: number; top: number; bottom: number }; geo: { west: number; east: number; north: number; south: number; left: number; right: number; top: number; bottom: number };
title: { right: number; top: number; height: number; fontSize: number; horizontalPadding: number; maximumWidth: number }; title: { right: number; top: number; height: number; fontSize: number; horizontalPadding: number; maximumWidth: number };
legend: { right: number; bottom: number; swatch: number; gap: number; fontSize: number }; legend: { right: number; bottom: number; swatch: number; gap: number; fontSize: number };
@@ -19,6 +23,8 @@ export const warningTemplates: Record<WarningOutput, WarningTemplate> = {
width: 1920, width: 1920,
height: 1080, height: 1080,
map: map1920, map: map1920,
border: border1920,
feather: 16,
geo: { west: 20.97, east: 28.24, north: 58.09, south: 55.67, left: 360, right: 1655, top: 115, bottom: 864 }, geo: { west: 20.97, east: 28.24, north: 58.09, south: 55.67, left: 360, right: 1655, top: 115, bottom: 864 },
title: { right: 88, top: 38, height: 62, fontSize: 30, horizontalPadding: 24, maximumWidth: 900 }, title: { right: 88, top: 38, height: 62, fontSize: 30, horizontalPadding: 24, maximumWidth: 900 },
legend: { right: 88, bottom: 58, swatch: 27, gap: 14, fontSize: 20 }, legend: { right: 88, bottom: 58, swatch: 27, gap: 14, fontSize: 20 },
@@ -27,6 +33,8 @@ export const warningTemplates: Record<WarningOutput, WarningTemplate> = {
width: 3840, width: 3840,
height: 1440, height: 1440,
map: map3840, map: map3840,
border: border3840,
feather: 28,
geo: { west: 20.97, east: 28.24, north: 58.09, south: 55.67, left: 985, right: 3150, top: 80, bottom: 1400 }, geo: { west: 20.97, east: 28.24, north: 58.09, south: 55.67, left: 985, right: 3150, top: 80, bottom: 1400 },
title: { right: 376, top: 76, height: 124, fontSize: 60, horizontalPadding: 48, maximumWidth: 1700 }, title: { right: 376, top: 76, height: 124, fontSize: 60, horizontalPadding: 48, maximumWidth: 1700 },
legend: { right: 376, bottom: 76, swatch: 42, gap: 22, fontSize: 30 }, legend: { right: 376, bottom: 76, swatch: 42, gap: 22, fontSize: 30 },