import { buildCamera, type MapScene } from './mapRenderer' import { TrackLayer } from '../layer/trackLayer' import { GpsLayer } from '../layer/gpsLayer' 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 trackLayer: TrackLayer gpsLayer: GpsLayer program: any positionBuffer: any colorBuffer: any positionLocation: number colorLocation: number constructor(trackLayer: TrackLayer, gpsLayer: GpsLayer) { this.canvas = null this.gl = null this.dpr = 1 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 camera = buildCamera(scene) const trackPoints = this.trackLayer.projectPoints(scene, camera) const gpsPoint = this.gpsLayer.projectPoint(scene, camera) const positions: number[] = [] const colors: number[] = [] 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) } 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) 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) } pushSegment( positions: number[], colors: number[], start: { x: number; y: number }, end: { x: number; y: number }, width: number, color: [number, number, number, number], 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: [number, number, number, number], 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: [number, number, number, number], ): 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, } } }