Add score-o mode and split game map HUD presentation

This commit is contained in:
2026-03-24 12:27:45 +08:00
parent a117a25824
commit 0295893b56
18 changed files with 1121 additions and 113 deletions

View File

@@ -5,9 +5,15 @@ 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
@@ -58,30 +64,51 @@ export class CourseLabelRenderer {
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()
ctx.textAlign = 'left'
ctx.textBaseline = 'middle'
ctx.font = `700 ${fontSizePx}px sans-serif`
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.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()
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 ACTIVE_LABEL_COLOR
return scene.controlVisualMode === 'multi-target' ? MULTI_ACTIVE_LABEL_COLOR : ACTIVE_LABEL_COLOR
}
if (scene.completedControlSequences.includes(sequence)) {
@@ -91,6 +118,18 @@ export class CourseLabelRenderer {
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)

View File

@@ -29,10 +29,17 @@ export interface MapScene {
gpsCalibrationOrigin: LonLatPoint
course: OrienteeringCourseData | null
cpRadiusMeters: number
controlVisualMode: 'single-target' | 'multi-target'
showCourseLegs: boolean
guidanceLegAnimationEnabled: boolean
focusableControlIds: string[]
focusedControlId: string | null
focusedControlSequences: number[]
activeControlSequences: number[]
activeStart: boolean
completedStart: boolean
activeFinish: boolean
focusedFinish: boolean
completedFinish: boolean
revealFullCourse: boolean
activeLegIndices: number[]

View File

@@ -8,6 +8,10 @@ import { GpsLayer } from '../layer/gpsLayer'
const COURSE_COLOR: [number, number, number, number] = [0.8, 0.0, 0.42, 0.96]
const COMPLETED_ROUTE_COLOR: [number, number, number, number] = [0.48, 0.5, 0.54, 0.82]
const ACTIVE_CONTROL_COLOR: [number, number, number, number] = [0.22, 1, 0.95, 1]
const MULTI_ACTIVE_CONTROL_COLOR: [number, number, number, number] = [1, 0.8, 0.2, 0.98]
const FOCUSED_CONTROL_COLOR: [number, number, number, number] = [0.98, 0.96, 0.98, 1]
const MULTI_ACTIVE_PULSE_COLOR: [number, number, number, number] = [0.18, 1, 0.96, 0.86]
const FOCUSED_PULSE_COLOR: [number, number, number, number] = [1, 0.36, 0.84, 0.88]
const ACTIVE_LEG_COLOR: [number, number, number, number] = [0.18, 1, 0.94, 0.5]
const EARTH_CIRCUMFERENCE_METERS = 40075016.686
const CONTROL_RING_WIDTH_RATIO = 0.2
@@ -231,19 +235,19 @@ export class WebGLVectorRenderer {
): void {
const controlRadiusMeters = this.getControlRadiusMeters(scene)
if (scene.revealFullCourse) {
for (let index = 0; index < course.legs.length; index += 1) {
const leg = course.legs[index]
this.pushCourseLeg(positions, colors, leg, controlRadiusMeters, this.getLegColor(scene, index), scene)
if (scene.activeLegIndices.includes(index)) {
this.pushCourseLegHighlight(positions, colors, leg, controlRadiusMeters, scene)
if (scene.revealFullCourse && scene.showCourseLegs) {
for (let index = 0; index < course.legs.length; index += 1) {
const leg = course.legs[index]
this.pushCourseLeg(positions, colors, leg, controlRadiusMeters, this.getLegColor(scene, index), scene)
if (scene.guidanceLegAnimationEnabled && scene.activeLegIndices.includes(index)) {
this.pushCourseLegHighlight(positions, colors, leg, controlRadiusMeters, scene)
}
}
}
const guideLeg = this.getGuideLeg(course, scene)
if (guideLeg) {
this.pushGuidanceFlow(positions, colors, guideLeg, controlRadiusMeters, scene, pulseFrame)
}
const guideLeg = this.getGuideLeg(course, scene)
if (guideLeg) {
this.pushGuidanceFlow(positions, colors, guideLeg, controlRadiusMeters, scene, pulseFrame)
}
}
for (const start of course.starts) {
@@ -258,7 +262,12 @@ export class WebGLVectorRenderer {
for (const control of course.controls) {
if (scene.activeControlSequences.includes(control.sequence)) {
this.pushActiveControlPulse(positions, colors, control.point.x, control.point.y, controlRadiusMeters, scene, pulseFrame)
if (scene.controlVisualMode === 'single-target') {
this.pushActiveControlPulse(positions, colors, control.point.x, control.point.y, controlRadiusMeters, scene, pulseFrame)
} else {
this.pushActiveControlPulse(positions, colors, control.point.x, control.point.y, controlRadiusMeters, scene, pulseFrame, MULTI_ACTIVE_PULSE_COLOR)
this.pushActiveControlPulse(positions, colors, control.point.x, control.point.y, controlRadiusMeters * 1.2, scene, pulseFrame + 9, [0.9, 1, 1, 0.52])
}
}
this.pushRing(
@@ -271,12 +280,31 @@ export class WebGLVectorRenderer {
this.getControlColor(scene, control.sequence),
scene,
)
if (scene.focusedControlSequences.includes(control.sequence)) {
this.pushActiveControlPulse(positions, colors, control.point.x, control.point.y, controlRadiusMeters * 1.02, scene, pulseFrame, FOCUSED_PULSE_COLOR)
this.pushActiveControlPulse(positions, colors, control.point.x, control.point.y, controlRadiusMeters * 1.32, scene, pulseFrame + 15, [1, 0.86, 0.94, 0.5])
this.pushRing(
positions,
colors,
control.point.x,
control.point.y,
this.getMetric(scene, controlRadiusMeters * 1.24),
this.getMetric(scene, controlRadiusMeters * 1.06),
FOCUSED_CONTROL_COLOR,
scene,
)
}
}
for (const finish of course.finishes) {
if (scene.activeFinish) {
this.pushActiveControlPulse(positions, colors, finish.point.x, finish.point.y, controlRadiusMeters, scene, pulseFrame)
}
if (scene.focusedFinish) {
this.pushActiveControlPulse(positions, colors, finish.point.x, finish.point.y, controlRadiusMeters * 1.04, scene, pulseFrame, FOCUSED_PULSE_COLOR)
this.pushActiveControlPulse(positions, colors, finish.point.x, finish.point.y, controlRadiusMeters * 1.34, scene, pulseFrame + 12, [1, 0.86, 0.94, 0.46])
}
const finishColor = this.getFinishColor(scene)
this.pushRing(
@@ -303,6 +331,10 @@ export class WebGLVectorRenderer {
}
getGuideLeg(course: ProjectedCourseLayers, scene: MapScene): ProjectedCourseLeg | null {
if (!scene.guidanceLegAnimationEnabled) {
return null
}
const activeIndex = scene.activeLegIndices.length ? scene.activeLegIndices[0] : -1
if (activeIndex >= 0 && activeIndex < course.legs.length) {
return course.legs[activeIndex]
@@ -366,12 +398,14 @@ export class WebGLVectorRenderer {
controlRadiusMeters: number,
scene: MapScene,
pulseFrame: number,
pulseColor?: RgbaColor,
): void {
const pulse = (Math.sin(pulseFrame * ACTIVE_CONTROL_PULSE_SPEED) + 1) / 2
const pulseScale = ACTIVE_CONTROL_PULSE_MIN_SCALE + (ACTIVE_CONTROL_PULSE_MAX_SCALE - ACTIVE_CONTROL_PULSE_MIN_SCALE) * pulse
const pulseWidthScale = pulseScale - ACTIVE_CONTROL_PULSE_WIDTH_RATIO
const glowAlpha = 0.24 + pulse * 0.34
const glowColor: RgbaColor = [0.36, 1, 0.96, glowAlpha]
const baseColor = pulseColor || ACTIVE_CONTROL_COLOR
const glowAlpha = Math.min(1, baseColor[3] * (0.46 + pulse * 0.5))
const glowColor: RgbaColor = [baseColor[0], baseColor[1], baseColor[2], glowAlpha]
this.pushRing(
positions,
@@ -430,7 +464,7 @@ export class WebGLVectorRenderer {
getControlColor(scene: MapScene, sequence: number): RgbaColor {
if (scene.activeControlSequences.includes(sequence)) {
return ACTIVE_CONTROL_COLOR
return scene.controlVisualMode === 'multi-target' ? MULTI_ACTIVE_CONTROL_COLOR : ACTIVE_CONTROL_COLOR
}
if (scene.completedControlSequences.includes(sequence)) {
@@ -442,6 +476,10 @@ export class WebGLVectorRenderer {
getFinishColor(scene: MapScene): RgbaColor {
if (scene.focusedFinish) {
return FOCUSED_CONTROL_COLOR
}
if (scene.activeFinish) {
return ACTIVE_CONTROL_COLOR
}