完善地图交互、动画与罗盘调试
This commit is contained in:
212
compass-debugging-notes.md
Normal file
212
compass-debugging-notes.md
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
# 罗盘问题排查记录
|
||||||
|
|
||||||
|
## 背景
|
||||||
|
|
||||||
|
本项目在微信小程序中使用罗盘驱动:
|
||||||
|
|
||||||
|
- 指北针针头
|
||||||
|
- 指北针顶部角度数字
|
||||||
|
- `heading-up` 自动转图
|
||||||
|
|
||||||
|
在一次围绕顶部提示窗、传感器显示链和性能优化的修改后,出现了以下问题:
|
||||||
|
|
||||||
|
- iOS 端偶发正常,偶发异常
|
||||||
|
- Android 端罗盘长期无样本
|
||||||
|
- 指北针不转
|
||||||
|
- `heading-up` 自动转图一起失效
|
||||||
|
|
||||||
|
## 最终结论
|
||||||
|
|
||||||
|
这次问题的主因不是算法本身,而是:
|
||||||
|
|
||||||
|
**Android 微信环境下,罗盘监听需要被持续保活;之前将多处看似冗余的 `compassController.start()` 清理掉后,Android 的罗盘样本链被破坏了。**
|
||||||
|
|
||||||
|
也就是说:
|
||||||
|
|
||||||
|
- iOS 对罗盘监听更宽容
|
||||||
|
- Android 对罗盘监听更脆弱
|
||||||
|
- 之前稳定,不是因为链路更“干净”,而是因为老代码里存在一条实际有效的“罗盘保活链”
|
||||||
|
|
||||||
|
## 现象总结
|
||||||
|
|
||||||
|
### 失效期
|
||||||
|
|
||||||
|
- Android 调试面板里 `Compass Source` 为 `无数据`
|
||||||
|
- iOS 仍可能有 `罗盘` 样本
|
||||||
|
- 若强行用 `DeviceMotion` 兜底,会出现:
|
||||||
|
- 指针会转
|
||||||
|
- 但方向不准
|
||||||
|
- 自动转图方向错误
|
||||||
|
|
||||||
|
### 恢复后
|
||||||
|
|
||||||
|
- Android `Compass Source` 恢复为 `罗盘`
|
||||||
|
- 指北针针头恢复
|
||||||
|
- 顶部角度数字恢复
|
||||||
|
- `heading-up` 恢复
|
||||||
|
|
||||||
|
## 误判过的方向
|
||||||
|
|
||||||
|
以下方向在本次排查中都被考虑过,但最终不是根因或不是主要根因:
|
||||||
|
|
||||||
|
### 1. `DeviceMotion` 兜底方案
|
||||||
|
|
||||||
|
问题:
|
||||||
|
|
||||||
|
- `DeviceMotion` 可以给出设备姿态角
|
||||||
|
- 但不能稳定代替“指向北”的绝对罗盘
|
||||||
|
- 用它兜底会导致:
|
||||||
|
- 能转
|
||||||
|
- 但方向明显不准
|
||||||
|
|
||||||
|
结论:
|
||||||
|
|
||||||
|
**`DeviceMotion` 不能作为正式指北针来源。**
|
||||||
|
|
||||||
|
### 2. 加速度计 / 其他传感器互斥
|
||||||
|
|
||||||
|
曾排查:
|
||||||
|
|
||||||
|
- `Accelerometer`
|
||||||
|
- `Gyroscope`
|
||||||
|
- `DeviceMotion`
|
||||||
|
- `Compass`
|
||||||
|
|
||||||
|
结论:
|
||||||
|
|
||||||
|
- 加速度计在当前微信 Android 环境下不稳定,已放弃
|
||||||
|
- 但这不是这次罗盘彻底失效的主因
|
||||||
|
|
||||||
|
### 3. 算法问题
|
||||||
|
|
||||||
|
曾尝试调整:
|
||||||
|
|
||||||
|
- 角度平滑
|
||||||
|
- 设备方向单位解释
|
||||||
|
- motion fallback 算法
|
||||||
|
|
||||||
|
结论:
|
||||||
|
|
||||||
|
这些会影响“顺不顺”、“准不准”,但**不能解释 Android 完全无罗盘样本**。
|
||||||
|
|
||||||
|
## 真正修复的方法
|
||||||
|
|
||||||
|
将之前被清理掉的多处 `this.compassController.start()` 恢复回去。
|
||||||
|
|
||||||
|
这些调用点主要分布在:
|
||||||
|
|
||||||
|
- `commitViewport(...)`
|
||||||
|
- `handleTouchStart(...)`
|
||||||
|
- `animatePreviewToRest(...)`
|
||||||
|
- `normalizeTranslate(...)`
|
||||||
|
- `zoomAroundPoint(...)`
|
||||||
|
- `handleRecenter(...)`
|
||||||
|
- `handleRotateStep(...)`
|
||||||
|
- `handleRotationReset(...)`
|
||||||
|
|
||||||
|
这些调用在代码审美上看起来像“重复启动”,但在 Android 微信环境里,它们实际上承担了:
|
||||||
|
|
||||||
|
**重新拉起 / 保活罗盘监听**
|
||||||
|
|
||||||
|
的作用。
|
||||||
|
|
||||||
|
## 当前工程判断
|
||||||
|
|
||||||
|
本项目当前应当采用以下原则:
|
||||||
|
|
||||||
|
### 1. 罗盘主来源只使用 `Compass`
|
||||||
|
|
||||||
|
不要再让:
|
||||||
|
|
||||||
|
- `DeviceMotion`
|
||||||
|
- 其它姿态角
|
||||||
|
|
||||||
|
参与正式指北针和自动转图的主方向链。
|
||||||
|
|
||||||
|
### 2. `DeviceMotion` 只保留为辅助或调试输入
|
||||||
|
|
||||||
|
可用于:
|
||||||
|
|
||||||
|
- 调试面板显示
|
||||||
|
- 设备姿态观察
|
||||||
|
- 未来原生端姿态融合参考
|
||||||
|
|
||||||
|
但不要直接驱动指北针。
|
||||||
|
|
||||||
|
### 3. Android 端罗盘需要保活
|
||||||
|
|
||||||
|
后续不要再把这些 `compassController.start()` 当成纯冗余逻辑随意清掉。
|
||||||
|
|
||||||
|
如果要优化代码,应该:
|
||||||
|
|
||||||
|
- 保留现有行为
|
||||||
|
- 将其收口为有明确语义的方法
|
||||||
|
|
||||||
|
例如:
|
||||||
|
|
||||||
|
- `ensureCompassAlive()`
|
||||||
|
- `refreshCompassBinding()`
|
||||||
|
|
||||||
|
而不是直接删掉。
|
||||||
|
|
||||||
|
## 与生命周期相关的硬约束
|
||||||
|
|
||||||
|
以下约束必须保持:
|
||||||
|
|
||||||
|
### 单实例
|
||||||
|
|
||||||
|
页面层必须保证任意时刻只有一个 `MapEngine` 活跃实例。
|
||||||
|
|
||||||
|
### 完整销毁
|
||||||
|
|
||||||
|
`MapEngine.destroy()` 中必须完整执行:
|
||||||
|
|
||||||
|
- `compassController.destroy()`
|
||||||
|
- 其它传感器 `destroy()`
|
||||||
|
|
||||||
|
防止旧监听残留。
|
||||||
|
|
||||||
|
### 调试状态不应影响罗盘主链
|
||||||
|
|
||||||
|
调试面板开关不应再控制:
|
||||||
|
|
||||||
|
- 罗盘是否启动
|
||||||
|
- 罗盘是否停止
|
||||||
|
|
||||||
|
否则容易再次引入平台差异问题。
|
||||||
|
|
||||||
|
## 推荐保留的调试字段
|
||||||
|
|
||||||
|
以下字段建议长期保留,便于后续定位:
|
||||||
|
|
||||||
|
- `Compass Source`
|
||||||
|
- `sensorHeadingText`
|
||||||
|
- 顶部角度数字
|
||||||
|
- `heading-up` 开关状态
|
||||||
|
|
||||||
|
其中 `Compass Source` 至少应显示:
|
||||||
|
|
||||||
|
- `罗盘`
|
||||||
|
- `无数据`
|
||||||
|
|
||||||
|
避免再次将问题误判为算法问题。
|
||||||
|
|
||||||
|
## 后续优化建议
|
||||||
|
|
||||||
|
如果后面要继续优化这段代码,推荐方向是:
|
||||||
|
|
||||||
|
### 可做
|
||||||
|
|
||||||
|
- 将分散的 `compassController.start()` 收口成命名明确的方法
|
||||||
|
- 为 Android 罗盘链补一层更可读的“保活机制”注释
|
||||||
|
- 保留当前稳定行为前提下做重构
|
||||||
|
|
||||||
|
### 不建议
|
||||||
|
|
||||||
|
- 再次移除这些重复 `start()` 调用
|
||||||
|
- 用 `DeviceMotion` 正式兜底指北针
|
||||||
|
- 让调试开关影响罗盘主链启动
|
||||||
|
|
||||||
|
## 一句话经验
|
||||||
|
|
||||||
|
**在微信小程序里,Android 罗盘监听的稳定性比 iOS 更脆;某些看似冗余的 `start()` 调用,实际是平台兼容补丁,不应该在没有真机回归的情况下清理。**
|
||||||
BIN
miniprogram/assets/btn_locked.png
Normal file
BIN
miniprogram/assets/btn_locked.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.9 KiB |
BIN
miniprogram/assets/btn_settings.png
Normal file
BIN
miniprogram/assets/btn_settings.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.2 KiB |
BIN
miniprogram/assets/btn_unlock.png
Normal file
BIN
miniprogram/assets/btn_unlock.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.2 KiB |
@@ -1,6 +1,6 @@
|
|||||||
import { getTileSizePx, screenToWorld, worldToScreen, type CameraState } from '../camera/camera'
|
import { getTileSizePx, screenToWorld, worldToScreen, type CameraState } from '../camera/camera'
|
||||||
import { AccelerometerController } from '../sensor/accelerometerController'
|
import { AccelerometerController } from '../sensor/accelerometerController'
|
||||||
import { CompassHeadingController } from '../sensor/compassHeadingController'
|
import { CompassHeadingController, type CompassTuningProfile } from '../sensor/compassHeadingController'
|
||||||
import { DeviceMotionController } from '../sensor/deviceMotionController'
|
import { DeviceMotionController } from '../sensor/deviceMotionController'
|
||||||
import { GyroscopeController } from '../sensor/gyroscopeController'
|
import { GyroscopeController } from '../sensor/gyroscopeController'
|
||||||
import { type HeartRateDiscoveredDevice } from '../sensor/heartRateController'
|
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 { lonLatToWorldTile, worldTileToLonLat, type LonLatPoint, type MapCalibration } from '../../utils/projection'
|
||||||
import { type OrienteeringCourseData } from '../../utils/orienteeringCourse'
|
import { type OrienteeringCourseData } from '../../utils/orienteeringCourse'
|
||||||
import { isTileWithinBounds, type RemoteMapConfig, type TileZoomBounds } from '../../utils/remoteMapConfig'
|
import { isTileWithinBounds, type RemoteMapConfig, type TileZoomBounds } from '../../utils/remoteMapConfig'
|
||||||
|
import { formatAnimationLevelText, resolveAnimationLevel, type AnimationLevel } from '../../utils/animationLevel'
|
||||||
import { GameRuntime } from '../../game/core/gameRuntime'
|
import { GameRuntime } from '../../game/core/gameRuntime'
|
||||||
import { type GameEffect, type GameResult } from '../../game/core/gameResult'
|
import { type GameEffect, type GameResult } from '../../game/core/gameResult'
|
||||||
import { buildGameDefinitionFromCourse } from '../../game/content/courseToGameDefinition'
|
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_DEADZONE_DEG = 4
|
||||||
const AUTO_ROTATE_MAX_STEP_DEG = 0.75
|
const AUTO_ROTATE_MAX_STEP_DEG = 0.75
|
||||||
const AUTO_ROTATE_HEADING_SMOOTHING = 0.46
|
const AUTO_ROTATE_HEADING_SMOOTHING = 0.46
|
||||||
const COMPASS_NEEDLE_MIN_SMOOTHING = 0.24
|
const COMPASS_TUNING_PRESETS: Record<CompassTuningProfile, {
|
||||||
const COMPASS_NEEDLE_MAX_SMOOTHING = 0.56
|
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_BLEND_START_SPEED_KMH = 1.2
|
||||||
const SMART_HEADING_MOVEMENT_SPEED_KMH = 3.0
|
const SMART_HEADING_MOVEMENT_SPEED_KMH = 3.0
|
||||||
const SMART_HEADING_MIN_DISTANCE_METERS = 12
|
const SMART_HEADING_MIN_DISTANCE_METERS = 12
|
||||||
@@ -88,6 +108,7 @@ export interface MapEngineStageRect {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface MapEngineViewState {
|
export interface MapEngineViewState {
|
||||||
|
animationLevel: AnimationLevel
|
||||||
buildVersion: string
|
buildVersion: string
|
||||||
renderMode: string
|
renderMode: string
|
||||||
projectionMode: string
|
projectionMode: string
|
||||||
@@ -110,7 +131,11 @@ export interface MapEngineViewState {
|
|||||||
accelerometerText: string
|
accelerometerText: string
|
||||||
gyroscopeText: string
|
gyroscopeText: string
|
||||||
deviceMotionText: string
|
deviceMotionText: string
|
||||||
|
compassSourceText: string
|
||||||
|
compassTuningProfile: CompassTuningProfile
|
||||||
|
compassTuningProfileText: string
|
||||||
compassDeclinationText: string
|
compassDeclinationText: string
|
||||||
|
northReferenceMode: NorthReferenceMode
|
||||||
northReferenceButtonText: string
|
northReferenceButtonText: string
|
||||||
autoRotateSourceText: string
|
autoRotateSourceText: string
|
||||||
autoRotateCalibrationText: string
|
autoRotateCalibrationText: string
|
||||||
@@ -199,6 +224,8 @@ export interface MapEngineViewState {
|
|||||||
contentCardTitle: string
|
contentCardTitle: string
|
||||||
contentCardBody: string
|
contentCardBody: string
|
||||||
punchButtonFxClass: string
|
punchButtonFxClass: string
|
||||||
|
panelProgressFxClass: string
|
||||||
|
panelDistanceFxClass: string
|
||||||
punchFeedbackFxClass: string
|
punchFeedbackFxClass: string
|
||||||
contentCardFxClass: string
|
contentCardFxClass: string
|
||||||
mapPulseVisible: boolean
|
mapPulseVisible: boolean
|
||||||
@@ -228,6 +255,7 @@ export interface MapEngineGameInfoSnapshot {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const VIEW_SYNC_KEYS: Array<keyof MapEngineViewState> = [
|
const VIEW_SYNC_KEYS: Array<keyof MapEngineViewState> = [
|
||||||
|
'animationLevel',
|
||||||
'buildVersion',
|
'buildVersion',
|
||||||
'renderMode',
|
'renderMode',
|
||||||
'projectionMode',
|
'projectionMode',
|
||||||
@@ -252,7 +280,11 @@ const VIEW_SYNC_KEYS: Array<keyof MapEngineViewState> = [
|
|||||||
'accelerometerText',
|
'accelerometerText',
|
||||||
'gyroscopeText',
|
'gyroscopeText',
|
||||||
'deviceMotionText',
|
'deviceMotionText',
|
||||||
|
'compassSourceText',
|
||||||
|
'compassTuningProfile',
|
||||||
|
'compassTuningProfileText',
|
||||||
'compassDeclinationText',
|
'compassDeclinationText',
|
||||||
|
'northReferenceMode',
|
||||||
'northReferenceButtonText',
|
'northReferenceButtonText',
|
||||||
'autoRotateSourceText',
|
'autoRotateSourceText',
|
||||||
'autoRotateCalibrationText',
|
'autoRotateCalibrationText',
|
||||||
@@ -330,6 +362,8 @@ const VIEW_SYNC_KEYS: Array<keyof MapEngineViewState> = [
|
|||||||
'contentCardTitle',
|
'contentCardTitle',
|
||||||
'contentCardBody',
|
'contentCardBody',
|
||||||
'punchButtonFxClass',
|
'punchButtonFxClass',
|
||||||
|
'panelProgressFxClass',
|
||||||
|
'panelDistanceFxClass',
|
||||||
'punchFeedbackFxClass',
|
'punchFeedbackFxClass',
|
||||||
'contentCardFxClass',
|
'contentCardFxClass',
|
||||||
'mapPulseVisible',
|
'mapPulseVisible',
|
||||||
@@ -342,6 +376,38 @@ const VIEW_SYNC_KEYS: Array<keyof MapEngineViewState> = [
|
|||||||
'osmReferenceText',
|
'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 {
|
function buildCenterText(zoom: number, x: number, y: number): string {
|
||||||
return `z${zoom} / x${x} / y${y}`
|
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)
|
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))
|
const deltaDeg = Math.abs(normalizeAngleDeltaDeg(targetDeg - currentDeg))
|
||||||
if (deltaDeg <= 4) {
|
if (deltaDeg <= 4) {
|
||||||
return COMPASS_NEEDLE_MIN_SMOOTHING
|
return preset.needleMinSmoothing
|
||||||
}
|
}
|
||||||
if (deltaDeg >= 36) {
|
if (deltaDeg >= 36) {
|
||||||
return COMPASS_NEEDLE_MAX_SMOOTHING
|
return preset.needleMaxSmoothing
|
||||||
}
|
}
|
||||||
|
|
||||||
const progress = (deltaDeg - 4) / (36 - 4)
|
const progress = (deltaDeg - 4) / (36 - 4)
|
||||||
return COMPASS_NEEDLE_MIN_SMOOTHING
|
return preset.needleMinSmoothing
|
||||||
+ (COMPASS_NEEDLE_MAX_SMOOTHING - COMPASS_NEEDLE_MIN_SMOOTHING) * progress
|
+ (preset.needleMaxSmoothing - preset.needleMinSmoothing) * progress
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMovementHeadingSmoothingFactor(speedKmh: number | null): number {
|
function getMovementHeadingSmoothingFactor(speedKmh: number | null): number {
|
||||||
@@ -434,7 +505,7 @@ function formatRotationText(rotationDeg: number): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function normalizeDegreeDisplayText(text: string): string {
|
function normalizeDegreeDisplayText(text: string): string {
|
||||||
return text.replace(/[°掳•]/g, '˚')
|
return text.replace(/[掳•˚]/g, '°')
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatHeadingText(headingDeg: number | null): string {
|
function formatHeadingText(headingDeg: number | null): string {
|
||||||
@@ -442,7 +513,7 @@ function formatHeadingText(headingDeg: number | null): string {
|
|||||||
return '--'
|
return '--'
|
||||||
}
|
}
|
||||||
|
|
||||||
return `${Math.round(normalizeRotationDeg(headingDeg))}˚`
|
return `${Math.round(normalizeRotationDeg(headingDeg))}°`
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatDevicePoseText(pose: 'upright' | 'tilted' | 'flat'): string {
|
function formatDevicePoseText(pose: 'upright' | 'tilted' | 'flat'): string {
|
||||||
@@ -494,9 +565,9 @@ function formatDeviceMotionText(motion: { alpha: number | null; beta: number | n
|
|||||||
return '--'
|
return '--'
|
||||||
}
|
}
|
||||||
|
|
||||||
const alphaDeg = motion.alpha === null ? '--' : Math.round(normalizeRotationDeg(360 - motion.alpha * 180 / Math.PI))
|
const alphaDeg = motion.alpha === null ? '--' : Math.round(normalizeRotationDeg(360 - motion.alpha))
|
||||||
const betaDeg = motion.beta === null ? '--' : Math.round(motion.beta * 180 / Math.PI)
|
const betaDeg = motion.beta === null ? '--' : Math.round(motion.beta)
|
||||||
const gammaDeg = motion.gamma === null ? '--' : Math.round(motion.gamma * 180 / Math.PI)
|
const gammaDeg = motion.gamma === null ? '--' : Math.round(motion.gamma)
|
||||||
return `a:${alphaDeg} b:${betaDeg} g:${gammaDeg}`
|
return `a:${alphaDeg} b:${betaDeg} g:${gammaDeg}`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -620,6 +691,26 @@ function formatCompassDeclinationText(mode: NorthReferenceMode): string {
|
|||||||
return ''
|
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 {
|
function formatNorthReferenceButtonText(mode: NorthReferenceMode): string {
|
||||||
return mode === 'magnetic' ? '北参照:磁北' : '北参照:真北'
|
return mode === 'magnetic' ? '北参照:磁北' : '北参照:真北'
|
||||||
}
|
}
|
||||||
@@ -702,6 +793,7 @@ function getInitialBearingDeg(from: LonLatPoint, to: LonLatPoint): number {
|
|||||||
|
|
||||||
export class MapEngine {
|
export class MapEngine {
|
||||||
buildVersion: string
|
buildVersion: string
|
||||||
|
animationLevel: AnimationLevel
|
||||||
renderer: WebGLMapRenderer
|
renderer: WebGLMapRenderer
|
||||||
accelerometerController: AccelerometerController
|
accelerometerController: AccelerometerController
|
||||||
compassController: CompassHeadingController
|
compassController: CompassHeadingController
|
||||||
@@ -742,6 +834,8 @@ export class MapEngine {
|
|||||||
sensorHeadingDeg: number | null
|
sensorHeadingDeg: number | null
|
||||||
smoothedSensorHeadingDeg: number | null
|
smoothedSensorHeadingDeg: number | null
|
||||||
compassDisplayHeadingDeg: number | null
|
compassDisplayHeadingDeg: number | null
|
||||||
|
compassSource: 'compass' | 'motion' | null
|
||||||
|
compassTuningProfile: CompassTuningProfile
|
||||||
smoothedMovementHeadingDeg: number | null
|
smoothedMovementHeadingDeg: number | null
|
||||||
autoRotateHeadingDeg: number | null
|
autoRotateHeadingDeg: number | null
|
||||||
courseHeadingDeg: number | null
|
courseHeadingDeg: number | null
|
||||||
@@ -789,6 +883,8 @@ export class MapEngine {
|
|||||||
|
|
||||||
constructor(buildVersion: string, callbacks: MapEngineCallbacks) {
|
constructor(buildVersion: string, callbacks: MapEngineCallbacks) {
|
||||||
this.buildVersion = buildVersion
|
this.buildVersion = buildVersion
|
||||||
|
this.animationLevel = resolveAnimationLevel(wx.getSystemInfoSync())
|
||||||
|
this.compassTuningProfile = 'balanced'
|
||||||
this.onData = callbacks.onData
|
this.onData = callbacks.onData
|
||||||
this.accelerometerErrorText = null
|
this.accelerometerErrorText = null
|
||||||
this.renderer = new WebGLMapRenderer(
|
this.renderer = new WebGLMapRenderer(
|
||||||
@@ -812,7 +908,7 @@ export class MapEngine {
|
|||||||
z,
|
z,
|
||||||
})
|
})
|
||||||
if (this.diagnosticUiEnabled) {
|
if (this.diagnosticUiEnabled) {
|
||||||
this.setState(this.getTelemetrySensorViewPatch(), true)
|
this.setState(this.getTelemetrySensorViewPatch())
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onError: (message) => {
|
onError: (message) => {
|
||||||
@@ -821,7 +917,7 @@ export class MapEngine {
|
|||||||
this.setState({
|
this.setState({
|
||||||
...this.getTelemetrySensorViewPatch(),
|
...this.getTelemetrySensorViewPatch(),
|
||||||
statusText: `加速度计启动失败 (${this.buildVersion})`,
|
statusText: `加速度计启动失败 (${this.buildVersion})`,
|
||||||
}, true)
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -833,6 +929,7 @@ export class MapEngine {
|
|||||||
this.handleCompassError(message)
|
this.handleCompassError(message)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
this.compassController.setTuningProfile(this.compassTuningProfile)
|
||||||
this.gyroscopeController = new GyroscopeController({
|
this.gyroscopeController = new GyroscopeController({
|
||||||
onSample: (x, y, z) => {
|
onSample: (x, y, z) => {
|
||||||
this.telemetryRuntime.dispatch({
|
this.telemetryRuntime.dispatch({
|
||||||
@@ -843,12 +940,12 @@ export class MapEngine {
|
|||||||
z,
|
z,
|
||||||
})
|
})
|
||||||
if (this.diagnosticUiEnabled) {
|
if (this.diagnosticUiEnabled) {
|
||||||
this.setState(this.getTelemetrySensorViewPatch(), true)
|
this.setState(this.getTelemetrySensorViewPatch())
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onError: () => {
|
onError: () => {
|
||||||
if (this.diagnosticUiEnabled) {
|
if (this.diagnosticUiEnabled) {
|
||||||
this.setState(this.getTelemetrySensorViewPatch(), true)
|
this.setState(this.getTelemetrySensorViewPatch())
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -865,16 +962,12 @@ export class MapEngine {
|
|||||||
this.setState({
|
this.setState({
|
||||||
...this.getTelemetrySensorViewPatch(),
|
...this.getTelemetrySensorViewPatch(),
|
||||||
autoRotateSourceText: this.getAutoRotateSourceText(),
|
autoRotateSourceText: this.getAutoRotateSourceText(),
|
||||||
}, true)
|
})
|
||||||
}
|
|
||||||
|
|
||||||
if (this.state.orientationMode === 'heading-up' && this.refreshAutoRotateTarget()) {
|
|
||||||
this.scheduleAutoRotate()
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onError: () => {
|
onError: () => {
|
||||||
if (this.diagnosticUiEnabled) {
|
if (this.diagnosticUiEnabled) {
|
||||||
this.setState(this.getTelemetrySensorViewPatch(), true)
|
this.setState(this.getTelemetrySensorViewPatch())
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -899,7 +992,7 @@ export class MapEngine {
|
|||||||
},
|
},
|
||||||
onDebugStateChange: () => {
|
onDebugStateChange: () => {
|
||||||
if (this.diagnosticUiEnabled) {
|
if (this.diagnosticUiEnabled) {
|
||||||
this.setState(this.getLocationControllerViewPatch(), true)
|
this.setState(this.getLocationControllerViewPatch())
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -963,12 +1056,12 @@ export class MapEngine {
|
|||||||
heartRateDiscoveredDevices: this.formatHeartRateDevices(devices),
|
heartRateDiscoveredDevices: this.formatHeartRateDevices(devices),
|
||||||
heartRateScanText: this.getHeartRateScanText(),
|
heartRateScanText: this.getHeartRateScanText(),
|
||||||
...this.getHeartRateControllerViewPatch(),
|
...this.getHeartRateControllerViewPatch(),
|
||||||
}, true)
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onDebugStateChange: () => {
|
onDebugStateChange: () => {
|
||||||
if (this.diagnosticUiEnabled) {
|
if (this.diagnosticUiEnabled) {
|
||||||
this.setState(this.getHeartRateControllerViewPatch(), true)
|
this.setState(this.getHeartRateControllerViewPatch())
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -982,6 +1075,12 @@ export class MapEngine {
|
|||||||
setPunchButtonFxClass: (className) => {
|
setPunchButtonFxClass: (className) => {
|
||||||
this.setPunchButtonFxClass(className)
|
this.setPunchButtonFxClass(className)
|
||||||
},
|
},
|
||||||
|
setHudProgressFxClass: (className) => {
|
||||||
|
this.setHudProgressFxClass(className)
|
||||||
|
},
|
||||||
|
setHudDistanceFxClass: (className) => {
|
||||||
|
this.setHudDistanceFxClass(className)
|
||||||
|
},
|
||||||
showMapPulse: (controlId, motionClass) => {
|
showMapPulse: (controlId, motionClass) => {
|
||||||
this.showMapPulse(controlId, motionClass)
|
this.showMapPulse(controlId, motionClass)
|
||||||
},
|
},
|
||||||
@@ -994,6 +1093,7 @@ export class MapEngine {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
this.feedbackDirector.setAnimationLevel(this.animationLevel)
|
||||||
this.minZoom = MIN_ZOOM
|
this.minZoom = MIN_ZOOM
|
||||||
this.maxZoom = MAX_ZOOM
|
this.maxZoom = MAX_ZOOM
|
||||||
this.defaultZoom = DEFAULT_ZOOM
|
this.defaultZoom = DEFAULT_ZOOM
|
||||||
@@ -1032,6 +1132,7 @@ export class MapEngine {
|
|||||||
this.sessionTimerInterval = 0
|
this.sessionTimerInterval = 0
|
||||||
this.hasGpsCenteredOnce = false
|
this.hasGpsCenteredOnce = false
|
||||||
this.state = {
|
this.state = {
|
||||||
|
animationLevel: this.animationLevel,
|
||||||
buildVersion: this.buildVersion,
|
buildVersion: this.buildVersion,
|
||||||
renderMode: RENDER_MODE,
|
renderMode: RENDER_MODE,
|
||||||
projectionMode: PROJECTION_MODE,
|
projectionMode: PROJECTION_MODE,
|
||||||
@@ -1051,10 +1152,14 @@ export class MapEngine {
|
|||||||
deviceHeadingText: '--',
|
deviceHeadingText: '--',
|
||||||
devicePoseText: '竖持',
|
devicePoseText: '竖持',
|
||||||
headingConfidenceText: '低',
|
headingConfidenceText: '低',
|
||||||
accelerometerText: '未启用',
|
accelerometerText: '未启用',
|
||||||
gyroscopeText: '--',
|
gyroscopeText: '--',
|
||||||
deviceMotionText: '--',
|
deviceMotionText: '--',
|
||||||
|
compassSourceText: '无数据',
|
||||||
|
compassTuningProfile: this.compassTuningProfile,
|
||||||
|
compassTuningProfileText: formatCompassTuningProfileText(this.compassTuningProfile),
|
||||||
compassDeclinationText: formatCompassDeclinationText(DEFAULT_NORTH_REFERENCE_MODE),
|
compassDeclinationText: formatCompassDeclinationText(DEFAULT_NORTH_REFERENCE_MODE),
|
||||||
|
northReferenceMode: DEFAULT_NORTH_REFERENCE_MODE,
|
||||||
northReferenceButtonText: formatNorthReferenceButtonText(DEFAULT_NORTH_REFERENCE_MODE),
|
northReferenceButtonText: formatNorthReferenceButtonText(DEFAULT_NORTH_REFERENCE_MODE),
|
||||||
autoRotateSourceText: formatAutoRotateSourceText('smart', false),
|
autoRotateSourceText: formatAutoRotateSourceText('smart', false),
|
||||||
autoRotateCalibrationText: formatAutoRotateCalibrationText(false, getMapNorthOffsetDeg(DEFAULT_NORTH_REFERENCE_MODE)),
|
autoRotateCalibrationText: formatAutoRotateCalibrationText(false, getMapNorthOffsetDeg(DEFAULT_NORTH_REFERENCE_MODE)),
|
||||||
@@ -1137,6 +1242,8 @@ export class MapEngine {
|
|||||||
contentCardTitle: '',
|
contentCardTitle: '',
|
||||||
contentCardBody: '',
|
contentCardBody: '',
|
||||||
punchButtonFxClass: '',
|
punchButtonFxClass: '',
|
||||||
|
panelProgressFxClass: '',
|
||||||
|
panelDistanceFxClass: '',
|
||||||
punchFeedbackFxClass: '',
|
punchFeedbackFxClass: '',
|
||||||
contentCardFxClass: '',
|
contentCardFxClass: '',
|
||||||
mapPulseVisible: false,
|
mapPulseVisible: false,
|
||||||
@@ -1177,6 +1284,8 @@ export class MapEngine {
|
|||||||
this.sensorHeadingDeg = null
|
this.sensorHeadingDeg = null
|
||||||
this.smoothedSensorHeadingDeg = null
|
this.smoothedSensorHeadingDeg = null
|
||||||
this.compassDisplayHeadingDeg = null
|
this.compassDisplayHeadingDeg = null
|
||||||
|
this.compassSource = null
|
||||||
|
this.compassTuningProfile = 'balanced'
|
||||||
this.smoothedMovementHeadingDeg = null
|
this.smoothedMovementHeadingDeg = null
|
||||||
this.autoRotateHeadingDeg = null
|
this.autoRotateHeadingDeg = null
|
||||||
this.courseHeadingDeg = null
|
this.courseHeadingDeg = null
|
||||||
@@ -1241,6 +1350,7 @@ export class MapEngine {
|
|||||||
{ label: '配置版本', value: this.configVersion || '--' },
|
{ label: '配置版本', value: this.configVersion || '--' },
|
||||||
{ label: 'Schema版本', value: this.configSchemaVersion || '--' },
|
{ label: 'Schema版本', value: this.configSchemaVersion || '--' },
|
||||||
{ label: '活动ID', value: this.configAppId || '--' },
|
{ label: '活动ID', value: this.configAppId || '--' },
|
||||||
|
{ label: '动画等级', value: formatAnimationLevelText(this.state.animationLevel) },
|
||||||
{ label: '地图', value: this.state.mapName || '--' },
|
{ label: '地图', value: this.state.mapName || '--' },
|
||||||
{ label: '模式', value: this.getGameModeText() },
|
{ label: '模式', value: this.getGameModeText() },
|
||||||
{ label: '状态', value: formatGameSessionStatusText(this.state.gameSessionStatus) },
|
{ label: '状态', value: formatGameSessionStatusText(this.state.gameSessionStatus) },
|
||||||
@@ -1417,20 +1527,23 @@ export class MapEngine {
|
|||||||
|
|
||||||
getTelemetrySensorViewPatch(): Partial<MapEngineViewState> {
|
getTelemetrySensorViewPatch(): Partial<MapEngineViewState> {
|
||||||
const telemetryState = this.telemetryRuntime.state
|
const telemetryState = this.telemetryRuntime.state
|
||||||
return {
|
return {
|
||||||
deviceHeadingText: formatHeadingText(
|
deviceHeadingText: formatHeadingText(
|
||||||
telemetryState.deviceHeadingDeg === null
|
telemetryState.deviceHeadingDeg === null
|
||||||
? null
|
? null
|
||||||
: getCompassReferenceHeadingDeg(this.northReferenceMode, telemetryState.deviceHeadingDeg),
|
: getCompassReferenceHeadingDeg(this.northReferenceMode, telemetryState.deviceHeadingDeg),
|
||||||
),
|
),
|
||||||
devicePoseText: formatDevicePoseText(telemetryState.devicePose),
|
devicePoseText: formatDevicePoseText(telemetryState.devicePose),
|
||||||
headingConfidenceText: formatHeadingConfidenceText(telemetryState.headingConfidence),
|
headingConfidenceText: formatHeadingConfidenceText(telemetryState.headingConfidence),
|
||||||
accelerometerText: telemetryState.accelerometer
|
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)}`
|
? `#${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),
|
gyroscopeText: formatGyroscopeText(telemetryState.gyroscope),
|
||||||
deviceMotionText: formatDeviceMotionText(telemetryState.deviceMotion),
|
deviceMotionText: formatDeviceMotionText(telemetryState.deviceMotion),
|
||||||
}
|
compassSourceText: formatCompassSourceText(this.compassSource),
|
||||||
|
compassTuningProfile: this.compassTuningProfile,
|
||||||
|
compassTuningProfileText: formatCompassTuningProfileText(this.compassTuningProfile),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getGameModeText(): string {
|
getGameModeText(): string {
|
||||||
@@ -1589,6 +1702,8 @@ export class MapEngine {
|
|||||||
stageFxVisible: false,
|
stageFxVisible: false,
|
||||||
stageFxClass: '',
|
stageFxClass: '',
|
||||||
punchButtonFxClass: '',
|
punchButtonFxClass: '',
|
||||||
|
panelProgressFxClass: '',
|
||||||
|
panelDistanceFxClass: '',
|
||||||
}, true)
|
}, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1675,6 +1790,18 @@ export class MapEngine {
|
|||||||
}, true)
|
}, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setHudProgressFxClass(className: string): void {
|
||||||
|
this.setState({
|
||||||
|
panelProgressFxClass: className,
|
||||||
|
}, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
setHudDistanceFxClass(className: string): void {
|
||||||
|
this.setState({
|
||||||
|
panelDistanceFxClass: className,
|
||||||
|
}, true)
|
||||||
|
}
|
||||||
|
|
||||||
showMapPulse(controlId: string, motionClass = ''): void {
|
showMapPulse(controlId: string, motionClass = ''): void {
|
||||||
const screenPoint = this.getControlScreenPoint(controlId)
|
const screenPoint = this.getControlScreenPoint(controlId)
|
||||||
if (!screenPoint) {
|
if (!screenPoint) {
|
||||||
@@ -1761,6 +1888,9 @@ export class MapEngine {
|
|||||||
applyGameEffects(effects: GameEffect[]): string | null {
|
applyGameEffects(effects: GameEffect[]): string | null {
|
||||||
this.feedbackDirector.handleEffects(effects)
|
this.feedbackDirector.handleEffects(effects)
|
||||||
if (effects.some((effect) => effect.type === 'session_finished')) {
|
if (effects.some((effect) => effect.type === 'session_finished')) {
|
||||||
|
if (this.locationController.listening) {
|
||||||
|
this.locationController.stop()
|
||||||
|
}
|
||||||
this.setState({
|
this.setState({
|
||||||
gpsTracking: false,
|
gpsTracking: false,
|
||||||
gpsTrackingText: '测试结束,定位已停止',
|
gpsTrackingText: '测试结束,定位已停止',
|
||||||
@@ -1845,12 +1975,17 @@ export class MapEngine {
|
|||||||
|
|
||||||
handleForceExitGame(): void {
|
handleForceExitGame(): void {
|
||||||
this.feedbackDirector.reset()
|
this.feedbackDirector.reset()
|
||||||
|
if (this.locationController.listening) {
|
||||||
|
this.locationController.stop()
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.courseData) {
|
if (!this.courseData) {
|
||||||
this.clearGameRuntime()
|
this.clearGameRuntime()
|
||||||
this.resetTransientGameUiState()
|
this.resetTransientGameUiState()
|
||||||
this.feedbackDirector.handleEffects([{ type: 'session_cancelled' }])
|
this.feedbackDirector.handleEffects([{ type: 'session_cancelled' }])
|
||||||
this.setState({
|
this.setState({
|
||||||
|
gpsTracking: false,
|
||||||
|
gpsTrackingText: '已退出对局,定位已停止',
|
||||||
...this.getGameViewPatch(`已退出当前对局 (${this.buildVersion})`),
|
...this.getGameViewPatch(`已退出当前对局 (${this.buildVersion})`),
|
||||||
}, true)
|
}, true)
|
||||||
this.syncRenderer()
|
this.syncRenderer()
|
||||||
@@ -1861,6 +1996,8 @@ export class MapEngine {
|
|||||||
this.resetTransientGameUiState()
|
this.resetTransientGameUiState()
|
||||||
this.feedbackDirector.handleEffects([{ type: 'session_cancelled' }])
|
this.feedbackDirector.handleEffects([{ type: 'session_cancelled' }])
|
||||||
this.setState({
|
this.setState({
|
||||||
|
gpsTracking: false,
|
||||||
|
gpsTrackingText: '已退出对局,定位已停止',
|
||||||
...this.getGameViewPatch(`已退出当前对局 (${this.buildVersion})`),
|
...this.getGameViewPatch(`已退出当前对局 (${this.buildVersion})`),
|
||||||
}, true)
|
}, true)
|
||||||
this.syncRenderer()
|
this.syncRenderer()
|
||||||
@@ -1946,7 +2083,7 @@ export class MapEngine {
|
|||||||
gpsLockEnabled: this.gpsLockEnabled,
|
gpsLockEnabled: this.gpsLockEnabled,
|
||||||
gpsLockAvailable: gpsInsideMap,
|
gpsLockAvailable: gpsInsideMap,
|
||||||
...this.getGameViewPatch(gameStatusText || (gpsInsideMap ? `GPS位置已更新 (${this.buildVersion})` : `GPS位置超出当前地图范围 (${this.buildVersion})`)),
|
...this.getGameViewPatch(gameStatusText || (gpsInsideMap ? `GPS位置已更新 (${this.buildVersion})` : `GPS位置超出当前地图范围 (${this.buildVersion})`)),
|
||||||
}, true)
|
})
|
||||||
this.syncRenderer()
|
this.syncRenderer()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2100,7 +2237,7 @@ export class MapEngine {
|
|||||||
this.setState({
|
this.setState({
|
||||||
heartRateDeviceText: this.heartRateController.currentDeviceName || '--',
|
heartRateDeviceText: this.heartRateController.currentDeviceName || '--',
|
||||||
heartRateScanText: this.getHeartRateScanText(),
|
heartRateScanText: this.getHeartRateScanText(),
|
||||||
}, true)
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
handleDebugHeartRateTone(tone: HeartRateTone): void {
|
handleDebugHeartRateTone(tone: HeartRateTone): void {
|
||||||
@@ -2112,7 +2249,7 @@ export class MapEngine {
|
|||||||
})
|
})
|
||||||
this.setState({
|
this.setState({
|
||||||
heartRateStatusText: `调试心率: ${sampleBpm} bpm / ${tone.toUpperCase()}`,
|
heartRateStatusText: `调试心率: ${sampleBpm} bpm / ${tone.toUpperCase()}`,
|
||||||
}, true)
|
})
|
||||||
this.syncSessionTimerText()
|
this.syncSessionTimerText()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2128,7 +2265,7 @@ export class MapEngine {
|
|||||||
: (this.heartRateController.sourceMode === 'mock' ? '模拟心率源未连接' : '心率带未连接'),
|
: (this.heartRateController.sourceMode === 'mock' ? '模拟心率源未连接' : '心率带未连接'),
|
||||||
heartRateScanText: this.getHeartRateScanText(),
|
heartRateScanText: this.getHeartRateScanText(),
|
||||||
...this.getHeartRateControllerViewPatch(),
|
...this.getHeartRateControllerViewPatch(),
|
||||||
}, true)
|
})
|
||||||
this.syncSessionTimerText()
|
this.syncSessionTimerText()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2250,7 +2387,7 @@ export class MapEngine {
|
|||||||
configStatusText: `配置已载入 / ${config.configTitle} / ${config.courseStatusText}`,
|
configStatusText: `配置已载入 / ${config.configTitle} / ${config.courseStatusText}`,
|
||||||
projectionMode: config.projectionModeText,
|
projectionMode: config.projectionModeText,
|
||||||
tileSource: config.tileSource,
|
tileSource: config.tileSource,
|
||||||
sensorHeadingText: formatHeadingText(this.smoothedSensorHeadingDeg === null ? null : getCompassReferenceHeadingDeg(this.northReferenceMode, this.smoothedSensorHeadingDeg)),
|
sensorHeadingText: formatHeadingText(this.compassDisplayHeadingDeg),
|
||||||
compassDeclinationText: formatCompassDeclinationText(this.northReferenceMode),
|
compassDeclinationText: formatCompassDeclinationText(this.northReferenceMode),
|
||||||
northReferenceButtonText: formatNorthReferenceButtonText(this.northReferenceMode),
|
northReferenceButtonText: formatNorthReferenceButtonText(this.northReferenceMode),
|
||||||
northReferenceText: formatNorthReferenceText(this.northReferenceMode),
|
northReferenceText: formatNorthReferenceText(this.northReferenceMode),
|
||||||
@@ -2308,7 +2445,7 @@ export class MapEngine {
|
|||||||
this.pinchAnchorWorldY = anchorWorld.y
|
this.pinchAnchorWorldY = anchorWorld.y
|
||||||
this.setPreviewState(this.pinchStartScale, origin.x, origin.y)
|
this.setPreviewState(this.pinchStartScale, origin.x, origin.y)
|
||||||
this.syncRenderer()
|
this.syncRenderer()
|
||||||
this.compassController.start()
|
this.compassController.start()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2567,7 +2704,7 @@ export class MapEngine {
|
|||||||
() => {
|
() => {
|
||||||
this.resetPreviewState()
|
this.resetPreviewState()
|
||||||
this.syncRenderer()
|
this.syncRenderer()
|
||||||
this.compassController.start()
|
this.compassController.start()
|
||||||
this.scheduleAutoRotate()
|
this.scheduleAutoRotate()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -2601,7 +2738,7 @@ export class MapEngine {
|
|||||||
() => {
|
() => {
|
||||||
this.resetPreviewState()
|
this.resetPreviewState()
|
||||||
this.syncRenderer()
|
this.syncRenderer()
|
||||||
this.compassController.start()
|
this.compassController.start()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -2638,7 +2775,7 @@ export class MapEngine {
|
|||||||
() => {
|
() => {
|
||||||
this.resetPreviewState()
|
this.resetPreviewState()
|
||||||
this.syncRenderer()
|
this.syncRenderer()
|
||||||
this.compassController.start()
|
this.compassController.start()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -2673,6 +2810,38 @@ export class MapEngine {
|
|||||||
this.cycleNorthReferenceMode()
|
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 {
|
handleAutoRotateCalibrate(): void {
|
||||||
if (this.state.orientationMode !== 'heading-up') {
|
if (this.state.orientationMode !== 'heading-up') {
|
||||||
this.setState({
|
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.sensorHeadingDeg = normalizeRotationDeg(headingDeg)
|
||||||
this.smoothedSensorHeadingDeg = this.smoothedSensorHeadingDeg === null
|
this.smoothedSensorHeadingDeg = this.smoothedSensorHeadingDeg === null
|
||||||
? this.sensorHeadingDeg
|
? this.sensorHeadingDeg
|
||||||
: interpolateAngleDeg(this.smoothedSensorHeadingDeg, this.sensorHeadingDeg, AUTO_ROTATE_HEADING_SMOOTHING)
|
: interpolateAngleDeg(this.smoothedSensorHeadingDeg, this.sensorHeadingDeg, AUTO_ROTATE_HEADING_SMOOTHING)
|
||||||
|
|
||||||
const compassHeadingDeg = getCompassReferenceHeadingDeg(this.northReferenceMode, this.smoothedSensorHeadingDeg)
|
const compassHeadingDeg = getCompassReferenceHeadingDeg(this.northReferenceMode, this.smoothedSensorHeadingDeg)
|
||||||
this.compassDisplayHeadingDeg = this.compassDisplayHeadingDeg === null
|
if (this.compassDisplayHeadingDeg === null) {
|
||||||
? compassHeadingDeg
|
this.compassDisplayHeadingDeg = compassHeadingDeg
|
||||||
: interpolateAngleDeg(
|
} else {
|
||||||
this.compassDisplayHeadingDeg,
|
const displayDeltaDeg = Math.abs(normalizeAngleDeltaDeg(compassHeadingDeg - this.compassDisplayHeadingDeg))
|
||||||
compassHeadingDeg,
|
if (displayDeltaDeg >= COMPASS_TUNING_PRESETS[this.compassTuningProfile].displayDeadzoneDeg) {
|
||||||
getCompassNeedleSmoothingFactor(this.compassDisplayHeadingDeg, compassHeadingDeg),
|
this.compassDisplayHeadingDeg = interpolateAngleDeg(
|
||||||
)
|
this.compassDisplayHeadingDeg,
|
||||||
|
compassHeadingDeg,
|
||||||
|
getCompassNeedleSmoothingFactor(
|
||||||
|
this.compassDisplayHeadingDeg,
|
||||||
|
compassHeadingDeg,
|
||||||
|
this.compassTuningProfile,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.autoRotateHeadingDeg = this.resolveAutoRotateInputHeadingDeg()
|
this.autoRotateHeadingDeg = this.resolveAutoRotateInputHeadingDeg()
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
compassNeedleDeg: formatCompassNeedleDegForMode(this.northReferenceMode, this.compassDisplayHeadingDeg),
|
compassNeedleDeg: formatCompassNeedleDegForMode(this.northReferenceMode, this.compassDisplayHeadingDeg),
|
||||||
|
sensorHeadingText: formatHeadingText(this.compassDisplayHeadingDeg),
|
||||||
|
compassDeclinationText: formatCompassDeclinationText(this.northReferenceMode),
|
||||||
...(this.diagnosticUiEnabled
|
...(this.diagnosticUiEnabled
|
||||||
? {
|
? {
|
||||||
sensorHeadingText: formatHeadingText(compassHeadingDeg),
|
|
||||||
...this.getTelemetrySensorViewPatch(),
|
...this.getTelemetrySensorViewPatch(),
|
||||||
compassDeclinationText: formatCompassDeclinationText(this.northReferenceMode),
|
|
||||||
northReferenceButtonText: formatNorthReferenceButtonText(this.northReferenceMode),
|
northReferenceButtonText: formatNorthReferenceButtonText(this.northReferenceMode),
|
||||||
autoRotateSourceText: this.getAutoRotateSourceText(),
|
autoRotateSourceText: this.getAutoRotateSourceText(),
|
||||||
northReferenceText: formatNorthReferenceText(this.northReferenceMode),
|
northReferenceText: formatNorthReferenceText(this.northReferenceMode),
|
||||||
@@ -2801,18 +2980,31 @@ export class MapEngine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleCompassHeading(headingDeg: number): void {
|
||||||
|
this.applyHeadingSample(headingDeg, 'compass')
|
||||||
|
}
|
||||||
|
|
||||||
handleCompassError(message: string): void {
|
handleCompassError(message: string): void {
|
||||||
this.clearAutoRotateTimer()
|
this.clearAutoRotateTimer()
|
||||||
this.targetAutoRotationDeg = null
|
this.targetAutoRotationDeg = null
|
||||||
this.autoRotateCalibrationPending = false
|
this.autoRotateCalibrationPending = false
|
||||||
|
this.compassSource = null
|
||||||
this.setState({
|
this.setState({
|
||||||
|
compassSourceText: formatCompassSourceText(null),
|
||||||
autoRotateCalibrationText: formatAutoRotateCalibrationText(false, this.autoRotateCalibrationOffsetDeg),
|
autoRotateCalibrationText: formatAutoRotateCalibrationText(false, this.autoRotateCalibrationOffsetDeg),
|
||||||
statusText: `${message} (${this.buildVersion})`,
|
statusText: `${message} (${this.buildVersion})`,
|
||||||
}, true)
|
}, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
cycleNorthReferenceMode(): void {
|
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 nextMapNorthOffsetDeg = getMapNorthOffsetDeg(nextMode)
|
||||||
const compassHeadingDeg = this.smoothedSensorHeadingDeg === null
|
const compassHeadingDeg = this.smoothedSensorHeadingDeg === null
|
||||||
? null
|
? null
|
||||||
@@ -2831,9 +3023,10 @@ export class MapEngine {
|
|||||||
rotationDeg: MAP_NORTH_OFFSET_DEG,
|
rotationDeg: MAP_NORTH_OFFSET_DEG,
|
||||||
rotationText: formatRotationText(MAP_NORTH_OFFSET_DEG),
|
rotationText: formatRotationText(MAP_NORTH_OFFSET_DEG),
|
||||||
northReferenceText: formatNorthReferenceText(nextMode),
|
northReferenceText: formatNorthReferenceText(nextMode),
|
||||||
sensorHeadingText: formatHeadingText(compassHeadingDeg),
|
sensorHeadingText: formatHeadingText(this.compassDisplayHeadingDeg),
|
||||||
...this.getTelemetrySensorViewPatch(),
|
...this.getTelemetrySensorViewPatch(),
|
||||||
compassDeclinationText: formatCompassDeclinationText(nextMode),
|
compassDeclinationText: formatCompassDeclinationText(nextMode),
|
||||||
|
northReferenceMode: nextMode,
|
||||||
northReferenceButtonText: formatNorthReferenceButtonText(nextMode),
|
northReferenceButtonText: formatNorthReferenceButtonText(nextMode),
|
||||||
compassNeedleDeg: formatCompassNeedleDegForMode(nextMode, this.compassDisplayHeadingDeg),
|
compassNeedleDeg: formatCompassNeedleDegForMode(nextMode, this.compassDisplayHeadingDeg),
|
||||||
autoRotateCalibrationText: formatAutoRotateCalibrationText(false, nextMapNorthOffsetDeg),
|
autoRotateCalibrationText: formatAutoRotateCalibrationText(false, nextMapNorthOffsetDeg),
|
||||||
@@ -2850,9 +3043,10 @@ export class MapEngine {
|
|||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
northReferenceText: formatNorthReferenceText(nextMode),
|
northReferenceText: formatNorthReferenceText(nextMode),
|
||||||
sensorHeadingText: formatHeadingText(compassHeadingDeg),
|
sensorHeadingText: formatHeadingText(this.compassDisplayHeadingDeg),
|
||||||
...this.getTelemetrySensorViewPatch(),
|
...this.getTelemetrySensorViewPatch(),
|
||||||
compassDeclinationText: formatCompassDeclinationText(nextMode),
|
compassDeclinationText: formatCompassDeclinationText(nextMode),
|
||||||
|
northReferenceMode: nextMode,
|
||||||
northReferenceButtonText: formatNorthReferenceButtonText(nextMode),
|
northReferenceButtonText: formatNorthReferenceButtonText(nextMode),
|
||||||
compassNeedleDeg: formatCompassNeedleDegForMode(nextMode, this.compassDisplayHeadingDeg),
|
compassNeedleDeg: formatCompassNeedleDegForMode(nextMode, this.compassDisplayHeadingDeg),
|
||||||
autoRotateCalibrationText: formatAutoRotateCalibrationText(false, nextMapNorthOffsetDeg),
|
autoRotateCalibrationText: formatAutoRotateCalibrationText(false, nextMapNorthOffsetDeg),
|
||||||
@@ -3167,6 +3361,7 @@ export class MapEngine {
|
|||||||
|
|
||||||
buildScene() {
|
buildScene() {
|
||||||
const exactCenter = this.getExactCenterFromTranslate(this.state.tileTranslateX, this.state.tileTranslateY)
|
const exactCenter = this.getExactCenterFromTranslate(this.state.tileTranslateX, this.state.tileTranslateY)
|
||||||
|
const readyControlSequences = this.resolveReadyControlSequences()
|
||||||
return {
|
return {
|
||||||
tileSource: this.state.tileSource,
|
tileSource: this.state.tileSource,
|
||||||
osmTileSource: OSM_TILE_SOURCE,
|
osmTileSource: OSM_TILE_SOURCE,
|
||||||
@@ -3183,6 +3378,7 @@ export class MapEngine {
|
|||||||
translateX: this.state.tileTranslateX,
|
translateX: this.state.tileTranslateX,
|
||||||
translateY: this.state.tileTranslateY,
|
translateY: this.state.tileTranslateY,
|
||||||
rotationRad: this.getRotationRad(this.state.rotationDeg),
|
rotationRad: this.getRotationRad(this.state.rotationDeg),
|
||||||
|
animationLevel: this.state.animationLevel,
|
||||||
previewScale: this.previewScale || 1,
|
previewScale: this.previewScale || 1,
|
||||||
previewOriginX: this.previewOriginX || this.state.stageWidth / 2,
|
previewOriginX: this.previewOriginX || this.state.stageWidth / 2,
|
||||||
previewOriginY: this.previewOriginY || this.state.stageHeight / 2,
|
previewOriginY: this.previewOriginY || this.state.stageHeight / 2,
|
||||||
@@ -3199,6 +3395,7 @@ export class MapEngine {
|
|||||||
focusedControlId: this.gamePresentation.map.focusedControlId,
|
focusedControlId: this.gamePresentation.map.focusedControlId,
|
||||||
focusedControlSequences: this.gamePresentation.map.focusedControlSequences,
|
focusedControlSequences: this.gamePresentation.map.focusedControlSequences,
|
||||||
activeControlSequences: this.gamePresentation.map.activeControlSequences,
|
activeControlSequences: this.gamePresentation.map.activeControlSequences,
|
||||||
|
readyControlSequences,
|
||||||
activeStart: this.gamePresentation.map.activeStart,
|
activeStart: this.gamePresentation.map.activeStart,
|
||||||
completedStart: this.gamePresentation.map.completedStart,
|
completedStart: this.gamePresentation.map.completedStart,
|
||||||
activeFinish: this.gamePresentation.map.activeFinish,
|
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 {
|
syncRenderer(): void {
|
||||||
if (!this.mounted || !this.state.stageWidth || !this.state.stageHeight) {
|
if (!this.mounted || !this.state.stageWidth || !this.state.stageHeight) {
|
||||||
return
|
return
|
||||||
@@ -3374,8 +3586,32 @@ export class MapEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const patch = this.pendingViewPatch
|
const patch = this.pendingViewPatch
|
||||||
this.pendingViewPatch = {}
|
const shouldDeferForInteraction = this.gestureMode !== 'idle' || !!this.inertiaTimer || !!this.previewResetTimer
|
||||||
this.onData(patch)
|
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 {
|
getTouchDistance(touches: TouchPoint[]): number {
|
||||||
@@ -3431,7 +3667,7 @@ export class MapEngine {
|
|||||||
if (Math.abs(startScale - 1) < 0.01) {
|
if (Math.abs(startScale - 1) < 0.01) {
|
||||||
this.resetPreviewState()
|
this.resetPreviewState()
|
||||||
this.syncRenderer()
|
this.syncRenderer()
|
||||||
this.compassController.start()
|
this.compassController.start()
|
||||||
this.scheduleAutoRotate()
|
this.scheduleAutoRotate()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -3443,12 +3679,12 @@ export class MapEngine {
|
|||||||
const nextScale = startScale + (1 - startScale) * eased
|
const nextScale = startScale + (1 - startScale) * eased
|
||||||
this.setPreviewState(nextScale, originX, originY)
|
this.setPreviewState(nextScale, originX, originY)
|
||||||
this.syncRenderer()
|
this.syncRenderer()
|
||||||
this.compassController.start()
|
this.compassController.start()
|
||||||
|
|
||||||
if (progress >= 1) {
|
if (progress >= 1) {
|
||||||
this.resetPreviewState()
|
this.resetPreviewState()
|
||||||
this.syncRenderer()
|
this.syncRenderer()
|
||||||
this.compassController.start()
|
this.compassController.start()
|
||||||
this.previewResetTimer = 0
|
this.previewResetTimer = 0
|
||||||
this.scheduleAutoRotate()
|
this.scheduleAutoRotate()
|
||||||
return
|
return
|
||||||
@@ -3467,7 +3703,7 @@ export class MapEngine {
|
|||||||
tileTranslateY: translateY,
|
tileTranslateY: translateY,
|
||||||
})
|
})
|
||||||
this.syncRenderer()
|
this.syncRenderer()
|
||||||
this.compassController.start()
|
this.compassController.start()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3530,7 +3766,7 @@ export class MapEngine {
|
|||||||
() => {
|
() => {
|
||||||
this.setPreviewState(residualScale, stageX, stageY)
|
this.setPreviewState(residualScale, stageX, stageY)
|
||||||
this.syncRenderer()
|
this.syncRenderer()
|
||||||
this.compassController.start()
|
this.compassController.start()
|
||||||
this.animatePreviewToRest()
|
this.animatePreviewToRest()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -3557,7 +3793,7 @@ export class MapEngine {
|
|||||||
() => {
|
() => {
|
||||||
this.setPreviewState(residualScale, stageX, stageY)
|
this.setPreviewState(residualScale, stageX, stageY)
|
||||||
this.syncRenderer()
|
this.syncRenderer()
|
||||||
this.compassController.start()
|
this.compassController.start()
|
||||||
this.animatePreviewToRest()
|
this.animatePreviewToRest()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -9,11 +9,14 @@ const SCORE_LABEL_FONT_SIZE_RATIO = 0.7
|
|||||||
const SCORE_LABEL_OFFSET_Y_RATIO = 0.06
|
const SCORE_LABEL_OFFSET_Y_RATIO = 0.06
|
||||||
const DEFAULT_LABEL_COLOR = 'rgba(204, 0, 107, 0.98)'
|
const DEFAULT_LABEL_COLOR = 'rgba(204, 0, 107, 0.98)'
|
||||||
const ACTIVE_LABEL_COLOR = 'rgba(255, 219, 54, 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 MULTI_ACTIVE_LABEL_COLOR = 'rgba(255, 202, 72, 0.96)'
|
||||||
const FOCUSED_LABEL_COLOR = 'rgba(255, 252, 255, 0.98)'
|
const FOCUSED_LABEL_COLOR = 'rgba(255, 252, 255, 0.98)'
|
||||||
const COMPLETED_LABEL_COLOR = 'rgba(126, 131, 138, 0.94)'
|
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_LABEL_COLOR = 'rgba(255, 252, 242, 0.98)'
|
||||||
const SCORE_COMPLETED_LABEL_COLOR = 'rgba(214, 218, 224, 0.94)'
|
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 {
|
export class CourseLabelRenderer {
|
||||||
courseLayer: CourseLayer
|
courseLayer: CourseLayer
|
||||||
@@ -107,6 +110,10 @@ export class CourseLabelRenderer {
|
|||||||
return FOCUSED_LABEL_COLOR
|
return FOCUSED_LABEL_COLOR
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (scene.readyControlSequences.includes(sequence)) {
|
||||||
|
return READY_LABEL_COLOR
|
||||||
|
}
|
||||||
|
|
||||||
if (scene.activeControlSequences.includes(sequence)) {
|
if (scene.activeControlSequences.includes(sequence)) {
|
||||||
return scene.controlVisualMode === 'multi-target' ? MULTI_ACTIVE_LABEL_COLOR : ACTIVE_LABEL_COLOR
|
return scene.controlVisualMode === 'multi-target' ? MULTI_ACTIVE_LABEL_COLOR : ACTIVE_LABEL_COLOR
|
||||||
}
|
}
|
||||||
@@ -116,7 +123,7 @@ export class CourseLabelRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (scene.skippedControlSequences.includes(sequence)) {
|
if (scene.skippedControlSequences.includes(sequence)) {
|
||||||
return COMPLETED_LABEL_COLOR
|
return SKIPPED_LABEL_COLOR
|
||||||
}
|
}
|
||||||
|
|
||||||
return DEFAULT_LABEL_COLOR
|
return DEFAULT_LABEL_COLOR
|
||||||
@@ -127,12 +134,16 @@ export class CourseLabelRenderer {
|
|||||||
return FOCUSED_LABEL_COLOR
|
return FOCUSED_LABEL_COLOR
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (scene.readyControlSequences.includes(sequence)) {
|
||||||
|
return READY_LABEL_COLOR
|
||||||
|
}
|
||||||
|
|
||||||
if (scene.completedControlSequences.includes(sequence)) {
|
if (scene.completedControlSequences.includes(sequence)) {
|
||||||
return SCORE_COMPLETED_LABEL_COLOR
|
return SCORE_COMPLETED_LABEL_COLOR
|
||||||
}
|
}
|
||||||
|
|
||||||
if (scene.skippedControlSequences.includes(sequence)) {
|
if (scene.skippedControlSequences.includes(sequence)) {
|
||||||
return SCORE_COMPLETED_LABEL_COLOR
|
return SCORE_SKIPPED_LABEL_COLOR
|
||||||
}
|
}
|
||||||
|
|
||||||
return SCORE_LABEL_COLOR
|
return SCORE_LABEL_COLOR
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { type TileStoreStats } from '../tile/tileStore'
|
|||||||
import { type LonLatPoint, type MapCalibration } from '../../utils/projection'
|
import { type LonLatPoint, type MapCalibration } from '../../utils/projection'
|
||||||
import { type TileZoomBounds } from '../../utils/remoteMapConfig'
|
import { type TileZoomBounds } from '../../utils/remoteMapConfig'
|
||||||
import { type OrienteeringCourseData } from '../../utils/orienteeringCourse'
|
import { type OrienteeringCourseData } from '../../utils/orienteeringCourse'
|
||||||
|
import { type AnimationLevel } from '../../utils/animationLevel'
|
||||||
|
|
||||||
export interface MapScene {
|
export interface MapScene {
|
||||||
tileSource: string
|
tileSource: string
|
||||||
@@ -20,6 +21,7 @@ export interface MapScene {
|
|||||||
translateX: number
|
translateX: number
|
||||||
translateY: number
|
translateY: number
|
||||||
rotationRad: number
|
rotationRad: number
|
||||||
|
animationLevel: AnimationLevel
|
||||||
previewScale: number
|
previewScale: number
|
||||||
previewOriginX: number
|
previewOriginX: number
|
||||||
previewOriginY: number
|
previewOriginY: number
|
||||||
@@ -36,6 +38,7 @@ export interface MapScene {
|
|||||||
focusedControlId: string | null
|
focusedControlId: string | null
|
||||||
focusedControlSequences: number[]
|
focusedControlSequences: number[]
|
||||||
activeControlSequences: number[]
|
activeControlSequences: number[]
|
||||||
|
readyControlSequences: number[]
|
||||||
activeStart: boolean
|
activeStart: boolean
|
||||||
completedStart: boolean
|
completedStart: boolean
|
||||||
activeFinish: boolean
|
activeFinish: boolean
|
||||||
|
|||||||
@@ -135,12 +135,16 @@ export class WebGLMapRenderer implements MapRenderer {
|
|||||||
this.scheduleRender()
|
this.scheduleRender()
|
||||||
}
|
}
|
||||||
|
|
||||||
this.animationTimer = setTimeout(tick, ANIMATION_FRAME_MS) as unknown as number
|
this.animationTimer = setTimeout(tick, this.getAnimationFrameMs()) as unknown as number
|
||||||
}
|
}
|
||||||
|
|
||||||
tick()
|
tick()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getAnimationFrameMs(): number {
|
||||||
|
return this.scene && this.scene.animationLevel === 'lite' ? 48 : ANIMATION_FRAME_MS
|
||||||
|
}
|
||||||
|
|
||||||
scheduleRender(): void {
|
scheduleRender(): void {
|
||||||
if (this.renderTimer || !this.scene || this.destroyed) {
|
if (this.renderTimer || !this.scene || this.destroyed) {
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -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 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 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 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 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 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 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 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 ACTIVE_LEG_COLOR: [number, number, number, number] = [0.18, 1, 0.94, 0.5]
|
||||||
const EARTH_CIRCUMFERENCE_METERS = 40075016.686
|
const EARTH_CIRCUMFERENCE_METERS = 40075016.686
|
||||||
const CONTROL_RING_WIDTH_RATIO = 0.2
|
const CONTROL_RING_WIDTH_RATIO = 0.2
|
||||||
@@ -196,6 +202,18 @@ export class WebGLVectorRenderer {
|
|||||||
gl.drawArrays(gl.TRIANGLES, 0, positions.length / 2)
|
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 {
|
getPixelsPerMeter(scene: MapScene): number {
|
||||||
const camera: CameraState = {
|
const camera: CameraState = {
|
||||||
centerWorldX: scene.exactCenterWorldX,
|
centerWorldX: scene.exactCenterWorldX,
|
||||||
@@ -249,6 +267,18 @@ export class WebGLVectorRenderer {
|
|||||||
if (scene.activeStart) {
|
if (scene.activeStart) {
|
||||||
this.pushActiveStartPulse(positions, colors, start.point.x, start.point.y, start.headingDeg, controlRadiusMeters, scene, pulseFrame)
|
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)
|
this.pushStartTriangle(positions, colors, start.point.x, start.point.y, start.headingDeg, controlRadiusMeters, this.getStartColor(scene), scene)
|
||||||
}
|
}
|
||||||
if (!scene.revealFullCourse) {
|
if (!scene.revealFullCourse) {
|
||||||
@@ -261,10 +291,29 @@ export class WebGLVectorRenderer {
|
|||||||
this.pushActiveControlPulse(positions, colors, control.point.x, control.point.y, controlRadiusMeters, scene, pulseFrame)
|
this.pushActiveControlPulse(positions, colors, control.point.x, control.point.y, controlRadiusMeters, scene, pulseFrame)
|
||||||
} else {
|
} 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, 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(
|
this.pushRing(
|
||||||
positions,
|
positions,
|
||||||
colors,
|
colors,
|
||||||
@@ -278,7 +327,9 @@ export class WebGLVectorRenderer {
|
|||||||
|
|
||||||
if (scene.focusedControlSequences.includes(control.sequence)) {
|
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.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(
|
this.pushRing(
|
||||||
positions,
|
positions,
|
||||||
colors,
|
colors,
|
||||||
@@ -290,6 +341,33 @@ export class WebGLVectorRenderer {
|
|||||||
scene,
|
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) {
|
for (const finish of course.finishes) {
|
||||||
@@ -298,10 +376,24 @@ export class WebGLVectorRenderer {
|
|||||||
}
|
}
|
||||||
if (scene.focusedFinish) {
|
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.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)
|
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(
|
this.pushRing(
|
||||||
positions,
|
positions,
|
||||||
colors,
|
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(
|
pushActiveStartPulse(
|
||||||
positions: number[],
|
positions: number[],
|
||||||
colors: number[],
|
colors: number[],
|
||||||
@@ -462,14 +575,22 @@ export class WebGLVectorRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getControlColor(scene: MapScene, sequence: number): RgbaColor {
|
getControlColor(scene: MapScene, sequence: number): RgbaColor {
|
||||||
|
if (scene.readyControlSequences.includes(sequence)) {
|
||||||
|
return READY_CONTROL_COLOR
|
||||||
|
}
|
||||||
|
|
||||||
if (scene.activeControlSequences.includes(sequence)) {
|
if (scene.activeControlSequences.includes(sequence)) {
|
||||||
return scene.controlVisualMode === 'multi-target' ? MULTI_ACTIVE_CONTROL_COLOR : ACTIVE_CONTROL_COLOR
|
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
|
return COMPLETED_ROUTE_COLOR
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.isSkippedControl(scene, sequence)) {
|
||||||
|
return SKIPPED_ROUTE_COLOR
|
||||||
|
}
|
||||||
|
|
||||||
return COURSE_COLOR
|
return COURSE_COLOR
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -633,7 +754,7 @@ export class WebGLVectorRenderer {
|
|||||||
color: RgbaColor,
|
color: RgbaColor,
|
||||||
scene: MapScene,
|
scene: MapScene,
|
||||||
): void {
|
): void {
|
||||||
const segments = 36
|
const segments = this.getRingSegments(scene)
|
||||||
for (let index = 0; index < segments; index += 1) {
|
for (let index = 0; index < segments; index += 1) {
|
||||||
const startAngle = index / segments * Math.PI * 2
|
const startAngle = index / segments * Math.PI * 2
|
||||||
const endAngle = (index + 1) / segments * Math.PI * 2
|
const endAngle = (index + 1) / segments * Math.PI * 2
|
||||||
@@ -682,7 +803,7 @@ export class WebGLVectorRenderer {
|
|||||||
color: RgbaColor,
|
color: RgbaColor,
|
||||||
scene: MapScene,
|
scene: MapScene,
|
||||||
): void {
|
): void {
|
||||||
const segments = 20
|
const segments = this.getCircleSegments(scene)
|
||||||
const center = this.toClip(centerX, centerY, scene)
|
const center = this.toClip(centerX, centerY, scene)
|
||||||
for (let index = 0; index < segments; index += 1) {
|
for (let index = 0; index < segments; index += 1) {
|
||||||
const startAngle = index / segments * Math.PI * 2
|
const startAngle = index / segments * Math.PI * 2
|
||||||
|
|||||||
@@ -5,7 +5,13 @@ export interface CompassHeadingControllerCallbacks {
|
|||||||
|
|
||||||
type SensorSource = 'compass' | 'motion' | null
|
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 {
|
function normalizeHeadingDeg(headingDeg: number): number {
|
||||||
const normalized = headingDeg % 360
|
const normalized = headingDeg % 360
|
||||||
@@ -41,6 +47,7 @@ export class CompassHeadingController {
|
|||||||
rollDeg: number | null
|
rollDeg: number | null
|
||||||
motionReady: boolean
|
motionReady: boolean
|
||||||
compassReady: boolean
|
compassReady: boolean
|
||||||
|
tuningProfile: CompassTuningProfile
|
||||||
|
|
||||||
constructor(callbacks: CompassHeadingControllerCallbacks) {
|
constructor(callbacks: CompassHeadingControllerCallbacks) {
|
||||||
this.callbacks = callbacks
|
this.callbacks = callbacks
|
||||||
@@ -53,6 +60,7 @@ export class CompassHeadingController {
|
|||||||
this.rollDeg = null
|
this.rollDeg = null
|
||||||
this.motionReady = false
|
this.motionReady = false
|
||||||
this.compassReady = false
|
this.compassReady = false
|
||||||
|
this.tuningProfile = 'balanced'
|
||||||
}
|
}
|
||||||
|
|
||||||
start(): void {
|
start(): void {
|
||||||
@@ -99,6 +107,10 @@ export class CompassHeadingController {
|
|||||||
this.stop()
|
this.stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setTuningProfile(profile: CompassTuningProfile): void {
|
||||||
|
this.tuningProfile = profile
|
||||||
|
}
|
||||||
|
|
||||||
startMotionSource(previousMessage: string): void {
|
startMotionSource(previousMessage: string): void {
|
||||||
if (typeof wx.startDeviceMotionListening !== 'function' || typeof wx.onDeviceMotionChange !== 'function') {
|
if (typeof wx.startDeviceMotionListening !== 'function' || typeof wx.onDeviceMotionChange !== 'function') {
|
||||||
this.callbacks.onError(previousMessage)
|
this.callbacks.onError(previousMessage)
|
||||||
@@ -111,14 +123,13 @@ export class CompassHeadingController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.pitchDeg = typeof result.beta === 'number' && !Number.isNaN(result.beta)
|
this.pitchDeg = typeof result.beta === 'number' && !Number.isNaN(result.beta)
|
||||||
? result.beta * 180 / Math.PI
|
? result.beta
|
||||||
: null
|
: null
|
||||||
this.rollDeg = typeof result.gamma === 'number' && !Number.isNaN(result.gamma)
|
this.rollDeg = typeof result.gamma === 'number' && !Number.isNaN(result.gamma)
|
||||||
? result.gamma * 180 / Math.PI
|
? result.gamma
|
||||||
: null
|
: null
|
||||||
|
|
||||||
const alphaDeg = result.alpha * 180 / Math.PI
|
this.applyAbsoluteHeading(normalizeHeadingDeg(360 - result.alpha), 'motion')
|
||||||
this.applyAbsoluteHeading(normalizeHeadingDeg(360 - alphaDeg), 'motion')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.motionCallback = callback
|
this.motionCallback = callback
|
||||||
@@ -163,10 +174,11 @@ export class CompassHeadingController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
applyAbsoluteHeading(headingDeg: number, source: 'compass' | 'motion'): void {
|
applyAbsoluteHeading(headingDeg: number, source: 'compass' | 'motion'): void {
|
||||||
|
const headingCorrection = HEADING_CORRECTION_BY_PROFILE[this.tuningProfile]
|
||||||
if (this.absoluteHeadingDeg === null) {
|
if (this.absoluteHeadingDeg === null) {
|
||||||
this.absoluteHeadingDeg = headingDeg
|
this.absoluteHeadingDeg = headingDeg
|
||||||
} else {
|
} else {
|
||||||
this.absoluteHeadingDeg = interpolateHeadingDeg(this.absoluteHeadingDeg, headingDeg, ABSOLUTE_HEADING_CORRECTION)
|
this.absoluteHeadingDeg = interpolateHeadingDeg(this.absoluteHeadingDeg, headingDeg, headingCorrection)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.source = source
|
this.source = source
|
||||||
@@ -200,5 +212,3 @@ export class CompassHeadingController {
|
|||||||
this.compassCallback = null
|
this.compassCallback = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { type AnimationLevel } from '../../utils/animationLevel'
|
||||||
|
|
||||||
export type FeedbackCueKey =
|
export type FeedbackCueKey =
|
||||||
| 'session_started'
|
| 'session_started'
|
||||||
| 'session_finished'
|
| 'session_finished'
|
||||||
@@ -14,7 +16,9 @@ export type UiPunchFeedbackMotion = 'none' | 'pop' | 'success' | 'warning'
|
|||||||
export type UiContentCardMotion = 'none' | 'pop' | 'finish'
|
export type UiContentCardMotion = 'none' | 'pop' | 'finish'
|
||||||
export type UiPunchButtonMotion = 'none' | 'ready' | 'warning'
|
export type UiPunchButtonMotion = 'none' | 'ready' | 'warning'
|
||||||
export type UiMapPulseMotion = 'none' | 'ready' | 'control' | 'finish'
|
export type UiMapPulseMotion = 'none' | 'ready' | 'control' | 'finish'
|
||||||
export type UiStageMotion = 'none' | 'finish'
|
export type UiStageMotion = 'none' | 'control' | 'finish'
|
||||||
|
export type UiHudProgressMotion = 'none' | 'success' | 'finish'
|
||||||
|
export type UiHudDistanceMotion = 'none' | 'success'
|
||||||
|
|
||||||
export interface HapticCueConfig {
|
export interface HapticCueConfig {
|
||||||
enabled: boolean
|
enabled: boolean
|
||||||
@@ -28,6 +32,8 @@ export interface UiCueConfig {
|
|||||||
punchButtonMotion: UiPunchButtonMotion
|
punchButtonMotion: UiPunchButtonMotion
|
||||||
mapPulseMotion: UiMapPulseMotion
|
mapPulseMotion: UiMapPulseMotion
|
||||||
stageMotion: UiStageMotion
|
stageMotion: UiStageMotion
|
||||||
|
hudProgressMotion: UiHudProgressMotion
|
||||||
|
hudDistanceMotion: UiHudDistanceMotion
|
||||||
durationMs: number
|
durationMs: number
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,6 +47,10 @@ export interface GameUiEffectsConfig {
|
|||||||
cues: Record<FeedbackCueKey, UiCueConfig>
|
cues: Record<FeedbackCueKey, UiCueConfig>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ResolvedGameUiEffectsConfig extends GameUiEffectsConfig {
|
||||||
|
animationLevel: AnimationLevel
|
||||||
|
}
|
||||||
|
|
||||||
export interface PartialHapticCueConfig {
|
export interface PartialHapticCueConfig {
|
||||||
enabled?: boolean
|
enabled?: boolean
|
||||||
pattern?: HapticPattern
|
pattern?: HapticPattern
|
||||||
@@ -53,6 +63,8 @@ export interface PartialUiCueConfig {
|
|||||||
punchButtonMotion?: UiPunchButtonMotion
|
punchButtonMotion?: UiPunchButtonMotion
|
||||||
mapPulseMotion?: UiMapPulseMotion
|
mapPulseMotion?: UiMapPulseMotion
|
||||||
stageMotion?: UiStageMotion
|
stageMotion?: UiStageMotion
|
||||||
|
hudProgressMotion?: UiHudProgressMotion
|
||||||
|
hudDistanceMotion?: UiHudDistanceMotion
|
||||||
durationMs?: number
|
durationMs?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,15 +96,15 @@ export const DEFAULT_GAME_HAPTICS_CONFIG: GameHapticsConfig = {
|
|||||||
export const DEFAULT_GAME_UI_EFFECTS_CONFIG: GameUiEffectsConfig = {
|
export const DEFAULT_GAME_UI_EFFECTS_CONFIG: GameUiEffectsConfig = {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
cues: {
|
cues: {
|
||||||
session_started: { enabled: false, punchFeedbackMotion: 'none', contentCardMotion: 'none', punchButtonMotion: 'none', mapPulseMotion: 'none', stageMotion: 'none', durationMs: 0 },
|
session_started: { enabled: false, punchFeedbackMotion: 'none', contentCardMotion: 'none', punchButtonMotion: 'none', mapPulseMotion: 'none', stageMotion: 'none', hudProgressMotion: 'none', hudDistanceMotion: 'none', durationMs: 0 },
|
||||||
session_finished: { enabled: false, punchFeedbackMotion: 'none', contentCardMotion: 'none', punchButtonMotion: 'none', mapPulseMotion: 'none', stageMotion: 'none', durationMs: 0 },
|
session_finished: { enabled: false, punchFeedbackMotion: 'none', contentCardMotion: 'none', punchButtonMotion: 'none', mapPulseMotion: 'none', stageMotion: 'none', hudProgressMotion: 'none', hudDistanceMotion: 'none', durationMs: 0 },
|
||||||
'control_completed:start': { enabled: true, punchFeedbackMotion: 'success', contentCardMotion: 'pop', punchButtonMotion: 'none', mapPulseMotion: 'control', stageMotion: 'none', durationMs: 0 },
|
'control_completed:start': { enabled: true, punchFeedbackMotion: 'success', contentCardMotion: 'pop', punchButtonMotion: 'none', mapPulseMotion: 'control', stageMotion: 'control', hudProgressMotion: 'success', hudDistanceMotion: 'success', durationMs: 560 },
|
||||||
'control_completed:control': { enabled: true, punchFeedbackMotion: 'success', contentCardMotion: 'pop', punchButtonMotion: 'none', mapPulseMotion: 'control', stageMotion: 'none', durationMs: 0 },
|
'control_completed:control': { enabled: true, punchFeedbackMotion: 'success', contentCardMotion: 'pop', punchButtonMotion: 'none', mapPulseMotion: 'control', stageMotion: 'control', hudProgressMotion: 'success', hudDistanceMotion: 'success', durationMs: 560 },
|
||||||
'control_completed:finish': { enabled: true, punchFeedbackMotion: 'success', contentCardMotion: 'finish', punchButtonMotion: 'none', mapPulseMotion: 'finish', stageMotion: 'finish', durationMs: 0 },
|
'control_completed:finish': { enabled: true, punchFeedbackMotion: 'success', contentCardMotion: 'finish', punchButtonMotion: 'none', mapPulseMotion: 'finish', stageMotion: 'finish', hudProgressMotion: 'finish', hudDistanceMotion: 'success', durationMs: 680 },
|
||||||
'punch_feedback:warning': { enabled: true, punchFeedbackMotion: 'warning', contentCardMotion: 'none', punchButtonMotion: 'warning', mapPulseMotion: 'none', stageMotion: 'none', durationMs: 560 },
|
'punch_feedback:warning': { enabled: true, punchFeedbackMotion: 'warning', contentCardMotion: 'none', punchButtonMotion: 'warning', mapPulseMotion: 'none', stageMotion: 'none', hudProgressMotion: 'none', hudDistanceMotion: 'none', durationMs: 560 },
|
||||||
'guidance:searching': { enabled: false, punchFeedbackMotion: 'none', contentCardMotion: 'none', punchButtonMotion: 'none', mapPulseMotion: 'none', stageMotion: 'none', durationMs: 0 },
|
'guidance:searching': { enabled: false, punchFeedbackMotion: 'none', contentCardMotion: 'none', punchButtonMotion: 'none', mapPulseMotion: 'none', stageMotion: 'none', hudProgressMotion: 'none', hudDistanceMotion: 'none', durationMs: 0 },
|
||||||
'guidance:approaching': { enabled: false, punchFeedbackMotion: 'none', contentCardMotion: 'none', punchButtonMotion: 'none', mapPulseMotion: 'none', stageMotion: 'none', durationMs: 0 },
|
'guidance:approaching': { enabled: false, punchFeedbackMotion: 'none', contentCardMotion: 'none', punchButtonMotion: 'none', mapPulseMotion: 'none', stageMotion: 'none', hudProgressMotion: 'none', hudDistanceMotion: 'none', durationMs: 0 },
|
||||||
'guidance:ready': { enabled: true, punchFeedbackMotion: 'none', contentCardMotion: 'none', punchButtonMotion: 'ready', mapPulseMotion: 'ready', stageMotion: 'none', durationMs: 900 },
|
'guidance:ready': { enabled: true, punchFeedbackMotion: 'none', contentCardMotion: 'none', punchButtonMotion: 'ready', mapPulseMotion: 'ready', stageMotion: 'none', hudProgressMotion: 'none', hudDistanceMotion: 'none', durationMs: 900 },
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,6 +127,8 @@ function mergeUiCue(baseCue: UiCueConfig, override?: PartialUiCueConfig): UiCueC
|
|||||||
punchButtonMotion: override && override.punchButtonMotion ? override.punchButtonMotion : baseCue.punchButtonMotion,
|
punchButtonMotion: override && override.punchButtonMotion ? override.punchButtonMotion : baseCue.punchButtonMotion,
|
||||||
mapPulseMotion: override && override.mapPulseMotion ? override.mapPulseMotion : baseCue.mapPulseMotion,
|
mapPulseMotion: override && override.mapPulseMotion ? override.mapPulseMotion : baseCue.mapPulseMotion,
|
||||||
stageMotion: override && override.stageMotion ? override.stageMotion : baseCue.stageMotion,
|
stageMotion: override && override.stageMotion ? override.stageMotion : baseCue.stageMotion,
|
||||||
|
hudProgressMotion: override && override.hudProgressMotion ? override.hudProgressMotion : baseCue.hudProgressMotion,
|
||||||
|
hudDistanceMotion: override && override.hudDistanceMotion ? override.hudDistanceMotion : baseCue.hudDistanceMotion,
|
||||||
durationMs: clampDuration(Number(override && override.durationMs), baseCue.durationMs),
|
durationMs: clampDuration(Number(override && override.durationMs), baseCue.durationMs),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { DEFAULT_GAME_AUDIO_CONFIG, type GameAudioConfig } from '../audio/audioConfig'
|
import { DEFAULT_GAME_AUDIO_CONFIG, type GameAudioConfig } from '../audio/audioConfig'
|
||||||
import { SoundDirector } from '../audio/soundDirector'
|
import { SoundDirector } from '../audio/soundDirector'
|
||||||
import { type GameEffect } from '../core/gameResult'
|
import { type GameEffect } from '../core/gameResult'
|
||||||
|
import { type AnimationLevel } from '../../utils/animationLevel'
|
||||||
import {
|
import {
|
||||||
DEFAULT_GAME_HAPTICS_CONFIG,
|
DEFAULT_GAME_HAPTICS_CONFIG,
|
||||||
DEFAULT_GAME_UI_EFFECTS_CONFIG,
|
DEFAULT_GAME_UI_EFFECTS_CONFIG,
|
||||||
@@ -41,6 +42,9 @@ export class FeedbackDirector {
|
|||||||
|
|
||||||
reset(): void {
|
reset(): void {
|
||||||
this.soundDirector.resetContexts()
|
this.soundDirector.resetContexts()
|
||||||
|
this.uiEffectDirector.clearPunchButtonMotion()
|
||||||
|
this.uiEffectDirector.clearHudProgressMotion()
|
||||||
|
this.uiEffectDirector.clearHudDistanceMotion()
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy(): void {
|
destroy(): void {
|
||||||
@@ -49,6 +53,10 @@ export class FeedbackDirector {
|
|||||||
this.uiEffectDirector.destroy()
|
this.uiEffectDirector.destroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setAnimationLevel(level: AnimationLevel): void {
|
||||||
|
this.uiEffectDirector.setAnimationLevel(level)
|
||||||
|
}
|
||||||
|
|
||||||
setAppAudioMode(mode: 'foreground' | 'background'): void {
|
setAppAudioMode(mode: 'foreground' | 'background'): void {
|
||||||
this.soundDirector.setAppAudioMode(mode)
|
this.soundDirector.setAppAudioMode(mode)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
import { type GameEffect } from '../core/gameResult'
|
import { type GameEffect } from '../core/gameResult'
|
||||||
|
import { type AnimationLevel } from '../../utils/animationLevel'
|
||||||
import {
|
import {
|
||||||
DEFAULT_GAME_UI_EFFECTS_CONFIG,
|
DEFAULT_GAME_UI_EFFECTS_CONFIG,
|
||||||
type FeedbackCueKey,
|
type FeedbackCueKey,
|
||||||
type GameUiEffectsConfig,
|
type GameUiEffectsConfig,
|
||||||
type UiContentCardMotion,
|
type UiContentCardMotion,
|
||||||
|
type UiHudDistanceMotion,
|
||||||
|
type UiHudProgressMotion,
|
||||||
type UiMapPulseMotion,
|
type UiMapPulseMotion,
|
||||||
type UiPunchButtonMotion,
|
type UiPunchButtonMotion,
|
||||||
type UiPunchFeedbackMotion,
|
type UiPunchFeedbackMotion,
|
||||||
|
type UiCueConfig,
|
||||||
type UiStageMotion,
|
type UiStageMotion,
|
||||||
} from './feedbackConfig'
|
} from './feedbackConfig'
|
||||||
|
|
||||||
@@ -14,6 +18,8 @@ export interface UiEffectHost {
|
|||||||
showPunchFeedback: (text: string, tone: 'neutral' | 'success' | 'warning', motionClass?: string) => void
|
showPunchFeedback: (text: string, tone: 'neutral' | 'success' | 'warning', motionClass?: string) => void
|
||||||
showContentCard: (title: string, body: string, motionClass?: string) => void
|
showContentCard: (title: string, body: string, motionClass?: string) => void
|
||||||
setPunchButtonFxClass: (className: string) => void
|
setPunchButtonFxClass: (className: string) => void
|
||||||
|
setHudProgressFxClass: (className: string) => void
|
||||||
|
setHudDistanceFxClass: (className: string) => void
|
||||||
showMapPulse: (controlId: string, motionClass?: string) => void
|
showMapPulse: (controlId: string, motionClass?: string) => void
|
||||||
showStageFx: (className: string) => void
|
showStageFx: (className: string) => void
|
||||||
}
|
}
|
||||||
@@ -23,30 +29,46 @@ export class UiEffectDirector {
|
|||||||
config: GameUiEffectsConfig
|
config: GameUiEffectsConfig
|
||||||
host: UiEffectHost
|
host: UiEffectHost
|
||||||
punchButtonMotionTimer: number
|
punchButtonMotionTimer: number
|
||||||
|
hudProgressMotionTimer: number
|
||||||
|
hudDistanceMotionTimer: number
|
||||||
punchButtonMotionToggle: boolean
|
punchButtonMotionToggle: boolean
|
||||||
|
animationLevel: AnimationLevel
|
||||||
|
|
||||||
constructor(host: UiEffectHost, config: GameUiEffectsConfig = DEFAULT_GAME_UI_EFFECTS_CONFIG) {
|
constructor(host: UiEffectHost, config: GameUiEffectsConfig = DEFAULT_GAME_UI_EFFECTS_CONFIG) {
|
||||||
this.enabled = true
|
this.enabled = true
|
||||||
this.host = host
|
this.host = host
|
||||||
this.config = config
|
this.config = config
|
||||||
this.punchButtonMotionTimer = 0
|
this.punchButtonMotionTimer = 0
|
||||||
|
this.hudProgressMotionTimer = 0
|
||||||
|
this.hudDistanceMotionTimer = 0
|
||||||
this.punchButtonMotionToggle = false
|
this.punchButtonMotionToggle = false
|
||||||
|
this.animationLevel = 'standard'
|
||||||
}
|
}
|
||||||
|
|
||||||
configure(config: GameUiEffectsConfig): void {
|
configure(config: GameUiEffectsConfig): void {
|
||||||
this.config = config
|
this.config = config
|
||||||
this.clearPunchButtonMotion()
|
this.clearPunchButtonMotion()
|
||||||
|
this.clearHudProgressMotion()
|
||||||
|
this.clearHudDistanceMotion()
|
||||||
}
|
}
|
||||||
|
|
||||||
setEnabled(enabled: boolean): void {
|
setEnabled(enabled: boolean): void {
|
||||||
this.enabled = enabled
|
this.enabled = enabled
|
||||||
if (!enabled) {
|
if (!enabled) {
|
||||||
this.clearPunchButtonMotion()
|
this.clearPunchButtonMotion()
|
||||||
|
this.clearHudProgressMotion()
|
||||||
|
this.clearHudDistanceMotion()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setAnimationLevel(level: AnimationLevel): void {
|
||||||
|
this.animationLevel = level
|
||||||
|
}
|
||||||
|
|
||||||
destroy(): void {
|
destroy(): void {
|
||||||
this.clearPunchButtonMotion()
|
this.clearPunchButtonMotion()
|
||||||
|
this.clearHudProgressMotion()
|
||||||
|
this.clearHudDistanceMotion()
|
||||||
}
|
}
|
||||||
|
|
||||||
clearPunchButtonMotion(): void {
|
clearPunchButtonMotion(): void {
|
||||||
@@ -57,6 +79,22 @@ export class UiEffectDirector {
|
|||||||
this.host.setPunchButtonFxClass('')
|
this.host.setPunchButtonFxClass('')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clearHudProgressMotion(): void {
|
||||||
|
if (this.hudProgressMotionTimer) {
|
||||||
|
clearTimeout(this.hudProgressMotionTimer)
|
||||||
|
this.hudProgressMotionTimer = 0
|
||||||
|
}
|
||||||
|
this.host.setHudProgressFxClass('')
|
||||||
|
}
|
||||||
|
|
||||||
|
clearHudDistanceMotion(): void {
|
||||||
|
if (this.hudDistanceMotionTimer) {
|
||||||
|
clearTimeout(this.hudDistanceMotionTimer)
|
||||||
|
this.hudDistanceMotionTimer = 0
|
||||||
|
}
|
||||||
|
this.host.setHudDistanceFxClass('')
|
||||||
|
}
|
||||||
|
|
||||||
getPunchFeedbackMotionClass(motion: UiPunchFeedbackMotion): string {
|
getPunchFeedbackMotionClass(motion: UiPunchFeedbackMotion): string {
|
||||||
if (motion === 'warning') {
|
if (motion === 'warning') {
|
||||||
return 'game-punch-feedback--fx-warning'
|
return 'game-punch-feedback--fx-warning'
|
||||||
@@ -94,12 +132,32 @@ export class UiEffectDirector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getStageMotionClass(motion: UiStageMotion): string {
|
getStageMotionClass(motion: UiStageMotion): string {
|
||||||
|
if (motion === 'control') {
|
||||||
|
return 'map-stage__stage-fx--control'
|
||||||
|
}
|
||||||
if (motion === 'finish') {
|
if (motion === 'finish') {
|
||||||
return 'map-stage__stage-fx--finish'
|
return 'map-stage__stage-fx--finish'
|
||||||
}
|
}
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getHudProgressMotionClass(motion: UiHudProgressMotion): string {
|
||||||
|
if (motion === 'finish') {
|
||||||
|
return 'race-panel__progress--fx-finish'
|
||||||
|
}
|
||||||
|
if (motion === 'success') {
|
||||||
|
return 'race-panel__progress--fx-success'
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
getHudDistanceMotionClass(motion: UiHudDistanceMotion): string {
|
||||||
|
if (motion === 'success') {
|
||||||
|
return 'race-panel__metric-group--fx-distance-success'
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
triggerPunchButtonMotion(motion: UiPunchButtonMotion, durationMs: number): void {
|
triggerPunchButtonMotion(motion: UiPunchButtonMotion, durationMs: number): void {
|
||||||
if (motion === 'none') {
|
if (motion === 'none') {
|
||||||
return
|
return
|
||||||
@@ -121,7 +179,37 @@ export class UiEffectDirector {
|
|||||||
}, durationMs) as unknown as number
|
}, durationMs) as unknown as number
|
||||||
}
|
}
|
||||||
|
|
||||||
getCue(key: FeedbackCueKey) {
|
triggerHudProgressMotion(motion: UiHudProgressMotion, durationMs: number): void {
|
||||||
|
const className = this.getHudProgressMotionClass(motion)
|
||||||
|
if (!className) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.host.setHudProgressFxClass(className)
|
||||||
|
if (this.hudProgressMotionTimer) {
|
||||||
|
clearTimeout(this.hudProgressMotionTimer)
|
||||||
|
}
|
||||||
|
this.hudProgressMotionTimer = setTimeout(() => {
|
||||||
|
this.hudProgressMotionTimer = 0
|
||||||
|
this.host.setHudProgressFxClass('')
|
||||||
|
}, durationMs) as unknown as number
|
||||||
|
}
|
||||||
|
|
||||||
|
triggerHudDistanceMotion(motion: UiHudDistanceMotion, durationMs: number): void {
|
||||||
|
const className = this.getHudDistanceMotionClass(motion)
|
||||||
|
if (!className) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.host.setHudDistanceFxClass(className)
|
||||||
|
if (this.hudDistanceMotionTimer) {
|
||||||
|
clearTimeout(this.hudDistanceMotionTimer)
|
||||||
|
}
|
||||||
|
this.hudDistanceMotionTimer = setTimeout(() => {
|
||||||
|
this.hudDistanceMotionTimer = 0
|
||||||
|
this.host.setHudDistanceFxClass('')
|
||||||
|
}, durationMs) as unknown as number
|
||||||
|
}
|
||||||
|
|
||||||
|
getCue(key: FeedbackCueKey): UiCueConfig | null {
|
||||||
if (!this.enabled || !this.config.enabled) {
|
if (!this.enabled || !this.config.enabled) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -131,7 +219,16 @@ export class UiEffectDirector {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
return cue
|
if (this.animationLevel === 'standard') {
|
||||||
|
return cue
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...cue,
|
||||||
|
stageMotion: 'none' as const,
|
||||||
|
hudDistanceMotion: 'none' as const,
|
||||||
|
durationMs: cue.durationMs > 0 ? Math.max(260, Math.round(cue.durationMs * 0.6)) : 0,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleEffects(effects: GameEffect[]): void {
|
handleEffects(effects: GameEffect[]): void {
|
||||||
@@ -172,6 +269,10 @@ export class UiEffectDirector {
|
|||||||
if (cue && cue.stageMotion !== 'none') {
|
if (cue && cue.stageMotion !== 'none') {
|
||||||
this.host.showStageFx(this.getStageMotionClass(cue.stageMotion))
|
this.host.showStageFx(this.getStageMotionClass(cue.stageMotion))
|
||||||
}
|
}
|
||||||
|
if (cue) {
|
||||||
|
this.triggerHudProgressMotion(cue.hudProgressMotion, cue.durationMs)
|
||||||
|
this.triggerHudDistanceMotion(cue.hudDistanceMotion, cue.durationMs)
|
||||||
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,10 +289,14 @@ export class UiEffectDirector {
|
|||||||
|
|
||||||
if (effect.type === 'session_finished') {
|
if (effect.type === 'session_finished') {
|
||||||
this.clearPunchButtonMotion()
|
this.clearPunchButtonMotion()
|
||||||
|
this.clearHudProgressMotion()
|
||||||
|
this.clearHudDistanceMotion()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (effect.type === 'session_cancelled') {
|
if (effect.type === 'session_cancelled') {
|
||||||
this.clearPunchButtonMotion()
|
this.clearPunchButtonMotion()
|
||||||
|
this.clearHudProgressMotion()
|
||||||
|
this.clearHudDistanceMotion()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,6 +52,44 @@ function interpolateHeadingDeg(currentDeg: number, targetDeg: number, factor: nu
|
|||||||
return normalizeHeadingDeg(currentDeg + normalizeHeadingDeltaDeg(targetDeg - currentDeg) * factor)
|
return normalizeHeadingDeg(currentDeg + normalizeHeadingDeltaDeg(targetDeg - currentDeg) * factor)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveMotionCompassHeadingDeg(
|
||||||
|
alpha: number | null,
|
||||||
|
beta: number | null,
|
||||||
|
gamma: number | null,
|
||||||
|
): number | null {
|
||||||
|
if (alpha === null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (beta === null || gamma === null) {
|
||||||
|
return normalizeHeadingDeg(360 - alpha)
|
||||||
|
}
|
||||||
|
|
||||||
|
const alphaRad = alpha * Math.PI / 180
|
||||||
|
const betaRad = beta * Math.PI / 180
|
||||||
|
const gammaRad = gamma * Math.PI / 180
|
||||||
|
|
||||||
|
const cA = Math.cos(alphaRad)
|
||||||
|
const sA = Math.sin(alphaRad)
|
||||||
|
const sB = Math.sin(betaRad)
|
||||||
|
const cG = Math.cos(gammaRad)
|
||||||
|
const sG = Math.sin(gammaRad)
|
||||||
|
|
||||||
|
const headingX = -cA * sG - sA * sB * cG
|
||||||
|
const headingY = -sA * sG + cA * sB * cG
|
||||||
|
|
||||||
|
if (Math.abs(headingX) < 1e-6 && Math.abs(headingY) < 1e-6) {
|
||||||
|
return normalizeHeadingDeg(360 - alpha)
|
||||||
|
}
|
||||||
|
|
||||||
|
let headingRad = Math.atan2(headingX, headingY)
|
||||||
|
if (headingRad < 0) {
|
||||||
|
headingRad += Math.PI * 2
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalizeHeadingDeg(headingRad * 180 / Math.PI)
|
||||||
|
}
|
||||||
|
|
||||||
function getApproxDistanceMeters(
|
function getApproxDistanceMeters(
|
||||||
a: { lon: number; lat: number },
|
a: { lon: number; lat: number },
|
||||||
b: { lon: number; lat: number },
|
b: { lon: number; lat: number },
|
||||||
@@ -530,13 +568,13 @@ export class TelemetryRuntime {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (event.type === 'device_motion_updated') {
|
if (event.type === 'device_motion_updated') {
|
||||||
const nextDeviceHeadingDeg = event.alpha === null
|
const motionHeadingDeg = resolveMotionCompassHeadingDeg(event.alpha, event.beta, event.gamma)
|
||||||
|
const nextDeviceHeadingDeg = motionHeadingDeg === null
|
||||||
? this.state.deviceHeadingDeg
|
? this.state.deviceHeadingDeg
|
||||||
: (() => {
|
: (() => {
|
||||||
const nextHeadingDeg = normalizeHeadingDeg(360 - event.alpha * 180 / Math.PI)
|
|
||||||
return this.state.deviceHeadingDeg === null
|
return this.state.deviceHeadingDeg === null
|
||||||
? nextHeadingDeg
|
? motionHeadingDeg
|
||||||
: interpolateHeadingDeg(this.state.deviceHeadingDeg, nextHeadingDeg, DEVICE_HEADING_SMOOTHING_ALPHA)
|
: interpolateHeadingDeg(this.state.deviceHeadingDeg, motionHeadingDeg, DEVICE_HEADING_SMOOTHING_ALPHA)
|
||||||
})()
|
})()
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
type MapEngineViewState,
|
type MapEngineViewState,
|
||||||
} from '../../engine/map/mapEngine'
|
} from '../../engine/map/mapEngine'
|
||||||
import { loadRemoteMapConfig } from '../../utils/remoteMapConfig'
|
import { loadRemoteMapConfig } from '../../utils/remoteMapConfig'
|
||||||
|
import { type AnimationLevel } from '../../utils/animationLevel'
|
||||||
type CompassTickData = {
|
type CompassTickData = {
|
||||||
angle: number
|
angle: number
|
||||||
long: boolean
|
long: boolean
|
||||||
@@ -31,9 +32,17 @@ type ScaleRulerMajorMarkData = {
|
|||||||
type SideButtonMode = 'all' | 'left' | 'right' | 'hidden'
|
type SideButtonMode = 'all' | 'left' | 'right' | 'hidden'
|
||||||
type SideActionButtonState = 'muted' | 'default' | 'active'
|
type SideActionButtonState = 'muted' | 'default' | 'active'
|
||||||
type CenterScaleRulerAnchorMode = 'screen-center' | 'compass-center'
|
type CenterScaleRulerAnchorMode = 'screen-center' | 'compass-center'
|
||||||
|
type UserNorthReferenceMode = 'magnetic' | 'true'
|
||||||
|
type StoredUserSettings = {
|
||||||
|
animationLevel?: AnimationLevel
|
||||||
|
northReferenceMode?: UserNorthReferenceMode
|
||||||
|
showCenterScaleRuler?: boolean
|
||||||
|
centerScaleRulerAnchorMode?: CenterScaleRulerAnchorMode
|
||||||
|
}
|
||||||
type MapPageData = MapEngineViewState & {
|
type MapPageData = MapEngineViewState & {
|
||||||
showDebugPanel: boolean
|
showDebugPanel: boolean
|
||||||
showGameInfoPanel: boolean
|
showGameInfoPanel: boolean
|
||||||
|
showSystemSettingsPanel: boolean
|
||||||
showCenterScaleRuler: boolean
|
showCenterScaleRuler: boolean
|
||||||
showPunchHintBanner: boolean
|
showPunchHintBanner: boolean
|
||||||
centerScaleRulerAnchorMode: CenterScaleRulerAnchorMode
|
centerScaleRulerAnchorMode: CenterScaleRulerAnchorMode
|
||||||
@@ -52,6 +61,10 @@ type MapPageData = MapEngineViewState & {
|
|||||||
panelDistanceValueText: string
|
panelDistanceValueText: string
|
||||||
panelProgressText: string
|
panelProgressText: string
|
||||||
panelSpeedValueText: string
|
panelSpeedValueText: string
|
||||||
|
panelTimerFxClass: string
|
||||||
|
panelMileageFxClass: string
|
||||||
|
panelSpeedFxClass: string
|
||||||
|
panelHeartRateFxClass: string
|
||||||
compassTicks: CompassTickData[]
|
compassTicks: CompassTickData[]
|
||||||
compassLabels: CompassLabelData[]
|
compassLabels: CompassLabelData[]
|
||||||
sideButtonMode: SideButtonMode
|
sideButtonMode: SideButtonMode
|
||||||
@@ -59,6 +72,7 @@ type MapPageData = MapEngineViewState & {
|
|||||||
sideButton2Class: string
|
sideButton2Class: string
|
||||||
sideButton4Class: string
|
sideButton4Class: string
|
||||||
sideButton11Class: string
|
sideButton11Class: string
|
||||||
|
sideButton12Class: string
|
||||||
sideButton13Class: string
|
sideButton13Class: string
|
||||||
sideButton14Class: string
|
sideButton14Class: string
|
||||||
sideButton16Class: string
|
sideButton16Class: string
|
||||||
@@ -75,7 +89,8 @@ type MapPageData = MapEngineViewState & {
|
|||||||
showRightButtonGroups: boolean
|
showRightButtonGroups: boolean
|
||||||
showBottomDebugButton: boolean
|
showBottomDebugButton: boolean
|
||||||
}
|
}
|
||||||
const INTERNAL_BUILD_VERSION = 'map-build-261'
|
const INTERNAL_BUILD_VERSION = 'map-build-282'
|
||||||
|
const USER_SETTINGS_STORAGE_KEY = 'cmr_user_settings_v1'
|
||||||
const CLASSIC_REMOTE_GAME_CONFIG_URL = 'https://oss-mbh5.colormaprun.com/gotomars/event/classic-sequential.json'
|
const CLASSIC_REMOTE_GAME_CONFIG_URL = 'https://oss-mbh5.colormaprun.com/gotomars/event/classic-sequential.json'
|
||||||
const SCORE_O_REMOTE_GAME_CONFIG_URL = 'https://oss-mbh5.colormaprun.com/gotomars/event/score-o.json'
|
const SCORE_O_REMOTE_GAME_CONFIG_URL = 'https://oss-mbh5.colormaprun.com/gotomars/event/score-o.json'
|
||||||
const PUNCH_HINT_AUTO_HIDE_MS = 30000
|
const PUNCH_HINT_AUTO_HIDE_MS = 30000
|
||||||
@@ -83,7 +98,43 @@ let mapEngine: MapEngine | null = null
|
|||||||
let stageCanvasAttached = false
|
let stageCanvasAttached = false
|
||||||
let gameInfoPanelSyncTimer = 0
|
let gameInfoPanelSyncTimer = 0
|
||||||
let centerScaleRulerSyncTimer = 0
|
let centerScaleRulerSyncTimer = 0
|
||||||
|
let centerScaleRulerUpdateTimer = 0
|
||||||
let punchHintDismissTimer = 0
|
let punchHintDismissTimer = 0
|
||||||
|
let panelTimerFxTimer = 0
|
||||||
|
let panelMileageFxTimer = 0
|
||||||
|
let panelSpeedFxTimer = 0
|
||||||
|
let panelHeartRateFxTimer = 0
|
||||||
|
let lastCenterScaleRulerStablePatch: Pick<
|
||||||
|
MapPageData,
|
||||||
|
| 'centerScaleRulerVisible'
|
||||||
|
| 'centerScaleRulerCenterXPx'
|
||||||
|
| 'centerScaleRulerZeroYPx'
|
||||||
|
| 'centerScaleRulerHeightPx'
|
||||||
|
| 'centerScaleRulerAxisBottomPx'
|
||||||
|
| 'centerScaleRulerZeroVisible'
|
||||||
|
| 'centerScaleRulerZeroLabel'
|
||||||
|
| 'centerScaleRulerMinorTicks'
|
||||||
|
| 'centerScaleRulerMajorMarks'
|
||||||
|
> = {
|
||||||
|
centerScaleRulerVisible: false,
|
||||||
|
centerScaleRulerCenterXPx: 0,
|
||||||
|
centerScaleRulerZeroYPx: 0,
|
||||||
|
centerScaleRulerHeightPx: 0,
|
||||||
|
centerScaleRulerAxisBottomPx: 0,
|
||||||
|
centerScaleRulerZeroVisible: false,
|
||||||
|
centerScaleRulerZeroLabel: '0 m',
|
||||||
|
centerScaleRulerMinorTicks: [],
|
||||||
|
centerScaleRulerMajorMarks: [],
|
||||||
|
}
|
||||||
|
let centerScaleRulerInputCache: Partial<Pick<
|
||||||
|
MapPageData,
|
||||||
|
'stageWidth'
|
||||||
|
| 'stageHeight'
|
||||||
|
| 'zoom'
|
||||||
|
| 'centerTileY'
|
||||||
|
| 'tileSizePx'
|
||||||
|
| 'previewScale'
|
||||||
|
>> = {}
|
||||||
|
|
||||||
const DEBUG_ONLY_VIEW_KEYS = new Set<string>([
|
const DEBUG_ONLY_VIEW_KEYS = new Set<string>([
|
||||||
'buildVersion',
|
'buildVersion',
|
||||||
@@ -93,14 +144,15 @@ const DEBUG_ONLY_VIEW_KEYS = new Set<string>([
|
|||||||
'mapReadyText',
|
'mapReadyText',
|
||||||
'mapName',
|
'mapName',
|
||||||
'configStatusText',
|
'configStatusText',
|
||||||
'sensorHeadingText',
|
|
||||||
'deviceHeadingText',
|
'deviceHeadingText',
|
||||||
'devicePoseText',
|
'devicePoseText',
|
||||||
'headingConfidenceText',
|
'headingConfidenceText',
|
||||||
'accelerometerText',
|
'accelerometerText',
|
||||||
'gyroscopeText',
|
'gyroscopeText',
|
||||||
'deviceMotionText',
|
'deviceMotionText',
|
||||||
'compassDeclinationText',
|
'compassSourceText',
|
||||||
|
'compassTuningProfile',
|
||||||
|
'compassTuningProfileText',
|
||||||
'northReferenceButtonText',
|
'northReferenceButtonText',
|
||||||
'autoRotateSourceText',
|
'autoRotateSourceText',
|
||||||
'autoRotateCalibrationText',
|
'autoRotateCalibrationText',
|
||||||
@@ -148,6 +200,15 @@ const CENTER_SCALE_RULER_DEP_KEYS = new Set<string>([
|
|||||||
'previewScale',
|
'previewScale',
|
||||||
])
|
])
|
||||||
|
|
||||||
|
const CENTER_SCALE_RULER_CACHE_KEYS: Array<keyof typeof centerScaleRulerInputCache> = [
|
||||||
|
'stageWidth',
|
||||||
|
'stageHeight',
|
||||||
|
'zoom',
|
||||||
|
'centerTileY',
|
||||||
|
'tileSizePx',
|
||||||
|
'previewScale',
|
||||||
|
]
|
||||||
|
|
||||||
const RULER_ONLY_VIEW_KEYS = new Set<string>([
|
const RULER_ONLY_VIEW_KEYS = new Set<string>([
|
||||||
'zoom',
|
'zoom',
|
||||||
'centerTileX',
|
'centerTileX',
|
||||||
@@ -213,12 +274,83 @@ function clearCenterScaleRulerSyncTimer() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function clearCenterScaleRulerUpdateTimer() {
|
||||||
|
if (centerScaleRulerUpdateTimer) {
|
||||||
|
clearTimeout(centerScaleRulerUpdateTimer)
|
||||||
|
centerScaleRulerUpdateTimer = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function clearPunchHintDismissTimer() {
|
function clearPunchHintDismissTimer() {
|
||||||
if (punchHintDismissTimer) {
|
if (punchHintDismissTimer) {
|
||||||
clearTimeout(punchHintDismissTimer)
|
clearTimeout(punchHintDismissTimer)
|
||||||
punchHintDismissTimer = 0
|
punchHintDismissTimer = 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function clearHudFxTimer(key: 'timer' | 'mileage' | 'speed' | 'heartRate') {
|
||||||
|
const timerMap = {
|
||||||
|
timer: panelTimerFxTimer,
|
||||||
|
mileage: panelMileageFxTimer,
|
||||||
|
speed: panelSpeedFxTimer,
|
||||||
|
heartRate: panelHeartRateFxTimer,
|
||||||
|
}
|
||||||
|
const timer = timerMap[key]
|
||||||
|
if (timer) {
|
||||||
|
clearTimeout(timer)
|
||||||
|
}
|
||||||
|
if (key === 'timer') {
|
||||||
|
panelTimerFxTimer = 0
|
||||||
|
} else if (key === 'mileage') {
|
||||||
|
panelMileageFxTimer = 0
|
||||||
|
} else if (key === 'speed') {
|
||||||
|
panelSpeedFxTimer = 0
|
||||||
|
} else {
|
||||||
|
panelHeartRateFxTimer = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateCenterScaleRulerInputCache(patch: Partial<MapPageData>) {
|
||||||
|
for (const key of CENTER_SCALE_RULER_CACHE_KEYS) {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(patch, key)) {
|
||||||
|
;(centerScaleRulerInputCache as Record<string, unknown>)[key] =
|
||||||
|
(patch as Record<string, unknown>)[key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadStoredUserSettings(): StoredUserSettings {
|
||||||
|
try {
|
||||||
|
const stored = wx.getStorageSync(USER_SETTINGS_STORAGE_KEY)
|
||||||
|
if (!stored || typeof stored !== 'object') {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalized = stored as Record<string, unknown>
|
||||||
|
const settings: StoredUserSettings = {}
|
||||||
|
if (normalized.animationLevel === 'standard' || normalized.animationLevel === 'lite') {
|
||||||
|
settings.animationLevel = normalized.animationLevel
|
||||||
|
}
|
||||||
|
if (normalized.northReferenceMode === 'magnetic' || normalized.northReferenceMode === 'true') {
|
||||||
|
settings.northReferenceMode = normalized.northReferenceMode
|
||||||
|
}
|
||||||
|
if (typeof normalized.showCenterScaleRuler === 'boolean') {
|
||||||
|
settings.showCenterScaleRuler = normalized.showCenterScaleRuler
|
||||||
|
}
|
||||||
|
if (normalized.centerScaleRulerAnchorMode === 'screen-center' || normalized.centerScaleRulerAnchorMode === 'compass-center') {
|
||||||
|
settings.centerScaleRulerAnchorMode = normalized.centerScaleRulerAnchorMode
|
||||||
|
}
|
||||||
|
return settings
|
||||||
|
} catch {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function persistStoredUserSettings(settings: StoredUserSettings) {
|
||||||
|
try {
|
||||||
|
wx.setStorageSync(USER_SETTINGS_STORAGE_KEY, settings)
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
function buildSideButtonVisibility(mode: SideButtonMode) {
|
function buildSideButtonVisibility(mode: SideButtonMode) {
|
||||||
return {
|
return {
|
||||||
sideButtonMode: mode,
|
sideButtonMode: mode,
|
||||||
@@ -296,7 +428,7 @@ function getSideActionButtonClass(state: SideActionButtonState): string {
|
|||||||
return 'map-side-button map-side-button--default'
|
return 'map-side-button map-side-button--default'
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildSideButtonState(data: Pick<MapPageData, 'sideButtonMode' | 'showGameInfoPanel' | 'showCenterScaleRuler' | 'centerScaleRulerAnchorMode' | 'skipButtonEnabled' | 'gameSessionStatus' | 'gpsLockEnabled' | 'gpsLockAvailable'>) {
|
function buildSideButtonState(data: Pick<MapPageData, 'sideButtonMode' | 'showGameInfoPanel' | 'showSystemSettingsPanel' | 'showCenterScaleRuler' | 'centerScaleRulerAnchorMode' | 'skipButtonEnabled' | 'gameSessionStatus' | 'gpsLockEnabled' | 'gpsLockAvailable'>) {
|
||||||
const sideButton2State: SideActionButtonState = !data.gpsLockAvailable
|
const sideButton2State: SideActionButtonState = !data.gpsLockAvailable
|
||||||
? 'muted'
|
? 'muted'
|
||||||
: data.gpsLockEnabled
|
: data.gpsLockEnabled
|
||||||
@@ -304,6 +436,7 @@ function buildSideButtonState(data: Pick<MapPageData, 'sideButtonMode' | 'showGa
|
|||||||
: 'default'
|
: 'default'
|
||||||
const sideButton4State: SideActionButtonState = data.gameSessionStatus === 'idle' ? 'default' : 'active'
|
const sideButton4State: SideActionButtonState = data.gameSessionStatus === 'idle' ? 'default' : 'active'
|
||||||
const sideButton11State: SideActionButtonState = data.showGameInfoPanel ? 'active' : 'default'
|
const sideButton11State: SideActionButtonState = data.showGameInfoPanel ? 'active' : 'default'
|
||||||
|
const sideButton12State: SideActionButtonState = data.showSystemSettingsPanel ? 'active' : 'default'
|
||||||
const sideButton13State: SideActionButtonState = data.showCenterScaleRuler ? 'active' : 'default'
|
const sideButton13State: SideActionButtonState = data.showCenterScaleRuler ? 'active' : 'default'
|
||||||
const sideButton14State: SideActionButtonState = !data.showCenterScaleRuler
|
const sideButton14State: SideActionButtonState = !data.showCenterScaleRuler
|
||||||
? 'muted'
|
? 'muted'
|
||||||
@@ -317,6 +450,7 @@ function buildSideButtonState(data: Pick<MapPageData, 'sideButtonMode' | 'showGa
|
|||||||
sideButton2Class: getSideActionButtonClass(sideButton2State),
|
sideButton2Class: getSideActionButtonClass(sideButton2State),
|
||||||
sideButton4Class: getSideActionButtonClass(sideButton4State),
|
sideButton4Class: getSideActionButtonClass(sideButton4State),
|
||||||
sideButton11Class: getSideActionButtonClass(sideButton11State),
|
sideButton11Class: getSideActionButtonClass(sideButton11State),
|
||||||
|
sideButton12Class: getSideActionButtonClass(sideButton12State),
|
||||||
sideButton13Class: getSideActionButtonClass(sideButton13State),
|
sideButton13Class: getSideActionButtonClass(sideButton13State),
|
||||||
sideButton14Class: getSideActionButtonClass(sideButton14State),
|
sideButton14Class: getSideActionButtonClass(sideButton14State),
|
||||||
sideButton16Class: getSideActionButtonClass(sideButton16State),
|
sideButton16Class: getSideActionButtonClass(sideButton16State),
|
||||||
@@ -367,7 +501,7 @@ function formatScaleDistanceLabel(distanceMeters: number): string {
|
|||||||
|
|
||||||
function buildCenterScaleRulerPatch(data: Pick<MapPageData, 'showCenterScaleRuler' | 'centerScaleRulerAnchorMode' | 'stageWidth' | 'stageHeight' | 'topInsetHeight' | 'zoom' | 'centerTileY' | 'tileSizePx' | 'previewScale'>) {
|
function buildCenterScaleRulerPatch(data: Pick<MapPageData, 'showCenterScaleRuler' | 'centerScaleRulerAnchorMode' | 'stageWidth' | 'stageHeight' | 'topInsetHeight' | 'zoom' | 'centerTileY' | 'tileSizePx' | 'previewScale'>) {
|
||||||
if (!data.showCenterScaleRuler) {
|
if (!data.showCenterScaleRuler) {
|
||||||
return {
|
lastCenterScaleRulerStablePatch = {
|
||||||
centerScaleRulerVisible: false,
|
centerScaleRulerVisible: false,
|
||||||
centerScaleRulerCenterXPx: 0,
|
centerScaleRulerCenterXPx: 0,
|
||||||
centerScaleRulerZeroYPx: 0,
|
centerScaleRulerZeroYPx: 0,
|
||||||
@@ -378,20 +512,11 @@ function buildCenterScaleRulerPatch(data: Pick<MapPageData, 'showCenterScaleRule
|
|||||||
centerScaleRulerMinorTicks: [] as ScaleRulerMinorTickData[],
|
centerScaleRulerMinorTicks: [] as ScaleRulerMinorTickData[],
|
||||||
centerScaleRulerMajorMarks: [] as ScaleRulerMajorMarkData[],
|
centerScaleRulerMajorMarks: [] as ScaleRulerMajorMarkData[],
|
||||||
}
|
}
|
||||||
|
return { ...lastCenterScaleRulerStablePatch }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!data.stageWidth || !data.stageHeight) {
|
if (!data.stageWidth || !data.stageHeight) {
|
||||||
return {
|
return { ...lastCenterScaleRulerStablePatch }
|
||||||
centerScaleRulerVisible: false,
|
|
||||||
centerScaleRulerCenterXPx: 0,
|
|
||||||
centerScaleRulerZeroYPx: 0,
|
|
||||||
centerScaleRulerHeightPx: 0,
|
|
||||||
centerScaleRulerAxisBottomPx: 0,
|
|
||||||
centerScaleRulerZeroVisible: false,
|
|
||||||
centerScaleRulerZeroLabel: '0 m',
|
|
||||||
centerScaleRulerMinorTicks: [] as ScaleRulerMinorTickData[],
|
|
||||||
centerScaleRulerMajorMarks: [] as ScaleRulerMajorMarkData[],
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const topPadding = 12
|
const topPadding = 12
|
||||||
@@ -414,15 +539,13 @@ function buildCenterScaleRulerPatch(data: Pick<MapPageData, 'showCenterScaleRule
|
|||||||
|| !Number.isFinite(data.centerTileY)
|
|| !Number.isFinite(data.centerTileY)
|
||||||
) {
|
) {
|
||||||
return {
|
return {
|
||||||
|
...lastCenterScaleRulerStablePatch,
|
||||||
centerScaleRulerVisible: true,
|
centerScaleRulerVisible: true,
|
||||||
centerScaleRulerCenterXPx: Math.round(data.stageWidth / 2),
|
centerScaleRulerCenterXPx: Math.round(data.stageWidth / 2),
|
||||||
centerScaleRulerZeroYPx: zeroYPx,
|
centerScaleRulerZeroYPx: zeroYPx,
|
||||||
centerScaleRulerHeightPx: fallbackHeight,
|
centerScaleRulerHeightPx: lastCenterScaleRulerStablePatch.centerScaleRulerHeightPx || fallbackHeight,
|
||||||
centerScaleRulerAxisBottomPx: coveredBottomPx,
|
centerScaleRulerAxisBottomPx: coveredBottomPx,
|
||||||
centerScaleRulerZeroVisible: data.centerScaleRulerAnchorMode !== 'compass-center',
|
centerScaleRulerZeroVisible: data.centerScaleRulerAnchorMode !== 'compass-center',
|
||||||
centerScaleRulerZeroLabel: '0 m',
|
|
||||||
centerScaleRulerMinorTicks: [] as ScaleRulerMinorTickData[],
|
|
||||||
centerScaleRulerMajorMarks: [] as ScaleRulerMajorMarkData[],
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -435,15 +558,13 @@ function buildCenterScaleRulerPatch(data: Pick<MapPageData, 'showCenterScaleRule
|
|||||||
|
|
||||||
if (!Number.isFinite(effectiveMetersPerPixel) || effectiveMetersPerPixel <= 0 || rulerHeight < 120) {
|
if (!Number.isFinite(effectiveMetersPerPixel) || effectiveMetersPerPixel <= 0 || rulerHeight < 120) {
|
||||||
return {
|
return {
|
||||||
|
...lastCenterScaleRulerStablePatch,
|
||||||
centerScaleRulerVisible: true,
|
centerScaleRulerVisible: true,
|
||||||
centerScaleRulerCenterXPx: Math.round(data.stageWidth / 2),
|
centerScaleRulerCenterXPx: Math.round(data.stageWidth / 2),
|
||||||
centerScaleRulerZeroYPx: zeroYPx,
|
centerScaleRulerZeroYPx: zeroYPx,
|
||||||
centerScaleRulerHeightPx: Math.max(rulerHeight, fallbackHeight),
|
centerScaleRulerHeightPx: lastCenterScaleRulerStablePatch.centerScaleRulerHeightPx || fallbackHeight,
|
||||||
centerScaleRulerAxisBottomPx: coveredBottomPx,
|
centerScaleRulerAxisBottomPx: coveredBottomPx,
|
||||||
centerScaleRulerZeroVisible: data.centerScaleRulerAnchorMode !== 'compass-center',
|
centerScaleRulerZeroVisible: data.centerScaleRulerAnchorMode !== 'compass-center',
|
||||||
centerScaleRulerZeroLabel: '0 m',
|
|
||||||
centerScaleRulerMinorTicks: [] as ScaleRulerMinorTickData[],
|
|
||||||
centerScaleRulerMajorMarks: [] as ScaleRulerMajorMarkData[],
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -480,7 +601,7 @@ function buildCenterScaleRulerPatch(data: Pick<MapPageData, 'showCenterScaleRule
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
lastCenterScaleRulerStablePatch = {
|
||||||
centerScaleRulerVisible: true,
|
centerScaleRulerVisible: true,
|
||||||
centerScaleRulerCenterXPx: Math.round(data.stageWidth / 2),
|
centerScaleRulerCenterXPx: Math.round(data.stageWidth / 2),
|
||||||
centerScaleRulerZeroYPx: zeroYPx,
|
centerScaleRulerZeroYPx: zeroYPx,
|
||||||
@@ -491,6 +612,7 @@ function buildCenterScaleRulerPatch(data: Pick<MapPageData, 'showCenterScaleRule
|
|||||||
centerScaleRulerMinorTicks: minorTicks,
|
centerScaleRulerMinorTicks: minorTicks,
|
||||||
centerScaleRulerMajorMarks: majorMarks,
|
centerScaleRulerMajorMarks: majorMarks,
|
||||||
}
|
}
|
||||||
|
return { ...lastCenterScaleRulerStablePatch }
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildEmptyGameInfoSnapshot(): MapEngineGameInfoSnapshot {
|
function buildEmptyGameInfoSnapshot(): MapEngineGameInfoSnapshot {
|
||||||
@@ -512,6 +634,7 @@ Page({
|
|||||||
data: {
|
data: {
|
||||||
showDebugPanel: false,
|
showDebugPanel: false,
|
||||||
showGameInfoPanel: false,
|
showGameInfoPanel: false,
|
||||||
|
showSystemSettingsPanel: false,
|
||||||
showCenterScaleRuler: false,
|
showCenterScaleRuler: false,
|
||||||
statusBarHeight: 0,
|
statusBarHeight: 0,
|
||||||
topInsetHeight: 12,
|
topInsetHeight: 12,
|
||||||
@@ -572,6 +695,9 @@ Page({
|
|||||||
accelerometerText: '--',
|
accelerometerText: '--',
|
||||||
gyroscopeText: '--',
|
gyroscopeText: '--',
|
||||||
deviceMotionText: '--',
|
deviceMotionText: '--',
|
||||||
|
compassSourceText: '无数据',
|
||||||
|
compassTuningProfile: 'balanced',
|
||||||
|
compassTuningProfileText: '平衡',
|
||||||
punchButtonText: '打点',
|
punchButtonText: '打点',
|
||||||
punchButtonEnabled: false,
|
punchButtonEnabled: false,
|
||||||
skipButtonEnabled: false,
|
skipButtonEnabled: false,
|
||||||
@@ -583,6 +709,8 @@ Page({
|
|||||||
contentCardTitle: '',
|
contentCardTitle: '',
|
||||||
contentCardBody: '',
|
contentCardBody: '',
|
||||||
punchButtonFxClass: '',
|
punchButtonFxClass: '',
|
||||||
|
panelProgressFxClass: '',
|
||||||
|
panelDistanceFxClass: '',
|
||||||
punchFeedbackFxClass: '',
|
punchFeedbackFxClass: '',
|
||||||
contentCardFxClass: '',
|
contentCardFxClass: '',
|
||||||
mapPulseVisible: false,
|
mapPulseVisible: false,
|
||||||
@@ -606,6 +734,7 @@ Page({
|
|||||||
...buildSideButtonState({
|
...buildSideButtonState({
|
||||||
sideButtonMode: 'left',
|
sideButtonMode: 'left',
|
||||||
showGameInfoPanel: false,
|
showGameInfoPanel: false,
|
||||||
|
showSystemSettingsPanel: false,
|
||||||
showCenterScaleRuler: false,
|
showCenterScaleRuler: false,
|
||||||
centerScaleRulerAnchorMode: 'screen-center',
|
centerScaleRulerAnchorMode: 'screen-center',
|
||||||
skipButtonEnabled: false,
|
skipButtonEnabled: false,
|
||||||
@@ -649,7 +778,10 @@ Page({
|
|||||||
nextData.mockHeartRateBridgeUrlDraft = nextPatch.mockHeartRateBridgeUrlText
|
nextData.mockHeartRateBridgeUrlDraft = nextPatch.mockHeartRateBridgeUrlText
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateCenterScaleRulerInputCache(nextPatch)
|
||||||
|
|
||||||
const mergedData = {
|
const mergedData = {
|
||||||
|
...centerScaleRulerInputCache,
|
||||||
...this.data,
|
...this.data,
|
||||||
...nextData,
|
...nextData,
|
||||||
} as MapPageData
|
} as MapPageData
|
||||||
@@ -659,6 +791,7 @@ Page({
|
|||||||
this.data.showCenterScaleRuler
|
this.data.showCenterScaleRuler
|
||||||
&& hasAnyPatchKey(nextPatch as Record<string, unknown>, CENTER_SCALE_RULER_DEP_KEYS)
|
&& hasAnyPatchKey(nextPatch as Record<string, unknown>, CENTER_SCALE_RULER_DEP_KEYS)
|
||||||
) {
|
) {
|
||||||
|
clearCenterScaleRulerUpdateTimer()
|
||||||
Object.assign(derivedPatch, buildCenterScaleRulerPatch(mergedData))
|
Object.assign(derivedPatch, buildCenterScaleRulerPatch(mergedData))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -685,6 +818,57 @@ Page({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const nextAnimationLevel = typeof nextPatch.animationLevel === 'string'
|
||||||
|
? nextPatch.animationLevel
|
||||||
|
: this.data.animationLevel
|
||||||
|
|
||||||
|
if (nextAnimationLevel === 'lite') {
|
||||||
|
clearHudFxTimer('timer')
|
||||||
|
clearHudFxTimer('mileage')
|
||||||
|
clearHudFxTimer('speed')
|
||||||
|
clearHudFxTimer('heartRate')
|
||||||
|
nextData.panelTimerFxClass = ''
|
||||||
|
nextData.panelMileageFxClass = ''
|
||||||
|
nextData.panelSpeedFxClass = ''
|
||||||
|
nextData.panelHeartRateFxClass = ''
|
||||||
|
} else {
|
||||||
|
if (typeof nextPatch.panelTimerText === 'string' && nextPatch.panelTimerText !== this.data.panelTimerText && this.data.panelTimerText !== '00:00:00') {
|
||||||
|
clearHudFxTimer('timer')
|
||||||
|
nextData.panelTimerFxClass = 'race-panel__timer--fx-tick'
|
||||||
|
panelTimerFxTimer = setTimeout(() => {
|
||||||
|
panelTimerFxTimer = 0
|
||||||
|
this.setData({ panelTimerFxClass: '' })
|
||||||
|
}, 320) as unknown as number
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof nextPatch.panelMileageText === 'string' && nextPatch.panelMileageText !== this.data.panelMileageText && this.data.panelMileageText !== '0m') {
|
||||||
|
clearHudFxTimer('mileage')
|
||||||
|
nextData.panelMileageFxClass = 'race-panel__mileage-wrap--fx-update'
|
||||||
|
panelMileageFxTimer = setTimeout(() => {
|
||||||
|
panelMileageFxTimer = 0
|
||||||
|
this.setData({ panelMileageFxClass: '' })
|
||||||
|
}, 360) as unknown as number
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof nextPatch.panelSpeedValueText === 'string' && nextPatch.panelSpeedValueText !== this.data.panelSpeedValueText && this.data.panelSpeedValueText !== '0') {
|
||||||
|
clearHudFxTimer('speed')
|
||||||
|
nextData.panelSpeedFxClass = 'race-panel__metric-group--fx-speed-update'
|
||||||
|
panelSpeedFxTimer = setTimeout(() => {
|
||||||
|
panelSpeedFxTimer = 0
|
||||||
|
this.setData({ panelSpeedFxClass: '' })
|
||||||
|
}, 360) as unknown as number
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof nextPatch.panelHeartRateValueText === 'string' && nextPatch.panelHeartRateValueText !== this.data.panelHeartRateValueText && this.data.panelHeartRateValueText !== '--') {
|
||||||
|
clearHudFxTimer('heartRate')
|
||||||
|
nextData.panelHeartRateFxClass = 'race-panel__metric-group--fx-heart-rate-update'
|
||||||
|
panelHeartRateFxTimer = setTimeout(() => {
|
||||||
|
panelHeartRateFxTimer = 0
|
||||||
|
this.setData({ panelHeartRateFxClass: '' })
|
||||||
|
}, 400) as unknown as number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (Object.keys(nextData).length || Object.keys(derivedPatch).length) {
|
if (Object.keys(nextData).length || Object.keys(derivedPatch).length) {
|
||||||
this.setData({
|
this.setData({
|
||||||
...nextData,
|
...nextData,
|
||||||
@@ -698,22 +882,46 @@ Page({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const storedUserSettings = loadStoredUserSettings()
|
||||||
|
if (storedUserSettings.animationLevel) {
|
||||||
|
mapEngine.handleSetAnimationLevel(storedUserSettings.animationLevel)
|
||||||
|
}
|
||||||
|
if (storedUserSettings.northReferenceMode) {
|
||||||
|
mapEngine.handleSetNorthReferenceMode(storedUserSettings.northReferenceMode)
|
||||||
|
}
|
||||||
|
|
||||||
mapEngine.setDiagnosticUiEnabled(false)
|
mapEngine.setDiagnosticUiEnabled(false)
|
||||||
|
centerScaleRulerInputCache = {
|
||||||
|
stageWidth: 0,
|
||||||
|
stageHeight: 0,
|
||||||
|
zoom: 0,
|
||||||
|
centerTileY: 0,
|
||||||
|
tileSizePx: 0,
|
||||||
|
previewScale: 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialShowCenterScaleRuler = !!storedUserSettings.showCenterScaleRuler
|
||||||
|
const initialCenterScaleRulerAnchorMode = storedUserSettings.centerScaleRulerAnchorMode || 'screen-center'
|
||||||
|
|
||||||
this.setData({
|
this.setData({
|
||||||
...mapEngine.getInitialData(),
|
...mapEngine.getInitialData(),
|
||||||
showDebugPanel: false,
|
showDebugPanel: false,
|
||||||
showGameInfoPanel: false,
|
showGameInfoPanel: false,
|
||||||
|
showSystemSettingsPanel: false,
|
||||||
|
showCenterScaleRuler: initialShowCenterScaleRuler,
|
||||||
statusBarHeight,
|
statusBarHeight,
|
||||||
topInsetHeight: Math.max(statusBarHeight + 12, menuButtonBottom + 20),
|
topInsetHeight: Math.max(statusBarHeight + 12, menuButtonBottom + 20),
|
||||||
hudPanelIndex: 0,
|
hudPanelIndex: 0,
|
||||||
configSourceText: '顺序赛配置',
|
configSourceText: '顺序赛配置',
|
||||||
|
centerScaleRulerAnchorMode: initialCenterScaleRulerAnchorMode,
|
||||||
gameInfoTitle: '当前游戏',
|
gameInfoTitle: '当前游戏',
|
||||||
gameInfoSubtitle: '未开始',
|
gameInfoSubtitle: '未开始',
|
||||||
gameInfoLocalRows: [],
|
gameInfoLocalRows: [],
|
||||||
gameInfoGlobalRows: buildEmptyGameInfoSnapshot().globalRows,
|
gameInfoGlobalRows: buildEmptyGameInfoSnapshot().globalRows,
|
||||||
panelTimerText: '00:00:00',
|
panelTimerText: '00:00:00',
|
||||||
|
panelTimerFxClass: '',
|
||||||
panelMileageText: '0m',
|
panelMileageText: '0m',
|
||||||
|
panelMileageFxClass: '',
|
||||||
panelActionTagText: '目标',
|
panelActionTagText: '目标',
|
||||||
panelDistanceTagText: '点距',
|
panelDistanceTagText: '点距',
|
||||||
panelDistanceValueText: '--',
|
panelDistanceValueText: '--',
|
||||||
@@ -740,6 +948,7 @@ Page({
|
|||||||
mockHeartRateBridgeUrlDraft: 'wss://gs.gotomars.xyz/mock-gps',
|
mockHeartRateBridgeUrlDraft: 'wss://gs.gotomars.xyz/mock-gps',
|
||||||
mockHeartRateText: '--',
|
mockHeartRateText: '--',
|
||||||
panelSpeedValueText: '0',
|
panelSpeedValueText: '0',
|
||||||
|
panelSpeedFxClass: '',
|
||||||
panelTelemetryTone: 'blue',
|
panelTelemetryTone: 'blue',
|
||||||
panelHeartRateZoneNameText: '--',
|
panelHeartRateZoneNameText: '--',
|
||||||
panelHeartRateZoneRangeText: '',
|
panelHeartRateZoneRangeText: '',
|
||||||
@@ -747,6 +956,7 @@ Page({
|
|||||||
heartRateStatusText: '心率带未连接',
|
heartRateStatusText: '心率带未连接',
|
||||||
heartRateDeviceText: '--',
|
heartRateDeviceText: '--',
|
||||||
panelHeartRateValueText: '--',
|
panelHeartRateValueText: '--',
|
||||||
|
panelHeartRateFxClass: '',
|
||||||
panelHeartRateUnitText: '',
|
panelHeartRateUnitText: '',
|
||||||
panelCaloriesValueText: '0',
|
panelCaloriesValueText: '0',
|
||||||
panelCaloriesUnitText: 'kcal',
|
panelCaloriesUnitText: 'kcal',
|
||||||
@@ -760,6 +970,9 @@ Page({
|
|||||||
accelerometerText: '--',
|
accelerometerText: '--',
|
||||||
gyroscopeText: '--',
|
gyroscopeText: '--',
|
||||||
deviceMotionText: '--',
|
deviceMotionText: '--',
|
||||||
|
compassSourceText: '无数据',
|
||||||
|
compassTuningProfile: 'balanced',
|
||||||
|
compassTuningProfileText: '平衡',
|
||||||
punchButtonText: '打点',
|
punchButtonText: '打点',
|
||||||
punchButtonEnabled: false,
|
punchButtonEnabled: false,
|
||||||
skipButtonEnabled: false,
|
skipButtonEnabled: false,
|
||||||
@@ -771,6 +984,8 @@ Page({
|
|||||||
contentCardTitle: '',
|
contentCardTitle: '',
|
||||||
contentCardBody: '',
|
contentCardBody: '',
|
||||||
punchButtonFxClass: '',
|
punchButtonFxClass: '',
|
||||||
|
panelProgressFxClass: '',
|
||||||
|
panelDistanceFxClass: '',
|
||||||
punchFeedbackFxClass: '',
|
punchFeedbackFxClass: '',
|
||||||
contentCardFxClass: '',
|
contentCardFxClass: '',
|
||||||
mapPulseVisible: false,
|
mapPulseVisible: false,
|
||||||
@@ -785,8 +1000,9 @@ Page({
|
|||||||
...buildSideButtonState({
|
...buildSideButtonState({
|
||||||
sideButtonMode: 'left',
|
sideButtonMode: 'left',
|
||||||
showGameInfoPanel: false,
|
showGameInfoPanel: false,
|
||||||
showCenterScaleRuler: false,
|
showSystemSettingsPanel: false,
|
||||||
centerScaleRulerAnchorMode: 'screen-center',
|
showCenterScaleRuler: initialShowCenterScaleRuler,
|
||||||
|
centerScaleRulerAnchorMode: initialCenterScaleRulerAnchorMode,
|
||||||
skipButtonEnabled: false,
|
skipButtonEnabled: false,
|
||||||
gameSessionStatus: 'idle',
|
gameSessionStatus: 'idle',
|
||||||
gpsLockEnabled: false,
|
gpsLockEnabled: false,
|
||||||
@@ -794,8 +1010,8 @@ Page({
|
|||||||
}),
|
}),
|
||||||
...buildCenterScaleRulerPatch({
|
...buildCenterScaleRulerPatch({
|
||||||
...(mapEngine.getInitialData() as MapPageData),
|
...(mapEngine.getInitialData() as MapPageData),
|
||||||
showCenterScaleRuler: false,
|
showCenterScaleRuler: initialShowCenterScaleRuler,
|
||||||
centerScaleRulerAnchorMode: 'screen-center',
|
centerScaleRulerAnchorMode: initialCenterScaleRulerAnchorMode,
|
||||||
stageWidth: 0,
|
stageWidth: 0,
|
||||||
stageHeight: 0,
|
stageHeight: 0,
|
||||||
topInsetHeight: Math.max(statusBarHeight + 12, menuButtonBottom + 20),
|
topInsetHeight: Math.max(statusBarHeight + 12, menuButtonBottom + 20),
|
||||||
@@ -827,7 +1043,12 @@ Page({
|
|||||||
onUnload() {
|
onUnload() {
|
||||||
clearGameInfoPanelSyncTimer()
|
clearGameInfoPanelSyncTimer()
|
||||||
clearCenterScaleRulerSyncTimer()
|
clearCenterScaleRulerSyncTimer()
|
||||||
|
clearCenterScaleRulerUpdateTimer()
|
||||||
clearPunchHintDismissTimer()
|
clearPunchHintDismissTimer()
|
||||||
|
clearHudFxTimer('timer')
|
||||||
|
clearHudFxTimer('mileage')
|
||||||
|
clearHudFxTimer('speed')
|
||||||
|
clearHudFxTimer('heartRate')
|
||||||
if (mapEngine) {
|
if (mapEngine) {
|
||||||
mapEngine.destroy()
|
mapEngine.destroy()
|
||||||
mapEngine = null
|
mapEngine = null
|
||||||
@@ -997,6 +1218,24 @@ Page({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
handleSetCompassTuningSmooth() {
|
||||||
|
if (mapEngine) {
|
||||||
|
mapEngine.handleSetCompassTuningProfile('smooth')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
handleSetCompassTuningBalanced() {
|
||||||
|
if (mapEngine) {
|
||||||
|
mapEngine.handleSetCompassTuningProfile('balanced')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
handleSetCompassTuningResponsive() {
|
||||||
|
if (mapEngine) {
|
||||||
|
mapEngine.handleSetCompassTuningProfile('responsive')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
handleAutoRotateCalibrate() {
|
handleAutoRotateCalibrate() {
|
||||||
if (mapEngine) {
|
if (mapEngine) {
|
||||||
mapEngine.handleAutoRotateCalibrate()
|
mapEngine.handleAutoRotateCalibrate()
|
||||||
@@ -1260,10 +1499,12 @@ Page({
|
|||||||
this.syncGameInfoPanelSnapshot()
|
this.syncGameInfoPanelSnapshot()
|
||||||
this.setData({
|
this.setData({
|
||||||
showDebugPanel: false,
|
showDebugPanel: false,
|
||||||
|
showSystemSettingsPanel: false,
|
||||||
showGameInfoPanel: true,
|
showGameInfoPanel: true,
|
||||||
...buildSideButtonState({
|
...buildSideButtonState({
|
||||||
sideButtonMode: this.data.sideButtonMode,
|
sideButtonMode: this.data.sideButtonMode,
|
||||||
showGameInfoPanel: true,
|
showGameInfoPanel: true,
|
||||||
|
showSystemSettingsPanel: false,
|
||||||
showCenterScaleRuler: this.data.showCenterScaleRuler,
|
showCenterScaleRuler: this.data.showCenterScaleRuler,
|
||||||
centerScaleRulerAnchorMode: this.data.centerScaleRulerAnchorMode,
|
centerScaleRulerAnchorMode: this.data.centerScaleRulerAnchorMode,
|
||||||
skipButtonEnabled: this.data.skipButtonEnabled,
|
skipButtonEnabled: this.data.skipButtonEnabled,
|
||||||
@@ -1281,6 +1522,7 @@ Page({
|
|||||||
...buildSideButtonState({
|
...buildSideButtonState({
|
||||||
sideButtonMode: this.data.sideButtonMode,
|
sideButtonMode: this.data.sideButtonMode,
|
||||||
showGameInfoPanel: false,
|
showGameInfoPanel: false,
|
||||||
|
showSystemSettingsPanel: this.data.showSystemSettingsPanel,
|
||||||
showCenterScaleRuler: this.data.showCenterScaleRuler,
|
showCenterScaleRuler: this.data.showCenterScaleRuler,
|
||||||
centerScaleRulerAnchorMode: this.data.centerScaleRulerAnchorMode,
|
centerScaleRulerAnchorMode: this.data.centerScaleRulerAnchorMode,
|
||||||
skipButtonEnabled: this.data.skipButtonEnabled,
|
skipButtonEnabled: this.data.skipButtonEnabled,
|
||||||
@@ -1293,6 +1535,89 @@ Page({
|
|||||||
|
|
||||||
handleGameInfoPanelTap() {},
|
handleGameInfoPanelTap() {},
|
||||||
|
|
||||||
|
handleOpenSystemSettingsPanel() {
|
||||||
|
clearGameInfoPanelSyncTimer()
|
||||||
|
this.setData({
|
||||||
|
showDebugPanel: false,
|
||||||
|
showGameInfoPanel: false,
|
||||||
|
showSystemSettingsPanel: true,
|
||||||
|
...buildSideButtonState({
|
||||||
|
sideButtonMode: this.data.sideButtonMode,
|
||||||
|
showGameInfoPanel: false,
|
||||||
|
showSystemSettingsPanel: true,
|
||||||
|
showCenterScaleRuler: this.data.showCenterScaleRuler,
|
||||||
|
centerScaleRulerAnchorMode: this.data.centerScaleRulerAnchorMode,
|
||||||
|
skipButtonEnabled: this.data.skipButtonEnabled,
|
||||||
|
gameSessionStatus: this.data.gameSessionStatus,
|
||||||
|
gpsLockEnabled: this.data.gpsLockEnabled,
|
||||||
|
gpsLockAvailable: this.data.gpsLockAvailable,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
handleCloseSystemSettingsPanel() {
|
||||||
|
this.setData({
|
||||||
|
showSystemSettingsPanel: false,
|
||||||
|
...buildSideButtonState({
|
||||||
|
sideButtonMode: this.data.sideButtonMode,
|
||||||
|
showGameInfoPanel: this.data.showGameInfoPanel,
|
||||||
|
showSystemSettingsPanel: false,
|
||||||
|
showCenterScaleRuler: this.data.showCenterScaleRuler,
|
||||||
|
centerScaleRulerAnchorMode: this.data.centerScaleRulerAnchorMode,
|
||||||
|
skipButtonEnabled: this.data.skipButtonEnabled,
|
||||||
|
gameSessionStatus: this.data.gameSessionStatus,
|
||||||
|
gpsLockEnabled: this.data.gpsLockEnabled,
|
||||||
|
gpsLockAvailable: this.data.gpsLockAvailable,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
handleSystemSettingsPanelTap() {},
|
||||||
|
|
||||||
|
handleSetAnimationLevelStandard() {
|
||||||
|
if (!mapEngine) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mapEngine.handleSetAnimationLevel('standard')
|
||||||
|
persistStoredUserSettings({
|
||||||
|
...loadStoredUserSettings(),
|
||||||
|
animationLevel: 'standard',
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
handleSetAnimationLevelLite() {
|
||||||
|
if (!mapEngine) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mapEngine.handleSetAnimationLevel('lite')
|
||||||
|
persistStoredUserSettings({
|
||||||
|
...loadStoredUserSettings(),
|
||||||
|
animationLevel: 'lite',
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
handleSetNorthReferenceMagnetic() {
|
||||||
|
if (!mapEngine) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mapEngine.handleSetNorthReferenceMode('magnetic')
|
||||||
|
persistStoredUserSettings({
|
||||||
|
...loadStoredUserSettings(),
|
||||||
|
northReferenceMode: 'magnetic',
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
handleSetNorthReferenceTrue() {
|
||||||
|
if (!mapEngine) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mapEngine.handleSetNorthReferenceMode('true')
|
||||||
|
persistStoredUserSettings({
|
||||||
|
...loadStoredUserSettings(),
|
||||||
|
northReferenceMode: 'true',
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
handleOverlayTouch() {},
|
handleOverlayTouch() {},
|
||||||
|
|
||||||
handlePunchAction() {
|
handlePunchAction() {
|
||||||
@@ -1318,6 +1643,8 @@ Page({
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
handlePunchHintTap() {},
|
||||||
|
|
||||||
handleHudPanelChange(event: WechatMiniprogram.CustomEvent<{ current: number }>) {
|
handleHudPanelChange(event: WechatMiniprogram.CustomEvent<{ current: number }>) {
|
||||||
this.setData({
|
this.setData({
|
||||||
hudPanelIndex: event.detail.current || 0,
|
hudPanelIndex: event.detail.current || 0,
|
||||||
@@ -1331,6 +1658,7 @@ Page({
|
|||||||
...buildSideButtonState({
|
...buildSideButtonState({
|
||||||
sideButtonMode: nextMode,
|
sideButtonMode: nextMode,
|
||||||
showGameInfoPanel: this.data.showGameInfoPanel,
|
showGameInfoPanel: this.data.showGameInfoPanel,
|
||||||
|
showSystemSettingsPanel: this.data.showSystemSettingsPanel,
|
||||||
showCenterScaleRuler: this.data.showCenterScaleRuler,
|
showCenterScaleRuler: this.data.showCenterScaleRuler,
|
||||||
centerScaleRulerAnchorMode: this.data.centerScaleRulerAnchorMode,
|
centerScaleRulerAnchorMode: this.data.centerScaleRulerAnchorMode,
|
||||||
skipButtonEnabled: this.data.skipButtonEnabled,
|
skipButtonEnabled: this.data.skipButtonEnabled,
|
||||||
@@ -1368,9 +1696,11 @@ Page({
|
|||||||
this.setData({
|
this.setData({
|
||||||
showDebugPanel: nextShowDebugPanel,
|
showDebugPanel: nextShowDebugPanel,
|
||||||
showGameInfoPanel: false,
|
showGameInfoPanel: false,
|
||||||
|
showSystemSettingsPanel: false,
|
||||||
...buildSideButtonState({
|
...buildSideButtonState({
|
||||||
sideButtonMode: this.data.sideButtonMode,
|
sideButtonMode: this.data.sideButtonMode,
|
||||||
showGameInfoPanel: false,
|
showGameInfoPanel: false,
|
||||||
|
showSystemSettingsPanel: false,
|
||||||
showCenterScaleRuler: this.data.showCenterScaleRuler,
|
showCenterScaleRuler: this.data.showCenterScaleRuler,
|
||||||
centerScaleRulerAnchorMode: this.data.centerScaleRulerAnchorMode,
|
centerScaleRulerAnchorMode: this.data.centerScaleRulerAnchorMode,
|
||||||
skipButtonEnabled: this.data.skipButtonEnabled,
|
skipButtonEnabled: this.data.skipButtonEnabled,
|
||||||
@@ -1390,6 +1720,7 @@ Page({
|
|||||||
...buildSideButtonState({
|
...buildSideButtonState({
|
||||||
sideButtonMode: this.data.sideButtonMode,
|
sideButtonMode: this.data.sideButtonMode,
|
||||||
showGameInfoPanel: this.data.showGameInfoPanel,
|
showGameInfoPanel: this.data.showGameInfoPanel,
|
||||||
|
showSystemSettingsPanel: this.data.showSystemSettingsPanel,
|
||||||
showCenterScaleRuler: this.data.showCenterScaleRuler,
|
showCenterScaleRuler: this.data.showCenterScaleRuler,
|
||||||
centerScaleRulerAnchorMode: this.data.centerScaleRulerAnchorMode,
|
centerScaleRulerAnchorMode: this.data.centerScaleRulerAnchorMode,
|
||||||
skipButtonEnabled: this.data.skipButtonEnabled,
|
skipButtonEnabled: this.data.skipButtonEnabled,
|
||||||
@@ -1400,25 +1731,29 @@ Page({
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
handleToggleCenterScaleRuler() {
|
applyCenterScaleRulerSettings(nextEnabled: boolean, nextAnchorMode: CenterScaleRulerAnchorMode) {
|
||||||
const nextEnabled = !this.data.showCenterScaleRuler
|
|
||||||
this.data.showCenterScaleRuler = nextEnabled
|
this.data.showCenterScaleRuler = nextEnabled
|
||||||
|
this.data.centerScaleRulerAnchorMode = nextAnchorMode
|
||||||
clearCenterScaleRulerSyncTimer()
|
clearCenterScaleRulerSyncTimer()
|
||||||
|
clearCenterScaleRulerUpdateTimer()
|
||||||
|
|
||||||
const syncRulerFromEngine = () => {
|
const syncRulerFromEngine = () => {
|
||||||
if (!mapEngine) {
|
if (!mapEngine) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const engineSnapshot = mapEngine.getInitialData() as Partial<MapPageData>
|
const engineSnapshot = mapEngine.getInitialData() as Partial<MapPageData>
|
||||||
|
updateCenterScaleRulerInputCache(engineSnapshot)
|
||||||
const mergedData = {
|
const mergedData = {
|
||||||
...engineSnapshot,
|
...centerScaleRulerInputCache,
|
||||||
...this.data,
|
...this.data,
|
||||||
showCenterScaleRuler: nextEnabled,
|
showCenterScaleRuler: nextEnabled,
|
||||||
|
centerScaleRulerAnchorMode: nextAnchorMode,
|
||||||
} as MapPageData
|
} as MapPageData
|
||||||
|
|
||||||
this.setData({
|
this.setData({
|
||||||
...filterDebugOnlyPatch(engineSnapshot, this.data.showDebugPanel, nextEnabled),
|
...filterDebugOnlyPatch(engineSnapshot, this.data.showDebugPanel, nextEnabled),
|
||||||
showCenterScaleRuler: nextEnabled,
|
showCenterScaleRuler: nextEnabled,
|
||||||
|
centerScaleRulerAnchorMode: nextAnchorMode,
|
||||||
...buildCenterScaleRulerPatch(mergedData),
|
...buildCenterScaleRulerPatch(mergedData),
|
||||||
...buildSideButtonState(mergedData),
|
...buildSideButtonState(mergedData),
|
||||||
})
|
})
|
||||||
@@ -1431,9 +1766,11 @@ Page({
|
|||||||
|
|
||||||
this.setData({
|
this.setData({
|
||||||
showCenterScaleRuler: true,
|
showCenterScaleRuler: true,
|
||||||
|
centerScaleRulerAnchorMode: nextAnchorMode,
|
||||||
...buildSideButtonState({
|
...buildSideButtonState({
|
||||||
...this.data,
|
...this.data,
|
||||||
showCenterScaleRuler: true,
|
showCenterScaleRuler: true,
|
||||||
|
centerScaleRulerAnchorMode: nextAnchorMode,
|
||||||
} as MapPageData),
|
} as MapPageData),
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -1450,6 +1787,42 @@ Page({
|
|||||||
}, 96) as unknown as number
|
}, 96) as unknown as number
|
||||||
},
|
},
|
||||||
|
|
||||||
|
handleSetCenterScaleRulerVisibleOn() {
|
||||||
|
this.applyCenterScaleRulerSettings(true, this.data.centerScaleRulerAnchorMode)
|
||||||
|
persistStoredUserSettings({
|
||||||
|
...loadStoredUserSettings(),
|
||||||
|
showCenterScaleRuler: true,
|
||||||
|
centerScaleRulerAnchorMode: this.data.centerScaleRulerAnchorMode,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
handleSetCenterScaleRulerVisibleOff() {
|
||||||
|
this.applyCenterScaleRulerSettings(false, this.data.centerScaleRulerAnchorMode)
|
||||||
|
persistStoredUserSettings({
|
||||||
|
...loadStoredUserSettings(),
|
||||||
|
showCenterScaleRuler: false,
|
||||||
|
centerScaleRulerAnchorMode: this.data.centerScaleRulerAnchorMode,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
handleSetCenterScaleRulerAnchorScreenCenter() {
|
||||||
|
this.applyCenterScaleRulerSettings(this.data.showCenterScaleRuler, 'screen-center')
|
||||||
|
persistStoredUserSettings({
|
||||||
|
...loadStoredUserSettings(),
|
||||||
|
showCenterScaleRuler: this.data.showCenterScaleRuler,
|
||||||
|
centerScaleRulerAnchorMode: 'screen-center',
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
handleSetCenterScaleRulerAnchorCompassCenter() {
|
||||||
|
this.applyCenterScaleRulerSettings(this.data.showCenterScaleRuler, 'compass-center')
|
||||||
|
persistStoredUserSettings({
|
||||||
|
...loadStoredUserSettings(),
|
||||||
|
showCenterScaleRuler: this.data.showCenterScaleRuler,
|
||||||
|
centerScaleRulerAnchorMode: 'compass-center',
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
handleToggleCenterScaleRulerAnchor() {
|
handleToggleCenterScaleRulerAnchor() {
|
||||||
if (!this.data.showCenterScaleRuler) {
|
if (!this.data.showCenterScaleRuler) {
|
||||||
return
|
return
|
||||||
@@ -1459,9 +1832,10 @@ Page({
|
|||||||
? 'compass-center'
|
? 'compass-center'
|
||||||
: 'screen-center'
|
: 'screen-center'
|
||||||
const engineSnapshot = mapEngine ? (mapEngine.getInitialData() as Partial<MapPageData>) : {}
|
const engineSnapshot = mapEngine ? (mapEngine.getInitialData() as Partial<MapPageData>) : {}
|
||||||
|
updateCenterScaleRulerInputCache(engineSnapshot)
|
||||||
this.data.centerScaleRulerAnchorMode = nextAnchorMode
|
this.data.centerScaleRulerAnchorMode = nextAnchorMode
|
||||||
const mergedData = {
|
const mergedData = {
|
||||||
...engineSnapshot,
|
...centerScaleRulerInputCache,
|
||||||
...this.data,
|
...this.data,
|
||||||
centerScaleRulerAnchorMode: nextAnchorMode,
|
centerScaleRulerAnchorMode: nextAnchorMode,
|
||||||
} as MapPageData
|
} as MapPageData
|
||||||
|
|||||||
@@ -28,10 +28,6 @@
|
|||||||
<view class="map-stage__map-pulse {{mapPulseFxClass}}" wx:if="{{mapPulseVisible}}" style="left: {{mapPulseLeftPx}}px; top: {{mapPulseTopPx}}px;"></view>
|
<view class="map-stage__map-pulse {{mapPulseFxClass}}" wx:if="{{mapPulseVisible}}" style="left: {{mapPulseLeftPx}}px; top: {{mapPulseTopPx}}px;"></view>
|
||||||
<view class="map-stage__stage-fx {{stageFxClass}}" wx:if="{{stageFxVisible}}"></view>
|
<view class="map-stage__stage-fx {{stageFxClass}}" wx:if="{{stageFxVisible}}"></view>
|
||||||
|
|
||||||
<view class="game-punch-hint" wx:if="{{showPunchHintBanner && punchHintText}}" style="top: {{topInsetHeight}}px;">
|
|
||||||
<view class="game-punch-hint__text">{{punchHintText}}</view>
|
|
||||||
<view class="game-punch-hint__close" bindtap="handleClosePunchHint">×</view>
|
|
||||||
</view>
|
|
||||||
<view class="game-punch-feedback game-punch-feedback--{{punchFeedbackTone}} {{punchFeedbackFxClass}}" wx:if="{{punchFeedbackVisible}}">{{punchFeedbackText}}</view>
|
<view class="game-punch-feedback game-punch-feedback--{{punchFeedbackTone}} {{punchFeedbackFxClass}}" wx:if="{{punchFeedbackVisible}}">{{punchFeedbackText}}</view>
|
||||||
<view class="game-content-card {{contentCardFxClass}}" wx:if="{{contentCardVisible}}" bindtap="handleCloseContentCard">
|
<view class="game-content-card {{contentCardFxClass}}" wx:if="{{contentCardVisible}}" bindtap="handleCloseContentCard">
|
||||||
<view class="game-content-card__title">{{contentCardTitle}}</view>
|
<view class="game-content-card__title">{{contentCardTitle}}</view>
|
||||||
@@ -40,7 +36,7 @@
|
|||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
|
||||||
<view class="map-stage__overlay-center-layer" wx:if="{{!showDebugPanel && !showGameInfoPanel}}">
|
<view class="map-stage__overlay-center-layer" wx:if="{{!showDebugPanel && !showGameInfoPanel && !showSystemSettingsPanel}}">
|
||||||
<view class="center-scale-ruler" wx:if="{{centerScaleRulerVisible}}" style="left: {{centerScaleRulerCenterXPx}}px; top: {{centerScaleRulerZeroYPx}}px; height: {{centerScaleRulerHeightPx}}px;">
|
<view class="center-scale-ruler" wx:if="{{centerScaleRulerVisible}}" style="left: {{centerScaleRulerCenterXPx}}px; top: {{centerScaleRulerZeroYPx}}px; height: {{centerScaleRulerHeightPx}}px;">
|
||||||
<view class="center-scale-ruler__axis" style="bottom: {{centerScaleRulerAxisBottomPx}}px;"></view>
|
<view class="center-scale-ruler__axis" style="bottom: {{centerScaleRulerAxisBottomPx}}px;"></view>
|
||||||
<view class="center-scale-ruler__arrow"></view>
|
<view class="center-scale-ruler__arrow"></view>
|
||||||
@@ -84,13 +80,18 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<cover-view class="map-side-toggle" wx:if="{{!showDebugPanel && !showGameInfoPanel}}" style="top: {{topInsetHeight}}px;" bindtap="handleCycleSideButtons">
|
<view class="game-punch-hint" wx:if="{{showPunchHintBanner && punchHintText}}" style="top: {{topInsetHeight}}px;" catchtouchstart="handlePunchHintTap" catchtouchmove="handlePunchHintTap" catchtouchend="handlePunchHintTap">
|
||||||
|
<view class="game-punch-hint__text">{{punchHintText}}</view>
|
||||||
|
<view class="game-punch-hint__close" catchtouchstart="handlePunchHintTap" catchtouchmove="handlePunchHintTap" catchtouchend="handlePunchHintTap" catchtap="handleClosePunchHint">×</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<cover-view class="map-side-toggle" wx:if="{{!showDebugPanel && !showGameInfoPanel && !showSystemSettingsPanel}}" style="top: {{topInsetHeight}}px;" bindtap="handleCycleSideButtons">
|
||||||
<cover-view class="map-side-button map-side-button--icon">
|
<cover-view class="map-side-button map-side-button--icon">
|
||||||
<cover-image class="map-side-button__image" src="{{sideToggleIconSrc}}"></cover-image>
|
<cover-image class="map-side-button__image" src="{{sideToggleIconSrc}}"></cover-image>
|
||||||
</cover-view>
|
</cover-view>
|
||||||
</cover-view>
|
</cover-view>
|
||||||
|
|
||||||
<cover-view class="map-side-column map-side-column--left map-side-column--left-group" wx:if="{{!showDebugPanel && !showGameInfoPanel && showLeftButtonGroup}}" style="top: {{topInsetHeight}}px;">
|
<cover-view class="map-side-column map-side-column--left map-side-column--left-group" wx:if="{{!showDebugPanel && !showGameInfoPanel && !showSystemSettingsPanel && showLeftButtonGroup}}" style="top: {{topInsetHeight}}px;">
|
||||||
<cover-view class="map-side-button map-side-button--icon" bindtap="handleToggleMapRotateMode"><cover-image class="map-side-button__rotate-image {{orientationMode === 'heading-up' ? 'map-side-button__rotate-image--active' : ''}}" src="../../assets/btn_map_rotate_cropped.png"></cover-image></cover-view>
|
<cover-view class="map-side-button map-side-button--icon" bindtap="handleToggleMapRotateMode"><cover-image class="map-side-button__rotate-image {{orientationMode === 'heading-up' ? 'map-side-button__rotate-image--active' : ''}}" src="../../assets/btn_map_rotate_cropped.png"></cover-image></cover-view>
|
||||||
<cover-view class="map-side-button map-side-button--muted"><cover-view class="map-side-button__text">1</cover-view></cover-view>
|
<cover-view class="map-side-button map-side-button--muted"><cover-view class="map-side-button__text">1</cover-view></cover-view>
|
||||||
<cover-view class="{{sideButton2Class}}" bindtap="handleToggleGpsLock"><cover-view class="map-side-button__text">2</cover-view></cover-view>
|
<cover-view class="{{sideButton2Class}}" bindtap="handleToggleGpsLock"><cover-view class="map-side-button__text">2</cover-view></cover-view>
|
||||||
@@ -98,7 +99,7 @@
|
|||||||
<cover-view class="{{sideButton4Class}}" bindtap="handleForceExitGame"><cover-image class="map-side-button__action-image" src="../../assets/btn_exit.png"></cover-image></cover-view>
|
<cover-view class="{{sideButton4Class}}" bindtap="handleForceExitGame"><cover-image class="map-side-button__action-image" src="../../assets/btn_exit.png"></cover-image></cover-view>
|
||||||
</cover-view>
|
</cover-view>
|
||||||
|
|
||||||
<cover-view class="map-side-column map-side-column--right-main" wx:if="{{!showDebugPanel && !showGameInfoPanel && showRightButtonGroups}}" style="top: {{topInsetHeight}}px;">
|
<cover-view class="map-side-column map-side-column--right-main" wx:if="{{!showDebugPanel && !showGameInfoPanel && !showSystemSettingsPanel && showRightButtonGroups}}" style="top: {{topInsetHeight}}px;">
|
||||||
<cover-view class="map-side-button"><cover-view class="map-side-button__text">5</cover-view></cover-view>
|
<cover-view class="map-side-button"><cover-view class="map-side-button__text">5</cover-view></cover-view>
|
||||||
<cover-view class="map-side-button map-side-button--active"><cover-view class="map-side-button__text">6</cover-view></cover-view>
|
<cover-view class="map-side-button map-side-button--active"><cover-view class="map-side-button__text">6</cover-view></cover-view>
|
||||||
<cover-view class="map-side-button"><cover-view class="map-side-button__text">7</cover-view></cover-view>
|
<cover-view class="map-side-button"><cover-view class="map-side-button__text">7</cover-view></cover-view>
|
||||||
@@ -107,24 +108,24 @@
|
|||||||
<cover-view class="map-side-button map-side-button--active"><cover-view class="map-side-button__text">10</cover-view></cover-view>
|
<cover-view class="map-side-button map-side-button--active"><cover-view class="map-side-button__text">10</cover-view></cover-view>
|
||||||
</cover-view>
|
</cover-view>
|
||||||
|
|
||||||
<cover-view class="map-side-column map-side-column--right-sub" wx:if="{{!showDebugPanel && !showGameInfoPanel && showRightButtonGroups}}" style="top: {{topInsetHeight}}px;">
|
<cover-view class="map-side-column map-side-column--right-sub" wx:if="{{!showDebugPanel && !showGameInfoPanel && !showSystemSettingsPanel && showRightButtonGroups}}" style="top: {{topInsetHeight}}px;">
|
||||||
<cover-view class="{{sideButton11Class}}" bindtap="handleOpenGameInfoPanel"><cover-image class="map-side-button__action-image" src="../../assets/btn_info.png"></cover-image></cover-view>
|
<cover-view class="{{sideButton11Class}}" bindtap="handleOpenGameInfoPanel"><cover-image class="map-side-button__action-image" src="../../assets/btn_info.png"></cover-image></cover-view>
|
||||||
<cover-view class="map-side-button"><cover-view class="map-side-button__text">12</cover-view></cover-view>
|
<cover-view class="{{sideButton12Class}}" bindtap="handleOpenSystemSettingsPanel"><cover-view class="map-side-button__text">12</cover-view></cover-view>
|
||||||
<cover-view class="{{sideButton13Class}}" bindtap="handleToggleCenterScaleRuler"><cover-view class="map-side-button__text">13</cover-view></cover-view>
|
<cover-view class="map-side-button"><cover-view class="map-side-button__text">13</cover-view></cover-view>
|
||||||
<cover-view class="{{sideButton14Class}}" bindtap="handleToggleCenterScaleRulerAnchor"><cover-view class="map-side-button__text">14</cover-view></cover-view>
|
<cover-view class="map-side-button"><cover-view class="map-side-button__text">14</cover-view></cover-view>
|
||||||
<cover-view class="map-side-button"><cover-view class="map-side-button__text">15</cover-view></cover-view>
|
<cover-view class="map-side-button"><cover-view class="map-side-button__text">15</cover-view></cover-view>
|
||||||
<cover-view class="{{sideButton16Class}}" bindtap="handleSkipAction"><cover-image class="map-side-button__action-image" src="../../assets/btn_skip_cp.png"></cover-image></cover-view>
|
<cover-view class="{{sideButton16Class}}" bindtap="handleSkipAction"><cover-image class="map-side-button__action-image" src="../../assets/btn_skip_cp.png"></cover-image></cover-view>
|
||||||
</cover-view>
|
</cover-view>
|
||||||
|
|
||||||
<cover-view class="map-punch-button {{punchButtonEnabled ? 'map-punch-button--active' : ''}} {{punchButtonFxClass}}" wx:if="{{!showDebugPanel && !showGameInfoPanel}}" bindtap="handlePunchAction">
|
<cover-view class="map-punch-button {{punchButtonEnabled ? 'map-punch-button--active' : ''}} {{punchButtonFxClass}}" wx:if="{{!showDebugPanel && !showGameInfoPanel && !showSystemSettingsPanel}}" bindtap="handlePunchAction">
|
||||||
<cover-view class="map-punch-button__text">{{punchButtonText}}</cover-view>
|
<cover-view class="map-punch-button__text">{{punchButtonText}}</cover-view>
|
||||||
</cover-view>
|
</cover-view>
|
||||||
|
|
||||||
<cover-view class="screen-button-layer screen-button-layer--start-left" wx:if="{{!showDebugPanel && !showGameInfoPanel && showBottomDebugButton && gameSessionStatus === 'idle'}}" bindtap="handleStartGame">
|
<cover-view class="screen-button-layer screen-button-layer--start-left" wx:if="{{!showDebugPanel && !showGameInfoPanel && !showSystemSettingsPanel && showBottomDebugButton && gameSessionStatus === 'idle'}}" bindtap="handleStartGame">
|
||||||
<cover-view class="screen-button-layer__text screen-button-layer__text--start">开始</cover-view>
|
<cover-view class="screen-button-layer__text screen-button-layer__text--start">开始</cover-view>
|
||||||
</cover-view>
|
</cover-view>
|
||||||
|
|
||||||
<cover-view class="screen-button-layer screen-button-layer--bottom-left" wx:if="{{!showDebugPanel && !showGameInfoPanel && showBottomDebugButton}}" bindtap="handleToggleDebugPanel">
|
<cover-view class="screen-button-layer screen-button-layer--bottom-left" wx:if="{{!showDebugPanel && !showGameInfoPanel && !showSystemSettingsPanel && showBottomDebugButton}}" bindtap="handleToggleDebugPanel">
|
||||||
<cover-view class="screen-button-layer__icon">
|
<cover-view class="screen-button-layer__icon">
|
||||||
<cover-view class="screen-button-layer__line"></cover-view>
|
<cover-view class="screen-button-layer__line"></cover-view>
|
||||||
<cover-view class="screen-button-layer__stand"></cover-view>
|
<cover-view class="screen-button-layer__stand"></cover-view>
|
||||||
@@ -132,7 +133,7 @@
|
|||||||
<cover-view class="screen-button-layer__text">调试</cover-view>
|
<cover-view class="screen-button-layer__text">调试</cover-view>
|
||||||
</cover-view>
|
</cover-view>
|
||||||
|
|
||||||
<swiper wx:if="{{!showGameInfoPanel}}" class="race-panel-swiper" current="{{hudPanelIndex}}" bindchange="handleHudPanelChange" duration="220" easing-function="easeOutCubic">
|
<swiper wx:if="{{!showGameInfoPanel && !showSystemSettingsPanel}}" class="race-panel-swiper" current="{{hudPanelIndex}}" bindchange="handleHudPanelChange" duration="220" easing-function="easeOutCubic">
|
||||||
<swiper-item>
|
<swiper-item>
|
||||||
<view class="race-panel race-panel--tone-{{panelTelemetryTone}}">
|
<view class="race-panel race-panel--tone-{{panelTelemetryTone}}">
|
||||||
<view class="race-panel__tag race-panel__tag--top-left">{{panelActionTagText}}</view>
|
<view class="race-panel__tag race-panel__tag--top-left">{{panelActionTagText}}</view>
|
||||||
@@ -155,10 +156,10 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="race-panel__cell race-panel__cell--timer">
|
<view class="race-panel__cell race-panel__cell--timer">
|
||||||
<text class="race-panel__timer">{{panelTimerText}}</text>
|
<text class="race-panel__timer {{panelTimerFxClass}}">{{panelTimerText}}</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="race-panel__cell race-panel__cell--mileage">
|
<view class="race-panel__cell race-panel__cell--mileage">
|
||||||
<view class="race-panel__mileage-wrap">
|
<view class="race-panel__mileage-wrap {{panelMileageFxClass}}">
|
||||||
<text class="race-panel__mileage">{{panelMileageText}}</text>
|
<text class="race-panel__mileage">{{panelMileageText}}</text>
|
||||||
<view class="race-panel__chevrons">
|
<view class="race-panel__chevrons">
|
||||||
<view class="race-panel__chevron"></view>
|
<view class="race-panel__chevron"></view>
|
||||||
@@ -167,16 +168,16 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="race-panel__cell race-panel__cell--distance">
|
<view class="race-panel__cell race-panel__cell--distance">
|
||||||
<view class="race-panel__metric-group race-panel__metric-group--left">
|
<view class="race-panel__metric-group race-panel__metric-group--left {{panelDistanceFxClass}}">
|
||||||
<text class="race-panel__metric-value race-panel__metric-value--distance">{{panelDistanceValueText}}</text>
|
<text class="race-panel__metric-value race-panel__metric-value--distance">{{panelDistanceValueText}}</text>
|
||||||
<text class="race-panel__metric-unit race-panel__metric-unit--distance">{{panelDistanceUnitText}}</text>
|
<text class="race-panel__metric-unit race-panel__metric-unit--distance">{{panelDistanceUnitText}}</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="race-panel__cell race-panel__cell--progress">
|
<view class="race-panel__cell race-panel__cell--progress">
|
||||||
<text class="race-panel__progress">{{panelProgressText}}</text>
|
<text class="race-panel__progress {{panelProgressFxClass}}">{{panelProgressText}}</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="race-panel__cell race-panel__cell--speed">
|
<view class="race-panel__cell race-panel__cell--speed">
|
||||||
<view class="race-panel__metric-group race-panel__metric-group--right">
|
<view class="race-panel__metric-group race-panel__metric-group--right {{panelSpeedFxClass}}">
|
||||||
<text class="race-panel__metric-value race-panel__metric-value--speed">{{panelSpeedValueText}}</text>
|
<text class="race-panel__metric-value race-panel__metric-value--speed">{{panelSpeedValueText}}</text>
|
||||||
<text class="race-panel__metric-unit race-panel__metric-unit--speed">km/h</text>
|
<text class="race-panel__metric-unit race-panel__metric-unit--speed">km/h</text>
|
||||||
</view>
|
</view>
|
||||||
@@ -201,13 +202,13 @@
|
|||||||
|
|
||||||
<view class="race-panel__grid">
|
<view class="race-panel__grid">
|
||||||
<view class="race-panel__cell race-panel__cell--action">
|
<view class="race-panel__cell race-panel__cell--action">
|
||||||
<view class="race-panel__metric-group race-panel__metric-group--left race-panel__metric-group--panel">
|
<view class="race-panel__metric-group race-panel__metric-group--left race-panel__metric-group--panel {{panelHeartRateFxClass}}">
|
||||||
<text class="race-panel__metric-value race-panel__metric-value--telemetry">{{panelHeartRateValueText}}</text>
|
<text class="race-panel__metric-value race-panel__metric-value--telemetry">{{panelHeartRateValueText}}</text>
|
||||||
<text class="race-panel__metric-unit race-panel__metric-unit--telemetry">{{panelHeartRateUnitText}}</text>
|
<text class="race-panel__metric-unit race-panel__metric-unit--telemetry">{{panelHeartRateUnitText}}</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="race-panel__cell race-panel__cell--timer">
|
<view class="race-panel__cell race-panel__cell--timer">
|
||||||
<text class="race-panel__timer">{{panelTimerText}}</text>
|
<text class="race-panel__timer {{panelTimerFxClass}}">{{panelTimerText}}</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="race-panel__cell race-panel__cell--mileage">
|
<view class="race-panel__cell race-panel__cell--mileage">
|
||||||
<view class="race-panel__metric-group race-panel__metric-group--right race-panel__metric-group--panel">
|
<view class="race-panel__metric-group race-panel__metric-group--right race-panel__metric-group--panel">
|
||||||
@@ -237,7 +238,7 @@
|
|||||||
</view>
|
</view>
|
||||||
</swiper-item>
|
</swiper-item>
|
||||||
</swiper>
|
</swiper>
|
||||||
<view class="race-panel-pager" wx:if="{{!showDebugPanel && !showGameInfoPanel}}">
|
<view class="race-panel-pager" wx:if="{{!showDebugPanel && !showGameInfoPanel && !showSystemSettingsPanel}}">
|
||||||
<view class="race-panel-pager__dot {{hudPanelIndex === 0 ? 'race-panel-pager__dot--active' : ''}}"></view>
|
<view class="race-panel-pager__dot {{hudPanelIndex === 0 ? 'race-panel-pager__dot--active' : ''}}"></view>
|
||||||
<view class="race-panel-pager__dot {{hudPanelIndex === 1 ? 'race-panel-pager__dot--active' : ''}}"></view>
|
<view class="race-panel-pager__dot {{hudPanelIndex === 1 ? 'race-panel-pager__dot--active' : ''}}"></view>
|
||||||
</view>
|
</view>
|
||||||
@@ -281,6 +282,93 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
<view class="game-info-modal" wx:if="{{showSystemSettingsPanel}}" bindtap="handleCloseSystemSettingsPanel">
|
||||||
|
<view class="game-info-modal__dialog" catchtap="handleSystemSettingsPanelTap">
|
||||||
|
<view class="game-info-modal__header">
|
||||||
|
<view class="game-info-modal__header-main">
|
||||||
|
<view class="game-info-modal__eyebrow">SYSTEM SETTINGS</view>
|
||||||
|
<view class="game-info-modal__title">系统设置</view>
|
||||||
|
<view class="game-info-modal__subtitle">用户端偏好与设备级选项</view>
|
||||||
|
</view>
|
||||||
|
<view class="game-info-modal__header-actions">
|
||||||
|
<view class="game-info-modal__close" bindtap="handleCloseSystemSettingsPanel">关闭</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<scroll-view class="game-info-modal__content" scroll-y enhanced show-scrollbar="true">
|
||||||
|
<view class="debug-section debug-section--info">
|
||||||
|
<view class="debug-section__header">
|
||||||
|
<view class="debug-section__title">01. 动画性能</view>
|
||||||
|
<view class="debug-section__desc">根据设备性能切换动画强度,低端机建议精简</view>
|
||||||
|
</view>
|
||||||
|
<view class="info-panel__row">
|
||||||
|
<text class="info-panel__label">当前级别</text>
|
||||||
|
<text class="info-panel__value">{{animationLevel === 'lite' ? '精简' : '标准'}}</text>
|
||||||
|
</view>
|
||||||
|
<view class="control-row">
|
||||||
|
<view class="control-chip {{animationLevel === 'standard' ? 'control-chip--active' : 'control-chip--secondary'}}" bindtap="handleSetAnimationLevelStandard">标准</view>
|
||||||
|
<view class="control-chip {{animationLevel === 'lite' ? 'control-chip--active' : 'control-chip--secondary'}}" bindtap="handleSetAnimationLevelLite">精简</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="debug-section debug-section--info">
|
||||||
|
<view class="debug-section__header">
|
||||||
|
<view class="debug-section__title">02. 比例尺显示</view>
|
||||||
|
<view class="debug-section__desc">控制比例尺显示与否,默认沿用你的本地偏好</view>
|
||||||
|
</view>
|
||||||
|
<view class="info-panel__row">
|
||||||
|
<text class="info-panel__label">当前状态</text>
|
||||||
|
<text class="info-panel__value">{{showCenterScaleRuler ? '显示' : '隐藏'}}</text>
|
||||||
|
</view>
|
||||||
|
<view class="control-row">
|
||||||
|
<view class="control-chip {{showCenterScaleRuler ? 'control-chip--active' : 'control-chip--secondary'}}" bindtap="handleSetCenterScaleRulerVisibleOn">显示</view>
|
||||||
|
<view class="control-chip {{!showCenterScaleRuler ? 'control-chip--active' : 'control-chip--secondary'}}" bindtap="handleSetCenterScaleRulerVisibleOff">隐藏</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="debug-section debug-section--info">
|
||||||
|
<view class="debug-section__header">
|
||||||
|
<view class="debug-section__title">03. 比例尺基准点</view>
|
||||||
|
<view class="debug-section__desc">设置比例尺零点锚定位置,可跟随屏幕中心或指北针圆心</view>
|
||||||
|
</view>
|
||||||
|
<view class="info-panel__row">
|
||||||
|
<text class="info-panel__label">当前锚点</text>
|
||||||
|
<text class="info-panel__value">{{centerScaleRulerAnchorMode === 'compass-center' ? '指北针圆心' : '屏幕中心'}}</text>
|
||||||
|
</view>
|
||||||
|
<view class="control-row">
|
||||||
|
<view class="control-chip {{centerScaleRulerAnchorMode === 'screen-center' ? 'control-chip--active' : 'control-chip--secondary'}}" bindtap="handleSetCenterScaleRulerAnchorScreenCenter">屏幕中心</view>
|
||||||
|
<view class="control-chip {{centerScaleRulerAnchorMode === 'compass-center' ? 'control-chip--active' : 'control-chip--secondary'}}" bindtap="handleSetCenterScaleRulerAnchorCompassCenter">指北针圆心</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="debug-section debug-section--info">
|
||||||
|
<view class="debug-section__header">
|
||||||
|
<view class="debug-section__title">04. 北参考</view>
|
||||||
|
<view class="debug-section__desc">切换磁北/真北作为地图与指北针参考</view>
|
||||||
|
</view>
|
||||||
|
<view class="info-panel__row">
|
||||||
|
<text class="info-panel__label">当前参考</text>
|
||||||
|
<text class="info-panel__value">{{northReferenceText}}</text>
|
||||||
|
</view>
|
||||||
|
<view class="control-row">
|
||||||
|
<view class="control-chip {{northReferenceMode === 'magnetic' ? 'control-chip--active' : 'control-chip--secondary'}}" bindtap="handleSetNorthReferenceMagnetic">磁北</view>
|
||||||
|
<view class="control-chip {{northReferenceMode === 'true' ? 'control-chip--active' : 'control-chip--secondary'}}" bindtap="handleSetNorthReferenceTrue">真北</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="debug-section debug-section--info">
|
||||||
|
<view class="debug-section__header">
|
||||||
|
<view class="debug-section__title">05. 心率设备</view>
|
||||||
|
<view class="debug-section__desc">清除已记住的首选心率带设备,下次重新选择</view>
|
||||||
|
</view>
|
||||||
|
<view class="control-row">
|
||||||
|
<view class="control-chip control-chip--secondary" bindtap="handleClearPreferredHeartRateDevice">清除首选设备</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
<view class="debug-modal" wx:if="{{showDebugPanel}}" bindtap="handleCloseDebugPanel">
|
<view class="debug-modal" wx:if="{{showDebugPanel}}" bindtap="handleCloseDebugPanel">
|
||||||
<view class="debug-modal__dialog" catchtap="handleDebugPanelTap">
|
<view class="debug-modal__dialog" catchtap="handleDebugPanelTap">
|
||||||
<view class="debug-modal__header">
|
<view class="debug-modal__header">
|
||||||
@@ -464,6 +552,19 @@
|
|||||||
<text class="info-panel__label">Heading Confidence</text>
|
<text class="info-panel__label">Heading Confidence</text>
|
||||||
<text class="info-panel__value">{{headingConfidenceText}}</text>
|
<text class="info-panel__value">{{headingConfidenceText}}</text>
|
||||||
</view>
|
</view>
|
||||||
|
<view class="info-panel__row">
|
||||||
|
<text class="info-panel__label">Compass Source</text>
|
||||||
|
<text class="info-panel__value">{{compassSourceText}}</text>
|
||||||
|
</view>
|
||||||
|
<view class="info-panel__row">
|
||||||
|
<text class="info-panel__label">Compass Tune</text>
|
||||||
|
<text class="info-panel__value">{{compassTuningProfileText}}</text>
|
||||||
|
</view>
|
||||||
|
<view class="control-row">
|
||||||
|
<view class="control-chip {{compassTuningProfile === 'smooth' ? 'control-chip--active' : 'control-chip--secondary'}}" bindtap="handleSetCompassTuningSmooth">顺滑</view>
|
||||||
|
<view class="control-chip {{compassTuningProfile === 'balanced' ? 'control-chip--active' : 'control-chip--secondary'}}" bindtap="handleSetCompassTuningBalanced">平衡</view>
|
||||||
|
<view class="control-chip {{compassTuningProfile === 'responsive' ? 'control-chip--active' : 'control-chip--secondary'}}" bindtap="handleSetCompassTuningResponsive">跟手</view>
|
||||||
|
</view>
|
||||||
<view class="info-panel__row info-panel__row--stack">
|
<view class="info-panel__row info-panel__row--stack">
|
||||||
<text class="info-panel__label">Accel</text>
|
<text class="info-panel__label">Accel</text>
|
||||||
<text class="info-panel__value">{{accelerometerText}}</text>
|
<text class="info-panel__value">{{accelerometerText}}</text>
|
||||||
|
|||||||
@@ -85,6 +85,10 @@
|
|||||||
animation: stage-fx-finish 0.76s ease-out 1;
|
animation: stage-fx-finish 0.76s ease-out 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.map-stage__stage-fx--control {
|
||||||
|
animation: stage-fx-control 0.52s ease-out 1;
|
||||||
|
}
|
||||||
|
|
||||||
.map-stage__overlay {
|
.map-stage__overlay {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
@@ -834,6 +838,10 @@
|
|||||||
text-shadow: 0 2rpx 0 rgba(255, 255, 255, 0.2);
|
text-shadow: 0 2rpx 0 rgba(255, 255, 255, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.race-panel__timer--fx-tick {
|
||||||
|
animation: race-panel-timer-tick 0.32s cubic-bezier(0.24, 0.86, 0.3, 1) 1;
|
||||||
|
}
|
||||||
|
|
||||||
.race-panel__mileage {
|
.race-panel__mileage {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
@@ -851,6 +859,10 @@
|
|||||||
transform: translateX(-16rpx);
|
transform: translateX(-16rpx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.race-panel__mileage-wrap--fx-update {
|
||||||
|
animation: race-panel-mileage-update 0.36s cubic-bezier(0.22, 0.88, 0.34, 1) 1;
|
||||||
|
}
|
||||||
|
|
||||||
.race-panel__metric-group {
|
.race-panel__metric-group {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
@@ -864,11 +876,23 @@
|
|||||||
transform: translateX(16rpx);
|
transform: translateX(16rpx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.race-panel__metric-group--fx-distance-success {
|
||||||
|
animation: race-panel-distance-success 0.56s cubic-bezier(0.22, 0.88, 0.34, 1) 1;
|
||||||
|
}
|
||||||
|
|
||||||
.race-panel__metric-group--right {
|
.race-panel__metric-group--right {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
transform: translateX(-16rpx);
|
transform: translateX(-16rpx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.race-panel__metric-group--fx-speed-update {
|
||||||
|
animation: race-panel-speed-update 0.36s cubic-bezier(0.22, 0.88, 0.34, 1) 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.race-panel__metric-group--fx-heart-rate-update {
|
||||||
|
animation: race-panel-heart-rate-update 0.4s cubic-bezier(0.2, 0.9, 0.3, 1) 1;
|
||||||
|
}
|
||||||
|
|
||||||
.race-panel__metric-value {
|
.race-panel__metric-value {
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
text-shadow: 0 2rpx 0 rgba(255, 255, 255, 0.16);
|
text-shadow: 0 2rpx 0 rgba(255, 255, 255, 0.16);
|
||||||
@@ -924,6 +948,38 @@
|
|||||||
text-shadow: 0 2rpx 0 rgba(255, 255, 255, 0.16);
|
text-shadow: 0 2rpx 0 rgba(255, 255, 255, 0.16);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.race-panel__progress--fx-success {
|
||||||
|
animation: race-panel-progress-success 0.56s cubic-bezier(0.2, 0.88, 0.32, 1) 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.race-panel__progress--fx-finish {
|
||||||
|
animation: race-panel-progress-finish 0.68s cubic-bezier(0.18, 0.92, 0.28, 1) 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes race-panel-timer-tick {
|
||||||
|
0% { transform: translateY(0) scale(1); opacity: 0.94; }
|
||||||
|
35% { transform: translateY(-2rpx) scale(1.04); opacity: 1; }
|
||||||
|
100% { transform: translateY(0) scale(1); opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes race-panel-mileage-update {
|
||||||
|
0% { transform: translateX(-16rpx) scale(1); opacity: 0.94; }
|
||||||
|
40% { transform: translateX(-16rpx) scale(1.05); opacity: 1; }
|
||||||
|
100% { transform: translateX(-16rpx) scale(1); opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes race-panel-speed-update {
|
||||||
|
0% { transform: translateX(-16rpx) scale(1); opacity: 0.94; }
|
||||||
|
40% { transform: translateX(-16rpx) scale(1.06); opacity: 1; }
|
||||||
|
100% { transform: translateX(-16rpx) scale(1); opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes race-panel-heart-rate-update {
|
||||||
|
0% { transform: translateX(16rpx) scale(1); opacity: 0.94; }
|
||||||
|
38% { transform: translateX(16rpx) scale(1.05); opacity: 1; }
|
||||||
|
100% { transform: translateX(16rpx) scale(1); opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
.race-panel__zone {
|
.race-panel__zone {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -982,6 +1038,72 @@
|
|||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes race-panel-distance-success {
|
||||||
|
0% {
|
||||||
|
transform: translateX(16rpx) scale(1);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
28% {
|
||||||
|
transform: translateX(16rpx) scale(1.09);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
62% {
|
||||||
|
transform: translateX(16rpx) scale(0.98);
|
||||||
|
opacity: 0.96;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: translateX(16rpx) scale(1);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes race-panel-progress-success {
|
||||||
|
0% {
|
||||||
|
transform: scale(1) translateY(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
24% {
|
||||||
|
transform: scale(1.16) translateY(-4rpx);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
60% {
|
||||||
|
transform: scale(0.98) translateY(0);
|
||||||
|
opacity: 0.96;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: scale(1) translateY(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes race-panel-progress-finish {
|
||||||
|
0% {
|
||||||
|
transform: scale(1) translateY(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
20% {
|
||||||
|
transform: scale(1.2) translateY(-6rpx);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
46% {
|
||||||
|
transform: scale(1.08) translateY(-2rpx);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: scale(1) translateY(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
.map-punch-button {
|
.map-punch-button {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 24rpx;
|
right: 24rpx;
|
||||||
@@ -1593,7 +1715,7 @@
|
|||||||
font-size: 24rpx;
|
font-size: 24rpx;
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
z-index: 16;
|
z-index: 40;
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1603,9 +1725,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.game-punch-hint__close {
|
.game-punch-hint__close {
|
||||||
width: 40rpx;
|
width: 56rpx;
|
||||||
height: 40rpx;
|
height: 56rpx;
|
||||||
flex: 0 0 40rpx;
|
flex: 0 0 56rpx;
|
||||||
border-radius: 999rpx;
|
border-radius: 999rpx;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -1939,3 +2061,21 @@
|
|||||||
backdrop-filter: brightness(1);
|
backdrop-filter: brightness(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes stage-fx-control {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
background: radial-gradient(circle at 50% 50%, rgba(138, 255, 235, 0.16) 0%, rgba(138, 255, 235, 0.06) 26%, rgba(255, 255, 255, 0) 60%);
|
||||||
|
backdrop-filter: brightness(1);
|
||||||
|
}
|
||||||
|
36% {
|
||||||
|
opacity: 1;
|
||||||
|
background: radial-gradient(circle at 50% 50%, rgba(138, 255, 235, 0.24) 0%, rgba(138, 255, 235, 0.1) 32%, rgba(255, 255, 255, 0.03) 72%);
|
||||||
|
backdrop-filter: brightness(1.03);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
background: radial-gradient(circle at 50% 50%, rgba(138, 255, 235, 0) 0%, rgba(138, 255, 235, 0) 100%);
|
||||||
|
backdrop-filter: brightness(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
24
miniprogram/utils/animationLevel.ts
Normal file
24
miniprogram/utils/animationLevel.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
export type AnimationLevel = 'standard' | 'lite'
|
||||||
|
|
||||||
|
const LITE_BENCHMARK_THRESHOLD = 18
|
||||||
|
const LITE_DEVICE_MEMORY_GB = 3
|
||||||
|
|
||||||
|
export function resolveAnimationLevel(systemInfo?: WechatMiniprogram.SystemInfo): AnimationLevel {
|
||||||
|
const info = systemInfo || wx.getSystemInfoSync()
|
||||||
|
const benchmarkLevel = Number((info as WechatMiniprogram.SystemInfo & { benchmarkLevel?: number }).benchmarkLevel)
|
||||||
|
const deviceMemory = Number((info as WechatMiniprogram.SystemInfo & { deviceMemory?: number }).deviceMemory)
|
||||||
|
|
||||||
|
if (Number.isFinite(benchmarkLevel) && benchmarkLevel > 0 && benchmarkLevel <= LITE_BENCHMARK_THRESHOLD) {
|
||||||
|
return 'lite'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Number.isFinite(deviceMemory) && deviceMemory > 0 && deviceMemory <= LITE_DEVICE_MEMORY_GB) {
|
||||||
|
return 'lite'
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'standard'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatAnimationLevelText(level: AnimationLevel): string {
|
||||||
|
return level === 'lite' ? '精简' : '标准'
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user