feat(hikes): use SAC-tier colours for the detail-page trail
Live and static heroes on the detail page were rendered in Nord red,
while the same trail on the /hikes overview map already used the
high-contrast SAC palette (orange/red/blue). The mismatch made the
detail trail look muted against the Pixelkarte; the overview's choice
also doubles as a navigational hint ("the orange trail you saw on
the map is this one").
Introduce $lib/data/sacColors as the single source of truth so
HikeMap, HikesOverviewMap, and the build-side static renderer all
pull the same palette. Bump HERO_RENDER_VERSION to v6 so stale
Nord-red static heroes get re-rendered on the next build.
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "homepage",
|
"name": "homepage",
|
||||||
"version": "1.75.4",
|
"version": "1.75.5",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
+20
-25
@@ -28,6 +28,7 @@ import {
|
|||||||
} from '../src/lib/server/gpx.js';
|
} from '../src/lib/server/gpx.js';
|
||||||
import { simplifyTrack } from '../src/lib/server/simplifyTrack.js';
|
import { simplifyTrack } from '../src/lib/server/simplifyTrack.js';
|
||||||
import { computeStaticMapPose, renderOverviewMap, renderStaticMap } from './staticHikeMap.js';
|
import { computeStaticMapPose, renderOverviewMap, renderStaticMap } from './staticHikeMap.js';
|
||||||
|
import { sacTrailColor, SAC_TRAIL_COLOR } from '../src/lib/data/sacColors.js';
|
||||||
import type {
|
import type {
|
||||||
Difficulty,
|
Difficulty,
|
||||||
HikeManifestEntry,
|
HikeManifestEntry,
|
||||||
@@ -565,9 +566,10 @@ const HERO_HEIGHT = 2400;
|
|||||||
// fly-to-fit animation on top.
|
// fly-to-fit animation on top.
|
||||||
const HERO_FIT_WIDTH = 1920;
|
const HERO_FIT_WIDTH = 1920;
|
||||||
const HERO_FIT_HEIGHT = 640;
|
const HERO_FIT_HEIGHT = 640;
|
||||||
// Nord red — same accent the live HikeMap uses for its polyline, so the
|
// Per-hike trail colour is picked from the SAC-tier palette in
|
||||||
// fade-over from static to interactive looks continuous.
|
// `$lib/data/sacColors` at render time — every static hero matches the
|
||||||
const HERO_TRAIL_COLOR = '#bf616a';
|
// live polyline colour and the overview-map polyline for the same hike,
|
||||||
|
// so the fade-over from static to interactive looks continuous.
|
||||||
// Photo-badge fill, border + icon-stroke colours per UI theme. Matches
|
// Photo-badge fill, border + icon-stroke colours per UI theme. Matches
|
||||||
// the live HikeMap's `.hike-photo-marker .badge`:
|
// the live HikeMap's `.hike-photo-marker .badge`:
|
||||||
// background: var(--color-primary) → Nord10 light / Nord8 dark
|
// background: var(--color-primary) → Nord10 light / Nord8 dark
|
||||||
@@ -585,7 +587,8 @@ const HERO_BADGE_ICON_DARK = '#2e3440';
|
|||||||
// Bumped whenever the static-map renderer's visual output changes (icons,
|
// Bumped whenever the static-map renderer's visual output changes (icons,
|
||||||
// stroke widths, marker shapes, ...) so the per-hike hash invalidates and
|
// stroke widths, marker shapes, ...) so the per-hike hash invalidates and
|
||||||
// existing files get re-rendered on the next build.
|
// existing files get re-rendered on the next build.
|
||||||
const HERO_RENDER_VERSION = 5;
|
// v6: per-hike trail colour switched from Nord red to SAC-tier palette.
|
||||||
|
const HERO_RENDER_VERSION = 6;
|
||||||
|
|
||||||
// Narrow-viewport variant for phones (≤ 560 px CSS width). Same renderer,
|
// Narrow-viewport variant for phones (≤ 560 px CSS width). Same renderer,
|
||||||
// but the pose is picked for a phone-sized container so the auto-fit zoom
|
// but the pose is picked for a phone-sized container so the auto-fit zoom
|
||||||
@@ -627,18 +630,6 @@ const HERO_VARIANT_SPECS: ReadonlyArray<{
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
// SAC-tier polyline colours for the overview hero. Must stay in sync with
|
|
||||||
// the `SAC_COLOR` map in `HikesOverviewMap.svelte` so the static hero's
|
|
||||||
// trails look identical to the live ones.
|
|
||||||
const OVERVIEW_SAC_COLOR: Record<Difficulty, string> = {
|
|
||||||
T1: '#f5a623',
|
|
||||||
T2: '#dc1d2a',
|
|
||||||
T3: '#dc1d2a',
|
|
||||||
T4: '#2965c8',
|
|
||||||
T5: '#2965c8',
|
|
||||||
T6: '#2965c8'
|
|
||||||
};
|
|
||||||
|
|
||||||
// Padding + max-zoom match the live overview map's
|
// Padding + max-zoom match the live overview map's
|
||||||
// `fitBounds(..., { padding: [32, 32], maxZoom: 13 })` so the static lands
|
// `fitBounds(..., { padding: [32, 32], maxZoom: 13 })` so the static lands
|
||||||
// at the same pose Leaflet will fit to. fitHeight matches the page's
|
// at the same pose Leaflet will fit to. fitHeight matches the page's
|
||||||
@@ -681,7 +672,7 @@ async function processOverview(
|
|||||||
.filter((h) => h.previewPolyline && h.previewPolyline.length >= 2)
|
.filter((h) => h.previewPolyline && h.previewPolyline.length >= 2)
|
||||||
.map((h) => ({
|
.map((h) => ({
|
||||||
points: h.previewPolyline,
|
points: h.previewPolyline,
|
||||||
color: OVERVIEW_SAC_COLOR[h.difficulty] ?? '#5e81ac'
|
color: SAC_TRAIL_COLOR[h.difficulty] ?? '#5e81ac'
|
||||||
}));
|
}));
|
||||||
if (lines.length === 0) return undefined;
|
if (lines.length === 0) return undefined;
|
||||||
|
|
||||||
@@ -817,10 +808,12 @@ async function processHero(
|
|||||||
slug: string,
|
slug: string,
|
||||||
track: GpxPoint[],
|
track: GpxPoint[],
|
||||||
bbox: [number, number, number, number],
|
bbox: [number, number, number, number],
|
||||||
imagePoints: ImagePoint[]
|
imagePoints: ImagePoint[],
|
||||||
|
difficulty: Difficulty
|
||||||
): Promise<Partial<Record<HeroVariant, HeroVariantResult>> | undefined> {
|
): Promise<Partial<Record<HeroVariant, HeroVariantResult>> | undefined> {
|
||||||
if (track.length < 2) return undefined;
|
if (track.length < 2) return undefined;
|
||||||
|
|
||||||
|
const trailColor = sacTrailColor(difficulty);
|
||||||
const polyline: Array<[number, number]> = track.map((p) => [p.lat, p.lng]);
|
const polyline: Array<[number, number]> = track.map((p) => [p.lat, p.lng]);
|
||||||
// Public photo markers only — the hero is rendered once and served to
|
// Public photo markers only — the hero is rendered once and served to
|
||||||
// everyone, including logged-out viewers, so private positions must
|
// everyone, including logged-out viewers, so private positions must
|
||||||
@@ -861,7 +854,7 @@ async function processHero(
|
|||||||
h: spec.height,
|
h: spec.height,
|
||||||
fw: spec.fitWidth,
|
fw: spec.fitWidth,
|
||||||
fh: spec.fitHeight,
|
fh: spec.fitHeight,
|
||||||
color: HERO_TRAIL_COLOR,
|
color: trailColor,
|
||||||
poly: polyline,
|
poly: polyline,
|
||||||
photos: photoMarkers,
|
photos: photoMarkers,
|
||||||
fill: fillColor,
|
fill: fillColor,
|
||||||
@@ -883,7 +876,7 @@ async function processHero(
|
|||||||
const ok = await renderStaticMap({
|
const ok = await renderStaticMap({
|
||||||
pose,
|
pose,
|
||||||
polyline,
|
polyline,
|
||||||
color: HERO_TRAIL_COLOR,
|
color: trailColor,
|
||||||
outputPath: outPath,
|
outputPath: outPath,
|
||||||
width: spec.width,
|
width: spec.width,
|
||||||
height: spec.height,
|
height: spec.height,
|
||||||
@@ -1079,13 +1072,19 @@ async function buildHike(slug: string, cache: GeocodeCache): Promise<HikeManifes
|
|||||||
if (r.point) imagePoints.push(r.point);
|
if (r.point) imagePoints.push(r.point);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Difficulty is hoisted from the manifest assembly below because the
|
||||||
|
// hero renderer needs it to pick the SAC-tier trail colour.
|
||||||
|
const difficulty = (typeof fm.difficulty === 'string' && VALID_DIFFICULTIES.includes(fm.difficulty as Difficulty))
|
||||||
|
? (fm.difficulty as Difficulty)
|
||||||
|
: 'T1';
|
||||||
|
|
||||||
// Per-route icon + pre-rendered hero map — handled here (before cleanup)
|
// Per-route icon + pre-rendered hero map — handled here (before cleanup)
|
||||||
// so their outNames join `keepFiles.images` and survive the orphan sweep,
|
// so their outNames join `keepFiles.images` and survive the orphan sweep,
|
||||||
// while previous-build `icon.<oldhash>.*` / `hero.<oldhash>.*` files
|
// while previous-build `icon.<oldhash>.*` / `hero.<oldhash>.*` files
|
||||||
// (different hash, not in keepFiles) get removed automatically.
|
// (different hash, not in keepFiles) get removed automatically.
|
||||||
const [iconResult, heroResult] = await Promise.all([
|
const [iconResult, heroResult] = await Promise.all([
|
||||||
processIcon(slug, hikeDir),
|
processIcon(slug, hikeDir),
|
||||||
processHero(slug, track, bbox, imagePoints)
|
processHero(slug, track, bbox, imagePoints, difficulty)
|
||||||
]);
|
]);
|
||||||
if (iconResult) keepFiles.images.add(iconResult.outName);
|
if (iconResult) keepFiles.images.add(iconResult.outName);
|
||||||
if (heroResult) {
|
if (heroResult) {
|
||||||
@@ -1146,10 +1145,6 @@ async function buildHike(slug: string, cache: GeocodeCache): Promise<HikeManifes
|
|||||||
await fs.writeFile(trackFile, trackJson);
|
await fs.writeFile(trackFile, trackJson);
|
||||||
console.log(`[build-hikes:${slug}] wrote track.${trackHash}.json (${trackJson.length} bytes)`);
|
console.log(`[build-hikes:${slug}] wrote track.${trackHash}.json (${trackJson.length} bytes)`);
|
||||||
|
|
||||||
const difficulty = (typeof fm.difficulty === 'string' && VALID_DIFFICULTIES.includes(fm.difficulty as Difficulty))
|
|
||||||
? (fm.difficulty as Difficulty)
|
|
||||||
: 'T1';
|
|
||||||
|
|
||||||
const date = typeof fm.date === 'string'
|
const date = typeof fm.date === 'string'
|
||||||
? fm.date
|
? fm.date
|
||||||
: (typeof fm.date === 'number' ? new Date(fm.date).toISOString().slice(0, 10) : new Date().toISOString().slice(0, 10));
|
: (typeof fm.date === 'number' ? new Date(fm.date).toISOString().slice(0, 10) : new Date().toISOString().slice(0, 10));
|
||||||
|
|||||||
@@ -29,6 +29,11 @@
|
|||||||
* finished loading — i.e. the map is visually complete. The detail
|
* finished loading — i.e. the map is visually complete. The detail
|
||||||
* page uses this to fade out the SSR-rendered static hero. */
|
* page uses this to fade out the SSR-rendered static hero. */
|
||||||
onReady?: () => void;
|
onReady?: () => void;
|
||||||
|
/** Polyline colour. Defaults to Nord red. Callers set this to the
|
||||||
|
* SAC-tier colour so the live trail matches the colour of the same
|
||||||
|
* route on the /hikes overview map (orange for T1, red for T2/T3,
|
||||||
|
* blue for T4–T6). */
|
||||||
|
trackColor?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -37,7 +42,8 @@
|
|||||||
showPrivate = false,
|
showPrivate = false,
|
||||||
initialCenter,
|
initialCenter,
|
||||||
initialZoom,
|
initialZoom,
|
||||||
onReady
|
onReady,
|
||||||
|
trackColor
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
// User-location toggle moved inside the map UI. localStorage-persisted so
|
// User-location toggle moved inside the map UI. localStorage-persisted so
|
||||||
@@ -209,12 +215,13 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Canvas-rendered polylines can't resolve CSS custom properties, so read
|
// Canvas-rendered polylines can't resolve CSS custom properties,
|
||||||
// the trail color from the document at mount time. Nord red contrasts
|
// so the caller hands us a literal colour. Falls back to Nord red
|
||||||
// strongly against both the schematic map and the aerial imagery.
|
// for any caller that hasn't been updated yet.
|
||||||
const trailColor =
|
const trailColor =
|
||||||
getComputedStyle(document.documentElement).getPropertyValue('--red').trim() ||
|
trackColor ??
|
||||||
'#bf616a';
|
(getComputedStyle(document.documentElement).getPropertyValue('--red').trim() ||
|
||||||
|
'#bf616a');
|
||||||
|
|
||||||
const polyline = L.polyline(latLngs, {
|
const polyline = L.polyline(latLngs, {
|
||||||
color: trailColor,
|
color: trailColor,
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
import type { Attachment } from 'svelte/attachments';
|
import type { Attachment } from 'svelte/attachments';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { resolve } from '$app/paths';
|
import { resolve } from '$app/paths';
|
||||||
import type { HikeManifestEntry, Difficulty } from '$types/hikes';
|
import { sacTrailColor } from '$lib/data/sacColors';
|
||||||
|
import type { HikeManifestEntry } from '$types/hikes';
|
||||||
import Map from '@lucide/svelte/icons/map';
|
import Map from '@lucide/svelte/icons/map';
|
||||||
import Satellite from '@lucide/svelte/icons/satellite';
|
import Satellite from '@lucide/svelte/icons/satellite';
|
||||||
import Landmark from '@lucide/svelte/icons/landmark';
|
import Landmark from '@lucide/svelte/icons/landmark';
|
||||||
@@ -27,18 +28,6 @@
|
|||||||
|
|
||||||
const { hikes, initialCenter, initialZoom, onReady }: Props = $props();
|
const { hikes, initialCenter, initialZoom, onReady }: Props = $props();
|
||||||
|
|
||||||
// Per-tier polyline colour, matching the painted-marker scheme on the
|
|
||||||
// SAC badges. Canvas-rendered polylines can't resolve CSS variables,
|
|
||||||
// so the values are hard-coded — keep in sync with HikeCard.svelte.
|
|
||||||
const SAC_COLOR: Record<Difficulty, string> = {
|
|
||||||
T1: '#f5a623',
|
|
||||||
T2: '#dc1d2a',
|
|
||||||
T3: '#dc1d2a',
|
|
||||||
T4: '#2965c8',
|
|
||||||
T5: '#2965c8',
|
|
||||||
T6: '#2965c8'
|
|
||||||
};
|
|
||||||
|
|
||||||
const SWISSTOPO_FARBE = 'https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.pixelkarte-farbe/default/current/3857/{z}/{x}/{y}.jpeg';
|
const SWISSTOPO_FARBE = 'https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.pixelkarte-farbe/default/current/3857/{z}/{x}/{y}.jpeg';
|
||||||
const SWISSTOPO_IMAGE = 'https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.swissimage/default/current/3857/{z}/{x}/{y}.jpeg';
|
const SWISSTOPO_IMAGE = 'https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.swissimage/default/current/3857/{z}/{x}/{y}.jpeg';
|
||||||
const SWISSTOPO_DUFOUR = 'https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.hiks-dufour/default/current/3857/{z}/{x}/{y}.png';
|
const SWISSTOPO_DUFOUR = 'https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.hiks-dufour/default/current/3857/{z}/{x}/{y}.png';
|
||||||
@@ -197,7 +186,7 @@
|
|||||||
for (const hike of hikes) {
|
for (const hike of hikes) {
|
||||||
if (!hike.previewPolyline || hike.previewPolyline.length < 2) continue;
|
if (!hike.previewPolyline || hike.previewPolyline.length < 2) continue;
|
||||||
const latLngs = hike.previewPolyline.map(([lat, lng]) => [lat, lng] as [number, number]);
|
const latLngs = hike.previewPolyline.map(([lat, lng]) => [lat, lng] as [number, number]);
|
||||||
const color = SAC_COLOR[hike.difficulty] ?? '#5e81ac';
|
const color = sacTrailColor(hike.difficulty);
|
||||||
const poly = L.polyline(latLngs, {
|
const poly = L.polyline(latLngs, {
|
||||||
color,
|
color,
|
||||||
weight: 4,
|
weight: 4,
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import type { Difficulty } from '$types/hikes';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SAC-tier trail colour, used as the polyline colour for every per-hike
|
||||||
|
* track rendering (overview map, detail-page live + static heroes).
|
||||||
|
*
|
||||||
|
* T1 orange — yellow Wegweiser trails (Wanderwege)
|
||||||
|
* T2/T3 red — white-red-white Bergwanderwege
|
||||||
|
* T4-T6 blue — white-blue-white Alpinwanderwege
|
||||||
|
*
|
||||||
|
* Single source of truth — keep the build script and every Svelte
|
||||||
|
* component pointed here so a re-tune lands in one place. Values are
|
||||||
|
* saturated on purpose to contrast strongly against the Pixelkarte's
|
||||||
|
* desaturated greens / browns; tweak the build-side `HERO_RENDER_VERSION`
|
||||||
|
* if these values change so stale static heroes get invalidated.
|
||||||
|
*/
|
||||||
|
export const SAC_TRAIL_COLOR: Record<Difficulty, string> = {
|
||||||
|
T1: '#f5a623',
|
||||||
|
T2: '#dc1d2a',
|
||||||
|
T3: '#dc1d2a',
|
||||||
|
T4: '#2965c8',
|
||||||
|
T5: '#2965c8',
|
||||||
|
T6: '#2965c8'
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Fallback when a hike's difficulty isn't a known SAC tier (defensive —
|
||||||
|
* the manifest type only allows valid tiers, but a stale build could leak
|
||||||
|
* an unexpected string here). */
|
||||||
|
export const SAC_TRAIL_COLOR_DEFAULT = '#bf616a';
|
||||||
|
|
||||||
|
export function sacTrailColor(difficulty: Difficulty | string | null | undefined): string {
|
||||||
|
if (!difficulty) return SAC_TRAIL_COLOR_DEFAULT;
|
||||||
|
return SAC_TRAIL_COLOR[difficulty as Difficulty] ?? SAC_TRAIL_COLOR_DEFAULT;
|
||||||
|
}
|
||||||
@@ -17,6 +17,7 @@
|
|||||||
import Download from '@lucide/svelte/icons/download';
|
import Download from '@lucide/svelte/icons/download';
|
||||||
import { buildGpx, type GpxWritePoint } from '$lib/gpx';
|
import { buildGpx, type GpxWritePoint } from '$lib/gpx';
|
||||||
import { resolveCanton } from '$lib/data/cantons';
|
import { resolveCanton } from '$lib/data/cantons';
|
||||||
|
import { sacTrailColor } from '$lib/data/sacColors';
|
||||||
import type { HikeTrackPoint } from '$types/hikes';
|
import type { HikeTrackPoint } from '$types/hikes';
|
||||||
import type { PageProps } from './$types';
|
import type { PageProps } from './$types';
|
||||||
|
|
||||||
@@ -51,6 +52,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
const canton = $derived(resolveCanton(hike.canton));
|
const canton = $derived(resolveCanton(hike.canton));
|
||||||
|
const trackColor = $derived(sacTrailColor(hike.difficulty));
|
||||||
|
|
||||||
// Publish date formatted in long German for the meta footer
|
// Publish date formatted in long German for the meta footer
|
||||||
// (matches the hike's `date: YYYY-MM-DD` frontmatter format).
|
// (matches the hike's `date: YYYY-MM-DD` frontmatter format).
|
||||||
@@ -336,6 +338,7 @@
|
|||||||
{track}
|
{track}
|
||||||
imagePoints={visibleImagePoints}
|
imagePoints={visibleImagePoints}
|
||||||
showPrivate
|
showPrivate
|
||||||
|
{trackColor}
|
||||||
initialCenter={heroPose?.center}
|
initialCenter={heroPose?.center}
|
||||||
initialZoom={heroPose?.zoom}
|
initialZoom={heroPose?.zoom}
|
||||||
onReady={() => (heroMapReady = true)}
|
onReady={() => (heroMapReady = true)}
|
||||||
@@ -451,7 +454,7 @@
|
|||||||
<section class="scroll-area">
|
<section class="scroll-area">
|
||||||
<aside class="trail-col">
|
<aside class="trail-col">
|
||||||
{#if track && track.length > 0}
|
{#if track && track.length > 0}
|
||||||
<HikeMap {track} imagePoints={visibleImagePoints} showPrivate />
|
<HikeMap {track} imagePoints={visibleImagePoints} showPrivate {trackColor} />
|
||||||
<ElevationProfile {track} />
|
<ElevationProfile {track} />
|
||||||
{/if}
|
{/if}
|
||||||
</aside>
|
</aside>
|
||||||
|
|||||||
Reference in New Issue
Block a user