diff options
Diffstat (limited to 'web')
| -rw-r--r-- | web/frontend/official-climb-profile.ts | 61 | ||||
| -rw-r--r-- | web/frontend/types.d.ts | 2 | ||||
| -rw-r--r-- | web/static/style.css | 4 |
3 files changed, 64 insertions, 3 deletions
diff --git a/web/frontend/official-climb-profile.ts b/web/frontend/official-climb-profile.ts index b60a1fa..bc851f5 100644 --- a/web/frontend/official-climb-profile.ts +++ b/web/frontend/official-climb-profile.ts @@ -38,6 +38,8 @@ export class OfficialClimbProfileController { "9-plus": resolveColor("--color-profile-9-plus"), plotSurface: resolveColor("--color-plot-surface"), grid: resolveColor("--color-plot-grid"), + xGrid: resolveColor("--color-profile-grid-x"), + yGrid: resolveColor("--color-profile-grid-y"), subtle: resolveColor("--color-subtle"), accent: resolveColor("--color-accent"), }; @@ -65,7 +67,7 @@ export class OfficialClimbProfileController { profileCanvas.width = Math.floor(rect.width * ratio); profileCanvas.height = Math.floor(rect.height * ratio); context.setTransform(ratio, 0, 0, ratio, 0, 0); - const plot = { left: 10, right: rect.width - 10, top: 16, bottom: rect.height - 34 }; + const plot = { left: 10, right: Math.max(11, rect.width - 44), top: 16, bottom: rect.height - 34 }; const climbLengthM = (points[endIndex].distanceKm - points[startIndex].distanceKm) * 1000; const sections = officialProfileSections(points, startIndex, endIndex, displayStepForLength(climbLengthM)); if (sections.length === 0) return; @@ -87,8 +89,40 @@ export class OfficialClimbProfileController { context.clearRect(0, 0, rect.width, rect.height); context.fillStyle = colors.plotSurface; context.fillRect(0, 0, rect.width, rect.height); - context.strokeStyle = colors.grid; + context.font = "9px system-ui, sans-serif"; context.lineWidth = 1; + context.setLineDash([4, 4]); + context.strokeStyle = colors.yGrid; + const yGridElevations: number[] = []; + for (let elevationM = Math.ceil(minElevation / 100) * 100; elevationM <= maxElevation; elevationM += 100) { + yGridElevations.push(elevationM); + } + context.save(); + context.beginPath(); + context.moveTo(plot.left, plot.bottom); + context.lineTo(plot.left, yForElevation(sections[0].startElevation)); + for (const section of sections) { + context.lineTo(xForDistance(section.endDistanceKm), yForElevation(section.endElevation)); + } + context.lineTo(plot.right, plot.bottom); + context.closePath(); + context.clip(); + for (const elevationM of yGridElevations) { + const y = yForElevation(elevationM); + context.beginPath(); + context.moveTo(plot.left, y); + context.lineTo(plot.right, y); + context.stroke(); + } + context.restore(); + context.fillStyle = colors.yGrid; + context.textAlign = "left"; + context.textBaseline = "middle"; + for (const elevationM of yGridElevations) { + context.fillText(`${elevationM} m`, plot.right + 6, yForElevation(elevationM)); + } + context.setLineDash([]); + context.strokeStyle = colors.grid; context.beginPath(); context.moveTo(plot.left, plot.bottom); context.lineTo(plot.right, plot.bottom); @@ -113,6 +147,29 @@ export class OfficialClimbProfileController { context.lineWidth = 2.5; context.stroke(); } + context.save(); + context.beginPath(); + context.moveTo(plot.left, plot.bottom); + context.lineTo(plot.left, yForElevation(sections[0].startElevation)); + for (const section of sections) { + context.lineTo(xForDistance(section.endDistanceKm), yForElevation(section.endElevation)); + } + context.lineTo(plot.right, plot.bottom); + context.closePath(); + context.clip(); + context.strokeStyle = colors.xGrid; + context.lineWidth = 1; + context.setLineDash([4, 4]); + for (let sectionIndex = 0; sectionIndex <= sections.length; sectionIndex++) { + const distanceKm = sectionIndex === sections.length ? maxDistance : sections[sectionIndex].startDistanceKm; + const x = xForDistance(distanceKm); + context.beginPath(); + context.moveTo(x, plot.top); + context.lineTo(x, plot.bottom); + context.stroke(); + } + context.restore(); + context.setLineDash([]); context.font = "9px system-ui, sans-serif"; context.fillStyle = colors.subtle; context.textBaseline = "bottom"; diff --git a/web/frontend/types.d.ts b/web/frontend/types.d.ts index c53b7e5..0b59dac 100644 --- a/web/frontend/types.d.ts +++ b/web/frontend/types.d.ts @@ -80,6 +80,8 @@ export interface OfficialProfileColors { "9-plus": string; plotSurface: string; grid: string; + xGrid: string; + yGrid: string; subtle: string; accent: string; } diff --git a/web/static/style.css b/web/static/style.css index 8c1d39f..8c4f20c 100644 --- a/web/static/style.css +++ b/web/static/style.css @@ -31,6 +31,8 @@ --color-plot-surface: oklch(from var(--palette-paper) calc(l + 0.03) calc(c * 0.35) h); --color-plot-surface-overlay: oklch(from var(--color-plot-surface) l c h / 90%); --color-plot-grid: var(--color-border-subtle); + --color-profile-grid-x: var(--palette-white); + --color-profile-grid-y: oklch(0% 0 0deg); --color-accent-fill: oklch(from var(--palette-coral) l c h / 12%); --color-profile-fill: oklch(from var(--palette-leaf) l c h / 16%); --color-profile-line: var(--color-forest); @@ -116,7 +118,7 @@ th { color: var(--color-subtle); font-size: .78rem; letter-spacing: .08em; text- .official-climb-chevron { color: var(--color-accent); font-size: 1.5rem; font-weight: 500; line-height: 1; } .official-climb-card[open] .official-climb-chevron { transform: rotate(180deg); } .official-climb-card-body { padding: .9rem 1rem 1rem; border-top: 1px solid var(--color-border-subtle); background: var(--color-surface); } -.official-climb-profile { display: block; width: 100%; height: 10rem; margin-top: .75rem; border-radius: .55rem; background: var(--color-plot-surface); outline: none; } +.official-climb-profile { display: block; width: 100%; height: auto; aspect-ratio: 3 / 1; margin-top: .75rem; border-radius: .55rem; background: var(--color-plot-surface); outline: none; } .official-climb-profile:focus-visible { box-shadow: 0 0 0 .2rem var(--color-focus); } .official-climb-profile-legend { display: flex; flex-wrap: wrap; gap: .45rem .8rem; margin-top: .65rem; color: var(--color-muted); font-size: .72rem; } .official-climb-profile-legend > span { display: inline-flex; align-items: center; gap: .3rem; } |