diff --git a/miniprogram/assets/compass-north-arrow.svg b/miniprogram/assets/compass-north-arrow.svg new file mode 100644 index 0000000..4fee519 --- /dev/null +++ b/miniprogram/assets/compass-north-arrow.svg @@ -0,0 +1,4 @@ + + + + diff --git a/miniprogram/assets/compass-north-arrow2.svg b/miniprogram/assets/compass-north-arrow2.svg new file mode 100644 index 0000000..60db67a --- /dev/null +++ b/miniprogram/assets/compass-north-arrow2.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/miniprogram/engine/layer/gpsLayer.ts b/miniprogram/engine/layer/gpsLayer.ts index c0e8e88..641626e 100644 --- a/miniprogram/engine/layer/gpsLayer.ts +++ b/miniprogram/engine/layer/gpsLayer.ts @@ -16,7 +16,11 @@ function buildVectorCamera(scene: MapScene): CameraState { } export class GpsLayer implements MapLayer { - projectPoint(scene: MapScene): ScreenPoint { + projectPoint(scene: MapScene): ScreenPoint | null { + if (!scene.gpsPoint) { + return null + } + const camera = buildVectorCamera(scene) const worldPoint = calibratedLonLatToWorldTile(scene.gpsPoint, scene.zoom, scene.gpsCalibration, scene.gpsCalibrationOrigin) return worldToScreen(camera, worldPoint, false) @@ -29,6 +33,10 @@ export class GpsLayer implements MapLayer { draw(context: LayerRenderContext): void { const { ctx, scene, pulseFrame } = context const gpsScreenPoint = this.projectPoint(scene) + if (!gpsScreenPoint) { + return + } + const pulse = this.getPulseRadius(pulseFrame) ctx.save() diff --git a/miniprogram/engine/layer/trackLayer.ts b/miniprogram/engine/layer/trackLayer.ts index 307a2c6..47d1dde 100644 --- a/miniprogram/engine/layer/trackLayer.ts +++ b/miniprogram/engine/layer/trackLayer.ts @@ -32,6 +32,9 @@ export class TrackLayer implements MapLayer { draw(context: LayerRenderContext): void { const { ctx, scene } = context const points = this.projectPoints(scene) + if (!points.length) { + return + } ctx.save() ctx.lineCap = 'round' diff --git a/miniprogram/engine/map/mapEngine.ts b/miniprogram/engine/map/mapEngine.ts index 04dde4c..de6e186 100644 --- a/miniprogram/engine/map/mapEngine.ts +++ b/miniprogram/engine/map/mapEngine.ts @@ -47,17 +47,6 @@ const COMPASS_NEEDLE_SMOOTHING = 0.12 const GPS_TRACK_MAX_POINTS = 200 const GPS_TRACK_MIN_STEP_METERS = 3 -const SAMPLE_TRACK_WGS84: LonLatPoint[] = [ - worldTileToLonLat({ x: DEFAULT_CENTER_TILE_X - 0.72, y: DEFAULT_CENTER_TILE_Y + 0.44 }, DEFAULT_ZOOM), - worldTileToLonLat({ x: DEFAULT_CENTER_TILE_X - 0.18, y: DEFAULT_CENTER_TILE_Y + 0.08 }, DEFAULT_ZOOM), - worldTileToLonLat({ x: DEFAULT_CENTER_TILE_X + 0.22, y: DEFAULT_CENTER_TILE_Y - 0.16 }, DEFAULT_ZOOM), - worldTileToLonLat({ x: DEFAULT_CENTER_TILE_X + 0.64, y: DEFAULT_CENTER_TILE_Y - 0.52 }, DEFAULT_ZOOM), -] -const SAMPLE_GPS_WGS84: LonLatPoint = worldTileToLonLat( - { x: DEFAULT_CENTER_TILE_X + 0.12, y: DEFAULT_CENTER_TILE_Y - 0.06 }, - DEFAULT_ZOOM, -) - type TouchPoint = WechatMiniprogram.TouchDetail type GestureMode = 'idle' | 'pan' | 'pinch' @@ -435,7 +424,7 @@ export class MapEngine { defaultCenterTileX: number defaultCenterTileY: number tileBoundsByZoom: Record | null - currentGpsPoint: LonLatPoint + currentGpsPoint: LonLatPoint | null currentGpsTrack: LonLatPoint[] currentGpsAccuracyMeters: number | null hasGpsCenteredOnce: boolean @@ -485,7 +474,7 @@ export class MapEngine { this.defaultCenterTileX = DEFAULT_CENTER_TILE_X this.defaultCenterTileY = DEFAULT_CENTER_TILE_Y this.tileBoundsByZoom = null - this.currentGpsPoint = SAMPLE_GPS_WGS84 + this.currentGpsPoint = null this.currentGpsTrack = [] this.currentGpsAccuracyMeters = null this.hasGpsCenteredOnce = false @@ -1302,13 +1291,12 @@ export class MapEngine { const exactCenter = this.getExactCenterFromTranslate(this.state.tileTranslateX, this.state.tileTranslateY) const resolvedViewport = this.resolveViewportForExactCenter(exactCenter.x, exactCenter.y, nextRotationDeg) - this.state = { - ...this.state, + this.setState({ ...resolvedViewport, rotationDeg: nextRotationDeg, rotationText: formatRotationText(nextRotationDeg), centerText: buildCenterText(this.state.zoom, resolvedViewport.centerTileX, resolvedViewport.centerTileY), - } + }) this.syncRenderer() } @@ -1408,7 +1396,7 @@ export class MapEngine { previewScale: this.previewScale || 1, previewOriginX: this.previewOriginX || this.state.stageWidth / 2, previewOriginY: this.previewOriginY || this.state.stageHeight / 2, - track: this.currentGpsTrack.length ? this.currentGpsTrack : SAMPLE_TRACK_WGS84, + track: this.currentGpsTrack, gpsPoint: this.currentGpsPoint, gpsCalibration: GPS_MAP_CALIBRATION, gpsCalibrationOrigin: worldTileToLonLat({ x: this.defaultCenterTileX, y: this.defaultCenterTileY }, this.defaultZoom), @@ -1807,6 +1795,8 @@ export class MapEngine { + + diff --git a/miniprogram/engine/renderer/mapRenderer.ts b/miniprogram/engine/renderer/mapRenderer.ts index 4b84a18..8bec3e3 100644 --- a/miniprogram/engine/renderer/mapRenderer.ts +++ b/miniprogram/engine/renderer/mapRenderer.ts @@ -23,7 +23,7 @@ export interface MapScene { previewOriginX: number previewOriginY: number track: LonLatPoint[] - gpsPoint: LonLatPoint + gpsPoint: LonLatPoint | null gpsCalibration: MapCalibration gpsCalibrationOrigin: LonLatPoint osmReferenceEnabled: boolean diff --git a/miniprogram/engine/renderer/webglVectorRenderer.ts b/miniprogram/engine/renderer/webglVectorRenderer.ts index c422633..0d408f5 100644 --- a/miniprogram/engine/renderer/webglVectorRenderer.ts +++ b/miniprogram/engine/renderer/webglVectorRenderer.ts @@ -137,9 +137,15 @@ export class WebGLVectorRenderer { this.pushCircle(positions, colors, point.x, point.y, 6.5, [0.97, 0.98, 0.95, 1], scene) } - this.pushCircle(positions, colors, gpsPoint.x, gpsPoint.y, this.gpsLayer.getPulseRadius(pulseFrame), [0.13, 0.62, 0.74, 0.22], scene) - this.pushCircle(positions, colors, gpsPoint.x, gpsPoint.y, 13, [1, 1, 1, 0.95], scene) - this.pushCircle(positions, colors, gpsPoint.x, gpsPoint.y, 9, [0.13, 0.63, 0.74, 1], scene) + if (gpsPoint) { + this.pushCircle(positions, colors, gpsPoint.x, gpsPoint.y, this.gpsLayer.getPulseRadius(pulseFrame), [0.13, 0.62, 0.74, 0.22], scene) + this.pushCircle(positions, colors, gpsPoint.x, gpsPoint.y, 13, [1, 1, 1, 0.95], scene) + this.pushCircle(positions, colors, gpsPoint.x, gpsPoint.y, 9, [0.13, 0.63, 0.74, 1], scene) + } + + if (!positions.length) { + return + } gl.viewport(0, 0, this.canvas.width, this.canvas.height) gl.useProgram(this.program) @@ -231,4 +237,3 @@ export class WebGLVectorRenderer { } } } - diff --git a/miniprogram/pages/map/map.json b/miniprogram/pages/map/map.json index d3aae06..b8be3f9 100644 --- a/miniprogram/pages/map/map.json +++ b/miniprogram/pages/map/map.json @@ -1,4 +1,4 @@ { - "navigationBarTitleText": "地图", + "navigationStyle": "custom", "disableScroll": true } diff --git a/miniprogram/pages/map/map.ts b/miniprogram/pages/map/map.ts index 715f052..06f29fe 100644 --- a/miniprogram/pages/map/map.ts +++ b/miniprogram/pages/map/map.ts @@ -1,39 +1,107 @@ import { MapEngine, type MapEngineStageRect, type MapEngineViewState } from '../../engine/map/mapEngine' import { loadRemoteMapConfig } from '../../utils/remoteMapConfig' - +type CompassTickData = { + angle: number + long: boolean + major: boolean +} +type CompassLabelData = { + text: string + angle: number + rotateBack: number + radius: number + className: string +} type MapPageData = MapEngineViewState & { showDebugPanel: boolean + statusBarHeight: number + topInsetHeight: number + panelTimerText: string + panelMileageText: string + panelDistanceValueText: string + panelProgressText: string + panelSpeedValueText: string + compassTicks: CompassTickData[] + compassLabels: CompassLabelData[] } - -const INTERNAL_BUILD_VERSION = 'map-build-99' +const INTERNAL_BUILD_VERSION = 'map-build-106' const REMOTE_GAME_CONFIG_URL = 'https://oss-mbh5.colormaprun.com/wxmini/test/game.json' - let mapEngine: MapEngine | null = null - +function buildCompassTicks(): CompassTickData[] { + const ticks: CompassTickData[] = [] + for (let angle = 0; angle < 360; angle += 5) { + ticks.push({ + angle, + long: angle % 15 === 0, + major: angle % 45 === 0, + }) + } + return ticks +} +function buildCompassLabels(): CompassLabelData[] { + return [ + { text: '\u5317', angle: 0, rotateBack: 0, radius: 68, className: 'compass-widget__mark--cardinal compass-widget__mark--north' }, + { text: '\u4e1c\u5317', angle: 45, rotateBack: 0, radius: 58, className: 'compass-widget__mark--intermediate compass-widget__mark--northeast' }, + { text: '\u4e1c', angle: 90, rotateBack: 0, radius: 68, className: 'compass-widget__mark--cardinal' }, + { text: '\u4e1c\u5357', angle: 135, rotateBack: 0, radius: 58, className: 'compass-widget__mark--intermediate' }, + { text: '\u5357', angle: 180, rotateBack: 0, radius: 68, className: 'compass-widget__mark--cardinal' }, + { text: '\u897f\u5357', angle: 225, rotateBack: 0, radius: 58, className: 'compass-widget__mark--intermediate' }, + { text: '\u897f', angle: 270, rotateBack: 0, radius: 68, className: 'compass-widget__mark--cardinal' }, + { text: '\u897f\u5317', angle: 315, rotateBack: 0, radius: 58, className: 'compass-widget__mark--intermediate compass-widget__mark--northwest' }, + ] +} function getFallbackStageRect(): MapEngineStageRect { const systemInfo = wx.getSystemInfoSync() - const width = Math.max(320, systemInfo.windowWidth - 20) - const height = Math.max(280, Math.floor(systemInfo.windowHeight * 0.66)) + const width = Math.max(320, systemInfo.windowWidth) + const height = Math.max(280, systemInfo.windowHeight) return { width, height, - left: 10, + left: 0, top: 0, } } Page({ - data: { showDebugPanel: false } as MapPageData, + data: { + showDebugPanel: false, + statusBarHeight: 0, + topInsetHeight: 12, + panelTimerText: '00:00:00', + panelMileageText: '0m', + panelDistanceValueText: '108', + panelProgressText: '0/14', + panelSpeedValueText: '0', + compassTicks: buildCompassTicks(), + compassLabels: buildCompassLabels(), + } as MapPageData, onLoad() { + const systemInfo = wx.getSystemInfoSync() + const statusBarHeight = systemInfo.statusBarHeight || 0 + const menuButtonRect = wx.getMenuButtonBoundingClientRect() + const menuButtonBottom = menuButtonRect && typeof menuButtonRect.bottom === 'number' ? menuButtonRect.bottom : statusBarHeight + mapEngine = new MapEngine(INTERNAL_BUILD_VERSION, { onData: (patch) => { this.setData(patch) }, }) - this.setData({ ...mapEngine.getInitialData(), showDebugPanel: false }) + this.setData({ + ...mapEngine.getInitialData(), + showDebugPanel: false, + statusBarHeight, + topInsetHeight: Math.max(statusBarHeight + 12, menuButtonBottom + 20), + panelTimerText: '00:00:00', + panelMileageText: '0m', + panelDistanceValueText: '108', + panelProgressText: '0/14', + panelSpeedValueText: '0', + compassTicks: buildCompassTicks(), + compassLabels: buildCompassLabels(), + }) }, onReady() { @@ -67,10 +135,10 @@ Page({ return } - const errorMessage = error && error.message ? error.message : '未知错误' + const errorMessage = error && error.message ? error.message : '鏈煡閿欒' this.setData({ - configStatusText: `载入失败: ${errorMessage}`, - statusText: `远程地图配置载入失败: ${errorMessage} (${INTERNAL_BUILD_VERSION})`, + configStatusText: `杞藉叆澶辫触: ${errorMessage}`, + statusText: `杩滅▼鍦板浘閰嶇疆杞藉叆澶辫触: ${errorMessage} (${INTERNAL_BUILD_VERSION})`, }) }) }, @@ -99,7 +167,7 @@ Page({ const canvasRef = canvasRes[0] as any if (!canvasRef || !canvasRef.node) { page.setData({ - statusText: `WebGL 引擎初始化失败 (${INTERNAL_BUILD_VERSION})`, + statusText: `WebGL 寮曟搸鍒濆鍖栧け璐?(${INTERNAL_BUILD_VERSION})`, }) return } @@ -109,7 +177,7 @@ Page({ currentEngine.attachCanvas(canvasRef.node, rect.width, rect.height, dpr) } catch (error) { page.setData({ - statusText: `WebGL 初始化失败 (${INTERNAL_BUILD_VERSION})`, + statusText: `WebGL 鍒濆鍖栧け璐?(${INTERNAL_BUILD_VERSION})`, }) } }) @@ -212,6 +280,14 @@ Page({ showDebugPanel: !this.data.showDebugPanel, }) }, + + handleCloseDebugPanel() { + this.setData({ + showDebugPanel: false, + }) + }, + + handleDebugPanelTap() {}, }) @@ -223,29 +299,3 @@ Page({ - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/miniprogram/pages/map/map.wxml b/miniprogram/pages/map/map.wxml index 78a9650..d2f0ffd 100644 --- a/miniprogram/pages/map/map.wxml +++ b/miniprogram/pages/map/map.wxml @@ -1,180 +1,254 @@ - - - CMR MINI PROGRAM - {{mapName}} + + + - {{mapReadyText}} - - - - - + + + + + + CMR MINI MAP + {{mapName}} + {{mapReadyText}} + - - - - - WEBGL MAP ENGINE - North Up / Heading Up / Manual - - 地图北已经固定为正上方。现在支持手动旋转、北朝上、朝向朝上三种模式,并提供指北针用于校验朝向。 - + + + 当前模式 + {{orientationModeText}} + {{gpsTrackingText}} · 缩放 {{zoom}} - - N - - + {{sensorHeadingText}} + + + + + + + + + + {{item.text}} + + + + + + + + - {{sensorHeadingText}} {{compassDeclinationText}} - - - Build - {{buildVersion}} - - - Config - {{configStatusText}} - - - Heading Mode - {{orientationModeText}} - - - Sensor Heading - {{sensorHeadingText}} - - - North Ref - {{northReferenceText}} - - - Zoom - {{zoom}} - - - Rotation - {{rotationText}} - - - Status - {{statusText}} - - - GPS - {{gpsTrackingText}} - - - GPS Coord - {{gpsCoordText}} - - - {{showDebugPanel ? '隐藏调试' : '查看调试'}} - + + + + + + 屏幕 + - - - Renderer - {{renderMode}} - - - Projection - {{projectionMode}} - - - Auto Source - {{autoRotateSourceText}} - - - Calibration - {{autoRotateCalibrationText}} - - - Tile URL - {{tileSource}} - - - Center Tile - {{centerText}} - - - Tile Size - {{tileSizePx}}px - - - Visible Tiles - {{visibleTileCount}} - - - Ready Tiles - {{readyTileCount}} - - - Memory Tiles - {{memoryTileCount}} - - - Disk Tiles - {{diskTileCount}} - - - Cache Hit - {{cacheHitRateText}} - - - Disk Hits - {{diskHitCount}} - - - Net Fetches - {{networkFetchCount}} - - + + 目标 + 里程 + 点距 + 速度 - - 回到首屏 - 旋转归零 + + + + + + + + + + + + + + {{panelTimerText}} + + + + {{panelMileageText}} + + + + + + + + + {{panelDistanceValueText}} + m + + + + {{panelProgressText}} + + + + {{panelSpeedValueText}} + km/h + + - - {{gpsTracking ? '停止定位' : '开启定位'}} - {{osmReferenceText}} + + + + + + + DEBUG PANEL + 地图调试信息 + + 关闭 + + + + + Build + {{buildVersion}} + + + Config + {{configStatusText}} + + + Heading Mode + {{orientationModeText}} + + + Sensor Heading + {{sensorHeadingText}} + + + North Ref + {{northReferenceText}} + + + Zoom + {{zoom}} + + + Rotation + {{rotationText}} + + + Status + {{statusText}} + + + GPS + {{gpsTrackingText}} + + + GPS Coord + {{gpsCoordText}} + + + Renderer + {{renderMode}} + + + Projection + {{projectionMode}} + + + Auto Source + {{autoRotateSourceText}} + + + Calibration + {{autoRotateCalibrationText}} + + + Tile URL + {{tileSource}} + + + Center Tile + {{centerText}} + + + Tile Size + {{tileSizePx}}px + + + Visible Tiles + {{visibleTileCount}} + + + Ready Tiles + {{readyTileCount}} + + + Memory Tiles + {{memoryTileCount}} + + + Disk Tiles + {{diskTileCount}} + + + Cache Hit + {{cacheHitRateText}} + + + Disk Hits + {{diskHitCount}} + + + Net Fetches + {{networkFetchCount}} + + + + 回到首屏 + 旋转归零 + + + {{gpsTracking ? '停止定位' : '开启定位'}} + {{osmReferenceText}} + + + 手动 + 北朝上 + 朝向朝上 + + + {{northReferenceButtonText}} + + + 按当前方向校准 + + + 旋转 +15° + + - - 手动 - 北朝上 - 朝向朝上 - - - {{northReferenceButtonText}} - - - 按当前方向校准 - - - 旋转 +15° - - + diff --git a/miniprogram/pages/map/map.wxss b/miniprogram/pages/map/map.wxss index 23ee6e5..8ea5667 100644 --- a/miniprogram/pages/map/map.wxss +++ b/miniprogram/pages/map/map.wxss @@ -1,63 +1,16 @@ .page { height: 100vh; - padding: 20rpx 20rpx 24rpx; - box-sizing: border-box; - display: flex; - flex-direction: column; + position: relative; overflow: hidden; - background: - radial-gradient(circle at top left, #d8f3dc 0%, rgba(216, 243, 220, 0) 32%), - linear-gradient(180deg, #f7fbf2 0%, #eef6ea 100%); + background: #dbeed4; color: #163020; } -.page__header { - display: flex; - align-items: flex-start; - justify-content: space-between; - margin-bottom: 16rpx; - flex-shrink: 0; -} - -.page__eyebrow { - font-size: 20rpx; - letter-spacing: 4rpx; - color: #5f7a65; -} - -.page__title { - margin-top: 8rpx; - font-size: 44rpx; - font-weight: 600; -} - -.page__badge { - padding: 10rpx 18rpx; - border-radius: 999rpx; - background: #163020; - color: #f7fbf2; - font-size: 22rpx; -} - -.map-stage-wrap { - position: relative; - width: 100%; - height: 66vh; - min-height: 520rpx; - max-height: 72vh; - flex-shrink: 0; - margin-bottom: 16rpx; -} - .map-stage { - position: relative; - width: 100%; - height: 100%; + position: absolute; + inset: 0; overflow: hidden; - border: 2rpx solid rgba(22, 48, 32, 0.08); - border-radius: 32rpx; background: #dbeed4; - box-shadow: 0 18rpx 40rpx rgba(22, 48, 32, 0.08); } .map-content { @@ -118,38 +71,140 @@ position: absolute; inset: 0; display: flex; - align-items: flex-start; + flex-direction: column; justify-content: space-between; - padding: 24rpx; + padding: 0 24rpx 248rpx; box-sizing: border-box; pointer-events: none; z-index: 4; } -.overlay-card { - width: 68%; - padding: 22rpx; - border-radius: 24rpx; - background: rgba(247, 251, 242, 0.92); - box-shadow: 0 12rpx 30rpx rgba(22, 48, 32, 0.08); +.map-stage__topbar { + display: flex; + align-items: flex-start; + justify-content: flex-start; } -.overlay-card__label { +.map-stage__meta { + max-width: 68%; + padding: 18rpx 20rpx 20rpx; + border-radius: 28rpx; + background: rgba(248, 251, 244, 0.92); + box-shadow: 0 14rpx 36rpx rgba(22, 48, 32, 0.12); + backdrop-filter: blur(12rpx); +} + +.map-stage__eyebrow { + font-size: 20rpx; + letter-spacing: 4rpx; + color: #5f7a65; +} + +.map-stage__title { + margin-top: 8rpx; + font-size: 38rpx; + font-weight: 600; +} + +.map-stage__badge { + display: inline-flex; + margin-top: 14rpx; + padding: 8rpx 18rpx; + border-radius: 999rpx; + background: rgba(22, 48, 32, 0.9); + color: #f7fbf2; + font-size: 22rpx; +} + +.screen-button-layer { + position: absolute; + right: 24rpx; + width: 116rpx; + min-height: 116rpx; + padding: 18rpx 0 14rpx; + border-radius: 30rpx; + background: rgba(248, 251, 244, 0.96); + box-shadow: 0 14rpx 36rpx rgba(22, 48, 32, 0.14); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + box-sizing: border-box; + z-index: 20; +} + +.screen-button-layer__icon { + position: relative; + width: 54rpx; + height: 32rpx; + margin: 0 auto; + border: 4rpx solid #163020; + border-radius: 8rpx; + box-sizing: border-box; +} + +.screen-button-layer__line { + position: absolute; + left: 8rpx; + right: 8rpx; + bottom: 6rpx; + height: 4rpx; + border-radius: 999rpx; + background: rgba(22, 48, 32, 0.3); +} + +.screen-button-layer__stand { + position: absolute; + left: 50%; + bottom: -12rpx; + width: 18rpx; + height: 4rpx; + margin-left: -9rpx; + border-radius: 999rpx; + background: #163020; +} + +.screen-button-layer__text { + margin-top: 18rpx; + text-align: center; + font-size: 22rpx; + font-weight: 600; + color: #163020; + line-height: 1.2; +} + +.map-stage__bottom { + display: flex; + align-items: flex-end; + justify-content: space-between; + gap: 20rpx; +} + +.map-stage__status { + max-width: 56%; + padding: 18rpx 20rpx; + border-radius: 28rpx; + background: rgba(248, 251, 244, 0.9); + box-shadow: 0 14rpx 30rpx rgba(22, 48, 32, 0.12); + backdrop-filter: blur(12rpx); +} + +.map-stage__status-label { font-size: 20rpx; letter-spacing: 3rpx; color: #5f7a65; } -.overlay-card__title { - margin-top: 10rpx; - font-size: 34rpx; +.map-stage__status-value { + margin-top: 8rpx; + font-size: 30rpx; font-weight: 600; + color: #163020; } -.overlay-card__desc { - margin-top: 12rpx; - font-size: 24rpx; - line-height: 1.6; +.map-stage__status-meta { + margin-top: 8rpx; + font-size: 22rpx; color: #45624b; } @@ -157,79 +212,546 @@ display: flex; flex-direction: column; align-items: center; - gap: 10rpx; + gap: 6rpx; + flex-shrink: 0; } -.compass-widget__ring { +.compass-widget__heading { + font-size: 14rpx; + line-height: 1; + font-weight: 600; + color: rgba(32, 42, 34, 0.72); + text-shadow: 0 1rpx 0 rgba(255, 255, 255, 0.35); +} + +.compass-widget__dial { position: relative; - width: 108rpx; - height: 108rpx; + width: 196rpx; + height: 196rpx; border-radius: 50%; - background: rgba(247, 251, 242, 0.94); - border: 2rpx solid rgba(22, 48, 32, 0.12); - box-shadow: 0 10rpx 24rpx rgba(22, 48, 32, 0.1); + background: radial-gradient(circle at 48% 44%, rgba(255, 255, 255, 0.3) 0%, rgba(242, 241, 214, 0.32) 46%, rgba(183, 188, 159, 0.4) 72%, rgba(64, 68, 58, 0.62) 100%); + border: 2rpx solid rgba(18, 24, 18, 0.48); + box-shadow: 0 6rpx 14rpx rgba(0, 0, 0, 0.14), inset 0 0 0 2rpx rgba(255, 255, 255, 0.24); + overflow: hidden; } -.compass-widget__north { +.compass-widget__glass, +.compass-widget__inner-shadow { position: absolute; - left: 50%; - top: 10rpx; - transform: translateX(-50%); - font-size: 20rpx; - font-weight: 700; - color: #d62828; + inset: 0; + border-radius: 50%; + pointer-events: none; } -.compass-widget__needle { +.compass-widget__glass { + background: radial-gradient(circle at 38% 30%, rgba(255, 255, 255, 0.24) 0%, rgba(255, 255, 255, 0.1) 24%, rgba(255, 255, 255, 0) 52%); +} + +.compass-widget__inner-shadow { + box-shadow: inset 0 0 0 12rpx rgba(0, 0, 0, 0.07), inset 0 0 18rpx rgba(255, 255, 255, 0.22); +} + +.compass-widget__card { position: absolute; - left: 50%; - top: 18rpx; - width: 4rpx; - height: 72rpx; - transform-origin: 50% 36rpx; - background: linear-gradient(180deg, #d62828 0%, #163020 100%); - border-radius: 999rpx; + inset: 0; + transform-origin: center; } -.compass-widget__center { + +.compass-widget__north-arrow { position: absolute; left: 50%; top: 50%; - width: 14rpx; - height: 14rpx; - transform: translate(-50%, -50%); - border-radius: 50%; - background: #163020; + width: 54rpx; + height: 176rpx; + transform: translate(-50%, -52%); + display: block; + pointer-events: none; + z-index: 1; } -.compass-widget__label { - min-width: 92rpx; - padding: 6rpx 10rpx; +.compass-widget__north-arrow-outline, +.compass-widget__north-arrow-fill, +.compass-widget__north-arrow-tail-outline, +.compass-widget__north-arrow-tail-fill { + display: none; +} + +.compass-widget__tick-anchor, +.compass-widget__mark-anchor, +.compass-widget__needle-anchor { + position: absolute; + left: 50%; + top: 50%; +} + + +.compass-widget__tick { + position: absolute; + left: 50%; + top: 50%; + width: 2rpx; border-radius: 999rpx; - background: rgba(247, 251, 242, 0.94); - font-size: 20rpx; - text-align: center; - color: #163020; - box-shadow: 0 8rpx 18rpx rgba(22, 48, 32, 0.08); + transform: translate(-50%, -90rpx); + background: rgba(28, 33, 28, 0.72); } -.compass-widget__hint { - margin-top: 8rpx; - font-size: 18rpx; - line-height: 1.4; - color: #d62828; - text-align: center; +.compass-widget__tick--short { + height: 8rpx; +} + +.compass-widget__tick--long { + height: 12rpx; +} + +.compass-widget__tick--major { + width: 3rpx; + height: 18rpx; + background: rgba(18, 22, 18, 0.88); +} + +.compass-widget__mark { + position: absolute; + left: 50%; + top: 50%; + line-height: 1; + color: rgba(40, 42, 37, 0.88); + text-shadow: 0 1rpx 0 rgba(255, 255, 255, 0.22); + white-space: nowrap; + transform-origin: center; +} + +.compass-widget__mark--cardinal { + font-size: 26rpx; font-weight: 700; } -.info-panel { - flex: 1; - min-height: 0; - padding: 22rpx 20rpx 28rpx; +.compass-widget__mark--intermediate { + font-size: 14rpx; + font-weight: 600; +} + +.compass-widget__mark--north { + color: #d62323; +} + +.compass-widget__mark--northeast, +.compass-widget__mark--northwest { + color: #577347; +} + +.compass-widget__needle-anchor { + width: 0; + height: 0; +} + +.compass-widget__needle-north, +.compass-widget__needle-south { + position: absolute; + left: 50%; + top: 50%; +} + +.compass-widget__needle-north { + width: 0; + height: 0; + border-left: 8rpx solid transparent; + border-right: 8rpx solid transparent; + border-bottom: 64rpx solid #ef2f2f; + transform: translate(-50%, -74rpx); + filter: drop-shadow(0 2rpx 3rpx rgba(96, 0, 0, 0.24)); +} + +.compass-widget__needle-south { + width: 7rpx; + height: 72rpx; + border-radius: 999rpx; + background: linear-gradient(180deg, rgba(236, 238, 232, 0.98) 0%, rgba(146, 151, 143, 0.98) 100%); + transform: translate(-50%, 2rpx); + box-shadow: 0 1rpx 3rpx rgba(32, 34, 31, 0.18); +} + +.compass-widget__hub { + position: absolute; + left: 50%; + top: 50%; + width: 26rpx; + height: 26rpx; + transform: translate(-50%, -52%); + border-radius: 50%; + background: radial-gradient(circle at 34% 32%, #f4f3e7 0%, #d7d2bd 40%, #928b78 72%, #5a554b 100%); + box-shadow: inset 0 0 0 2rpx rgba(255, 255, 255, 0.32), 0 2rpx 5rpx rgba(0, 0, 0, 0.16); +} + +.compass-widget__hub-core { + position: absolute; + left: 50%; + top: 50%; + width: 10rpx; + height: 10rpx; + transform: translate(-50%, -52%); + border-radius: 50%; + background: rgba(173, 170, 156, 0.92); + box-shadow: inset 0 0 0 2rpx rgba(255, 255, 255, 0.2); +} + +.compass-widget__hint { + max-width: 196rpx; + font-size: 14rpx; + line-height: 1.3; + color: #d62828; + text-align: center; + font-weight: 700; + text-shadow: 0 1rpx 0 rgba(255, 255, 255, 0.24); +} +.race-panel { + position: absolute; + left: 0; + right: 0; + bottom: 0; + height: 216rpx; + background: linear-gradient(180deg, #1d97ec 0%, #168ce4 100%); + box-shadow: 0 -10rpx 24rpx rgba(10, 75, 125, 0.2); + z-index: 15; + overflow: hidden; +} + +.race-panel__grid { + position: relative; + z-index: 2; + display: grid; + grid-template-columns: 1fr 1fr 1fr; + grid-template-rows: 1fr 1fr; + width: 100%; + height: 100%; +} + +.race-panel__cell { + position: relative; + display: flex; + align-items: center; + justify-content: center; + color: #ffffff; + box-sizing: border-box; +} + +.race-panel__cell--action, +.race-panel__cell--timer, +.race-panel__cell--mileage { + padding-top: 10rpx; +} + +.race-panel__cell--distance, +.race-panel__cell--progress, +.race-panel__cell--speed { + padding-top: 2rpx; +} + +.race-panel__cell--action { + justify-content: center; + padding-left: 0; +} + +.race-panel__cell--timer { + padding-left: 0; + padding-right: 0; +} + +.race-panel__cell--mileage { + justify-content: center; + padding-right: 0; +} + +.race-panel__cell--distance { + justify-content: center; + padding-left: 0; +} + +.race-panel__cell--progress { + padding-left: 0; + padding-right: 0; +} + +.race-panel__cell--speed { + justify-content: center; + padding-right: 0; +} + +.race-panel__play { + width: 0; + height: 0; + margin-left: 2rpx; + border-top: 20rpx solid transparent; + border-bottom: 20rpx solid transparent; + border-left: 30rpx solid #ffffff; + filter: drop-shadow(0 2rpx 0 rgba(255, 255, 255, 0.25)); + transform: translateX(16rpx); +} + +.race-panel__timer { + max-width: 100%; + box-sizing: border-box; + font-size: 50rpx; + line-height: 1; + letter-spacing: 2rpx; + font-family: 'Courier New', monospace; + text-shadow: 0 2rpx 0 rgba(255, 255, 255, 0.2); +} + +.race-panel__mileage { + max-width: 100%; + box-sizing: border-box; + font-size: 40rpx; + line-height: 1; + font-weight: 300; + text-shadow: 0 2rpx 0 rgba(255, 255, 255, 0.16); +} + +.race-panel__mileage-wrap { + display: flex; + align-items: center; + justify-content: center; + gap: 10rpx; + transform: translateX(-16rpx); +} + +.race-panel__metric-group { + max-width: 100%; + box-sizing: border-box; + display: flex; + align-items: baseline; + color: #ffffff; +} + +.race-panel__metric-group--left { + justify-content: center; + transform: translateX(16rpx); +} + +.race-panel__metric-group--right { + justify-content: center; + transform: translateX(-16rpx); +} + +.race-panel__metric-value { + line-height: 1; + text-shadow: 0 2rpx 0 rgba(255, 255, 255, 0.16); +} + +.race-panel__metric-value--distance { + font-size: 54rpx; + font-weight: 700; +} + +.race-panel__metric-value--speed { + font-size: 48rpx; + font-weight: 400; +} + +.race-panel__metric-unit { + line-height: 1; + margin-left: 6rpx; + opacity: 0.95; +} + +.race-panel__metric-unit--distance { + font-size: 24rpx; + font-weight: 600; +} + +.race-panel__metric-unit--speed { + font-size: 18rpx; + font-weight: 500; +} + +.race-panel__progress { + max-width: 100%; + box-sizing: border-box; + font-size: 50rpx; + line-height: 1; + font-weight: 400; + text-shadow: 0 2rpx 0 rgba(255, 255, 255, 0.16); +} + +.race-panel__tag { + position: absolute; + z-index: 3; + min-width: 56rpx; + height: 32rpx; + padding: 0 10rpx; + background: #000000; + color: #ffffff; + font-size: 16rpx; + line-height: 32rpx; + text-align: center; + letter-spacing: 2rpx; +} + +.race-panel__tag--top-left { + top: 0; + left: 0; +} + +.race-panel__tag--top-right { + top: 0; + right: 0; +} + +.race-panel__tag--bottom-left { + left: 0; + bottom: 0; +} + +.race-panel__tag--bottom-right { + right: 0; + bottom: 0; +} + +.race-panel__line { + position: absolute; + z-index: 1; + height: 2rpx; + box-shadow: 0 0 6rpx rgba(255, 255, 255, 0.2); +} + +.race-panel__line--center { + left: 33.3333%; + right: 33.3333%; + top: 50%; + transform: translateY(-50%); + background: rgba(255, 255, 255, 0.86); +} + +.race-panel__line--left-mid { + left: 0; + width: 33.3333%; + top: 50%; + transform: translateY(-50%); + background: linear-gradient(90deg, rgba(255, 255, 255, 0.08) 0%, rgba(255, 255, 255, 0.78) 100%); +} + +.race-panel__line--right-mid { + right: 0; + width: 33.3333%; + top: 50%; + transform: translateY(-50%); + background: linear-gradient(90deg, rgba(255, 255, 255, 0.78) 0%, rgba(255, 255, 255, 0.08) 100%); +} + +.race-panel__line--left-top, +.race-panel__line--left-bottom, +.race-panel__line--right-top, +.race-panel__line--right-bottom { + width: 23%; + top: 50%; +} + +.race-panel__line--left-top { + right: 66.6667%; + transform-origin: right center; + transform: translateY(-50%) rotate(70deg); + background: linear-gradient(90deg, rgba(255, 255, 255, 0.08) 0%, rgba(255, 255, 255, 0.82) 100%); +} + +.race-panel__line--left-bottom { + right: 66.6667%; + transform-origin: right center; + transform: translateY(-50%) rotate(-70deg); + background: linear-gradient(90deg, rgba(255, 255, 255, 0.08) 0%, rgba(255, 255, 255, 0.82) 100%); +} + +.race-panel__line--right-top { + left: 66.6667%; + transform-origin: left center; + transform: translateY(-50%) rotate(-70deg); + background: linear-gradient(90deg, rgba(255, 255, 255, 0.82) 0%, rgba(255, 255, 255, 0.08) 100%); +} + +.race-panel__line--right-bottom { + left: 66.6667%; + transform-origin: left center; + transform: translateY(-50%) rotate(70deg); + background: linear-gradient(90deg, rgba(255, 255, 255, 0.82) 0%, rgba(255, 255, 255, 0.08) 100%); +} + +.race-panel__chevrons { + position: relative; + width: 24rpx; + height: 24rpx; + opacity: 0.5; + flex-shrink: 0; +} + +.race-panel__chevron { + position: absolute; + right: 6rpx; + top: 50%; + width: 10rpx; + height: 10rpx; + border-top: 3rpx solid rgba(255, 255, 255, 0.78); + border-right: 3rpx solid rgba(255, 255, 255, 0.78); + transform: translateY(-50%) rotate(45deg); +} + +.race-panel__chevron--offset { + right: 0; +} +.debug-modal { + position: absolute; + inset: 0; + display: flex; + align-items: flex-end; + justify-content: center; + padding: 0 20rpx 28rpx; + box-sizing: border-box; + background: rgba(7, 18, 12, 0.34); + z-index: 30; +} + +.debug-modal__dialog { + width: 100%; + max-height: 72vh; + border-radius: 36rpx; + background: rgba(248, 251, 244, 0.98); + box-shadow: 0 20rpx 60rpx rgba(7, 18, 12, 0.24); + overflow: hidden; +} + +.debug-modal__header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 20rpx; + padding: 28rpx 28rpx 20rpx; + border-bottom: 1rpx solid rgba(22, 48, 32, 0.08); +} + +.debug-modal__eyebrow { + font-size: 20rpx; + letter-spacing: 4rpx; + color: #5f7a65; +} + +.debug-modal__title { + margin-top: 8rpx; + font-size: 34rpx; + font-weight: 600; + color: #163020; +} + +.debug-modal__close { + flex-shrink: 0; + padding: 14rpx 22rpx; + border-radius: 999rpx; + background: #163020; + color: #f7fbf2; + font-size: 24rpx; +} + +.debug-modal__content { + max-height: calc(72vh - 108rpx); + padding: 12rpx 28rpx 30rpx; box-sizing: border-box; - border-radius: 28rpx; - background: rgba(255, 255, 255, 0.88); - box-shadow: 0 12rpx 32rpx rgba(22, 48, 32, 0.08); } .info-panel__row { @@ -272,40 +794,6 @@ line-height: 1.5; } -.info-panel__actions { - display: flex; - gap: 14rpx; - margin-top: 18rpx; -} - -.info-panel__actions--triple .info-panel__action { - font-size: 23rpx; -} - -.info-panel__action { - flex: 1; - min-width: 0; - border-radius: 999rpx; - background: #d7e8da; - color: #163020; - font-size: 26rpx; -} - -.info-panel__action--primary { - background: #2d6a4f; - color: #f7fbf2; -} - -.info-panel__action--secondary { - background: #eef6ea; - color: #45624b; -} - -.info-panel__action--active { - background: #2d6a4f; - color: #f7fbf2; -} - .control-row { display: flex; gap: 14rpx; @@ -344,3 +832,54 @@ } + + + + + +.race-panel__cell--action { + justify-content: flex-start; + padding-left: 44rpx; +} +.race-panel__cell--timer { + padding-left: 12rpx; + padding-right: 12rpx; +} +.race-panel__cell--mileage { + justify-content: flex-end; + padding-right: 56rpx; +} +.race-panel__cell--distance { + justify-content: flex-start; + padding-left: 28rpx; +} +.race-panel__cell--progress { + padding-left: 8rpx; + padding-right: 8rpx; +} +.race-panel__cell--speed { + justify-content: flex-end; + padding-right: 32rpx; +} +.race-panel__timer, +.race-panel__progress, +.race-panel__mileage, +.race-panel__metric-group { + max-width: 100%; + box-sizing: border-box; +} + + + + + + + + + + + + + + +