353 lines
12 KiB
JavaScript
353 lines
12 KiB
JavaScript
(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);
|
|
};
|
|
})();
|