Complete TypeScript frontend migration #22
+69
-56
@@ -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;
|
||||
|
||||
@@ -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"),
|
||||
|
||||
+128
-94
@@ -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<string, Rgba>();
|
||||
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<string, TextureEntry>();
|
||||
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);
|
||||
@@ -0,0 +1,33 @@
|
||||
// SPDX-FileCopyrightText: 2026 Stan Grams <sjg@haxx.space>
|
||||
//
|
||||
// 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]);
|
||||
});
|
||||
Reference in New Issue
Block a user