131 lines
4.1 KiB
JavaScript
131 lines
4.1 KiB
JavaScript
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 listCountEl = document.getElementById("list-count");
|
||
|
||
function esc(s) {
|
||
return String(s)
|
||
.replace(/&/g, "&")
|
||
.replace(/</g, "<")
|
||
.replace(/>/g, ">")
|
||
.replace(/"/g, """);
|
||
}
|
||
|
||
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 (listCountEl) {
|
||
listCountEl.textContent = items.length ? `${items.length} 项` : "";
|
||
}
|
||
if (!items.length) {
|
||
listEl.innerHTML =
|
||
'<div class="empty-state">暂无着色器,请在上方创建并保存。</div>';
|
||
return;
|
||
}
|
||
items.forEach((item) => {
|
||
const row = document.createElement("div");
|
||
row.className = "item";
|
||
row.innerHTML = `
|
||
<div class="item-main">
|
||
<strong>${esc(item.name)}</strong>
|
||
<div class="item-meta">${esc(item.author || "unknown")} · ${esc(item.id)}</div>
|
||
</div>
|
||
<div class="item-actions">
|
||
<a class="btn btn-dl" href="${API_BASE}/${encodeURIComponent(item.id)}/download" download>下载 .glsl</a>
|
||
<button type="button" class="btn btn-danger">删除</button>
|
||
</div>
|
||
`;
|
||
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();
|