(function () { const THUMB_W = 512; const THUMB_H = 320; const THUMB_TIME_CANDIDATES = [ 0, 0.1, 0.22, 0.45, 0.78, 1.15, 1.7, 2.25, 2.85, 3.45, 4.2, 5.1, 6.28, 7.5, 9, 11, 13, ]; const GL_CONTEXT_OPTS = { alpha: false, antialias: false, depth: false, stencil: false, powerPreference: "high-performance", desynchronized: true, }; 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"); 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; uniform vec4 iDate; uniform sampler2D iChannel0; uniform sampler2D iChannel1; uniform sampler2D iChannel2; uniform sampler2D iChannel3; ${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 (shader too complex or driver limits)."; } 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."; } gl.deleteProgram(program); throw new Error(`Program link:\n${info}`); } return program; } function createTexture(gl, width, height, data) { const tex = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, tex); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT); gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, data); gl.bindTexture(gl.TEXTURE_2D, null); return tex; } function createDefaultChannelTextures(gl, noiseSize) { const w0 = 512; const h0 = 2; const d0 = new Uint8Array(w0 * h0 * 4); for (let x = 0; x < w0; x++) { const t = x / (w0 - 1); const fft = Math.floor(255 * Math.pow(1.0 - t, 1.8)); const wave = Math.floor(128 + 127 * Math.sin(t * Math.PI * 10.0)); const i0 = (x + 0 * w0) * 4; const i1 = (x + 1 * w0) * 4; d0[i0] = fft; d0[i0 + 1] = fft; d0[i0 + 2] = fft; d0[i0 + 3] = 255; d0[i1] = wave; d0[i1 + 1] = wave; d0[i1 + 2] = wave; d0[i1 + 3] = 255; } const size = noiseSize; const makeNoise = (scaleA, scaleB) => { const d = new Uint8Array(size * size * 4); for (let y = 0; y < size; y++) { for (let x = 0; x < size; x++) { const i = (y * size + x) * 4; const n = Math.floor( 127 + 127 * Math.sin((x * scaleA + y * scaleB) * 0.11) * Math.cos((x * scaleB - y * scaleA) * 0.07) ); d[i] = n; d[i + 1] = (n * 37) & 255; d[i + 2] = (n * 73) & 255; d[i + 3] = 255; } } return d; }; return [ createTexture(gl, w0, h0, d0), createTexture(gl, size, size, makeNoise(1.0, 0.7)), createTexture(gl, size, size, makeNoise(0.8, 1.3)), createTexture(gl, size, size, makeNoise(1.7, 0.4)), ]; } function bindChannelTextures(gl, channelUniformLocs, channelTextures) { for (let i = 0; i < 4; i++) { if (channelUniformLocs[i] == null) continue; gl.activeTexture(gl.TEXTURE0 + i); gl.bindTexture(gl.TEXTURE_2D, channelTextures[i]); gl.uniform1i(channelUniformLocs[i], i); } gl.activeTexture(gl.TEXTURE0); } function sampleThumbStats(data, width, height) { const row = width * 4; let sumL = 0; let sumL2 = 0; let n = 0; for (let y = 0; y < height; y += 5) { for (let x = 0; x < width; x += 6) { const i = y * row + x * 4; if (i + 2 >= data.length) continue; const r = data[i] / 255; const g = data[i + 1] / 255; const b = data[i + 2] / 255; const L = 0.299 * r + 0.587 * g + 0.114 * b; sumL += L; sumL2 += L * L; n++; } } if (n < 4) return { mean: 0, varL: 0 }; const mean = sumL / n; const varL = Math.max(0, sumL2 / n - mean * mean); return { mean, varL }; } function isAcceptableThumb(mean, varL) { if (mean < 0.017 || mean > 0.99) return false; if (varL < 0.00065) return false; return true; } function thumbScore(mean, varL) { return varL * 85 + (1 - Math.abs(mean - 0.4) * 1.05); } window.captureShaderThumbnail = function captureShaderThumbnail(code, name) { const canvas = document.createElement("canvas"); canvas.width = THUMB_W; canvas.height = THUMB_H; const gl = canvas.getContext("webgl2", GL_CONTEXT_OPTS); if (!gl) { return Promise.reject(new Error("当前浏览器不支持 WebGL2")); } let program; let codeForRender = code; try { program = compileProgram(gl, buildFragmentShader(codeForRender)); } catch (_e1) { try { codeForRender = toPortableGlsl(code); program = compileProgram(gl, buildFragmentShader(codeForRender)); } catch (_e2) { codeForRender = fallbackShader(name); program = compileProgram(gl, buildFragmentShader(codeForRender)); } } const { vao } = createQuad(gl); const channelTextures = createDefaultChannelTextures(gl, 128); const channelUniforms = [ gl.getUniformLocation(program, "iChannel0"), gl.getUniformLocation(program, "iChannel1"), gl.getUniformLocation(program, "iChannel2"), gl.getUniformLocation(program, "iChannel3"), ]; 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"), }; gl.viewport(0, 0, THUMB_W, THUMB_H); gl.useProgram(program); bindChannelTextures(gl, channelUniforms, channelTextures); gl.uniform3f(uniforms.iResolution, THUMB_W, THUMB_H, 1); gl.uniform1f(uniforms.iTimeDelta, 0); setIDateUniform(gl, uniforms.iDate); gl.uniform4f(uniforms.iMouse, 0, 0, 0, 0); gl.bindVertexArray(vao); gl.pixelStorei(gl.PACK_ALIGNMENT, 1); const pixels = new Uint8Array(THUMB_W * THUMB_H * 4); let bestStrict = { t: 0.5, score: -1e9 }; let bestLoose = { t: 0.5, varL: -1 }; for (let ti = 0; ti < THUMB_TIME_CANDIDATES.length; ti++) { const t = THUMB_TIME_CANDIDATES[ti]; gl.uniform1f(uniforms.iTime, t); gl.uniform1i(uniforms.iFrame, Math.max(0, Math.floor(t * 60))); gl.drawArrays(gl.TRIANGLES, 0, 6); gl.readPixels(0, 0, THUMB_W, THUMB_H, gl.RGBA, gl.UNSIGNED_BYTE, pixels); const { mean, varL } = sampleThumbStats(pixels, THUMB_W, THUMB_H); if (varL > bestLoose.varL) bestLoose = { t, varL }; if (isAcceptableThumb(mean, varL)) { const sc = thumbScore(mean, varL); if (sc > bestStrict.score) bestStrict = { t, score: sc }; } } const pickT = bestStrict.score > -1e8 ? bestStrict.t : bestLoose.t; gl.uniform1f(uniforms.iTime, pickT); gl.uniform1i(uniforms.iFrame, Math.max(0, Math.floor(pickT * 60))); gl.drawArrays(gl.TRIANGLES, 0, 6); gl.bindVertexArray(null); const dataUrl = canvas.toDataURL("image/png"); const ext = gl.getExtension("WEBGL_lose_context"); if (ext) ext.loseContext(); return Promise.resolve(dataUrl); }; })();