完善样式系统与调试链路底座

This commit is contained in:
2026-03-30 18:19:05 +08:00
parent 2c0fd4c549
commit 3b9117427e
40 changed files with 7526 additions and 389 deletions

View File

@@ -4,6 +4,109 @@ import { type MapLayer, type LayerRenderContext } from './mapLayer'
import { type MapScene } from '../renderer/mapRenderer'
import { type ScreenPoint } from './trackLayer'
function getGpsMarkerMetrics(size: MapScene['gpsMarkerStyleConfig']['size']) {
if (size === 'small') {
return {
coreRadius: 7,
ringRadius: 8.35,
pulseRadius: 14,
indicatorOffset: 1.1,
indicatorSize: 7,
ringWidth: 2.5,
}
}
if (size === 'large') {
return {
coreRadius: 11,
ringRadius: 12.95,
pulseRadius: 22,
indicatorOffset: 1.45,
indicatorSize: 10,
ringWidth: 3.5,
}
}
return {
coreRadius: 9,
ringRadius: 10.65,
pulseRadius: 18,
indicatorOffset: 1.25,
indicatorSize: 8.5,
ringWidth: 3,
}
}
function scaleGpsMarkerMetrics(
metrics: ReturnType<typeof getGpsMarkerMetrics>,
effectScale: number,
): ReturnType<typeof getGpsMarkerMetrics> {
const safeScale = Math.max(0.88, Math.min(1.28, effectScale || 1))
return {
coreRadius: metrics.coreRadius * safeScale,
ringRadius: metrics.ringRadius * safeScale,
pulseRadius: metrics.pulseRadius * safeScale,
indicatorOffset: metrics.indicatorOffset * safeScale,
indicatorSize: metrics.indicatorSize * safeScale,
ringWidth: Math.max(2, metrics.ringWidth * (0.96 + (safeScale - 1) * 0.35)),
}
}
function rotatePoint(x: number, y: number, angleRad: number): { x: number; y: number } {
const cos = Math.cos(angleRad)
const sin = Math.sin(angleRad)
return {
x: x * cos - y * sin,
y: x * sin + y * cos,
}
}
function hexToRgbTuple(hex: string): [number, number, number] {
const safeHex = /^#[0-9a-fA-F]{6}$/.test(hex) ? hex : '#ffffff'
return [
parseInt(safeHex.slice(1, 3), 16),
parseInt(safeHex.slice(3, 5), 16),
parseInt(safeHex.slice(5, 7), 16),
]
}
function getGpsPulsePhase(
pulseFrame: number,
motionState: MapScene['gpsMarkerStyleConfig']['motionState'],
): number {
const divisor = motionState === 'idle'
? 11.5
: motionState === 'moving'
? 6.2
: motionState === 'fast-moving'
? 4.3
: 4.8
return 0.5 + 0.5 * Math.sin(pulseFrame / divisor)
}
function getAnimatedPulseRadius(
pulseFrame: number,
metrics: ReturnType<typeof getGpsMarkerMetrics>,
motionState: MapScene['gpsMarkerStyleConfig']['motionState'],
pulseStrength: number,
motionIntensity: number,
): number {
const phase = getGpsPulsePhase(pulseFrame, motionState)
const baseRadius = motionState === 'idle'
? metrics.pulseRadius * 0.82
: motionState === 'moving'
? metrics.pulseRadius * 0.94
: motionState === 'fast-moving'
? metrics.pulseRadius * 1.04
: metrics.pulseRadius
const amplitude = motionState === 'idle'
? metrics.pulseRadius * 0.12
: motionState === 'moving'
? metrics.pulseRadius * 0.18
: motionState === 'fast-moving'
? metrics.pulseRadius * 0.24
: metrics.pulseRadius * 0.2
return baseRadius + amplitude * phase * (0.8 + pulseStrength * 0.18 + motionIntensity * 0.1)
}
function buildVectorCamera(scene: MapScene): CameraState {
return {
centerWorldX: scene.exactCenterWorldX,
@@ -32,35 +135,156 @@ export class GpsLayer implements MapLayer {
draw(context: LayerRenderContext): void {
const { ctx, scene, pulseFrame } = context
if (!scene.gpsMarkerStyleConfig.visible) {
return
}
const gpsScreenPoint = this.projectPoint(scene)
if (!gpsScreenPoint) {
return
}
const pulse = this.getPulseRadius(pulseFrame)
const metrics = scaleGpsMarkerMetrics(
getGpsMarkerMetrics(scene.gpsMarkerStyleConfig.size),
scene.gpsMarkerStyleConfig.effectScale || 1,
)
const style = scene.gpsMarkerStyleConfig.style
const hasBadgeLogo = style === 'badge' && !!scene.gpsMarkerStyleConfig.logoUrl
const pulseStrength = Math.max(0.45, Math.min(1.85, scene.gpsMarkerStyleConfig.pulseStrength || 1))
const motionState = scene.gpsMarkerStyleConfig.motionState || 'idle'
const motionIntensity = Math.max(0, Math.min(1.2, scene.gpsMarkerStyleConfig.motionIntensity || 0))
const wakeStrength = Math.max(0, Math.min(1, scene.gpsMarkerStyleConfig.wakeStrength || 0))
const warningGlowStrength = Math.max(0, Math.min(1, scene.gpsMarkerStyleConfig.warningGlowStrength || 0))
const indicatorScale = Math.max(0.86, Math.min(1.28, scene.gpsMarkerStyleConfig.indicatorScale || 1))
const pulse = style === 'dot'
? metrics.pulseRadius * 0.82
: getAnimatedPulseRadius(pulseFrame, metrics, motionState, pulseStrength, motionIntensity)
const [markerR, markerG, markerB] = hexToRgbTuple(scene.gpsMarkerStyleConfig.colorHex)
ctx.save()
ctx.beginPath()
ctx.fillStyle = 'rgba(33, 158, 188, 0.22)'
ctx.arc(gpsScreenPoint.x, gpsScreenPoint.y, pulse, 0, Math.PI * 2)
ctx.fill()
if (wakeStrength > 0.05 && scene.gpsHeadingDeg !== null) {
const headingScreenRad = (scene.gpsHeadingDeg * Math.PI / 180) - scene.rotationRad
const wakeHeadingRad = headingScreenRad + Math.PI
const wakeCount = motionState === 'fast-moving' ? 3 : 2
for (let index = 0; index < wakeCount; index += 1) {
const offset = metrics.coreRadius * (0.85 + index * 0.64) * (0.9 + wakeStrength * 0.72)
const center = rotatePoint(0, -offset, wakeHeadingRad)
const radius = metrics.coreRadius * Math.max(0.22, 0.58 - index * 0.12 + wakeStrength * 0.08)
const alpha = Math.max(0.06, (0.14 + wakeStrength * 0.12) * (1 - index * 0.26))
ctx.beginPath()
ctx.fillStyle = `rgba(${markerR}, ${markerG}, ${markerB}, ${alpha})`
ctx.arc(gpsScreenPoint.x + center.x, gpsScreenPoint.y + center.y, radius, 0, Math.PI * 2)
ctx.fill()
}
}
if (warningGlowStrength > 0.04) {
const glowPhase = getGpsPulsePhase(pulseFrame, motionState)
ctx.beginPath()
ctx.strokeStyle = `rgba(${markerR}, ${markerG}, ${markerB}, ${0.18 + warningGlowStrength * 0.18})`
ctx.lineWidth = Math.max(2, metrics.ringWidth * (1.04 + warningGlowStrength * 0.2))
ctx.arc(
gpsScreenPoint.x,
gpsScreenPoint.y,
metrics.ringRadius * (1.18 + warningGlowStrength * 0.12 + glowPhase * 0.04),
0,
Math.PI * 2,
)
ctx.stroke()
}
if (style === 'beacon' || (style === 'badge' && !hasBadgeLogo)) {
ctx.beginPath()
const pulseAlpha = style === 'badge'
? Math.min(0.2, 0.08 + pulseStrength * 0.06)
: Math.min(0.26, 0.1 + pulseStrength * 0.08)
ctx.fillStyle = `rgba(255, 255, 255, ${pulseAlpha})`
ctx.arc(gpsScreenPoint.x, gpsScreenPoint.y, pulse, 0, Math.PI * 2)
ctx.fill()
}
ctx.beginPath()
ctx.fillStyle = '#21a1bc'
ctx.arc(gpsScreenPoint.x, gpsScreenPoint.y, 9, 0, Math.PI * 2)
ctx.fill()
if (style === 'dot') {
ctx.beginPath()
ctx.fillStyle = scene.gpsMarkerStyleConfig.colorHex
ctx.arc(gpsScreenPoint.x, gpsScreenPoint.y, metrics.coreRadius * 0.82, 0, Math.PI * 2)
ctx.fill()
ctx.beginPath()
ctx.strokeStyle = scene.gpsMarkerStyleConfig.ringColorHex
ctx.lineWidth = metrics.ringWidth
ctx.arc(gpsScreenPoint.x, gpsScreenPoint.y, metrics.coreRadius + metrics.ringWidth * 0.3, 0, Math.PI * 2)
ctx.stroke()
} else if (style === 'disc') {
ctx.beginPath()
ctx.strokeStyle = scene.gpsMarkerStyleConfig.ringColorHex
ctx.lineWidth = metrics.ringWidth * 1.18
ctx.arc(gpsScreenPoint.x, gpsScreenPoint.y, metrics.ringRadius * 1.05, 0, Math.PI * 2)
ctx.stroke()
ctx.beginPath()
ctx.fillStyle = scene.gpsMarkerStyleConfig.colorHex
ctx.arc(gpsScreenPoint.x, gpsScreenPoint.y, metrics.coreRadius * 1.02, 0, Math.PI * 2)
ctx.fill()
ctx.beginPath()
ctx.fillStyle = 'rgba(255, 255, 255, 0.96)'
ctx.arc(gpsScreenPoint.x, gpsScreenPoint.y, metrics.coreRadius * 0.22, 0, Math.PI * 2)
ctx.fill()
} else if (style === 'badge') {
ctx.beginPath()
ctx.strokeStyle = 'rgba(255, 255, 255, 0.98)'
ctx.lineWidth = metrics.ringWidth * 1.12
ctx.arc(gpsScreenPoint.x, gpsScreenPoint.y, metrics.ringRadius * 1.06, 0, Math.PI * 2)
ctx.stroke()
ctx.beginPath()
ctx.fillStyle = scene.gpsMarkerStyleConfig.colorHex
ctx.arc(gpsScreenPoint.x, gpsScreenPoint.y, metrics.coreRadius * 0.98, 0, Math.PI * 2)
ctx.fill()
if (!hasBadgeLogo) {
ctx.beginPath()
ctx.fillStyle = 'rgba(255, 255, 255, 0.16)'
ctx.arc(gpsScreenPoint.x - metrics.coreRadius * 0.16, gpsScreenPoint.y - metrics.coreRadius * 0.22, metrics.coreRadius * 0.18, 0, Math.PI * 2)
ctx.fill()
}
} else {
ctx.beginPath()
ctx.fillStyle = scene.gpsMarkerStyleConfig.colorHex
ctx.arc(gpsScreenPoint.x, gpsScreenPoint.y, metrics.coreRadius, 0, Math.PI * 2)
ctx.fill()
ctx.beginPath()
ctx.strokeStyle = scene.gpsMarkerStyleConfig.ringColorHex
ctx.lineWidth = metrics.ringWidth
ctx.arc(gpsScreenPoint.x, gpsScreenPoint.y, metrics.ringRadius, 0, Math.PI * 2)
ctx.stroke()
ctx.beginPath()
ctx.fillStyle = 'rgba(255, 255, 255, 0.22)'
ctx.arc(gpsScreenPoint.x, gpsScreenPoint.y, metrics.coreRadius * 0.18, 0, Math.PI * 2)
ctx.fill()
}
ctx.beginPath()
ctx.strokeStyle = '#ffffff'
ctx.lineWidth = 3
ctx.arc(gpsScreenPoint.x, gpsScreenPoint.y, 13, 0, Math.PI * 2)
ctx.stroke()
if (scene.gpsHeadingDeg !== null && scene.gpsMarkerStyleConfig.showHeadingIndicator) {
const headingScreenRad = (scene.gpsHeadingDeg * Math.PI / 180) - scene.rotationRad
const indicatorBaseDistance = metrics.ringRadius + metrics.indicatorOffset
const indicatorSize = metrics.indicatorSize * indicatorScale
const indicatorTipDistance = indicatorBaseDistance + indicatorSize * 0.92
const tip = rotatePoint(0, -indicatorTipDistance, headingScreenRad)
const left = rotatePoint(-indicatorSize * 0.56, -indicatorBaseDistance, headingScreenRad)
const right = rotatePoint(indicatorSize * 0.56, -indicatorBaseDistance, headingScreenRad)
const alpha = scene.gpsHeadingAlpha
ctx.fillStyle = '#0b3d4a'
ctx.font = 'bold 16px sans-serif'
ctx.textAlign = 'left'
ctx.textBaseline = 'bottom'
ctx.fillText('GPS', gpsScreenPoint.x + 14, gpsScreenPoint.y - 12)
ctx.beginPath()
ctx.fillStyle = `rgba(255, 255, 255, ${Math.max(0.42, alpha)})`
ctx.moveTo(gpsScreenPoint.x + tip.x, gpsScreenPoint.y + tip.y)
ctx.lineTo(gpsScreenPoint.x + left.x, gpsScreenPoint.y + left.y)
ctx.lineTo(gpsScreenPoint.x + right.x, gpsScreenPoint.y + right.y)
ctx.closePath()
ctx.fill()
const innerTip = rotatePoint(0, -(indicatorBaseDistance + indicatorSize * 0.72), headingScreenRad)
const innerLeft = rotatePoint(-indicatorSize * 0.4, -(indicatorBaseDistance + 0.12), headingScreenRad)
const innerRight = rotatePoint(indicatorSize * 0.4, -(indicatorBaseDistance + 0.12), headingScreenRad)
ctx.beginPath()
ctx.fillStyle = `rgba(${parseInt(scene.gpsMarkerStyleConfig.indicatorColorHex.slice(1, 3), 16)}, ${parseInt(scene.gpsMarkerStyleConfig.indicatorColorHex.slice(3, 5), 16)}, ${parseInt(scene.gpsMarkerStyleConfig.indicatorColorHex.slice(5, 7), 16)}, ${alpha})`
ctx.moveTo(gpsScreenPoint.x + innerTip.x, gpsScreenPoint.y + innerTip.y)
ctx.lineTo(gpsScreenPoint.x + innerLeft.x, gpsScreenPoint.y + innerLeft.y)
ctx.lineTo(gpsScreenPoint.x + innerRight.x, gpsScreenPoint.y + innerRight.y)
ctx.closePath()
ctx.fill()
}
ctx.restore()
}
}