Draw news in GEV's own colours

The news layer brought its own ColorBrewer yellow, orange and red, solid
coloured marker discs, a coloured edge on every card, an amber developing
ring and pink disputed areas, which read as foreign next to GEV's cyan glass.

Severity now uses colours GEV already has (dim and primary text, the military
layers' amber, the Cockpit's emergency red), drawn as GEV's dark disc with a
severity rim and icon; attestation is still the rim's form. Cards lose the
coloured edge and name significant and major events in a mono badge;
attestation chips follow the status badge; outlines stay in the accent family
with disputed areas dotted in the primary text colour.
This commit is contained in:
Denis Groshev committed 2026-11-06 00:45:25 +01:00
1 parent e95ae7b80c
commit f3e206bf37
8 files changed
+68 -55

No files matched your search

+4 -7
View File
@@ -149,14 +149,11 @@ function paint(canvas, view, look, pic) {
}
}
if (pic?.state !== 'ok') {
// No picture: a tile in the severity colour with the category icon.
const g = ctx.createLinearGradient(px, PAD, px + pw, PAD + PIC_H);
g.addColorStop(0, WORLD_OVERLAY_STYLE.background);
g.addColorStop(1, color);
ctx.fillStyle = g;
ctx.globalAlpha = 0.55;
// No picture: a plain glass tile with the event's marker.
ctx.fillStyle = WORLD_OVERLAY_STYLE.background;
ctx.fillRect(px, PAD, pw, PIC_H);
ctx.fillStyle = 'rgba(255, 255, 255, 0.04)';
ctx.fillRect(px, PAD, pw, PIC_H);
ctx.globalAlpha = 1;
paintMarker(ctx, look, px + pw / 2, PAD + PIC_H / 2, 34);
}
ctx.restore();
+5 -3
View File
@@ -66,9 +66,11 @@ const FEED_UNAVAILABLE = msg('News feed unavailable');
const REPORT_PREFIX = `${NEWS_PICK_PREFIX}report:`;
const HOTSPOT_PREFIX = `${NEWS_PICK_PREFIX}hotspot:`;
const DEVELOPING_PREFIX = `${NEWS_PICK_PREFIX}developing:`;
const DEVELOPING_COLOR = '#ffb020';
// Disputed areas: dotted, in their own colour, apart from ordinary borders.
const DISPUTED_COLOR = '#f472b6';
// Outlines in GEV's own colours, apart from the severity ramp: hotspots in
// the accent, developing places in its lighter tint, disputed areas dotted
// in the primary text colour.
const DEVELOPING_COLOR = '#9defff';
const DISPUTED_COLOR = '#e8eaed';
const HOTSPOT_COLOR = '#00d4ff';
// A filtered feed page that shows almost nothing reads the next page by
// itself, a few times, so a strict severity floor still fills the list.
+9 -7
View File
@@ -161,13 +161,15 @@ export const NEWS_CATEGORIES = Object.freeze({
},
});
// Severity 0–3. Lightness falls as severity rises, so the ramp still reads
// under colour blindness and in greyscale.
// Severity 0–3, in colours GEV already uses: its dim and primary text, the
// military layers' amber and the Cockpit's emergency red. They are drawn on
// a dark disc, so from minor up lightness falls as severity rises and the
// ramp still reads under colour blindness and in greyscale.
export const NEWS_SEVERITY = Object.freeze([
{ label: msg('No physical event'), color: '#8a9aa8' },
{ label: msg('Minor incident'), color: '#f2d64b' },
{ label: msg('Significant'), color: '#f28c28' },
{ label: msg('Major'), color: '#d7301f' },
{ label: msg('No physical event'), color: '#7c878d' },
{ label: msg('Minor incident'), color: '#e8eaed' },
{ label: msg('Significant'), color: '#ffb800' },
{ label: msg('Major'), color: '#ff4444' },
]);
/** The feed's default floor: significant and major events. */
export const NEWS_DEFAULT_MIN_SEVERITY = 2;
@@ -309,7 +311,7 @@ export function ageWords(at, now = Date.now()) {
}
/**
* How a report's marker looks: fill = severity, icon = category, rim =
* How a report's marker looks: colour = severity, icon = category, rim =
* attestation (`confirmed` solid, `claimed` dashed, else thin), size =
* sources, opacity = age (fading to 45 % over a day).
*/
+1 -1
View File
@@ -453,7 +453,7 @@ test('feed cards read like Breaking: headline, meta parts, sides and chips', ()
assert.equal(view.original, 'Удар по Харкову');
assert.equal(view.placedByModel, true);
assert.equal(view.severityLabel, 'Major');
assert.equal(view.color, '#d7301f');
assert.equal(view.color, '#ff4444');
assert.equal(view.category, 'Strike');
assert.equal(view.place, 'Kharkiv');
assert.equal(view.sources, '6 sources');
+17 -23
View File
@@ -1,5 +1,5 @@
// Marker sprites for news reports: a disc filled with the severity colour,
// the category's icon in it, and a rim for attestation. Drawn once per look
// Marker sprites for news reports: a dark disc with the category's icon and
// a rim in the severity colour, the rim's form telling attestation. Drawn once per look
// on a canvas and handed to Cesium as a data URL, so its texture atlas keeps
// one copy per look however often the layer re-renders.
@@ -7,8 +7,7 @@ import { NEWS_CATEGORIES, NEWS_SEVERITY } from './model.js';
const CANVAS = 64;
const BODY = 52; // disc diameter inside the canvas; the rest is rim and ring
const INK = '#0a0a0f';
const RIM = '#ffffff';
const INK = 'rgba(10, 10, 15, 0.92)';
const SELECTED = '#00d4ff';
/** Canvas pixels per marker pixel: the billboard is drawn at this ratio. */
@@ -28,30 +27,25 @@ export function paintMarker(ctx, { category, severity, rim }, cx, cy, body) {
ctx.arc(cx, cy, r + 1.5 * k, 0, 2 * Math.PI);
ctx.fillStyle = 'rgba(0, 0, 0, 0.85)';
ctx.fill();
// GEV's dark glass disc, as its HUD buttons are; severity colours the rim
// and the icon rather than filling the disc.
ctx.beginPath();
ctx.arc(cx, cy, r, 0, 2 * Math.PI);
ctx.fillStyle = (NEWS_SEVERITY[severity] || NEWS_SEVERITY[0]).color;
ctx.fillStyle = INK;
ctx.fill();
const color = (NEWS_SEVERITY[severity] || NEWS_SEVERITY[0]).color;
// Attestation is the rim's form, never its colour.
if (rim === 'confirmed' || rim === 'claimed') {
ctx.beginPath();
ctx.arc(cx, cy, r - 3.5 * k, 0, 2 * Math.PI);
ctx.setLineDash(rim === 'claimed' ? [7 * k, 5 * k] : []);
ctx.lineWidth = 7 * k;
ctx.strokeStyle = RIM;
ctx.stroke();
ctx.setLineDash([]);
} else {
ctx.beginPath();
ctx.arc(cx, cy, r - 1 * k, 0, 2 * Math.PI);
ctx.lineWidth = 2 * k;
ctx.strokeStyle = 'rgba(0, 0, 0, 0.75)';
ctx.stroke();
}
ctx.beginPath();
ctx.arc(cx, cy, r - 3 * k, 0, 2 * Math.PI);
ctx.setLineDash(rim === 'claimed' ? [7 * k, 5 * k] : []);
ctx.lineWidth = (rim === 'thin' ? 2.5 : 6) * k;
ctx.strokeStyle = color;
ctx.stroke();
ctx.setLineDash([]);
const style = NEWS_CATEGORIES[category] || NEWS_CATEGORIES.other;
const icon = body * 0.56;
const icon = body * 0.5;
ctx.save();
ctx.translate(cx - icon / 2, cy - icon / 2);
ctx.scale(icon / 16, icon / 16);
@@ -59,9 +53,9 @@ export function paintMarker(ctx, { category, severity, rim }, cx, cy, body) {
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.lineWidth = 1.7;
ctx.strokeStyle = INK;
ctx.strokeStyle = color;
if (style.solid) {
ctx.fillStyle = INK;
ctx.fillStyle = color;
ctx.fill(path);
} else ctx.stroke(path);
ctx.restore();
+4 -1
View File
@@ -78,7 +78,10 @@ const meta = () =>
:title="item.severityLabel"
><svg viewBox="0 0 16 16" aria-hidden="true">
<path :d="item.icon" /></svg></span
>{{ meta() }}</span
><span v-if="item.severity >= 2" class="news-card__severity">{{
item.severityLabel
}}</span
>{{ item.severity >= 2 ? ` · ${meta()}` : meta() }}</span
>
<span v-if="item.sides" class="news-card__sides">{{ item.sides }}</span>
<span v-if="item.chips.length" class="news-card__chips"
+1 -1
View File
@@ -136,7 +136,7 @@ test('the panel shows the last 24 h as event cards and opens once', () => {
);
assert.equal(
first.querySelector('.news-card__meta').textContent,
'Strike · Kharkiv · 4 sources · 10 min',
'Major · Strike · Kharkiv · 4 sources · 10 min',
);
assert.equal(
first.querySelector('.news-card__marker').getAttribute('aria-label'),
+27 -12
View File
@@ -172,16 +172,17 @@
.news-card {
border: 1px solid var(--glass-border);
border-inline-start: 3px solid var(--news-color, var(--glass-border));
border-radius: 6px;
background: rgba(255, 255, 255, 0.02);
}
.news-card:hover,
.news-card.is-open {
border-color: var(--glass-border-hover);
border-inline-start-color: var(--news-color);
background: rgba(0, 212, 255, 0.06);
}
.news-card.is-open {
border-color: rgba(0, 212, 255, 0.45);
}
.news-card.is-unmapped .news-card__title {
color: var(--text-secondary);
}
@@ -254,45 +255,59 @@
color: var(--text-secondary);
font: 10px var(--font-mono);
}
/* The globe's marker in small: a dark disc, rim and icon in the severity
colour (sprites.js). */
.news-card__marker {
flex: none;
display: inline-grid;
place-items: center;
box-sizing: border-box;
width: 14px;
height: 14px;
border: 1px solid rgba(0, 0, 0, 0.8);
border: 1.5px solid var(--news-color);
border-radius: 50%;
background: var(--news-color);
background: var(--bg-dark);
}
.news-card__marker svg {
width: 9px;
height: 9px;
width: 8px;
height: 8px;
fill: none;
stroke: var(--bg-dark);
stroke: var(--news-color);
stroke-width: 1.8;
stroke-linecap: round;
stroke-linejoin: round;
}
.news-card__marker.solid svg {
fill: var(--bg-dark);
fill: var(--news-color);
stroke: none;
}
.news-card__severity {
flex: none;
color: var(--news-color);
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.news-card__chips {
display: flex;
flex-wrap: wrap;
gap: 4px;
}
/* GEV's read-only badge (status-badge.css), neutral unless attested. */
.news-chip {
padding: 1px 6px;
padding: 2px 7px;
border: 1px solid var(--glass-border-hover);
border-radius: 999px;
color: var(--text-secondary);
font: 9px var(--font-mono);
font: 8px var(--font-mono);
letter-spacing: 0.08em;
text-transform: uppercase;
}
.news-chip--official,
.news-chip--confirmed {
border-color: rgba(0, 212, 255, 0.45);
color: var(--text-primary);
border-color: rgba(0, 212, 255, 0.32);
background: rgba(0, 212, 255, 0.09);
color: #9defff;
}
.news-chip--claimed {
border-style: dashed;