const API_BASE = "/api/shaders"; const nameInput = document.getElementById("name-input"); const authorInput = document.getElementById("author-input"); const codeInput = document.getElementById("code-input"); const saveBtn = document.getElementById("save-btn"); const exampleBtn = document.getElementById("example-btn"); const listEl = document.getElementById("list"); const EXAMPLE = `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 * 9.0 + t * 2.0) + cos(uv.y * 8.0 - t * 1.4); vec3 col = 0.5 + 0.5 * cos(vec3(0.2, 1.2, 2.2) + v + t); fragColor = vec4(col, 1.0); }`; function validateClientCode(code) { const hasMainImage = code.includes("mainImage"); const hasAngleMain = code.includes("_umainImage"); if (!hasMainImage && !hasAngleMain) return "代码需包含 mainImage 或 _umainImage。"; return ""; } async function fetchList() { const res = await fetch(API_BASE); if (!res.ok) throw new Error("加载失败"); return res.json(); } function renderList(items) { listEl.innerHTML = ""; if (!items.length) { listEl.innerHTML = "
暂无数据
"; return; } items.forEach((item) => { const row = document.createElement("div"); row.className = "item"; row.innerHTML = `
${item.name}
${item.author || "unknown"} · ${item.id}
`; row.querySelector("button").addEventListener("click", async () => { await fetch(`${API_BASE}/${encodeURIComponent(item.id)}`, { method: "DELETE" }); await reload(); }); listEl.appendChild(row); }); } async function reload() { const items = await fetchList(); renderList(items); } saveBtn.addEventListener("click", async () => { const name = nameInput.value.trim(); const author = authorInput.value.trim(); const code = codeInput.value.trim(); if (!name) { alert("名称必填"); return; } const err = validateClientCode(code); if (err) { alert(err); return; } const res = await fetch(API_BASE, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name, author, code }), }); if (!res.ok) { const payload = await res.json().catch(() => ({})); alert(payload.error || "保存失败"); return; } const payload = await res.json().catch(() => ({})); if (payload.sourceFormat === "angle-metal-auto-converted") { alert("已自动解构并转换为 GLSL,展示页可直接渲染。"); } if (payload.id && typeof window.captureShaderThumbnail === "function") { try { const dataUrl = await window.captureShaderThumbnail(payload.code, payload.name); const thumbRes = await fetch(`${API_BASE}/${encodeURIComponent(payload.id)}/thumbnail`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ pngBase64: dataUrl }), }); if (!thumbRes.ok) { const t = await thumbRes.json().catch(() => ({})); console.warn("缩略图上传失败:", t.error || thumbRes.status); } } catch (e) { console.warn("缩略图生成失败:", e); } } nameInput.value = ""; authorInput.value = ""; codeInput.value = ""; await reload(); }); exampleBtn.addEventListener("click", () => { nameInput.value = "New Shader"; authorInput.value = "you"; codeInput.value = EXAMPLE; }); reload();