webglVectorRenderer.ts 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689
  1. import { getTileSizePx, type CameraState } from '../camera/camera'
  2. import { worldTileToLonLat } from '../../utils/projection'
  3. import { type MapScene } from './mapRenderer'
  4. import { CourseLayer, type ProjectedCourseLayers, type ProjectedCourseLeg } from '../layer/courseLayer'
  5. import { TrackLayer } from '../layer/trackLayer'
  6. import { GpsLayer } from '../layer/gpsLayer'
  7. const COURSE_COLOR: [number, number, number, number] = [0.8, 0.0, 0.42, 0.96]
  8. const COMPLETED_ROUTE_COLOR: [number, number, number, number] = [0.48, 0.5, 0.54, 0.82]
  9. const ACTIVE_CONTROL_COLOR: [number, number, number, number] = [0.22, 1, 0.95, 1]
  10. const ACTIVE_LEG_COLOR: [number, number, number, number] = [0.18, 1, 0.94, 0.5]
  11. const EARTH_CIRCUMFERENCE_METERS = 40075016.686
  12. const CONTROL_RING_WIDTH_RATIO = 0.2
  13. const FINISH_INNER_RADIUS_RATIO = 0.6
  14. const FINISH_RING_WIDTH_RATIO = 0.2
  15. const START_RING_WIDTH_RATIO = 0.2
  16. const LEG_WIDTH_RATIO = 0.2
  17. const LEG_TRIM_TO_RING_CENTER_RATIO = 1 - CONTROL_RING_WIDTH_RATIO / 2
  18. const ACTIVE_CONTROL_PULSE_SPEED = 0.18
  19. const ACTIVE_CONTROL_PULSE_MIN_SCALE = 1.12
  20. const ACTIVE_CONTROL_PULSE_MAX_SCALE = 1.46
  21. const ACTIVE_CONTROL_PULSE_WIDTH_RATIO = 0.12
  22. const GUIDE_FLOW_COUNT = 5
  23. const GUIDE_FLOW_SPEED = 0.02
  24. const GUIDE_FLOW_TRAIL = 0.16
  25. const GUIDE_FLOW_MIN_WIDTH_RATIO = 0.12
  26. const GUIDE_FLOW_MAX_WIDTH_RATIO = 0.22
  27. const GUIDE_FLOW_HEAD_RADIUS_RATIO = 0.18
  28. type RgbaColor = [number, number, number, number]
  29. function createShader(gl: any, type: number, source: string): any {
  30. const shader = gl.createShader(type)
  31. if (!shader) {
  32. throw new Error('WebGL shader 创建失败')
  33. }
  34. gl.shaderSource(shader, source)
  35. gl.compileShader(shader)
  36. if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
  37. const message = gl.getShaderInfoLog(shader) || 'unknown shader error'
  38. gl.deleteShader(shader)
  39. throw new Error(message)
  40. }
  41. return shader
  42. }
  43. function createProgram(gl: any, vertexSource: string, fragmentSource: string): any {
  44. const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexSource)
  45. const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentSource)
  46. const program = gl.createProgram()
  47. if (!program) {
  48. throw new Error('WebGL program 创建失败')
  49. }
  50. gl.attachShader(program, vertexShader)
  51. gl.attachShader(program, fragmentShader)
  52. gl.linkProgram(program)
  53. gl.deleteShader(vertexShader)
  54. gl.deleteShader(fragmentShader)
  55. if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
  56. const message = gl.getProgramInfoLog(program) || 'unknown program error'
  57. gl.deleteProgram(program)
  58. throw new Error(message)
  59. }
  60. return program
  61. }
  62. export class WebGLVectorRenderer {
  63. canvas: any
  64. gl: any
  65. dpr: number
  66. courseLayer: CourseLayer
  67. trackLayer: TrackLayer
  68. gpsLayer: GpsLayer
  69. program: any
  70. positionBuffer: any
  71. colorBuffer: any
  72. positionLocation: number
  73. colorLocation: number
  74. constructor(courseLayer: CourseLayer, trackLayer: TrackLayer, gpsLayer: GpsLayer) {
  75. this.canvas = null
  76. this.gl = null
  77. this.dpr = 1
  78. this.courseLayer = courseLayer
  79. this.trackLayer = trackLayer
  80. this.gpsLayer = gpsLayer
  81. this.program = null
  82. this.positionBuffer = null
  83. this.colorBuffer = null
  84. this.positionLocation = -1
  85. this.colorLocation = -1
  86. }
  87. attachCanvas(canvasNode: any, width: number, height: number, dpr: number): void {
  88. this.canvas = canvasNode
  89. this.dpr = dpr || 1
  90. canvasNode.width = Math.max(1, Math.floor(width * this.dpr))
  91. canvasNode.height = Math.max(1, Math.floor(height * this.dpr))
  92. this.attachContext(canvasNode.getContext('webgl') || canvasNode.getContext('experimental-webgl'), canvasNode)
  93. }
  94. attachContext(gl: any, canvasNode: any): void {
  95. if (!gl) {
  96. throw new Error('当前环境不支持 WebGL Vector Layer')
  97. }
  98. this.canvas = canvasNode
  99. this.gl = gl
  100. this.program = createProgram(
  101. gl,
  102. '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; }',
  103. 'precision mediump float; varying vec4 v_color; void main() { gl_FragColor = v_color; }',
  104. )
  105. this.positionBuffer = gl.createBuffer()
  106. this.colorBuffer = gl.createBuffer()
  107. this.positionLocation = gl.getAttribLocation(this.program, 'a_position')
  108. this.colorLocation = gl.getAttribLocation(this.program, 'a_color')
  109. gl.viewport(0, 0, canvasNode.width, canvasNode.height)
  110. gl.enable(gl.BLEND)
  111. gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
  112. }
  113. destroy(): void {
  114. if (this.gl) {
  115. if (this.program) {
  116. this.gl.deleteProgram(this.program)
  117. }
  118. if (this.positionBuffer) {
  119. this.gl.deleteBuffer(this.positionBuffer)
  120. }
  121. if (this.colorBuffer) {
  122. this.gl.deleteBuffer(this.colorBuffer)
  123. }
  124. }
  125. this.program = null
  126. this.positionBuffer = null
  127. this.colorBuffer = null
  128. this.gl = null
  129. this.canvas = null
  130. }
  131. render(scene: MapScene, pulseFrame: number): void {
  132. if (!this.gl || !this.program || !this.positionBuffer || !this.colorBuffer || !this.canvas) {
  133. return
  134. }
  135. const gl = this.gl
  136. const course = this.courseLayer.projectCourse(scene)
  137. const trackPoints = this.trackLayer.projectPoints(scene)
  138. const gpsPoint = this.gpsLayer.projectPoint(scene)
  139. const positions: number[] = []
  140. const colors: number[] = []
  141. if (course) {
  142. this.pushCourse(positions, colors, course, scene, pulseFrame)
  143. }
  144. for (let index = 1; index < trackPoints.length; index += 1) {
  145. this.pushSegment(positions, colors, trackPoints[index - 1], trackPoints[index], 6, [0.09, 0.43, 0.36, 0.96], scene)
  146. }
  147. for (const point of trackPoints) {
  148. this.pushCircle(positions, colors, point.x, point.y, 10, [0.09, 0.43, 0.36, 1], scene)
  149. this.pushCircle(positions, colors, point.x, point.y, 6.5, [0.97, 0.98, 0.95, 1], scene)
  150. }
  151. if (gpsPoint) {
  152. this.pushCircle(positions, colors, gpsPoint.x, gpsPoint.y, this.gpsLayer.getPulseRadius(pulseFrame), [0.13, 0.62, 0.74, 0.22], scene)
  153. this.pushCircle(positions, colors, gpsPoint.x, gpsPoint.y, 13, [1, 1, 1, 0.95], scene)
  154. this.pushCircle(positions, colors, gpsPoint.x, gpsPoint.y, 9, [0.13, 0.63, 0.74, 1], scene)
  155. }
  156. if (!positions.length) {
  157. return
  158. }
  159. gl.viewport(0, 0, this.canvas.width, this.canvas.height)
  160. gl.useProgram(this.program)
  161. gl.bindBuffer(gl.ARRAY_BUFFER, this.positionBuffer)
  162. gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STREAM_DRAW)
  163. gl.enableVertexAttribArray(this.positionLocation)
  164. gl.vertexAttribPointer(this.positionLocation, 2, gl.FLOAT, false, 0, 0)
  165. gl.bindBuffer(gl.ARRAY_BUFFER, this.colorBuffer)
  166. gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STREAM_DRAW)
  167. gl.enableVertexAttribArray(this.colorLocation)
  168. gl.vertexAttribPointer(this.colorLocation, 4, gl.FLOAT, false, 0, 0)
  169. gl.drawArrays(gl.TRIANGLES, 0, positions.length / 2)
  170. }
  171. getPixelsPerMeter(scene: MapScene): number {
  172. const camera: CameraState = {
  173. centerWorldX: scene.exactCenterWorldX,
  174. centerWorldY: scene.exactCenterWorldY,
  175. viewportWidth: scene.viewportWidth,
  176. viewportHeight: scene.viewportHeight,
  177. visibleColumns: scene.visibleColumns,
  178. }
  179. const tileSizePx = getTileSizePx(camera)
  180. const centerLonLat = worldTileToLonLat({ x: scene.exactCenterWorldX, y: scene.exactCenterWorldY }, scene.zoom)
  181. const metersPerTile = Math.cos(centerLonLat.lat * Math.PI / 180) * EARTH_CIRCUMFERENCE_METERS / Math.pow(2, scene.zoom)
  182. if (!tileSizePx || !metersPerTile) {
  183. return 0
  184. }
  185. return tileSizePx / metersPerTile
  186. }
  187. getMetric(scene: MapScene, meters: number): number {
  188. return meters * this.getPixelsPerMeter(scene)
  189. }
  190. getControlRadiusMeters(scene: MapScene): number {
  191. return scene.cpRadiusMeters > 0 ? scene.cpRadiusMeters : 5
  192. }
  193. pushCourse(
  194. positions: number[],
  195. colors: number[],
  196. course: ProjectedCourseLayers,
  197. scene: MapScene,
  198. pulseFrame: number,
  199. ): void {
  200. const controlRadiusMeters = this.getControlRadiusMeters(scene)
  201. if (scene.revealFullCourse) {
  202. for (let index = 0; index < course.legs.length; index += 1) {
  203. const leg = course.legs[index]
  204. this.pushCourseLeg(positions, colors, leg, controlRadiusMeters, this.getLegColor(scene, index), scene)
  205. if (scene.activeLegIndices.includes(index)) {
  206. this.pushCourseLegHighlight(positions, colors, leg, controlRadiusMeters, scene)
  207. }
  208. }
  209. const guideLeg = this.getGuideLeg(course, scene)
  210. if (guideLeg) {
  211. this.pushGuidanceFlow(positions, colors, guideLeg, controlRadiusMeters, scene, pulseFrame)
  212. }
  213. }
  214. for (const start of course.starts) {
  215. if (scene.activeStart) {
  216. this.pushActiveStartPulse(positions, colors, start.point.x, start.point.y, start.headingDeg, controlRadiusMeters, scene, pulseFrame)
  217. }
  218. this.pushStartTriangle(positions, colors, start.point.x, start.point.y, start.headingDeg, controlRadiusMeters, this.getStartColor(scene), scene)
  219. }
  220. if (!scene.revealFullCourse) {
  221. return
  222. }
  223. for (const control of course.controls) {
  224. if (scene.activeControlSequences.includes(control.sequence)) {
  225. this.pushActiveControlPulse(positions, colors, control.point.x, control.point.y, controlRadiusMeters, scene, pulseFrame)
  226. }
  227. this.pushRing(
  228. positions,
  229. colors,
  230. control.point.x,
  231. control.point.y,
  232. this.getMetric(scene, controlRadiusMeters),
  233. this.getMetric(scene, controlRadiusMeters * (1 - CONTROL_RING_WIDTH_RATIO)),
  234. this.getControlColor(scene, control.sequence),
  235. scene,
  236. )
  237. }
  238. for (const finish of course.finishes) {
  239. if (scene.activeFinish) {
  240. this.pushActiveControlPulse(positions, colors, finish.point.x, finish.point.y, controlRadiusMeters, scene, pulseFrame)
  241. }
  242. const finishColor = this.getFinishColor(scene)
  243. this.pushRing(
  244. positions,
  245. colors,
  246. finish.point.x,
  247. finish.point.y,
  248. this.getMetric(scene, controlRadiusMeters),
  249. this.getMetric(scene, controlRadiusMeters * (1 - FINISH_RING_WIDTH_RATIO)),
  250. finishColor,
  251. scene,
  252. )
  253. this.pushRing(
  254. positions,
  255. colors,
  256. finish.point.x,
  257. finish.point.y,
  258. this.getMetric(scene, controlRadiusMeters * FINISH_INNER_RADIUS_RATIO),
  259. this.getMetric(scene, controlRadiusMeters * FINISH_INNER_RADIUS_RATIO * (1 - FINISH_RING_WIDTH_RATIO / FINISH_INNER_RADIUS_RATIO)),
  260. finishColor,
  261. scene,
  262. )
  263. }
  264. }
  265. getGuideLeg(course: ProjectedCourseLayers, scene: MapScene): ProjectedCourseLeg | null {
  266. const activeIndex = scene.activeLegIndices.length ? scene.activeLegIndices[0] : -1
  267. if (activeIndex >= 0 && activeIndex < course.legs.length) {
  268. return course.legs[activeIndex]
  269. }
  270. return null
  271. }
  272. getLegColor(scene: MapScene, index: number): RgbaColor {
  273. return this.isCompletedLeg(scene, index) ? COMPLETED_ROUTE_COLOR : COURSE_COLOR
  274. }
  275. isCompletedLeg(scene: MapScene, index: number): boolean {
  276. return scene.completedLegIndices.includes(index)
  277. }
  278. pushCourseLeg(
  279. positions: number[],
  280. colors: number[],
  281. leg: ProjectedCourseLeg,
  282. controlRadiusMeters: number,
  283. color: RgbaColor,
  284. scene: MapScene,
  285. ): void {
  286. const trimmed = this.getTrimmedCourseLeg(leg, controlRadiusMeters, scene)
  287. if (!trimmed) {
  288. return
  289. }
  290. this.pushSegment(positions, colors, trimmed.from, trimmed.to, this.getMetric(scene, controlRadiusMeters * LEG_WIDTH_RATIO), color, scene)
  291. }
  292. pushCourseLegHighlight(
  293. positions: number[],
  294. colors: number[],
  295. leg: ProjectedCourseLeg,
  296. controlRadiusMeters: number,
  297. scene: MapScene,
  298. ): void {
  299. const trimmed = this.getTrimmedCourseLeg(leg, controlRadiusMeters, scene)
  300. if (!trimmed) {
  301. return
  302. }
  303. this.pushSegment(
  304. positions,
  305. colors,
  306. trimmed.from,
  307. trimmed.to,
  308. this.getMetric(scene, controlRadiusMeters * LEG_WIDTH_RATIO * 1.5),
  309. ACTIVE_LEG_COLOR,
  310. scene,
  311. )
  312. }
  313. pushActiveControlPulse(
  314. positions: number[],
  315. colors: number[],
  316. centerX: number,
  317. centerY: number,
  318. controlRadiusMeters: number,
  319. scene: MapScene,
  320. pulseFrame: number,
  321. ): void {
  322. const pulse = (Math.sin(pulseFrame * ACTIVE_CONTROL_PULSE_SPEED) + 1) / 2
  323. const pulseScale = ACTIVE_CONTROL_PULSE_MIN_SCALE + (ACTIVE_CONTROL_PULSE_MAX_SCALE - ACTIVE_CONTROL_PULSE_MIN_SCALE) * pulse
  324. const pulseWidthScale = pulseScale - ACTIVE_CONTROL_PULSE_WIDTH_RATIO
  325. const glowAlpha = 0.24 + pulse * 0.34
  326. const glowColor: RgbaColor = [0.36, 1, 0.96, glowAlpha]
  327. this.pushRing(
  328. positions,
  329. colors,
  330. centerX,
  331. centerY,
  332. this.getMetric(scene, controlRadiusMeters * pulseScale),
  333. this.getMetric(scene, controlRadiusMeters * Math.max(1, pulseWidthScale)),
  334. glowColor,
  335. scene,
  336. )
  337. }
  338. pushActiveStartPulse(
  339. positions: number[],
  340. colors: number[],
  341. centerX: number,
  342. centerY: number,
  343. headingDeg: number | null,
  344. controlRadiusMeters: number,
  345. scene: MapScene,
  346. pulseFrame: number,
  347. ): void {
  348. const pulse = (Math.sin(pulseFrame * ACTIVE_CONTROL_PULSE_SPEED) + 1) / 2
  349. const pulseScale = ACTIVE_CONTROL_PULSE_MIN_SCALE + (ACTIVE_CONTROL_PULSE_MAX_SCALE - ACTIVE_CONTROL_PULSE_MIN_SCALE) * pulse
  350. const pulseWidthScale = pulseScale - ACTIVE_CONTROL_PULSE_WIDTH_RATIO
  351. const glowAlpha = 0.24 + pulse * 0.34
  352. const glowColor: RgbaColor = [0.36, 1, 0.96, glowAlpha]
  353. const headingRad = ((headingDeg === null ? 0 : headingDeg) - 90) * Math.PI / 180
  354. const ringCenterX = centerX + Math.cos(headingRad) * this.getMetric(scene, controlRadiusMeters * 0.04)
  355. const ringCenterY = centerY + Math.sin(headingRad) * this.getMetric(scene, controlRadiusMeters * 0.04)
  356. this.pushRing(
  357. positions,
  358. colors,
  359. ringCenterX,
  360. ringCenterY,
  361. this.getMetric(scene, controlRadiusMeters * pulseScale),
  362. this.getMetric(scene, controlRadiusMeters * Math.max(1, pulseWidthScale)),
  363. glowColor,
  364. scene,
  365. )
  366. }
  367. getStartColor(scene: MapScene): RgbaColor {
  368. if (scene.activeStart) {
  369. return ACTIVE_CONTROL_COLOR
  370. }
  371. if (scene.completedStart) {
  372. return COMPLETED_ROUTE_COLOR
  373. }
  374. return COURSE_COLOR
  375. }
  376. getControlColor(scene: MapScene, sequence: number): RgbaColor {
  377. if (scene.activeControlSequences.includes(sequence)) {
  378. return ACTIVE_CONTROL_COLOR
  379. }
  380. if (scene.completedControlSequences.includes(sequence)) {
  381. return COMPLETED_ROUTE_COLOR
  382. }
  383. return COURSE_COLOR
  384. }
  385. getFinishColor(scene: MapScene): RgbaColor {
  386. if (scene.activeFinish) {
  387. return ACTIVE_CONTROL_COLOR
  388. }
  389. if (scene.completedFinish) {
  390. return COMPLETED_ROUTE_COLOR
  391. }
  392. return COURSE_COLOR
  393. }
  394. pushGuidanceFlow(
  395. positions: number[],
  396. colors: number[],
  397. leg: ProjectedCourseLeg,
  398. controlRadiusMeters: number,
  399. scene: MapScene,
  400. pulseFrame: number,
  401. ): void {
  402. const trimmed = this.getTrimmedCourseLeg(leg, controlRadiusMeters, scene)
  403. if (!trimmed) {
  404. return
  405. }
  406. const dx = trimmed.to.x - trimmed.from.x
  407. const dy = trimmed.to.y - trimmed.from.y
  408. const length = Math.sqrt(dx * dx + dy * dy)
  409. if (!length) {
  410. return
  411. }
  412. for (let index = 0; index < GUIDE_FLOW_COUNT; index += 1) {
  413. const progress = (pulseFrame * GUIDE_FLOW_SPEED + index / GUIDE_FLOW_COUNT) % 1
  414. const tailProgress = Math.max(0, progress - GUIDE_FLOW_TRAIL)
  415. const head = {
  416. x: trimmed.from.x + dx * progress,
  417. y: trimmed.from.y + dy * progress,
  418. }
  419. const tail = {
  420. x: trimmed.from.x + dx * tailProgress,
  421. y: trimmed.from.y + dy * tailProgress,
  422. }
  423. const eased = progress * progress
  424. const width = this.getMetric(
  425. scene,
  426. controlRadiusMeters * (GUIDE_FLOW_MIN_WIDTH_RATIO + (GUIDE_FLOW_MAX_WIDTH_RATIO - GUIDE_FLOW_MIN_WIDTH_RATIO) * eased),
  427. )
  428. const outerColor = this.getGuideFlowOuterColor(eased)
  429. const innerColor = this.getGuideFlowInnerColor(eased)
  430. const headRadius = this.getMetric(scene, controlRadiusMeters * GUIDE_FLOW_HEAD_RADIUS_RATIO * (0.72 + eased * 0.42))
  431. this.pushSegment(positions, colors, tail, head, width * 1.9, outerColor, scene)
  432. this.pushSegment(positions, colors, tail, head, width, innerColor, scene)
  433. this.pushCircle(positions, colors, head.x, head.y, headRadius * 1.35, outerColor, scene)
  434. this.pushCircle(positions, colors, head.x, head.y, headRadius, innerColor, scene)
  435. }
  436. }
  437. getTrimmedCourseLeg(
  438. leg: ProjectedCourseLeg,
  439. controlRadiusMeters: number,
  440. scene: MapScene,
  441. ): { from: { x: number; y: number }; to: { x: number; y: number } } | null {
  442. return this.trimSegment(
  443. leg.from,
  444. leg.to,
  445. this.getLegTrim(leg.fromKind, controlRadiusMeters, scene),
  446. this.getLegTrim(leg.toKind, controlRadiusMeters, scene),
  447. )
  448. }
  449. getGuideFlowOuterColor(progress: number): RgbaColor {
  450. return [0.28, 0.92, 1, 0.14 + progress * 0.22]
  451. }
  452. getGuideFlowInnerColor(progress: number): RgbaColor {
  453. return [0.94, 0.99, 1, 0.38 + progress * 0.42]
  454. }
  455. getLegTrim(kind: ProjectedCourseLeg['fromKind'], controlRadiusMeters: number, scene: MapScene): number {
  456. if (kind === 'start') {
  457. return this.getMetric(scene, controlRadiusMeters * (1 - START_RING_WIDTH_RATIO / 2))
  458. }
  459. if (kind === 'finish') {
  460. return this.getMetric(scene, controlRadiusMeters * (1 - FINISH_RING_WIDTH_RATIO / 2))
  461. }
  462. return this.getMetric(scene, controlRadiusMeters * LEG_TRIM_TO_RING_CENTER_RATIO)
  463. }
  464. trimSegment(
  465. from: { x: number; y: number },
  466. to: { x: number; y: number },
  467. fromTrim: number,
  468. toTrim: number,
  469. ): { from: { x: number; y: number }; to: { x: number; y: number } } | null {
  470. const dx = to.x - from.x
  471. const dy = to.y - from.y
  472. const length = Math.sqrt(dx * dx + dy * dy)
  473. if (!length || length <= fromTrim + toTrim) {
  474. return null
  475. }
  476. const ux = dx / length
  477. const uy = dy / length
  478. return {
  479. from: {
  480. x: from.x + ux * fromTrim,
  481. y: from.y + uy * fromTrim,
  482. },
  483. to: {
  484. x: to.x - ux * toTrim,
  485. y: to.y - uy * toTrim,
  486. },
  487. }
  488. }
  489. pushStartTriangle(
  490. positions: number[],
  491. colors: number[],
  492. centerX: number,
  493. centerY: number,
  494. headingDeg: number | null,
  495. controlRadiusMeters: number,
  496. color: RgbaColor,
  497. scene: MapScene,
  498. ): void {
  499. const startRadius = this.getMetric(scene, controlRadiusMeters)
  500. const startRingWidth = this.getMetric(scene, controlRadiusMeters * START_RING_WIDTH_RATIO)
  501. const headingRad = ((headingDeg === null ? 0 : headingDeg) - 90) * Math.PI / 180
  502. const vertices = [0, 1, 2].map((index) => {
  503. const angle = headingRad + index * (Math.PI * 2 / 3)
  504. return {
  505. x: centerX + Math.cos(angle) * startRadius,
  506. y: centerY + Math.sin(angle) * startRadius,
  507. }
  508. })
  509. this.pushSegment(positions, colors, vertices[0], vertices[1], startRingWidth, color, scene)
  510. this.pushSegment(positions, colors, vertices[1], vertices[2], startRingWidth, color, scene)
  511. this.pushSegment(positions, colors, vertices[2], vertices[0], startRingWidth, color, scene)
  512. }
  513. pushRing(
  514. positions: number[],
  515. colors: number[],
  516. centerX: number,
  517. centerY: number,
  518. outerRadius: number,
  519. innerRadius: number,
  520. color: RgbaColor,
  521. scene: MapScene,
  522. ): void {
  523. const segments = 36
  524. for (let index = 0; index < segments; index += 1) {
  525. const startAngle = index / segments * Math.PI * 2
  526. const endAngle = (index + 1) / segments * Math.PI * 2
  527. const outerStart = this.toClip(centerX + Math.cos(startAngle) * outerRadius, centerY + Math.sin(startAngle) * outerRadius, scene)
  528. const outerEnd = this.toClip(centerX + Math.cos(endAngle) * outerRadius, centerY + Math.sin(endAngle) * outerRadius, scene)
  529. const innerStart = this.toClip(centerX + Math.cos(startAngle) * innerRadius, centerY + Math.sin(startAngle) * innerRadius, scene)
  530. const innerEnd = this.toClip(centerX + Math.cos(endAngle) * innerRadius, centerY + Math.sin(endAngle) * innerRadius, scene)
  531. this.pushTriangle(positions, colors, outerStart, outerEnd, innerStart, color)
  532. this.pushTriangle(positions, colors, innerStart, outerEnd, innerEnd, color)
  533. }
  534. }
  535. pushSegment(
  536. positions: number[],
  537. colors: number[],
  538. start: { x: number; y: number },
  539. end: { x: number; y: number },
  540. width: number,
  541. color: RgbaColor,
  542. scene: MapScene,
  543. ): void {
  544. const deltaX = end.x - start.x
  545. const deltaY = end.y - start.y
  546. const length = Math.sqrt(deltaX * deltaX + deltaY * deltaY)
  547. if (!length) {
  548. return
  549. }
  550. const normalX = -deltaY / length * (width / 2)
  551. const normalY = deltaX / length * (width / 2)
  552. const topLeft = this.toClip(start.x + normalX, start.y + normalY, scene)
  553. const topRight = this.toClip(end.x + normalX, end.y + normalY, scene)
  554. const bottomLeft = this.toClip(start.x - normalX, start.y - normalY, scene)
  555. const bottomRight = this.toClip(end.x - normalX, end.y - normalY, scene)
  556. this.pushTriangle(positions, colors, topLeft, topRight, bottomLeft, color)
  557. this.pushTriangle(positions, colors, bottomLeft, topRight, bottomRight, color)
  558. }
  559. pushCircle(
  560. positions: number[],
  561. colors: number[],
  562. centerX: number,
  563. centerY: number,
  564. radius: number,
  565. color: RgbaColor,
  566. scene: MapScene,
  567. ): void {
  568. const segments = 20
  569. const center = this.toClip(centerX, centerY, scene)
  570. for (let index = 0; index < segments; index += 1) {
  571. const startAngle = index / segments * Math.PI * 2
  572. const endAngle = (index + 1) / segments * Math.PI * 2
  573. const start = this.toClip(centerX + Math.cos(startAngle) * radius, centerY + Math.sin(startAngle) * radius, scene)
  574. const end = this.toClip(centerX + Math.cos(endAngle) * radius, centerY + Math.sin(endAngle) * radius, scene)
  575. this.pushTriangle(positions, colors, center, start, end, color)
  576. }
  577. }
  578. pushTriangle(
  579. positions: number[],
  580. colors: number[],
  581. first: { x: number; y: number },
  582. second: { x: number; y: number },
  583. third: { x: number; y: number },
  584. color: RgbaColor,
  585. ): void {
  586. positions.push(first.x, first.y, second.x, second.y, third.x, third.y)
  587. for (let index = 0; index < 3; index += 1) {
  588. colors.push(color[0], color[1], color[2], color[3])
  589. }
  590. }
  591. toClip(x: number, y: number, scene: MapScene): { x: number; y: number } {
  592. const previewScale = scene.previewScale || 1
  593. const originX = scene.previewOriginX || scene.viewportWidth / 2
  594. const originY = scene.previewOriginY || scene.viewportHeight / 2
  595. const scaledX = originX + (x - originX) * previewScale
  596. const scaledY = originY + (y - originY) * previewScale
  597. return {
  598. x: scaledX / scene.viewportWidth * 2 - 1,
  599. y: 1 - scaledY / scene.viewportHeight * 2,
  600. }
  601. }
  602. }