Add animated orienteering course overlays and labels
This commit is contained in:
120
miniprogram/engine/renderer/courseLabelRenderer.ts
Normal file
120
miniprogram/engine/renderer/courseLabelRenderer.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
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
|
||||
|
||||
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) {
|
||||
return
|
||||
}
|
||||
|
||||
const controlRadiusMeters = scene.cpRadiusMeters > 0 ? scene.cpRadiusMeters : 5
|
||||
const fontSizePx = this.getMetric(scene, controlRadiusMeters * LABEL_FONT_SIZE_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()
|
||||
ctx.fillStyle = 'rgba(204, 0, 107, 0.98)'
|
||||
ctx.textAlign = 'left'
|
||||
ctx.textBaseline = 'middle'
|
||||
ctx.font = `700 ${fontSizePx}px sans-serif`
|
||||
|
||||
for (const control of course.controls) {
|
||||
ctx.save()
|
||||
ctx.translate(control.point.x, control.point.y)
|
||||
ctx.rotate(scene.rotationRad)
|
||||
ctx.fillText(String(control.sequence), offsetX, offsetY)
|
||||
ctx.restore()
|
||||
}
|
||||
|
||||
ctx.restore()
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user