(function initTrxWebGl(global) { "use strict"; const cssColorCache = new Map(); let cssColorProbe = null; function ensureCssColorProbe() { if (cssColorProbe) return cssColorProbe; const el = document.createElement("span"); el.style.position = "absolute"; el.style.left = "-9999px"; el.style.top = "-9999px"; el.style.pointerEvents = "none"; el.style.opacity = "0"; document.body.appendChild(el); cssColorProbe = el; return cssColorProbe; } function parseRgbString(value) { const m = /^rgba?\(([^)]+)\)$/.exec(String(value || "").trim()); if (!m) return null; 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]); const b = Number(parts[2]); const a = parts.length > 3 ? Number(parts[3]) : 1; if (![r, g, b, a].every(Number.isFinite)) return null; return [ Math.max(0, Math.min(1, r / 255)), Math.max(0, Math.min(1, g / 255)), Math.max(0, Math.min(1, b / 255)), Math.max(0, Math.min(1, a)), ]; } function parseHexColor(value) { const raw = String(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) { hex = hex.split("").map((ch) => ch + ch).join(""); } if (!(hex.length === 6 || hex.length === 8)) return null; const r = parseInt(hex.slice(0, 2), 16) / 255; const g = parseInt(hex.slice(2, 4), 16) / 255; const b = parseInt(hex.slice(4, 6), 16) / 255; const a = hex.length === 8 ? parseInt(hex.slice(6, 8), 16) / 255 : 1; return [r, g, b, a]; } function parseCssColor(value) { const key = String(value ?? ""); if (cssColorCache.has(key)) return cssColorCache.get(key).slice(); let parsed = parseHexColor(key) || parseRgbString(key); if (!parsed) { const probe = ensureCssColorProbe(); probe.style.color = ""; probe.style.color = key; const computed = getComputedStyle(probe).color; parsed = parseRgbString(computed) || [0, 0, 0, 1]; } cssColorCache.set(key, parsed.slice()); return parsed.slice(); } 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 q = lig < 0.5 ? lig * (1 + sat) : lig + sat - lig * sat; const p = 2 * lig - q; const hueToRgb = (t) => { let tt = t; if (tt < 0) tt += 1; if (tt > 1) tt -= 1; if (tt < 1 / 6) return p + (q - p) * 6 * tt; if (tt < 1 / 2) return q; if (tt < 2 / 3) return p + (q - p) * (2 / 3 - tt) * 6; return p; }; 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)))]; } function normalizeColor(input, alphaMul = 1) { let rgba; if (Array.isArray(input)) { const arr = input.map((v) => Number(v)); if (arr.length >= 4) { rgba = [arr[0], arr[1], arr[2], arr[3]]; } 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]; } const out = [ Math.max(0, Math.min(1, rgba[0])), Math.max(0, Math.min(1, rgba[1])), Math.max(0, Math.min(1, rgba[2])), Math.max(0, Math.min(1, rgba[3] * alphaMul)), ]; return out; } function compileShader(gl, type, source) { const shader = gl.createShader(type); gl.shaderSource(shader, source); gl.compileShader(shader); if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { const log = gl.getShaderInfoLog(shader) || "shader compile error"; gl.deleteShader(shader); throw new Error(log); } return shader; } function createProgram(gl, vertexSrc, fragmentSrc) { const vs = compileShader(gl, gl.VERTEX_SHADER, vertexSrc); const fs = compileShader(gl, gl.FRAGMENT_SHADER, fragmentSrc); const program = gl.createProgram(); gl.attachShader(program, vs); gl.attachShader(program, fs); gl.linkProgram(program); gl.deleteShader(vs); gl.deleteShader(fs); if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { const log = gl.getProgramInfoLog(program) || "program link error"; gl.deleteProgram(program); throw new Error(log); } return program; } function pushColoredVertex(target, x, y, rgba) { target.push(x, y, rgba[0], rgba[1], rgba[2], rgba[3]); } function segmentToQuadVertices(out, x0, y0, x1, y1, halfW, rgba) { const dx = x1 - x0; const dy = y1 - y0; const len = Math.hypot(dx, dy); if (!(len > 0.0001)) return; const nx = (-dy / len) * halfW; const ny = (dx / len) * halfW; const ax = x0 - nx, ay = y0 - ny; const bx = x0 + nx, by = y0 + ny; const cx = x1 + nx, cy = y1 + ny; const dx2 = x1 - nx, dy2 = y1 - ny; pushColoredVertex(out, ax, ay, rgba); pushColoredVertex(out, bx, by, rgba); pushColoredVertex(out, cx, cy, rgba); pushColoredVertex(out, ax, ay, rgba); pushColoredVertex(out, cx, cy, rgba); pushColoredVertex(out, dx2, dy2, rgba); } class TrxWebGlRenderer { 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.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; gl.disable(gl.DEPTH_TEST); gl.disable(gl.CULL_FACE); gl.enable(gl.BLEND); gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); const colorVertexSrc = "attribute vec2 a_pos;\n" + "attribute vec4 a_color;\n" + "uniform vec2 u_resolution;\n" + "varying vec4 v_color;\n" + "void main() {\n" + " vec2 zeroToOne = a_pos / u_resolution;\n" + " vec2 clip = zeroToOne * 2.0 - 1.0;\n" + " gl_Position = vec4(clip * vec2(1.0, -1.0), 0.0, 1.0);\n" + " v_color = a_color;\n" + "}\n"; const colorFragmentSrc = "precision mediump float;\n" + "varying vec4 v_color;\n" + "void main() {\n" + " gl_FragColor = v_color;\n" + "}\n"; const textureVertexSrc = "attribute vec2 a_pos;\n" + "attribute vec2 a_uv;\n" + "uniform vec2 u_resolution;\n" + "varying vec2 v_uv;\n" + "void main() {\n" + " vec2 zeroToOne = a_pos / u_resolution;\n" + " vec2 clip = zeroToOne * 2.0 - 1.0;\n" + " gl_Position = vec4(clip * vec2(1.0, -1.0), 0.0, 1.0);\n" + " v_uv = a_uv;\n" + "}\n"; const textureFragmentSrc = "precision mediump float;\n" + "varying vec2 v_uv;\n" + "uniform sampler2D u_tex;\n" + "uniform float u_alpha;\n" + "void main() {\n" + " vec4 c = texture2D(u_tex, v_uv);\n" + " gl_FragColor = vec4(c.rgb, c.a * u_alpha);\n" + "}\n"; this.colorProgram = createProgram(gl, colorVertexSrc, colorFragmentSrc); this.colorBuffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, this.colorBuffer); gl.bufferData(gl.ARRAY_BUFFER, this._colorScratch, gl.DYNAMIC_DRAW); this._colorGpuSize = this._colorScratch.length; this.colorLoc = { pos: gl.getAttribLocation(this.colorProgram, "a_pos"), color: gl.getAttribLocation(this.colorProgram, "a_color"), resolution: gl.getUniformLocation(this.colorProgram, "u_resolution"), }; this.textureProgram = createProgram(gl, textureVertexSrc, textureFragmentSrc); this.textureBuffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, this.textureBuffer); gl.bufferData(gl.ARRAY_BUFFER, this._texScratch, gl.DYNAMIC_DRAW); this.textureLoc = { pos: gl.getAttribLocation(this.textureProgram, "a_pos"), uv: gl.getAttribLocation(this.textureProgram, "a_uv"), resolution: gl.getUniformLocation(this.textureProgram, "u_resolution"), alpha: gl.getUniformLocation(this.textureProgram, "u_alpha"), tex: gl.getUniformLocation(this.textureProgram, "u_tex"), }; } ensureSize(cssWidth, cssHeight, dpr = (window.devicePixelRatio || 1)) { if (!this.ready) 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; if (changed) { this.canvas.width = nextW; this.canvas.height = nextH; } this.gl.viewport(0, 0, this.canvas.width, this.canvas.height); return changed; } clear(color) { if (!this.ready) 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) { this._drawColorGeometry(vertices, this.gl.TRIANGLES); } drawTriangleStrip(vertices) { this._drawColorGeometry(vertices, this.gl.TRIANGLE_STRIP); } _drawColorGeometry(vertices, mode) { if (!this.ready || !vertices || vertices.length === 0) return; const gl = this.gl; const count = vertices.length; // Grow scratch buffer if needed (doubles each time to amortise copies). if (count > this._colorScratch.length) { let newLen = this._colorScratch.length; while (newLen < count) newLen *= 2; this._colorScratch = new Float32Array(newLen); } // Copy into scratch (set() is a fast typed memcpy; avoids new allocation). this._colorScratch.set(vertices); const view = this._colorScratch.subarray(0, count); gl.useProgram(this.colorProgram); gl.bindBuffer(gl.ARRAY_BUFFER, this.colorBuffer); // Only reallocate the GPU buffer when it is too small; otherwise use // bufferSubData which avoids a GPU reallocation (Safari is sensitive to this). if (count > this._colorGpuSize) { gl.bufferData(gl.ARRAY_BUFFER, this._colorScratch, gl.DYNAMIC_DRAW); this._colorGpuSize = this._colorScratch.length; } else { gl.bufferSubData(gl.ARRAY_BUFFER, 0, view); } gl.enableVertexAttribArray(this.colorLoc.pos); gl.vertexAttribPointer(this.colorLoc.pos, 2, gl.FLOAT, false, 24, 0); gl.enableVertexAttribArray(this.colorLoc.color); gl.vertexAttribPointer(this.colorLoc.color, 4, gl.FLOAT, false, 24, 8); gl.uniform2f(this.colorLoc.resolution, this.canvas.width, this.canvas.height); gl.drawArrays(mode, 0, count / 6); } fillRect(x, y, w, h, color) { if (w <= 0 || h <= 0) return; const rgba = normalizeColor(color); const v = []; 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); } fillGradientRect(x, y, w, h, colorTL, colorTR, colorBR, colorBL) { if (w <= 0 || h <= 0) return; const tl = normalizeColor(colorTL); const tr = normalizeColor(colorTR); const br = normalizeColor(colorBR); const bl = normalizeColor(colorBL); const v = []; 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); } 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 verts = []; for (let i = 0; i < points.length - 2; i += 2) { segmentToQuadVertices( verts, points[i], points[i + 1], points[i + 2], points[i + 3], halfW, rgba, ); } this._drawColorGeometry(verts, this.gl.TRIANGLES); } 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 verts = []; for (let i = 0; i < segments.length - 3; i += 4) { segmentToQuadVertices( verts, segments[i], segments[i + 1], segments[i + 2], segments[i + 3], halfW, rgba, ); } this._drawColorGeometry(verts, this.gl.TRIANGLES); } 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); } this._drawColorGeometry(verts, this.gl.TRIANGLE_STRIP); } drawPoints(points, size, color) { if (!Array.isArray(points) || points.length < 2) return; const radius = Math.max(1, Number(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 w = radius * 2; const h = radius * 2; pushColoredVertex(verts, x, y, rgba); pushColoredVertex(verts, x + w, y, rgba); pushColoredVertex(verts, x + w, y + h, rgba); pushColoredVertex(verts, x, y, rgba); pushColoredVertex(verts, x + w, y + h, rgba); pushColoredVertex(verts, x, y + h, rgba); } this._drawColorGeometry(verts, this.gl.TRIANGLES); } 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 top = Math.min(y0, y1); const bottom = Math.max(y0, y1); const segments = []; for (let y = top; y < bottom; y += dash + gap) { const segEnd = Math.min(bottom, y + dash); segments.push(x, y, x, segEnd); } this.drawSegments(segments, color, width); } uploadRgbaTexture(name, width, height, data, filter = "linear") { if (!this.ready || !name || !data) return null; const gl = this.gl; let entry = this.textures.get(name); if (!entry) { const texture = gl.createTexture(); entry = { texture, width: 0, height: 0 }; this.textures.set(name, entry); } gl.bindTexture(gl.TEXTURE_2D, entry.texture); gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1); const mode = filter === "nearest" ? gl.NEAREST : gl.LINEAR; gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, mode); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, mode); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); if (entry.width !== width || entry.height !== height) { gl.texImage2D( gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, data, ); entry.width = width; entry.height = height; } else { gl.texSubImage2D( gl.TEXTURE_2D, 0, 0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, data, ); } return entry.texture; } drawTexture(name, x, y, w, h, alpha = 1, flipY = true) { if (!this.ready || !name || w <= 0 || h <= 0) return; const entry = this.textures.get(name); if (!entry) return; const gl = this.gl; const s = this._texScratch; const x2 = x + w, y2 = y + h; if (flipY) { s[0]=x; s[1]=y; s[2]=0; s[3]=1; s[4]=x2; s[5]=y; s[6]=1; s[7]=1; s[8]=x2; s[9]=y2; s[10]=1;s[11]=0; s[12]=x; s[13]=y; s[14]=0;s[15]=1; s[16]=x2;s[17]=y2;s[18]=1;s[19]=0; s[20]=x; s[21]=y2;s[22]=0;s[23]=0; } else { s[0]=x; s[1]=y; s[2]=0; s[3]=0; s[4]=x2; s[5]=y; s[6]=1; s[7]=0; s[8]=x2; s[9]=y2; s[10]=1;s[11]=1; s[12]=x; s[13]=y; s[14]=0;s[15]=0; s[16]=x2;s[17]=y2;s[18]=1;s[19]=1; s[20]=x; s[21]=y2;s[22]=0;s[23]=1; } gl.useProgram(this.textureProgram); gl.bindBuffer(gl.ARRAY_BUFFER, this.textureBuffer); gl.bufferSubData(gl.ARRAY_BUFFER, 0, s); gl.enableVertexAttribArray(this.textureLoc.pos); gl.vertexAttribPointer(this.textureLoc.pos, 2, gl.FLOAT, false, 16, 0); 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.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) { return new TrxWebGlRenderer(canvas, options); } global.trxParseCssColor = parseCssColor; global.trxHslToRgba = hslToRgba; global.createTrxWebGlRenderer = createRenderer; })(window);