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 = `
${name}
by ${author} views ${views} | likes ${likes}
`; 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);