Add score-o mode and split game map HUD presentation
This commit is contained in:
@@ -55,6 +55,8 @@ const AUTO_ROTATE_HEADING_SMOOTHING = 0.32
|
||||
const COMPASS_NEEDLE_SMOOTHING = 0.12
|
||||
const GPS_TRACK_MAX_POINTS = 200
|
||||
const GPS_TRACK_MIN_STEP_METERS = 3
|
||||
const MAP_TAP_MOVE_THRESHOLD_PX = 14
|
||||
const MAP_TAP_DURATION_MS = 280
|
||||
|
||||
type TouchPoint = WechatMiniprogram.TouchDetail
|
||||
|
||||
@@ -124,8 +126,11 @@ export interface MapEngineViewState {
|
||||
heartRateStatusText: string
|
||||
heartRateDeviceText: string
|
||||
gameSessionStatus: 'idle' | 'running' | 'finished' | 'failed'
|
||||
gameModeText: string
|
||||
panelTimerText: string
|
||||
panelMileageText: string
|
||||
panelActionTagText: string
|
||||
panelDistanceTagText: string
|
||||
panelDistanceValueText: string
|
||||
panelDistanceUnitText: string
|
||||
panelProgressText: string
|
||||
@@ -209,8 +214,11 @@ const VIEW_SYNC_KEYS: Array<keyof MapEngineViewState> = [
|
||||
'heartRateStatusText',
|
||||
'heartRateDeviceText',
|
||||
'gameSessionStatus',
|
||||
'gameModeText',
|
||||
'panelTimerText',
|
||||
'panelMileageText',
|
||||
'panelActionTagText',
|
||||
'panelDistanceTagText',
|
||||
'panelDistanceValueText',
|
||||
'panelDistanceUnitText',
|
||||
'panelProgressText',
|
||||
@@ -492,6 +500,9 @@ export class MapEngine {
|
||||
panLastX: number
|
||||
panLastY: number
|
||||
panLastTimestamp: number
|
||||
tapStartX: number
|
||||
tapStartY: number
|
||||
tapStartAt: number
|
||||
panVelocityX: number
|
||||
panVelocityY: number
|
||||
pinchStartDistance: number
|
||||
@@ -531,7 +542,7 @@ export class MapEngine {
|
||||
gameRuntime: GameRuntime
|
||||
telemetryRuntime: TelemetryRuntime
|
||||
gamePresentation: GamePresentationState
|
||||
gameMode: 'classic-sequential'
|
||||
gameMode: 'classic-sequential' | 'score-o'
|
||||
punchPolicy: 'enter' | 'enter-confirm'
|
||||
punchRadiusMeters: number
|
||||
autoFinishOnLastControl: boolean
|
||||
@@ -711,6 +722,8 @@ export class MapEngine {
|
||||
heartRateDeviceText: '--',
|
||||
panelTimerText: '00:00:00',
|
||||
panelMileageText: '0m',
|
||||
panelActionTagText: '目标',
|
||||
panelDistanceTagText: '点距',
|
||||
panelDistanceValueText: '--',
|
||||
panelDistanceUnitText: '',
|
||||
panelProgressText: '0/0',
|
||||
@@ -728,6 +741,7 @@ export class MapEngine {
|
||||
panelAccuracyUnitText: '',
|
||||
punchButtonText: '打点',
|
||||
gameSessionStatus: 'idle',
|
||||
gameModeText: '顺序赛',
|
||||
punchButtonEnabled: false,
|
||||
punchHintText: '等待进入检查点范围',
|
||||
punchFeedbackVisible: false,
|
||||
@@ -754,6 +768,9 @@ export class MapEngine {
|
||||
this.panLastX = 0
|
||||
this.panLastY = 0
|
||||
this.panLastTimestamp = 0
|
||||
this.tapStartX = 0
|
||||
this.tapStartY = 0
|
||||
this.tapStartAt = 0
|
||||
this.panVelocityX = 0
|
||||
this.panVelocityY = 0
|
||||
this.pinchStartDistance = 0
|
||||
@@ -812,6 +829,14 @@ export class MapEngine {
|
||||
this.setCourseHeading(null)
|
||||
}
|
||||
|
||||
getHudTargetControlId(): string | null {
|
||||
return this.gamePresentation.hud.hudTargetControlId
|
||||
}
|
||||
|
||||
getGameModeText(): string {
|
||||
return this.gameMode === 'score-o' ? '积分赛' : '顺序赛'
|
||||
}
|
||||
|
||||
loadGameDefinitionFromCourse(): GameEffect[] {
|
||||
if (!this.courseData) {
|
||||
this.clearGameRuntime()
|
||||
@@ -828,20 +853,23 @@ export class MapEngine {
|
||||
)
|
||||
const result = this.gameRuntime.loadDefinition(definition)
|
||||
this.telemetryRuntime.loadDefinition(definition)
|
||||
this.telemetryRuntime.syncGameState(this.gameRuntime.definition, result.nextState)
|
||||
this.gamePresentation = result.presentation
|
||||
this.telemetryRuntime.syncGameState(this.gameRuntime.definition, result.nextState, this.getHudTargetControlId())
|
||||
this.refreshCourseHeadingFromPresentation()
|
||||
this.updateSessionTimerLoop()
|
||||
this.setState({
|
||||
gameModeText: this.getGameModeText(),
|
||||
})
|
||||
return result.effects
|
||||
}
|
||||
|
||||
refreshCourseHeadingFromPresentation(): void {
|
||||
if (!this.courseData || !this.gamePresentation.activeLegIndices.length) {
|
||||
if (!this.courseData || !this.gamePresentation.map.activeLegIndices.length) {
|
||||
this.setCourseHeading(null)
|
||||
return
|
||||
}
|
||||
|
||||
const activeLegIndex = this.gamePresentation.activeLegIndices[0]
|
||||
const activeLegIndex = this.gamePresentation.map.activeLegIndices[0]
|
||||
const activeLeg = this.courseData.layers.legs[activeLegIndex]
|
||||
if (!activeLeg) {
|
||||
this.setCourseHeading(null)
|
||||
@@ -876,8 +904,11 @@ export class MapEngine {
|
||||
const telemetryPresentation = this.telemetryRuntime.getPresentation()
|
||||
const patch: Partial<MapEngineViewState> = {
|
||||
gameSessionStatus: this.gameRuntime.state ? this.gameRuntime.state.status : 'idle',
|
||||
gameModeText: this.getGameModeText(),
|
||||
panelTimerText: telemetryPresentation.timerText,
|
||||
panelMileageText: telemetryPresentation.mileageText,
|
||||
panelActionTagText: this.gamePresentation.hud.actionTagText,
|
||||
panelDistanceTagText: this.gamePresentation.hud.distanceTagText,
|
||||
panelDistanceValueText: telemetryPresentation.distanceToTargetValueText,
|
||||
panelDistanceUnitText: telemetryPresentation.distanceToTargetUnitText,
|
||||
panelSpeedValueText: telemetryPresentation.speedText,
|
||||
@@ -892,10 +923,10 @@ export class MapEngine {
|
||||
panelAverageSpeedUnitText: telemetryPresentation.averageSpeedUnitText,
|
||||
panelAccuracyValueText: telemetryPresentation.accuracyValueText,
|
||||
panelAccuracyUnitText: telemetryPresentation.accuracyUnitText,
|
||||
panelProgressText: this.gamePresentation.progressText,
|
||||
punchButtonText: this.gamePresentation.punchButtonText,
|
||||
punchButtonEnabled: this.gamePresentation.punchButtonEnabled,
|
||||
punchHintText: this.gamePresentation.punchHintText,
|
||||
panelProgressText: this.gamePresentation.hud.progressText,
|
||||
punchButtonText: this.gamePresentation.hud.punchButtonText,
|
||||
punchButtonEnabled: this.gamePresentation.hud.punchButtonEnabled,
|
||||
punchHintText: this.gamePresentation.hud.punchHintText,
|
||||
}
|
||||
|
||||
if (statusText) {
|
||||
@@ -945,6 +976,8 @@ export class MapEngine {
|
||||
this.setState({
|
||||
panelTimerText: telemetryPresentation.timerText,
|
||||
panelMileageText: telemetryPresentation.mileageText,
|
||||
panelActionTagText: this.gamePresentation.hud.actionTagText,
|
||||
panelDistanceTagText: this.gamePresentation.hud.distanceTagText,
|
||||
panelDistanceValueText: telemetryPresentation.distanceToTargetValueText,
|
||||
panelDistanceUnitText: telemetryPresentation.distanceToTargetUnitText,
|
||||
panelSpeedValueText: telemetryPresentation.speedText,
|
||||
@@ -1099,7 +1132,7 @@ export class MapEngine {
|
||||
|
||||
applyGameEffects(effects: GameEffect[]): string | null {
|
||||
this.feedbackDirector.handleEffects(effects)
|
||||
this.telemetryRuntime.syncGameState(this.gameRuntime.definition, this.gameRuntime.state)
|
||||
this.telemetryRuntime.syncGameState(this.gameRuntime.definition, this.gameRuntime.state, this.getHudTargetControlId())
|
||||
this.updateSessionTimerLoop()
|
||||
return this.resolveGameStatusText(effects)
|
||||
}
|
||||
@@ -1239,6 +1272,22 @@ export class MapEngine {
|
||||
this.locationController.start()
|
||||
}
|
||||
|
||||
handleSetGameMode(nextMode: 'classic-sequential' | 'score-o'): void {
|
||||
if (this.gameMode === nextMode) {
|
||||
return
|
||||
}
|
||||
|
||||
this.gameMode = nextMode
|
||||
const effects = this.loadGameDefinitionFromCourse()
|
||||
const modeText = this.getGameModeText()
|
||||
const statusText = this.applyGameEffects(effects) || `已切换到${modeText} (${this.buildVersion})`
|
||||
this.setState({
|
||||
...this.getGameViewPatch(statusText),
|
||||
gameModeText: modeText,
|
||||
}, true)
|
||||
this.syncRenderer()
|
||||
}
|
||||
|
||||
handleConnectHeartRate(): void {
|
||||
this.heartRateController.startScanAndConnect()
|
||||
}
|
||||
@@ -1392,6 +1441,9 @@ export class MapEngine {
|
||||
this.panLastX = event.touches[0].pageX
|
||||
this.panLastY = event.touches[0].pageY
|
||||
this.panLastTimestamp = event.timeStamp || Date.now()
|
||||
this.tapStartX = event.touches[0].pageX
|
||||
this.tapStartY = event.touches[0].pageY
|
||||
this.tapStartAt = event.timeStamp || Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1463,6 +1515,14 @@ export class MapEngine {
|
||||
}
|
||||
|
||||
handleTouchEnd(event: WechatMiniprogram.TouchEvent): void {
|
||||
const changedTouch = event.changedTouches && event.changedTouches.length ? event.changedTouches[0] : null
|
||||
const endedAsTap = changedTouch
|
||||
&& this.gestureMode === 'pan'
|
||||
&& event.touches.length === 0
|
||||
&& Math.abs(changedTouch.pageX - this.tapStartX) <= MAP_TAP_MOVE_THRESHOLD_PX
|
||||
&& Math.abs(changedTouch.pageY - this.tapStartY) <= MAP_TAP_MOVE_THRESHOLD_PX
|
||||
&& ((event.timeStamp || Date.now()) - this.tapStartAt) <= MAP_TAP_DURATION_MS
|
||||
|
||||
if (this.gestureMode === 'pinch' && event.touches.length < 2) {
|
||||
const gestureScale = this.previewScale || 1
|
||||
const zoomDelta = Math.round(Math.log2(gestureScale))
|
||||
@@ -1509,6 +1569,10 @@ export class MapEngine {
|
||||
return
|
||||
}
|
||||
|
||||
if (endedAsTap && changedTouch) {
|
||||
this.handleMapTap(changedTouch.pageX - this.state.stageLeft, changedTouch.pageY - this.state.stageTop)
|
||||
}
|
||||
|
||||
this.gestureMode = 'idle'
|
||||
this.resetPinchState()
|
||||
this.renderer.setAnimationPaused(false)
|
||||
@@ -1526,6 +1590,80 @@ export class MapEngine {
|
||||
this.scheduleAutoRotate()
|
||||
}
|
||||
|
||||
handleMapTap(stageX: number, stageY: number): void {
|
||||
if (!this.gameRuntime.definition || !this.gameRuntime.state || this.gameRuntime.definition.mode !== 'score-o') {
|
||||
return
|
||||
}
|
||||
|
||||
const focusedControlId = this.findFocusableControlAt(stageX, stageY)
|
||||
if (focusedControlId === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
const gameResult = this.gameRuntime.dispatch({
|
||||
type: 'control_focused',
|
||||
at: Date.now(),
|
||||
controlId: focusedControlId,
|
||||
})
|
||||
this.gamePresentation = gameResult.presentation
|
||||
this.telemetryRuntime.syncGameState(this.gameRuntime.definition, this.gameRuntime.state, this.getHudTargetControlId())
|
||||
this.setState({
|
||||
...this.getGameViewPatch(focusedControlId ? `已选择目标点 (${this.buildVersion})` : `已取消目标点选择 (${this.buildVersion})`),
|
||||
}, true)
|
||||
this.syncRenderer()
|
||||
}
|
||||
|
||||
findFocusableControlAt(stageX: number, stageY: number): string | null | undefined {
|
||||
if (!this.gameRuntime.definition || !this.courseData || !this.state.stageWidth || !this.state.stageHeight) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const focusableControls = this.gameRuntime.definition.controls.filter((control) => (
|
||||
this.gamePresentation.map.focusableControlIds.includes(control.id)
|
||||
))
|
||||
|
||||
let matchedControlId: string | null | undefined
|
||||
let matchedDistance = Number.POSITIVE_INFINITY
|
||||
const hitRadiusPx = Math.max(28, this.getControlHitRadiusPx())
|
||||
|
||||
for (const control of focusableControls) {
|
||||
const screenPoint = this.getControlScreenPoint(control.id)
|
||||
if (!screenPoint) {
|
||||
continue
|
||||
}
|
||||
|
||||
const distancePx = Math.sqrt(
|
||||
Math.pow(screenPoint.x - stageX, 2)
|
||||
+ Math.pow(screenPoint.y - stageY, 2),
|
||||
)
|
||||
if (distancePx <= hitRadiusPx && distancePx < matchedDistance) {
|
||||
matchedDistance = distancePx
|
||||
matchedControlId = control.id
|
||||
}
|
||||
}
|
||||
|
||||
if (matchedControlId === undefined) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return matchedControlId === this.gamePresentation.map.focusedControlId ? null : matchedControlId
|
||||
}
|
||||
|
||||
getControlHitRadiusPx(): number {
|
||||
if (!this.state.tileSizePx) {
|
||||
return 28
|
||||
}
|
||||
|
||||
const centerLonLat = worldTileToLonLat({ x: this.state.centerTileX + 0.5, y: this.state.centerTileY + 0.5 }, this.state.zoom)
|
||||
const metersPerTile = Math.cos(centerLonLat.lat * Math.PI / 180) * 40075016.686 / Math.pow(2, this.state.zoom)
|
||||
if (!metersPerTile) {
|
||||
return 28
|
||||
}
|
||||
|
||||
const pixelsPerMeter = this.state.tileSizePx / metersPerTile
|
||||
return Math.max(28, this.cpRadiusMeters * pixelsPerMeter * 1.6)
|
||||
}
|
||||
|
||||
handleRecenter(): void {
|
||||
this.clearInertiaTimer()
|
||||
this.clearPreviewResetTimer()
|
||||
@@ -2054,15 +2192,22 @@ export class MapEngine {
|
||||
gpsCalibrationOrigin: worldTileToLonLat({ x: this.defaultCenterTileX, y: this.defaultCenterTileY }, this.defaultZoom),
|
||||
course: this.courseData,
|
||||
cpRadiusMeters: this.cpRadiusMeters,
|
||||
activeControlSequences: this.gamePresentation.activeControlSequences,
|
||||
activeStart: this.gamePresentation.activeStart,
|
||||
completedStart: this.gamePresentation.completedStart,
|
||||
activeFinish: this.gamePresentation.activeFinish,
|
||||
completedFinish: this.gamePresentation.completedFinish,
|
||||
revealFullCourse: this.gamePresentation.revealFullCourse,
|
||||
activeLegIndices: this.gamePresentation.activeLegIndices,
|
||||
completedLegIndices: this.gamePresentation.completedLegIndices,
|
||||
completedControlSequences: this.gamePresentation.completedControlSequences,
|
||||
controlVisualMode: this.gamePresentation.map.controlVisualMode,
|
||||
showCourseLegs: this.gamePresentation.map.showCourseLegs,
|
||||
guidanceLegAnimationEnabled: this.gamePresentation.map.guidanceLegAnimationEnabled,
|
||||
focusableControlIds: this.gamePresentation.map.focusableControlIds,
|
||||
focusedControlId: this.gamePresentation.map.focusedControlId,
|
||||
focusedControlSequences: this.gamePresentation.map.focusedControlSequences,
|
||||
activeControlSequences: this.gamePresentation.map.activeControlSequences,
|
||||
activeStart: this.gamePresentation.map.activeStart,
|
||||
completedStart: this.gamePresentation.map.completedStart,
|
||||
activeFinish: this.gamePresentation.map.activeFinish,
|
||||
focusedFinish: this.gamePresentation.map.focusedFinish,
|
||||
completedFinish: this.gamePresentation.map.completedFinish,
|
||||
revealFullCourse: this.gamePresentation.map.revealFullCourse,
|
||||
activeLegIndices: this.gamePresentation.map.activeLegIndices,
|
||||
completedLegIndices: this.gamePresentation.map.completedLegIndices,
|
||||
completedControlSequences: this.gamePresentation.map.completedControlSequences,
|
||||
osmReferenceEnabled: this.state.osmReferenceEnabled,
|
||||
overlayOpacity: MAP_OVERLAY_OPACITY,
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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[]
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user