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",
|
||||
"version": "1.75.4",
|
||||
"version": "1.75.5",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
+20
-25
@@ -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<Difficulty, string> = {
|
||||
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<Partial<Record<HeroVariant, HeroVariantResult>> | 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<HikeManifes
|
||||
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)
|
||||
// so their outNames join `keepFiles.images` and survive the orphan sweep,
|
||||
// while previous-build `icon.<oldhash>.*` / `hero.<oldhash>.*` 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<HikeManifes
|
||||
await fs.writeFile(trackFile, trackJson);
|
||||
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'
|
||||
? fm.date
|
||||
: (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
|
||||
* page uses this to fade out the SSR-rendered static hero. */
|
||||
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 {
|
||||
@@ -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,
|
||||
|
||||
@@ -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<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_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,
|
||||
|
||||
@@ -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 { 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 @@
|
||||
<section class="scroll-area">
|
||||
<aside class="trail-col">
|
||||
{#if track && track.length > 0}
|
||||
<HikeMap {track} imagePoints={visibleImagePoints} showPrivate />
|
||||
<HikeMap {track} imagePoints={visibleImagePoints} showPrivate {trackColor} />
|
||||
<ElevationProfile {track} />
|
||||
{/if}
|
||||
</aside>
|
||||
|
||||
Reference in New Issue
Block a user