courseLabelRenderer.ts 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  1. import { type MapScene } from './mapRenderer'
  2. import { CourseLayer } from '../layer/courseLayer'
  3. const EARTH_CIRCUMFERENCE_METERS = 40075016.686
  4. const LABEL_FONT_SIZE_RATIO = 1.08
  5. const LABEL_OFFSET_X_RATIO = 1.18
  6. const LABEL_OFFSET_Y_RATIO = -0.68
  7. const SCORE_LABEL_FONT_SIZE_RATIO = 0.7
  8. const SCORE_LABEL_OFFSET_Y_RATIO = 0.06
  9. const DEFAULT_LABEL_COLOR = 'rgba(204, 0, 107, 0.98)'
  10. const ACTIVE_LABEL_COLOR = 'rgba(255, 219, 54, 0.98)'
  11. const READY_LABEL_COLOR = 'rgba(98, 255, 214, 0.98)'
  12. const MULTI_ACTIVE_LABEL_COLOR = 'rgba(255, 202, 72, 0.96)'
  13. const FOCUSED_LABEL_COLOR = 'rgba(255, 252, 255, 0.98)'
  14. const COMPLETED_LABEL_COLOR = 'rgba(126, 131, 138, 0.94)'
  15. const SKIPPED_LABEL_COLOR = 'rgba(152, 156, 162, 0.88)'
  16. const SCORE_LABEL_COLOR = 'rgba(255, 252, 242, 0.98)'
  17. const SCORE_COMPLETED_LABEL_COLOR = 'rgba(214, 218, 224, 0.94)'
  18. const SCORE_SKIPPED_LABEL_COLOR = 'rgba(176, 182, 188, 0.9)'
  19. export class CourseLabelRenderer {
  20. courseLayer: CourseLayer
  21. canvas: any
  22. ctx: any
  23. dpr: number
  24. width: number
  25. height: number
  26. constructor(courseLayer: CourseLayer) {
  27. this.courseLayer = courseLayer
  28. this.canvas = null
  29. this.ctx = null
  30. this.dpr = 1
  31. this.width = 0
  32. this.height = 0
  33. }
  34. attachCanvas(canvasNode: any, width: number, height: number, dpr: number): void {
  35. this.canvas = canvasNode
  36. this.ctx = canvasNode.getContext('2d')
  37. this.dpr = dpr || 1
  38. this.width = width
  39. this.height = height
  40. canvasNode.width = Math.max(1, Math.floor(width * this.dpr))
  41. canvasNode.height = Math.max(1, Math.floor(height * this.dpr))
  42. }
  43. destroy(): void {
  44. this.ctx = null
  45. this.canvas = null
  46. this.width = 0
  47. this.height = 0
  48. }
  49. render(scene: MapScene): void {
  50. if (!this.ctx || !this.canvas) {
  51. return
  52. }
  53. const course = this.courseLayer.projectCourse(scene)
  54. const ctx = this.ctx
  55. this.clearCanvas(ctx)
  56. if (!course || !course.controls.length || !scene.revealFullCourse) {
  57. return
  58. }
  59. const controlRadiusMeters = scene.cpRadiusMeters > 0 ? scene.cpRadiusMeters : 5
  60. const fontSizePx = this.getMetric(scene, controlRadiusMeters * LABEL_FONT_SIZE_RATIO)
  61. const scoreFontSizePx = this.getMetric(scene, controlRadiusMeters * SCORE_LABEL_FONT_SIZE_RATIO)
  62. const scoreOffsetY = this.getMetric(scene, controlRadiusMeters * SCORE_LABEL_OFFSET_Y_RATIO)
  63. const offsetX = this.getMetric(scene, controlRadiusMeters * LABEL_OFFSET_X_RATIO)
  64. const offsetY = this.getMetric(scene, controlRadiusMeters * LABEL_OFFSET_Y_RATIO)
  65. this.applyPreviewTransform(ctx, scene)
  66. ctx.save()
  67. if (scene.controlVisualMode === 'multi-target') {
  68. ctx.textAlign = 'center'
  69. ctx.textBaseline = 'middle'
  70. ctx.font = `900 ${scoreFontSizePx}px sans-serif`
  71. for (const control of course.controls) {
  72. ctx.save()
  73. ctx.fillStyle = this.getScoreLabelColor(scene, control.sequence)
  74. ctx.translate(control.point.x, control.point.y)
  75. ctx.rotate(scene.rotationRad)
  76. ctx.fillText(String(control.sequence), 0, scoreOffsetY)
  77. ctx.restore()
  78. }
  79. } else {
  80. ctx.textAlign = 'left'
  81. ctx.textBaseline = 'middle'
  82. ctx.font = `700 ${fontSizePx}px sans-serif`
  83. for (const control of course.controls) {
  84. ctx.save()
  85. ctx.fillStyle = this.getLabelColor(scene, control.sequence)
  86. ctx.translate(control.point.x, control.point.y)
  87. ctx.rotate(scene.rotationRad)
  88. ctx.fillText(String(control.sequence), offsetX, offsetY)
  89. ctx.restore()
  90. }
  91. }
  92. ctx.restore()
  93. }
  94. getLabelColor(scene: MapScene, sequence: number): string {
  95. if (scene.focusedControlSequences.includes(sequence)) {
  96. return FOCUSED_LABEL_COLOR
  97. }
  98. if (scene.readyControlSequences.includes(sequence)) {
  99. return READY_LABEL_COLOR
  100. }
  101. if (scene.activeControlSequences.includes(sequence)) {
  102. return scene.controlVisualMode === 'multi-target' ? MULTI_ACTIVE_LABEL_COLOR : ACTIVE_LABEL_COLOR
  103. }
  104. if (scene.completedControlSequences.includes(sequence)) {
  105. return COMPLETED_LABEL_COLOR
  106. }
  107. if (scene.skippedControlSequences.includes(sequence)) {
  108. return SKIPPED_LABEL_COLOR
  109. }
  110. return DEFAULT_LABEL_COLOR
  111. }
  112. getScoreLabelColor(scene: MapScene, sequence: number): string {
  113. if (scene.focusedControlSequences.includes(sequence)) {
  114. return FOCUSED_LABEL_COLOR
  115. }
  116. if (scene.readyControlSequences.includes(sequence)) {
  117. return READY_LABEL_COLOR
  118. }
  119. if (scene.completedControlSequences.includes(sequence)) {
  120. return SCORE_COMPLETED_LABEL_COLOR
  121. }
  122. if (scene.skippedControlSequences.includes(sequence)) {
  123. return SCORE_SKIPPED_LABEL_COLOR
  124. }
  125. return SCORE_LABEL_COLOR
  126. }
  127. clearCanvas(ctx: any): void {
  128. ctx.setTransform(1, 0, 0, 1, 0, 0)
  129. ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
  130. }
  131. applyPreviewTransform(ctx: any, scene: MapScene): void {
  132. const previewScale = scene.previewScale || 1
  133. const previewOriginX = scene.previewOriginX || scene.viewportWidth / 2
  134. const previewOriginY = scene.previewOriginY || scene.viewportHeight / 2
  135. const translateX = (previewOriginX - previewOriginX * previewScale) * this.dpr
  136. const translateY = (previewOriginY - previewOriginY * previewScale) * this.dpr
  137. ctx.setTransform(
  138. this.dpr * previewScale,
  139. 0,
  140. 0,
  141. this.dpr * previewScale,
  142. translateX,
  143. translateY,
  144. )
  145. }
  146. getMetric(scene: MapScene, meters: number): number {
  147. return meters * this.getPixelsPerMeter(scene)
  148. }
  149. getPixelsPerMeter(scene: MapScene): number {
  150. const tileSizePx = scene.viewportWidth / scene.visibleColumns
  151. const centerLat = this.worldTileYToLat(scene.exactCenterWorldY, scene.zoom)
  152. const metersPerTile = Math.cos(centerLat * Math.PI / 180) * EARTH_CIRCUMFERENCE_METERS / Math.pow(2, scene.zoom)
  153. if (!tileSizePx || !metersPerTile) {
  154. return 0
  155. }
  156. return tileSizePx / metersPerTile
  157. }
  158. worldTileYToLat(worldY: number, zoom: number): number {
  159. const scale = Math.pow(2, zoom)
  160. const latRad = Math.atan(Math.sinh(Math.PI * (1 - 2 * worldY / scale)))
  161. return latRad * 180 / Math.PI
  162. }
  163. }