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

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

@@ -1,6 +1,6 @@
import { getTileSizePx, screenToWorld, worldToScreen, type CameraState } from '../camera/camera'
import { AccelerometerController } from '../sensor/accelerometerController'
import { CompassHeadingController } from '../sensor/compassHeadingController'
import { CompassHeadingController, type CompassTuningProfile } from '../sensor/compassHeadingController'
import { DeviceMotionController } from '../sensor/deviceMotionController'
import { GyroscopeController } from '../sensor/gyroscopeController'
import { type HeartRateDiscoveredDevice } from '../sensor/heartRateController'
@@ -11,6 +11,7 @@ import { type MapRendererStats } from '../renderer/mapRenderer'
import { lonLatToWorldTile, worldTileToLonLat, type LonLatPoint, type MapCalibration } from '../../utils/projection'
import { type OrienteeringCourseData } from '../../utils/orienteeringCourse'
import { isTileWithinBounds, type RemoteMapConfig, type TileZoomBounds } from '../../utils/remoteMapConfig'
import { formatAnimationLevelText, resolveAnimationLevel, type AnimationLevel } from '../../utils/animationLevel'
import { GameRuntime } from '../../game/core/gameRuntime'
import { type GameEffect, type GameResult } from '../../game/core/gameResult'
import { buildGameDefinitionFromCourse } from '../../game/content/courseToGameDefinition'
@@ -56,8 +57,27 @@ const AUTO_ROTATE_SNAP_DEG = 0.1
const AUTO_ROTATE_DEADZONE_DEG = 4
const AUTO_ROTATE_MAX_STEP_DEG = 0.75
const AUTO_ROTATE_HEADING_SMOOTHING = 0.46
const COMPASS_NEEDLE_MIN_SMOOTHING = 0.24
const COMPASS_NEEDLE_MAX_SMOOTHING = 0.56
const COMPASS_TUNING_PRESETS: Record<CompassTuningProfile, {
needleMinSmoothing: number
needleMaxSmoothing: number
displayDeadzoneDeg: number
}> = {
smooth: {
needleMinSmoothing: 0.16,
needleMaxSmoothing: 0.4,
displayDeadzoneDeg: 0.75,
},
balanced: {
needleMinSmoothing: 0.22,
needleMaxSmoothing: 0.52,
displayDeadzoneDeg: 0.45,
},
responsive: {
needleMinSmoothing: 0.3,
needleMaxSmoothing: 0.68,
displayDeadzoneDeg: 0.2,
},
}
const SMART_HEADING_BLEND_START_SPEED_KMH = 1.2
const SMART_HEADING_MOVEMENT_SPEED_KMH = 3.0
const SMART_HEADING_MIN_DISTANCE_METERS = 12
@@ -88,6 +108,7 @@ export interface MapEngineStageRect {
}
export interface MapEngineViewState {
animationLevel: AnimationLevel
buildVersion: string
renderMode: string
projectionMode: string
@@ -110,7 +131,11 @@ export interface MapEngineViewState {
accelerometerText: string
gyroscopeText: string
deviceMotionText: string
compassSourceText: string
compassTuningProfile: CompassTuningProfile
compassTuningProfileText: string
compassDeclinationText: string
northReferenceMode: NorthReferenceMode
northReferenceButtonText: string
autoRotateSourceText: string
autoRotateCalibrationText: string
@@ -199,6 +224,8 @@ export interface MapEngineViewState {
contentCardTitle: string
contentCardBody: string
punchButtonFxClass: string
panelProgressFxClass: string
panelDistanceFxClass: string
punchFeedbackFxClass: string
contentCardFxClass: string
mapPulseVisible: boolean
@@ -228,6 +255,7 @@ export interface MapEngineGameInfoSnapshot {
}
const VIEW_SYNC_KEYS: Array<keyof MapEngineViewState> = [
'animationLevel',
'buildVersion',
'renderMode',
'projectionMode',
@@ -252,7 +280,11 @@ const VIEW_SYNC_KEYS: Array<keyof MapEngineViewState> = [
'accelerometerText',
'gyroscopeText',
'deviceMotionText',
'compassSourceText',
'compassTuningProfile',
'compassTuningProfileText',
'compassDeclinationText',
'northReferenceMode',
'northReferenceButtonText',
'autoRotateSourceText',
'autoRotateCalibrationText',
@@ -330,6 +362,8 @@ const VIEW_SYNC_KEYS: Array<keyof MapEngineViewState> = [
'contentCardTitle',
'contentCardBody',
'punchButtonFxClass',
'panelProgressFxClass',
'panelDistanceFxClass',
'punchFeedbackFxClass',
'contentCardFxClass',
'mapPulseVisible',
@@ -342,6 +376,38 @@ const VIEW_SYNC_KEYS: Array<keyof MapEngineViewState> = [
'osmReferenceText',
]
const INTERACTION_DEFERRED_VIEW_KEYS = new Set<keyof MapEngineViewState>([
'rotationText',
'sensorHeadingText',
'deviceHeadingText',
'devicePoseText',
'headingConfidenceText',
'accelerometerText',
'gyroscopeText',
'deviceMotionText',
'compassSourceText',
'compassTuningProfile',
'compassTuningProfileText',
'compassDeclinationText',
'autoRotateSourceText',
'autoRotateCalibrationText',
'northReferenceText',
'centerText',
'gpsCoordText',
'visibleTileCount',
'readyTileCount',
'memoryTileCount',
'diskTileCount',
'memoryHitCount',
'diskHitCount',
'networkFetchCount',
'cacheHitRateText',
'heartRateDiscoveredDevices',
'mockCoordText',
'mockSpeedText',
'mockHeartRateText',
])
function buildCenterText(zoom: number, x: number, y: number): string {
return `z${zoom} / x${x} / y${y}`
}
@@ -387,18 +453,23 @@ function interpolateAngleDeg(currentDeg: number, targetDeg: number, factor: numb
return normalizeRotationDeg(currentDeg + normalizeAngleDeltaDeg(targetDeg - currentDeg) * factor)
}
function getCompassNeedleSmoothingFactor(currentDeg: number, targetDeg: number): number {
function getCompassNeedleSmoothingFactor(
currentDeg: number,
targetDeg: number,
profile: CompassTuningProfile,
): number {
const preset = COMPASS_TUNING_PRESETS[profile]
const deltaDeg = Math.abs(normalizeAngleDeltaDeg(targetDeg - currentDeg))
if (deltaDeg <= 4) {
return COMPASS_NEEDLE_MIN_SMOOTHING
return preset.needleMinSmoothing
}
if (deltaDeg >= 36) {
return COMPASS_NEEDLE_MAX_SMOOTHING
return preset.needleMaxSmoothing
}
const progress = (deltaDeg - 4) / (36 - 4)
return COMPASS_NEEDLE_MIN_SMOOTHING
+ (COMPASS_NEEDLE_MAX_SMOOTHING - COMPASS_NEEDLE_MIN_SMOOTHING) * progress
return preset.needleMinSmoothing
+ (preset.needleMaxSmoothing - preset.needleMinSmoothing) * progress
}
function getMovementHeadingSmoothingFactor(speedKmh: number | null): number {
@@ -434,7 +505,7 @@ function formatRotationText(rotationDeg: number): string {
}
function normalizeDegreeDisplayText(text: string): string {
return text.replace(/[°掳•]/g, '˚')
return text.replace(/[掳•˚]/g, '°')
}
function formatHeadingText(headingDeg: number | null): string {
@@ -442,7 +513,7 @@ function formatHeadingText(headingDeg: number | null): string {
return '--'
}
return `${Math.round(normalizeRotationDeg(headingDeg))}˚`
return `${Math.round(normalizeRotationDeg(headingDeg))}°`
}
function formatDevicePoseText(pose: 'upright' | 'tilted' | 'flat'): string {
@@ -494,9 +565,9 @@ function formatDeviceMotionText(motion: { alpha: number | null; beta: number | n
return '--'
}
const alphaDeg = motion.alpha === null ? '--' : Math.round(normalizeRotationDeg(360 - motion.alpha * 180 / Math.PI))
const betaDeg = motion.beta === null ? '--' : Math.round(motion.beta * 180 / Math.PI)
const gammaDeg = motion.gamma === null ? '--' : Math.round(motion.gamma * 180 / Math.PI)
const alphaDeg = motion.alpha === null ? '--' : Math.round(normalizeRotationDeg(360 - motion.alpha))
const betaDeg = motion.beta === null ? '--' : Math.round(motion.beta)
const gammaDeg = motion.gamma === null ? '--' : Math.round(motion.gamma)
return `a:${alphaDeg} b:${betaDeg} g:${gammaDeg}`
}
@@ -620,6 +691,26 @@ function formatCompassDeclinationText(mode: NorthReferenceMode): string {
return ''
}
function formatCompassSourceText(source: 'compass' | 'motion' | null): string {
if (source === 'compass') {
return '罗盘'
}
if (source === 'motion') {
return '设备方向兜底'
}
return '无数据'
}
function formatCompassTuningProfileText(profile: CompassTuningProfile): string {
if (profile === 'smooth') {
return '顺滑'
}
if (profile === 'responsive') {
return '跟手'
}
return '平衡'
}
function formatNorthReferenceButtonText(mode: NorthReferenceMode): string {
return mode === 'magnetic' ? '北参照:磁北' : '北参照:真北'
}
@@ -702,6 +793,7 @@ function getInitialBearingDeg(from: LonLatPoint, to: LonLatPoint): number {
export class MapEngine {
buildVersion: string
animationLevel: AnimationLevel
renderer: WebGLMapRenderer
accelerometerController: AccelerometerController
compassController: CompassHeadingController
@@ -742,6 +834,8 @@ export class MapEngine {
sensorHeadingDeg: number | null
smoothedSensorHeadingDeg: number | null
compassDisplayHeadingDeg: number | null
compassSource: 'compass' | 'motion' | null
compassTuningProfile: CompassTuningProfile
smoothedMovementHeadingDeg: number | null
autoRotateHeadingDeg: number | null
courseHeadingDeg: number | null
@@ -789,6 +883,8 @@ export class MapEngine {
constructor(buildVersion: string, callbacks: MapEngineCallbacks) {
this.buildVersion = buildVersion
this.animationLevel = resolveAnimationLevel(wx.getSystemInfoSync())
this.compassTuningProfile = 'balanced'
this.onData = callbacks.onData
this.accelerometerErrorText = null
this.renderer = new WebGLMapRenderer(
@@ -812,7 +908,7 @@ export class MapEngine {
z,
})
if (this.diagnosticUiEnabled) {
this.setState(this.getTelemetrySensorViewPatch(), true)
this.setState(this.getTelemetrySensorViewPatch())
}
},
onError: (message) => {
@@ -821,7 +917,7 @@ export class MapEngine {
this.setState({
...this.getTelemetrySensorViewPatch(),
statusText: `加速度计启动失败 (${this.buildVersion})`,
}, true)
})
}
},
})
@@ -833,6 +929,7 @@ export class MapEngine {
this.handleCompassError(message)
},
})
this.compassController.setTuningProfile(this.compassTuningProfile)
this.gyroscopeController = new GyroscopeController({
onSample: (x, y, z) => {
this.telemetryRuntime.dispatch({
@@ -843,12 +940,12 @@ export class MapEngine {
z,
})
if (this.diagnosticUiEnabled) {
this.setState(this.getTelemetrySensorViewPatch(), true)
this.setState(this.getTelemetrySensorViewPatch())
}
},
onError: () => {
if (this.diagnosticUiEnabled) {
this.setState(this.getTelemetrySensorViewPatch(), true)
this.setState(this.getTelemetrySensorViewPatch())
}
},
})
@@ -865,16 +962,12 @@ export class MapEngine {
this.setState({
...this.getTelemetrySensorViewPatch(),
autoRotateSourceText: this.getAutoRotateSourceText(),
}, true)
}
if (this.state.orientationMode === 'heading-up' && this.refreshAutoRotateTarget()) {
this.scheduleAutoRotate()
})
}
},
onError: () => {
if (this.diagnosticUiEnabled) {
this.setState(this.getTelemetrySensorViewPatch(), true)
this.setState(this.getTelemetrySensorViewPatch())
}
},
})
@@ -899,7 +992,7 @@ export class MapEngine {
},
onDebugStateChange: () => {
if (this.diagnosticUiEnabled) {
this.setState(this.getLocationControllerViewPatch(), true)
this.setState(this.getLocationControllerViewPatch())
}
},
})
@@ -963,12 +1056,12 @@ export class MapEngine {
heartRateDiscoveredDevices: this.formatHeartRateDevices(devices),
heartRateScanText: this.getHeartRateScanText(),
...this.getHeartRateControllerViewPatch(),
}, true)
})
}
},
onDebugStateChange: () => {
if (this.diagnosticUiEnabled) {
this.setState(this.getHeartRateControllerViewPatch(), true)
this.setState(this.getHeartRateControllerViewPatch())
}
},
})
@@ -982,6 +1075,12 @@ export class MapEngine {
setPunchButtonFxClass: (className) => {
this.setPunchButtonFxClass(className)
},
setHudProgressFxClass: (className) => {
this.setHudProgressFxClass(className)
},
setHudDistanceFxClass: (className) => {
this.setHudDistanceFxClass(className)
},
showMapPulse: (controlId, motionClass) => {
this.showMapPulse(controlId, motionClass)
},
@@ -994,6 +1093,7 @@ export class MapEngine {
}
},
})
this.feedbackDirector.setAnimationLevel(this.animationLevel)
this.minZoom = MIN_ZOOM
this.maxZoom = MAX_ZOOM
this.defaultZoom = DEFAULT_ZOOM
@@ -1032,6 +1132,7 @@ export class MapEngine {
this.sessionTimerInterval = 0
this.hasGpsCenteredOnce = false
this.state = {
animationLevel: this.animationLevel,
buildVersion: this.buildVersion,
renderMode: RENDER_MODE,
projectionMode: PROJECTION_MODE,
@@ -1051,10 +1152,14 @@ export class MapEngine {
deviceHeadingText: '--',
devicePoseText: '竖持',
headingConfidenceText: '低',
accelerometerText: '未启用',
accelerometerText: '未启用',
gyroscopeText: '--',
deviceMotionText: '--',
compassSourceText: '无数据',
compassTuningProfile: this.compassTuningProfile,
compassTuningProfileText: formatCompassTuningProfileText(this.compassTuningProfile),
compassDeclinationText: formatCompassDeclinationText(DEFAULT_NORTH_REFERENCE_MODE),
northReferenceMode: DEFAULT_NORTH_REFERENCE_MODE,
northReferenceButtonText: formatNorthReferenceButtonText(DEFAULT_NORTH_REFERENCE_MODE),
autoRotateSourceText: formatAutoRotateSourceText('smart', false),
autoRotateCalibrationText: formatAutoRotateCalibrationText(false, getMapNorthOffsetDeg(DEFAULT_NORTH_REFERENCE_MODE)),
@@ -1137,6 +1242,8 @@ export class MapEngine {
contentCardTitle: '',
contentCardBody: '',
punchButtonFxClass: '',
panelProgressFxClass: '',
panelDistanceFxClass: '',
punchFeedbackFxClass: '',
contentCardFxClass: '',
mapPulseVisible: false,
@@ -1177,6 +1284,8 @@ export class MapEngine {
this.sensorHeadingDeg = null
this.smoothedSensorHeadingDeg = null
this.compassDisplayHeadingDeg = null
this.compassSource = null
this.compassTuningProfile = 'balanced'
this.smoothedMovementHeadingDeg = null
this.autoRotateHeadingDeg = null
this.courseHeadingDeg = null
@@ -1241,6 +1350,7 @@ export class MapEngine {
{ label: '配置版本', value: this.configVersion || '--' },
{ label: 'Schema版本', value: this.configSchemaVersion || '--' },
{ label: '活动ID', value: this.configAppId || '--' },
{ label: '动画等级', value: formatAnimationLevelText(this.state.animationLevel) },
{ label: '地图', value: this.state.mapName || '--' },
{ label: '模式', value: this.getGameModeText() },
{ label: '状态', value: formatGameSessionStatusText(this.state.gameSessionStatus) },
@@ -1417,20 +1527,23 @@ export class MapEngine {
getTelemetrySensorViewPatch(): Partial<MapEngineViewState> {
const telemetryState = this.telemetryRuntime.state
return {
deviceHeadingText: formatHeadingText(
telemetryState.deviceHeadingDeg === null
? null
: getCompassReferenceHeadingDeg(this.northReferenceMode, telemetryState.deviceHeadingDeg),
),
devicePoseText: formatDevicePoseText(telemetryState.devicePose),
headingConfidenceText: formatHeadingConfidenceText(telemetryState.headingConfidence),
accelerometerText: telemetryState.accelerometer
? `#${telemetryState.accelerometerSampleCount} ${formatClockTime(telemetryState.accelerometerUpdatedAt)} x:${telemetryState.accelerometer.x.toFixed(3)} y:${telemetryState.accelerometer.y.toFixed(3)} z:${telemetryState.accelerometer.z.toFixed(3)}`
: '未启用',
gyroscopeText: formatGyroscopeText(telemetryState.gyroscope),
deviceMotionText: formatDeviceMotionText(telemetryState.deviceMotion),
}
return {
deviceHeadingText: formatHeadingText(
telemetryState.deviceHeadingDeg === null
? null
: getCompassReferenceHeadingDeg(this.northReferenceMode, telemetryState.deviceHeadingDeg),
),
devicePoseText: formatDevicePoseText(telemetryState.devicePose),
headingConfidenceText: formatHeadingConfidenceText(telemetryState.headingConfidence),
accelerometerText: telemetryState.accelerometer
? `#${telemetryState.accelerometerSampleCount} ${formatClockTime(telemetryState.accelerometerUpdatedAt)} x:${telemetryState.accelerometer.x.toFixed(3)} y:${telemetryState.accelerometer.y.toFixed(3)} z:${telemetryState.accelerometer.z.toFixed(3)}`
: '未启用',
gyroscopeText: formatGyroscopeText(telemetryState.gyroscope),
deviceMotionText: formatDeviceMotionText(telemetryState.deviceMotion),
compassSourceText: formatCompassSourceText(this.compassSource),
compassTuningProfile: this.compassTuningProfile,
compassTuningProfileText: formatCompassTuningProfileText(this.compassTuningProfile),
}
}
getGameModeText(): string {
@@ -1589,6 +1702,8 @@ export class MapEngine {
stageFxVisible: false,
stageFxClass: '',
punchButtonFxClass: '',
panelProgressFxClass: '',
panelDistanceFxClass: '',
}, true)
}
@@ -1675,6 +1790,18 @@ export class MapEngine {
}, true)
}
setHudProgressFxClass(className: string): void {
this.setState({
panelProgressFxClass: className,
}, true)
}
setHudDistanceFxClass(className: string): void {
this.setState({
panelDistanceFxClass: className,
}, true)
}
showMapPulse(controlId: string, motionClass = ''): void {
const screenPoint = this.getControlScreenPoint(controlId)
if (!screenPoint) {
@@ -1761,6 +1888,9 @@ export class MapEngine {
applyGameEffects(effects: GameEffect[]): string | null {
this.feedbackDirector.handleEffects(effects)
if (effects.some((effect) => effect.type === 'session_finished')) {
if (this.locationController.listening) {
this.locationController.stop()
}
this.setState({
gpsTracking: false,
gpsTrackingText: '测试结束,定位已停止',
@@ -1845,12 +1975,17 @@ export class MapEngine {
handleForceExitGame(): void {
this.feedbackDirector.reset()
if (this.locationController.listening) {
this.locationController.stop()
}
if (!this.courseData) {
this.clearGameRuntime()
this.resetTransientGameUiState()
this.feedbackDirector.handleEffects([{ type: 'session_cancelled' }])
this.setState({
gpsTracking: false,
gpsTrackingText: '已退出对局,定位已停止',
...this.getGameViewPatch(`已退出当前对局 (${this.buildVersion})`),
}, true)
this.syncRenderer()
@@ -1861,6 +1996,8 @@ export class MapEngine {
this.resetTransientGameUiState()
this.feedbackDirector.handleEffects([{ type: 'session_cancelled' }])
this.setState({
gpsTracking: false,
gpsTrackingText: '已退出对局,定位已停止',
...this.getGameViewPatch(`已退出当前对局 (${this.buildVersion})`),
}, true)
this.syncRenderer()
@@ -1946,7 +2083,7 @@ export class MapEngine {
gpsLockEnabled: this.gpsLockEnabled,
gpsLockAvailable: gpsInsideMap,
...this.getGameViewPatch(gameStatusText || (gpsInsideMap ? `GPS位置已更新 (${this.buildVersion})` : `GPS位置超出当前地图范围 (${this.buildVersion})`)),
}, true)
})
this.syncRenderer()
}
@@ -2100,7 +2237,7 @@ export class MapEngine {
this.setState({
heartRateDeviceText: this.heartRateController.currentDeviceName || '--',
heartRateScanText: this.getHeartRateScanText(),
}, true)
})
}
handleDebugHeartRateTone(tone: HeartRateTone): void {
@@ -2112,7 +2249,7 @@ export class MapEngine {
})
this.setState({
heartRateStatusText: `调试心率: ${sampleBpm} bpm / ${tone.toUpperCase()}`,
}, true)
})
this.syncSessionTimerText()
}
@@ -2128,7 +2265,7 @@ export class MapEngine {
: (this.heartRateController.sourceMode === 'mock' ? '模拟心率源未连接' : '心率带未连接'),
heartRateScanText: this.getHeartRateScanText(),
...this.getHeartRateControllerViewPatch(),
}, true)
})
this.syncSessionTimerText()
}
@@ -2250,7 +2387,7 @@ export class MapEngine {
configStatusText: `配置已载入 / ${config.configTitle} / ${config.courseStatusText}`,
projectionMode: config.projectionModeText,
tileSource: config.tileSource,
sensorHeadingText: formatHeadingText(this.smoothedSensorHeadingDeg === null ? null : getCompassReferenceHeadingDeg(this.northReferenceMode, this.smoothedSensorHeadingDeg)),
sensorHeadingText: formatHeadingText(this.compassDisplayHeadingDeg),
compassDeclinationText: formatCompassDeclinationText(this.northReferenceMode),
northReferenceButtonText: formatNorthReferenceButtonText(this.northReferenceMode),
northReferenceText: formatNorthReferenceText(this.northReferenceMode),
@@ -2308,7 +2445,7 @@ export class MapEngine {
this.pinchAnchorWorldY = anchorWorld.y
this.setPreviewState(this.pinchStartScale, origin.x, origin.y)
this.syncRenderer()
this.compassController.start()
this.compassController.start()
return
}
@@ -2567,7 +2704,7 @@ export class MapEngine {
() => {
this.resetPreviewState()
this.syncRenderer()
this.compassController.start()
this.compassController.start()
this.scheduleAutoRotate()
},
)
@@ -2601,7 +2738,7 @@ export class MapEngine {
() => {
this.resetPreviewState()
this.syncRenderer()
this.compassController.start()
this.compassController.start()
},
)
}
@@ -2638,7 +2775,7 @@ export class MapEngine {
() => {
this.resetPreviewState()
this.syncRenderer()
this.compassController.start()
this.compassController.start()
},
)
}
@@ -2673,6 +2810,38 @@ export class MapEngine {
this.cycleNorthReferenceMode()
}
handleSetNorthReferenceMode(mode: NorthReferenceMode): void {
this.setNorthReferenceMode(mode)
}
handleSetAnimationLevel(level: AnimationLevel): void {
if (this.animationLevel === level) {
return
}
this.animationLevel = level
this.feedbackDirector.setAnimationLevel(level)
this.setState({
animationLevel: level,
statusText: `动画性能已切换为${formatAnimationLevelText(level)} (${this.buildVersion})`,
})
this.syncRenderer()
}
handleSetCompassTuningProfile(profile: CompassTuningProfile): void {
if (this.compassTuningProfile === profile) {
return
}
this.compassTuningProfile = profile
this.compassController.setTuningProfile(profile)
this.setState({
compassTuningProfile: profile,
compassTuningProfileText: formatCompassTuningProfileText(profile),
statusText: `指北针响应已切换为${formatCompassTuningProfileText(profile)} (${this.buildVersion})`,
}, true)
}
handleAutoRotateCalibrate(): void {
if (this.state.orientationMode !== 'heading-up') {
this.setState({
@@ -2761,30 +2930,40 @@ export class MapEngine {
}
}
handleCompassHeading(headingDeg: number): void {
applyHeadingSample(headingDeg: number, source: 'compass' | 'motion'): void {
this.compassSource = source
this.sensorHeadingDeg = normalizeRotationDeg(headingDeg)
this.smoothedSensorHeadingDeg = this.smoothedSensorHeadingDeg === null
? this.sensorHeadingDeg
: interpolateAngleDeg(this.smoothedSensorHeadingDeg, this.sensorHeadingDeg, AUTO_ROTATE_HEADING_SMOOTHING)
const compassHeadingDeg = getCompassReferenceHeadingDeg(this.northReferenceMode, this.smoothedSensorHeadingDeg)
this.compassDisplayHeadingDeg = this.compassDisplayHeadingDeg === null
? compassHeadingDeg
: interpolateAngleDeg(
this.compassDisplayHeadingDeg,
compassHeadingDeg,
getCompassNeedleSmoothingFactor(this.compassDisplayHeadingDeg, compassHeadingDeg),
)
if (this.compassDisplayHeadingDeg === null) {
this.compassDisplayHeadingDeg = compassHeadingDeg
} else {
const displayDeltaDeg = Math.abs(normalizeAngleDeltaDeg(compassHeadingDeg - this.compassDisplayHeadingDeg))
if (displayDeltaDeg >= COMPASS_TUNING_PRESETS[this.compassTuningProfile].displayDeadzoneDeg) {
this.compassDisplayHeadingDeg = interpolateAngleDeg(
this.compassDisplayHeadingDeg,
compassHeadingDeg,
getCompassNeedleSmoothingFactor(
this.compassDisplayHeadingDeg,
compassHeadingDeg,
this.compassTuningProfile,
),
)
}
}
this.autoRotateHeadingDeg = this.resolveAutoRotateInputHeadingDeg()
this.setState({
compassNeedleDeg: formatCompassNeedleDegForMode(this.northReferenceMode, this.compassDisplayHeadingDeg),
sensorHeadingText: formatHeadingText(this.compassDisplayHeadingDeg),
compassDeclinationText: formatCompassDeclinationText(this.northReferenceMode),
...(this.diagnosticUiEnabled
? {
sensorHeadingText: formatHeadingText(compassHeadingDeg),
...this.getTelemetrySensorViewPatch(),
compassDeclinationText: formatCompassDeclinationText(this.northReferenceMode),
northReferenceButtonText: formatNorthReferenceButtonText(this.northReferenceMode),
autoRotateSourceText: this.getAutoRotateSourceText(),
northReferenceText: formatNorthReferenceText(this.northReferenceMode),
@@ -2801,18 +2980,31 @@ export class MapEngine {
}
}
handleCompassHeading(headingDeg: number): void {
this.applyHeadingSample(headingDeg, 'compass')
}
handleCompassError(message: string): void {
this.clearAutoRotateTimer()
this.targetAutoRotationDeg = null
this.autoRotateCalibrationPending = false
this.compassSource = null
this.setState({
compassSourceText: formatCompassSourceText(null),
autoRotateCalibrationText: formatAutoRotateCalibrationText(false, this.autoRotateCalibrationOffsetDeg),
statusText: `${message} (${this.buildVersion})`,
}, true)
}
cycleNorthReferenceMode(): void {
const nextMode = getNextNorthReferenceMode(this.northReferenceMode)
this.setNorthReferenceMode(getNextNorthReferenceMode(this.northReferenceMode))
}
setNorthReferenceMode(nextMode: NorthReferenceMode): void {
if (nextMode === this.northReferenceMode) {
return
}
const nextMapNorthOffsetDeg = getMapNorthOffsetDeg(nextMode)
const compassHeadingDeg = this.smoothedSensorHeadingDeg === null
? null
@@ -2831,9 +3023,10 @@ export class MapEngine {
rotationDeg: MAP_NORTH_OFFSET_DEG,
rotationText: formatRotationText(MAP_NORTH_OFFSET_DEG),
northReferenceText: formatNorthReferenceText(nextMode),
sensorHeadingText: formatHeadingText(compassHeadingDeg),
sensorHeadingText: formatHeadingText(this.compassDisplayHeadingDeg),
...this.getTelemetrySensorViewPatch(),
compassDeclinationText: formatCompassDeclinationText(nextMode),
northReferenceMode: nextMode,
northReferenceButtonText: formatNorthReferenceButtonText(nextMode),
compassNeedleDeg: formatCompassNeedleDegForMode(nextMode, this.compassDisplayHeadingDeg),
autoRotateCalibrationText: formatAutoRotateCalibrationText(false, nextMapNorthOffsetDeg),
@@ -2850,9 +3043,10 @@ export class MapEngine {
this.setState({
northReferenceText: formatNorthReferenceText(nextMode),
sensorHeadingText: formatHeadingText(compassHeadingDeg),
sensorHeadingText: formatHeadingText(this.compassDisplayHeadingDeg),
...this.getTelemetrySensorViewPatch(),
compassDeclinationText: formatCompassDeclinationText(nextMode),
northReferenceMode: nextMode,
northReferenceButtonText: formatNorthReferenceButtonText(nextMode),
compassNeedleDeg: formatCompassNeedleDegForMode(nextMode, this.compassDisplayHeadingDeg),
autoRotateCalibrationText: formatAutoRotateCalibrationText(false, nextMapNorthOffsetDeg),
@@ -3167,6 +3361,7 @@ export class MapEngine {
buildScene() {
const exactCenter = this.getExactCenterFromTranslate(this.state.tileTranslateX, this.state.tileTranslateY)
const readyControlSequences = this.resolveReadyControlSequences()
return {
tileSource: this.state.tileSource,
osmTileSource: OSM_TILE_SOURCE,
@@ -3183,6 +3378,7 @@ export class MapEngine {
translateX: this.state.tileTranslateX,
translateY: this.state.tileTranslateY,
rotationRad: this.getRotationRad(this.state.rotationDeg),
animationLevel: this.state.animationLevel,
previewScale: this.previewScale || 1,
previewOriginX: this.previewOriginX || this.state.stageWidth / 2,
previewOriginY: this.previewOriginY || this.state.stageHeight / 2,
@@ -3199,6 +3395,7 @@ export class MapEngine {
focusedControlId: this.gamePresentation.map.focusedControlId,
focusedControlSequences: this.gamePresentation.map.focusedControlSequences,
activeControlSequences: this.gamePresentation.map.activeControlSequences,
readyControlSequences,
activeStart: this.gamePresentation.map.activeStart,
completedStart: this.gamePresentation.map.completedStart,
activeFinish: this.gamePresentation.map.activeFinish,
@@ -3215,6 +3412,21 @@ export class MapEngine {
}
}
resolveReadyControlSequences(): number[] {
const punchableControlId = this.gamePresentation.hud.punchableControlId
const definition = this.gameRuntime.definition
if (!punchableControlId || !definition) {
return []
}
const control = definition.controls.find((item) => item.id === punchableControlId)
if (!control || control.sequence === null) {
return []
}
return [control.sequence]
}
syncRenderer(): void {
if (!this.mounted || !this.state.stageWidth || !this.state.stageHeight) {
return
@@ -3374,8 +3586,32 @@ export class MapEngine {
}
const patch = this.pendingViewPatch
this.pendingViewPatch = {}
this.onData(patch)
const shouldDeferForInteraction = this.gestureMode !== 'idle' || !!this.inertiaTimer || !!this.previewResetTimer
const nextPendingPatch = {} as Partial<MapEngineViewState>
const outputPatch = {} as Partial<MapEngineViewState>
for (const [key, value] of Object.entries(patch) as Array<[keyof MapEngineViewState, MapEngineViewState[keyof MapEngineViewState]]>) {
if (shouldDeferForInteraction && INTERACTION_DEFERRED_VIEW_KEYS.has(key)) {
;(nextPendingPatch as Record<string, unknown>)[key] = value
continue
}
;(outputPatch as Record<string, unknown>)[key] = value
}
this.pendingViewPatch = nextPendingPatch
if (Object.keys(this.pendingViewPatch).length && !this.viewSyncTimer) {
this.viewSyncTimer = setTimeout(() => {
this.viewSyncTimer = 0
this.flushViewPatch()
}, UI_SYNC_INTERVAL_MS) as unknown as number
}
if (!Object.keys(outputPatch).length) {
return
}
this.onData(outputPatch)
}
getTouchDistance(touches: TouchPoint[]): number {
@@ -3431,7 +3667,7 @@ export class MapEngine {
if (Math.abs(startScale - 1) < 0.01) {
this.resetPreviewState()
this.syncRenderer()
this.compassController.start()
this.compassController.start()
this.scheduleAutoRotate()
return
}
@@ -3443,12 +3679,12 @@ export class MapEngine {
const nextScale = startScale + (1 - startScale) * eased
this.setPreviewState(nextScale, originX, originY)
this.syncRenderer()
this.compassController.start()
this.compassController.start()
if (progress >= 1) {
this.resetPreviewState()
this.syncRenderer()
this.compassController.start()
this.compassController.start()
this.previewResetTimer = 0
this.scheduleAutoRotate()
return
@@ -3467,7 +3703,7 @@ export class MapEngine {
tileTranslateY: translateY,
})
this.syncRenderer()
this.compassController.start()
this.compassController.start()
return
}
@@ -3530,7 +3766,7 @@ export class MapEngine {
() => {
this.setPreviewState(residualScale, stageX, stageY)
this.syncRenderer()
this.compassController.start()
this.compassController.start()
this.animatePreviewToRest()
},
)
@@ -3557,7 +3793,7 @@ export class MapEngine {
() => {
this.setPreviewState(residualScale, stageX, stageY)
this.syncRenderer()
this.compassController.start()
this.compassController.start()
this.animatePreviewToRest()
},
)

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

View File

@@ -5,7 +5,13 @@ export interface CompassHeadingControllerCallbacks {
type SensorSource = 'compass' | 'motion' | null
const ABSOLUTE_HEADING_CORRECTION = 0.44
export type CompassTuningProfile = 'smooth' | 'balanced' | 'responsive'
const HEADING_CORRECTION_BY_PROFILE: Record<CompassTuningProfile, number> = {
smooth: 0.3,
balanced: 0.4,
responsive: 0.54,
}
function normalizeHeadingDeg(headingDeg: number): number {
const normalized = headingDeg % 360
@@ -41,6 +47,7 @@ export class CompassHeadingController {
rollDeg: number | null
motionReady: boolean
compassReady: boolean
tuningProfile: CompassTuningProfile
constructor(callbacks: CompassHeadingControllerCallbacks) {
this.callbacks = callbacks
@@ -53,6 +60,7 @@ export class CompassHeadingController {
this.rollDeg = null
this.motionReady = false
this.compassReady = false
this.tuningProfile = 'balanced'
}
start(): void {
@@ -99,6 +107,10 @@ export class CompassHeadingController {
this.stop()
}
setTuningProfile(profile: CompassTuningProfile): void {
this.tuningProfile = profile
}
startMotionSource(previousMessage: string): void {
if (typeof wx.startDeviceMotionListening !== 'function' || typeof wx.onDeviceMotionChange !== 'function') {
this.callbacks.onError(previousMessage)
@@ -111,14 +123,13 @@ export class CompassHeadingController {
}
this.pitchDeg = typeof result.beta === 'number' && !Number.isNaN(result.beta)
? result.beta * 180 / Math.PI
? result.beta
: null
this.rollDeg = typeof result.gamma === 'number' && !Number.isNaN(result.gamma)
? result.gamma * 180 / Math.PI
? result.gamma
: null
const alphaDeg = result.alpha * 180 / Math.PI
this.applyAbsoluteHeading(normalizeHeadingDeg(360 - alphaDeg), 'motion')
this.applyAbsoluteHeading(normalizeHeadingDeg(360 - result.alpha), 'motion')
}
this.motionCallback = callback
@@ -163,10 +174,11 @@ export class CompassHeadingController {
}
applyAbsoluteHeading(headingDeg: number, source: 'compass' | 'motion'): void {
const headingCorrection = HEADING_CORRECTION_BY_PROFILE[this.tuningProfile]
if (this.absoluteHeadingDeg === null) {
this.absoluteHeadingDeg = headingDeg
} else {
this.absoluteHeadingDeg = interpolateHeadingDeg(this.absoluteHeadingDeg, headingDeg, ABSOLUTE_HEADING_CORRECTION)
this.absoluteHeadingDeg = interpolateHeadingDeg(this.absoluteHeadingDeg, headingDeg, headingCorrection)
}
this.source = source
@@ -200,5 +212,3 @@ export class CompassHeadingController {
this.compassCallback = null
}
}