fix:bug
This commit is contained in:
4
admin.js
4
admin.js
@@ -15,7 +15,9 @@ const EXAMPLE = `void mainImage(out vec4 fragColor, in vec2 fragCoord) {
|
|||||||
}`;
|
}`;
|
||||||
|
|
||||||
function validateClientCode(code) {
|
function validateClientCode(code) {
|
||||||
if (!code.includes("mainImage")) return "代码需包含 mainImage。";
|
const hasMainImage = code.includes("mainImage");
|
||||||
|
const hasAngleMain = code.includes("_umainImage");
|
||||||
|
if (!hasMainImage && !hasAngleMain) return "代码需包含 mainImage 或 _umainImage。";
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
503
display.js
503
display.js
@@ -13,6 +13,27 @@ const backBtn = document.getElementById("back-btn");
|
|||||||
|
|
||||||
const API_BASE = "/api/shaders";
|
const API_BASE = "/api/shaders";
|
||||||
|
|
||||||
|
/** Grid previews: limit fill-rate so many cards can stay near 60fps (detail view uses full quality). */
|
||||||
|
const PREVIEW_MAX_PIXEL_RATIO = 1.25;
|
||||||
|
const PREVIEW_MAX_BUFFER_LONG_SIDE = 520;
|
||||||
|
const DETAIL_MAX_PIXEL_RATIO = 2;
|
||||||
|
const DETAIL_MAX_BUFFER_LONG_SIDE = 2560;
|
||||||
|
|
||||||
|
const GL_CONTEXT_OPTS = {
|
||||||
|
alpha: false,
|
||||||
|
antialias: false,
|
||||||
|
depth: false,
|
||||||
|
stencil: false,
|
||||||
|
powerPreference: "high-performance",
|
||||||
|
desynchronized: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Browsers cap concurrent WebGL contexts (~8); keep grid below so detail view + pool stay safe. */
|
||||||
|
const MAX_GRID_WEBGL_CONTEXTS = 7;
|
||||||
|
const WEBGL_TEARDOWN_DEBOUNCE_MS = 450;
|
||||||
|
|
||||||
|
let gridGlTouchCounter = 0;
|
||||||
|
|
||||||
const vertexShaderSource = `#version 300 es
|
const vertexShaderSource = `#version 300 es
|
||||||
precision highp float;
|
precision highp float;
|
||||||
layout(location = 0) in vec2 aPosition;
|
layout(location = 0) in vec2 aPosition;
|
||||||
@@ -85,6 +106,10 @@ uniform float iTime;
|
|||||||
uniform float iTimeDelta;
|
uniform float iTimeDelta;
|
||||||
uniform int iFrame;
|
uniform int iFrame;
|
||||||
uniform vec4 iMouse;
|
uniform vec4 iMouse;
|
||||||
|
uniform sampler2D iChannel0;
|
||||||
|
uniform sampler2D iChannel1;
|
||||||
|
uniform sampler2D iChannel2;
|
||||||
|
uniform sampler2D iChannel3;
|
||||||
${userCode}
|
${userCode}
|
||||||
void main() { mainImage(outColor, gl_FragCoord.xy); }`;
|
void main() { mainImage(outColor, gl_FragCoord.xy); }`;
|
||||||
}
|
}
|
||||||
@@ -130,7 +155,253 @@ function compileProgram(gl, fragmentShaderSource) {
|
|||||||
return program;
|
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 = 256) {
|
||||||
|
// iChannel0: audio-like lookup texture (matches many shadertoy audio assumptions)
|
||||||
|
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));
|
||||||
|
// row0: fft, row1: waveform
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// iChannel1/2/3: tiling noise/grain-like textures (smaller in grid previews).
|
||||||
|
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 computePreviewBufferSize(cssW, cssH) {
|
||||||
|
const dpr = Math.min(window.devicePixelRatio || 1, PREVIEW_MAX_PIXEL_RATIO);
|
||||||
|
let w = Math.max(1, Math.floor(cssW * dpr));
|
||||||
|
let h = Math.max(1, Math.floor(cssH * dpr));
|
||||||
|
const long = Math.max(w, h);
|
||||||
|
if (long > PREVIEW_MAX_BUFFER_LONG_SIDE) {
|
||||||
|
const s = PREVIEW_MAX_BUFFER_LONG_SIDE / long;
|
||||||
|
w = Math.max(1, Math.floor(w * s));
|
||||||
|
h = Math.max(1, Math.floor(h * s));
|
||||||
|
}
|
||||||
|
return { w, h };
|
||||||
|
}
|
||||||
|
|
||||||
|
function computeDetailBufferSize(cssW, cssH) {
|
||||||
|
const dpr = Math.min(window.devicePixelRatio || 1, DETAIL_MAX_PIXEL_RATIO);
|
||||||
|
let w = Math.max(1, Math.floor(cssW * dpr));
|
||||||
|
let h = Math.max(1, Math.floor(cssH * dpr));
|
||||||
|
const long = Math.max(w, h);
|
||||||
|
if (long > DETAIL_MAX_BUFFER_LONG_SIDE) {
|
||||||
|
const s = DETAIL_MAX_BUFFER_LONG_SIDE / long;
|
||||||
|
w = Math.max(1, Math.floor(w * s));
|
||||||
|
h = Math.max(1, Math.floor(h * s));
|
||||||
|
}
|
||||||
|
return { w, h };
|
||||||
|
}
|
||||||
|
|
||||||
|
function countGridWebGLContexts() {
|
||||||
|
let n = 0;
|
||||||
|
for (const p of previews) {
|
||||||
|
if (p.gl) n += 1;
|
||||||
|
}
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
function tearDownPreviewWebGL(state) {
|
||||||
|
if (state._tearDownTimer) {
|
||||||
|
clearTimeout(state._tearDownTimer);
|
||||||
|
state._tearDownTimer = null;
|
||||||
|
}
|
||||||
|
state._staticRendered = false;
|
||||||
|
state._staticDirty = true;
|
||||||
|
if (!state.gl) return;
|
||||||
|
const gl = state.gl;
|
||||||
|
try {
|
||||||
|
if (state.program) gl.deleteProgram(state.program);
|
||||||
|
if (state.vao) gl.deleteVertexArray(state.vao);
|
||||||
|
if (state.vbo) gl.deleteBuffer(state.vbo);
|
||||||
|
if (state.channelTextures) state.channelTextures.forEach((t) => gl.deleteTexture(t));
|
||||||
|
} catch (_) {
|
||||||
|
/* context may already be lost */
|
||||||
|
}
|
||||||
|
const ext = gl.getExtension("WEBGL_lose_context");
|
||||||
|
if (ext) ext.loseContext();
|
||||||
|
state.gl = null;
|
||||||
|
state.program = null;
|
||||||
|
state.vao = null;
|
||||||
|
state.vbo = null;
|
||||||
|
state.channelTextures = null;
|
||||||
|
state.channelUniforms = null;
|
||||||
|
state.uniforms = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Only evict GPU for cards that are not visible / filtered out — avoids thrash when many cards are on-screen. */
|
||||||
|
function evictOffscreenGridWebGLContext() {
|
||||||
|
const off = previews.filter((p) => p.gl && (!p.visible || p.card.style.display === "none"));
|
||||||
|
if (!off.length) return false;
|
||||||
|
off.sort((a, b) => (a._glTouchSeq || 0) - (b._glTouchSeq || 0));
|
||||||
|
tearDownPreviewWebGL(off[0]);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensurePreviewWebGL(state) {
|
||||||
|
if (state.gl || state.compileFailed) return;
|
||||||
|
const errorEl = state.card.querySelector(".error");
|
||||||
|
while (countGridWebGLContexts() >= MAX_GRID_WEBGL_CONTEXTS) {
|
||||||
|
if (!evictOffscreenGridWebGLContext()) break;
|
||||||
|
}
|
||||||
|
if (countGridWebGLContexts() >= MAX_GRID_WEBGL_CONTEXTS) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const gl = state.canvas.getContext("webgl2", GL_CONTEXT_OPTS);
|
||||||
|
if (!gl) {
|
||||||
|
errorEl.textContent = "当前浏览器不支持 WebGL2";
|
||||||
|
state.compileFailed = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let program;
|
||||||
|
let codeForRender = state.code;
|
||||||
|
try {
|
||||||
|
program = compileProgram(gl, buildFragmentShader(codeForRender));
|
||||||
|
errorEl.textContent = "";
|
||||||
|
} catch (_err1) {
|
||||||
|
try {
|
||||||
|
codeForRender = toPortableGlsl(state.code);
|
||||||
|
program = compileProgram(gl, buildFragmentShader(codeForRender));
|
||||||
|
errorEl.textContent = "";
|
||||||
|
} catch (_err2) {
|
||||||
|
codeForRender = fallbackShader(state.name);
|
||||||
|
try {
|
||||||
|
program = compileProgram(gl, buildFragmentShader(codeForRender));
|
||||||
|
errorEl.textContent = "";
|
||||||
|
} catch (err3) {
|
||||||
|
errorEl.textContent = `编译失败:\n${String(err3.message || err3)}`;
|
||||||
|
state.compileFailed = true;
|
||||||
|
const ext = gl.getExtension("WEBGL_lose_context");
|
||||||
|
if (ext) ext.loseContext();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
state.codeForRender = codeForRender;
|
||||||
|
const { vao, vbo } = 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"),
|
||||||
|
};
|
||||||
|
state.gl = gl;
|
||||||
|
state.program = program;
|
||||||
|
state.vao = vao;
|
||||||
|
state.vbo = vbo;
|
||||||
|
state.channelTextures = channelTextures;
|
||||||
|
state.channelUniforms = channelUniforms;
|
||||||
|
state.uniforms = uniforms;
|
||||||
|
state._glTouchSeq = ++gridGlTouchCounter;
|
||||||
|
state._staticDirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
const previews = [];
|
const previews = [];
|
||||||
|
/** Driven by scheduleRender(); only runs while detail / hover / pending static thumbs need a frame. */
|
||||||
|
let renderRafId = 0;
|
||||||
|
|
||||||
|
function scheduleRender() {
|
||||||
|
if (renderRafId) return;
|
||||||
|
renderRafId = requestAnimationFrame((ts) => {
|
||||||
|
renderRafId = 0;
|
||||||
|
renderAll(ts);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldKeepAnimatingGrid() {
|
||||||
|
for (const p of previews) {
|
||||||
|
if (p.card.style.display === "none") continue;
|
||||||
|
if (!p.visible) continue;
|
||||||
|
if (p.isPointerOver) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function needsStaticThumbnailPass() {
|
||||||
|
for (const p of previews) {
|
||||||
|
if (p.card.style.display === "none") continue;
|
||||||
|
if (!p.visible) continue;
|
||||||
|
if (p.isPointerOver) continue;
|
||||||
|
if (p.compileFailed) continue;
|
||||||
|
if (p._staticRendered && !p._staticDirty) continue;
|
||||||
|
if (!p.gl && countGridWebGLContexts() >= MAX_GRID_WEBGL_CONTEXTS) continue;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldScheduleNextFrame() {
|
||||||
|
if (detailRuntime) return true;
|
||||||
|
if (shouldKeepAnimatingGrid()) return true;
|
||||||
|
if (needsStaticThumbnailPass()) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
let paused = false;
|
let paused = false;
|
||||||
let elapsed = 0;
|
let elapsed = 0;
|
||||||
let frame = 0;
|
let frame = 0;
|
||||||
@@ -168,64 +439,57 @@ function createPreviewCard(shader) {
|
|||||||
const canvas = card.querySelector("canvas");
|
const canvas = card.querySelector("canvas");
|
||||||
const errorEl = card.querySelector(".error");
|
const errorEl = card.querySelector(".error");
|
||||||
const pauseLocalBtn = card.querySelector(".pause-local-btn");
|
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 = {
|
const state = {
|
||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
code,
|
code,
|
||||||
codeForRender,
|
codeForRender: code,
|
||||||
|
compileFailed: false,
|
||||||
card,
|
card,
|
||||||
canvas,
|
canvas,
|
||||||
gl,
|
gl: null,
|
||||||
program,
|
program: null,
|
||||||
vao,
|
vao: null,
|
||||||
uniforms,
|
vbo: null,
|
||||||
|
channelTextures: null,
|
||||||
|
channelUniforms: null,
|
||||||
|
uniforms: null,
|
||||||
mouse: { x: 0, y: 0, downX: 0, downY: 0, down: false },
|
mouse: { x: 0, y: 0, downX: 0, downY: 0, down: false },
|
||||||
isPlaying: true,
|
isPlaying: true,
|
||||||
localTime: 0,
|
localTime: 0,
|
||||||
localFrame: 0,
|
localFrame: 0,
|
||||||
lastTick: performance.now(),
|
lastTick: performance.now(),
|
||||||
|
visible: false,
|
||||||
|
_layoutW: 0,
|
||||||
|
_layoutH: 0,
|
||||||
|
_glTouchSeq: 0,
|
||||||
|
_tearDownTimer: null,
|
||||||
|
isPointerOver: false,
|
||||||
|
_staticRendered: false,
|
||||||
|
_staticDirty: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
card.addEventListener("mouseenter", () => {
|
||||||
|
state.isPointerOver = true;
|
||||||
|
state.lastTick = performance.now();
|
||||||
|
scheduleRender();
|
||||||
|
});
|
||||||
|
card.addEventListener("mouseleave", () => {
|
||||||
|
state.isPointerOver = false;
|
||||||
|
state.localTime = 0;
|
||||||
|
state.localFrame = 0;
|
||||||
|
state._staticDirty = true;
|
||||||
|
state._staticRendered = false;
|
||||||
|
scheduleRender();
|
||||||
|
});
|
||||||
|
|
||||||
canvas.addEventListener("mousemove", (event) => {
|
canvas.addEventListener("mousemove", (event) => {
|
||||||
const rect = canvas.getBoundingClientRect();
|
const rect = canvas.getBoundingClientRect();
|
||||||
state.mouse.x = ((event.clientX - rect.left) / rect.width) * canvas.width;
|
const bw = canvas.width || Math.max(1, state._layoutW || rect.width);
|
||||||
state.mouse.y = ((rect.bottom - event.clientY) / rect.height) * canvas.height;
|
const bh = canvas.height || Math.max(1, state._layoutH || rect.height);
|
||||||
|
state.mouse.x = ((event.clientX - rect.left) / rect.width) * bw;
|
||||||
|
state.mouse.y = ((rect.bottom - event.clientY) / rect.height) * bh;
|
||||||
});
|
});
|
||||||
canvas.addEventListener("mousedown", () => {
|
canvas.addEventListener("mousedown", () => {
|
||||||
state.mouse.down = true;
|
state.mouse.down = true;
|
||||||
@@ -241,15 +505,63 @@ function createPreviewCard(shader) {
|
|||||||
state.isPlaying = !state.isPlaying;
|
state.isPlaying = !state.isPlaying;
|
||||||
state.lastTick = performance.now();
|
state.lastTick = performance.now();
|
||||||
pauseLocalBtn.textContent = state.isPlaying ? "暂停" : "继续";
|
pauseLocalBtn.textContent = state.isPlaying ? "暂停" : "继续";
|
||||||
|
scheduleRender();
|
||||||
});
|
});
|
||||||
canvas.addEventListener("click", () => openDetail(state));
|
canvas.addEventListener("click", () => openDetail(state));
|
||||||
|
|
||||||
gridEl.appendChild(card);
|
gridEl.appendChild(card);
|
||||||
|
|
||||||
|
const ro = new ResizeObserver((entries) => {
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (entry.target !== canvas) continue;
|
||||||
|
const cr = entry.contentRect;
|
||||||
|
state._layoutW = cr.width;
|
||||||
|
state._layoutH = cr.height;
|
||||||
|
state._staticDirty = true;
|
||||||
|
scheduleRender();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ro.observe(canvas);
|
||||||
|
state._resizeObserver = ro;
|
||||||
|
|
||||||
|
const io = new IntersectionObserver(
|
||||||
|
(entries) => {
|
||||||
|
for (const e of entries) {
|
||||||
|
if (e.target !== canvas) continue;
|
||||||
|
state.visible = e.isIntersecting;
|
||||||
|
if (e.isIntersecting) {
|
||||||
|
if (state._tearDownTimer) {
|
||||||
|
clearTimeout(state._tearDownTimer);
|
||||||
|
state._tearDownTimer = null;
|
||||||
|
}
|
||||||
|
state._staticDirty = true;
|
||||||
|
state._staticRendered = false;
|
||||||
|
ensurePreviewWebGL(state);
|
||||||
|
scheduleRender();
|
||||||
|
} else {
|
||||||
|
if (state._tearDownTimer) clearTimeout(state._tearDownTimer);
|
||||||
|
state._tearDownTimer = setTimeout(() => {
|
||||||
|
state._tearDownTimer = null;
|
||||||
|
if (!state.visible) tearDownPreviewWebGL(state);
|
||||||
|
}, WEBGL_TEARDOWN_DEBOUNCE_MS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ root: null, rootMargin: "120px", threshold: 0.01 }
|
||||||
|
);
|
||||||
|
io.observe(canvas);
|
||||||
|
state._io = io;
|
||||||
|
|
||||||
previews.push(state);
|
previews.push(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearPreviews() {
|
function clearPreviews() {
|
||||||
previews.forEach((preview) => preview.card.remove());
|
previews.forEach((preview) => {
|
||||||
|
tearDownPreviewWebGL(preview);
|
||||||
|
if (preview._resizeObserver) preview._resizeObserver.disconnect();
|
||||||
|
if (preview._io) preview._io.disconnect();
|
||||||
|
preview.card.remove();
|
||||||
|
});
|
||||||
previews.length = 0;
|
previews.length = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -257,16 +569,18 @@ function closeDetail() {
|
|||||||
detailViewEl.classList.remove("active");
|
detailViewEl.classList.remove("active");
|
||||||
detailViewEl.setAttribute("aria-hidden", "true");
|
detailViewEl.setAttribute("aria-hidden", "true");
|
||||||
if (!detailRuntime) return;
|
if (!detailRuntime) return;
|
||||||
const { gl, program, vao, vbo } = detailRuntime;
|
const { gl, program, vao, vbo, channelTextures = [], _detailResizeObserver } = detailRuntime;
|
||||||
|
if (_detailResizeObserver) _detailResizeObserver.disconnect();
|
||||||
gl.deleteProgram(program);
|
gl.deleteProgram(program);
|
||||||
gl.deleteVertexArray(vao);
|
gl.deleteVertexArray(vao);
|
||||||
gl.deleteBuffer(vbo);
|
gl.deleteBuffer(vbo);
|
||||||
|
channelTextures.forEach((tex) => gl.deleteTexture(tex));
|
||||||
detailRuntime = null;
|
detailRuntime = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function openDetail(previewState) {
|
function openDetail(previewState) {
|
||||||
closeDetail();
|
closeDetail();
|
||||||
const gl = detailCanvas.getContext("webgl2", { antialias: false, alpha: false });
|
const gl = detailCanvas.getContext("webgl2", GL_CONTEXT_OPTS);
|
||||||
if (!gl) return;
|
if (!gl) return;
|
||||||
let program;
|
let program;
|
||||||
const detailCode = previewState.codeForRender || previewState.code;
|
const detailCode = previewState.codeForRender || previewState.code;
|
||||||
@@ -277,11 +591,20 @@ function openDetail(previewState) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const { vao, vbo } = createQuad(gl);
|
const { vao, vbo } = createQuad(gl);
|
||||||
|
const channelTextures = createDefaultChannelTextures(gl, 256);
|
||||||
|
const channelUniforms = [
|
||||||
|
gl.getUniformLocation(program, "iChannel0"),
|
||||||
|
gl.getUniformLocation(program, "iChannel1"),
|
||||||
|
gl.getUniformLocation(program, "iChannel2"),
|
||||||
|
gl.getUniformLocation(program, "iChannel3"),
|
||||||
|
];
|
||||||
detailRuntime = {
|
detailRuntime = {
|
||||||
gl,
|
gl,
|
||||||
program,
|
program,
|
||||||
vao,
|
vao,
|
||||||
vbo,
|
vbo,
|
||||||
|
channelTextures,
|
||||||
|
channelUniforms,
|
||||||
uniforms: {
|
uniforms: {
|
||||||
iResolution: gl.getUniformLocation(program, "iResolution"),
|
iResolution: gl.getUniformLocation(program, "iResolution"),
|
||||||
iTime: gl.getUniformLocation(program, "iTime"),
|
iTime: gl.getUniformLocation(program, "iTime"),
|
||||||
@@ -290,10 +613,24 @@ function openDetail(previewState) {
|
|||||||
iMouse: gl.getUniformLocation(program, "iMouse"),
|
iMouse: gl.getUniformLocation(program, "iMouse"),
|
||||||
},
|
},
|
||||||
mouse: { x: 0, y: 0, downX: 0, downY: 0, down: false },
|
mouse: { x: 0, y: 0, downX: 0, downY: 0, down: false },
|
||||||
|
_layoutW: 0,
|
||||||
|
_layoutH: 0,
|
||||||
};
|
};
|
||||||
|
const dro = new ResizeObserver((entries) => {
|
||||||
|
for (const e of entries) {
|
||||||
|
if (e.target !== detailCanvas) continue;
|
||||||
|
const cr = e.contentRect;
|
||||||
|
detailRuntime._layoutW = cr.width;
|
||||||
|
detailRuntime._layoutH = cr.height;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
dro.observe(detailCanvas);
|
||||||
|
detailRuntime._detailResizeObserver = dro;
|
||||||
|
|
||||||
detailTitleEl.textContent = previewState.name;
|
detailTitleEl.textContent = previewState.name;
|
||||||
detailViewEl.classList.add("active");
|
detailViewEl.classList.add("active");
|
||||||
detailViewEl.setAttribute("aria-hidden", "false");
|
detailViewEl.setAttribute("aria-hidden", "false");
|
||||||
|
scheduleRender();
|
||||||
}
|
}
|
||||||
|
|
||||||
function applySearch() {
|
function applySearch() {
|
||||||
@@ -301,6 +638,7 @@ function applySearch() {
|
|||||||
for (const preview of previews) {
|
for (const preview of previews) {
|
||||||
preview.card.style.display = !keyword || preview.card.dataset.search.includes(keyword) ? "" : "none";
|
preview.card.style.display = !keyword || preview.card.dataset.search.includes(keyword) ? "" : "none";
|
||||||
}
|
}
|
||||||
|
scheduleRender();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadShaders() {
|
async function loadShaders() {
|
||||||
@@ -346,29 +684,63 @@ function renderAll(ts) {
|
|||||||
fpsCounter += 1;
|
fpsCounter += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const detailOpen = !!detailRuntime;
|
||||||
|
|
||||||
for (const preview of previews) {
|
for (const preview of previews) {
|
||||||
const { gl, canvas, program, vao, uniforms, mouse } = preview;
|
let localDt = 0;
|
||||||
|
if (!detailOpen) {
|
||||||
const now = performance.now();
|
const now = performance.now();
|
||||||
const localDt = Math.min((now - preview.lastTick) / 1000, 0.05);
|
localDt = Math.min((now - preview.lastTick) / 1000, 0.05);
|
||||||
preview.lastTick = now;
|
preview.lastTick = now;
|
||||||
if (preview.isPlaying && !paused) {
|
if (preview.isPointerOver && preview.isPlaying && !paused) {
|
||||||
preview.localTime += localDt;
|
preview.localTime += localDt;
|
||||||
preview.localFrame += 1;
|
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));
|
if (
|
||||||
const h = Math.max(1, Math.floor(rect.height * dpr));
|
!detailOpen &&
|
||||||
|
preview.card.style.display !== "none" &&
|
||||||
|
preview.visible &&
|
||||||
|
!preview.gl &&
|
||||||
|
!preview.compileFailed
|
||||||
|
) {
|
||||||
|
ensurePreviewWebGL(preview);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (detailOpen) continue;
|
||||||
|
if (preview.card.style.display === "none") continue;
|
||||||
|
if (preview.visible === false) continue;
|
||||||
|
if (!preview.gl) continue;
|
||||||
|
|
||||||
|
const isAnim = preview.isPointerOver;
|
||||||
|
if (!isAnim && preview._staticRendered && !preview._staticDirty) continue;
|
||||||
|
|
||||||
|
const { gl, canvas, program, vao, uniforms, mouse, channelTextures, channelUniforms } = preview;
|
||||||
|
if (!preview._layoutW || !preview._layoutH) {
|
||||||
|
const r = canvas.getBoundingClientRect();
|
||||||
|
preview._layoutW = r.width;
|
||||||
|
preview._layoutH = r.height;
|
||||||
|
}
|
||||||
|
const { w, h } = computePreviewBufferSize(preview._layoutW, preview._layoutH);
|
||||||
if (canvas.width !== w || canvas.height !== h) {
|
if (canvas.width !== w || canvas.height !== h) {
|
||||||
canvas.width = w;
|
canvas.width = w;
|
||||||
canvas.height = h;
|
canvas.height = h;
|
||||||
gl.viewport(0, 0, w, h);
|
gl.viewport(0, 0, w, h);
|
||||||
|
if (!isAnim) preview._staticDirty = true;
|
||||||
}
|
}
|
||||||
gl.useProgram(program);
|
gl.useProgram(program);
|
||||||
|
bindChannelTextures(gl, channelUniforms, channelTextures);
|
||||||
gl.uniform3f(uniforms.iResolution, canvas.width, canvas.height, 1);
|
gl.uniform3f(uniforms.iResolution, canvas.width, canvas.height, 1);
|
||||||
|
if (isAnim) {
|
||||||
gl.uniform1f(uniforms.iTime, preview.localTime);
|
gl.uniform1f(uniforms.iTime, preview.localTime);
|
||||||
gl.uniform1f(uniforms.iTimeDelta, preview.isPlaying && !paused ? localDt : 0);
|
gl.uniform1f(uniforms.iTimeDelta, preview.isPlaying && !paused ? localDt : 0);
|
||||||
gl.uniform1i(uniforms.iFrame, preview.localFrame);
|
gl.uniform1i(uniforms.iFrame, preview.localFrame);
|
||||||
|
} else {
|
||||||
|
gl.uniform1f(uniforms.iTime, 0);
|
||||||
|
gl.uniform1f(uniforms.iTimeDelta, 0);
|
||||||
|
gl.uniform1i(uniforms.iFrame, 0);
|
||||||
|
}
|
||||||
gl.uniform4f(
|
gl.uniform4f(
|
||||||
uniforms.iMouse,
|
uniforms.iMouse,
|
||||||
mouse.x,
|
mouse.x,
|
||||||
@@ -379,19 +751,27 @@ function renderAll(ts) {
|
|||||||
gl.bindVertexArray(vao);
|
gl.bindVertexArray(vao);
|
||||||
gl.drawArrays(gl.TRIANGLES, 0, 6);
|
gl.drawArrays(gl.TRIANGLES, 0, 6);
|
||||||
gl.bindVertexArray(null);
|
gl.bindVertexArray(null);
|
||||||
|
preview._glTouchSeq = ++gridGlTouchCounter;
|
||||||
|
if (!isAnim) {
|
||||||
|
preview._staticRendered = true;
|
||||||
|
preview._staticDirty = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (detailRuntime) {
|
if (detailRuntime) {
|
||||||
const { gl, program, vao, uniforms, mouse } = detailRuntime;
|
const { gl, program, vao, uniforms, mouse, channelTextures, channelUniforms } = detailRuntime;
|
||||||
const dpr = Math.min(window.devicePixelRatio || 1, 2);
|
const lw =
|
||||||
const w = Math.max(1, Math.floor(detailCanvas.clientWidth * dpr));
|
detailRuntime._layoutW > 0 ? detailRuntime._layoutW : detailCanvas.clientWidth || 1;
|
||||||
const h = Math.max(1, Math.floor(detailCanvas.clientHeight * dpr));
|
const lh =
|
||||||
|
detailRuntime._layoutH > 0 ? detailRuntime._layoutH : detailCanvas.clientHeight || 1;
|
||||||
|
const { w, h } = computeDetailBufferSize(lw, lh);
|
||||||
if (detailCanvas.width !== w || detailCanvas.height !== h) {
|
if (detailCanvas.width !== w || detailCanvas.height !== h) {
|
||||||
detailCanvas.width = w;
|
detailCanvas.width = w;
|
||||||
detailCanvas.height = h;
|
detailCanvas.height = h;
|
||||||
gl.viewport(0, 0, w, h);
|
gl.viewport(0, 0, w, h);
|
||||||
}
|
}
|
||||||
gl.useProgram(program);
|
gl.useProgram(program);
|
||||||
|
bindChannelTextures(gl, channelUniforms, channelTextures);
|
||||||
gl.uniform3f(uniforms.iResolution, detailCanvas.width, detailCanvas.height, 1);
|
gl.uniform3f(uniforms.iResolution, detailCanvas.width, detailCanvas.height, 1);
|
||||||
gl.uniform1f(uniforms.iTime, elapsed);
|
gl.uniform1f(uniforms.iTime, elapsed);
|
||||||
gl.uniform1f(uniforms.iTimeDelta, dt);
|
gl.uniform1f(uniforms.iTimeDelta, dt);
|
||||||
@@ -416,13 +796,14 @@ function renderAll(ts) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
requestAnimationFrame(renderAll);
|
if (shouldScheduleNextFrame()) scheduleRender();
|
||||||
}
|
}
|
||||||
|
|
||||||
pauseBtn.addEventListener("click", () => {
|
pauseBtn.addEventListener("click", () => {
|
||||||
paused = !paused;
|
paused = !paused;
|
||||||
pauseBtn.textContent = paused ? "全局继续" : "全局暂停";
|
pauseBtn.textContent = paused ? "全局继续" : "全局暂停";
|
||||||
syncStats();
|
syncStats();
|
||||||
|
scheduleRender();
|
||||||
});
|
});
|
||||||
resetBtn.addEventListener("click", () => {
|
resetBtn.addEventListener("click", () => {
|
||||||
elapsed = 0;
|
elapsed = 0;
|
||||||
@@ -432,8 +813,12 @@ resetBtn.addEventListener("click", () => {
|
|||||||
detailLastSec = 0;
|
detailLastSec = 0;
|
||||||
detailFpsCounter = 0;
|
detailFpsCounter = 0;
|
||||||
syncStats();
|
syncStats();
|
||||||
|
scheduleRender();
|
||||||
|
});
|
||||||
|
backBtn.addEventListener("click", () => {
|
||||||
|
closeDetail();
|
||||||
|
scheduleRender();
|
||||||
});
|
});
|
||||||
backBtn.addEventListener("click", closeDetail);
|
|
||||||
detailCanvas.addEventListener("mousemove", (event) => {
|
detailCanvas.addEventListener("mousemove", (event) => {
|
||||||
if (!detailRuntime) return;
|
if (!detailRuntime) return;
|
||||||
const rect = detailCanvas.getBoundingClientRect();
|
const rect = detailCanvas.getBoundingClientRect();
|
||||||
@@ -457,4 +842,4 @@ searchInput.addEventListener("input", (event) => {
|
|||||||
|
|
||||||
loadShaders();
|
loadShaders();
|
||||||
setInterval(loadShaders, 3000);
|
setInterval(loadShaders, 3000);
|
||||||
requestAnimationFrame(renderAll);
|
scheduleRender();
|
||||||
|
|||||||
13
index.html
13
index.html
@@ -100,6 +100,14 @@
|
|||||||
color: #c3c5cd;
|
color: #c3c5cd;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.view-hint {
|
||||||
|
margin: 8px 0 0;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.45;
|
||||||
|
color: #8b92a8;
|
||||||
|
max-width: 720px;
|
||||||
|
}
|
||||||
|
|
||||||
.main {
|
.main {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
}
|
}
|
||||||
@@ -124,6 +132,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
|
content-visibility: auto;
|
||||||
|
contain-intrinsic-size: 300px 220px;
|
||||||
border: 1px solid rgba(104, 129, 255, 0.35);
|
border: 1px solid rgba(104, 129, 255, 0.35);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -294,6 +304,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stats" id="stats">准备就绪</div>
|
<div class="stats" id="stats">准备就绪</div>
|
||||||
|
<p class="view-hint">
|
||||||
|
列表默认仅显示首帧静态预览;鼠标悬停在卡片上时才会持续渲染动画。点击进入全屏仍为实时播放。
|
||||||
|
</p>
|
||||||
<section id="preview-grid" class="preview-grid"></section>
|
<section id="preview-grid" class="preview-grid"></section>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
119
server.js
119
server.js
@@ -81,6 +81,35 @@ function extractFunctionBlocks(code) {
|
|||||||
return blocks;
|
return blocks;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildFunctionMap(blocks) {
|
||||||
|
const map = new Map();
|
||||||
|
for (const block of blocks) {
|
||||||
|
const m = block.match(/^\s*(?:void|float|vec[234]|mat[234]|int|uint)\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(/m);
|
||||||
|
if (!m) continue;
|
||||||
|
map.set(m[1], block);
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectFunctionDependencies(fnMap, roots) {
|
||||||
|
const keep = new Set();
|
||||||
|
const queue = [...roots];
|
||||||
|
while (queue.length) {
|
||||||
|
const name = queue.shift();
|
||||||
|
if (keep.has(name)) continue;
|
||||||
|
const block = fnMap.get(name);
|
||||||
|
if (!block) continue;
|
||||||
|
keep.add(name);
|
||||||
|
const callPattern = /\b([A-Za-z_][A-Za-z0-9_]*)\s*\(/g;
|
||||||
|
let m;
|
||||||
|
while ((m = callPattern.exec(block)) !== null) {
|
||||||
|
const callee = m[1];
|
||||||
|
if (fnMap.has(callee) && !keep.has(callee)) queue.push(callee);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return keep;
|
||||||
|
}
|
||||||
|
|
||||||
function convertAngleLikeToGlsl(code) {
|
function convertAngleLikeToGlsl(code) {
|
||||||
let s = code;
|
let s = code;
|
||||||
const cut = s.indexOf("fragment ANGLE_FragmentOut");
|
const cut = s.indexOf("fragment ANGLE_FragmentOut");
|
||||||
@@ -110,6 +139,14 @@ function convertAngleLikeToGlsl(code) {
|
|||||||
.replace(/\bANGLE_userUniforms\._uiTime\b/g, "iTime")
|
.replace(/\bANGLE_userUniforms\._uiTime\b/g, "iTime")
|
||||||
.replace(/\bANGLE_userUniforms\._uiMouse\b/g, "iMouse")
|
.replace(/\bANGLE_userUniforms\._uiMouse\b/g, "iMouse")
|
||||||
.replace(/\bANGLE_userUniforms\./g, "")
|
.replace(/\bANGLE_userUniforms\./g, "")
|
||||||
|
.replace(/ANGLE_texture\(\s*ANGLE_textureEnvs\._uiChannel0\s*,\s*([^)]+)\)/g, "texture(iChannel0, $1)")
|
||||||
|
.replace(/ANGLE_texture\(\s*ANGLE_textureEnvs\._uiChannel1\s*,\s*([^)]+)\)/g, "texture(iChannel1, $1)")
|
||||||
|
.replace(/ANGLE_texture\(\s*ANGLE_textureEnvs\._uiChannel2\s*,\s*([^)]+)\)/g, "texture(iChannel2, $1)")
|
||||||
|
.replace(/ANGLE_texture\(\s*ANGLE_textureEnvs\._uiChannel3\s*,\s*([^)]+)\)/g, "texture(iChannel3, $1)")
|
||||||
|
.replace(/ANGLE_texelFetch\(\s*ANGLE_textureEnvs\._uiChannel0\s*,\s*([^,]+)\s*,\s*([^)]+)\)/g, "texelFetch(iChannel0, $1, $2)")
|
||||||
|
.replace(/ANGLE_texelFetch\(\s*ANGLE_textureEnvs\._uiChannel1\s*,\s*([^,]+)\s*,\s*([^)]+)\)/g, "texelFetch(iChannel1, $1, $2)")
|
||||||
|
.replace(/ANGLE_texelFetch\(\s*ANGLE_textureEnvs\._uiChannel2\s*,\s*([^,]+)\s*,\s*([^)]+)\)/g, "texelFetch(iChannel2, $1, $2)")
|
||||||
|
.replace(/ANGLE_texelFetch\(\s*ANGLE_textureEnvs\._uiChannel3\s*,\s*([^,]+)\s*,\s*([^)]+)\)/g, "texelFetch(iChannel3, $1, $2)")
|
||||||
.replace(/ANGLE_texture\([^)]*\)/g, "vec4(0.0)")
|
.replace(/ANGLE_texture\([^)]*\)/g, "vec4(0.0)")
|
||||||
.replace(/ANGLE_texelFetch\([^)]*\)/g, "vec4(0.0)")
|
.replace(/ANGLE_texelFetch\([^)]*\)/g, "vec4(0.0)")
|
||||||
.replace(/\bANGLE_mod\(/g, "mod(")
|
.replace(/\bANGLE_mod\(/g, "mod(")
|
||||||
@@ -123,14 +160,21 @@ function convertAngleLikeToGlsl(code) {
|
|||||||
.replace(/\(\s*,/g, "(")
|
.replace(/\(\s*,/g, "(")
|
||||||
.replace(/,\s*,/g, ",")
|
.replace(/,\s*,/g, ",")
|
||||||
.replace(/,\s*\)/g, ")")
|
.replace(/,\s*\)/g, ")")
|
||||||
|
.replace(/\bANGLE_nonConstGlobals\./g, "")
|
||||||
.replace(/(\d+\.\d+|\d+)f\b/g, "$1")
|
.replace(/(\d+\.\d+|\d+)f\b/g, "$1")
|
||||||
.replace(/;\s*;/g, ";");
|
.replace(/;\s*;/g, ";");
|
||||||
|
|
||||||
s = s.replace(/void\s+_umainImage\s*\([^)]*\)/g, "void _umainImage(out vec4 _ufragColor, vec2 _ufragCoord)");
|
s = s.replace(/void\s+_umainImage\s*\([^)]*\)/g, "void _umainImage(out vec4 _ufragColor, vec2 _ufragCoord)");
|
||||||
|
|
||||||
const blocks = extractFunctionBlocks(s).filter((b) =>
|
const fnBlocks = extractFunctionBlocks(s).filter((b) =>
|
||||||
/(ANGLE_sc|ANGLE_sd|_u|mainImage|ANGLE_loopForwardProgress|_umainImage)/.test(b)
|
/(ANGLE_sc|ANGLE_sd|_u|mainImage|ANGLE_loopForwardProgress|_umainImage)/.test(b)
|
||||||
);
|
);
|
||||||
|
const fnMap = buildFunctionMap(fnBlocks);
|
||||||
|
const keepNames = collectFunctionDependencies(fnMap, ["mainImage", "_umainImage"]);
|
||||||
|
if (fnMap.has("ANGLE_loopForwardProgress")) keepNames.add("ANGLE_loopForwardProgress");
|
||||||
|
const blocks = [...fnMap.entries()]
|
||||||
|
.filter(([name]) => keepNames.has(name))
|
||||||
|
.map(([, block]) => block);
|
||||||
|
|
||||||
if (!blocks.length || !s.includes("_umainImage")) return "";
|
if (!blocks.length || !s.includes("_umainImage")) return "";
|
||||||
|
|
||||||
@@ -153,6 +197,42 @@ function convertAngleLikeToGlsl(code) {
|
|||||||
return withoutDupLoop.join("\n\n");
|
return withoutDupLoop.join("\n\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Strip ShaderToy export preamble (uniform iResolution…), fix `}uniform` gluing, keep first mainImage only. */
|
||||||
|
function sanitizeShadertoyUserGlsl(code) {
|
||||||
|
if (!code || typeof code !== "string") return "";
|
||||||
|
let s = code.replace(/\r\n/g, "\n");
|
||||||
|
s = s.replace(/\}\s*(?=uniform\s+)/g, "}\n");
|
||||||
|
const lines = s.split("\n");
|
||||||
|
s = lines
|
||||||
|
.filter((line) => !/^\s*uniform\s+/.test(line.trim()))
|
||||||
|
.join("\n");
|
||||||
|
s = s.replace(/^\s+/, "").replace(/\s+$/, "");
|
||||||
|
|
||||||
|
const re = /\bvoid\s+mainImage\s*\(/g;
|
||||||
|
const matches = [...s.matchAll(re)];
|
||||||
|
if (matches.length <= 1) return s;
|
||||||
|
|
||||||
|
const start = matches[0].index;
|
||||||
|
const braceStart = s.indexOf("{", s.indexOf("(", start));
|
||||||
|
if (braceStart < 0) return s;
|
||||||
|
let depth = 0;
|
||||||
|
let i = braceStart;
|
||||||
|
for (; i < s.length; i++) {
|
||||||
|
const ch = s[i];
|
||||||
|
if (ch === "{") depth++;
|
||||||
|
else if (ch === "}") {
|
||||||
|
depth--;
|
||||||
|
if (depth === 0) {
|
||||||
|
i++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const firstMain = s.slice(start, i);
|
||||||
|
const prefix = s.slice(0, start).trimEnd();
|
||||||
|
return (prefix ? `${prefix}\n\n` : "") + firstMain.trim();
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeIncomingCode(code) {
|
function normalizeIncomingCode(code) {
|
||||||
if (!code || typeof code !== "string") return { error: "code 不能为空", normalized: "" };
|
if (!code || typeof code !== "string") return { error: "code 不能为空", normalized: "" };
|
||||||
if (isAngleLike(code)) {
|
if (isAngleLike(code)) {
|
||||||
@@ -162,10 +242,11 @@ function normalizeIncomingCode(code) {
|
|||||||
}
|
}
|
||||||
return { error: "", normalized: converted };
|
return { error: "", normalized: converted };
|
||||||
}
|
}
|
||||||
if (!code.includes("mainImage")) {
|
const sanitized = sanitizeShadertoyUserGlsl(code);
|
||||||
|
if (!sanitized.includes("mainImage")) {
|
||||||
return { error: "code 必须包含 mainImage", normalized: "" };
|
return { error: "code 必须包含 mainImage", normalized: "" };
|
||||||
}
|
}
|
||||||
return { error: "", normalized: code };
|
return { error: "", normalized: sanitized };
|
||||||
}
|
}
|
||||||
|
|
||||||
async function autoNormalizeStoredShaders() {
|
async function autoNormalizeStoredShaders() {
|
||||||
@@ -173,9 +254,10 @@ async function autoNormalizeStoredShaders() {
|
|||||||
let changed = false;
|
let changed = false;
|
||||||
const next = shaders.map((item) => {
|
const next = shaders.map((item) => {
|
||||||
const code = String(item.code || "");
|
const code = String(item.code || "");
|
||||||
if (isAngleLike(code) || code.includes("struct ANGLE_") || item.sourceFormat === "angle-metal-auto-converted") {
|
const angle = isAngleLike(code) || code.includes("struct ANGLE_");
|
||||||
|
if (angle) {
|
||||||
const { error, normalized } = normalizeIncomingCode(code);
|
const { error, normalized } = normalizeIncomingCode(code);
|
||||||
if (!error && normalized) {
|
if (!error && normalized && normalized !== code) {
|
||||||
changed = true;
|
changed = true;
|
||||||
return {
|
return {
|
||||||
...item,
|
...item,
|
||||||
@@ -184,6 +266,18 @@ async function autoNormalizeStoredShaders() {
|
|||||||
sourceFormat: "angle-metal-auto-converted",
|
sourceFormat: "angle-metal-auto-converted",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
const { error, normalized } = normalizeIncomingCode(code);
|
||||||
|
if (!error && normalized && normalized !== code) {
|
||||||
|
changed = true;
|
||||||
|
const fixTag = item.sourceFormat === "angle-metal-auto-converted" ? { sourceFormat: "glsl" } : {};
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
code: normalized,
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
...fixTag,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
return item;
|
return item;
|
||||||
});
|
});
|
||||||
@@ -195,10 +289,13 @@ app.get("/api/shaders", async (_req, res) => {
|
|||||||
let shaders = await readDb();
|
let shaders = await readDb();
|
||||||
// Runtime safeguard: always return normalized code for display layer.
|
// Runtime safeguard: always return normalized code for display layer.
|
||||||
shaders = shaders.map((item) => {
|
shaders = shaders.map((item) => {
|
||||||
const { error, normalized } = normalizeIncomingCode(String(item.code || ""));
|
const raw = String(item.code || "");
|
||||||
|
const { error, normalized } = normalizeIncomingCode(raw);
|
||||||
if (error || !normalized) return item;
|
if (error || !normalized) return item;
|
||||||
if (normalized === item.code) return item;
|
if (normalized === raw) return item;
|
||||||
return { ...item, code: normalized, sourceFormat: "angle-metal-auto-converted" };
|
const next = { ...item, code: normalized };
|
||||||
|
if (isAngleLike(raw)) next.sourceFormat = "angle-metal-auto-converted";
|
||||||
|
return next;
|
||||||
});
|
});
|
||||||
res.json(shaders);
|
res.json(shaders);
|
||||||
} catch {
|
} catch {
|
||||||
@@ -250,8 +347,10 @@ app.delete("/api/shaders/:id", async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ensureDb().then(() => {
|
ensureDb().then(async () => {
|
||||||
autoNormalizeStoredShaders().catch(() => {});
|
try {
|
||||||
|
await autoNormalizeStoredShaders();
|
||||||
|
} catch (_) {}
|
||||||
app.listen(PORT, () => {
|
app.listen(PORT, () => {
|
||||||
console.log(`Server running: http://localhost:${PORT}`);
|
console.log(`Server running: http://localhost:${PORT}`);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user