Add warning map border overlays and feathered fills
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 50 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 96 KiB |
@@ -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 { download } from '../../helpers/download';
|
||||
import { LoadingSpinner } from '../../components/spinner/LoadingSpinner';
|
||||
@@ -19,7 +19,7 @@ export function Warnings() {
|
||||
const [title, setTitle] = createSignal('');
|
||||
const [selectedDate, setSelectedDate] = 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);
|
||||
let canvas!: HTMLCanvasElement;
|
||||
|
||||
@@ -33,24 +33,33 @@ export function Warnings() {
|
||||
return dateMatches && phenomenonMatches;
|
||||
}));
|
||||
|
||||
const loadBackground = () => {
|
||||
const image = new Image();
|
||||
image.onload = () => setBackground(image);
|
||||
image.src = template().map;
|
||||
const loadArtwork = () => {
|
||||
const current = template();
|
||||
setArtwork(undefined);
|
||||
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(); loadBackground(); });
|
||||
createEffect(() => { output(); loadArtwork(); });
|
||||
createEffect(() => {
|
||||
const image = background();
|
||||
const images = artwork();
|
||||
const current = template();
|
||||
const warnings = filteredWarnings();
|
||||
const currentTitle = title();
|
||||
if (!canvas || !image) return;
|
||||
if (!canvas || !images) return;
|
||||
if (canvas.width !== current.width) canvas.width = current.width;
|
||||
if (canvas.height !== current.height) canvas.height = current.height;
|
||||
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),
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ export function warningSeverity(value: string): Severity {
|
||||
export function drawWarningMap(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
background: HTMLImageElement,
|
||||
border: HTMLImageElement,
|
||||
template: WarningTemplate,
|
||||
warnings: WeatherWarning[],
|
||||
title: string,
|
||||
@@ -40,9 +41,23 @@ export function drawWarningMap(
|
||||
ctx.clearRect(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]
|
||||
.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);
|
||||
drawLegend(ctx, template, warnings);
|
||||
@@ -62,10 +77,6 @@ function drawWarning(ctx: CanvasRenderingContext2D, template: WarningTemplate, w
|
||||
ctx.closePath();
|
||||
ctx.fillStyle = style.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.textBaseline = 'middle';
|
||||
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[]) {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import map1920 from '../../assets/map_1920x1080.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';
|
||||
|
||||
@@ -7,6 +9,8 @@ export type WarningTemplate = {
|
||||
width: number;
|
||||
height: number;
|
||||
map: string;
|
||||
border: string;
|
||||
feather: 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 };
|
||||
legend: { right: number; bottom: number; swatch: number; gap: number; fontSize: number };
|
||||
@@ -19,6 +23,8 @@ export const warningTemplates: Record<WarningOutput, WarningTemplate> = {
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
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 },
|
||||
title: { right: 88, top: 38, height: 62, fontSize: 30, horizontalPadding: 24, maximumWidth: 900 },
|
||||
legend: { right: 88, bottom: 58, swatch: 27, gap: 14, fontSize: 20 },
|
||||
@@ -27,6 +33,8 @@ export const warningTemplates: Record<WarningOutput, WarningTemplate> = {
|
||||
width: 3840,
|
||||
height: 1440,
|
||||
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 },
|
||||
title: { right: 376, top: 76, height: 124, fontSize: 60, horizontalPadding: 48, maximumWidth: 1700 },
|
||||
legend: { right: 376, bottom: 76, swatch: 42, gap: 22, fontSize: 30 },
|
||||
|
||||
Reference in New Issue
Block a user