486 lines
18 KiB
JavaScript
486 lines
18 KiB
JavaScript
"use strict";
|
|
(function initTrxWebGl(global) {
|
|
"use strict";
|
|
const cssColorCache = /* @__PURE__ */ new Map();
|
|
let cssColorProbe = null;
|
|
function clearCssColorCache() {
|
|
cssColorCache.clear();
|
|
}
|
|
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 > 1e-4)) 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 = /* @__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;
|
|
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;\nattribute vec4 a_color;\nuniform vec2 u_resolution;\nvarying vec4 v_color;\nvoid 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;\nvarying vec4 v_color;\nvoid main() {\n gl_FragColor = v_color;\n}\n";
|
|
const textureVertexSrc = "attribute vec2 a_pos;\nattribute vec2 a_uv;\nuniform vec2 u_resolution;\nvarying vec2 v_uv;\nvoid 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;\nvarying vec2 v_uv;\nuniform sampler2D u_tex;\nuniform float u_alpha;\nvoid 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;
|
|
if (count > this._colorScratch.length) {
|
|
let newLen = this._colorScratch.length;
|
|
while (newLen < count) newLen *= 2;
|
|
this._colorScratch = new Float32Array(newLen);
|
|
}
|
|
this._colorScratch.set(vertices);
|
|
const view = this._colorScratch.subarray(0, count);
|
|
gl.useProgram(this.colorProgram);
|
|
gl.bindBuffer(gl.ARRAY_BUFFER, this.colorBuffer);
|
|
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;
|
|
global.trxClearCssColorCache = clearCssColorCache;
|
|
})(window);
|