refactor: convert WebGL renderer to TypeScript
This commit is contained in:
+69
-56
@@ -19,9 +19,9 @@
|
|||||||
return cssColorProbe;
|
return cssColorProbe;
|
||||||
}
|
}
|
||||||
function parseRgbString(value) {
|
function parseRgbString(value) {
|
||||||
const m = /^rgba?\(([^)]+)\)$/.exec(String(value || "").trim());
|
const m = /^rgba?\(([^)]+)\)$/.exec(value.trim());
|
||||||
if (!m) return null;
|
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;
|
if (parts.length < 3) return null;
|
||||||
const r = Number(parts[0]);
|
const r = Number(parts[0]);
|
||||||
const g = Number(parts[1]);
|
const g = Number(parts[1]);
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
function parseHexColor(value) {
|
function parseHexColor(value) {
|
||||||
const raw = String(value || "").trim();
|
const raw = value.trim();
|
||||||
if (!/^#([0-9a-f]{3,8})$/i.test(raw)) return null;
|
if (!/^#([0-9a-f]{3,8})$/i.test(raw)) return null;
|
||||||
let hex = raw.slice(1);
|
let hex = raw.slice(1);
|
||||||
if (hex.length === 3 || hex.length === 4) {
|
if (hex.length === 3 || hex.length === 4) {
|
||||||
@@ -50,8 +50,9 @@
|
|||||||
return [r, g, b, a];
|
return [r, g, b, a];
|
||||||
}
|
}
|
||||||
function parseCssColor(value) {
|
function parseCssColor(value) {
|
||||||
const key = String(value ?? "");
|
const key = value;
|
||||||
if (cssColorCache.has(key)) return cssColorCache.get(key).slice();
|
const cached = cssColorCache.get(key);
|
||||||
|
if (cached) return [...cached];
|
||||||
let parsed = parseHexColor(key) || parseRgbString(key);
|
let parsed = parseHexColor(key) || parseRgbString(key);
|
||||||
if (!parsed) {
|
if (!parsed) {
|
||||||
const probe = ensureCssColorProbe();
|
const probe = ensureCssColorProbe();
|
||||||
@@ -60,13 +61,13 @@
|
|||||||
const computed = getComputedStyle(probe).color;
|
const computed = getComputedStyle(probe).color;
|
||||||
parsed = parseRgbString(computed) || [0, 0, 0, 1];
|
parsed = parseRgbString(computed) || [0, 0, 0, 1];
|
||||||
}
|
}
|
||||||
cssColorCache.set(key, parsed.slice());
|
cssColorCache.set(key, [...parsed]);
|
||||||
return parsed.slice();
|
return [...parsed];
|
||||||
}
|
}
|
||||||
function hslToRgba(h, s, l, a = 1) {
|
function hslToRgba(h, s, l, a = 1) {
|
||||||
const hue = ((Number(h) || 0) % 360 + 360) % 360 / 360;
|
const hue = ((h || 0) % 360 + 360) % 360 / 360;
|
||||||
const sat = Math.max(0, Math.min(1, (Number(s) || 0) / 100));
|
const sat = Math.max(0, Math.min(1, (s || 0) / 100));
|
||||||
const lig = Math.max(0, Math.min(1, (Number(l) || 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 q = lig < 0.5 ? lig * (1 + sat) : lig + sat - lig * sat;
|
||||||
const p = 2 * lig - q;
|
const p = 2 * lig - q;
|
||||||
const hueToRgb = (t) => {
|
const hueToRgb = (t) => {
|
||||||
@@ -81,28 +82,26 @@
|
|||||||
const r = sat === 0 ? lig : hueToRgb(hue + 1 / 3);
|
const r = sat === 0 ? lig : hueToRgb(hue + 1 / 3);
|
||||||
const g = sat === 0 ? lig : hueToRgb(hue);
|
const g = sat === 0 ? lig : hueToRgb(hue);
|
||||||
const b = sat === 0 ? lig : hueToRgb(hue - 1 / 3);
|
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) {
|
function normalizeColor(input, alphaMul = 1) {
|
||||||
let rgba;
|
let rgba;
|
||||||
if (Array.isArray(input)) {
|
if (Array.isArray(input)) {
|
||||||
const arr = input.map((v) => Number(v));
|
const arr = input;
|
||||||
if (arr.length >= 4) {
|
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 {
|
} else {
|
||||||
rgba = [0, 0, 0, 1];
|
rgba = [0, 0, 0, 1];
|
||||||
}
|
}
|
||||||
} else if (typeof input === "string") {
|
} else if (typeof input === "string") {
|
||||||
rgba = parseCssColor(input);
|
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 {
|
} else {
|
||||||
rgba = [0, 0, 0, 1];
|
rgba = [
|
||||||
|
input.r || 0,
|
||||||
|
input.g || 0,
|
||||||
|
input.b || 0,
|
||||||
|
input.a ?? 1
|
||||||
|
];
|
||||||
}
|
}
|
||||||
const out = [
|
const out = [
|
||||||
Math.max(0, Math.min(1, rgba[0])),
|
Math.max(0, Math.min(1, rgba[0])),
|
||||||
@@ -114,6 +113,7 @@
|
|||||||
}
|
}
|
||||||
function compileShader(gl, type, source) {
|
function compileShader(gl, type, source) {
|
||||||
const shader = gl.createShader(type);
|
const shader = gl.createShader(type);
|
||||||
|
if (shader === null) throw new Error("Unable to create WebGL shader");
|
||||||
gl.shaderSource(shader, source);
|
gl.shaderSource(shader, source);
|
||||||
gl.compileShader(shader);
|
gl.compileShader(shader);
|
||||||
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
||||||
@@ -161,17 +161,28 @@
|
|||||||
pushColoredVertex(out, dx2, dy2, rgba);
|
pushColoredVertex(out, dx2, dy2, rgba);
|
||||||
}
|
}
|
||||||
class TrxWebGlRenderer {
|
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 = {}) {
|
constructor(canvas, options = {}) {
|
||||||
this.canvas = canvas;
|
this.canvas = canvas;
|
||||||
this.options = { alpha: true, premultipliedAlpha: false, ...options };
|
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.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;
|
if (!this.ready) return;
|
||||||
const gl = this.gl;
|
const gl = this.gl;
|
||||||
|
if (!gl) return;
|
||||||
gl.disable(gl.DEPTH_TEST);
|
gl.disable(gl.DEPTH_TEST);
|
||||||
gl.disable(gl.CULL_FACE);
|
gl.disable(gl.CULL_FACE);
|
||||||
gl.enable(gl.BLEND);
|
gl.enable(gl.BLEND);
|
||||||
@@ -203,7 +214,7 @@
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
ensureSize(cssWidth, cssHeight, dpr = window.devicePixelRatio || 1) {
|
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 nextW = Math.max(1, Math.round(cssWidth * dpr));
|
||||||
const nextH = Math.max(1, Math.round(cssHeight * dpr));
|
const nextH = Math.max(1, Math.round(cssHeight * dpr));
|
||||||
const changed = this.canvas.width !== nextW || this.canvas.height !== nextH;
|
const changed = this.canvas.width !== nextW || this.canvas.height !== nextH;
|
||||||
@@ -215,20 +226,22 @@
|
|||||||
return changed;
|
return changed;
|
||||||
}
|
}
|
||||||
clear(color) {
|
clear(color) {
|
||||||
if (!this.ready) return;
|
if (!this.gl) return;
|
||||||
const gl = this.gl;
|
const gl = this.gl;
|
||||||
const rgba = normalizeColor(color);
|
const rgba = normalizeColor(color);
|
||||||
gl.clearColor(rgba[0], rgba[1], rgba[2], rgba[3]);
|
gl.clearColor(rgba[0], rgba[1], rgba[2], rgba[3]);
|
||||||
gl.clear(gl.COLOR_BUFFER_BIT);
|
gl.clear(gl.COLOR_BUFFER_BIT);
|
||||||
}
|
}
|
||||||
drawTriangles(vertices) {
|
drawTriangles(vertices) {
|
||||||
|
if (!this.gl) return;
|
||||||
this._drawColorGeometry(vertices, this.gl.TRIANGLES);
|
this._drawColorGeometry(vertices, this.gl.TRIANGLES);
|
||||||
}
|
}
|
||||||
drawTriangleStrip(vertices) {
|
drawTriangleStrip(vertices) {
|
||||||
|
if (!this.gl) return;
|
||||||
this._drawColorGeometry(vertices, this.gl.TRIANGLE_STRIP);
|
this._drawColorGeometry(vertices, this.gl.TRIANGLE_STRIP);
|
||||||
}
|
}
|
||||||
_drawColorGeometry(vertices, mode) {
|
_drawColorGeometry(vertices, mode) {
|
||||||
if (!this.ready || !vertices || vertices.length === 0) return;
|
if (!this.gl || vertices.length === 0) return;
|
||||||
const gl = this.gl;
|
const gl = this.gl;
|
||||||
const count = vertices.length;
|
const count = vertices.length;
|
||||||
if (count > this._colorScratch.length) {
|
if (count > this._colorScratch.length) {
|
||||||
@@ -263,7 +276,7 @@
|
|||||||
pushColoredVertex(v, x, y, rgba);
|
pushColoredVertex(v, x, y, rgba);
|
||||||
pushColoredVertex(v, x + w, y + h, rgba);
|
pushColoredVertex(v, x + w, y + h, rgba);
|
||||||
pushColoredVertex(v, x, 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, y, w, h, colorTL, colorTR, colorBR, colorBL) {
|
||||||
if (w <= 0 || h <= 0) return;
|
if (w <= 0 || h <= 0) return;
|
||||||
@@ -278,62 +291,62 @@
|
|||||||
pushColoredVertex(v, x, y, tl);
|
pushColoredVertex(v, x, y, tl);
|
||||||
pushColoredVertex(v, x + w, y + h, br);
|
pushColoredVertex(v, x + w, y + h, br);
|
||||||
pushColoredVertex(v, x, y + h, bl);
|
pushColoredVertex(v, x, y + h, bl);
|
||||||
this._drawColorGeometry(v, this.gl.TRIANGLES);
|
this.drawTriangles(v);
|
||||||
}
|
}
|
||||||
drawPolyline(points, color, width = 1) {
|
drawPolyline(points, color, width = 1) {
|
||||||
if (!Array.isArray(points) || points.length < 4) return;
|
if (!Array.isArray(points) || points.length < 4) return;
|
||||||
const rgba = normalizeColor(color);
|
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 = [];
|
const verts = [];
|
||||||
for (let i = 0; i < points.length - 2; i += 2) {
|
for (let i = 0; i < points.length - 2; i += 2) {
|
||||||
segmentToQuadVertices(
|
segmentToQuadVertices(
|
||||||
verts,
|
verts,
|
||||||
points[i],
|
points[i] ?? 0,
|
||||||
points[i + 1],
|
points[i + 1] ?? 0,
|
||||||
points[i + 2],
|
points[i + 2] ?? 0,
|
||||||
points[i + 3],
|
points[i + 3] ?? 0,
|
||||||
halfW,
|
halfW,
|
||||||
rgba
|
rgba
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
this._drawColorGeometry(verts, this.gl.TRIANGLES);
|
this.drawTriangles(verts);
|
||||||
}
|
}
|
||||||
drawSegments(segments, color, width = 1) {
|
drawSegments(segments, color, width = 1) {
|
||||||
if (!Array.isArray(segments) || segments.length < 4) return;
|
if (!Array.isArray(segments) || segments.length < 4) return;
|
||||||
const rgba = normalizeColor(color);
|
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 = [];
|
const verts = [];
|
||||||
for (let i = 0; i < segments.length - 3; i += 4) {
|
for (let i = 0; i < segments.length - 3; i += 4) {
|
||||||
segmentToQuadVertices(
|
segmentToQuadVertices(
|
||||||
verts,
|
verts,
|
||||||
segments[i],
|
segments[i] ?? 0,
|
||||||
segments[i + 1],
|
segments[i + 1] ?? 0,
|
||||||
segments[i + 2],
|
segments[i + 2] ?? 0,
|
||||||
segments[i + 3],
|
segments[i + 3] ?? 0,
|
||||||
halfW,
|
halfW,
|
||||||
rgba
|
rgba
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
this._drawColorGeometry(verts, this.gl.TRIANGLES);
|
this.drawTriangles(verts);
|
||||||
}
|
}
|
||||||
drawFilledArea(points, baselineY, color) {
|
drawFilledArea(points, baselineY, color) {
|
||||||
if (!Array.isArray(points) || points.length < 4) return;
|
if (!Array.isArray(points) || points.length < 4) return;
|
||||||
const rgba = normalizeColor(color);
|
const rgba = normalizeColor(color);
|
||||||
const verts = [];
|
const verts = [];
|
||||||
for (let i = 0; i < points.length; i += 2) {
|
for (let i = 0; i < points.length; i += 2) {
|
||||||
pushColoredVertex(verts, points[i], baselineY, rgba);
|
pushColoredVertex(verts, points[i] ?? 0, baselineY, rgba);
|
||||||
pushColoredVertex(verts, points[i], points[i + 1], 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, size, color) {
|
||||||
if (!Array.isArray(points) || points.length < 2) return;
|
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 rgba = normalizeColor(color);
|
||||||
const verts = [];
|
const verts = [];
|
||||||
for (let i = 0; i < points.length; i += 2) {
|
for (let i = 0; i < points.length; i += 2) {
|
||||||
const x = points[i] - radius;
|
const x = (points[i] ?? 0) - radius;
|
||||||
const y = points[i + 1] - radius;
|
const y = (points[i + 1] ?? 0) - radius;
|
||||||
const w = radius * 2;
|
const w = radius * 2;
|
||||||
const h = radius * 2;
|
const h = radius * 2;
|
||||||
pushColoredVertex(verts, x, y, rgba);
|
pushColoredVertex(verts, x, y, rgba);
|
||||||
@@ -343,11 +356,11 @@
|
|||||||
pushColoredVertex(verts, x + w, y + h, rgba);
|
pushColoredVertex(verts, x + w, y + h, rgba);
|
||||||
pushColoredVertex(verts, x, 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) {
|
drawDashedVerticalLine(x, y0, y1, dashLen, gapLen, color, width = 1) {
|
||||||
const dash = Math.max(1, Number(dashLen) || 1);
|
const dash = Math.max(1, dashLen || 1);
|
||||||
const gap = Math.max(1, Number(gapLen) || 1);
|
const gap = Math.max(1, gapLen || 1);
|
||||||
const top = Math.min(y0, y1);
|
const top = Math.min(y0, y1);
|
||||||
const bottom = Math.max(y0, y1);
|
const bottom = Math.max(y0, y1);
|
||||||
const segments = [];
|
const segments = [];
|
||||||
@@ -358,7 +371,7 @@
|
|||||||
this.drawSegments(segments, color, width);
|
this.drawSegments(segments, color, width);
|
||||||
}
|
}
|
||||||
uploadRgbaTexture(name, width, height, data, filter = "linear") {
|
uploadRgbaTexture(name, width, height, data, filter = "linear") {
|
||||||
if (!this.ready || !name || !data) return null;
|
if (!this.gl || !name) return null;
|
||||||
const gl = this.gl;
|
const gl = this.gl;
|
||||||
let entry = this.textures.get(name);
|
let entry = this.textures.get(name);
|
||||||
if (!entry) {
|
if (!entry) {
|
||||||
@@ -403,7 +416,7 @@
|
|||||||
return entry.texture;
|
return entry.texture;
|
||||||
}
|
}
|
||||||
drawTexture(name, x, y, w, h, alpha = 1, flipY = true) {
|
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);
|
const entry = this.textures.get(name);
|
||||||
if (!entry) return;
|
if (!entry) return;
|
||||||
const gl = this.gl;
|
const gl = this.gl;
|
||||||
@@ -468,14 +481,14 @@
|
|||||||
gl.enableVertexAttribArray(this.textureLoc.uv);
|
gl.enableVertexAttribArray(this.textureLoc.uv);
|
||||||
gl.vertexAttribPointer(this.textureLoc.uv, 2, gl.FLOAT, false, 16, 8);
|
gl.vertexAttribPointer(this.textureLoc.uv, 2, gl.FLOAT, false, 16, 8);
|
||||||
gl.uniform2f(this.textureLoc.resolution, this.canvas.width, this.canvas.height);
|
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.activeTexture(gl.TEXTURE0);
|
||||||
gl.bindTexture(gl.TEXTURE_2D, entry.texture);
|
gl.bindTexture(gl.TEXTURE_2D, entry.texture);
|
||||||
gl.uniform1i(this.textureLoc.tex, 0);
|
gl.uniform1i(this.textureLoc.tex, 0);
|
||||||
gl.drawArrays(gl.TRIANGLES, 0, 6);
|
gl.drawArrays(gl.TRIANGLES, 0, 6);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function createRenderer(canvas, options) {
|
function createRenderer(canvas, options = {}) {
|
||||||
return new TrxWebGlRenderer(canvas, options);
|
return new TrxWebGlRenderer(canvas, options);
|
||||||
}
|
}
|
||||||
global.trxParseCssColor = parseCssColor;
|
global.trxParseCssColor = parseCssColor;
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ await build({
|
|||||||
"ui-core": path.join(sourceDir, "ui-core.ts"),
|
"ui-core": path.join(sourceDir, "ui-core.ts"),
|
||||||
"map-core": path.join(sourceDir, "map-core.js"),
|
"map-core": path.join(sourceDir, "map-core.js"),
|
||||||
screenshot: path.join(sourceDir, "screenshot.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"),
|
"leaflet-ais-tracksymbol": path.join(sourceDir, "leaflet-ais-tracksymbol.js"),
|
||||||
ais: path.join(sourceDir, "plugins", "ais.js"),
|
ais: path.join(sourceDir, "plugins", "ais.js"),
|
||||||
aprs: path.join(sourceDir, "plugins", "aprs.js"),
|
aprs: path.join(sourceDir, "plugins", "aprs.js"),
|
||||||
|
|||||||
+128
-94
@@ -2,17 +2,39 @@
|
|||||||
//
|
//
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// 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";
|
"use strict";
|
||||||
|
|
||||||
const cssColorCache = new Map();
|
const cssColorCache = new Map<string, Rgba>();
|
||||||
let cssColorProbe = null;
|
let cssColorProbe: HTMLSpanElement | null = null;
|
||||||
|
|
||||||
function clearCssColorCache() {
|
function clearCssColorCache() {
|
||||||
cssColorCache.clear();
|
cssColorCache.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureCssColorProbe() {
|
function ensureCssColorProbe(): HTMLSpanElement {
|
||||||
if (cssColorProbe) return cssColorProbe;
|
if (cssColorProbe) return cssColorProbe;
|
||||||
const el = document.createElement("span");
|
const el = document.createElement("span");
|
||||||
el.style.position = "absolute";
|
el.style.position = "absolute";
|
||||||
@@ -25,10 +47,10 @@
|
|||||||
return cssColorProbe;
|
return cssColorProbe;
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseRgbString(value) {
|
function parseRgbString(value: string): Rgba | null {
|
||||||
const m = /^rgba?\(([^)]+)\)$/.exec(String(value || "").trim());
|
const m = /^rgba?\(([^)]+)\)$/.exec(value.trim());
|
||||||
if (!m) return null;
|
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;
|
if (parts.length < 3) return null;
|
||||||
const r = Number(parts[0]);
|
const r = Number(parts[0]);
|
||||||
const g = Number(parts[1]);
|
const g = Number(parts[1]);
|
||||||
@@ -43,8 +65,8 @@
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseHexColor(value) {
|
function parseHexColor(value: string): Rgba | null {
|
||||||
const raw = String(value || "").trim();
|
const raw = value.trim();
|
||||||
if (!/^#([0-9a-f]{3,8})$/i.test(raw)) return null;
|
if (!/^#([0-9a-f]{3,8})$/i.test(raw)) return null;
|
||||||
let hex = raw.slice(1);
|
let hex = raw.slice(1);
|
||||||
if (hex.length === 3 || hex.length === 4) {
|
if (hex.length === 3 || hex.length === 4) {
|
||||||
@@ -58,9 +80,10 @@
|
|||||||
return [r, g, b, a];
|
return [r, g, b, a];
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseCssColor(value) {
|
function parseCssColor(value: string): Rgba {
|
||||||
const key = String(value ?? "");
|
const key = value;
|
||||||
if (cssColorCache.has(key)) return cssColorCache.get(key).slice();
|
const cached = cssColorCache.get(key);
|
||||||
|
if (cached) return [...cached];
|
||||||
|
|
||||||
let parsed = parseHexColor(key) || parseRgbString(key);
|
let parsed = parseHexColor(key) || parseRgbString(key);
|
||||||
if (!parsed) {
|
if (!parsed) {
|
||||||
@@ -70,18 +93,18 @@
|
|||||||
const computed = getComputedStyle(probe).color;
|
const computed = getComputedStyle(probe).color;
|
||||||
parsed = parseRgbString(computed) || [0, 0, 0, 1];
|
parsed = parseRgbString(computed) || [0, 0, 0, 1];
|
||||||
}
|
}
|
||||||
cssColorCache.set(key, parsed.slice());
|
cssColorCache.set(key, [...parsed]);
|
||||||
return parsed.slice();
|
return [...parsed];
|
||||||
}
|
}
|
||||||
|
|
||||||
function hslToRgba(h, s, l, a = 1) {
|
function hslToRgba(h: number, s: number, l: number, a = 1): Rgba {
|
||||||
const hue = ((((Number(h) || 0) % 360) + 360) % 360) / 360;
|
const hue = ((((h || 0) % 360) + 360) % 360) / 360;
|
||||||
const sat = Math.max(0, Math.min(1, (Number(s) || 0) / 100));
|
const sat = Math.max(0, Math.min(1, (s || 0) / 100));
|
||||||
const lig = Math.max(0, Math.min(1, (Number(l) || 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 q = lig < 0.5 ? lig * (1 + sat) : lig + sat - lig * sat;
|
||||||
const p = 2 * lig - q;
|
const p = 2 * lig - q;
|
||||||
const hueToRgb = (t) => {
|
const hueToRgb = (t: number): number => {
|
||||||
let tt = t;
|
let tt = t;
|
||||||
if (tt < 0) tt += 1;
|
if (tt < 0) tt += 1;
|
||||||
if (tt > 1) tt -= 1;
|
if (tt > 1) tt -= 1;
|
||||||
@@ -94,31 +117,29 @@
|
|||||||
const r = sat === 0 ? lig : hueToRgb(hue + 1 / 3);
|
const r = sat === 0 ? lig : hueToRgb(hue + 1 / 3);
|
||||||
const g = sat === 0 ? lig : hueToRgb(hue);
|
const g = sat === 0 ? lig : hueToRgb(hue);
|
||||||
const b = sat === 0 ? lig : hueToRgb(hue - 1 / 3);
|
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) {
|
function normalizeColor(input: ColorInput, alphaMul = 1): Rgba {
|
||||||
let rgba;
|
let rgba: Rgba;
|
||||||
if (Array.isArray(input)) {
|
if (Array.isArray(input)) {
|
||||||
const arr = input.map((v) => Number(v));
|
const arr = input;
|
||||||
if (arr.length >= 4) {
|
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 {
|
} else {
|
||||||
rgba = [0, 0, 0, 1];
|
rgba = [0, 0, 0, 1];
|
||||||
}
|
}
|
||||||
} else if (typeof input === "string") {
|
} else if (typeof input === "string") {
|
||||||
rgba = parseCssColor(input);
|
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 {
|
} 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[0])),
|
||||||
Math.max(0, Math.min(1, rgba[1])),
|
Math.max(0, Math.min(1, rgba[1])),
|
||||||
Math.max(0, Math.min(1, rgba[2])),
|
Math.max(0, Math.min(1, rgba[2])),
|
||||||
@@ -127,8 +148,9 @@
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
function compileShader(gl, type, source) {
|
function compileShader(gl: WebGLRenderingContext, type: number, source: string): WebGLShader {
|
||||||
const shader = gl.createShader(type);
|
const shader = gl.createShader(type);
|
||||||
|
if (shader === null) throw new Error("Unable to create WebGL shader");
|
||||||
gl.shaderSource(shader, source);
|
gl.shaderSource(shader, source);
|
||||||
gl.compileShader(shader);
|
gl.compileShader(shader);
|
||||||
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
||||||
@@ -139,7 +161,7 @@
|
|||||||
return shader;
|
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 vs = compileShader(gl, gl.VERTEX_SHADER, vertexSrc);
|
||||||
const fs = compileShader(gl, gl.FRAGMENT_SHADER, fragmentSrc);
|
const fs = compileShader(gl, gl.FRAGMENT_SHADER, fragmentSrc);
|
||||||
const program = gl.createProgram();
|
const program = gl.createProgram();
|
||||||
@@ -156,11 +178,11 @@
|
|||||||
return program;
|
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]);
|
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 dx = x1 - x0;
|
||||||
const dy = y1 - y0;
|
const dy = y1 - y0;
|
||||||
const len = Math.hypot(dx, dy);
|
const len = Math.hypot(dx, dy);
|
||||||
@@ -183,23 +205,33 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
class TrxWebGlRenderer {
|
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.canvas = canvas;
|
||||||
this.options = { alpha: true, premultipliedAlpha: false, ...options };
|
this.options = { alpha: true, premultipliedAlpha: false, ...options };
|
||||||
this.gl =
|
this.gl = (canvas.getContext("webgl", this.options) ||
|
||||||
canvas?.getContext("webgl", this.options) ||
|
canvas.getContext("experimental-webgl", this.options)) as WebGLRenderingContext | null;
|
||||||
canvas?.getContext("experimental-webgl", this.options) ||
|
|
||||||
null;
|
|
||||||
this.ready = !!this.gl;
|
this.ready = !!this.gl;
|
||||||
this.textures = new Map();
|
|
||||||
// Reusable scratch buffers — avoids per-draw-call Float32Array allocation
|
// Reusable scratch buffers — avoids per-draw-call Float32Array allocation
|
||||||
// and lets us use bufferSubData instead of bufferData (no GPU realloc).
|
// 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;
|
if (!this.ready) return;
|
||||||
|
|
||||||
const gl = this.gl;
|
const gl = this.gl;
|
||||||
|
if (!gl) return;
|
||||||
gl.disable(gl.DEPTH_TEST);
|
gl.disable(gl.DEPTH_TEST);
|
||||||
gl.disable(gl.CULL_FACE);
|
gl.disable(gl.CULL_FACE);
|
||||||
gl.enable(gl.BLEND);
|
gl.enable(gl.BLEND);
|
||||||
@@ -268,8 +300,8 @@
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
ensureSize(cssWidth, cssHeight, dpr = (window.devicePixelRatio || 1)) {
|
ensureSize(cssWidth: number, cssHeight: number, dpr = (window.devicePixelRatio || 1)): boolean {
|
||||||
if (!this.ready) return false;
|
if (!this.gl) return false;
|
||||||
const nextW = Math.max(1, Math.round(cssWidth * dpr));
|
const nextW = Math.max(1, Math.round(cssWidth * dpr));
|
||||||
const nextH = Math.max(1, Math.round(cssHeight * dpr));
|
const nextH = Math.max(1, Math.round(cssHeight * dpr));
|
||||||
const changed = this.canvas.width !== nextW || this.canvas.height !== nextH;
|
const changed = this.canvas.width !== nextW || this.canvas.height !== nextH;
|
||||||
@@ -281,24 +313,26 @@
|
|||||||
return changed;
|
return changed;
|
||||||
}
|
}
|
||||||
|
|
||||||
clear(color) {
|
clear(color: ColorInput) {
|
||||||
if (!this.ready) return;
|
if (!this.gl) return;
|
||||||
const gl = this.gl;
|
const gl = this.gl;
|
||||||
const rgba = normalizeColor(color);
|
const rgba = normalizeColor(color);
|
||||||
gl.clearColor(rgba[0], rgba[1], rgba[2], rgba[3]);
|
gl.clearColor(rgba[0], rgba[1], rgba[2], rgba[3]);
|
||||||
gl.clear(gl.COLOR_BUFFER_BIT);
|
gl.clear(gl.COLOR_BUFFER_BIT);
|
||||||
}
|
}
|
||||||
|
|
||||||
drawTriangles(vertices) {
|
drawTriangles(vertices: number[]) {
|
||||||
|
if (!this.gl) return;
|
||||||
this._drawColorGeometry(vertices, this.gl.TRIANGLES);
|
this._drawColorGeometry(vertices, this.gl.TRIANGLES);
|
||||||
}
|
}
|
||||||
|
|
||||||
drawTriangleStrip(vertices) {
|
drawTriangleStrip(vertices: number[]) {
|
||||||
|
if (!this.gl) return;
|
||||||
this._drawColorGeometry(vertices, this.gl.TRIANGLE_STRIP);
|
this._drawColorGeometry(vertices, this.gl.TRIANGLE_STRIP);
|
||||||
}
|
}
|
||||||
|
|
||||||
_drawColorGeometry(vertices, mode) {
|
private _drawColorGeometry(vertices: number[], mode: number) {
|
||||||
if (!this.ready || !vertices || vertices.length === 0) return;
|
if (!this.gl || vertices.length === 0) return;
|
||||||
const gl = this.gl;
|
const gl = this.gl;
|
||||||
const count = vertices.length;
|
const count = vertices.length;
|
||||||
|
|
||||||
@@ -333,88 +367,88 @@
|
|||||||
gl.drawArrays(mode, 0, count / 6);
|
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;
|
if (w <= 0 || h <= 0) return;
|
||||||
const rgba = normalizeColor(color);
|
const rgba = normalizeColor(color);
|
||||||
const v = [];
|
const v: number[] = [];
|
||||||
pushColoredVertex(v, x, y, rgba);
|
pushColoredVertex(v, x, y, rgba);
|
||||||
pushColoredVertex(v, x + w, y, rgba);
|
pushColoredVertex(v, x + w, y, rgba);
|
||||||
pushColoredVertex(v, x + w, y + h, rgba);
|
pushColoredVertex(v, x + w, y + h, rgba);
|
||||||
pushColoredVertex(v, x, y, rgba);
|
pushColoredVertex(v, x, y, rgba);
|
||||||
pushColoredVertex(v, x + w, y + h, rgba);
|
pushColoredVertex(v, x + w, y + h, rgba);
|
||||||
pushColoredVertex(v, x, 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;
|
if (w <= 0 || h <= 0) return;
|
||||||
const tl = normalizeColor(colorTL);
|
const tl = normalizeColor(colorTL);
|
||||||
const tr = normalizeColor(colorTR);
|
const tr = normalizeColor(colorTR);
|
||||||
const br = normalizeColor(colorBR);
|
const br = normalizeColor(colorBR);
|
||||||
const bl = normalizeColor(colorBL);
|
const bl = normalizeColor(colorBL);
|
||||||
const v = [];
|
const v: number[] = [];
|
||||||
pushColoredVertex(v, x, y, tl);
|
pushColoredVertex(v, x, y, tl);
|
||||||
pushColoredVertex(v, x + w, y, tr);
|
pushColoredVertex(v, x + w, y, tr);
|
||||||
pushColoredVertex(v, x + w, y + h, br);
|
pushColoredVertex(v, x + w, y + h, br);
|
||||||
pushColoredVertex(v, x, y, tl);
|
pushColoredVertex(v, x, y, tl);
|
||||||
pushColoredVertex(v, x + w, y + h, br);
|
pushColoredVertex(v, x + w, y + h, br);
|
||||||
pushColoredVertex(v, x, y + h, bl);
|
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;
|
if (!Array.isArray(points) || points.length < 4) return;
|
||||||
const rgba = normalizeColor(color);
|
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 = [];
|
const verts: number[] = [];
|
||||||
for (let i = 0; i < points.length - 2; i += 2) {
|
for (let i = 0; i < points.length - 2; i += 2) {
|
||||||
segmentToQuadVertices(
|
segmentToQuadVertices(
|
||||||
verts,
|
verts,
|
||||||
points[i], points[i + 1],
|
points[i] ?? 0, points[i + 1] ?? 0,
|
||||||
points[i + 2], points[i + 3],
|
points[i + 2] ?? 0, points[i + 3] ?? 0,
|
||||||
halfW,
|
halfW,
|
||||||
rgba,
|
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;
|
if (!Array.isArray(segments) || segments.length < 4) return;
|
||||||
const rgba = normalizeColor(color);
|
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 = [];
|
const verts: number[] = [];
|
||||||
for (let i = 0; i < segments.length - 3; i += 4) {
|
for (let i = 0; i < segments.length - 3; i += 4) {
|
||||||
segmentToQuadVertices(
|
segmentToQuadVertices(
|
||||||
verts,
|
verts,
|
||||||
segments[i], segments[i + 1],
|
segments[i] ?? 0, segments[i + 1] ?? 0,
|
||||||
segments[i + 2], segments[i + 3],
|
segments[i + 2] ?? 0, segments[i + 3] ?? 0,
|
||||||
halfW,
|
halfW,
|
||||||
rgba,
|
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;
|
if (!Array.isArray(points) || points.length < 4) return;
|
||||||
const rgba = normalizeColor(color);
|
const rgba = normalizeColor(color);
|
||||||
const verts = [];
|
const verts: number[] = [];
|
||||||
for (let i = 0; i < points.length; i += 2) {
|
for (let i = 0; i < points.length; i += 2) {
|
||||||
pushColoredVertex(verts, points[i], baselineY, rgba);
|
pushColoredVertex(verts, points[i] ?? 0, baselineY, rgba);
|
||||||
pushColoredVertex(verts, points[i], points[i + 1], 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;
|
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 rgba = normalizeColor(color);
|
||||||
const verts = [];
|
const verts: number[] = [];
|
||||||
for (let i = 0; i < points.length; i += 2) {
|
for (let i = 0; i < points.length; i += 2) {
|
||||||
const x = points[i] - radius;
|
const x = (points[i] ?? 0) - radius;
|
||||||
const y = points[i + 1] - radius;
|
const y = (points[i + 1] ?? 0) - radius;
|
||||||
const w = radius * 2;
|
const w = radius * 2;
|
||||||
const h = radius * 2;
|
const h = radius * 2;
|
||||||
pushColoredVertex(verts, x, y, rgba);
|
pushColoredVertex(verts, x, y, rgba);
|
||||||
@@ -424,15 +458,15 @@
|
|||||||
pushColoredVertex(verts, x + w, y + h, rgba);
|
pushColoredVertex(verts, x + w, y + h, rgba);
|
||||||
pushColoredVertex(verts, x, 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) {
|
drawDashedVerticalLine(x: number, y0: number, y1: number, dashLen: number, gapLen: number, color: ColorInput, width = 1) {
|
||||||
const dash = Math.max(1, Number(dashLen) || 1);
|
const dash = Math.max(1, dashLen || 1);
|
||||||
const gap = Math.max(1, Number(gapLen) || 1);
|
const gap = Math.max(1, gapLen || 1);
|
||||||
const top = Math.min(y0, y1);
|
const top = Math.min(y0, y1);
|
||||||
const bottom = Math.max(y0, y1);
|
const bottom = Math.max(y0, y1);
|
||||||
const segments = [];
|
const segments: number[] = [];
|
||||||
for (let y = top; y < bottom; y += dash + gap) {
|
for (let y = top; y < bottom; y += dash + gap) {
|
||||||
const segEnd = Math.min(bottom, y + dash);
|
const segEnd = Math.min(bottom, y + dash);
|
||||||
segments.push(x, y, x, segEnd);
|
segments.push(x, y, x, segEnd);
|
||||||
@@ -440,8 +474,8 @@
|
|||||||
this.drawSegments(segments, color, width);
|
this.drawSegments(segments, color, width);
|
||||||
}
|
}
|
||||||
|
|
||||||
uploadRgbaTexture(name, width, height, data, filter = "linear") {
|
uploadRgbaTexture(name: string, width: number, height: number, data: ArrayBufferView, filter: "linear" | "nearest" = "linear"): WebGLTexture | null {
|
||||||
if (!this.ready || !name || !data) return null;
|
if (!this.gl || !name) return null;
|
||||||
const gl = this.gl;
|
const gl = this.gl;
|
||||||
let entry = this.textures.get(name);
|
let entry = this.textures.get(name);
|
||||||
if (!entry) {
|
if (!entry) {
|
||||||
@@ -486,8 +520,8 @@
|
|||||||
return entry.texture;
|
return entry.texture;
|
||||||
}
|
}
|
||||||
|
|
||||||
drawTexture(name, x, y, w, h, alpha = 1, flipY = true) {
|
drawTexture(name: string, x: number, y: number, w: number, h: number, 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);
|
const entry = this.textures.get(name);
|
||||||
if (!entry) return;
|
if (!entry) return;
|
||||||
const gl = this.gl;
|
const gl = this.gl;
|
||||||
@@ -516,7 +550,7 @@
|
|||||||
gl.enableVertexAttribArray(this.textureLoc.uv);
|
gl.enableVertexAttribArray(this.textureLoc.uv);
|
||||||
gl.vertexAttribPointer(this.textureLoc.uv, 2, gl.FLOAT, false, 16, 8);
|
gl.vertexAttribPointer(this.textureLoc.uv, 2, gl.FLOAT, false, 16, 8);
|
||||||
gl.uniform2f(this.textureLoc.resolution, this.canvas.width, this.canvas.height);
|
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.activeTexture(gl.TEXTURE0);
|
||||||
gl.bindTexture(gl.TEXTURE_2D, entry.texture);
|
gl.bindTexture(gl.TEXTURE_2D, entry.texture);
|
||||||
gl.uniform1i(this.textureLoc.tex, 0);
|
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);
|
return new TrxWebGlRenderer(canvas, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -532,4 +566,4 @@
|
|||||||
global.trxHslToRgba = hslToRgba;
|
global.trxHslToRgba = hslToRgba;
|
||||||
global.createTrxWebGlRenderer = createRenderer;
|
global.createTrxWebGlRenderer = createRenderer;
|
||||||
global.trxClearCssColorCache = clearCssColorCache;
|
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