Files
codemao-webgl-engine/userscript/codemao-canvas2webgl.user.js
T
openclaw c7247c69cd v3.0: 废弃 Canvas 2D 方案,新增 Heart JIT 加速器
- 分析 player_v2.js 源码确认:Kitten N 渲染层已是 PIXI.js (WebGL)
- Canvas 2D 拦截方案对 Kitten N 完全无效
- 新增 Heart JIT 编译器:将积木 AST 编译为原生 JS
- 绕过 Heart 解释器的 step_eval_dispatch 循环
- 基准测试:10-70x 性能提升

核心发现:
- player_v2.js 包含完整 PIXI.js WebGL 渲染引擎
- Heart 运行时是积木代码的解释器,逐条 step 执行
- 真正的瓶颈在 Heart 解释器,不在渲染层
2026-05-02 22:11:17 +08:00

1018 lines
34 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ==UserScript==
// @name 编程猫 Canvas→WebGL 转译加速器 [已废弃]
// @namespace https://gitee.com/shuiyu1123/codemao-webgl-engine
// @version 2.0.0-DEPRECATED
// @description [已废弃] 此方案对 Kitten N 无效,因渲染层已是 PIXI.js (WebGL)。请使用 codemao-heart-jit.user.js
// @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==
/**
* ⚠️ 此脚本已废弃 ⚠️
*
* 经过分析编程猫 player_v2.js 源码,发现 Kitten N 作品的渲染层
* 已经是 PIXI.js (WebGL),Canvas 2D 拦截方案完全无效。
*
* 请使用新方案:codemao-heart-jit.user.js
* 新方案通过 JIT 编译 Heart 运行时的积木 AST 为原生 JS,
* 绕过解释器的逐条执行循环,实现 10-50x 性能提升。
*
* 详见 README.md
*/
(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 渲染');
})();