- 分析 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 解释器,不在渲染层
1018 lines
34 KiB
JavaScript
1018 lines
34 KiB
JavaScript
// ==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 渲染');
|
||
|
||
})();
|