import { Angle, Color } from '../../numbers' import { AgentSet, CellGrid } from '../../structures' import { Agent, Cell } from '../../entities' import { AgentStyle } from '../../entities/Agent' import Render2D, { Render2DConstructor, ColorFunction, StyleFunction } from './Render2D' // --------------------------------------------------------------------------- // Module-level unit geometry constants (TRIANGLES format, xy pairs only) // --------------------------------------------------------------------------- const CIRCLE_SEGMENTS = 30 // 30 triangles × 3 vertices × 2 floats = 180 floats const CIRCLE_UNIT_VERTS = (() => { const data = new Float32Array(CIRCLE_SEGMENTS * 3 * 2) let offset = 0 for (let i = 0; i < CIRCLE_SEGMENTS; i++) { const a1 = (i / CIRCLE_SEGMENTS) * 2 * Math.PI const a2 = ((i + 1) / CIRCLE_SEGMENTS) * 2 * Math.PI // center data[offset++] = 0 data[offset++] = 0 // edge point i data[offset++] = Math.cos(a1) data[offset++] = Math.sin(a1) // edge point i+1 data[offset++] = Math.cos(a2) data[offset++] = Math.sin(a2) } return data })() // 30 edge points for LINE_LOOP stroke const CIRCLE_EDGE_VERTS = (() => { const data = new Float32Array(CIRCLE_SEGMENTS * 2) for (let i = 0; i < CIRCLE_SEGMENTS; i++) { const a = (i / CIRCLE_SEGMENTS) * 2 * Math.PI data[i * 2] = Math.cos(a) data[i * 2 + 1] = Math.sin(a) } return data })() // 2 triangles × 3 vertices × 2 floats = 12 floats // unit square: TL=(0,0), TR=(1,0), BR=(1,1), BL=(0,1) const SQUARE_UNIT_VERTS = new Float32Array([ 0, 0, 1, 0, 0, 1, // triangle 1 1, 0, 1, 1, 0, 1, // triangle 2 ]) // equilateral triangle, tip pointing up, centered at origin, unit width=1 const TRIANGLE_UNIT_H = Math.sqrt(3) / 2 + 0.5 const TRIANGLE_UNIT_VERTS = new Float32Array([ 0, -TRIANGLE_UNIT_H / 2, // top -0.5, TRIANGLE_UNIT_H / 2, // bottom-left 0.5, TRIANGLE_UNIT_H / 2, // bottom-right ]) // --------------------------------------------------------------------------- // Shaders // --------------------------------------------------------------------------- const VERT_SRC = `#version 300 es precision mediump float; in vec2 a_position; in vec4 a_color; uniform vec2 u_resolution; out vec4 v_color; void main() { vec2 clip = ((a_position / u_resolution) * 2.0 - 1.0) * vec2(1.0, -1.0); gl_Position = vec4(clip, 0.0, 1.0); v_color = a_color; }` const FRAG_SRC = `#version 300 es precision mediump float; in vec4 v_color; out vec4 outColor; void main() { outColor = v_color; }` // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- function compileShader(gl: WebGL2RenderingContext, type: number, source: string): WebGLShader { const shader = gl.createShader(type)! gl.shaderSource(shader, source) gl.compileShader(shader) if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { const log = gl.getShaderInfoLog(shader) gl.deleteShader(shader) throw new Error(`WebGL2D shader compile error: ${log}`) } return shader } function createProgram(gl: WebGL2RenderingContext): WebGLProgram { const vert = compileShader(gl, gl.VERTEX_SHADER, VERT_SRC) const frag = compileShader(gl, gl.FRAGMENT_SHADER, FRAG_SRC) const program = gl.createProgram()! gl.attachShader(program, vert) gl.attachShader(program, frag) gl.linkProgram(program) if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { throw new Error(`WebGL2D program link error: ${gl.getProgramInfoLog(program)}`) } gl.deleteShader(vert) gl.deleteShader(frag) return program } // Floats per vertex: x, y, r, g, b, a const STRIDE = 24 // 6 floats × 4 bytes function setupVAO( gl: WebGL2RenderingContext, vao: WebGLVertexArrayObject, vbo: WebGLBuffer, posLoc: number, colorLoc: number ) { gl.bindVertexArray(vao) gl.bindBuffer(gl.ARRAY_BUFFER, vbo) gl.vertexAttribPointer(posLoc, 2, gl.FLOAT, false, STRIDE, 0) gl.enableVertexAttribArray(posLoc) gl.vertexAttribPointer(colorLoc, 4, gl.FLOAT, false, STRIDE, 8) gl.enableVertexAttribArray(colorLoc) gl.bindVertexArray(null) } // --------------------------------------------------------------------------- // WebGL2D // --------------------------------------------------------------------------- export class WebGL2D { renderWidth: number renderHeight: number frameRate: number cellWidth: number cellHeight: number private gl: WebGL2RenderingContext private program: WebGLProgram private posLoc: number private colorLoc: number private resolutionLoc: WebGLUniformLocation private gridVAO: WebGLVertexArrayObject private gridVBO: WebGLBuffer private agentVAO: WebGLVertexArrayObject private agentVBO: WebGLBuffer private miscVAO: WebGLVertexArrayObject private miscVBO: WebGLBuffer constructor(opts: Render2DConstructor) { const { root, worldWidth, renderHeight, renderWidth, title, id, autoPlay, frameRate } = opts this.renderHeight = renderHeight this.renderWidth = renderWidth this.cellWidth = renderWidth / worldWidth this.cellHeight = renderHeight / worldWidth this.frameRate = frameRate // Create draggable canvas container const draggable = document.createElement('drag-pane') draggable.style.zIndex = '1' draggable.id = 'canvas_main' draggable.setAttribute('heading', title) draggable.setAttribute('key', id) const canvas = document.createElement('canvas') canvas.width = renderWidth canvas.height = renderHeight const gl = canvas.getContext('webgl2') if (!gl) { throw new Error('WebGL2 is not supported in this browser.') } this.gl = gl // Compile shaders and create program this.program = createProgram(gl) gl.useProgram(this.program) // Cache attribute and uniform locations this.posLoc = gl.getAttribLocation(this.program, 'a_position') this.colorLoc = gl.getAttribLocation(this.program, 'a_color') this.resolutionLoc = gl.getUniformLocation(this.program, 'u_resolution')! // Set resolution uniform once (it does not change) gl.uniform2f(this.resolutionLoc, renderWidth, renderHeight) // Enable alpha blending gl.enable(gl.BLEND) gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA) // Create VBOs this.gridVBO = gl.createBuffer()! this.agentVBO = gl.createBuffer()! this.miscVBO = gl.createBuffer()! // Create and configure VAOs this.gridVAO = gl.createVertexArray()! this.agentVAO = gl.createVertexArray()! this.miscVAO = gl.createVertexArray()! setupVAO(gl, this.gridVAO, this.gridVBO, this.posLoc, this.colorLoc) setupVAO(gl, this.agentVAO, this.agentVBO, this.posLoc, this.colorLoc) setupVAO(gl, this.miscVAO, this.miscVBO, this.posLoc, this.colorLoc) // Click event handler canvas.addEventListener('click', (e) => { const rect = canvas.getBoundingClientRect() const x = e.clientX - rect.left const y = e.clientY - rect.top const event = new CustomEvent('inspectClick', { detail: [Math.floor(x / this.cellWidth), Math.floor(y / this.cellHeight)] }) if (opts.onCellClick) { opts.onCellClick( [Math.floor(x / this.cellWidth), Math.floor(y / this.cellHeight)], this as unknown as Render2D ) } window.dispatchEvent(event) }) draggable.appendChild(canvas) const controlBar = document.createElement('animation-toolbar') controlBar.setAttribute('autoPlay', autoPlay.toString()) controlBar.setAttribute('fps', frameRate.toString()) draggable.appendChild(controlBar) root.appendChild(draggable) } // ------------------------------------------------------------------------- // Clear // ------------------------------------------------------------------------- clear() { const gl = this.gl gl.clearColor(0, 0, 0, 0) gl.clear(gl.COLOR_BUFFER_BIT) } // ------------------------------------------------------------------------- // Draw cell grid — 1 draw call for the entire grid // ------------------------------------------------------------------------- drawCellGrid( world: CellGrid, opts: { colorFunction?: ColorFunction } = {} ) { const { colorFunction } = opts const gl = this.gl const W = world.width const H = world.height const cW = this.cellWidth const cH = this.cellHeight // 6 vertices per cell, 6 floats per vertex const data = new Float32Array(W * H * 6 * 6) let offset = 0 for (let x = 0; x < W; x++) { for (let y = 0; y < H; y++) { const cell = world.getCell([x, y])! const color = colorFunction ? colorFunction(cell) : { fill: cell.color, stroke: cell.strokeColor } const fill = color.fill const r = fill.r / 255 const g = fill.g / 255 const b = fill.b / 255 const a = fill.a const x0 = x * cW const y0 = y * cH const x1 = (x + 1) * cW const y1 = (y + 1) * cH // Triangle 1: TL, TR, BL data[offset++] = x0; data[offset++] = y0; data[offset++] = r; data[offset++] = g; data[offset++] = b; data[offset++] = a data[offset++] = x1; data[offset++] = y0; data[offset++] = r; data[offset++] = g; data[offset++] = b; data[offset++] = a data[offset++] = x0; data[offset++] = y1; data[offset++] = r; data[offset++] = g; data[offset++] = b; data[offset++] = a // Triangle 2: TR, BR, BL data[offset++] = x1; data[offset++] = y0; data[offset++] = r; data[offset++] = g; data[offset++] = b; data[offset++] = a data[offset++] = x1; data[offset++] = y1; data[offset++] = r; data[offset++] = g; data[offset++] = b; data[offset++] = a data[offset++] = x0; data[offset++] = y1; data[offset++] = r; data[offset++] = g; data[offset++] = b; data[offset++] = a } } gl.bindVertexArray(this.gridVAO) gl.bindBuffer(gl.ARRAY_BUFFER, this.gridVBO) gl.bufferData(gl.ARRAY_BUFFER, data, gl.DYNAMIC_DRAW) gl.drawArrays(gl.TRIANGLES, 0, W * H * 6) gl.bindVertexArray(null) } // ------------------------------------------------------------------------- // Draw agent set — batched per style group (≤3 draw calls) // ------------------------------------------------------------------------- drawAgentSet( agents: AgentSet, opts: { colorFunction?: ColorFunction, styleFunction?: StyleFunction } = {} ) { const { colorFunction, styleFunction } = opts const circles: T[] = [] const squares: T[] = [] const triangles: T[] = [] agents.forEach(agent => { const style = styleFunction ? styleFunction(agent) : agent.style if (style === AgentStyle.CIRCLE) circles.push(agent) else if (style === AgentStyle.SQUARE) squares.push(agent) else triangles.push(agent) }) if (circles.length > 0) this.drawAgentBatch(circles, AgentStyle.CIRCLE, colorFunction) if (squares.length > 0) this.drawAgentBatch(squares, AgentStyle.SQUARE, colorFunction) if (triangles.length > 0) this.drawAgentBatch(triangles, AgentStyle.TRIANGLE, colorFunction) } private drawAgentBatch( agents: T[], style: AgentStyle, colorFunction?: ColorFunction ) { const gl = this.gl const cW = this.cellWidth const cH = this.cellHeight const vertsPerAgent = style === AgentStyle.CIRCLE ? CIRCLE_SEGMENTS * 3 : style === AgentStyle.SQUARE ? 6 : 3 const data = new Float32Array(agents.length * vertsPerAgent * 6) let offset = 0 for (const agent of agents) { const [ax, ay] = agent.position.components const px = ax * cW const py = ay * cH const color = colorFunction ? colorFunction(agent) : { fill: agent.color } const fill = color.fill const r = fill.r / 255 const g = fill.g / 255 const b = fill.b / 255 const a = fill.a if (style === AgentStyle.CIRCLE) { const scale = agent.radius * (cW / 2) for (let i = 0; i < CIRCLE_UNIT_VERTS.length; i += 2) { data[offset++] = CIRCLE_UNIT_VERTS[i] * scale + px data[offset++] = CIRCLE_UNIT_VERTS[i + 1] * scale + py data[offset++] = r; data[offset++] = g; data[offset++] = b; data[offset++] = a } } else if (style === AgentStyle.TRIANGLE) { const scale = agent.radius * cW const rot = agent.rotation.asRadians() const cosR = Math.cos(rot) const sinR = Math.sin(rot) for (let i = 0; i < TRIANGLE_UNIT_VERTS.length; i += 2) { const sx = TRIANGLE_UNIT_VERTS[i] * scale const sy = TRIANGLE_UNIT_VERTS[i + 1] * scale data[offset++] = sx * cosR - sy * sinR + px data[offset++] = sx * sinR + sy * cosR + py data[offset++] = r; data[offset++] = g; data[offset++] = b; data[offset++] = a } } else { // SQUARE const scaleX = agent.radius * cW const scaleY = agent.radius * cH for (let i = 0; i < SQUARE_UNIT_VERTS.length; i += 2) { data[offset++] = SQUARE_UNIT_VERTS[i] * scaleX + px data[offset++] = SQUARE_UNIT_VERTS[i + 1] * scaleY + py data[offset++] = r; data[offset++] = g; data[offset++] = b; data[offset++] = a } } } gl.bindVertexArray(this.agentVAO) gl.bindBuffer(gl.ARRAY_BUFFER, this.agentVBO) gl.bufferData(gl.ARRAY_BUFFER, data, gl.DYNAMIC_DRAW) gl.drawArrays(gl.TRIANGLES, 0, agents.length * vertsPerAgent) gl.bindVertexArray(null) } // ------------------------------------------------------------------------- // Draw single agent — fallback for onCellClick handlers // ------------------------------------------------------------------------- drawAgent( agent: T, opts: { colorFunction?: ColorFunction, styleFunction?: StyleFunction } = {} ) { const { colorFunction, styleFunction } = opts const style = styleFunction ? styleFunction(agent) : agent.style const color = colorFunction ? colorFunction(agent) : { fill: agent.color, stroke: agent.strokeColor } const [x, y] = agent.position.components const options = { width: agent.radius, height: agent.radius, radius: agent.radius, rotation: agent.rotation, fill: color.fill, stroke: color.stroke, } if (style === AgentStyle.CIRCLE) this.drawCircle(x, y, options) else if (style === AgentStyle.TRIANGLE) this.drawTriangle(x, y, options) else this.drawRectangle(x, y, options) } // ------------------------------------------------------------------------- // Misc draw methods — individual draw calls // ------------------------------------------------------------------------- public drawCircle( x: number, y: number, options?: { radius?: number, rotation?: Angle, fill?: Color, stroke?: Color, lineWidth?: number, showRotation?: boolean, } ) { const { radius = 1, rotation = new Angle(0, 'rad'), fill = Color.fromName('blue'), stroke = undefined, showRotation = true, } = options ?? {} const gl = this.gl const px = x * this.cellWidth const py = y * this.cellHeight const r_px = radius * (this.cellWidth / 2) const rf = fill.r / 255, gf = fill.g / 255, bf = fill.b / 255, af = fill.a // Fill: TRIANGLE_FAN packed as TRIANGLES using CIRCLE_UNIT_VERTS const fillData = new Float32Array(CIRCLE_SEGMENTS * 3 * 6) let offset = 0 for (let i = 0; i < CIRCLE_UNIT_VERTS.length; i += 2) { fillData[offset++] = CIRCLE_UNIT_VERTS[i] * r_px + px fillData[offset++] = CIRCLE_UNIT_VERTS[i + 1] * r_px + py fillData[offset++] = rf; fillData[offset++] = gf; fillData[offset++] = bf; fillData[offset++] = af } gl.bindVertexArray(this.miscVAO) gl.bindBuffer(gl.ARRAY_BUFFER, this.miscVBO) gl.bufferData(gl.ARRAY_BUFFER, fillData, gl.DYNAMIC_DRAW) gl.drawArrays(gl.TRIANGLES, 0, CIRCLE_SEGMENTS * 3) // Stroke: LINE_LOOP around edge if (stroke) { const rs = stroke.r / 255, gs = stroke.g / 255, bs = stroke.b / 255, as = stroke.a const strokeData = new Float32Array(CIRCLE_SEGMENTS * 6) let so = 0 for (let i = 0; i < CIRCLE_EDGE_VERTS.length; i += 2) { strokeData[so++] = CIRCLE_EDGE_VERTS[i] * r_px + px strokeData[so++] = CIRCLE_EDGE_VERTS[i + 1] * r_px + py strokeData[so++] = rs; strokeData[so++] = gs; strokeData[so++] = bs; strokeData[so++] = as } gl.bufferData(gl.ARRAY_BUFFER, strokeData, gl.DYNAMIC_DRAW) gl.drawArrays(gl.LINE_LOOP, 0, CIRCLE_SEGMENTS) } // Rotation indicator line if (rotation && showRotation) { const rot = rotation.asRadians() const lineData = new Float32Array(2 * 6) const rc = stroke ? stroke.r / 255 : rf const gc = stroke ? stroke.g / 255 : gf const bc = stroke ? stroke.b / 255 : bf const ac = stroke ? stroke.a : af lineData[0] = px; lineData[1] = py; lineData[2] = rc; lineData[3] = gc; lineData[4] = bc; lineData[5] = ac lineData[6] = px + r_px * Math.cos(rot); lineData[7] = py + r_px * Math.sin(rot) lineData[8] = rc; lineData[9] = gc; lineData[10] = bc; lineData[11] = ac gl.bufferData(gl.ARRAY_BUFFER, lineData, gl.DYNAMIC_DRAW) gl.drawArrays(gl.LINES, 0, 2) } gl.bindVertexArray(null) } public drawRectangle( x: number, y: number, options?: { width?: number, height?: number, rotation?: Angle, fill?: Color, stroke?: Color, lineWidth?: number, } ) { const { width = 1, height = 1, rotation = new Angle(0, 'rad'), fill = Color.fromName('blue'), stroke = undefined, } = options ?? {} const gl = this.gl const _x = x * this.cellWidth const _y = y * this.cellHeight const _w = width * this.cellWidth const _h = height * this.cellHeight const rf = fill.r / 255, gf = fill.g / 255, bf = fill.b / 255, af = fill.a // 2 triangles = 6 vertices const fillData = new Float32Array(6 * 6) const verts = [ _x, _y, _x + _w, _y, _x, _y + _h, _x + _w, _y, _x + _w, _y + _h, _x, _y + _h, ] for (let i = 0; i < 6; i++) { const base = i * 6 fillData[base] = verts[i * 2] fillData[base + 1] = verts[i * 2 + 1] fillData[base + 2] = rf; fillData[base + 3] = gf; fillData[base + 4] = bf; fillData[base + 5] = af } gl.bindVertexArray(this.miscVAO) gl.bindBuffer(gl.ARRAY_BUFFER, this.miscVBO) gl.bufferData(gl.ARRAY_BUFFER, fillData, gl.DYNAMIC_DRAW) gl.drawArrays(gl.TRIANGLES, 0, 6) // Stroke: LINE_LOOP around the 4 corners if (stroke) { const rs = stroke.r / 255, gs = stroke.g / 255, bs = stroke.b / 255, as = stroke.a const strokeData = new Float32Array(4 * 6) const corners = [_x, _y, _x + _w, _y, _x + _w, _y + _h, _x, _y + _h] for (let i = 0; i < 4; i++) { const base = i * 6 strokeData[base] = corners[i * 2] strokeData[base + 1] = corners[i * 2 + 1] strokeData[base + 2] = rs; strokeData[base + 3] = gs; strokeData[base + 4] = bs; strokeData[base + 5] = as } gl.bufferData(gl.ARRAY_BUFFER, strokeData, gl.DYNAMIC_DRAW) gl.drawArrays(gl.LINE_LOOP, 0, 4) } // Rotation indicator line from center to edge if (rotation) { const rot = rotation.asRadians() const cx = _x + _w / 2 const cy = _y + _h / 2 const rc = stroke ? stroke.r / 255 : rf const gc = stroke ? stroke.g / 255 : gf const bc = stroke ? stroke.b / 255 : bf const ac = stroke ? stroke.a : af const lineData = new Float32Array(2 * 6) lineData[0] = cx; lineData[1] = cy; lineData[2] = rc; lineData[3] = gc; lineData[4] = bc; lineData[5] = ac lineData[6] = cx + _w / 2 * Math.cos(rot); lineData[7] = cy + _h / 2 * Math.sin(rot) lineData[8] = rc; lineData[9] = gc; lineData[10] = bc; lineData[11] = ac gl.bufferData(gl.ARRAY_BUFFER, lineData, gl.DYNAMIC_DRAW) gl.drawArrays(gl.LINES, 0, 2) } gl.bindVertexArray(null) } public drawTriangle( x: number, y: number, options?: { width?: number, rotation?: Angle, fill?: Color, stroke?: Color, lineWidth?: number, } ) { const { width = 1, rotation = new Angle(0, 'rad'), fill = Color.fromName('blue'), stroke = undefined, } = options ?? {} const gl = this.gl const _x = x * this.cellWidth const _y = y * this.cellHeight const _w = width * this.cellWidth const halfWidth = _w / 2 const triHeight = (Math.sqrt(3) / 2) * _w + _w / 2 const rot = rotation.asRadians() const cosR = Math.cos(rot) const sinR = Math.sin(rot) // Unit verts (centered) const localVerts: [number, number][] = [ [0, -triHeight / 2], [-halfWidth, triHeight / 2], [halfWidth, triHeight / 2], ] const rf = fill.r / 255, gf = fill.g / 255, bf = fill.b / 255, af = fill.a const fillData = new Float32Array(3 * 6) for (let i = 0; i < 3; i++) { const lx = localVerts[i][0] const ly = localVerts[i][1] const vx = lx * cosR - ly * sinR + _x const vy = lx * sinR + ly * cosR + _y const base = i * 6 fillData[base] = vx; fillData[base + 1] = vy fillData[base + 2] = rf; fillData[base + 3] = gf; fillData[base + 4] = bf; fillData[base + 5] = af } gl.bindVertexArray(this.miscVAO) gl.bindBuffer(gl.ARRAY_BUFFER, this.miscVBO) gl.bufferData(gl.ARRAY_BUFFER, fillData, gl.DYNAMIC_DRAW) gl.drawArrays(gl.TRIANGLES, 0, 3) if (stroke) { const rs = stroke.r / 255, gs = stroke.g / 255, bs = stroke.b / 255, as = stroke.a const strokeData = new Float32Array(3 * 6) for (let i = 0; i < 3; i++) { const lx = localVerts[i][0] const ly = localVerts[i][1] const vx = lx * cosR - ly * sinR + _x const vy = lx * sinR + ly * cosR + _y const base = i * 6 strokeData[base] = vx; strokeData[base + 1] = vy strokeData[base + 2] = rs; strokeData[base + 3] = gs; strokeData[base + 4] = bs; strokeData[base + 5] = as } gl.bufferData(gl.ARRAY_BUFFER, strokeData, gl.DYNAMIC_DRAW) gl.drawArrays(gl.LINE_LOOP, 0, 3) } gl.bindVertexArray(null) } public drawLine( x1: number, y1: number, x2: number, y2: number, stroke: Color, options?: { lineWidth?: number, } ) { const gl = this.gl const rs = stroke.r / 255, gs = stroke.g / 255, bs = stroke.b / 255, as = stroke.a const data = new Float32Array(2 * 6) data[0] = x1 * this.cellWidth; data[1] = y1 * this.cellHeight data[2] = rs; data[3] = gs; data[4] = bs; data[5] = as data[6] = x2 * this.cellWidth; data[7] = y2 * this.cellHeight data[8] = rs; data[9] = gs; data[10] = bs; data[11] = as if (options?.lineWidth) { gl.lineWidth(options.lineWidth) } gl.bindVertexArray(this.miscVAO) gl.bindBuffer(gl.ARRAY_BUFFER, this.miscVBO) gl.bufferData(gl.ARRAY_BUFFER, data, gl.DYNAMIC_DRAW) gl.drawArrays(gl.LINES, 0, 2) gl.bindVertexArray(null) } public drawPolygon( points: [number, number][], options?: { fill?: Color, stroke?: Color, lineWidth?: number, } ): void { if (points.length < 3) { throw new Error('A polygon must have at least 3 vertices') } const { fill = Color.fromName('blue'), stroke = undefined, } = options ?? {} const gl = this.gl // Convert to pixel space const px = points.map(([x, y]) => [x * this.cellWidth, y * this.cellHeight] as [number, number]) // Compute centroid const cx = px.reduce((s, p) => s + p[0], 0) / px.length const cy = px.reduce((s, p) => s + p[1], 0) / px.length const n = px.length const rf = fill.r / 255, gf = fill.g / 255, bf = fill.b / 255, af = fill.a // Fan triangulation from centroid: n triangles × 3 vertices × 6 floats const fillData = new Float32Array(n * 3 * 6) let offset = 0 for (let i = 0; i < n; i++) { const j = (i + 1) % n const verts: [number, number][] = [[cx, cy], px[i], px[j]] for (const [vx, vy] of verts) { fillData[offset++] = vx; fillData[offset++] = vy fillData[offset++] = rf; fillData[offset++] = gf; fillData[offset++] = bf; fillData[offset++] = af } } gl.bindVertexArray(this.miscVAO) gl.bindBuffer(gl.ARRAY_BUFFER, this.miscVBO) gl.bufferData(gl.ARRAY_BUFFER, fillData, gl.DYNAMIC_DRAW) gl.drawArrays(gl.TRIANGLES, 0, n * 3) if (stroke) { const rs = stroke.r / 255, gs = stroke.g / 255, bs = stroke.b / 255, as = stroke.a const strokeData = new Float32Array(n * 6) for (let i = 0; i < n; i++) { const base = i * 6 strokeData[base] = px[i][0]; strokeData[base + 1] = px[i][1] strokeData[base + 2] = rs; strokeData[base + 3] = gs; strokeData[base + 4] = bs; strokeData[base + 5] = as } gl.bufferData(gl.ARRAY_BUFFER, strokeData, gl.DYNAMIC_DRAW) gl.drawArrays(gl.LINE_LOOP, 0, n) } gl.bindVertexArray(null) } public drawPolyline( points: [number, number][], stroke: Color, options?: { lineWidth?: number, } ): void { if (points.length < 2) { throw new Error('A polyline must have at least 2 vertices') } const gl = this.gl const rs = stroke.r / 255, gs = stroke.g / 255, bs = stroke.b / 255, as = stroke.a const n = points.length const data = new Float32Array(n * 6) for (let i = 0; i < n; i++) { const base = i * 6 data[base] = points[i][0] * this.cellWidth data[base + 1] = points[i][1] * this.cellHeight data[base + 2] = rs; data[base + 3] = gs; data[base + 4] = bs; data[base + 5] = as } if (options?.lineWidth) { gl.lineWidth(options.lineWidth) } gl.bindVertexArray(this.miscVAO) gl.bindBuffer(gl.ARRAY_BUFFER, this.miscVBO) gl.bufferData(gl.ARRAY_BUFFER, data, gl.DYNAMIC_DRAW) gl.drawArrays(gl.LINE_STRIP, 0, n) gl.bindVertexArray(null) } } export default WebGL2D