| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175 |
- import { type MapScene } from './mapRenderer'
- import { CourseLayer } from '../layer/courseLayer'
- const EARTH_CIRCUMFERENCE_METERS = 40075016.686
- const LABEL_FONT_SIZE_RATIO = 1.08
- const LABEL_OFFSET_X_RATIO = 1.18
- const LABEL_OFFSET_Y_RATIO = -0.68
- const SCORE_LABEL_FONT_SIZE_RATIO = 0.7
- const SCORE_LABEL_OFFSET_Y_RATIO = 0.06
- const DEFAULT_LABEL_COLOR = 'rgba(204, 0, 107, 0.98)'
- const ACTIVE_LABEL_COLOR = 'rgba(255, 219, 54, 0.98)'
- const MULTI_ACTIVE_LABEL_COLOR = 'rgba(255, 202, 72, 0.96)'
- const FOCUSED_LABEL_COLOR = 'rgba(255, 252, 255, 0.98)'
- const COMPLETED_LABEL_COLOR = 'rgba(126, 131, 138, 0.94)'
- const SCORE_LABEL_COLOR = 'rgba(255, 252, 242, 0.98)'
- const SCORE_COMPLETED_LABEL_COLOR = 'rgba(214, 218, 224, 0.94)'
- export class CourseLabelRenderer {
- courseLayer: CourseLayer
- canvas: any
- ctx: any
- dpr: number
- width: number
- height: number
- constructor(courseLayer: CourseLayer) {
- this.courseLayer = courseLayer
- this.canvas = null
- this.ctx = null
- this.dpr = 1
- this.width = 0
- this.height = 0
- }
- attachCanvas(canvasNode: any, width: number, height: number, dpr: number): void {
- this.canvas = canvasNode
- this.ctx = canvasNode.getContext('2d')
- this.dpr = dpr || 1
- this.width = width
- this.height = height
- canvasNode.width = Math.max(1, Math.floor(width * this.dpr))
- canvasNode.height = Math.max(1, Math.floor(height * this.dpr))
- }
- destroy(): void {
- this.ctx = null
- this.canvas = null
- this.width = 0
- this.height = 0
- }
- render(scene: MapScene): void {
- if (!this.ctx || !this.canvas) {
- return
- }
- const course = this.courseLayer.projectCourse(scene)
- const ctx = this.ctx
- this.clearCanvas(ctx)
- if (!course || !course.controls.length || !scene.revealFullCourse) {
- return
- }
- const controlRadiusMeters = scene.cpRadiusMeters > 0 ? scene.cpRadiusMeters : 5
- const fontSizePx = this.getMetric(scene, controlRadiusMeters * LABEL_FONT_SIZE_RATIO)
- const scoreFontSizePx = this.getMetric(scene, controlRadiusMeters * SCORE_LABEL_FONT_SIZE_RATIO)
- const scoreOffsetY = this.getMetric(scene, controlRadiusMeters * SCORE_LABEL_OFFSET_Y_RATIO)
- const offsetX = this.getMetric(scene, controlRadiusMeters * LABEL_OFFSET_X_RATIO)
- const offsetY = this.getMetric(scene, controlRadiusMeters * LABEL_OFFSET_Y_RATIO)
- this.applyPreviewTransform(ctx, scene)
- ctx.save()
- if (scene.controlVisualMode === 'multi-target') {
- ctx.textAlign = 'center'
- ctx.textBaseline = 'middle'
- ctx.font = `900 ${scoreFontSizePx}px sans-serif`
- for (const control of course.controls) {
- ctx.save()
- ctx.fillStyle = this.getScoreLabelColor(scene, control.sequence)
- ctx.translate(control.point.x, control.point.y)
- ctx.rotate(scene.rotationRad)
- ctx.fillText(String(control.sequence), 0, scoreOffsetY)
- ctx.restore()
- }
- } else {
- ctx.textAlign = 'left'
- ctx.textBaseline = 'middle'
- ctx.font = `700 ${fontSizePx}px sans-serif`
- for (const control of course.controls) {
- ctx.save()
- ctx.fillStyle = this.getLabelColor(scene, control.sequence)
- ctx.translate(control.point.x, control.point.y)
- ctx.rotate(scene.rotationRad)
- ctx.fillText(String(control.sequence), offsetX, offsetY)
- ctx.restore()
- }
- }
- ctx.restore()
- }
- getLabelColor(scene: MapScene, sequence: number): string {
- if (scene.focusedControlSequences.includes(sequence)) {
- return FOCUSED_LABEL_COLOR
- }
- if (scene.activeControlSequences.includes(sequence)) {
- return scene.controlVisualMode === 'multi-target' ? MULTI_ACTIVE_LABEL_COLOR : ACTIVE_LABEL_COLOR
- }
- if (scene.completedControlSequences.includes(sequence)) {
- return COMPLETED_LABEL_COLOR
- }
- return DEFAULT_LABEL_COLOR
- }
- getScoreLabelColor(scene: MapScene, sequence: number): string {
- if (scene.focusedControlSequences.includes(sequence)) {
- return FOCUSED_LABEL_COLOR
- }
- if (scene.completedControlSequences.includes(sequence)) {
- return SCORE_COMPLETED_LABEL_COLOR
- }
- return SCORE_LABEL_COLOR
- }
- clearCanvas(ctx: any): void {
- ctx.setTransform(1, 0, 0, 1, 0, 0)
- ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
- }
- applyPreviewTransform(ctx: any, scene: MapScene): void {
- const previewScale = scene.previewScale || 1
- const previewOriginX = scene.previewOriginX || scene.viewportWidth / 2
- const previewOriginY = scene.previewOriginY || scene.viewportHeight / 2
- const translateX = (previewOriginX - previewOriginX * previewScale) * this.dpr
- const translateY = (previewOriginY - previewOriginY * previewScale) * this.dpr
- ctx.setTransform(
- this.dpr * previewScale,
- 0,
- 0,
- this.dpr * previewScale,
- translateX,
- translateY,
- )
- }
- getMetric(scene: MapScene, meters: number): number {
- return meters * this.getPixelsPerMeter(scene)
- }
- getPixelsPerMeter(scene: MapScene): number {
- const tileSizePx = scene.viewportWidth / scene.visibleColumns
- const centerLat = this.worldTileYToLat(scene.exactCenterWorldY, scene.zoom)
- const metersPerTile = Math.cos(centerLat * Math.PI / 180) * EARTH_CIRCUMFERENCE_METERS / Math.pow(2, scene.zoom)
- if (!tileSizePx || !metersPerTile) {
- return 0
- }
- return tileSizePx / metersPerTile
- }
- worldTileYToLat(worldY: number, zoom: number): number {
- const scale = Math.pow(2, zoom)
- const latRad = Math.atan(Math.sinh(Math.PI * (1 - 2 * worldY / scale)))
- return latRad * 180 / Math.PI
- }
- }
|