From b0b62f6a1e5682e1dca68802a869fcbba3d1a592 Mon Sep 17 00:00:00 2001 From: codemao-webgl-engine Date: Sat, 2 May 2026 21:33:29 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20Canvas=202D=20=E2=86=92=20WebGL=20?= =?UTF-8?q?=E5=AE=9E=E6=97=B6=E8=BD=AC=E8=AF=91=E5=99=A8=20v2.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 核心思路: 拦截 getContext('2d'),返回 Canvas2DProxy 代理 所有 Canvas 2D API 内部转译为 WebGL GPU 渲染 转译覆盖: - fillRect/strokeRect/clearRect → 纯色四边形 - drawImage → 纹理采样四边形 - fillText/strokeText → 离屏Canvas→纹理→四边形 - fill/stroke (路径) → ear-clipping三角化/粗线四边形 - arc/ellipse/bezierCurveTo → 细分折线 - save/restore/translate/rotate/scale → 变换矩阵栈 - getImageData/putImageData → gl.readPixels/texImage2D - globalAlpha/globalCompositeOperation → alpha/blendFunc 性能优化: - 批量渲染: 4096四边形/批次,单次drawArrays - 纹理缓存: WeakMap + 8纹理槽 - 关闭antialias/强制独显/低延迟 --- README.md | 49 ++ userscript/codemao-canvas2webgl.user.js | 1004 +++++++++++++++++++++++ 2 files changed, 1053 insertions(+) create mode 100644 README.md create mode 100644 userscript/codemao-canvas2webgl.user.js diff --git a/README.md b/README.md new file mode 100644 index 0000000..c2b0fc0 --- /dev/null +++ b/README.md @@ -0,0 +1,49 @@ +# 编程猫 Canvas→WebGL 转译加速器 + +## 原理 + +编程猫作品用 Canvas 2D 渲染(CPU),本脚本拦截 `getContext('2d')`,返回一个兼容代理对象,**所有 Canvas 2D 操作内部转译为 WebGL(GPU)执行**。 + +``` +原作品代码 → canvas.getContext('2d') → 被拦截 + ↓ + 返回 Canvas2DProxy(兼容所有2D API) + ↓ + fillRect / drawImage / fill / stroke... + ↓ + 内部全部转为 WebGL 绘制 + ↓ + GPU 硬件渲染 🚀 +``` + +## 转译对照 + +| Canvas 2D API | WebGL 转译方式 | +|---------------|---------------| +| `fillRect(x,y,w,h)` | 纯色四边形,直接顶点绘制 | +| `drawImage(img,...)` | 纹理采样四边形 | +| `fillText(text,x,y)` | 离屏Canvas渲染→纹理上传→四边形 | +| `fill()` (路径) | ear-clipping 三角化→三角形绘制 | +| `stroke()` (路径) | 粗线四边形近似 | +| `arc()` / `ellipse()` | 细分三角形扇 | +| `bezierCurveTo()` | 细分折线 | +| `save()` / `restore()` | WebGL 变换矩阵栈 | +| `clearRect()` | destination-out 混合 | +| `getImageData()` | gl.readPixels | +| `globalAlpha` | 顶点颜色 alpha 通道 | +| `globalCompositeOperation` | gl.blendFunc 切换 | + +## 手机使用 + +1. 用支持油猴的手机浏览器(Kiwi / Firefox / X浏览器 / Alook) +2. 安装 Tampermonkey 或暴力猴 +3. 新建脚本,粘贴 `userscript/codemao-canvas2webgl.user.js` 内容 +4. 打开编程猫任意作品 → 自动 GPU 加速 + +## 书签脚本(最简单) + +复制这段代码,保存为书签,在编程猫作品页面点击: + +``` +javascript:void(function(){var s=document.createElement('script');s.src='https://gitee.com/shuiyu1123/codemao-webgl-engine/raw/master/userscript/codemao-canvas2webgl.user.js';document.head.prepend(s);}()); +``` diff --git a/userscript/codemao-canvas2webgl.user.js b/userscript/codemao-canvas2webgl.user.js new file mode 100644 index 0000000..3b0034d --- /dev/null +++ b/userscript/codemao-canvas2webgl.user.js @@ -0,0 +1,1004 @@ +// ==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 渲染'); + +})();