完善地图交互、动画与罗盘调试

This commit is contained in:
2026-03-26 16:58:53 +08:00
parent d695308a55
commit 5fc996dea1
18 changed files with 1566 additions and 165 deletions

View File

@@ -9,11 +9,14 @@ 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 READY_LABEL_COLOR = 'rgba(98, 255, 214, 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 SKIPPED_LABEL_COLOR = 'rgba(152, 156, 162, 0.88)'
const SCORE_LABEL_COLOR = 'rgba(255, 252, 242, 0.98)'
const SCORE_COMPLETED_LABEL_COLOR = 'rgba(214, 218, 224, 0.94)'
const SCORE_SKIPPED_LABEL_COLOR = 'rgba(176, 182, 188, 0.9)'
export class CourseLabelRenderer {
courseLayer: CourseLayer
@@ -107,6 +110,10 @@ export class CourseLabelRenderer {
return FOCUSED_LABEL_COLOR
}
if (scene.readyControlSequences.includes(sequence)) {
return READY_LABEL_COLOR
}
if (scene.activeControlSequences.includes(sequence)) {
return scene.controlVisualMode === 'multi-target' ? MULTI_ACTIVE_LABEL_COLOR : ACTIVE_LABEL_COLOR
}
@@ -116,7 +123,7 @@ export class CourseLabelRenderer {
}
if (scene.skippedControlSequences.includes(sequence)) {
return COMPLETED_LABEL_COLOR
return SKIPPED_LABEL_COLOR
}
return DEFAULT_LABEL_COLOR
@@ -127,12 +134,16 @@ export class CourseLabelRenderer {
return FOCUSED_LABEL_COLOR
}
if (scene.readyControlSequences.includes(sequence)) {
return READY_LABEL_COLOR
}
if (scene.completedControlSequences.includes(sequence)) {
return SCORE_COMPLETED_LABEL_COLOR
}
if (scene.skippedControlSequences.includes(sequence)) {
return SCORE_COMPLETED_LABEL_COLOR
return SCORE_SKIPPED_LABEL_COLOR
}
return SCORE_LABEL_COLOR

View File

@@ -3,6 +3,7 @@ import { type TileStoreStats } from '../tile/tileStore'
import { type LonLatPoint, type MapCalibration } from '../../utils/projection'
import { type TileZoomBounds } from '../../utils/remoteMapConfig'
import { type OrienteeringCourseData } from '../../utils/orienteeringCourse'
import { type AnimationLevel } from '../../utils/animationLevel'
export interface MapScene {
tileSource: string
@@ -20,6 +21,7 @@ export interface MapScene {
translateX: number
translateY: number
rotationRad: number
animationLevel: AnimationLevel
previewScale: number
previewOriginX: number
previewOriginY: number
@@ -36,6 +38,7 @@ export interface MapScene {
focusedControlId: string | null
focusedControlSequences: number[]
activeControlSequences: number[]
readyControlSequences: number[]
activeStart: boolean
completedStart: boolean
activeFinish: boolean

View File

@@ -135,12 +135,16 @@ export class WebGLMapRenderer implements MapRenderer {
this.scheduleRender()
}
this.animationTimer = setTimeout(tick, ANIMATION_FRAME_MS) as unknown as number
this.animationTimer = setTimeout(tick, this.getAnimationFrameMs()) as unknown as number
}
tick()
}
getAnimationFrameMs(): number {
return this.scene && this.scene.animationLevel === 'lite' ? 48 : ANIMATION_FRAME_MS
}
scheduleRender(): void {
if (this.renderTimer || !this.scene || this.destroyed) {
return

View File

@@ -7,11 +7,17 @@ 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 SKIPPED_ROUTE_COLOR: [number, number, number, number] = [0.38, 0.4, 0.44, 0.72]
const ACTIVE_CONTROL_COLOR: [number, number, number, number] = [0.22, 1, 0.95, 1]
const READY_CONTROL_COLOR: [number, number, number, number] = [0.38, 1, 0.92, 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 READY_PULSE_COLOR: [number, number, number, number] = [0.44, 1, 0.92, 0.98]
const COMPLETED_SETTLE_COLOR: [number, number, number, number] = [0.86, 0.9, 0.94, 0.24]
const SKIPPED_SETTLE_COLOR: [number, number, number, number] = [0.72, 0.76, 0.82, 0.18]
const SKIPPED_SLASH_COLOR: [number, number, number, number] = [0.78, 0.82, 0.88, 0.9]
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
@@ -196,6 +202,18 @@ export class WebGLVectorRenderer {
gl.drawArrays(gl.TRIANGLES, 0, positions.length / 2)
}
isLite(scene: MapScene): boolean {
return scene.animationLevel === 'lite'
}
getRingSegments(scene: MapScene): number {
return this.isLite(scene) ? 24 : 36
}
getCircleSegments(scene: MapScene): number {
return this.isLite(scene) ? 14 : 20
}
getPixelsPerMeter(scene: MapScene): number {
const camera: CameraState = {
centerWorldX: scene.exactCenterWorldX,
@@ -249,6 +267,18 @@ export class WebGLVectorRenderer {
if (scene.activeStart) {
this.pushActiveStartPulse(positions, colors, start.point.x, start.point.y, start.headingDeg, controlRadiusMeters, scene, pulseFrame)
}
if (scene.completedStart) {
this.pushRing(
positions,
colors,
start.point.x,
start.point.y,
this.getMetric(scene, controlRadiusMeters * 1.16),
this.getMetric(scene, controlRadiusMeters * 1.02),
COMPLETED_SETTLE_COLOR,
scene,
)
}
this.pushStartTriangle(positions, colors, start.point.x, start.point.y, start.headingDeg, controlRadiusMeters, this.getStartColor(scene), scene)
}
if (!scene.revealFullCourse) {
@@ -261,10 +291,29 @@ export class WebGLVectorRenderer {
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])
if (!this.isLite(scene)) {
this.pushActiveControlPulse(positions, colors, control.point.x, control.point.y, controlRadiusMeters * 1.2, scene, pulseFrame + 9, [0.9, 1, 1, 0.52])
}
}
}
if (scene.readyControlSequences.includes(control.sequence)) {
this.pushActiveControlPulse(positions, colors, control.point.x, control.point.y, controlRadiusMeters * 1.04, scene, pulseFrame, READY_PULSE_COLOR)
if (!this.isLite(scene)) {
this.pushActiveControlPulse(positions, colors, control.point.x, control.point.y, controlRadiusMeters * 1.22, scene, pulseFrame + 11, [0.92, 1, 1, 0.42])
}
this.pushRing(
positions,
colors,
control.point.x,
control.point.y,
this.getMetric(scene, controlRadiusMeters * 1.16),
this.getMetric(scene, controlRadiusMeters * 1.02),
READY_CONTROL_COLOR,
scene,
)
}
this.pushRing(
positions,
colors,
@@ -278,7 +327,9 @@ export class WebGLVectorRenderer {
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])
if (!this.isLite(scene)) {
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,
@@ -290,6 +341,33 @@ export class WebGLVectorRenderer {
scene,
)
}
if (scene.completedControlSequences.includes(control.sequence)) {
this.pushRing(
positions,
colors,
control.point.x,
control.point.y,
this.getMetric(scene, controlRadiusMeters * 1.14),
this.getMetric(scene, controlRadiusMeters * 1.02),
COMPLETED_SETTLE_COLOR,
scene,
)
}
if (this.isSkippedControl(scene, control.sequence)) {
this.pushRing(
positions,
colors,
control.point.x,
control.point.y,
this.getMetric(scene, controlRadiusMeters * 1.1),
this.getMetric(scene, controlRadiusMeters * 1.01),
SKIPPED_SETTLE_COLOR,
scene,
)
this.pushSkippedControlSlash(positions, colors, control.point.x, control.point.y, controlRadiusMeters, scene)
}
}
for (const finish of course.finishes) {
@@ -298,10 +376,24 @@ export class WebGLVectorRenderer {
}
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])
if (!this.isLite(scene)) {
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)
if (scene.completedFinish) {
this.pushRing(
positions,
colors,
finish.point.x,
finish.point.y,
this.getMetric(scene, controlRadiusMeters * 1.18),
this.getMetric(scene, controlRadiusMeters * 1.02),
COMPLETED_SETTLE_COLOR,
scene,
)
}
this.pushRing(
positions,
colors,
@@ -418,6 +510,27 @@ export class WebGLVectorRenderer {
)
}
pushSkippedControlSlash(
positions: number[],
colors: number[],
centerX: number,
centerY: number,
controlRadiusMeters: number,
scene: MapScene,
): void {
const slashRadius = this.getMetric(scene, controlRadiusMeters * 0.72)
const slashWidth = this.getMetric(scene, controlRadiusMeters * 0.08)
this.pushSegment(
positions,
colors,
{ x: centerX - slashRadius, y: centerY + slashRadius },
{ x: centerX + slashRadius, y: centerY - slashRadius },
slashWidth,
SKIPPED_SLASH_COLOR,
scene,
)
}
pushActiveStartPulse(
positions: number[],
colors: number[],
@@ -462,14 +575,22 @@ export class WebGLVectorRenderer {
}
getControlColor(scene: MapScene, sequence: number): RgbaColor {
if (scene.readyControlSequences.includes(sequence)) {
return READY_CONTROL_COLOR
}
if (scene.activeControlSequences.includes(sequence)) {
return scene.controlVisualMode === 'multi-target' ? MULTI_ACTIVE_CONTROL_COLOR : ACTIVE_CONTROL_COLOR
}
if (scene.completedControlSequences.includes(sequence) || this.isSkippedControl(scene, sequence)) {
if (scene.completedControlSequences.includes(sequence)) {
return COMPLETED_ROUTE_COLOR
}
if (this.isSkippedControl(scene, sequence)) {
return SKIPPED_ROUTE_COLOR
}
return COURSE_COLOR
}
@@ -633,7 +754,7 @@ export class WebGLVectorRenderer {
color: RgbaColor,
scene: MapScene,
): void {
const segments = 36
const segments = this.getRingSegments(scene)
for (let index = 0; index < segments; index += 1) {
const startAngle = index / segments * Math.PI * 2
const endAngle = (index + 1) / segments * Math.PI * 2
@@ -682,7 +803,7 @@ export class WebGLVectorRenderer {
color: RgbaColor,
scene: MapScene,
): void {
const segments = 20
const segments = this.getCircleSegments(scene)
const center = this.toClip(centerX, centerY, scene)
for (let index = 0; index < segments; index += 1) {
const startAngle = index / segments * Math.PI * 2