690 lines
23 KiB
TypeScript
690 lines
23 KiB
TypeScript
import { getTileSizePx, type CameraState } from '../camera/camera'
|
|
import { worldTileToLonLat } from '../../utils/projection'
|
|
import { type MapScene } from './mapRenderer'
|
|
import { CourseLayer, type ProjectedCourseLayers, type ProjectedCourseLeg } from '../layer/courseLayer'
|
|
import { TrackLayer } from '../layer/trackLayer'
|
|
import { GpsLayer } from '../layer/gpsLayer'
|
|
|
|
const COURSE_COLOR: [number, number, number, number] = [0.8, 0.0, 0.42, 0.96]
|
|
const COMPLETED_ROUTE_COLOR: [number, number, number, number] = [0.48, 0.5, 0.54, 0.82]
|
|
const ACTIVE_CONTROL_COLOR: [number, number, number, number] = [0.22, 1, 0.95, 1]
|
|
const ACTIVE_LEG_COLOR: [number, number, number, number] = [0.18, 1, 0.94, 0.5]
|
|
const EARTH_CIRCUMFERENCE_METERS = 40075016.686
|
|
const CONTROL_RING_WIDTH_RATIO = 0.2
|
|
const FINISH_INNER_RADIUS_RATIO = 0.6
|
|
const FINISH_RING_WIDTH_RATIO = 0.2
|
|
const START_RING_WIDTH_RATIO = 0.2
|
|
const LEG_WIDTH_RATIO = 0.2
|
|
const LEG_TRIM_TO_RING_CENTER_RATIO = 1 - CONTROL_RING_WIDTH_RATIO / 2
|
|
const ACTIVE_CONTROL_PULSE_SPEED = 0.18
|
|
const ACTIVE_CONTROL_PULSE_MIN_SCALE = 1.12
|
|
const ACTIVE_CONTROL_PULSE_MAX_SCALE = 1.46
|
|
const ACTIVE_CONTROL_PULSE_WIDTH_RATIO = 0.12
|
|
const GUIDE_FLOW_COUNT = 5
|
|
const GUIDE_FLOW_SPEED = 0.02
|
|
const GUIDE_FLOW_TRAIL = 0.16
|
|
const GUIDE_FLOW_MIN_WIDTH_RATIO = 0.12
|
|
const GUIDE_FLOW_MAX_WIDTH_RATIO = 0.22
|
|
const GUIDE_FLOW_HEAD_RADIUS_RATIO = 0.18
|
|
|
|
type RgbaColor = [number, number, number, number]
|
|
|
|
function createShader(gl: any, type: number, source: string): any {
|
|
const shader = gl.createShader(type)
|
|
if (!shader) {
|
|
throw new Error('WebGL shader 创建失败')
|
|
}
|
|
|
|
gl.shaderSource(shader, source)
|
|
gl.compileShader(shader)
|
|
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
|
const message = gl.getShaderInfoLog(shader) || 'unknown shader error'
|
|
gl.deleteShader(shader)
|
|
throw new Error(message)
|
|
}
|
|
|
|
return shader
|
|
}
|
|
|
|
function createProgram(gl: any, vertexSource: string, fragmentSource: string): any {
|
|
const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexSource)
|
|
const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentSource)
|
|
const program = gl.createProgram()
|
|
if (!program) {
|
|
throw new Error('WebGL program 创建失败')
|
|
}
|
|
|
|
gl.attachShader(program, vertexShader)
|
|
gl.attachShader(program, fragmentShader)
|
|
gl.linkProgram(program)
|
|
gl.deleteShader(vertexShader)
|
|
gl.deleteShader(fragmentShader)
|
|
|
|
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
|
|
const message = gl.getProgramInfoLog(program) || 'unknown program error'
|
|
gl.deleteProgram(program)
|
|
throw new Error(message)
|
|
}
|
|
|
|
return program
|
|
}
|
|
|
|
export class WebGLVectorRenderer {
|
|
canvas: any
|
|
gl: any
|
|
dpr: number
|
|
courseLayer: CourseLayer
|
|
trackLayer: TrackLayer
|
|
gpsLayer: GpsLayer
|
|
program: any
|
|
positionBuffer: any
|
|
colorBuffer: any
|
|
positionLocation: number
|
|
colorLocation: number
|
|
|
|
constructor(courseLayer: CourseLayer, trackLayer: TrackLayer, gpsLayer: GpsLayer) {
|
|
this.canvas = null
|
|
this.gl = null
|
|
this.dpr = 1
|
|
this.courseLayer = courseLayer
|
|
this.trackLayer = trackLayer
|
|
this.gpsLayer = gpsLayer
|
|
this.program = null
|
|
this.positionBuffer = null
|
|
this.colorBuffer = null
|
|
this.positionLocation = -1
|
|
this.colorLocation = -1
|
|
}
|
|
|
|
attachCanvas(canvasNode: any, width: number, height: number, dpr: number): void {
|
|
this.canvas = canvasNode
|
|
this.dpr = dpr || 1
|
|
canvasNode.width = Math.max(1, Math.floor(width * this.dpr))
|
|
canvasNode.height = Math.max(1, Math.floor(height * this.dpr))
|
|
this.attachContext(canvasNode.getContext('webgl') || canvasNode.getContext('experimental-webgl'), canvasNode)
|
|
}
|
|
|
|
attachContext(gl: any, canvasNode: any): void {
|
|
if (!gl) {
|
|
throw new Error('当前环境不支持 WebGL Vector Layer')
|
|
}
|
|
|
|
this.canvas = canvasNode
|
|
this.gl = gl
|
|
this.program = createProgram(
|
|
gl,
|
|
'attribute vec2 a_position; attribute vec4 a_color; varying vec4 v_color; void main() { gl_Position = vec4(a_position, 0.0, 1.0); v_color = a_color; }',
|
|
'precision mediump float; varying vec4 v_color; void main() { gl_FragColor = v_color; }',
|
|
)
|
|
this.positionBuffer = gl.createBuffer()
|
|
this.colorBuffer = gl.createBuffer()
|
|
this.positionLocation = gl.getAttribLocation(this.program, 'a_position')
|
|
this.colorLocation = gl.getAttribLocation(this.program, 'a_color')
|
|
|
|
gl.viewport(0, 0, canvasNode.width, canvasNode.height)
|
|
gl.enable(gl.BLEND)
|
|
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
|
|
}
|
|
|
|
destroy(): void {
|
|
if (this.gl) {
|
|
if (this.program) {
|
|
this.gl.deleteProgram(this.program)
|
|
}
|
|
if (this.positionBuffer) {
|
|
this.gl.deleteBuffer(this.positionBuffer)
|
|
}
|
|
if (this.colorBuffer) {
|
|
this.gl.deleteBuffer(this.colorBuffer)
|
|
}
|
|
}
|
|
|
|
this.program = null
|
|
this.positionBuffer = null
|
|
this.colorBuffer = null
|
|
this.gl = null
|
|
this.canvas = null
|
|
}
|
|
|
|
render(scene: MapScene, pulseFrame: number): void {
|
|
if (!this.gl || !this.program || !this.positionBuffer || !this.colorBuffer || !this.canvas) {
|
|
return
|
|
}
|
|
|
|
const gl = this.gl
|
|
const course = this.courseLayer.projectCourse(scene)
|
|
const trackPoints = this.trackLayer.projectPoints(scene)
|
|
const gpsPoint = this.gpsLayer.projectPoint(scene)
|
|
const positions: number[] = []
|
|
const colors: number[] = []
|
|
|
|
if (course) {
|
|
this.pushCourse(positions, colors, course, scene, pulseFrame)
|
|
}
|
|
|
|
for (let index = 1; index < trackPoints.length; index += 1) {
|
|
this.pushSegment(positions, colors, trackPoints[index - 1], trackPoints[index], 6, [0.09, 0.43, 0.36, 0.96], scene)
|
|
}
|
|
|
|
for (const point of trackPoints) {
|
|
this.pushCircle(positions, colors, point.x, point.y, 10, [0.09, 0.43, 0.36, 1], scene)
|
|
this.pushCircle(positions, colors, point.x, point.y, 6.5, [0.97, 0.98, 0.95, 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)
|
|
|
|
gl.bindBuffer(gl.ARRAY_BUFFER, this.positionBuffer)
|
|
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STREAM_DRAW)
|
|
gl.enableVertexAttribArray(this.positionLocation)
|
|
gl.vertexAttribPointer(this.positionLocation, 2, gl.FLOAT, false, 0, 0)
|
|
|
|
gl.bindBuffer(gl.ARRAY_BUFFER, this.colorBuffer)
|
|
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STREAM_DRAW)
|
|
gl.enableVertexAttribArray(this.colorLocation)
|
|
gl.vertexAttribPointer(this.colorLocation, 4, gl.FLOAT, false, 0, 0)
|
|
|
|
gl.drawArrays(gl.TRIANGLES, 0, positions.length / 2)
|
|
}
|
|
|
|
getPixelsPerMeter(scene: MapScene): number {
|
|
const camera: CameraState = {
|
|
centerWorldX: scene.exactCenterWorldX,
|
|
centerWorldY: scene.exactCenterWorldY,
|
|
viewportWidth: scene.viewportWidth,
|
|
viewportHeight: scene.viewportHeight,
|
|
visibleColumns: scene.visibleColumns,
|
|
}
|
|
const tileSizePx = getTileSizePx(camera)
|
|
const centerLonLat = worldTileToLonLat({ x: scene.exactCenterWorldX, y: scene.exactCenterWorldY }, scene.zoom)
|
|
const metersPerTile = Math.cos(centerLonLat.lat * Math.PI / 180) * EARTH_CIRCUMFERENCE_METERS / Math.pow(2, scene.zoom)
|
|
if (!tileSizePx || !metersPerTile) {
|
|
return 0
|
|
}
|
|
return tileSizePx / metersPerTile
|
|
}
|
|
|
|
getMetric(scene: MapScene, meters: number): number {
|
|
return meters * this.getPixelsPerMeter(scene)
|
|
}
|
|
|
|
getControlRadiusMeters(scene: MapScene): number {
|
|
return scene.cpRadiusMeters > 0 ? scene.cpRadiusMeters : 5
|
|
}
|
|
|
|
pushCourse(
|
|
positions: number[],
|
|
colors: number[],
|
|
course: ProjectedCourseLayers,
|
|
scene: MapScene,
|
|
pulseFrame: number,
|
|
): void {
|
|
const controlRadiusMeters = this.getControlRadiusMeters(scene)
|
|
|
|
if (scene.revealFullCourse) {
|
|
for (let index = 0; index < course.legs.length; index += 1) {
|
|
const leg = course.legs[index]
|
|
this.pushCourseLeg(positions, colors, leg, controlRadiusMeters, this.getLegColor(scene, index), scene)
|
|
if (scene.activeLegIndices.includes(index)) {
|
|
this.pushCourseLegHighlight(positions, colors, leg, controlRadiusMeters, scene)
|
|
}
|
|
}
|
|
|
|
const guideLeg = this.getGuideLeg(course, scene)
|
|
if (guideLeg) {
|
|
this.pushGuidanceFlow(positions, colors, guideLeg, controlRadiusMeters, scene, pulseFrame)
|
|
}
|
|
}
|
|
|
|
for (const start of course.starts) {
|
|
if (scene.activeStart) {
|
|
this.pushActiveStartPulse(positions, colors, start.point.x, start.point.y, start.headingDeg, controlRadiusMeters, scene, pulseFrame)
|
|
}
|
|
this.pushStartTriangle(positions, colors, start.point.x, start.point.y, start.headingDeg, controlRadiusMeters, this.getStartColor(scene), scene)
|
|
}
|
|
if (!scene.revealFullCourse) {
|
|
return
|
|
}
|
|
|
|
for (const control of course.controls) {
|
|
if (scene.activeControlSequences.includes(control.sequence)) {
|
|
this.pushActiveControlPulse(positions, colors, control.point.x, control.point.y, controlRadiusMeters, scene, pulseFrame)
|
|
}
|
|
|
|
this.pushRing(
|
|
positions,
|
|
colors,
|
|
control.point.x,
|
|
control.point.y,
|
|
this.getMetric(scene, controlRadiusMeters),
|
|
this.getMetric(scene, controlRadiusMeters * (1 - CONTROL_RING_WIDTH_RATIO)),
|
|
this.getControlColor(scene, control.sequence),
|
|
scene,
|
|
)
|
|
}
|
|
|
|
for (const finish of course.finishes) {
|
|
if (scene.activeFinish) {
|
|
this.pushActiveControlPulse(positions, colors, finish.point.x, finish.point.y, controlRadiusMeters, scene, pulseFrame)
|
|
}
|
|
|
|
const finishColor = this.getFinishColor(scene)
|
|
this.pushRing(
|
|
positions,
|
|
colors,
|
|
finish.point.x,
|
|
finish.point.y,
|
|
this.getMetric(scene, controlRadiusMeters),
|
|
this.getMetric(scene, controlRadiusMeters * (1 - FINISH_RING_WIDTH_RATIO)),
|
|
finishColor,
|
|
scene,
|
|
)
|
|
this.pushRing(
|
|
positions,
|
|
colors,
|
|
finish.point.x,
|
|
finish.point.y,
|
|
this.getMetric(scene, controlRadiusMeters * FINISH_INNER_RADIUS_RATIO),
|
|
this.getMetric(scene, controlRadiusMeters * FINISH_INNER_RADIUS_RATIO * (1 - FINISH_RING_WIDTH_RATIO / FINISH_INNER_RADIUS_RATIO)),
|
|
finishColor,
|
|
scene,
|
|
)
|
|
}
|
|
}
|
|
|
|
getGuideLeg(course: ProjectedCourseLayers, scene: MapScene): ProjectedCourseLeg | null {
|
|
const activeIndex = scene.activeLegIndices.length ? scene.activeLegIndices[0] : -1
|
|
if (activeIndex >= 0 && activeIndex < course.legs.length) {
|
|
return course.legs[activeIndex]
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
getLegColor(scene: MapScene, index: number): RgbaColor {
|
|
return this.isCompletedLeg(scene, index) ? COMPLETED_ROUTE_COLOR : COURSE_COLOR
|
|
}
|
|
|
|
isCompletedLeg(scene: MapScene, index: number): boolean {
|
|
return scene.completedLegIndices.includes(index)
|
|
}
|
|
|
|
pushCourseLeg(
|
|
positions: number[],
|
|
colors: number[],
|
|
leg: ProjectedCourseLeg,
|
|
controlRadiusMeters: number,
|
|
color: RgbaColor,
|
|
scene: MapScene,
|
|
): void {
|
|
const trimmed = this.getTrimmedCourseLeg(leg, controlRadiusMeters, scene)
|
|
if (!trimmed) {
|
|
return
|
|
}
|
|
|
|
this.pushSegment(positions, colors, trimmed.from, trimmed.to, this.getMetric(scene, controlRadiusMeters * LEG_WIDTH_RATIO), color, scene)
|
|
}
|
|
|
|
pushCourseLegHighlight(
|
|
positions: number[],
|
|
colors: number[],
|
|
leg: ProjectedCourseLeg,
|
|
controlRadiusMeters: number,
|
|
scene: MapScene,
|
|
): void {
|
|
const trimmed = this.getTrimmedCourseLeg(leg, controlRadiusMeters, scene)
|
|
if (!trimmed) {
|
|
return
|
|
}
|
|
|
|
this.pushSegment(
|
|
positions,
|
|
colors,
|
|
trimmed.from,
|
|
trimmed.to,
|
|
this.getMetric(scene, controlRadiusMeters * LEG_WIDTH_RATIO * 1.5),
|
|
ACTIVE_LEG_COLOR,
|
|
scene,
|
|
)
|
|
}
|
|
|
|
pushActiveControlPulse(
|
|
positions: number[],
|
|
colors: number[],
|
|
centerX: number,
|
|
centerY: number,
|
|
controlRadiusMeters: number,
|
|
scene: MapScene,
|
|
pulseFrame: number,
|
|
): void {
|
|
const pulse = (Math.sin(pulseFrame * ACTIVE_CONTROL_PULSE_SPEED) + 1) / 2
|
|
const pulseScale = ACTIVE_CONTROL_PULSE_MIN_SCALE + (ACTIVE_CONTROL_PULSE_MAX_SCALE - ACTIVE_CONTROL_PULSE_MIN_SCALE) * pulse
|
|
const pulseWidthScale = pulseScale - ACTIVE_CONTROL_PULSE_WIDTH_RATIO
|
|
const glowAlpha = 0.24 + pulse * 0.34
|
|
const glowColor: RgbaColor = [0.36, 1, 0.96, glowAlpha]
|
|
|
|
this.pushRing(
|
|
positions,
|
|
colors,
|
|
centerX,
|
|
centerY,
|
|
this.getMetric(scene, controlRadiusMeters * pulseScale),
|
|
this.getMetric(scene, controlRadiusMeters * Math.max(1, pulseWidthScale)),
|
|
glowColor,
|
|
scene,
|
|
)
|
|
}
|
|
|
|
pushActiveStartPulse(
|
|
positions: number[],
|
|
colors: number[],
|
|
centerX: number,
|
|
centerY: number,
|
|
headingDeg: number | null,
|
|
controlRadiusMeters: number,
|
|
scene: MapScene,
|
|
pulseFrame: number,
|
|
): void {
|
|
const pulse = (Math.sin(pulseFrame * ACTIVE_CONTROL_PULSE_SPEED) + 1) / 2
|
|
const pulseScale = ACTIVE_CONTROL_PULSE_MIN_SCALE + (ACTIVE_CONTROL_PULSE_MAX_SCALE - ACTIVE_CONTROL_PULSE_MIN_SCALE) * pulse
|
|
const pulseWidthScale = pulseScale - ACTIVE_CONTROL_PULSE_WIDTH_RATIO
|
|
const glowAlpha = 0.24 + pulse * 0.34
|
|
const glowColor: RgbaColor = [0.36, 1, 0.96, glowAlpha]
|
|
const headingRad = ((headingDeg === null ? 0 : headingDeg) - 90) * Math.PI / 180
|
|
const ringCenterX = centerX + Math.cos(headingRad) * this.getMetric(scene, controlRadiusMeters * 0.04)
|
|
const ringCenterY = centerY + Math.sin(headingRad) * this.getMetric(scene, controlRadiusMeters * 0.04)
|
|
|
|
this.pushRing(
|
|
positions,
|
|
colors,
|
|
ringCenterX,
|
|
ringCenterY,
|
|
this.getMetric(scene, controlRadiusMeters * pulseScale),
|
|
this.getMetric(scene, controlRadiusMeters * Math.max(1, pulseWidthScale)),
|
|
glowColor,
|
|
scene,
|
|
)
|
|
}
|
|
|
|
getStartColor(scene: MapScene): RgbaColor {
|
|
if (scene.activeStart) {
|
|
return ACTIVE_CONTROL_COLOR
|
|
}
|
|
|
|
if (scene.completedStart) {
|
|
return COMPLETED_ROUTE_COLOR
|
|
}
|
|
|
|
return COURSE_COLOR
|
|
}
|
|
|
|
getControlColor(scene: MapScene, sequence: number): RgbaColor {
|
|
if (scene.activeControlSequences.includes(sequence)) {
|
|
return ACTIVE_CONTROL_COLOR
|
|
}
|
|
|
|
if (scene.completedControlSequences.includes(sequence)) {
|
|
return COMPLETED_ROUTE_COLOR
|
|
}
|
|
|
|
return COURSE_COLOR
|
|
}
|
|
|
|
|
|
getFinishColor(scene: MapScene): RgbaColor {
|
|
if (scene.activeFinish) {
|
|
return ACTIVE_CONTROL_COLOR
|
|
}
|
|
|
|
if (scene.completedFinish) {
|
|
return COMPLETED_ROUTE_COLOR
|
|
}
|
|
|
|
return COURSE_COLOR
|
|
}
|
|
|
|
pushGuidanceFlow(
|
|
positions: number[],
|
|
colors: number[],
|
|
leg: ProjectedCourseLeg,
|
|
controlRadiusMeters: number,
|
|
scene: MapScene,
|
|
pulseFrame: number,
|
|
): void {
|
|
const trimmed = this.getTrimmedCourseLeg(leg, controlRadiusMeters, scene)
|
|
if (!trimmed) {
|
|
return
|
|
}
|
|
|
|
const dx = trimmed.to.x - trimmed.from.x
|
|
const dy = trimmed.to.y - trimmed.from.y
|
|
const length = Math.sqrt(dx * dx + dy * dy)
|
|
if (!length) {
|
|
return
|
|
}
|
|
|
|
for (let index = 0; index < GUIDE_FLOW_COUNT; index += 1) {
|
|
const progress = (pulseFrame * GUIDE_FLOW_SPEED + index / GUIDE_FLOW_COUNT) % 1
|
|
const tailProgress = Math.max(0, progress - GUIDE_FLOW_TRAIL)
|
|
const head = {
|
|
x: trimmed.from.x + dx * progress,
|
|
y: trimmed.from.y + dy * progress,
|
|
}
|
|
const tail = {
|
|
x: trimmed.from.x + dx * tailProgress,
|
|
y: trimmed.from.y + dy * tailProgress,
|
|
}
|
|
const eased = progress * progress
|
|
const width = this.getMetric(
|
|
scene,
|
|
controlRadiusMeters * (GUIDE_FLOW_MIN_WIDTH_RATIO + (GUIDE_FLOW_MAX_WIDTH_RATIO - GUIDE_FLOW_MIN_WIDTH_RATIO) * eased),
|
|
)
|
|
const outerColor = this.getGuideFlowOuterColor(eased)
|
|
const innerColor = this.getGuideFlowInnerColor(eased)
|
|
const headRadius = this.getMetric(scene, controlRadiusMeters * GUIDE_FLOW_HEAD_RADIUS_RATIO * (0.72 + eased * 0.42))
|
|
|
|
this.pushSegment(positions, colors, tail, head, width * 1.9, outerColor, scene)
|
|
this.pushSegment(positions, colors, tail, head, width, innerColor, scene)
|
|
this.pushCircle(positions, colors, head.x, head.y, headRadius * 1.35, outerColor, scene)
|
|
this.pushCircle(positions, colors, head.x, head.y, headRadius, innerColor, scene)
|
|
}
|
|
}
|
|
|
|
getTrimmedCourseLeg(
|
|
leg: ProjectedCourseLeg,
|
|
controlRadiusMeters: number,
|
|
scene: MapScene,
|
|
): { from: { x: number; y: number }; to: { x: number; y: number } } | null {
|
|
return this.trimSegment(
|
|
leg.from,
|
|
leg.to,
|
|
this.getLegTrim(leg.fromKind, controlRadiusMeters, scene),
|
|
this.getLegTrim(leg.toKind, controlRadiusMeters, scene),
|
|
)
|
|
}
|
|
|
|
getGuideFlowOuterColor(progress: number): RgbaColor {
|
|
return [0.28, 0.92, 1, 0.14 + progress * 0.22]
|
|
}
|
|
|
|
getGuideFlowInnerColor(progress: number): RgbaColor {
|
|
return [0.94, 0.99, 1, 0.38 + progress * 0.42]
|
|
}
|
|
|
|
getLegTrim(kind: ProjectedCourseLeg['fromKind'], controlRadiusMeters: number, scene: MapScene): number {
|
|
if (kind === 'start') {
|
|
return this.getMetric(scene, controlRadiusMeters * (1 - START_RING_WIDTH_RATIO / 2))
|
|
}
|
|
|
|
if (kind === 'finish') {
|
|
return this.getMetric(scene, controlRadiusMeters * (1 - FINISH_RING_WIDTH_RATIO / 2))
|
|
}
|
|
|
|
return this.getMetric(scene, controlRadiusMeters * LEG_TRIM_TO_RING_CENTER_RATIO)
|
|
}
|
|
|
|
trimSegment(
|
|
from: { x: number; y: number },
|
|
to: { x: number; y: number },
|
|
fromTrim: number,
|
|
toTrim: number,
|
|
): { from: { x: number; y: number }; to: { x: number; y: number } } | null {
|
|
const dx = to.x - from.x
|
|
const dy = to.y - from.y
|
|
const length = Math.sqrt(dx * dx + dy * dy)
|
|
if (!length || length <= fromTrim + toTrim) {
|
|
return null
|
|
}
|
|
|
|
const ux = dx / length
|
|
const uy = dy / length
|
|
return {
|
|
from: {
|
|
x: from.x + ux * fromTrim,
|
|
y: from.y + uy * fromTrim,
|
|
},
|
|
to: {
|
|
x: to.x - ux * toTrim,
|
|
y: to.y - uy * toTrim,
|
|
},
|
|
}
|
|
}
|
|
|
|
pushStartTriangle(
|
|
positions: number[],
|
|
colors: number[],
|
|
centerX: number,
|
|
centerY: number,
|
|
headingDeg: number | null,
|
|
controlRadiusMeters: number,
|
|
color: RgbaColor,
|
|
scene: MapScene,
|
|
): void {
|
|
const startRadius = this.getMetric(scene, controlRadiusMeters)
|
|
const startRingWidth = this.getMetric(scene, controlRadiusMeters * START_RING_WIDTH_RATIO)
|
|
const headingRad = ((headingDeg === null ? 0 : headingDeg) - 90) * Math.PI / 180
|
|
const vertices = [0, 1, 2].map((index) => {
|
|
const angle = headingRad + index * (Math.PI * 2 / 3)
|
|
return {
|
|
x: centerX + Math.cos(angle) * startRadius,
|
|
y: centerY + Math.sin(angle) * startRadius,
|
|
}
|
|
})
|
|
|
|
this.pushSegment(positions, colors, vertices[0], vertices[1], startRingWidth, color, scene)
|
|
this.pushSegment(positions, colors, vertices[1], vertices[2], startRingWidth, color, scene)
|
|
this.pushSegment(positions, colors, vertices[2], vertices[0], startRingWidth, color, scene)
|
|
}
|
|
|
|
pushRing(
|
|
positions: number[],
|
|
colors: number[],
|
|
centerX: number,
|
|
centerY: number,
|
|
outerRadius: number,
|
|
innerRadius: number,
|
|
color: RgbaColor,
|
|
scene: MapScene,
|
|
): void {
|
|
const segments = 36
|
|
for (let index = 0; index < segments; index += 1) {
|
|
const startAngle = index / segments * Math.PI * 2
|
|
const endAngle = (index + 1) / segments * Math.PI * 2
|
|
const outerStart = this.toClip(centerX + Math.cos(startAngle) * outerRadius, centerY + Math.sin(startAngle) * outerRadius, scene)
|
|
const outerEnd = this.toClip(centerX + Math.cos(endAngle) * outerRadius, centerY + Math.sin(endAngle) * outerRadius, scene)
|
|
const innerStart = this.toClip(centerX + Math.cos(startAngle) * innerRadius, centerY + Math.sin(startAngle) * innerRadius, scene)
|
|
const innerEnd = this.toClip(centerX + Math.cos(endAngle) * innerRadius, centerY + Math.sin(endAngle) * innerRadius, scene)
|
|
this.pushTriangle(positions, colors, outerStart, outerEnd, innerStart, color)
|
|
this.pushTriangle(positions, colors, innerStart, outerEnd, innerEnd, color)
|
|
}
|
|
}
|
|
|
|
pushSegment(
|
|
positions: number[],
|
|
colors: number[],
|
|
start: { x: number; y: number },
|
|
end: { x: number; y: number },
|
|
width: number,
|
|
color: RgbaColor,
|
|
scene: MapScene,
|
|
): void {
|
|
const deltaX = end.x - start.x
|
|
const deltaY = end.y - start.y
|
|
const length = Math.sqrt(deltaX * deltaX + deltaY * deltaY)
|
|
if (!length) {
|
|
return
|
|
}
|
|
|
|
const normalX = -deltaY / length * (width / 2)
|
|
const normalY = deltaX / length * (width / 2)
|
|
const topLeft = this.toClip(start.x + normalX, start.y + normalY, scene)
|
|
const topRight = this.toClip(end.x + normalX, end.y + normalY, scene)
|
|
const bottomLeft = this.toClip(start.x - normalX, start.y - normalY, scene)
|
|
const bottomRight = this.toClip(end.x - normalX, end.y - normalY, scene)
|
|
|
|
this.pushTriangle(positions, colors, topLeft, topRight, bottomLeft, color)
|
|
this.pushTriangle(positions, colors, bottomLeft, topRight, bottomRight, color)
|
|
}
|
|
|
|
pushCircle(
|
|
positions: number[],
|
|
colors: number[],
|
|
centerX: number,
|
|
centerY: number,
|
|
radius: number,
|
|
color: RgbaColor,
|
|
scene: MapScene,
|
|
): void {
|
|
const segments = 20
|
|
const center = this.toClip(centerX, centerY, scene)
|
|
for (let index = 0; index < segments; index += 1) {
|
|
const startAngle = index / segments * Math.PI * 2
|
|
const endAngle = (index + 1) / segments * Math.PI * 2
|
|
const start = this.toClip(centerX + Math.cos(startAngle) * radius, centerY + Math.sin(startAngle) * radius, scene)
|
|
const end = this.toClip(centerX + Math.cos(endAngle) * radius, centerY + Math.sin(endAngle) * radius, scene)
|
|
this.pushTriangle(positions, colors, center, start, end, color)
|
|
}
|
|
}
|
|
|
|
pushTriangle(
|
|
positions: number[],
|
|
colors: number[],
|
|
first: { x: number; y: number },
|
|
second: { x: number; y: number },
|
|
third: { x: number; y: number },
|
|
color: RgbaColor,
|
|
): void {
|
|
positions.push(first.x, first.y, second.x, second.y, third.x, third.y)
|
|
for (let index = 0; index < 3; index += 1) {
|
|
colors.push(color[0], color[1], color[2], color[3])
|
|
}
|
|
}
|
|
|
|
toClip(x: number, y: number, scene: MapScene): { x: number; y: number } {
|
|
const previewScale = scene.previewScale || 1
|
|
const originX = scene.previewOriginX || scene.viewportWidth / 2
|
|
const originY = scene.previewOriginY || scene.viewportHeight / 2
|
|
const scaledX = originX + (x - originX) * previewScale
|
|
const scaledY = originY + (y - originY) * previewScale
|
|
|
|
return {
|
|
x: scaledX / scene.viewportWidth * 2 - 1,
|
|
y: 1 - scaledY / scene.viewportHeight * 2,
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|