From 9ac6d7f82ccc45c6dac1ec4fb0476e9cd994d16d Mon Sep 17 00:00:00 2001 From: Stan Grams Date: Sat, 1 Aug 2026 12:07:02 +0200 Subject: [PATCH] refactor: convert WebGL renderer to TypeScript --- .../assets/web/generated/webgl-renderer.js | 125 +++++----- .../trx-frontend-http/frontend/build.mjs | 2 +- .../{webgl-renderer.js => webgl-renderer.ts} | 222 ++++++++++-------- .../frontend/tests/webgl-renderer.test.mjs | 33 +++ 4 files changed, 231 insertions(+), 151 deletions(-) rename src/trx-client/trx-frontend/trx-frontend-http/frontend/src/{webgl-renderer.js => webgl-renderer.ts} (70%) create mode 100644 src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/webgl-renderer.test.mjs diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/webgl-renderer.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/webgl-renderer.js index d05792b2..41afa5d7 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/webgl-renderer.js +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/webgl-renderer.js @@ -19,9 +19,9 @@ return cssColorProbe; } function parseRgbString(value) { - const m = /^rgba?\(([^)]+)\)$/.exec(String(value || "").trim()); + const m = /^rgba?\(([^)]+)\)$/.exec(value.trim()); if (!m) return null; - const parts = m[1].split(",").map((p) => p.trim()); + const parts = m[1]?.split(",").map((p) => p.trim()) ?? []; if (parts.length < 3) return null; const r = Number(parts[0]); const g = Number(parts[1]); @@ -36,7 +36,7 @@ ]; } function parseHexColor(value) { - const raw = String(value || "").trim(); + const raw = value.trim(); if (!/^#([0-9a-f]{3,8})$/i.test(raw)) return null; let hex = raw.slice(1); if (hex.length === 3 || hex.length === 4) { @@ -50,8 +50,9 @@ return [r, g, b, a]; } function parseCssColor(value) { - const key = String(value ?? ""); - if (cssColorCache.has(key)) return cssColorCache.get(key).slice(); + const key = value; + const cached = cssColorCache.get(key); + if (cached) return [...cached]; let parsed = parseHexColor(key) || parseRgbString(key); if (!parsed) { const probe = ensureCssColorProbe(); @@ -60,13 +61,13 @@ const computed = getComputedStyle(probe).color; parsed = parseRgbString(computed) || [0, 0, 0, 1]; } - cssColorCache.set(key, parsed.slice()); - return parsed.slice(); + cssColorCache.set(key, [...parsed]); + return [...parsed]; } function hslToRgba(h, s, l, a = 1) { - const hue = ((Number(h) || 0) % 360 + 360) % 360 / 360; - const sat = Math.max(0, Math.min(1, (Number(s) || 0) / 100)); - const lig = Math.max(0, Math.min(1, (Number(l) || 0) / 100)); + const hue = ((h || 0) % 360 + 360) % 360 / 360; + const sat = Math.max(0, Math.min(1, (s || 0) / 100)); + const lig = Math.max(0, Math.min(1, (l || 0) / 100)); const q = lig < 0.5 ? lig * (1 + sat) : lig + sat - lig * sat; const p = 2 * lig - q; const hueToRgb = (t) => { @@ -81,28 +82,26 @@ const r = sat === 0 ? lig : hueToRgb(hue + 1 / 3); const g = sat === 0 ? lig : hueToRgb(hue); const b = sat === 0 ? lig : hueToRgb(hue - 1 / 3); - return [r, g, b, Math.max(0, Math.min(1, Number(a)))]; + return [r, g, b, Math.max(0, Math.min(1, a))]; } function normalizeColor(input, alphaMul = 1) { let rgba; if (Array.isArray(input)) { - const arr = input.map((v) => Number(v)); + const arr = input; if (arr.length >= 4) { - rgba = [arr[0], arr[1], arr[2], arr[3]]; + rgba = [arr[0] ?? 0, arr[1] ?? 0, arr[2] ?? 0, arr[3] ?? 1]; } else { rgba = [0, 0, 0, 1]; } } else if (typeof input === "string") { rgba = parseCssColor(input); - } else if (input && typeof input === "object") { - rgba = [ - Number(input.r) || 0, - Number(input.g) || 0, - Number(input.b) || 0, - Number(input.a ?? 1) - ]; } else { - rgba = [0, 0, 0, 1]; + rgba = [ + input.r || 0, + input.g || 0, + input.b || 0, + input.a ?? 1 + ]; } const out = [ Math.max(0, Math.min(1, rgba[0])), @@ -114,6 +113,7 @@ } function compileShader(gl, type, source) { const shader = gl.createShader(type); + if (shader === null) throw new Error("Unable to create WebGL shader"); gl.shaderSource(shader, source); gl.compileShader(shader); if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { @@ -161,17 +161,28 @@ pushColoredVertex(out, dx2, dy2, rgba); } class TrxWebGlRenderer { + canvas; + options; + gl; + ready; + textures = /* @__PURE__ */ new Map(); + _colorScratch = new Float32Array(4096 * 6); + _colorGpuSize = 0; + _texScratch = new Float32Array(6 * 4); + colorProgram; + colorBuffer; + colorLoc; + textureProgram; + textureBuffer; + textureLoc; constructor(canvas, options = {}) { this.canvas = canvas; this.options = { alpha: true, premultipliedAlpha: false, ...options }; - this.gl = canvas?.getContext("webgl", this.options) || canvas?.getContext("experimental-webgl", this.options) || null; + this.gl = canvas.getContext("webgl", this.options) || canvas.getContext("experimental-webgl", this.options); this.ready = !!this.gl; - this.textures = /* @__PURE__ */ new Map(); - this._colorScratch = new Float32Array(4096 * 6); - this._colorGpuSize = 0; - this._texScratch = new Float32Array(6 * 4); if (!this.ready) return; const gl = this.gl; + if (!gl) return; gl.disable(gl.DEPTH_TEST); gl.disable(gl.CULL_FACE); gl.enable(gl.BLEND); @@ -203,7 +214,7 @@ }; } ensureSize(cssWidth, cssHeight, dpr = window.devicePixelRatio || 1) { - if (!this.ready) return false; + if (!this.gl) return false; const nextW = Math.max(1, Math.round(cssWidth * dpr)); const nextH = Math.max(1, Math.round(cssHeight * dpr)); const changed = this.canvas.width !== nextW || this.canvas.height !== nextH; @@ -215,20 +226,22 @@ return changed; } clear(color) { - if (!this.ready) return; + if (!this.gl) return; const gl = this.gl; const rgba = normalizeColor(color); gl.clearColor(rgba[0], rgba[1], rgba[2], rgba[3]); gl.clear(gl.COLOR_BUFFER_BIT); } drawTriangles(vertices) { + if (!this.gl) return; this._drawColorGeometry(vertices, this.gl.TRIANGLES); } drawTriangleStrip(vertices) { + if (!this.gl) return; this._drawColorGeometry(vertices, this.gl.TRIANGLE_STRIP); } _drawColorGeometry(vertices, mode) { - if (!this.ready || !vertices || vertices.length === 0) return; + if (!this.gl || vertices.length === 0) return; const gl = this.gl; const count = vertices.length; if (count > this._colorScratch.length) { @@ -263,7 +276,7 @@ pushColoredVertex(v, x, y, rgba); pushColoredVertex(v, x + w, y + h, rgba); pushColoredVertex(v, x, y + h, rgba); - this._drawColorGeometry(v, this.gl.TRIANGLES); + this.drawTriangles(v); } fillGradientRect(x, y, w, h, colorTL, colorTR, colorBR, colorBL) { if (w <= 0 || h <= 0) return; @@ -278,62 +291,62 @@ pushColoredVertex(v, x, y, tl); pushColoredVertex(v, x + w, y + h, br); pushColoredVertex(v, x, y + h, bl); - this._drawColorGeometry(v, this.gl.TRIANGLES); + this.drawTriangles(v); } drawPolyline(points, color, width = 1) { if (!Array.isArray(points) || points.length < 4) return; const rgba = normalizeColor(color); - const halfW = Math.max(0.5, Number(width) || 1) / 2; + const halfW = Math.max(0.5, width || 1) / 2; const verts = []; for (let i = 0; i < points.length - 2; i += 2) { segmentToQuadVertices( verts, - points[i], - points[i + 1], - points[i + 2], - points[i + 3], + points[i] ?? 0, + points[i + 1] ?? 0, + points[i + 2] ?? 0, + points[i + 3] ?? 0, halfW, rgba ); } - this._drawColorGeometry(verts, this.gl.TRIANGLES); + this.drawTriangles(verts); } drawSegments(segments, color, width = 1) { if (!Array.isArray(segments) || segments.length < 4) return; const rgba = normalizeColor(color); - const halfW = Math.max(0.5, Number(width) || 1) / 2; + const halfW = Math.max(0.5, width || 1) / 2; const verts = []; for (let i = 0; i < segments.length - 3; i += 4) { segmentToQuadVertices( verts, - segments[i], - segments[i + 1], - segments[i + 2], - segments[i + 3], + segments[i] ?? 0, + segments[i + 1] ?? 0, + segments[i + 2] ?? 0, + segments[i + 3] ?? 0, halfW, rgba ); } - this._drawColorGeometry(verts, this.gl.TRIANGLES); + this.drawTriangles(verts); } drawFilledArea(points, baselineY, color) { if (!Array.isArray(points) || points.length < 4) return; const rgba = normalizeColor(color); const verts = []; for (let i = 0; i < points.length; i += 2) { - pushColoredVertex(verts, points[i], baselineY, rgba); - pushColoredVertex(verts, points[i], points[i + 1], rgba); + pushColoredVertex(verts, points[i] ?? 0, baselineY, rgba); + pushColoredVertex(verts, points[i] ?? 0, points[i + 1] ?? 0, rgba); } - this._drawColorGeometry(verts, this.gl.TRIANGLE_STRIP); + this.drawTriangleStrip(verts); } drawPoints(points, size, color) { if (!Array.isArray(points) || points.length < 2) return; - const radius = Math.max(1, Number(size) || 1); + const radius = Math.max(1, size || 1); const rgba = normalizeColor(color); const verts = []; for (let i = 0; i < points.length; i += 2) { - const x = points[i] - radius; - const y = points[i + 1] - radius; + const x = (points[i] ?? 0) - radius; + const y = (points[i + 1] ?? 0) - radius; const w = radius * 2; const h = radius * 2; pushColoredVertex(verts, x, y, rgba); @@ -343,11 +356,11 @@ pushColoredVertex(verts, x + w, y + h, rgba); pushColoredVertex(verts, x, y + h, rgba); } - this._drawColorGeometry(verts, this.gl.TRIANGLES); + this.drawTriangles(verts); } drawDashedVerticalLine(x, y0, y1, dashLen, gapLen, color, width = 1) { - const dash = Math.max(1, Number(dashLen) || 1); - const gap = Math.max(1, Number(gapLen) || 1); + const dash = Math.max(1, dashLen || 1); + const gap = Math.max(1, gapLen || 1); const top = Math.min(y0, y1); const bottom = Math.max(y0, y1); const segments = []; @@ -358,7 +371,7 @@ this.drawSegments(segments, color, width); } uploadRgbaTexture(name, width, height, data, filter = "linear") { - if (!this.ready || !name || !data) return null; + if (!this.gl || !name) return null; const gl = this.gl; let entry = this.textures.get(name); if (!entry) { @@ -403,7 +416,7 @@ return entry.texture; } drawTexture(name, x, y, w, h, alpha = 1, flipY = true) { - if (!this.ready || !name || w <= 0 || h <= 0) return; + if (!this.gl || !name || w <= 0 || h <= 0) return; const entry = this.textures.get(name); if (!entry) return; const gl = this.gl; @@ -468,14 +481,14 @@ gl.enableVertexAttribArray(this.textureLoc.uv); gl.vertexAttribPointer(this.textureLoc.uv, 2, gl.FLOAT, false, 16, 8); gl.uniform2f(this.textureLoc.resolution, this.canvas.width, this.canvas.height); - gl.uniform1f(this.textureLoc.alpha, Math.max(0, Math.min(1, Number(alpha) || 0))); + gl.uniform1f(this.textureLoc.alpha, Math.max(0, Math.min(1, alpha || 0))); gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, entry.texture); gl.uniform1i(this.textureLoc.tex, 0); gl.drawArrays(gl.TRIANGLES, 0, 6); } } - function createRenderer(canvas, options) { + function createRenderer(canvas, options = {}) { return new TrxWebGlRenderer(canvas, options); } global.trxParseCssColor = parseCssColor; diff --git a/src/trx-client/trx-frontend/trx-frontend-http/frontend/build.mjs b/src/trx-client/trx-frontend/trx-frontend-http/frontend/build.mjs index 7f4775df..565dc12c 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/frontend/build.mjs +++ b/src/trx-client/trx-frontend/trx-frontend-http/frontend/build.mjs @@ -20,7 +20,7 @@ await build({ "ui-core": path.join(sourceDir, "ui-core.ts"), "map-core": path.join(sourceDir, "map-core.js"), screenshot: path.join(sourceDir, "screenshot.js"), - "webgl-renderer": path.join(sourceDir, "webgl-renderer.js"), + "webgl-renderer": path.join(sourceDir, "webgl-renderer.ts"), "leaflet-ais-tracksymbol": path.join(sourceDir, "leaflet-ais-tracksymbol.js"), ais: path.join(sourceDir, "plugins", "ais.js"), aprs: path.join(sourceDir, "plugins", "aprs.js"), diff --git a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/webgl-renderer.js b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/webgl-renderer.ts similarity index 70% rename from src/trx-client/trx-frontend/trx-frontend-http/frontend/src/webgl-renderer.js rename to src/trx-client/trx-frontend/trx-frontend-http/frontend/src/webgl-renderer.ts index 29a05575..462bae0b 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/webgl-renderer.js +++ b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/webgl-renderer.ts @@ -2,17 +2,39 @@ // // SPDX-License-Identifier: GPL-2.0-or-later -(function initTrxWebGl(global) { +type Rgba = [number, number, number, number]; +type ColorInput = string | number[] | { r: number; g: number; b: number; a?: number }; +interface TextureEntry { texture: WebGLTexture | null; width: number; height: number } +interface ColorLocations { + pos: number; + color: number; + resolution: WebGLUniformLocation | null; +} +interface TextureLocations { + pos: number; + uv: number; + resolution: WebGLUniformLocation | null; + alpha: WebGLUniformLocation | null; + tex: WebGLUniformLocation | null; +} +interface TrxWebGlGlobals { + trxParseCssColor?: (value: string) => Rgba; + trxHslToRgba?: (h: number, s: number, l: number, a?: number) => Rgba; + createTrxWebGlRenderer?: (canvas: HTMLCanvasElement, options?: WebGLContextAttributes) => object; + trxClearCssColorCache?: () => void; +} + +(function initTrxWebGl(global: TrxWebGlGlobals) { "use strict"; - const cssColorCache = new Map(); - let cssColorProbe = null; + const cssColorCache = new Map(); + let cssColorProbe: HTMLSpanElement | null = null; function clearCssColorCache() { cssColorCache.clear(); } - function ensureCssColorProbe() { + function ensureCssColorProbe(): HTMLSpanElement { if (cssColorProbe) return cssColorProbe; const el = document.createElement("span"); el.style.position = "absolute"; @@ -25,10 +47,10 @@ return cssColorProbe; } - function parseRgbString(value) { - const m = /^rgba?\(([^)]+)\)$/.exec(String(value || "").trim()); + function parseRgbString(value: string): Rgba | null { + const m = /^rgba?\(([^)]+)\)$/.exec(value.trim()); if (!m) return null; - const parts = m[1].split(",").map((p) => p.trim()); + const parts = m[1]?.split(",").map((p) => p.trim()) ?? []; if (parts.length < 3) return null; const r = Number(parts[0]); const g = Number(parts[1]); @@ -43,8 +65,8 @@ ]; } - function parseHexColor(value) { - const raw = String(value || "").trim(); + function parseHexColor(value: string): Rgba | null { + const raw = value.trim(); if (!/^#([0-9a-f]{3,8})$/i.test(raw)) return null; let hex = raw.slice(1); if (hex.length === 3 || hex.length === 4) { @@ -58,9 +80,10 @@ return [r, g, b, a]; } - function parseCssColor(value) { - const key = String(value ?? ""); - if (cssColorCache.has(key)) return cssColorCache.get(key).slice(); + function parseCssColor(value: string): Rgba { + const key = value; + const cached = cssColorCache.get(key); + if (cached) return [...cached]; let parsed = parseHexColor(key) || parseRgbString(key); if (!parsed) { @@ -70,18 +93,18 @@ const computed = getComputedStyle(probe).color; parsed = parseRgbString(computed) || [0, 0, 0, 1]; } - cssColorCache.set(key, parsed.slice()); - return parsed.slice(); + cssColorCache.set(key, [...parsed]); + return [...parsed]; } - function hslToRgba(h, s, l, a = 1) { - const hue = ((((Number(h) || 0) % 360) + 360) % 360) / 360; - const sat = Math.max(0, Math.min(1, (Number(s) || 0) / 100)); - const lig = Math.max(0, Math.min(1, (Number(l) || 0) / 100)); + function hslToRgba(h: number, s: number, l: number, a = 1): Rgba { + const hue = ((((h || 0) % 360) + 360) % 360) / 360; + const sat = Math.max(0, Math.min(1, (s || 0) / 100)); + const lig = Math.max(0, Math.min(1, (l || 0) / 100)); const q = lig < 0.5 ? lig * (1 + sat) : lig + sat - lig * sat; const p = 2 * lig - q; - const hueToRgb = (t) => { + const hueToRgb = (t: number): number => { let tt = t; if (tt < 0) tt += 1; if (tt > 1) tt -= 1; @@ -94,31 +117,29 @@ const r = sat === 0 ? lig : hueToRgb(hue + 1 / 3); const g = sat === 0 ? lig : hueToRgb(hue); const b = sat === 0 ? lig : hueToRgb(hue - 1 / 3); - return [r, g, b, Math.max(0, Math.min(1, Number(a)))]; + return [r, g, b, Math.max(0, Math.min(1, a))]; } - function normalizeColor(input, alphaMul = 1) { - let rgba; + function normalizeColor(input: ColorInput, alphaMul = 1): Rgba { + let rgba: Rgba; if (Array.isArray(input)) { - const arr = input.map((v) => Number(v)); + const arr = input; if (arr.length >= 4) { - rgba = [arr[0], arr[1], arr[2], arr[3]]; + rgba = [arr[0] ?? 0, arr[1] ?? 0, arr[2] ?? 0, arr[3] ?? 1]; } else { rgba = [0, 0, 0, 1]; } } else if (typeof input === "string") { rgba = parseCssColor(input); - } else if (input && typeof input === "object") { - rgba = [ - Number(input.r) || 0, - Number(input.g) || 0, - Number(input.b) || 0, - Number(input.a ?? 1), - ]; } else { - rgba = [0, 0, 0, 1]; + rgba = [ + input.r || 0, + input.g || 0, + input.b || 0, + input.a ?? 1, + ]; } - const out = [ + const out: Rgba = [ Math.max(0, Math.min(1, rgba[0])), Math.max(0, Math.min(1, rgba[1])), Math.max(0, Math.min(1, rgba[2])), @@ -127,8 +148,9 @@ return out; } - function compileShader(gl, type, source) { + function compileShader(gl: WebGLRenderingContext, type: number, source: string): WebGLShader { const shader = gl.createShader(type); + if (shader === null) throw new Error("Unable to create WebGL shader"); gl.shaderSource(shader, source); gl.compileShader(shader); if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { @@ -139,7 +161,7 @@ return shader; } - function createProgram(gl, vertexSrc, fragmentSrc) { + function createProgram(gl: WebGLRenderingContext, vertexSrc: string, fragmentSrc: string): WebGLProgram { const vs = compileShader(gl, gl.VERTEX_SHADER, vertexSrc); const fs = compileShader(gl, gl.FRAGMENT_SHADER, fragmentSrc); const program = gl.createProgram(); @@ -156,11 +178,11 @@ return program; } - function pushColoredVertex(target, x, y, rgba) { + function pushColoredVertex(target: number[], x: number, y: number, rgba: Rgba) { target.push(x, y, rgba[0], rgba[1], rgba[2], rgba[3]); } - function segmentToQuadVertices(out, x0, y0, x1, y1, halfW, rgba) { + function segmentToQuadVertices(out: number[], x0: number, y0: number, x1: number, y1: number, halfW: number, rgba: Rgba) { const dx = x1 - x0; const dy = y1 - y0; const len = Math.hypot(dx, dy); @@ -183,23 +205,33 @@ } class TrxWebGlRenderer { - constructor(canvas, options = {}) { + readonly canvas: HTMLCanvasElement; + readonly options: WebGLContextAttributes; + readonly gl: WebGLRenderingContext | null; + readonly ready: boolean; + readonly textures = new Map(); + private _colorScratch = new Float32Array(4096 * 6); + private _colorGpuSize = 0; + private readonly _texScratch = new Float32Array(6 * 4); + private colorProgram!: WebGLProgram; + private colorBuffer!: WebGLBuffer | null; + private colorLoc!: ColorLocations; + private textureProgram!: WebGLProgram; + private textureBuffer!: WebGLBuffer | null; + private textureLoc!: TextureLocations; + + constructor(canvas: HTMLCanvasElement, options: WebGLContextAttributes = {}) { this.canvas = canvas; this.options = { alpha: true, premultipliedAlpha: false, ...options }; - this.gl = - canvas?.getContext("webgl", this.options) || - canvas?.getContext("experimental-webgl", this.options) || - null; + this.gl = (canvas.getContext("webgl", this.options) || + canvas.getContext("experimental-webgl", this.options)) as WebGLRenderingContext | null; this.ready = !!this.gl; - this.textures = new Map(); // Reusable scratch buffers — avoids per-draw-call Float32Array allocation // and lets us use bufferSubData instead of bufferData (no GPU realloc). - this._colorScratch = new Float32Array(4096 * 6); // grows as needed - this._colorGpuSize = 0; // current GPU buffer size (floats) - this._texScratch = new Float32Array(6 * 4); // fixed: 6 verts × (xy+uv) if (!this.ready) return; const gl = this.gl; + if (!gl) return; gl.disable(gl.DEPTH_TEST); gl.disable(gl.CULL_FACE); gl.enable(gl.BLEND); @@ -268,8 +300,8 @@ }; } - ensureSize(cssWidth, cssHeight, dpr = (window.devicePixelRatio || 1)) { - if (!this.ready) return false; + ensureSize(cssWidth: number, cssHeight: number, dpr = (window.devicePixelRatio || 1)): boolean { + if (!this.gl) return false; const nextW = Math.max(1, Math.round(cssWidth * dpr)); const nextH = Math.max(1, Math.round(cssHeight * dpr)); const changed = this.canvas.width !== nextW || this.canvas.height !== nextH; @@ -281,24 +313,26 @@ return changed; } - clear(color) { - if (!this.ready) return; + clear(color: ColorInput) { + if (!this.gl) return; const gl = this.gl; const rgba = normalizeColor(color); gl.clearColor(rgba[0], rgba[1], rgba[2], rgba[3]); gl.clear(gl.COLOR_BUFFER_BIT); } - drawTriangles(vertices) { + drawTriangles(vertices: number[]) { + if (!this.gl) return; this._drawColorGeometry(vertices, this.gl.TRIANGLES); } - drawTriangleStrip(vertices) { + drawTriangleStrip(vertices: number[]) { + if (!this.gl) return; this._drawColorGeometry(vertices, this.gl.TRIANGLE_STRIP); } - _drawColorGeometry(vertices, mode) { - if (!this.ready || !vertices || vertices.length === 0) return; + private _drawColorGeometry(vertices: number[], mode: number) { + if (!this.gl || vertices.length === 0) return; const gl = this.gl; const count = vertices.length; @@ -333,88 +367,88 @@ gl.drawArrays(mode, 0, count / 6); } - fillRect(x, y, w, h, color) { + fillRect(x: number, y: number, w: number, h: number, color: ColorInput) { if (w <= 0 || h <= 0) return; const rgba = normalizeColor(color); - const v = []; + const v: number[] = []; pushColoredVertex(v, x, y, rgba); pushColoredVertex(v, x + w, y, rgba); pushColoredVertex(v, x + w, y + h, rgba); pushColoredVertex(v, x, y, rgba); pushColoredVertex(v, x + w, y + h, rgba); pushColoredVertex(v, x, y + h, rgba); - this._drawColorGeometry(v, this.gl.TRIANGLES); + this.drawTriangles(v); } - fillGradientRect(x, y, w, h, colorTL, colorTR, colorBR, colorBL) { + fillGradientRect(x: number, y: number, w: number, h: number, colorTL: ColorInput, colorTR: ColorInput, colorBR: ColorInput, colorBL: ColorInput) { if (w <= 0 || h <= 0) return; const tl = normalizeColor(colorTL); const tr = normalizeColor(colorTR); const br = normalizeColor(colorBR); const bl = normalizeColor(colorBL); - const v = []; + const v: number[] = []; pushColoredVertex(v, x, y, tl); pushColoredVertex(v, x + w, y, tr); pushColoredVertex(v, x + w, y + h, br); pushColoredVertex(v, x, y, tl); pushColoredVertex(v, x + w, y + h, br); pushColoredVertex(v, x, y + h, bl); - this._drawColorGeometry(v, this.gl.TRIANGLES); + this.drawTriangles(v); } - drawPolyline(points, color, width = 1) { + drawPolyline(points: number[], color: ColorInput, width = 1) { if (!Array.isArray(points) || points.length < 4) return; const rgba = normalizeColor(color); - const halfW = Math.max(0.5, Number(width) || 1) / 2; - const verts = []; + const halfW = Math.max(0.5, width || 1) / 2; + const verts: number[] = []; for (let i = 0; i < points.length - 2; i += 2) { segmentToQuadVertices( verts, - points[i], points[i + 1], - points[i + 2], points[i + 3], + points[i] ?? 0, points[i + 1] ?? 0, + points[i + 2] ?? 0, points[i + 3] ?? 0, halfW, rgba, ); } - this._drawColorGeometry(verts, this.gl.TRIANGLES); + this.drawTriangles(verts); } - drawSegments(segments, color, width = 1) { + drawSegments(segments: number[], color: ColorInput, width = 1) { if (!Array.isArray(segments) || segments.length < 4) return; const rgba = normalizeColor(color); - const halfW = Math.max(0.5, Number(width) || 1) / 2; - const verts = []; + const halfW = Math.max(0.5, width || 1) / 2; + const verts: number[] = []; for (let i = 0; i < segments.length - 3; i += 4) { segmentToQuadVertices( verts, - segments[i], segments[i + 1], - segments[i + 2], segments[i + 3], + segments[i] ?? 0, segments[i + 1] ?? 0, + segments[i + 2] ?? 0, segments[i + 3] ?? 0, halfW, rgba, ); } - this._drawColorGeometry(verts, this.gl.TRIANGLES); + this.drawTriangles(verts); } - drawFilledArea(points, baselineY, color) { + drawFilledArea(points: number[], baselineY: number, color: ColorInput) { if (!Array.isArray(points) || points.length < 4) return; const rgba = normalizeColor(color); - const verts = []; + const verts: number[] = []; for (let i = 0; i < points.length; i += 2) { - pushColoredVertex(verts, points[i], baselineY, rgba); - pushColoredVertex(verts, points[i], points[i + 1], rgba); + pushColoredVertex(verts, points[i] ?? 0, baselineY, rgba); + pushColoredVertex(verts, points[i] ?? 0, points[i + 1] ?? 0, rgba); } - this._drawColorGeometry(verts, this.gl.TRIANGLE_STRIP); + this.drawTriangleStrip(verts); } - drawPoints(points, size, color) { + drawPoints(points: number[], size: number, color: ColorInput) { if (!Array.isArray(points) || points.length < 2) return; - const radius = Math.max(1, Number(size) || 1); + const radius = Math.max(1, size || 1); const rgba = normalizeColor(color); - const verts = []; + const verts: number[] = []; for (let i = 0; i < points.length; i += 2) { - const x = points[i] - radius; - const y = points[i + 1] - radius; + const x = (points[i] ?? 0) - radius; + const y = (points[i + 1] ?? 0) - radius; const w = radius * 2; const h = radius * 2; pushColoredVertex(verts, x, y, rgba); @@ -424,15 +458,15 @@ pushColoredVertex(verts, x + w, y + h, rgba); pushColoredVertex(verts, x, y + h, rgba); } - this._drawColorGeometry(verts, this.gl.TRIANGLES); + this.drawTriangles(verts); } - drawDashedVerticalLine(x, y0, y1, dashLen, gapLen, color, width = 1) { - const dash = Math.max(1, Number(dashLen) || 1); - const gap = Math.max(1, Number(gapLen) || 1); + drawDashedVerticalLine(x: number, y0: number, y1: number, dashLen: number, gapLen: number, color: ColorInput, width = 1) { + const dash = Math.max(1, dashLen || 1); + const gap = Math.max(1, gapLen || 1); const top = Math.min(y0, y1); const bottom = Math.max(y0, y1); - const segments = []; + const segments: number[] = []; for (let y = top; y < bottom; y += dash + gap) { const segEnd = Math.min(bottom, y + dash); segments.push(x, y, x, segEnd); @@ -440,8 +474,8 @@ this.drawSegments(segments, color, width); } - uploadRgbaTexture(name, width, height, data, filter = "linear") { - if (!this.ready || !name || !data) return null; + uploadRgbaTexture(name: string, width: number, height: number, data: ArrayBufferView, filter: "linear" | "nearest" = "linear"): WebGLTexture | null { + if (!this.gl || !name) return null; const gl = this.gl; let entry = this.textures.get(name); if (!entry) { @@ -486,8 +520,8 @@ return entry.texture; } - drawTexture(name, x, y, w, h, alpha = 1, flipY = true) { - if (!this.ready || !name || w <= 0 || h <= 0) return; + drawTexture(name: string, x: number, y: number, w: number, h: number, alpha = 1, flipY = true) { + if (!this.gl || !name || w <= 0 || h <= 0) return; const entry = this.textures.get(name); if (!entry) return; const gl = this.gl; @@ -516,7 +550,7 @@ gl.enableVertexAttribArray(this.textureLoc.uv); gl.vertexAttribPointer(this.textureLoc.uv, 2, gl.FLOAT, false, 16, 8); gl.uniform2f(this.textureLoc.resolution, this.canvas.width, this.canvas.height); - gl.uniform1f(this.textureLoc.alpha, Math.max(0, Math.min(1, Number(alpha) || 0))); + gl.uniform1f(this.textureLoc.alpha, Math.max(0, Math.min(1, alpha || 0))); gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, entry.texture); gl.uniform1i(this.textureLoc.tex, 0); @@ -524,7 +558,7 @@ } } - function createRenderer(canvas, options) { + function createRenderer(canvas: HTMLCanvasElement, options: WebGLContextAttributes = {}) { return new TrxWebGlRenderer(canvas, options); } @@ -532,4 +566,4 @@ global.trxHslToRgba = hslToRgba; global.createTrxWebGlRenderer = createRenderer; global.trxClearCssColorCache = clearCssColorCache; -})(window); +})(window as TrxWebGlGlobals); diff --git a/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/webgl-renderer.test.mjs b/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/webgl-renderer.test.mjs new file mode 100644 index 00000000..72e21ba9 --- /dev/null +++ b/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/webgl-renderer.test.mjs @@ -0,0 +1,33 @@ +// SPDX-FileCopyrightText: 2026 Stan Grams +// +// SPDX-License-Identifier: GPL-2.0-or-later + +import assert from "node:assert/strict"; +import { readFile } from "node:fs/promises"; +import test from "node:test"; +import vm from "node:vm"; + +test("WebGL color helpers normalize CSS and HSL colors", async () => { + const window = {}; + const context = vm.createContext({ + window, + document: { + body: { appendChild() {} }, + createElement: () => ({ style: {} }), + }, + getComputedStyle: () => ({ color: "rgb(0, 0, 0)" }), + Map, + Math, + Number, + String, + parseInt, + }); + const source = await readFile( + new URL("../../assets/web/generated/webgl-renderer.js", import.meta.url), + "utf8", + ); + new vm.Script(source).runInContext(context); + + assert.deepEqual(Array.from(window.trxParseCssColor("#ff800080")), [1, 128 / 255, 0, 128 / 255]); + assert.deepEqual(Array.from(window.trxHslToRgba(0, 100, 50)), [1, 0, 0, 1]); +});