// ==UserScript== // @name 编程猫 Canvas→WebGL 转译加速器 // @namespace https://gitee.com/shuiyu1123/codemao-webgl-engine // @version 2.0.0 // @description 把编程猫作品的 Canvas 2D 渲染实时转译为 WebGL,调用 GPU 加速 // @author codemao-webgl-engine // @match https://player.codemao.cn/* // @match https://shequ.codemao.cn/* // @match https://kitten.codemao.cn/* // @match https://ide.codemao.cn/* // @run-at document-start // @grant none // @license MIT // ==/UserScript== (function () { 'use strict'; // ================================================================ // 核心:Canvas 2D → WebGL 转译器 // 拦截 getContext('2d'),返回一个 Proxy 对象 // 所有 Canvas 2D API 调用内部转为 WebGL 操作 // ================================================================ const TAG = '🐱[Canvas→WebGL]'; const log = (...a) => console.log(TAG, ...a); // ------------------------------------------------ // WebGL 渲染核心 // ------------------------------------------------ class GLRenderer { constructor(canvas) { this.canvas = canvas; this.gl = canvas.getContext('webgl2', { alpha: true, antialias: false, premultipliedAlpha: true, preserveDrawingBuffer: false, powerPreference: 'high-performance', desynchronized: true, failIfMajorPerformanceCaveat: false }) || canvas.getContext('webgl', { alpha: true, antialias: false, powerPreference: 'high-performance', desynchronized: true, failIfMajorPerformanceCaveat: false }); if (!this.gl) { throw new Error('WebGL not available'); } this.gl.enable(this.gl.BLEND); this.gl.blendFunc(this.gl.SRC_ALPHA, this.gl.ONE_MINUS_SRC_ALPHA); // 批量渲染缓冲 this.maxQuads = 4096; this.vertexData = new Float32Array(this.maxQuads * 6 * 9); // 6 verts/quad, 9 floats/vert this.quadCount = 0; this.currentTexture = null; this.textureSlot = 0; this.drawCalls = 0; // 变换矩阵栈 this.transformStack = []; this.matrix = new Float32Array([1, 0, 0, 0, 1, 0, 0, 0, 1]); // 投影矩阵 this._updateProjection(); // 纹理缓存 this.textureCache = new Map(); this.textureSlots = new Array(8).fill(null); // 初始化着色器和缓冲 this._initShaders(); this._initBuffers(); // 离屏 Canvas 用于文字渲染 this._textCanvas = document.createElement('canvas'); this._textCtx = this._textCanvas.getContext('2d'); log('✅ WebGL 渲染器初始化成功'); } _updateProjection() { const w = this.canvas.width; const h = this.canvas.height; this.projection = new Float32Array([2 / w, 0, 0, 0, -2 / h, 0, -1, 1, 1]); } _initShaders() { const gl = this.gl; const vsSrc = ` attribute vec2 a_pos; attribute vec2 a_uv; attribute vec4 a_color; attribute float a_texId; uniform mat3 u_proj; varying vec2 v_uv; varying vec4 v_color; varying float v_texId; void main() { vec3 p = u_proj * vec3(a_pos, 1.0); gl_Position = vec4(p.xy, 0.0, 1.0); v_uv = a_uv; v_color = a_color; v_texId = a_texId; } `; const fsSrc = ` precision mediump float; varying vec2 v_uv; varying vec4 v_color; varying float v_texId; uniform sampler2D u_tex0; uniform sampler2D u_tex1; uniform sampler2D u_tex2; uniform sampler2D u_tex3; uniform sampler2D u_tex4; uniform sampler2D u_tex5; uniform sampler2D u_tex6; uniform sampler2D u_tex7; uniform bool u_useTex; void main() { if (u_useTex) { vec4 tc; int i = int(v_texId); if (i==0) tc = texture2D(u_tex0, v_uv); else if (i==1) tc = texture2D(u_tex1, v_uv); else if (i==2) tc = texture2D(u_tex2, v_uv); else if (i==3) tc = texture2D(u_tex3, v_uv); else if (i==4) tc = texture2D(u_tex4, v_uv); else if (i==5) tc = texture2D(u_tex5, v_uv); else if (i==6) tc = texture2D(u_tex6, v_uv); else tc = texture2D(u_tex7, v_uv); gl_FragColor = tc * v_color; } else { gl_FragColor = v_color; } if (gl_FragColor.a < 0.004) discard; } `; const compile = (src, type) => { const s = gl.createShader(type); gl.shaderSource(s, src); gl.compileShader(s); if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) { console.error(gl.getShaderInfoLog(s)); return null; } return s; }; const vs = compile(vsSrc, gl.VERTEX_SHADER); const fs = compile(fsSrc, gl.FRAGMENT_SHADER); this.program = gl.createProgram(); gl.attachShader(this.program, vs); gl.attachShader(this.program, fs); gl.linkProgram(this.program); if (!gl.getProgramParameter(this.program, gl.LINK_STATUS)) { console.error(gl.getProgramInfoLog(this.program)); } gl.useProgram(this.program); // 获取 locations this.loc = { a_pos: gl.getAttribLocation(this.program, 'a_pos'), a_uv: gl.getAttribLocation(this.program, 'a_uv'), a_color: gl.getAttribLocation(this.program, 'a_color'), a_texId: gl.getAttribLocation(this.program, 'a_texId'), u_proj: gl.getUniformLocation(this.program, 'u_proj'), u_useTex: gl.getUniformLocation(this.program, 'u_useTex'), u_tex: [] }; for (let i = 0; i < 8; i++) { this.loc.u_tex.push(gl.getUniformLocation(this.program, `u_tex${i}`)); } } _initBuffers() { const gl = this.gl; this.vbo = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, this.vbo); gl.bufferData(gl.ARRAY_BUFFER, this.vertexData.byteLength, gl.DYNAMIC_DRAW); const stride = 9 * 4; gl.enableVertexAttribArray(this.loc.a_pos); gl.vertexAttribPointer(this.loc.a_pos, 2, gl.FLOAT, false, stride, 0); gl.enableVertexAttribArray(this.loc.a_uv); gl.vertexAttribPointer(this.loc.a_uv, 2, gl.FLOAT, false, stride, 8); gl.enableVertexAttribArray(this.loc.a_color); gl.vertexAttribPointer(this.loc.a_color, 4, gl.FLOAT, false, stride, 16); if (this.loc.a_texId >= 0) { gl.enableVertexAttribArray(this.loc.a_texId); gl.vertexAttribPointer(this.loc.a_texId, 1, gl.FLOAT, false, stride, 32); } } resize(w, h) { this.canvas.width = w; this.canvas.height = h; this.gl.viewport(0, 0, w, h); this._updateProjection(); } clear(r = 0, g = 0, b = 0, a = 0) { const gl = this.gl; gl.clearColor(r, g, b, a); gl.clear(gl.COLOR_BUFFER_BIT); } // 变换操作 save() { this.transformStack.push(new Float32Array(this.matrix)); } restore() { if (this.transformStack.length) { this.matrix.set(this.transformStack.pop()); } } translate(tx, ty) { const m = this.matrix; m[6] += m[0] * tx + m[3] * ty; m[7] += m[1] * tx + m[4] * ty; } rotate(angle) { const c = Math.cos(angle), s = Math.sin(angle); const m = this.matrix; const [a, b, d, e] = [m[0], m[1], m[3], m[4]]; m[0] = a * c + d * s; m[1] = b * c + e * s; m[3] = d * c - a * s; m[4] = e * c - b * s; } scale(sx, sy) { sy = sy ?? sx; const m = this.matrix; m[0] *= sx; m[1] *= sx; m[3] *= sy; m[4] *= sy; } transform(a, b, c, d, e, f) { const m = this.matrix; const [m0, m1, m3, m4, m6, m7] = [m[0], m[1], m[3], m[4], m[6], m[7]]; m[0] = m0 * a + m3 * b; m[1] = m1 * a + m4 * b; m[3] = m0 * c + m3 * d; m[4] = m1 * c + m4 * d; m[6] = m0 * e + m3 * f + m6; m[7] = m1 * e + m4 * f + m7; } setTransform(a, b, c, d, e, f) { this.matrix.set([a, b, 0, c, d, 0, e, f, 1]); } resetTransform() { this.matrix.set([1, 0, 0, 0, 1, 0, 0, 0, 1]); } // 生成四边形顶点 _makeQuad(x, y, w, h, u0 = 0, v0 = 0, u1 = 1, v1 = 1, r = 1, g = 1, b = 1, a = 1) { const m = this.matrix; const p = this.projection; const corners = [ [x, y], [x + w, y], [x, y + h], [x + w, y], [x + w, y + h], [x, y + h] ]; const uvs = [ [u0, v0], [u1, v0], [u0, v1], [u1, v0], [u1, v1], [u0, v1] ]; const verts = new Float32Array(54); // 6 * 9 for (let i = 0; i < 6; i++) { const [cx, cy] = corners[i]; const wx = m[0] * cx + m[3] * cy + m[6]; const wy = m[1] * cx + m[4] * cy + m[7]; const off = i * 9; verts[off] = p[0] * wx + p[6]; verts[off + 1] = p[4] * wy + p[7]; verts[off + 2] = uvs[i][0]; verts[off + 3] = uvs[i][1]; verts[off + 4] = r; verts[off + 5] = g; verts[off + 6] = b; verts[off + 7] = a; verts[off + 8] = 0; } return verts; } // 添加四边形到批次 addQuad(verts, texture = null) { if (this.quadCount >= this.maxQuads || (texture && texture !== this.currentTexture && this.textureSlot >= 7)) { this.flush(); } if (texture) { let slot = this.textureSlots.indexOf(texture); if (slot === -1) { slot = this.textureSlot++; this.textureSlots[slot] = texture; } for (let i = 0; i < 6; i++) { verts[i * 9 + 8] = slot; } } const offset = this.quadCount * 54; this.vertexData.set(verts, offset); this.quadCount++; this.currentTexture = texture; } // 提交绘制 flush() { if (this.quadCount === 0) return; const gl = this.gl; gl.useProgram(this.program); gl.bindBuffer(gl.ARRAY_BUFFER, this.vbo); gl.bufferSubData(gl.ARRAY_BUFFER, 0, this.vertexData.subarray(0, this.quadCount * 54)); gl.uniformMatrix3fv(this.loc.u_proj, false, this.projection); const useTex = this.textureSlot > 0; gl.uniform1i(this.loc.u_useTex, useTex); if (useTex) { for (let i = 0; i < this.textureSlot; i++) { gl.activeTexture(gl.TEXTURE0 + i); gl.bindTexture(gl.TEXTURE_2D, this.textureSlots[i]); gl.uniform1i(this.loc.u_tex[i], i); } } const stride = 9 * 4; gl.vertexAttribPointer(this.loc.a_pos, 2, gl.FLOAT, false, stride, 0); gl.vertexAttribPointer(this.loc.a_uv, 2, gl.FLOAT, false, stride, 8); gl.vertexAttribPointer(this.loc.a_color, 4, gl.FLOAT, false, stride, 16); if (this.loc.a_texId >= 0) gl.vertexAttribPointer(this.loc.a_texId, 1, gl.FLOAT, false, stride, 32); gl.drawArrays(gl.TRIANGLES, 0, this.quadCount * 6); this.drawCalls++; this.quadCount = 0; this.textureSlot = 0; this.textureSlots.fill(null); this.currentTexture = null; } // 获取或创建纹理 getTexture(source) { if (this.textureCache.has(source)) { return this.textureCache.get(source); } const gl = this.gl; const tex = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, tex); 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); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); try { gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, source); } catch (e) { // source 可能还没加载完 gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array([255, 255, 255, 255])); if (source.complete === false || source.readyState < 2) { const onload = () => { gl.bindTexture(gl.TEXTURE_2D, tex); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, source); source.removeEventListener('load', onload); }; source.addEventListener('load', onload); } } this.textureCache.set(source, tex); return tex; } // 从颜色值创建 1x1 纯色纹理 colorTex(r, g, b, a) { const key = `${r},${g},${b},${a}`; if (this.textureCache.has(key)) return this.textureCache.get(key); const gl = this.gl; const tex = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, tex); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array([r * 255 | 0, g * 255 | 0, b * 255 | 0, a * 255 | 0])); this.textureCache.set(key, tex); return tex; } beginFrame() { this.quadCount = 0; this.textureSlot = 0; this.textureSlots.fill(null); this.drawCalls = 0; this.transformStack.length = 0; this.matrix.set([1, 0, 0, 0, 1, 0, 0, 0, 1]); } endFrame() { this.flush(); } } // ------------------------------------------------ // 颜色解析 // ------------------------------------------------ function parseColor(color) { if (!color) return { r: 0, g: 0, b: 0, a: 1 }; if (typeof color !== 'string') return { r: 0, g: 0, b: 0, a: 1 }; if (color.startsWith('#')) { let h = color.slice(1); if (h.length === 3) h = h[0] + h[0] + h[1] + h[1] + h[2] + h[2]; return { r: parseInt(h.substring(0, 2), 16) / 255, g: parseInt(h.substring(2, 4), 16) / 255, b: parseInt(h.substring(4, 6), 16) / 255, a: h.length >= 8 ? parseInt(h.substring(6, 8), 16) / 255 : 1 }; } const m = color.match(/([\d.]+)/g); if (m) { return { r: parseInt(m[0]) / 255, g: parseInt(m[1]) / 255, b: parseInt(m[2]) / 255, a: m[3] !== undefined ? parseFloat(m[3]) : 1 }; } return { r: 0, g: 0, b: 0, a: 1 }; } // ------------------------------------------------ // Canvas 2D API 代理(转译层) // ------------------------------------------------ class Canvas2DProxy { constructor(canvas, renderer) { this._canvas = canvas; this._gl = renderer; this._state = { fillStyle: '#000000', strokeStyle: '#000000', lineWidth: 1, globalAlpha: 1, globalCompositeOperation: 'source-over', font: '10px sans-serif', textAlign: 'start', textBaseline: 'alphabetic', shadowBlur: 0, shadowColor: 'rgba(0,0,0,0)', shadowOffsetX: 0, shadowOffsetY: 0, imageSmoothingEnabled: true, lineCap: 'butt', lineJoin: 'miter', miterLimit: 10, lineDashOffset: 0 }; this._path = []; this._currentPath = []; } get canvas() { return this._canvas; } // 属性代理 get fillStyle() { return this._state.fillStyle; } set fillStyle(v) { this._state.fillStyle = v; } get strokeStyle() { return this._state.strokeStyle; } set strokeStyle(v) { this._state.strokeStyle = v; } get lineWidth() { return this._state.lineWidth; } set lineWidth(v) { this._state.lineWidth = v; } get globalAlpha() { return this._state.globalAlpha; } set globalAlpha(v) { this._state.globalAlpha = v; } get globalCompositeOperation() { return this._state.globalCompositeOperation; } set globalCompositeOperation(v) { this._state.globalCompositeOperation = v; } get font() { return this._state.font; } set font(v) { this._state.font = v; } get textAlign() { return this._state.textAlign; } set textAlign(v) { this._state.textAlign = v; } get textBaseline() { return this._state.textBaseline; } set textBaseline(v) { this._state.textBaseline = v; } get shadowBlur() { return this._state.shadowBlur; } set shadowBlur(v) { this._state.shadowBlur = v; } get shadowColor() { return this._state.shadowColor; } set shadowColor(v) { this._state.shadowColor = v; } get shadowOffsetX() { return this._state.shadowOffsetX; } set shadowOffsetX(v) { this._state.shadowOffsetX = v; } get shadowOffsetY() { return this._state.shadowOffsetY; } set shadowOffsetY(v) { this._state.shadowOffsetY = v; } get imageSmoothingEnabled() { return this._state.imageSmoothingEnabled; } set imageSmoothingEnabled(v) { this._state.imageSmoothingEnabled = v; } get lineCap() { return this._state.lineCap; } set lineCap(v) { this._state.lineCap = v; } get lineJoin() { return this._state.lineJoin; } set lineJoin(v) { this._state.lineJoin = v; } get miterLimit() { return this._state.miterLimit; } set miterLimit(v) { this._state.miterLimit = v; } get lineDashOffset() { return this._state.lineDashOffset; } set lineDashOffset(v) { this._state.lineDashOffset = v; } // 变换 save() { this._gl.save(); } restore() { this._gl.restore(); } translate(x, y) { this._gl.translate(x, y); } rotate(a) { this._gl.rotate(a); } scale(x, y) { this._gl.scale(x, y); } transform(a, b, c, d, e, f) { this._gl.transform(a, b, c, d, e, f); } setTransform(a, b, c, d, e, f) { this._gl.setTransform(a, b, c, d, e, f); } resetTransform() { this._gl.resetTransform(); } // 矩形操作 → 直接画四边形 fillRect(x, y, w, h) { const c = parseColor(this._state.fillStyle); const a = c.a * this._state.globalAlpha; const verts = this._gl._makeQuad(x, y, w, h, 0, 0, 0, 0, c.r, c.g, c.b, a); this._gl.addQuad(verts); } strokeRect(x, y, w, h) { const c = parseColor(this._state.strokeStyle); const a = c.a * this._state.globalAlpha; const lw = this._state.lineWidth; // 4 条边 this._fillRectRaw(x, y, w, lw, c.r, c.g, c.b, a); this._fillRectRaw(x, y + h - lw, w, lw, c.r, c.g, c.b, a); this._fillRectRaw(x, y, lw, h, c.r, c.g, c.b, a); this._fillRectRaw(x + w - lw, y, lw, h, c.r, c.g, c.b, a); } clearRect(x, y, w, h) { // 用 destination-out 混合清除 const gl = this._gl.gl; const oldBlend = gl.getParameter(gl.BLEND_DST_RGB); gl.blendFunc(gl.ZERO, gl.ONE_MINUS_SRC_ALPHA); const verts = this._gl._makeQuad(x, y, w, h, 0, 0, 0, 0, 0, 0, 0, 1); this._gl.addQuad(verts); this._gl.flush(); gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); } _fillRectRaw(x, y, w, h, r, g, b, a) { const verts = this._gl._makeQuad(x, y, w, h, 0, 0, 0, 0, r, g, b, a); this._gl.addQuad(verts); } // 图像绘制 drawImage(image, ...args) { let sx, sy, sw, sh, dx, dy, dw, dh; const iw = image.naturalWidth || image.width || image.videoWidth; const ih = image.naturalHeight || image.height || image.videoHeight; if (args.length === 2) { [dx, dy] = args; sx = 0; sy = 0; sw = iw; sh = ih; dw = sw; dh = sh; } else if (args.length === 4) { [dx, dy, dw, dh] = args; sx = 0; sy = 0; sw = iw; sh = ih; } else { [sx, sy, sw, sh, dx, dy, dw, dh] = args; } try { const tex = this._gl.getTexture(image); const verts = this._gl._makeQuad( dx, dy, dw, dh, sx / iw, sy / ih, (sx + sw) / iw, (sy + sh) / ih, 1, 1, 1, this._state.globalAlpha ); this._gl.addQuad(verts, tex); } catch (e) { // 纹理创建失败,静默忽略 } } // 路径操作 beginPath() { this._currentPath = []; } moveTo(x, y) { this._currentPath.push({ type: 'M', x, y }); } lineTo(x, y) { this._currentPath.push({ type: 'L', x, y }); } closePath() { this._currentPath.push({ type: 'Z' }); } arc(cx, cy, r, start, end, ccw = false) { const steps = Math.max(12, Math.abs(end - start) * r | 0); const step = (end - start) / steps * (ccw ? -1 : 1); for (let i = 0; i <= steps; i++) { const a = start + step * i; const x = cx + r * Math.cos(a); const y = cy + r * Math.sin(a); if (i === 0) this.moveTo(x, y); else this.lineTo(x, y); } } arcTo(x1, y1, x2, y2, r) { // 简化实现 const last = this._currentPath[this._currentPath.length - 1]; if (!last) { this.moveTo(x1, y1); return; } this.lineTo(x1, y1); } ellipse(cx, cy, rx, ry, rot, start, end, ccw = false) { const steps = Math.max(16, Math.abs(end - start) * Math.max(rx, ry) | 0); const step = (end - start) / steps * (ccw ? -1 : 1); for (let i = 0; i <= steps; i++) { const a = start + step * i; const x = cx + rx * Math.cos(a) * Math.cos(rot) - ry * Math.sin(a) * Math.sin(rot); const y = cy + rx * Math.cos(a) * Math.sin(rot) + ry * Math.sin(a) * Math.cos(rot); if (i === 0) this.moveTo(x, y); else this.lineTo(x, y); } } rect(x, y, w, h) { this.moveTo(x, y); this.lineTo(x + w, y); this.lineTo(x + w, y + h); this.lineTo(x, y + h); this.closePath(); } quadraticCurveTo(cpx, cpy, x, y) { const last = this._currentPath[this._currentPath.length - 1]; if (!last) return; for (let i = 1; i <= 16; i++) { const t = i / 16, mt = 1 - t; this.lineTo(mt * mt * last.x + 2 * mt * t * cpx + t * t * x, mt * mt * last.y + 2 * mt * t * cpy + t * t * y); } } bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y) { const last = this._currentPath[this._currentPath.length - 1]; if (!last) return; for (let i = 1; i <= 16; i++) { const t = i / 16, mt = 1 - t; this.lineTo(mt * mt * mt * last.x + 3 * mt * mt * t * cp1x + 3 * mt * t * t * cp2x + t * t * t * x, mt * mt * mt * last.y + 3 * mt * mt * t * cp1y + 3 * mt * t * t * cp2y + t * t * t * y); } } // 填充路径 → 三角化后用 WebGL 画 fill() { if (this._currentPath.length < 3) return; const c = parseColor(this._state.fillStyle); const a = c.a * this._state.globalAlpha; // 提取点坐标 const points = this._currentPath.filter(p => p.type !== 'Z').map(p => ({ x: p.x, y: p.y })); if (points.length < 3) return; // ear-clipping 三角化 const tris = this._triangulate(points); for (let i = 0; i < tris.length; i += 3) { this._drawTriangle(tris[i], tris[i + 1], tris[i + 2], c.r, c.g, c.b, a); } } stroke() { if (this._currentPath.length < 2) return; const c = parseColor(this._state.strokeStyle); const a = c.a * this._state.globalAlpha; const lw = this._state.lineWidth; const pts = this._currentPath.filter(p => p.type !== 'Z'); for (let i = 0; i < pts.length - 1; i++) { this._drawThickLine(pts[i].x, pts[i].y, pts[i + 1].x, pts[i + 1].y, lw, c.r, c.g, c.b, a); } // 闭合 const last = this._currentPath[this._currentPath.length - 1]; if (last?.type === 'Z' && pts.length > 2) { this._drawThickLine(pts[pts.length - 1].x, pts[pts.length - 1].y, pts[0].x, pts[0].y, lw, c.r, c.g, c.b, a); } } clip() { // WebGL stencil 实现较复杂,先忽略 } isPointInPath(x, y) { const pts = this._currentPath.filter(p => p.type === 'M' || p.type === 'L'); let inside = false; for (let i = 0, j = pts.length - 1; i < pts.length; j = i++) { const xi = pts[i].x, yi = pts[i].y, xj = pts[j].x, yj = pts[j].y; if ((yi > y) !== (yj > y) && x < (xj - xi) * (y - yi) / (yj - yi) + xi) inside = !inside; } return inside; } // 文字绘制 → 用离屏 Canvas 渲染后作为纹理 fillText(text, x, y, maxWidth) { const tc = this._textCtx; tc.font = this._state.font; const m = tc.measureText(text); const fs = parseInt(this._state.font) || 10; const w = Math.ceil(Math.min(m.width, maxWidth || Infinity)) + 4; const h = Math.ceil(fs * 1.5) + 4; this._textCanvas.width = w; this._textCanvas.height = h; tc.font = this._state.font; tc.textBaseline = 'top'; tc.fillStyle = this._state.fillStyle; tc.clearRect(0, 0, w, h); tc.fillText(text, 2, 2, maxWidth); try { const tex = this._gl.getTexture(this._textCanvas); // 需要更新纹理 const gl = this._gl.gl; gl.bindTexture(gl.TEXTURE_2D, tex); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, this._textCanvas); let dx = x; if (this._state.textAlign === 'center') dx -= w / 2; else if (this._state.textAlign === 'right' || this._state.textAlign === 'end') dx -= w; let dy = y; if (this._state.textBaseline === 'middle') dy -= h / 2; else if (this._state.textBaseline === 'bottom' || this._state.textBaseline === 'alphabetic') dy -= h; const verts = this._gl._makeQuad(dx, dy, w, h, 0, 0, 1, 1, 1, 1, 1, this._state.globalAlpha); this._gl.addQuad(verts, tex); } catch (e) {} } strokeText(text, x, y, maxWidth) { const tc = this._textCtx; tc.font = this._state.font; const m = tc.measureText(text); const fs = parseInt(this._state.font) || 10; const w = Math.ceil(Math.min(m.width, maxWidth || Infinity)) + 4; const h = Math.ceil(fs * 1.5) + 4; this._textCanvas.width = w; this._textCanvas.height = h; tc.font = this._state.font; tc.textBaseline = 'top'; tc.strokeStyle = this._state.strokeStyle; tc.lineWidth = this._state.lineWidth; tc.clearRect(0, 0, w, h); tc.strokeText(text, 2, 2, maxWidth); try { const tex = this._gl.getTexture(this._textCanvas); const gl = this._gl.gl; gl.bindTexture(gl.TEXTURE_2D, tex); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, this._textCanvas); const verts = this._gl._makeQuad(x, y, w, h, 0, 0, 1, 1, 1, 1, 1, this._state.globalAlpha); this._gl.addQuad(verts, tex); } catch (e) {} } measureText(text) { this._textCtx.font = this._state.font; return this._textCtx.measureText(text); } // 像素操作 getImageData(x, y, w, h) { const gl = this._gl.gl; const pixels = new Uint8Array(w * h * 4); gl.readPixels(x, this._canvas.height - y - h, w, h, gl.RGBA, gl.UNSIGNED_BYTE, pixels); // 翻转 Y const flipped = new Uint8Array(w * h * 4); for (let r = 0; r < h; r++) { flipped.set(pixels.subarray(r * w * 4, (r + 1) * w * 4), (h - r - 1) * w * 4); } return new ImageData(new Uint8ClampedArray(flipped.buffer), w, h); } putImageData(imageData, x, y) { const gl = this._gl.gl; const tex = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, tex); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, imageData.width, imageData.height, 0, gl.RGBA, gl.UNSIGNED_BYTE, imageData.data); 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); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); const verts = this._gl._makeQuad(x, y, imageData.width, imageData.height, 0, 0, 1, 1, 1, 1, 1, 1); this._gl.addQuad(verts, tex); } createImageData(w, h) { return new ImageData(w, h); } // 渐变(简化:返回一个可 addColorStop 的对象) createLinearGradient(x0, y0, x1, y1) { return { _type: 'linear', _stops: [], addColorStop(o, c) { this._stops.push({ o, c }); } }; } createRadialGradient(x0, y0, r0, x1, y1, r1) { return { _type: 'radial', _stops: [], addColorStop(o, c) { this._stops.push({ o, c }); } }; } createPattern(image, rep) { return { _type: 'pattern', image, rep }; } // getLineDash / setLineDash getLineDash() { return []; } setLineDash() {} // 内部:三角形绘制 _drawTriangle(p0, p1, p2, r, g, b, a) { const m = this._gl.matrix; const pr = this._gl.projection; const transform = (x, y) => { const wx = m[0] * x + m[3] * y + m[6]; const wy = m[1] * x + m[4] * y + m[7]; return [pr[0] * wx + pr[6], pr[4] * wy + pr[7]]; }; const [x0, y0] = transform(p0.x, p0.y); const [x1, y1] = transform(p1.x, p1.y); const [x2, y2] = transform(p2.x, p2.y); const verts = new Float32Array([ x0, y0, 0, 0, r, g, b, a, 0, x1, y1, 0, 0, r, g, b, a, 0, x2, y2, 0, 0, r, g, b, a, 0, x0, y0, 0, 0, r, g, b, a, 0, x2, y2, 0, 0, r, g, b, a, 0, x1, y1, 0, 0, r, g, b, a, 0, ]); this._gl.flush(); // 先刷新当前批次 const gl = this._gl.gl; gl.bindBuffer(gl.ARRAY_BUFFER, this._gl.vbo); gl.bufferSubData(gl.ARRAY_BUFFER, 0, verts); gl.uniformMatrix3fv(this._gl.loc.u_proj, false, this._gl.projection); gl.uniform1i(this._gl.loc.u_useTex, false); gl.vertexAttribPointer(this._gl.loc.a_pos, 2, gl.FLOAT, false, 36, 0); gl.vertexAttribPointer(this._gl.loc.a_uv, 2, gl.FLOAT, false, 36, 8); gl.vertexAttribPointer(this._gl.loc.a_color, 4, gl.FLOAT, false, 36, 16); gl.drawArrays(gl.TRIANGLES, 0, 6); this._gl.drawCalls++; } // 内部:粗线绘制 _drawThickLine(x0, y0, x1, y1, w, r, g, b, a) { const dx = x1 - x0, dy = y1 - y0; const len = Math.sqrt(dx * dx + dy * dy); if (len === 0) return; const nx = -dy / len * w / 2, ny = dx / len * w / 2; const verts = this._gl._makeQuad( Math.min(x0 + nx, x1 + nx, x0 - nx, x1 - nx), Math.min(y0 + ny, y1 + ny, y0 - ny, y1 - ny), Math.abs(dx) + w, Math.abs(dy) + w, 0, 0, 0, 0, r, g, b, a ); this._gl.addQuad(verts); } // 内部:ear-clipping 三角化 _triangulate(points) { if (points.length < 3) return []; const result = []; const verts = [...points]; if (this._polyArea(verts) > 0) verts.reverse(); let n = verts.length, i = 0; while (n > 2 && i < 1000) { i++; const prev = (i - 1 + n) % n, next = (i + 1) % n; if (this._isEar(prev, i % n, next, verts, n)) { result.push(verts[prev], verts[i % n], verts[next]); verts.splice(i % n, 1); n--; i = 0; } } return result; } _polyArea(p) { let a = 0; for (let i = 0; i < p.length; i++) { const j = (i + 1) % p.length; a += p[i].x * p[j].y - p[j].x * p[i].y; } return a / 2; } _isEar(prev, cur, next, v, n) { const a = v[prev], b = v[cur % n], c = v[next]; if ((b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x) <= 0) return false; for (let i = 0; i < n; i++) { if (i === prev || i === cur % n || i === next) continue; if (this._inTri(v[i], a, b, c)) return false; } return true; } _inTri(p, a, b, c) { const d1 = (p.x - b.x) * (a.y - b.y) - (a.x - b.x) * (p.y - b.y); const d2 = (p.x - c.x) * (b.y - c.y) - (b.x - c.x) * (p.y - c.y); const d3 = (p.x - a.x) * (c.y - a.y) - (c.x - a.x) * (p.y - a.y); const hasNeg = (d1 < 0) || (d2 < 0) || (d3 < 0); const hasPos = (d1 > 0) || (d2 > 0) || (d3 > 0); return !(hasNeg && hasPos); } } // ================================================================ // 注入:拦截 getContext('2d'),返回转译代理 // ================================================================ const origGetContext = HTMLCanvasElement.prototype.getContext; const rendererMap = new WeakMap(); // canvas → GLRenderer const proxyMap = new WeakMap(); // canvas → Canvas2DProxy HTMLCanvasElement.prototype.getContext = function (type, ...args) { // 2d 请求 → 转译为 WebGL if (type === '2d' || type === '2D') { if (proxyMap.has(this)) return proxyMap.get(this); try { let renderer = rendererMap.get(this); if (!renderer) { renderer = new GLRenderer(this); rendererMap.set(this, renderer); } const proxy = new Canvas2DProxy(this, renderer); proxyMap.set(this, proxy); log(`✅ Canvas 2D → WebGL 转译成功 (${this.width}x${this.height})`); return proxy; } catch (e) { // WebGL 不可用,降级到原生 Canvas 2D log('⚠️ WebGL 不可用,降级到 Canvas 2D:', e.message); return origGetContext.call(this, type, ...args); } } // WebGL 请求 → 优化参数 if (type === 'webgl' || type === 'webgl2' || type === 'experimental-webgl') { const opts = args[0] || {}; Object.assign(opts, { antialias: false, powerPreference: 'high-performance', desynchronized: true, failIfMajorPerformanceCaveat: false }); args[0] = opts; } return origGetContext.call(this, type, ...args); }; // ================================================================ // 帧循环:自动 beginFrame / endFrame // ================================================================ const origRAF = window.requestAnimationFrame; let frameScheduled = false; function scheduleFlush() { if (frameScheduled) return; frameScheduled = true; origRAF.call(window, () => { // 刷新所有活跃的渲染器 for (const [, renderer] of rendererMap) { renderer.endFrame(); renderer.beginFrame(); } frameScheduled = false; }); } // 拦截 requestAnimationFrame 以自动刷新 window.requestAnimationFrame = function (cb) { return origRAF.call(window, function (...a) { scheduleFlush(); cb(...a); }); }; // ================================================================ // CSS 优化 // ================================================================ const style = document.createElement('style'); style.textContent = 'canvas{will-change:transform!important}'; (document.head || document.documentElement).prepend(style); // ================================================================ // 完成 // ================================================================ log('🚀 Canvas → WebGL 转译器已注入'); log(' 所有 Canvas 2D 操作将自动转译为 WebGL GPU 渲染'); })();