358 lines
12 KiB
JavaScript
358 lines
12 KiB
JavaScript
const express = require("express");
|
|
const fs = require("fs/promises");
|
|
const path = require("path");
|
|
const crypto = require("crypto");
|
|
|
|
const app = express();
|
|
const PORT = process.env.PORT || 5180;
|
|
const DB_PATH = path.join(__dirname, "data", "shaders.json");
|
|
|
|
app.use(express.json({ limit: "1mb" }));
|
|
app.use(express.static(__dirname));
|
|
|
|
async function ensureDb() {
|
|
await fs.mkdir(path.dirname(DB_PATH), { recursive: true });
|
|
try {
|
|
await fs.access(DB_PATH);
|
|
} catch {
|
|
await fs.writeFile(DB_PATH, "[]", "utf8");
|
|
}
|
|
}
|
|
|
|
async function readDb() {
|
|
await ensureDb();
|
|
const raw = await fs.readFile(DB_PATH, "utf8");
|
|
return JSON.parse(raw);
|
|
}
|
|
|
|
async function writeDb(list) {
|
|
await ensureDb();
|
|
await fs.writeFile(DB_PATH, JSON.stringify(list, null, 2), "utf8");
|
|
}
|
|
|
|
function extractFunctionSection(code) {
|
|
const endMarker = code.indexOf("void ANGLE__0_main");
|
|
const end = endMarker >= 0 ? endMarker : code.length;
|
|
const firstFn = code.search(
|
|
/(metal::float[234](x2)?|float|void)\s+[A-Za-z_][A-Za-z0-9_]*\s*\([^)]*\)\s*\{/m
|
|
);
|
|
if (firstFn < 0 || firstFn >= end) return "";
|
|
return code.slice(firstFn, end);
|
|
}
|
|
|
|
function isAngleLike(code) {
|
|
return (
|
|
code.includes("_umainImage") &&
|
|
(code.includes("ANGLE_") ||
|
|
code.includes("metal::") ||
|
|
code.includes("[[function_constant") ||
|
|
code.includes("float2") ||
|
|
code.includes("float3"))
|
|
);
|
|
}
|
|
|
|
function extractFunctionBlocks(code) {
|
|
const blocks = [];
|
|
const sig = /(?:^|\n)\s*(?:void|float|vec[234]|mat[234]|float[234](?:x[234])?)\s+([A-Za-z_][A-Za-z0-9_]*)\s*\([^;]*\)\s*\{/g;
|
|
let m;
|
|
while ((m = sig.exec(code)) !== null) {
|
|
const name = m[1];
|
|
if (name === "main0" || name === "ANGLE__0_main") continue;
|
|
let i = code.indexOf("{", m.index);
|
|
if (i < 0) continue;
|
|
let depth = 0;
|
|
let end = -1;
|
|
for (let p = i; p < code.length; p++) {
|
|
const ch = code[p];
|
|
if (ch === "{") depth++;
|
|
else if (ch === "}") {
|
|
depth--;
|
|
if (depth === 0) {
|
|
end = p + 1;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (end > i) {
|
|
blocks.push(code.slice(m.index, end));
|
|
sig.lastIndex = end;
|
|
}
|
|
}
|
|
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) {
|
|
let s = code;
|
|
const cut = s.indexOf("fragment ANGLE_FragmentOut");
|
|
if (cut > 0) s = s.slice(0, cut);
|
|
|
|
s = s
|
|
.replace(/metal::fast::normalize/g, "normalize")
|
|
.replace(/metal::/g, "")
|
|
.replace(/\[\[[^\]]+\]\]/g, "")
|
|
.replace(/\bthread\b/g, "")
|
|
.replace(/\bconstant\b/g, "")
|
|
.replace(/\bprecise::tanh\b/g, "tanh")
|
|
.replace(/\bfloat2x2\b/g, "mat2")
|
|
.replace(/\bfloat3x3\b/g, "mat3")
|
|
.replace(/\bfloat4x4\b/g, "mat4")
|
|
.replace(/\bfloat2\b/g, "vec2")
|
|
.replace(/\bfloat3\b/g, "vec3")
|
|
.replace(/\bfloat4\b/g, "vec4")
|
|
.replace(/\bint2\b/g, "ivec2")
|
|
.replace(/\bint3\b/g, "ivec3")
|
|
.replace(/\bint4\b/g, "ivec4")
|
|
.replace(/\buint2\b/g, "uvec2")
|
|
.replace(/\buint3\b/g, "uvec3")
|
|
.replace(/\buint4\b/g, "uvec4")
|
|
.replace(/\buint32_t\b/g, "uint")
|
|
.replace(/\bANGLE_userUniforms\._uiResolution\b/g, "iResolution")
|
|
.replace(/\bANGLE_userUniforms\._uiTime\b/g, "iTime")
|
|
.replace(/\bANGLE_userUniforms\._uiMouse\b/g, "iMouse")
|
|
.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_texelFetch\([^)]*\)/g, "vec4(0.0)")
|
|
.replace(/\bANGLE_mod\(/g, "mod(")
|
|
.replace(/\bANGLE_out\(([^)]+)\)/g, "$1")
|
|
.replace(/\bANGLE_swizzle_ref\(([^)]+)\)/g, "$1")
|
|
.replace(/\bANGLE_elem_ref\(([^)]+)\)/g, "$1")
|
|
.replace(/\bANGLE_addressof\(([^)]+)\)/g, "$1")
|
|
.replace(/\bANGLE_UserUniforms\s*&\s*ANGLE_userUniforms\s*,?/g, "")
|
|
.replace(/\bANGLE_TextureEnvs\s*&\s*ANGLE_textureEnvs\s*,?/g, "")
|
|
.replace(/\bANGLE_NonConstGlobals\s*&\s*ANGLE_nonConstGlobals\s*,?/g, "")
|
|
.replace(/\(\s*,/g, "(")
|
|
.replace(/,\s*,/g, ",")
|
|
.replace(/,\s*\)/g, ")")
|
|
.replace(/\bANGLE_nonConstGlobals\./g, "")
|
|
.replace(/(\d+\.\d+|\d+)f\b/g, "$1")
|
|
.replace(/;\s*;/g, ";");
|
|
|
|
s = s.replace(/void\s+_umainImage\s*\([^)]*\)/g, "void _umainImage(out vec4 _ufragColor, vec2 _ufragCoord)");
|
|
|
|
const fnBlocks = extractFunctionBlocks(s).filter((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 "";
|
|
|
|
const hasMainImage = blocks.some((b) => /void\s+mainImage\s*\(/.test(b));
|
|
const withoutDupLoop = [];
|
|
let seenLoop = false;
|
|
for (const b of blocks) {
|
|
if (/void\s+ANGLE_loopForwardProgress\s*\(/.test(b)) {
|
|
if (seenLoop) continue;
|
|
seenLoop = true;
|
|
withoutDupLoop.push("void ANGLE_loopForwardProgress() {}");
|
|
continue;
|
|
}
|
|
withoutDupLoop.push(b);
|
|
}
|
|
if (!hasMainImage) {
|
|
withoutDupLoop.push("void mainImage(out vec4 fragColor, in vec2 fragCoord) { _umainImage(fragColor, fragCoord); }");
|
|
}
|
|
|
|
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) {
|
|
if (!code || typeof code !== "string") return { error: "code 不能为空", normalized: "" };
|
|
if (isAngleLike(code)) {
|
|
const converted = convertAngleLikeToGlsl(code);
|
|
if (!converted) {
|
|
return { error: "自动解构失败:请检查粘贴内容是否完整。", normalized: "" };
|
|
}
|
|
return { error: "", normalized: converted };
|
|
}
|
|
const sanitized = sanitizeShadertoyUserGlsl(code);
|
|
if (!sanitized.includes("mainImage")) {
|
|
return { error: "code 必须包含 mainImage", normalized: "" };
|
|
}
|
|
return { error: "", normalized: sanitized };
|
|
}
|
|
|
|
async function autoNormalizeStoredShaders() {
|
|
const shaders = await readDb();
|
|
let changed = false;
|
|
const next = shaders.map((item) => {
|
|
const code = String(item.code || "");
|
|
const angle = isAngleLike(code) || code.includes("struct ANGLE_");
|
|
if (angle) {
|
|
const { error, normalized } = normalizeIncomingCode(code);
|
|
if (!error && normalized && normalized !== code) {
|
|
changed = true;
|
|
return {
|
|
...item,
|
|
code: normalized,
|
|
updatedAt: new Date().toISOString(),
|
|
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;
|
|
});
|
|
if (changed) await writeDb(next);
|
|
}
|
|
|
|
app.get("/api/shaders", async (_req, res) => {
|
|
try {
|
|
let shaders = await readDb();
|
|
// Runtime safeguard: always return normalized code for display layer.
|
|
shaders = shaders.map((item) => {
|
|
const raw = String(item.code || "");
|
|
const { error, normalized } = normalizeIncomingCode(raw);
|
|
if (error || !normalized) return item;
|
|
if (normalized === raw) return item;
|
|
const next = { ...item, code: normalized };
|
|
if (isAngleLike(raw)) next.sourceFormat = "angle-metal-auto-converted";
|
|
return next;
|
|
});
|
|
res.json(shaders);
|
|
} catch {
|
|
res.status(500).json({ error: "读取失败" });
|
|
}
|
|
});
|
|
|
|
app.post("/api/shaders", async (req, res) => {
|
|
const { name, author = "unknown", code } = req.body || {};
|
|
if (!name || typeof name !== "string") {
|
|
return res.status(400).json({ error: "name 必填" });
|
|
}
|
|
const { error: parseError, normalized } = normalizeIncomingCode(code);
|
|
if (parseError) {
|
|
return res.status(400).json({ error: parseError });
|
|
}
|
|
|
|
try {
|
|
const shaders = await readDb();
|
|
const item = {
|
|
id: crypto.randomUUID(),
|
|
name: name.trim(),
|
|
author: String(author || "unknown").trim() || "unknown",
|
|
code: normalized,
|
|
views: Math.floor(3000 + Math.random() * 22000),
|
|
likes: Math.floor(40 + Math.random() * 700),
|
|
createdAt: new Date().toISOString(),
|
|
sourceFormat: isAngleLike(code) ? "angle-metal-auto-converted" : "glsl",
|
|
};
|
|
shaders.unshift(item);
|
|
await writeDb(shaders);
|
|
res.status(201).json(item);
|
|
} catch {
|
|
res.status(500).json({ error: "保存失败" });
|
|
}
|
|
});
|
|
|
|
app.delete("/api/shaders/:id", async (req, res) => {
|
|
try {
|
|
const shaders = await readDb();
|
|
const next = shaders.filter((it) => it.id !== req.params.id);
|
|
if (next.length === shaders.length) {
|
|
return res.status(404).json({ error: "未找到该 shader" });
|
|
}
|
|
await writeDb(next);
|
|
res.status(204).end();
|
|
} catch {
|
|
res.status(500).json({ error: "删除失败" });
|
|
}
|
|
});
|
|
|
|
ensureDb().then(async () => {
|
|
try {
|
|
await autoNormalizeStoredShaders();
|
|
} catch (_) {}
|
|
app.listen(PORT, () => {
|
|
console.log(`Server running: http://localhost:${PORT}`);
|
|
});
|
|
});
|