Files
cmr-mini/miniprogram/engine/renderer/courseLabelRenderer.ts

184 lines
5.7 KiB
TypeScript

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
}
if (scene.skippedControlSequences.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
}
if (scene.skippedControlSequences.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
}
}