mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-15 07:42:23 +00:00
feat(transit): give vehicle icons a readable front, and none without a course
At the ~20 px the fleet renders at, the bus, tram, metro and unknown glyphs were near-symmetric boxes: rotated along the course, nothing showed which end was the front. Each now has a front built into its outline (an arrow-like nose for the bus, a pointed cab for the tram, a chamfered cab for the metro), so it survives the tint, the halo and the sensor profiles, which keep only the outline. A vehicle with no known course sits at rotation 0, where a pointed glyph would claim it is heading north. Those vehicles, fleet and selected alike, now get a symmetric outline (rail and ferry included) and switch to the pointed one once a course arrives. The raster cache doubles to a fixed 72 variants. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
7d8b80db0a
commit
6172be3995
4 files changed
+225
-89
No files matched your search
+83
-24
@@ -41,33 +41,46 @@ const DETAIL =
|
||||
/**
|
||||
* Each body is drawn in a centred frame (origin 0,0 = glyph centre), front
|
||||
* toward -Y, in the 96-unit space (half-extent up to ~45).
|
||||
*
|
||||
* Two sets. `HEADING_BODIES` is what a vehicle with a known course wears: every
|
||||
* outline has a front that cannot be mistaken for its back at fleet size — a
|
||||
* nose, a bow or a chamfered cab — because at twenty pixels a bus drawn as a
|
||||
* near-symmetric box gives no hint which way it is going, and a hairline
|
||||
* windscreen is far below one pixel. The front is part of the OUTLINE, not
|
||||
* interior ink, so it survives the tint, the halo (which strokes the outline)
|
||||
* and the sensor profiles (which keep only the outline).
|
||||
*
|
||||
* `NEUTRAL_BODIES` is what a vehicle with NO known course wears. Its billboard
|
||||
* sits at rotation 0, so a pointed glyph would claim it is heading north; these
|
||||
* outlines are the same footprint, symmetric end to end, and claim nothing.
|
||||
*/
|
||||
const BODIES = {
|
||||
// ── Bus: short, blunt, square-shouldered. The shape everything else reads
|
||||
// against, and the one most people will see most often.
|
||||
const HEADING_BODIES = {
|
||||
// ── Bus: short, broad, square-shouldered behind an arrow-like nose. The
|
||||
// shape everything else reads against, and the one most people will see
|
||||
// most often, so it carries the boldest front.
|
||||
bus: `
|
||||
<path d="M-17,-28 C-17,-32 -14,-34 0,-34 C14,-34 17,-32 17,-28
|
||||
L17,28 C17,32 14,34 0,34 C-14,34 -17,32 -17,28 Z"
|
||||
<path d="M0,-41 L17,-25 L17,28 C17,32 14,34 0,34 C-14,34 -17,32 -17,28
|
||||
L-17,-25 Z"
|
||||
fill="white" ${STROKE}/>
|
||||
<path d="M-11,-24 L11,-24" ${DETAIL}/>
|
||||
<path d="M-11,-21 L11,-21" ${DETAIL}/>
|
||||
<path d="M-11,20 L11,20" ${DETAIL}/>`,
|
||||
|
||||
// ── Tram / light rail: longer and slimmer than a bus, with the two
|
||||
// articulation joints that make a modern low-floor tram unmistakable.
|
||||
// ── Tram / light rail: longer and slimmer than a bus, a short pointed cab
|
||||
// and the two articulation joints of a modern low-floor tram.
|
||||
tram: `
|
||||
<path d="M-10,-38 C-10,-41 -8,-42.5 0,-42.5 C8,-42.5 10,-41 10,-38
|
||||
L10,38 C10,41 8,42.5 0,42.5 C-8,42.5 -10,41 -10,38 Z"
|
||||
<path d="M0,-44 L10,-33 L10,38 C10,41 8,42.5 0,42.5 C-8,42.5 -10,41 -10,38
|
||||
L-10,-33 Z"
|
||||
fill="white" ${STROKE}/>
|
||||
<path d="M-10,-13 L10,-13" ${DETAIL}/>
|
||||
<path d="M-10,13 L10,13" ${DETAIL}/>
|
||||
<path d="M-8,-34 L8,-34" ${DETAIL}/>`,
|
||||
<path d="M-8,-29 L8,-29" ${DETAIL}/>`,
|
||||
|
||||
// ── Metro / subway: squared off at both ends, wider than a tram, a single
|
||||
// cab line at the front. Reads as rolling stock, not a road vehicle.
|
||||
// ── Metro / subway: squared off and wider than a tram, the cab end
|
||||
// chamfered to a narrow flat face; the rear stays square.
|
||||
subway: `
|
||||
<path d="M-11.5,-40 L11.5,-40 L11.5,40 L-11.5,40 Z"
|
||||
<path d="M-4,-40 L4,-40 L11.5,-30 L11.5,40 L-11.5,40 L-11.5,-30 Z"
|
||||
fill="white" ${STROKE}/>
|
||||
<path d="M-11.5,-32 L11.5,-32" ${DETAIL}/>
|
||||
<path d="M-11.5,-26 L11.5,-26" ${DETAIL}/>
|
||||
<path d="M-11.5,0 L11.5,0" ${DETAIL}/>
|
||||
<path d="M-11.5,32 L11.5,32" ${DETAIL}/>`,
|
||||
|
||||
@@ -90,8 +103,49 @@ const BODIES = {
|
||||
fill="white" ${STROKE}/>
|
||||
<path d="M-7.5,-6 L7.5,-6 L7.5,20 L-7.5,20 Z" ${DETAIL}/>`,
|
||||
|
||||
// ── Unknown: a plain rounded marker. Deliberately featureless, so an
|
||||
// ── Unknown: a plain marker with a nose and nothing else, so an
|
||||
// unclassified vehicle never pretends to be a mode it is not.
|
||||
unknown: `
|
||||
<path d="M0,-38 L10,-24 L10,26 C10,30 7,32 0,32 C-7,32 -10,30 -10,26
|
||||
L-10,-24 Z"
|
||||
fill="white" ${STROKE}/>`,
|
||||
};
|
||||
|
||||
const NEUTRAL_BODIES = {
|
||||
bus: `
|
||||
<path d="M-17,-28 C-17,-32 -14,-34 0,-34 C14,-34 17,-32 17,-28
|
||||
L17,28 C17,32 14,34 0,34 C-14,34 -17,32 -17,28 Z"
|
||||
fill="white" ${STROKE}/>
|
||||
<path d="M-11,-20 L11,-20" ${DETAIL}/>
|
||||
<path d="M-11,20 L11,20" ${DETAIL}/>`,
|
||||
|
||||
tram: `
|
||||
<path d="M-10,-38 C-10,-41 -8,-42.5 0,-42.5 C8,-42.5 10,-41 10,-38
|
||||
L10,38 C10,41 8,42.5 0,42.5 C-8,42.5 -10,41 -10,38 Z"
|
||||
fill="white" ${STROKE}/>
|
||||
<path d="M-10,-13 L10,-13" ${DETAIL}/>
|
||||
<path d="M-10,13 L10,13" ${DETAIL}/>`,
|
||||
|
||||
subway: `
|
||||
<path d="M-11.5,-40 L11.5,-40 L11.5,40 L-11.5,40 Z"
|
||||
fill="white" ${STROKE}/>
|
||||
<path d="M-11.5,-32 L11.5,-32" ${DETAIL}/>
|
||||
<path d="M-11.5,0 L11.5,0" ${DETAIL}/>
|
||||
<path d="M-11.5,32 L11.5,32" ${DETAIL}/>`,
|
||||
|
||||
rail: `
|
||||
<path d="M-10.5,-40 C-10.5,-43 -8.5,-44.5 0,-44.5 C8.5,-44.5 10.5,-43 10.5,-40
|
||||
L10.5,40 C10.5,43 8.5,44.5 0,44.5 C-8.5,44.5 -10.5,43 -10.5,40 Z"
|
||||
fill="white" ${STROKE}/>
|
||||
<path d="M-10.5,-6 L10.5,-6" ${DETAIL}/>
|
||||
<path d="M-10.5,6 L10.5,6" ${DETAIL}/>`,
|
||||
|
||||
ferry: `
|
||||
<path d="M-12.5,-30 C-12.5,-36 -8,-38 0,-38 C8,-38 12.5,-36 12.5,-30
|
||||
L12.5,30 C12.5,36 8,38 0,38 C-8,38 -12.5,36 -12.5,30 Z"
|
||||
fill="white" ${STROKE}/>
|
||||
<path d="M-7.5,-13 L7.5,-13 L7.5,13 L-7.5,13 Z" ${DETAIL}/>`,
|
||||
|
||||
unknown: `
|
||||
<path d="M-10,-26 C-10,-30 -7,-32 0,-32 C7,-32 10,-30 10,-26
|
||||
L10,26 C10,30 7,32 0,32 C-7,32 -10,30 -10,26 Z"
|
||||
@@ -99,7 +153,7 @@ const BODIES = {
|
||||
};
|
||||
|
||||
/** The modes this module can draw. */
|
||||
export const TRANSIT_ICON_KINDS = Object.freeze(Object.keys(BODIES));
|
||||
export const TRANSIT_ICON_KINDS = Object.freeze(Object.keys(HEADING_BODIES));
|
||||
|
||||
const _iconCache = new Map();
|
||||
|
||||
@@ -155,8 +209,8 @@ export function transitIconCacheSize() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Data URI for one mode's silhouette, lazily built and cached per kind, size
|
||||
* and halo.
|
||||
* Data URI for one mode's silhouette, lazily built and cached per kind, size,
|
||||
* halo and whether the vehicle has a known course.
|
||||
*
|
||||
* With a halo, the body is drawn twice: first as a wide dark stroke, then as
|
||||
* the shipped white fill with its hairline edge on top, so the ring sits
|
||||
@@ -166,11 +220,14 @@ export function transitIconCacheSize() {
|
||||
*
|
||||
* @param {string} kind One of TRANSIT_ICON_KINDS; anything else draws `unknown`.
|
||||
* @param {number} [px] Raster size.
|
||||
* @param {{haloScreenPx?: number, screenPx?: number}} [options]
|
||||
* @param {{haloScreenPx?: number, screenPx?: number, style?: string,
|
||||
* heading?: boolean}} [options] `heading: false` draws the symmetric outline
|
||||
* for a vehicle whose course is unknown (its billboard is not rotated, so a
|
||||
* front would point north for no reason).
|
||||
* @returns {string} `data:image/svg+xml;base64,...`
|
||||
*/
|
||||
export function transitIcon(kind, px = FLEET_RASTER_PX, options = {}) {
|
||||
const k = BODIES[kind] ? kind : 'unknown';
|
||||
const k = HEADING_BODIES[kind] ? kind : 'unknown';
|
||||
const selected = px > FLEET_RASTER_PX;
|
||||
px = selected ? SELECTED_ICON_PX : FLEET_RASTER_PX;
|
||||
const profile = transitStyleProfile(
|
||||
@@ -185,16 +242,18 @@ export function transitIcon(kind, px = FLEET_RASTER_PX, options = {}) {
|
||||
profile === 'mono' ? 'thermal' : profile === 'crt' ? 'retro' : 'normal';
|
||||
const displayPx = (selected ? 30 : 20) * presetSpriteScale(style, selected);
|
||||
const frame = haloFrame(presetSpriteOutlinePx(style, selected), displayPx);
|
||||
const key = `${k}:${selected ? 1 : 0}:${profile}`;
|
||||
const heading = options.heading !== false;
|
||||
const bodies = heading ? HEADING_BODIES : NEUTRAL_BODIES;
|
||||
const key = `${k}:${selected ? 1 : 0}:${profile}:${heading ? 'h' : 'n'}`;
|
||||
let uri = _iconCache.get(key);
|
||||
if (!uri) {
|
||||
const size = VIEW + 2 * frame.pad;
|
||||
// Reuse the exact outer path. Sensors fill its interior without panel ink.
|
||||
const silhouette = BODIES[k].match(/<path d="([^"]+)"/)[1];
|
||||
const silhouette = bodies[k].match(/<path d="([^"]+)"/)[1];
|
||||
const body =
|
||||
profile === 'mono'
|
||||
? `<path d="${silhouette}" fill="white" />`
|
||||
: BODIES[k];
|
||||
: bodies[k];
|
||||
const halo =
|
||||
frame.units > 0
|
||||
? profile === 'mono'
|
||||
|
||||
Reference in new issue
Block a user