diff --git a/web/src/assets/production/BRIDINAJUMI_BORDER_1920.png b/web/src/assets/production/BRIDINAJUMI_BORDER_1920.png new file mode 100644 index 0000000..89bb01c Binary files /dev/null and b/web/src/assets/production/BRIDINAJUMI_BORDER_1920.png differ diff --git a/web/src/assets/production/BRIDINAJUMI_BORDER_3840.png b/web/src/assets/production/BRIDINAJUMI_BORDER_3840.png new file mode 100644 index 0000000..6ab685c Binary files /dev/null and b/web/src/assets/production/BRIDINAJUMI_BORDER_3840.png differ diff --git a/web/src/pages/warnings/Warnings.tsx b/web/src/pages/warnings/Warnings.tsx index 0a1a0ed..24bab48 100644 --- a/web/src/pages/warnings/Warnings.tsx +++ b/web/src/pages/warnings/Warnings.tsx @@ -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(); + 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), ); }); diff --git a/web/src/pages/warnings/warningDraw.ts b/web/src/pages/warnings/warningDraw.ts index 99244fd..4e753e4 100644 --- a/web/src/pages/warnings/warningDraw.ts +++ b/web/src/pages/warnings/warningDraw.ts @@ -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[]) { diff --git a/web/src/pages/warnings/warningTemplates.ts b/web/src/pages/warnings/warningTemplates.ts index 313a4bb..d41f241 100644 --- a/web/src/pages/warnings/warningTemplates.ts +++ b/web/src/pages/warnings/warningTemplates.ts @@ -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 = { 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 = { 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 },