webglVectorRenderer.ts 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517
  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 EARTH_CIRCUMFERENCE_METERS = 40075016.686
  9. const CONTROL_RING_WIDTH_RATIO = 0.2
  10. const FINISH_INNER_RADIUS_RATIO = 0.6
  11. const FINISH_RING_WIDTH_RATIO = 0.2
  12. const START_RING_WIDTH_RATIO = 0.2
  13. const LEG_WIDTH_RATIO = 0.2
  14. const LEG_TRIM_TO_RING_CENTER_RATIO = 1 - CONTROL_RING_WIDTH_RATIO / 2
  15. type RgbaColor = [number, number, number, number]
  16. const GUIDE_FLOW_COUNT = 6
  17. const GUIDE_FLOW_SPEED = 0.022
  18. const GUIDE_FLOW_MIN_RADIUS_RATIO = 0.14
  19. const GUIDE_FLOW_MAX_RADIUS_RATIO = 0.34
  20. const GUIDE_FLOW_OUTER_SCALE = 1.45
  21. const GUIDE_FLOW_INNER_SCALE = 0.56
  22. function createShader(gl: any, type: number, source: string): any {
  23. const shader = gl.createShader(type)
  24. if (!shader) {
  25. throw new Error('WebGL shader 创建失败')
  26. }
  27. gl.shaderSource(shader, source)
  28. gl.compileShader(shader)
  29. if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
  30. const message = gl.getShaderInfoLog(shader) || 'unknown shader error'
  31. gl.deleteShader(shader)
  32. throw new Error(message)
  33. }
  34. return shader
  35. }
  36. function createProgram(gl: any, vertexSource: string, fragmentSource: string): any {
  37. const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexSource)
  38. const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentSource)
  39. const program = gl.createProgram()
  40. if (!program) {
  41. throw new Error('WebGL program 创建失败')
  42. }
  43. gl.attachShader(program, vertexShader)
  44. gl.attachShader(program, fragmentShader)
  45. gl.linkProgram(program)
  46. gl.deleteShader(vertexShader)
  47. gl.deleteShader(fragmentShader)
  48. if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
  49. const message = gl.getProgramInfoLog(program) || 'unknown program error'
  50. gl.deleteProgram(program)
  51. throw new Error(message)
  52. }
  53. return program
  54. }
  55. export class WebGLVectorRenderer {
  56. canvas: any
  57. gl: any
  58. dpr: number
  59. courseLayer: CourseLayer
  60. trackLayer: TrackLayer
  61. gpsLayer: GpsLayer
  62. program: any
  63. positionBuffer: any
  64. colorBuffer: any
  65. positionLocation: number
  66. colorLocation: number
  67. constructor(courseLayer: CourseLayer, trackLayer: TrackLayer, gpsLayer: GpsLayer) {
  68. this.canvas = null
  69. this.gl = null
  70. this.dpr = 1
  71. this.courseLayer = courseLayer
  72. this.trackLayer = trackLayer
  73. this.gpsLayer = gpsLayer
  74. this.program = null
  75. this.positionBuffer = null
  76. this.colorBuffer = null
  77. this.positionLocation = -1
  78. this.colorLocation = -1
  79. }
  80. attachCanvas(canvasNode: any, width: number, height: number, dpr: number): void {
  81. this.canvas = canvasNode
  82. this.dpr = dpr || 1
  83. canvasNode.width = Math.max(1, Math.floor(width * this.dpr))
  84. canvasNode.height = Math.max(1, Math.floor(height * this.dpr))
  85. this.attachContext(canvasNode.getContext('webgl') || canvasNode.getContext('experimental-webgl'), canvasNode)
  86. }
  87. attachContext(gl: any, canvasNode: any): void {
  88. if (!gl) {
  89. throw new Error('当前环境不支持 WebGL Vector Layer')
  90. }
  91. this.canvas = canvasNode
  92. this.gl = gl
  93. this.program = createProgram(
  94. gl,
  95. '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; }',
  96. 'precision mediump float; varying vec4 v_color; void main() { gl_FragColor = v_color; }',
  97. )
  98. this.positionBuffer = gl.createBuffer()
  99. this.colorBuffer = gl.createBuffer()
  100. this.positionLocation = gl.getAttribLocation(this.program, 'a_position')
  101. this.colorLocation = gl.getAttribLocation(this.program, 'a_color')
  102. gl.viewport(0, 0, canvasNode.width, canvasNode.height)
  103. gl.enable(gl.BLEND)
  104. gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
  105. }
  106. destroy(): void {
  107. if (this.gl) {
  108. if (this.program) {
  109. this.gl.deleteProgram(this.program)
  110. }
  111. if (this.positionBuffer) {
  112. this.gl.deleteBuffer(this.positionBuffer)
  113. }
  114. if (this.colorBuffer) {
  115. this.gl.deleteBuffer(this.colorBuffer)
  116. }
  117. }
  118. this.program = null
  119. this.positionBuffer = null
  120. this.colorBuffer = null
  121. this.gl = null
  122. this.canvas = null
  123. }
  124. render(scene: MapScene, pulseFrame: number): void {
  125. if (!this.gl || !this.program || !this.positionBuffer || !this.colorBuffer || !this.canvas) {
  126. return
  127. }
  128. const gl = this.gl
  129. const course = this.courseLayer.projectCourse(scene)
  130. const trackPoints = this.trackLayer.projectPoints(scene)
  131. const gpsPoint = this.gpsLayer.projectPoint(scene)
  132. const positions: number[] = []
  133. const colors: number[] = []
  134. if (course) {
  135. this.pushCourse(positions, colors, course, scene, pulseFrame)
  136. }
  137. for (let index = 1; index < trackPoints.length; index += 1) {
  138. this.pushSegment(positions, colors, trackPoints[index - 1], trackPoints[index], 6, [0.09, 0.43, 0.36, 0.96], scene)
  139. }
  140. for (const point of trackPoints) {
  141. this.pushCircle(positions, colors, point.x, point.y, 10, [0.09, 0.43, 0.36, 1], scene)
  142. this.pushCircle(positions, colors, point.x, point.y, 6.5, [0.97, 0.98, 0.95, 1], scene)
  143. }
  144. if (gpsPoint) {
  145. this.pushCircle(positions, colors, gpsPoint.x, gpsPoint.y, this.gpsLayer.getPulseRadius(pulseFrame), [0.13, 0.62, 0.74, 0.22], scene)
  146. this.pushCircle(positions, colors, gpsPoint.x, gpsPoint.y, 13, [1, 1, 1, 0.95], scene)
  147. this.pushCircle(positions, colors, gpsPoint.x, gpsPoint.y, 9, [0.13, 0.63, 0.74, 1], scene)
  148. }
  149. if (!positions.length) {
  150. return
  151. }
  152. gl.viewport(0, 0, this.canvas.width, this.canvas.height)
  153. gl.useProgram(this.program)
  154. gl.bindBuffer(gl.ARRAY_BUFFER, this.positionBuffer)
  155. gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STREAM_DRAW)
  156. gl.enableVertexAttribArray(this.positionLocation)
  157. gl.vertexAttribPointer(this.positionLocation, 2, gl.FLOAT, false, 0, 0)
  158. gl.bindBuffer(gl.ARRAY_BUFFER, this.colorBuffer)
  159. gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STREAM_DRAW)
  160. gl.enableVertexAttribArray(this.colorLocation)
  161. gl.vertexAttribPointer(this.colorLocation, 4, gl.FLOAT, false, 0, 0)
  162. gl.drawArrays(gl.TRIANGLES, 0, positions.length / 2)
  163. }
  164. getPixelsPerMeter(scene: MapScene): number {
  165. const camera: CameraState = {
  166. centerWorldX: scene.exactCenterWorldX,
  167. centerWorldY: scene.exactCenterWorldY,
  168. viewportWidth: scene.viewportWidth,
  169. viewportHeight: scene.viewportHeight,
  170. visibleColumns: scene.visibleColumns,
  171. }
  172. const tileSizePx = getTileSizePx(camera)
  173. const centerLonLat = worldTileToLonLat({ x: scene.exactCenterWorldX, y: scene.exactCenterWorldY }, scene.zoom)
  174. const metersPerTile = Math.cos(centerLonLat.lat * Math.PI / 180) * EARTH_CIRCUMFERENCE_METERS / Math.pow(2, scene.zoom)
  175. if (!tileSizePx || !metersPerTile) {
  176. return 0
  177. }
  178. return tileSizePx / metersPerTile
  179. }
  180. getMetric(scene: MapScene, meters: number): number {
  181. return meters * this.getPixelsPerMeter(scene)
  182. }
  183. getControlRadiusMeters(scene: MapScene): number {
  184. return scene.cpRadiusMeters > 0 ? scene.cpRadiusMeters : 5
  185. }
  186. pushCourse(
  187. positions: number[],
  188. colors: number[],
  189. course: ProjectedCourseLayers,
  190. scene: MapScene,
  191. pulseFrame: number,
  192. ): void {
  193. const controlRadiusMeters = this.getControlRadiusMeters(scene)
  194. for (const leg of course.legs) {
  195. this.pushCourseLeg(positions, colors, leg, controlRadiusMeters, scene)
  196. }
  197. const guideLeg = this.getGuideLeg(course)
  198. if (guideLeg) {
  199. this.pushGuidanceFlow(positions, colors, guideLeg, controlRadiusMeters, scene, pulseFrame)
  200. }
  201. for (const start of course.starts) {
  202. this.pushStartTriangle(positions, colors, start.point.x, start.point.y, start.headingDeg, controlRadiusMeters, scene)
  203. }
  204. for (const control of course.controls) {
  205. this.pushRing(
  206. positions,
  207. colors,
  208. control.point.x,
  209. control.point.y,
  210. this.getMetric(scene, controlRadiusMeters),
  211. this.getMetric(scene, controlRadiusMeters * (1 - CONTROL_RING_WIDTH_RATIO)),
  212. COURSE_COLOR,
  213. scene,
  214. )
  215. }
  216. for (const finish of course.finishes) {
  217. this.pushRing(
  218. positions,
  219. colors,
  220. finish.point.x,
  221. finish.point.y,
  222. this.getMetric(scene, controlRadiusMeters),
  223. this.getMetric(scene, controlRadiusMeters * (1 - FINISH_RING_WIDTH_RATIO)),
  224. COURSE_COLOR,
  225. scene,
  226. )
  227. this.pushRing(
  228. positions,
  229. colors,
  230. finish.point.x,
  231. finish.point.y,
  232. this.getMetric(scene, controlRadiusMeters * FINISH_INNER_RADIUS_RATIO),
  233. this.getMetric(scene, controlRadiusMeters * FINISH_INNER_RADIUS_RATIO * (1 - FINISH_RING_WIDTH_RATIO / FINISH_INNER_RADIUS_RATIO)),
  234. COURSE_COLOR,
  235. scene,
  236. )
  237. }
  238. }
  239. getGuideLeg(course: ProjectedCourseLayers): ProjectedCourseLeg | null {
  240. return course.legs.length ? course.legs[0] : null
  241. }
  242. pushCourseLeg(
  243. positions: number[],
  244. colors: number[],
  245. leg: ProjectedCourseLeg,
  246. controlRadiusMeters: number,
  247. scene: MapScene,
  248. ): void {
  249. const trimmed = this.getTrimmedCourseLeg(leg, controlRadiusMeters, scene)
  250. if (!trimmed) {
  251. return
  252. }
  253. this.pushSegment(positions, colors, trimmed.from, trimmed.to, this.getMetric(scene, controlRadiusMeters * LEG_WIDTH_RATIO), COURSE_COLOR, scene)
  254. }
  255. pushGuidanceFlow(
  256. positions: number[],
  257. colors: number[],
  258. leg: ProjectedCourseLeg,
  259. controlRadiusMeters: number,
  260. scene: MapScene,
  261. pulseFrame: number,
  262. ): void {
  263. const trimmed = this.getTrimmedCourseLeg(leg, controlRadiusMeters, scene)
  264. if (!trimmed) {
  265. return
  266. }
  267. const dx = trimmed.to.x - trimmed.from.x
  268. const dy = trimmed.to.y - trimmed.from.y
  269. const length = Math.sqrt(dx * dx + dy * dy)
  270. if (!length) {
  271. return
  272. }
  273. for (let index = 0; index < GUIDE_FLOW_COUNT; index += 1) {
  274. const progress = (pulseFrame * GUIDE_FLOW_SPEED + index / GUIDE_FLOW_COUNT) % 1
  275. const eased = progress * progress
  276. const x = trimmed.from.x + dx * progress
  277. const y = trimmed.from.y + dy * progress
  278. const radius = this.getMetric(
  279. scene,
  280. controlRadiusMeters * (GUIDE_FLOW_MIN_RADIUS_RATIO + (GUIDE_FLOW_MAX_RADIUS_RATIO - GUIDE_FLOW_MIN_RADIUS_RATIO) * eased),
  281. )
  282. const outerColor = this.getGuideFlowOuterColor(eased)
  283. const innerColor = this.getGuideFlowInnerColor(eased)
  284. this.pushCircle(positions, colors, x, y, radius * GUIDE_FLOW_OUTER_SCALE, outerColor, scene)
  285. this.pushCircle(positions, colors, x, y, radius * GUIDE_FLOW_INNER_SCALE, innerColor, scene)
  286. }
  287. }
  288. getTrimmedCourseLeg(
  289. leg: ProjectedCourseLeg,
  290. controlRadiusMeters: number,
  291. scene: MapScene,
  292. ): { from: { x: number; y: number }; to: { x: number; y: number } } | null {
  293. return this.trimSegment(
  294. leg.from,
  295. leg.to,
  296. this.getLegTrim(leg.fromKind, controlRadiusMeters, scene),
  297. this.getLegTrim(leg.toKind, controlRadiusMeters, scene),
  298. )
  299. }
  300. getGuideFlowOuterColor(progress: number): RgbaColor {
  301. return [1, 0.18, 0.6, 0.16 + progress * 0.34]
  302. }
  303. getGuideFlowInnerColor(progress: number): RgbaColor {
  304. return [1, 0.95, 0.98, 0.3 + progress * 0.54]
  305. }
  306. getLegTrim(kind: ProjectedCourseLeg['fromKind'], controlRadiusMeters: number, scene: MapScene): number {
  307. if (kind === 'start') {
  308. return this.getMetric(scene, controlRadiusMeters * (1 - START_RING_WIDTH_RATIO / 2))
  309. }
  310. if (kind === 'finish') {
  311. return this.getMetric(scene, controlRadiusMeters * (1 - FINISH_RING_WIDTH_RATIO / 2))
  312. }
  313. return this.getMetric(scene, controlRadiusMeters * LEG_TRIM_TO_RING_CENTER_RATIO)
  314. }
  315. trimSegment(
  316. from: { x: number; y: number },
  317. to: { x: number; y: number },
  318. fromTrim: number,
  319. toTrim: number,
  320. ): { from: { x: number; y: number }; to: { x: number; y: number } } | null {
  321. const dx = to.x - from.x
  322. const dy = to.y - from.y
  323. const length = Math.sqrt(dx * dx + dy * dy)
  324. if (!length || length <= fromTrim + toTrim) {
  325. return null
  326. }
  327. const ux = dx / length
  328. const uy = dy / length
  329. return {
  330. from: {
  331. x: from.x + ux * fromTrim,
  332. y: from.y + uy * fromTrim,
  333. },
  334. to: {
  335. x: to.x - ux * toTrim,
  336. y: to.y - uy * toTrim,
  337. },
  338. }
  339. }
  340. pushStartTriangle(
  341. positions: number[],
  342. colors: number[],
  343. centerX: number,
  344. centerY: number,
  345. headingDeg: number | null,
  346. controlRadiusMeters: number,
  347. scene: MapScene,
  348. ): void {
  349. const startRadius = this.getMetric(scene, controlRadiusMeters)
  350. const startRingWidth = this.getMetric(scene, controlRadiusMeters * START_RING_WIDTH_RATIO)
  351. const headingRad = ((headingDeg === null ? 0 : headingDeg) - 90) * Math.PI / 180
  352. const vertices = [0, 1, 2].map((index) => {
  353. const angle = headingRad + index * (Math.PI * 2 / 3)
  354. return {
  355. x: centerX + Math.cos(angle) * startRadius,
  356. y: centerY + Math.sin(angle) * startRadius,
  357. }
  358. })
  359. this.pushSegment(positions, colors, vertices[0], vertices[1], startRingWidth, COURSE_COLOR, scene)
  360. this.pushSegment(positions, colors, vertices[1], vertices[2], startRingWidth, COURSE_COLOR, scene)
  361. this.pushSegment(positions, colors, vertices[2], vertices[0], startRingWidth, COURSE_COLOR, scene)
  362. }
  363. pushRing(
  364. positions: number[],
  365. colors: number[],
  366. centerX: number,
  367. centerY: number,
  368. outerRadius: number,
  369. innerRadius: number,
  370. color: RgbaColor,
  371. scene: MapScene,
  372. ): void {
  373. const segments = 36
  374. for (let index = 0; index < segments; index += 1) {
  375. const startAngle = index / segments * Math.PI * 2
  376. const endAngle = (index + 1) / segments * Math.PI * 2
  377. const outerStart = this.toClip(centerX + Math.cos(startAngle) * outerRadius, centerY + Math.sin(startAngle) * outerRadius, scene)
  378. const outerEnd = this.toClip(centerX + Math.cos(endAngle) * outerRadius, centerY + Math.sin(endAngle) * outerRadius, scene)
  379. const innerStart = this.toClip(centerX + Math.cos(startAngle) * innerRadius, centerY + Math.sin(startAngle) * innerRadius, scene)
  380. const innerEnd = this.toClip(centerX + Math.cos(endAngle) * innerRadius, centerY + Math.sin(endAngle) * innerRadius, scene)
  381. this.pushTriangle(positions, colors, outerStart, outerEnd, innerStart, color)
  382. this.pushTriangle(positions, colors, innerStart, outerEnd, innerEnd, color)
  383. }
  384. }
  385. pushSegment(
  386. positions: number[],
  387. colors: number[],
  388. start: { x: number; y: number },
  389. end: { x: number; y: number },
  390. width: number,
  391. color: RgbaColor,
  392. scene: MapScene,
  393. ): void {
  394. const deltaX = end.x - start.x
  395. const deltaY = end.y - start.y
  396. const length = Math.sqrt(deltaX * deltaX + deltaY * deltaY)
  397. if (!length) {
  398. return
  399. }
  400. const normalX = -deltaY / length * (width / 2)
  401. const normalY = deltaX / length * (width / 2)
  402. const topLeft = this.toClip(start.x + normalX, start.y + normalY, scene)
  403. const topRight = this.toClip(end.x + normalX, end.y + normalY, scene)
  404. const bottomLeft = this.toClip(start.x - normalX, start.y - normalY, scene)
  405. const bottomRight = this.toClip(end.x - normalX, end.y - normalY, scene)
  406. this.pushTriangle(positions, colors, topLeft, topRight, bottomLeft, color)
  407. this.pushTriangle(positions, colors, bottomLeft, topRight, bottomRight, color)
  408. }
  409. pushCircle(
  410. positions: number[],
  411. colors: number[],
  412. centerX: number,
  413. centerY: number,
  414. radius: number,
  415. color: RgbaColor,
  416. scene: MapScene,
  417. ): void {
  418. const segments = 20
  419. const center = this.toClip(centerX, centerY, scene)
  420. for (let index = 0; index < segments; index += 1) {
  421. const startAngle = index / segments * Math.PI * 2
  422. const endAngle = (index + 1) / segments * Math.PI * 2
  423. const start = this.toClip(centerX + Math.cos(startAngle) * radius, centerY + Math.sin(startAngle) * radius, scene)
  424. const end = this.toClip(centerX + Math.cos(endAngle) * radius, centerY + Math.sin(endAngle) * radius, scene)
  425. this.pushTriangle(positions, colors, center, start, end, color)
  426. }
  427. }
  428. pushTriangle(
  429. positions: number[],
  430. colors: number[],
  431. first: { x: number; y: number },
  432. second: { x: number; y: number },
  433. third: { x: number; y: number },
  434. color: RgbaColor,
  435. ): void {
  436. positions.push(first.x, first.y, second.x, second.y, third.x, third.y)
  437. for (let index = 0; index < 3; index += 1) {
  438. colors.push(color[0], color[1], color[2], color[3])
  439. }
  440. }
  441. toClip(x: number, y: number, scene: MapScene): { x: number; y: number } {
  442. const previewScale = scene.previewScale || 1
  443. const originX = scene.previewOriginX || scene.viewportWidth / 2
  444. const originY = scene.previewOriginY || scene.viewportHeight / 2
  445. const scaledX = originX + (x - originX) * previewScale
  446. const scaledY = originY + (y - originY) * previewScale
  447. return {
  448. x: scaledX / scene.viewportWidth * 2 - 1,
  449. y: 1 - scaledY / scene.viewportHeight * 2,
  450. }
  451. }
  452. }