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 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 vertexShaderSource = `#version 300 es precision highp float; layout(location = 0) in vec2 aPosition; void main() { gl_Position = vec4(aPosition, 0.0, 1.0); }`; const presetShaders = [ { name: "Alien Core (Ported)", code: `vec3 paletteAC(float d) { return mix(vec3(0.2, 0.7, 0.9), vec3(1.0, 0.0, 1.0), d); } vec2 rotateAC(vec2 p, float a) { float c = cos(a); float s = sin(a); return mat2(c, -s, s, c) * p; } float mapAC(vec3 p) { for (int i = 0; i < 8; i++) { float t = iTime * 0.2; p.xz = rotateAC(p.xz, t); p.xy = rotateAC(p.xy, t * 1.89); p.xz = abs(p.xz) - 0.5; } return dot(sign(p), p) / 5.0; } vec4 raymarchAC(vec3 ro, vec3 rd) { float t = 0.0; vec3 col = vec3(0.0); float d = 0.0; for (int i = 0; i < 64; i++) { vec3 p = ro + rd * t; d = mapAC(p) * 0.5; if (d < 0.02 || d > 100.0) break; col += paletteAC(length(p) * 0.1) / (400.0 * d); t += d; } float alpha = 1.0 / (max(d, 0.0001) * 100.0); return vec4(col, alpha); } void mainImage(out vec4 fragColor, in vec2 fragCoord) { vec2 uv = (fragCoord - 0.5 * iResolution.xy) / iResolution.x; vec3 ro = vec3(0.0, 0.0, -50.0); ro.xz = rotateAC(ro.xz, iTime); vec3 cf = normalize(-ro); vec3 cs = normalize(cross(cf, vec3(0.0, 1.0, 0.0))); vec3 cu = normalize(cross(cf, cs)); vec3 uuv = ro + cf * 3.0 + uv.x * cs + uv.y * cu; vec3 rd = normalize(uuv - ro); fragColor = raymarchAC(ro, rd); }`, }, { name: "Neon Flow", code: `void mainImage(out vec4 fragColor, in vec2 fragCoord) { vec2 uv = (fragCoord - 0.5 * iResolution.xy) / iResolution.y; float t = iTime * 0.7; float n = sin(uv.x * 7.0 + t) + cos(uv.y * 11.0 - t * 1.2); float glow = 0.015 / (abs(uv.y + 0.2 * sin(uv.x * 5.0 + t)) + 0.005); vec3 col = vec3(0.02, 0.05, 0.12) + vec3(0.1, 0.5, 0.9) * (0.5 + 0.5 * n); col += vec3(0.9, 0.2, 1.0) * glow; fragColor = vec4(col, 1.0); }`, }, { name: "Plasma Warp", code: `void mainImage(out vec4 fragColor, in vec2 fragCoord) { vec2 uv = fragCoord / iResolution.xy; float t = iTime * 0.9; float p = sin((uv.x + t) * 10.0) + sin((uv.y - t) * 12.0); p += sin((uv.x + uv.y + t * 0.7) * 18.0); vec3 col = 0.5 + 0.5 * cos(vec3(0.2, 1.3, 2.4) + p + t); fragColor = vec4(col, 1.0); }`, }, { name: "Tunnel Pulse", code: `void mainImage(out vec4 fragColor, in vec2 fragCoord) { vec2 uv = (fragCoord - 0.5 * iResolution.xy) / iResolution.y; float t = iTime; float a = atan(uv.y, uv.x); float r = length(uv); float rings = sin(18.0 * r - t * 6.0); float rays = sin(a * 12.0 + t * 3.0); vec3 col = vec3(0.05, 0.03, 0.08); col += vec3(0.2, 0.7, 1.0) * smoothstep(0.98, 1.0, rings); col += vec3(1.0, 0.3, 0.6) * smoothstep(0.95, 1.0, rays) * (1.0 - r); fragColor = vec4(col, 1.0); }`, }, { name: "Grid Energy", code: `float hash21(vec2 p) { p = fract(p * vec2(123.34, 456.21)); p += dot(p, p + 45.32); return fract(p.x * p.y); } void mainImage(out vec4 fragColor, in vec2 fragCoord) { vec2 uv = (fragCoord - 0.5 * iResolution.xy) / iResolution.y; vec2 p = uv * 5.0; vec2 gv = fract(p) - 0.5; vec2 id = floor(p); float rnd = hash21(id); float line = min(abs(gv.x), abs(gv.y)); float spark = 0.01 / (line + 0.004); spark *= 0.5 + 0.5 * sin(iTime * 4.0 + rnd * 20.0); vec3 col = vec3(0.03, 0.05, 0.08) + spark * vec3(0.3, 1.0, 0.9); fragColor = vec4(col, 1.0); }`, }, { name: "Smoke Bands", code: `void mainImage(out vec4 fragColor, in vec2 fragCoord) { vec2 uv = (fragCoord - 0.5 * iResolution.xy) / iResolution.y; float t = iTime * 0.35; float w = sin(uv.x * 8.0 + t) + sin(uv.x * 14.0 - t * 1.4); float m = smoothstep(0.2, -0.3, abs(uv.y + 0.2 * w)); vec3 col = mix(vec3(0.02, 0.02, 0.03), vec3(0.4, 0.5, 0.9), m); 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; uniform vec4 iDate; ${userCode} void main() { mainImage(outColor, gl_FragCoord.xy); }`; } function setIDateUniform(gl, loc) { if (loc == null) return; const d = new Date(); gl.uniform4f( loc, d.getFullYear(), d.getMonth() + 1, d.getDate(), d.getHours() * 3600 + d.getMinutes() * 60 + d.getSeconds() + d.getMilliseconds() * 0.001 ); } 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, label, source) { if (gl.isContextLost?.()) { throw new Error(`${label}: WebGL context lost`); } const shader = gl.createShader(type); gl.shaderSource(shader, source); gl.compileShader(shader); if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { let info = (gl.getShaderInfoLog(shader) || "").trim(); if (!info) { const err = gl.getError(); info = err && err !== gl.NO_ERROR ? `WebGL getError 0x${err.toString(16)} (driver returned no compile log)` : "No compile log (often: shader too complex, loop/unroll limits, or driver bug)."; } gl.deleteShader(shader); throw new Error(`${label} shader:\n${info}`); } return shader; } const vs = compile(gl.VERTEX_SHADER, "Vertex", vertexShaderSource); const fs = compile(gl.FRAGMENT_SHADER, "Fragment", 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)) { let info = (gl.getProgramInfoLog(program) || "").trim(); if (!info) { const err = gl.getError(); info = err && err !== gl.NO_ERROR ? `WebGL getError 0x${err.toString(16)} (no link log)` : "No link log (vertex/fragment interface mismatch or resource limits)."; } gl.deleteProgram(program); throw new Error(`Program link:\n${info}`); } 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; function createPreviewCard(name, code) { const views = Math.floor(3000 + Math.random() * 22000); const likes = Math.floor(40 + Math.random() * 700); const author = ["glkt", "aciekick", "iq", "shane", "bers", "felixfaire"][ Math.floor(Math.random() * 6) ]; const card = document.createElement("article"); card.className = "card"; 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 removeBtn = card.querySelector(".remove-btn"); const gl = canvas.getContext("webgl2", { antialias: false, alpha: false }); if (!gl) { errorEl.textContent = "当前浏览器不支持 WebGL2"; return; } const fragment = buildFragmentShader(code); let program; try { program = compileProgram(gl, fragment); } catch (err) { errorEl.textContent = `编译失败:\n${String(err.message || err)}`; 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"), iDate: gl.getUniformLocation(program, "iDate"), }; const state = { name, code, 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(), removed: false, }; function resizeCanvas() { 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); } } resizeCanvas(); 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; }); removeBtn.addEventListener("click", () => { state.removed = true; card.remove(); const idx = previews.indexOf(state); if (idx >= 0) previews.splice(idx, 1); syncStats(); }); pauseLocalBtn.addEventListener("click", () => { state.isPlaying = !state.isPlaying; state.lastTick = performance.now(); pauseLocalBtn.textContent = state.isPlaying ? "暂停" : "继续"; }); canvas.addEventListener("click", () => { openDetail(state); }); gridEl.appendChild(card); previews.push(state); syncStats(); } 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) { statsEl.textContent = "无法打开详情:当前浏览器不支持 WebGL2。"; return; } let program; try { program = compileProgram(gl, buildFragmentShader(previewState.code)); } catch (err) { statsEl.textContent = `详情编译失败: ${String(err.message || err)}`; return; } const { vao, vbo } = 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"), iDate: gl.getUniformLocation(program, "iDate"), }; detailRuntime = { gl, program, vao, vbo, uniforms, 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 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) { if (preview.removed) continue; 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); setIDateUniform(gl, uniforms.iDate); 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); setIDateUniform(gl, uniforms.iDate); 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); } function syncStats() { statsEl.textContent = `预览数量: ${previews.length} | 全局时间: ${elapsed.toFixed(2)}s | 状态: ${ paused ? "已暂停" : "运行中" }`; if (shaderCountEl) { shaderCountEl.textContent = `${previews.length} shaders`; } } 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; }); for (const preset of presetShaders) { createPreviewCard(preset.name, preset.code); } syncStats(); requestAnimationFrame(renderAll);