diff --git a/package.json b/package.json index 74b30001..d973455a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "homepage", - "version": "1.75.4", + "version": "1.75.5", "private": true, "type": "module", "scripts": { diff --git a/scripts/build-hikes.ts b/scripts/build-hikes.ts index 4374facc..fcb62485 100644 --- a/scripts/build-hikes.ts +++ b/scripts/build-hikes.ts @@ -28,6 +28,7 @@ import { } from '../src/lib/server/gpx.js'; import { simplifyTrack } from '../src/lib/server/simplifyTrack.js'; import { computeStaticMapPose, renderOverviewMap, renderStaticMap } from './staticHikeMap.js'; +import { sacTrailColor, SAC_TRAIL_COLOR } from '../src/lib/data/sacColors.js'; import type { Difficulty, HikeManifestEntry, @@ -565,9 +566,10 @@ const HERO_HEIGHT = 2400; // fly-to-fit animation on top. const HERO_FIT_WIDTH = 1920; const HERO_FIT_HEIGHT = 640; -// Nord red — same accent the live HikeMap uses for its polyline, so the -// fade-over from static to interactive looks continuous. -const HERO_TRAIL_COLOR = '#bf616a'; +// Per-hike trail colour is picked from the SAC-tier palette in +// `$lib/data/sacColors` at render time — every static hero matches the +// 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 // the live HikeMap's `.hike-photo-marker .badge`: // 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, // stroke widths, marker shapes, ...) so the per-hike hash invalidates and // 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, // 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 = { - T1: '#f5a623', - T2: '#dc1d2a', - T3: '#dc1d2a', - T4: '#2965c8', - T5: '#2965c8', - T6: '#2965c8' -}; - // Padding + max-zoom match the live overview map's // `fitBounds(..., { padding: [32, 32], maxZoom: 13 })` so the static lands // 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) .map((h) => ({ points: h.previewPolyline, - color: OVERVIEW_SAC_COLOR[h.difficulty] ?? '#5e81ac' + color: SAC_TRAIL_COLOR[h.difficulty] ?? '#5e81ac' })); if (lines.length === 0) return undefined; @@ -817,10 +808,12 @@ async function processHero( slug: string, track: GpxPoint[], bbox: [number, number, number, number], - imagePoints: ImagePoint[] + imagePoints: ImagePoint[], + difficulty: Difficulty ): Promise> | undefined> { if (track.length < 2) return undefined; + const trailColor = sacTrailColor(difficulty); const polyline: Array<[number, number]> = track.map((p) => [p.lat, p.lng]); // Public photo markers only — the hero is rendered once and served to // everyone, including logged-out viewers, so private positions must @@ -861,7 +854,7 @@ async function processHero( h: spec.height, fw: spec.fitWidth, fh: spec.fitHeight, - color: HERO_TRAIL_COLOR, + color: trailColor, poly: polyline, photos: photoMarkers, fill: fillColor, @@ -883,7 +876,7 @@ async function processHero( const ok = await renderStaticMap({ pose, polyline, - color: HERO_TRAIL_COLOR, + color: trailColor, outputPath: outPath, width: spec.width, height: spec.height, @@ -1079,13 +1072,19 @@ async function buildHike(slug: string, cache: GeocodeCache): Promise.*` / `hero..*` files // (different hash, not in keepFiles) get removed automatically. const [iconResult, heroResult] = await Promise.all([ processIcon(slug, hikeDir), - processHero(slug, track, bbox, imagePoints) + processHero(slug, track, bbox, imagePoints, difficulty) ]); if (iconResult) keepFiles.images.add(iconResult.outName); if (heroResult) { @@ -1146,10 +1145,6 @@ async function buildHike(slug: string, cache: GeocodeCache): Promise 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 { @@ -37,7 +42,8 @@ showPrivate = false, initialCenter, initialZoom, - onReady + onReady, + trackColor }: Props = $props(); // 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 - // the trail color from the document at mount time. Nord red contrasts - // strongly against both the schematic map and the aerial imagery. + // Canvas-rendered polylines can't resolve CSS custom properties, + // so the caller hands us a literal colour. Falls back to Nord red + // for any caller that hasn't been updated yet. const trailColor = - getComputedStyle(document.documentElement).getPropertyValue('--red').trim() || - '#bf616a'; + trackColor ?? + (getComputedStyle(document.documentElement).getPropertyValue('--red').trim() || + '#bf616a'); const polyline = L.polyline(latLngs, { color: trailColor, diff --git a/src/lib/components/hikes/HikesOverviewMap.svelte b/src/lib/components/hikes/HikesOverviewMap.svelte index 87dbdcc4..1b27021e 100644 --- a/src/lib/components/hikes/HikesOverviewMap.svelte +++ b/src/lib/components/hikes/HikesOverviewMap.svelte @@ -2,7 +2,8 @@ import type { Attachment } from 'svelte/attachments'; import { goto } from '$app/navigation'; 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 Satellite from '@lucide/svelte/icons/satellite'; import Landmark from '@lucide/svelte/icons/landmark'; @@ -27,18 +28,6 @@ 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 = { - 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_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'; @@ -197,7 +186,7 @@ for (const hike of hikes) { if (!hike.previewPolyline || hike.previewPolyline.length < 2) continue; 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, { color, weight: 4, diff --git a/src/lib/data/sacColors.ts b/src/lib/data/sacColors.ts new file mode 100644 index 00000000..adde4fd1 --- /dev/null +++ b/src/lib/data/sacColors.ts @@ -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 = { + 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; +} diff --git a/src/routes/hikes/[slug]/+page.svelte b/src/routes/hikes/[slug]/+page.svelte index b7647e37..4c7b0564 100644 --- a/src/routes/hikes/[slug]/+page.svelte +++ b/src/routes/hikes/[slug]/+page.svelte @@ -17,6 +17,7 @@ import Download from '@lucide/svelte/icons/download'; import { buildGpx, type GpxWritePoint } from '$lib/gpx'; import { resolveCanton } from '$lib/data/cantons'; + import { sacTrailColor } from '$lib/data/sacColors'; import type { HikeTrackPoint } from '$types/hikes'; import type { PageProps } from './$types'; @@ -51,6 +52,7 @@ }); const canton = $derived(resolveCanton(hike.canton)); + const trackColor = $derived(sacTrailColor(hike.difficulty)); // Publish date formatted in long German for the meta footer // (matches the hike's `date: YYYY-MM-DD` frontmatter format). @@ -336,6 +338,7 @@ {track} imagePoints={visibleImagePoints} showPrivate + {trackColor} initialCenter={heroPose?.center} initialZoom={heroPose?.zoom} onReady={() => (heroMapReady = true)} @@ -451,7 +454,7 @@