Files
codemao-webgl-engine/userscript/codemao-canvas2webgl.user.js
T
codemao-webgl-engine b0b62f6a1e feat: Canvas 2D → WebGL 实时转译器 v2.0
核心思路: 拦截 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/强制独显/低延迟
2026-05-02 21:33:29 +08:00

1005 lines
34 KiB
JavaScript

// ==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 渲染');
})();