fix:修复bug

This commit is contained in:
Daniel
2026-04-01 20:29:33 +08:00
parent afbcd99224
commit a80d2b8430
9 changed files with 477 additions and 179 deletions

460
display.js Normal file
View File

@@ -0,0 +1,460 @@
const pauseBtn = document.getElementById("pause-btn");
const resetBtn = document.getElementById("reset-btn");
const statsEl = document.getElementById("stats");
const globalFpsEl = document.getElementById("global-fps");
const shaderCountEl = document.getElementById("shader-count");
const searchInput = document.getElementById("search-input");
const gridEl = document.getElementById("preview-grid");
const detailViewEl = document.getElementById("detail-view");
const detailTitleEl = document.getElementById("detail-title");
const detailFpsEl = document.getElementById("detail-fps");
const detailCanvas = document.getElementById("detail-canvas");
const backBtn = document.getElementById("back-btn");
const API_BASE = "/api/shaders";
const vertexShaderSource = `#version 300 es
precision highp float;
layout(location = 0) in vec2 aPosition;
void main() { gl_Position = vec4(aPosition, 0.0, 1.0); }`;
function toPortableGlsl(code) {
if (!code) return "";
let s = String(code);
const start = s.indexOf("void mainImage");
if (start >= 0) {
s = s.slice(0, s.length);
}
s = s
.replace(/float2/g, "vec2")
.replace(/float3/g, "vec3")
.replace(/float4/g, "vec4")
.replace(/int2/g, "ivec2")
.replace(/int3/g, "ivec3")
.replace(/int4/g, "ivec4")
.replace(/precise::tanh/g, "tanh")
.replace(/ANGLE_userUniforms\._uiResolution/g, "iResolution")
.replace(/ANGLE_userUniforms\._uiTime/g, "iTime")
.replace(/ANGLE_userUniforms\._uiMouse/g, "iMouse")
.replace(/ANGLE_texture\([^)]*\)/g, "vec4(0.0)")
.replace(/ANGLE_texelFetch\([^)]*\)/g, "vec4(0.0)")
.replace(/ANGLE_mod\(/g, "mod(")
.replace(/\[\[[^\]]+\]\]/g, "")
.replace(/template\s*[^\n]*\n/g, "")
.replace(/ANGLE_out\(([^)]+)\)/g, "$1")
.replace(/ANGLE_swizzle_ref\(([^)]+)\)/g, "$1")
.replace(/ANGLE_elem_ref\(([^)]+)\)/g, "$1")
.replace(/(\d+\.\d+|\d+)f\b/g, "$1");
// Keep only helper functions + mainImage block, drop struct/template leftovers.
const mi = s.indexOf("void mainImage");
if (mi >= 0) {
const head = s.slice(0, mi);
const helpers = head
.split("\n\n")
.filter((blk) => /(void|float|vec[234]|mat[234])\s+[A-Za-z_][A-Za-z0-9_]*\s*\(/.test(blk))
.join("\n\n");
const mainPart = s.slice(mi);
return `${helpers}\n\n${mainPart}`;
}
return s;
}
function fallbackShader(name) {
const h = (name || "shader")
.split("")
.reduce((a, c) => (a * 33 + c.charCodeAt(0)) >>> 0, 5381);
const k1 = ((h % 97) / 97).toFixed(3);
const k2 = (((h >> 5) % 89) / 89).toFixed(3);
return `void mainImage(out vec4 fragColor, in vec2 fragCoord) {
vec2 uv = (fragCoord - 0.5 * iResolution.xy) / iResolution.y;
float t = iTime;
float v = sin(uv.x * (8.0 + ${k1} * 8.0) + t * (1.4 + ${k2} * 1.2))
+ cos(uv.y * (10.0 + ${k2} * 6.0) - t * (1.1 + ${k1}));
vec3 col = 0.5 + 0.5 * cos(vec3(0.2, 1.2, 2.2) + v + t);
fragColor = vec4(col, 1.0);
}`;
}
function buildFragmentShader(userCode) {
return `#version 300 es
precision highp float;
out vec4 outColor;
uniform vec3 iResolution;
uniform float iTime;
uniform float iTimeDelta;
uniform int iFrame;
uniform vec4 iMouse;
${userCode}
void main() { mainImage(outColor, gl_FragCoord.xy); }`;
}
function createQuad(gl) {
const quad = new Float32Array([-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1]);
const vao = gl.createVertexArray();
const vbo = gl.createBuffer();
gl.bindVertexArray(vao);
gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
gl.bufferData(gl.ARRAY_BUFFER, quad, gl.STATIC_DRAW);
gl.enableVertexAttribArray(0);
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
gl.bindVertexArray(null);
return { vao, vbo };
}
function compileProgram(gl, fragmentShaderSource) {
function compile(type, source) {
const shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
const info = gl.getShaderInfoLog(shader);
gl.deleteShader(shader);
throw new Error(info || "unknown compile error");
}
return shader;
}
const vs = compile(gl.VERTEX_SHADER, vertexShaderSource);
const fs = compile(gl.FRAGMENT_SHADER, fragmentShaderSource);
const program = gl.createProgram();
gl.attachShader(program, vs);
gl.attachShader(program, fs);
gl.linkProgram(program);
gl.deleteShader(vs);
gl.deleteShader(fs);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
const info = gl.getProgramInfoLog(program);
gl.deleteProgram(program);
throw new Error(info || "unknown link error");
}
return program;
}
const previews = [];
let paused = false;
let elapsed = 0;
let frame = 0;
let lastTs = performance.now();
let fpsCounter = 0;
let fps = 0;
let lastSec = 0;
let detailFpsCounter = 0;
let detailFps = 0;
let detailLastSec = 0;
let detailRuntime = null;
let searchKeyword = "";
let lastSignature = "";
function createPreviewCard(shader) {
const { id, name, author = "unknown", views = 0, likes = 0, code } = shader;
const card = document.createElement("article");
card.className = "card";
card.dataset.search = `${name} ${author}`.toLowerCase();
card.innerHTML = `
<div class="card-head">
<strong>${name}</strong>
<div style="display:flex; gap:6px;">
<button type="button" class="pause-local-btn">暂停</button>
</div>
</div>
<canvas></canvas>
<div class="card-meta">
<span>by ${author}</span>
<span>views ${views} | likes ${likes}</span>
</div>
<div class="card-head error"></div>
`;
const canvas = card.querySelector("canvas");
const errorEl = card.querySelector(".error");
const pauseLocalBtn = card.querySelector(".pause-local-btn");
const gl = canvas.getContext("webgl2", { antialias: false, alpha: false });
if (!gl) {
errorEl.textContent = "当前浏览器不支持 WebGL2";
return;
}
let program;
let codeForRender = code;
try {
program = compileProgram(gl, buildFragmentShader(codeForRender));
} catch (_err1) {
try {
codeForRender = toPortableGlsl(code);
program = compileProgram(gl, buildFragmentShader(codeForRender));
errorEl.textContent = "已自动兼容转换后渲染";
} catch (_err2) {
codeForRender = fallbackShader(name);
try {
program = compileProgram(gl, buildFragmentShader(codeForRender));
errorEl.textContent = "原始代码不兼容,已使用兼容预览模式";
} catch (err3) {
errorEl.textContent = `编译失败:\n${String(err3.message || err3)}`;
return;
}
}
}
const { vao } = createQuad(gl);
const uniforms = {
iResolution: gl.getUniformLocation(program, "iResolution"),
iTime: gl.getUniformLocation(program, "iTime"),
iTimeDelta: gl.getUniformLocation(program, "iTimeDelta"),
iFrame: gl.getUniformLocation(program, "iFrame"),
iMouse: gl.getUniformLocation(program, "iMouse"),
};
const state = {
id,
name,
code,
codeForRender,
card,
canvas,
gl,
program,
vao,
uniforms,
mouse: { x: 0, y: 0, downX: 0, downY: 0, down: false },
isPlaying: true,
localTime: 0,
localFrame: 0,
lastTick: performance.now(),
};
canvas.addEventListener("mousemove", (event) => {
const rect = canvas.getBoundingClientRect();
state.mouse.x = ((event.clientX - rect.left) / rect.width) * canvas.width;
state.mouse.y = ((rect.bottom - event.clientY) / rect.height) * canvas.height;
});
canvas.addEventListener("mousedown", () => {
state.mouse.down = true;
state.mouse.downX = state.mouse.x;
state.mouse.downY = state.mouse.y;
});
window.addEventListener("mouseup", () => {
state.mouse.down = false;
});
pauseLocalBtn.addEventListener("click", (event) => {
event.stopPropagation();
state.isPlaying = !state.isPlaying;
state.lastTick = performance.now();
pauseLocalBtn.textContent = state.isPlaying ? "暂停" : "继续";
});
canvas.addEventListener("click", () => openDetail(state));
gridEl.appendChild(card);
previews.push(state);
}
function clearPreviews() {
previews.forEach((preview) => preview.card.remove());
previews.length = 0;
}
function closeDetail() {
detailViewEl.classList.remove("active");
detailViewEl.setAttribute("aria-hidden", "true");
if (!detailRuntime) return;
const { gl, program, vao, vbo } = detailRuntime;
gl.deleteProgram(program);
gl.deleteVertexArray(vao);
gl.deleteBuffer(vbo);
detailRuntime = null;
}
function openDetail(previewState) {
closeDetail();
const gl = detailCanvas.getContext("webgl2", { antialias: false, alpha: false });
if (!gl) return;
let program;
const detailCode = previewState.codeForRender || previewState.code;
try {
program = compileProgram(gl, buildFragmentShader(detailCode));
} catch (err) {
statsEl.textContent = `详情编译失败: ${String(err.message || err)}`;
return;
}
const { vao, vbo } = createQuad(gl);
detailRuntime = {
gl,
program,
vao,
vbo,
uniforms: {
iResolution: gl.getUniformLocation(program, "iResolution"),
iTime: gl.getUniformLocation(program, "iTime"),
iTimeDelta: gl.getUniformLocation(program, "iTimeDelta"),
iFrame: gl.getUniformLocation(program, "iFrame"),
iMouse: gl.getUniformLocation(program, "iMouse"),
},
mouse: { x: 0, y: 0, downX: 0, downY: 0, down: false },
};
detailTitleEl.textContent = previewState.name;
detailViewEl.classList.add("active");
detailViewEl.setAttribute("aria-hidden", "false");
}
function applySearch() {
const keyword = searchKeyword.trim().toLowerCase();
for (const preview of previews) {
preview.card.style.display = !keyword || preview.card.dataset.search.includes(keyword) ? "" : "none";
}
}
async function loadShaders() {
try {
const res = await fetch(`${API_BASE}?t=${Date.now()}`, { cache: "no-store" });
if (!res.ok) throw new Error("加载失败");
const shaders = await res.json();
const signature = JSON.stringify(
shaders.map((s) => ({ id: s.id, updatedAt: s.updatedAt || s.createdAt || "", name: s.name }))
);
if (signature !== lastSignature) {
lastSignature = signature;
clearPreviews();
shaders.forEach(createPreviewCard);
applySearch();
syncStats();
}
} catch (err) {
statsEl.textContent = `读取后端失败:${String(err.message || err)}`;
}
}
function syncStats() {
statsEl.textContent = `预览数量: ${previews.length} | 全局时间: ${elapsed.toFixed(2)}s | 状态: ${
paused ? "已暂停" : "运行中"
}`;
shaderCountEl.textContent = `${previews.length} shaders`;
}
function renderAll(ts) {
const dt = Math.min((ts - lastTs) / 1000, 0.05);
lastTs = ts;
if (!paused) {
elapsed += dt;
frame += 1;
}
if (Math.floor(elapsed) !== lastSec) {
lastSec = Math.floor(elapsed);
fps = fpsCounter;
fpsCounter = 0;
globalFpsEl.textContent = `FPS: ${fps}`;
} else {
fpsCounter += 1;
}
for (const preview of previews) {
const { gl, canvas, program, vao, uniforms, mouse } = preview;
const now = performance.now();
const localDt = Math.min((now - preview.lastTick) / 1000, 0.05);
preview.lastTick = now;
if (preview.isPlaying && !paused) {
preview.localTime += localDt;
preview.localFrame += 1;
}
const rect = canvas.getBoundingClientRect();
const dpr = Math.min(window.devicePixelRatio || 1, 2);
const w = Math.max(1, Math.floor(rect.width * dpr));
const h = Math.max(1, Math.floor(rect.height * dpr));
if (canvas.width !== w || canvas.height !== h) {
canvas.width = w;
canvas.height = h;
gl.viewport(0, 0, w, h);
}
gl.useProgram(program);
gl.uniform3f(uniforms.iResolution, canvas.width, canvas.height, 1);
gl.uniform1f(uniforms.iTime, preview.localTime);
gl.uniform1f(uniforms.iTimeDelta, preview.isPlaying && !paused ? localDt : 0);
gl.uniform1i(uniforms.iFrame, preview.localFrame);
gl.uniform4f(
uniforms.iMouse,
mouse.x,
mouse.y,
mouse.down ? mouse.downX : -mouse.downX,
mouse.down ? mouse.downY : -mouse.downY
);
gl.bindVertexArray(vao);
gl.drawArrays(gl.TRIANGLES, 0, 6);
gl.bindVertexArray(null);
}
if (detailRuntime) {
const { gl, program, vao, uniforms, mouse } = detailRuntime;
const dpr = Math.min(window.devicePixelRatio || 1, 2);
const w = Math.max(1, Math.floor(detailCanvas.clientWidth * dpr));
const h = Math.max(1, Math.floor(detailCanvas.clientHeight * dpr));
if (detailCanvas.width !== w || detailCanvas.height !== h) {
detailCanvas.width = w;
detailCanvas.height = h;
gl.viewport(0, 0, w, h);
}
gl.useProgram(program);
gl.uniform3f(uniforms.iResolution, detailCanvas.width, detailCanvas.height, 1);
gl.uniform1f(uniforms.iTime, elapsed);
gl.uniform1f(uniforms.iTimeDelta, dt);
gl.uniform1i(uniforms.iFrame, frame);
gl.uniform4f(
uniforms.iMouse,
mouse.x,
mouse.y,
mouse.down ? mouse.downX : -mouse.downX,
mouse.down ? mouse.downY : -mouse.downY
);
gl.bindVertexArray(vao);
gl.drawArrays(gl.TRIANGLES, 0, 6);
gl.bindVertexArray(null);
if (Math.floor(elapsed) !== detailLastSec) {
detailLastSec = Math.floor(elapsed);
detailFps = detailFpsCounter;
detailFpsCounter = 0;
detailFpsEl.textContent = `FPS: ${detailFps}`;
} else {
detailFpsCounter += 1;
}
}
requestAnimationFrame(renderAll);
}
pauseBtn.addEventListener("click", () => {
paused = !paused;
pauseBtn.textContent = paused ? "全局继续" : "全局暂停";
syncStats();
});
resetBtn.addEventListener("click", () => {
elapsed = 0;
frame = 0;
fpsCounter = 0;
lastSec = 0;
detailLastSec = 0;
detailFpsCounter = 0;
syncStats();
});
backBtn.addEventListener("click", closeDetail);
detailCanvas.addEventListener("mousemove", (event) => {
if (!detailRuntime) return;
const rect = detailCanvas.getBoundingClientRect();
detailRuntime.mouse.x = ((event.clientX - rect.left) / rect.width) * detailCanvas.width;
detailRuntime.mouse.y = ((rect.bottom - event.clientY) / rect.height) * detailCanvas.height;
});
detailCanvas.addEventListener("mousedown", () => {
if (!detailRuntime) return;
detailRuntime.mouse.down = true;
detailRuntime.mouse.downX = detailRuntime.mouse.x;
detailRuntime.mouse.downY = detailRuntime.mouse.y;
});
window.addEventListener("mouseup", () => {
if (!detailRuntime) return;
detailRuntime.mouse.down = false;
});
searchInput.addEventListener("input", (event) => {
searchKeyword = event.target.value || "";
applySearch();
});
loadShaders();
setInterval(loadShaders, 3000);
requestAnimationFrame(renderAll);