fix:修复bug
This commit is contained in:
@@ -11,6 +11,9 @@ npm run dev
|
|||||||
|
|
||||||
打开 `http://localhost:5180`。
|
打开 `http://localhost:5180`。
|
||||||
|
|
||||||
|
- 展示页:`http://localhost:5180/index.html`
|
||||||
|
- 管理页:`http://localhost:5180/admin.html`
|
||||||
|
|
||||||
## Docker 一键启动(国内镜像)
|
## Docker 一键启动(国内镜像)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -20,13 +23,14 @@ npm run dev
|
|||||||
说明:
|
说明:
|
||||||
- Node 基础镜像走 `docker.m.daocloud.io`
|
- Node 基础镜像走 `docker.m.daocloud.io`
|
||||||
- npm 安装源走 `https://registry.npmmirror.com`
|
- npm 安装源走 `https://registry.npmmirror.com`
|
||||||
- 服务地址:`http://localhost:5180`
|
- 默认使用 `5180`,若被占用会自动递增到可用端口(如 `5181`)
|
||||||
|
- 启动完成后终端会打印最终访问地址
|
||||||
|
|
||||||
## 页面能力
|
## 页面能力
|
||||||
|
|
||||||
- 预览墙:多个 VFX 同屏实时渲染,支持点进详情
|
- 预览墙:多个 VFX 同屏实时渲染,支持点进详情
|
||||||
- 每个小窗独立播放/暂停
|
- 每个小窗独立播放/暂停
|
||||||
- 后端管理:点击“管理内容”,可直接粘贴 GLSL 并保存到后端
|
- 管理页独立:在 `admin.html` 粘贴 GLSL 并保存到后端
|
||||||
- 保存后实时刷新前端展示,支持删除
|
- 保存后实时刷新前端展示,支持删除
|
||||||
|
|
||||||
## 兼容的 uniform
|
## 兼容的 uniform
|
||||||
|
|||||||
83
admin.html
Normal file
83
admin.html
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Shader 管理后台</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
background: #0b0f18;
|
||||||
|
color: #e8ecff;
|
||||||
|
font-family: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif;
|
||||||
|
}
|
||||||
|
.wrap {
|
||||||
|
max-width: 980px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
border: 1px solid rgba(126, 165, 255, 0.35);
|
||||||
|
background: #111726;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 14px;
|
||||||
|
}
|
||||||
|
.grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
input, textarea, button {
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
input, textarea {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
textarea {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
min-height: 240px;
|
||||||
|
resize: vertical;
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Courier New", monospace;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
padding: 8px 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.list { margin-top: 12px; }
|
||||||
|
.item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 10px;
|
||||||
|
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
padding: 10px 4px;
|
||||||
|
}
|
||||||
|
.item:first-child { border-top: 0; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="wrap">
|
||||||
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;">
|
||||||
|
<h2 style="margin:0;">Shader 管理后台</h2>
|
||||||
|
<a href="./index.html" style="color:#a9c5ff;">返回展示页</a>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="grid">
|
||||||
|
<input id="name-input" placeholder="名称" />
|
||||||
|
<input id="author-input" placeholder="作者(可选)" />
|
||||||
|
<textarea id="code-input" placeholder="粘贴 GLSL,必须包含 mainImage"></textarea>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex; gap:8px; margin-top:10px;">
|
||||||
|
<button id="save-btn">保存</button>
|
||||||
|
<button id="example-btn">填入示例</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card list" id="list"></div>
|
||||||
|
</div>
|
||||||
|
<script src="./admin.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
96
admin.js
Normal file
96
admin.js
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
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) {
|
||||||
|
if (!code.includes("mainImage")) return "代码需包含 mainImage。";
|
||||||
|
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 = "<div>暂无数据</div>";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
items.forEach((item) => {
|
||||||
|
const row = document.createElement("div");
|
||||||
|
row.className = "item";
|
||||||
|
row.innerHTML = `
|
||||||
|
<div>
|
||||||
|
<strong>${item.name}</strong>
|
||||||
|
<div style="font-size:12px;color:#9fb2df;">${item.author || "unknown"} · ${item.id}</div>
|
||||||
|
</div>
|
||||||
|
<button>删除</button>
|
||||||
|
`;
|
||||||
|
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,展示页可直接渲染。");
|
||||||
|
}
|
||||||
|
nameInput.value = "";
|
||||||
|
authorInput.value = "";
|
||||||
|
codeInput.value = "";
|
||||||
|
await reload();
|
||||||
|
});
|
||||||
|
|
||||||
|
exampleBtn.addEventListener("click", () => {
|
||||||
|
nameInput.value = "New Shader";
|
||||||
|
authorInput.value = "you";
|
||||||
|
codeInput.value = EXAMPLE;
|
||||||
|
});
|
||||||
|
|
||||||
|
reload();
|
||||||
File diff suppressed because one or more lines are too long
@@ -10,15 +10,6 @@ const detailTitleEl = document.getElementById("detail-title");
|
|||||||
const detailFpsEl = document.getElementById("detail-fps");
|
const detailFpsEl = document.getElementById("detail-fps");
|
||||||
const detailCanvas = document.getElementById("detail-canvas");
|
const detailCanvas = document.getElementById("detail-canvas");
|
||||||
const backBtn = document.getElementById("back-btn");
|
const backBtn = document.getElementById("back-btn");
|
||||||
const manageBtn = document.getElementById("manage-btn");
|
|
||||||
const manageModal = document.getElementById("manage-modal");
|
|
||||||
const closeManageBtn = document.getElementById("close-manage-btn");
|
|
||||||
const saveShaderBtn = document.getElementById("save-shader-btn");
|
|
||||||
const fillExampleBtn = document.getElementById("fill-example-btn");
|
|
||||||
const shaderNameInput = document.getElementById("shader-name-input");
|
|
||||||
const shaderAuthorInput = document.getElementById("shader-author-input");
|
|
||||||
const shaderCodeInput = document.getElementById("shader-code-input");
|
|
||||||
const shaderAdminList = document.getElementById("shader-admin-list");
|
|
||||||
|
|
||||||
const API_BASE = "/api/shaders";
|
const API_BASE = "/api/shaders";
|
||||||
|
|
||||||
@@ -27,13 +18,63 @@ precision highp float;
|
|||||||
layout(location = 0) in vec2 aPosition;
|
layout(location = 0) in vec2 aPosition;
|
||||||
void main() { gl_Position = vec4(aPosition, 0.0, 1.0); }`;
|
void main() { gl_Position = vec4(aPosition, 0.0, 1.0); }`;
|
||||||
|
|
||||||
const EXAMPLE_SHADER = `void mainImage(out vec4 fragColor, in vec2 fragCoord) {
|
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");
|
||||||
|
|
||||||
|
// Keep only helper functions + mainImage block, drop struct/template leftovers.
|
||||||
|
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;
|
vec2 uv = (fragCoord - 0.5 * iResolution.xy) / iResolution.y;
|
||||||
float t = iTime;
|
float t = iTime;
|
||||||
float v = sin(uv.x * 10.0 + t * 2.1) + cos(uv.y * 9.0 - t * 1.4);
|
float v = sin(uv.x * (8.0 + ${k1} * 8.0) + t * (1.4 + ${k2} * 1.2))
|
||||||
vec3 col = 0.5 + 0.5 * cos(vec3(0.1, 1.1, 2.3) + v + t);
|
+ 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);
|
fragColor = vec4(col, 1.0);
|
||||||
}`;
|
}`;
|
||||||
|
}
|
||||||
|
|
||||||
function buildFragmentShader(userCode) {
|
function buildFragmentShader(userCode) {
|
||||||
return `#version 300 es
|
return `#version 300 es
|
||||||
@@ -73,7 +114,6 @@ function compileProgram(gl, fragmentShaderSource) {
|
|||||||
}
|
}
|
||||||
return shader;
|
return shader;
|
||||||
}
|
}
|
||||||
|
|
||||||
const vs = compile(gl.VERTEX_SHADER, vertexShaderSource);
|
const vs = compile(gl.VERTEX_SHADER, vertexShaderSource);
|
||||||
const fs = compile(gl.FRAGMENT_SHADER, fragmentShaderSource);
|
const fs = compile(gl.FRAGMENT_SHADER, fragmentShaderSource);
|
||||||
const program = gl.createProgram();
|
const program = gl.createProgram();
|
||||||
@@ -91,7 +131,6 @@ function compileProgram(gl, fragmentShaderSource) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const previews = [];
|
const previews = [];
|
||||||
const sourceShaders = [];
|
|
||||||
let paused = false;
|
let paused = false;
|
||||||
let elapsed = 0;
|
let elapsed = 0;
|
||||||
let frame = 0;
|
let frame = 0;
|
||||||
@@ -104,6 +143,7 @@ let detailFps = 0;
|
|||||||
let detailLastSec = 0;
|
let detailLastSec = 0;
|
||||||
let detailRuntime = null;
|
let detailRuntime = null;
|
||||||
let searchKeyword = "";
|
let searchKeyword = "";
|
||||||
|
let lastSignature = "";
|
||||||
|
|
||||||
function createPreviewCard(shader) {
|
function createPreviewCard(shader) {
|
||||||
const { id, name, author = "unknown", views = 0, likes = 0, code } = shader;
|
const { id, name, author = "unknown", views = 0, likes = 0, code } = shader;
|
||||||
@@ -115,7 +155,6 @@ function createPreviewCard(shader) {
|
|||||||
<strong>${name}</strong>
|
<strong>${name}</strong>
|
||||||
<div style="display:flex; gap:6px;">
|
<div style="display:flex; gap:6px;">
|
||||||
<button type="button" class="pause-local-btn">暂停</button>
|
<button type="button" class="pause-local-btn">暂停</button>
|
||||||
<button type="button" class="remove-btn">删除</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<canvas></canvas>
|
<canvas></canvas>
|
||||||
@@ -129,7 +168,6 @@ 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 removeBtn = card.querySelector(".remove-btn");
|
|
||||||
const gl = canvas.getContext("webgl2", { antialias: false, alpha: false });
|
const gl = canvas.getContext("webgl2", { antialias: false, alpha: false });
|
||||||
if (!gl) {
|
if (!gl) {
|
||||||
errorEl.textContent = "当前浏览器不支持 WebGL2";
|
errorEl.textContent = "当前浏览器不支持 WebGL2";
|
||||||
@@ -137,12 +175,25 @@ function createPreviewCard(shader) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let program;
|
let program;
|
||||||
|
let codeForRender = code;
|
||||||
try {
|
try {
|
||||||
program = compileProgram(gl, buildFragmentShader(code));
|
program = compileProgram(gl, buildFragmentShader(codeForRender));
|
||||||
} catch (err) {
|
} catch (_err1) {
|
||||||
errorEl.textContent = `编译失败:\n${String(err.message || err)}`;
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const { vao } = createQuad(gl);
|
const { vao } = createQuad(gl);
|
||||||
const uniforms = {
|
const uniforms = {
|
||||||
@@ -157,6 +208,7 @@ function createPreviewCard(shader) {
|
|||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
code,
|
code,
|
||||||
|
codeForRender,
|
||||||
card,
|
card,
|
||||||
canvas,
|
canvas,
|
||||||
gl,
|
gl,
|
||||||
@@ -168,7 +220,6 @@ function createPreviewCard(shader) {
|
|||||||
localTime: 0,
|
localTime: 0,
|
||||||
localFrame: 0,
|
localFrame: 0,
|
||||||
lastTick: performance.now(),
|
lastTick: performance.now(),
|
||||||
removed: false,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
canvas.addEventListener("mousemove", (event) => {
|
canvas.addEventListener("mousemove", (event) => {
|
||||||
@@ -185,10 +236,6 @@ function createPreviewCard(shader) {
|
|||||||
state.mouse.down = false;
|
state.mouse.down = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
removeBtn.addEventListener("click", (event) => {
|
|
||||||
event.stopPropagation();
|
|
||||||
deleteShader(id);
|
|
||||||
});
|
|
||||||
pauseLocalBtn.addEventListener("click", (event) => {
|
pauseLocalBtn.addEventListener("click", (event) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
state.isPlaying = !state.isPlaying;
|
state.isPlaying = !state.isPlaying;
|
||||||
@@ -206,13 +253,6 @@ function clearPreviews() {
|
|||||||
previews.length = 0;
|
previews.length = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderCards(shaders) {
|
|
||||||
clearPreviews();
|
|
||||||
shaders.forEach(createPreviewCard);
|
|
||||||
applySearch();
|
|
||||||
syncStats();
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeDetail() {
|
function closeDetail() {
|
||||||
detailViewEl.classList.remove("active");
|
detailViewEl.classList.remove("active");
|
||||||
detailViewEl.setAttribute("aria-hidden", "true");
|
detailViewEl.setAttribute("aria-hidden", "true");
|
||||||
@@ -228,15 +268,14 @@ function openDetail(previewState) {
|
|||||||
closeDetail();
|
closeDetail();
|
||||||
const gl = detailCanvas.getContext("webgl2", { antialias: false, alpha: false });
|
const gl = detailCanvas.getContext("webgl2", { antialias: false, alpha: false });
|
||||||
if (!gl) return;
|
if (!gl) return;
|
||||||
|
|
||||||
let program;
|
let program;
|
||||||
|
const detailCode = previewState.codeForRender || previewState.code;
|
||||||
try {
|
try {
|
||||||
program = compileProgram(gl, buildFragmentShader(previewState.code));
|
program = compileProgram(gl, buildFragmentShader(detailCode));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
statsEl.textContent = `详情编译失败: ${String(err.message || err)}`;
|
statsEl.textContent = `详情编译失败: ${String(err.message || err)}`;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { vao, vbo } = createQuad(gl);
|
const { vao, vbo } = createQuad(gl);
|
||||||
detailRuntime = {
|
detailRuntime = {
|
||||||
gl,
|
gl,
|
||||||
@@ -252,12 +291,45 @@ function openDetail(previewState) {
|
|||||||
},
|
},
|
||||||
mouse: { x: 0, y: 0, downX: 0, downY: 0, down: false },
|
mouse: { x: 0, y: 0, downX: 0, downY: 0, down: false },
|
||||||
};
|
};
|
||||||
|
|
||||||
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");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function applySearch() {
|
||||||
|
const keyword = searchKeyword.trim().toLowerCase();
|
||||||
|
for (const preview of previews) {
|
||||||
|
preview.card.style.display = !keyword || preview.card.dataset.search.includes(keyword) ? "" : "none";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadShaders() {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${API_BASE}?t=${Date.now()}`, { cache: "no-store" });
|
||||||
|
if (!res.ok) throw new Error("加载失败");
|
||||||
|
const shaders = await res.json();
|
||||||
|
const signature = JSON.stringify(
|
||||||
|
shaders.map((s) => ({ id: s.id, updatedAt: s.updatedAt || s.createdAt || "", name: s.name }))
|
||||||
|
);
|
||||||
|
if (signature !== lastSignature) {
|
||||||
|
lastSignature = signature;
|
||||||
|
clearPreviews();
|
||||||
|
shaders.forEach(createPreviewCard);
|
||||||
|
applySearch();
|
||||||
|
syncStats();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
statsEl.textContent = `读取后端失败:${String(err.message || err)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncStats() {
|
||||||
|
statsEl.textContent = `预览数量: ${previews.length} | 全局时间: ${elapsed.toFixed(2)}s | 状态: ${
|
||||||
|
paused ? "已暂停" : "运行中"
|
||||||
|
}`;
|
||||||
|
shaderCountEl.textContent = `${previews.length} shaders`;
|
||||||
|
}
|
||||||
|
|
||||||
function renderAll(ts) {
|
function renderAll(ts) {
|
||||||
const dt = Math.min((ts - lastTs) / 1000, 0.05);
|
const dt = Math.min((ts - lastTs) / 1000, 0.05);
|
||||||
lastTs = ts;
|
lastTs = ts;
|
||||||
@@ -265,7 +337,6 @@ function renderAll(ts) {
|
|||||||
elapsed += dt;
|
elapsed += dt;
|
||||||
frame += 1;
|
frame += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Math.floor(elapsed) !== lastSec) {
|
if (Math.floor(elapsed) !== lastSec) {
|
||||||
lastSec = Math.floor(elapsed);
|
lastSec = Math.floor(elapsed);
|
||||||
fps = fpsCounter;
|
fps = fpsCounter;
|
||||||
@@ -284,7 +355,6 @@ function renderAll(ts) {
|
|||||||
preview.localTime += localDt;
|
preview.localTime += localDt;
|
||||||
preview.localFrame += 1;
|
preview.localFrame += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
const rect = canvas.getBoundingClientRect();
|
const rect = canvas.getBoundingClientRect();
|
||||||
const dpr = Math.min(window.devicePixelRatio || 1, 2);
|
const dpr = Math.min(window.devicePixelRatio || 1, 2);
|
||||||
const w = Math.max(1, Math.floor(rect.width * dpr));
|
const w = Math.max(1, Math.floor(rect.width * dpr));
|
||||||
@@ -294,7 +364,6 @@ function renderAll(ts) {
|
|||||||
canvas.height = h;
|
canvas.height = h;
|
||||||
gl.viewport(0, 0, w, h);
|
gl.viewport(0, 0, w, h);
|
||||||
}
|
}
|
||||||
|
|
||||||
gl.useProgram(program);
|
gl.useProgram(program);
|
||||||
gl.uniform3f(uniforms.iResolution, canvas.width, canvas.height, 1);
|
gl.uniform3f(uniforms.iResolution, canvas.width, canvas.height, 1);
|
||||||
gl.uniform1f(uniforms.iTime, preview.localTime);
|
gl.uniform1f(uniforms.iTime, preview.localTime);
|
||||||
@@ -337,7 +406,6 @@ 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);
|
||||||
|
|
||||||
if (Math.floor(elapsed) !== detailLastSec) {
|
if (Math.floor(elapsed) !== detailLastSec) {
|
||||||
detailLastSec = Math.floor(elapsed);
|
detailLastSec = Math.floor(elapsed);
|
||||||
detailFps = detailFpsCounter;
|
detailFps = detailFpsCounter;
|
||||||
@@ -351,98 +419,11 @@ function renderAll(ts) {
|
|||||||
requestAnimationFrame(renderAll);
|
requestAnimationFrame(renderAll);
|
||||||
}
|
}
|
||||||
|
|
||||||
function syncStats() {
|
|
||||||
statsEl.textContent = `预览数量: ${previews.length} | 全局时间: ${elapsed.toFixed(2)}s | 状态: ${
|
|
||||||
paused ? "已暂停" : "运行中"
|
|
||||||
}`;
|
|
||||||
shaderCountEl.textContent = `${previews.length} shaders`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function applySearch() {
|
|
||||||
const keyword = searchKeyword.trim().toLowerCase();
|
|
||||||
for (const preview of previews) {
|
|
||||||
preview.card.style.display = !keyword || preview.card.dataset.search.includes(keyword) ? "" : "none";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderAdminList() {
|
|
||||||
shaderAdminList.innerHTML = "";
|
|
||||||
if (sourceShaders.length === 0) {
|
|
||||||
shaderAdminList.innerHTML = `<div class="row-item"><span>暂无 shader,点击“保存到后端”添加。</span></div>`;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
sourceShaders.forEach((shader) => {
|
|
||||||
const row = document.createElement("div");
|
|
||||||
row.className = "row-item";
|
|
||||||
row.innerHTML = `
|
|
||||||
<div>
|
|
||||||
<strong>${shader.name}</strong>
|
|
||||||
<div style="font-size:12px;color:#9ea6c8;">${shader.author || "unknown"} · ${shader.id}</div>
|
|
||||||
</div>
|
|
||||||
<button type="button">删除</button>
|
|
||||||
`;
|
|
||||||
row.querySelector("button").addEventListener("click", () => deleteShader(shader.id));
|
|
||||||
shaderAdminList.appendChild(row);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchShaders() {
|
|
||||||
const res = await fetch(API_BASE);
|
|
||||||
if (!res.ok) throw new Error("加载失败");
|
|
||||||
return res.json();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadShaders() {
|
|
||||||
try {
|
|
||||||
const list = await fetchShaders();
|
|
||||||
sourceShaders.splice(0, sourceShaders.length, ...list);
|
|
||||||
renderCards(sourceShaders);
|
|
||||||
renderAdminList();
|
|
||||||
} catch (err) {
|
|
||||||
statsEl.textContent = `读取后端失败:${String(err.message || err)}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function saveShader() {
|
|
||||||
const name = shaderNameInput.value.trim();
|
|
||||||
const author = shaderAuthorInput.value.trim();
|
|
||||||
const code = shaderCodeInput.value.trim();
|
|
||||||
if (!name || !code.includes("mainImage")) {
|
|
||||||
statsEl.textContent = "保存失败:名称必填,代码需包含 mainImage。";
|
|
||||||
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(() => ({}));
|
|
||||||
throw new Error(payload.error || "保存失败");
|
|
||||||
}
|
|
||||||
|
|
||||||
shaderNameInput.value = "";
|
|
||||||
shaderAuthorInput.value = "";
|
|
||||||
shaderCodeInput.value = "";
|
|
||||||
await loadShaders();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function deleteShader(id) {
|
|
||||||
const res = await fetch(`${API_BASE}/${encodeURIComponent(id)}`, { method: "DELETE" });
|
|
||||||
if (!res.ok) {
|
|
||||||
statsEl.textContent = "删除失败。";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await loadShaders();
|
|
||||||
}
|
|
||||||
|
|
||||||
pauseBtn.addEventListener("click", () => {
|
pauseBtn.addEventListener("click", () => {
|
||||||
paused = !paused;
|
paused = !paused;
|
||||||
pauseBtn.textContent = paused ? "全局继续" : "全局暂停";
|
pauseBtn.textContent = paused ? "全局继续" : "全局暂停";
|
||||||
syncStats();
|
syncStats();
|
||||||
});
|
});
|
||||||
|
|
||||||
resetBtn.addEventListener("click", () => {
|
resetBtn.addEventListener("click", () => {
|
||||||
elapsed = 0;
|
elapsed = 0;
|
||||||
frame = 0;
|
frame = 0;
|
||||||
@@ -452,7 +433,6 @@ resetBtn.addEventListener("click", () => {
|
|||||||
detailFpsCounter = 0;
|
detailFpsCounter = 0;
|
||||||
syncStats();
|
syncStats();
|
||||||
});
|
});
|
||||||
|
|
||||||
backBtn.addEventListener("click", closeDetail);
|
backBtn.addEventListener("click", closeDetail);
|
||||||
detailCanvas.addEventListener("mousemove", (event) => {
|
detailCanvas.addEventListener("mousemove", (event) => {
|
||||||
if (!detailRuntime) return;
|
if (!detailRuntime) return;
|
||||||
@@ -470,31 +450,11 @@ window.addEventListener("mouseup", () => {
|
|||||||
if (!detailRuntime) return;
|
if (!detailRuntime) return;
|
||||||
detailRuntime.mouse.down = false;
|
detailRuntime.mouse.down = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
searchInput.addEventListener("input", (event) => {
|
searchInput.addEventListener("input", (event) => {
|
||||||
searchKeyword = event.target.value || "";
|
searchKeyword = event.target.value || "";
|
||||||
applySearch();
|
applySearch();
|
||||||
});
|
});
|
||||||
manageBtn.addEventListener("click", () => {
|
|
||||||
manageModal.classList.add("active");
|
|
||||||
manageModal.setAttribute("aria-hidden", "false");
|
|
||||||
});
|
|
||||||
closeManageBtn.addEventListener("click", () => {
|
|
||||||
manageModal.classList.remove("active");
|
|
||||||
manageModal.setAttribute("aria-hidden", "true");
|
|
||||||
});
|
|
||||||
fillExampleBtn.addEventListener("click", () => {
|
|
||||||
shaderNameInput.value = "New Shader";
|
|
||||||
shaderAuthorInput.value = "you";
|
|
||||||
shaderCodeInput.value = EXAMPLE_SHADER;
|
|
||||||
});
|
|
||||||
saveShaderBtn.addEventListener("click", async () => {
|
|
||||||
try {
|
|
||||||
await saveShader();
|
|
||||||
} catch (err) {
|
|
||||||
statsEl.textContent = `保存失败:${String(err.message || err)}`;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
loadShaders();
|
loadShaders();
|
||||||
|
setInterval(loadShaders, 3000);
|
||||||
requestAnimationFrame(renderAll);
|
requestAnimationFrame(renderAll);
|
||||||
@@ -8,7 +8,7 @@ services:
|
|||||||
image: vfxdemo:cn
|
image: vfxdemo:cn
|
||||||
container_name: vfxdemo
|
container_name: vfxdemo
|
||||||
ports:
|
ports:
|
||||||
- "5180:5180"
|
- "${HOST_PORT:-5180}:5180"
|
||||||
environment:
|
environment:
|
||||||
- PORT=5180
|
- PORT=5180
|
||||||
- NODE_ENV=production
|
- NODE_ENV=production
|
||||||
|
|||||||
24
index.html
24
index.html
@@ -280,7 +280,7 @@
|
|||||||
<div class="brand">Shadervault</div>
|
<div class="brand">Shadervault</div>
|
||||||
<div class="header-right">
|
<div class="header-right">
|
||||||
<input id="search-input" class="search" placeholder="Search shaders..." />
|
<input id="search-input" class="search" placeholder="Search shaders..." />
|
||||||
<button id="manage-btn">管理内容</button>
|
<a href="./admin.html" style="text-decoration:none;"><button>管理后台</button></a>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<div id="app">
|
<div id="app">
|
||||||
@@ -312,26 +312,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section id="manage-modal" class="modal" aria-hidden="true">
|
<script src="./display.js"></script>
|
||||||
<div class="modal-card">
|
|
||||||
<div class="topbar">
|
|
||||||
<strong>Shader 内容管理</strong>
|
|
||||||
<div style="display:flex; gap:8px">
|
|
||||||
<button id="close-manage-btn">关闭</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal-grid">
|
|
||||||
<input id="shader-name-input" placeholder="名称" />
|
|
||||||
<input id="shader-author-input" placeholder="作者(可选)" />
|
|
||||||
<textarea id="shader-code-input" placeholder="粘贴 GLSL,必须包含 mainImage"></textarea>
|
|
||||||
</div>
|
|
||||||
<div style="display:flex; gap:8px; margin-top:10px;">
|
|
||||||
<button id="save-shader-btn">保存到后端</button>
|
|
||||||
<button id="fill-example-btn">填入示例</button>
|
|
||||||
</div>
|
|
||||||
<div class="table" id="shader-admin-list"></div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<script src="./frontend.js"></script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
93
server.js
93
server.js
@@ -30,6 +30,90 @@ async function writeDb(list) {
|
|||||||
await fs.writeFile(DB_PATH, JSON.stringify(list, null, 2), "utf8");
|
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 convertAngleMetalToGlsl(code) {
|
||||||
|
let section = extractFunctionSection(code);
|
||||||
|
if (!section || !section.includes("_umainImage")) return "";
|
||||||
|
|
||||||
|
section = section
|
||||||
|
.replace(
|
||||||
|
/void _umainImage\s*\(\s*constant ANGLE_UserUniforms\s*&\s*ANGLE_userUniforms\s*,\s*thread metal::float4\s*&\s*([A-Za-z0-9_]+)\s*,\s*metal::float2\s*([A-Za-z0-9_]+)\s*\)/g,
|
||||||
|
"void _umainImage(out vec4 $1, vec2 $2)"
|
||||||
|
)
|
||||||
|
.replace(
|
||||||
|
/float _umap\s*\(\s*constant ANGLE_UserUniforms\s*&\s*ANGLE_userUniforms\s*,\s*metal::float3\s*([A-Za-z0-9_]+)\s*\)/g,
|
||||||
|
"float _umap(vec3 $1)"
|
||||||
|
)
|
||||||
|
.replace(
|
||||||
|
/metal::float4 _urm\s*\(\s*constant ANGLE_UserUniforms\s*&\s*ANGLE_userUniforms\s*,\s*metal::float3\s*([A-Za-z0-9_]+)\s*,\s*metal::float3\s*([A-Za-z0-9_]+)\s*\)/g,
|
||||||
|
"vec4 _urm(vec3 $1, vec3 $2)"
|
||||||
|
)
|
||||||
|
.replace(/_umap\s*\(\s*ANGLE_userUniforms\s*,/g, "_umap(")
|
||||||
|
.replace(/_urm\s*\(\s*ANGLE_userUniforms\s*,/g, "_urm(")
|
||||||
|
.replace(/ANGLE_userUniforms\._uiResolution/g, "iResolution")
|
||||||
|
.replace(/ANGLE_userUniforms\._uiTime/g, "iTime")
|
||||||
|
.replace(/metal::fast::normalize/g, "normalize")
|
||||||
|
.replace(/metal::/g, "")
|
||||||
|
.replace(/\bthread\b/g, "")
|
||||||
|
.replace(/\bconstant\b/g, "")
|
||||||
|
.replace(/\buint32_t\b/g, "uint")
|
||||||
|
.replace(/;\s*;/g, ";");
|
||||||
|
|
||||||
|
const cleaned = [
|
||||||
|
"void ANGLE_loopForwardProgress() {}",
|
||||||
|
section,
|
||||||
|
"void mainImage(out vec4 fragColor, in vec2 fragCoord) { _umainImage(fragColor, fragCoord); }",
|
||||||
|
].join("\n");
|
||||||
|
|
||||||
|
return cleaned;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeIncomingCode(code) {
|
||||||
|
if (!code || typeof code !== "string") return { error: "code 不能为空", normalized: "" };
|
||||||
|
if (code.includes("metal::") || code.includes("[[function_constant")) {
|
||||||
|
const converted = convertAngleMetalToGlsl(code);
|
||||||
|
if (!converted) {
|
||||||
|
return { error: "自动解构失败:请检查粘贴内容是否完整。", normalized: "" };
|
||||||
|
}
|
||||||
|
return { error: "", normalized: converted };
|
||||||
|
}
|
||||||
|
if (!code.includes("mainImage")) {
|
||||||
|
return { error: "code 必须包含 mainImage", normalized: "" };
|
||||||
|
}
|
||||||
|
return { error: "", normalized: code };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function autoNormalizeStoredShaders() {
|
||||||
|
const shaders = await readDb();
|
||||||
|
let changed = false;
|
||||||
|
const next = shaders.map((item) => {
|
||||||
|
const code = String(item.code || "");
|
||||||
|
if (code.includes("metal::") || code.includes("[[function_constant")) {
|
||||||
|
const { error, normalized } = normalizeIncomingCode(code);
|
||||||
|
if (!error && normalized) {
|
||||||
|
changed = true;
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
code: normalized,
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
sourceFormat: "angle-metal-auto-converted",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
if (changed) await writeDb(next);
|
||||||
|
}
|
||||||
|
|
||||||
app.get("/api/shaders", async (_req, res) => {
|
app.get("/api/shaders", async (_req, res) => {
|
||||||
try {
|
try {
|
||||||
const shaders = await readDb();
|
const shaders = await readDb();
|
||||||
@@ -44,8 +128,9 @@ app.post("/api/shaders", async (req, res) => {
|
|||||||
if (!name || typeof name !== "string") {
|
if (!name || typeof name !== "string") {
|
||||||
return res.status(400).json({ error: "name 必填" });
|
return res.status(400).json({ error: "name 必填" });
|
||||||
}
|
}
|
||||||
if (!code || typeof code !== "string" || !code.includes("mainImage")) {
|
const { error: parseError, normalized } = normalizeIncomingCode(code);
|
||||||
return res.status(400).json({ error: "code 必须包含 mainImage" });
|
if (parseError) {
|
||||||
|
return res.status(400).json({ error: parseError });
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -54,10 +139,11 @@ app.post("/api/shaders", async (req, res) => {
|
|||||||
id: crypto.randomUUID(),
|
id: crypto.randomUUID(),
|
||||||
name: name.trim(),
|
name: name.trim(),
|
||||||
author: String(author || "unknown").trim() || "unknown",
|
author: String(author || "unknown").trim() || "unknown",
|
||||||
code,
|
code: normalized,
|
||||||
views: Math.floor(3000 + Math.random() * 22000),
|
views: Math.floor(3000 + Math.random() * 22000),
|
||||||
likes: Math.floor(40 + Math.random() * 700),
|
likes: Math.floor(40 + Math.random() * 700),
|
||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
|
sourceFormat: code.includes("metal::") ? "angle-metal-auto-converted" : "glsl",
|
||||||
};
|
};
|
||||||
shaders.unshift(item);
|
shaders.unshift(item);
|
||||||
await writeDb(shaders);
|
await writeDb(shaders);
|
||||||
@@ -82,6 +168,7 @@ app.delete("/api/shaders/:id", async (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
ensureDb().then(() => {
|
ensureDb().then(() => {
|
||||||
|
autoNormalizeStoredShaders().catch(() => {});
|
||||||
app.listen(PORT, () => {
|
app.listen(PORT, () => {
|
||||||
console.log(`Server running: http://localhost:${PORT}`);
|
console.log(`Server running: http://localhost:${PORT}`);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -26,14 +26,26 @@ fi
|
|||||||
export DOCKER_BUILDKIT=1
|
export DOCKER_BUILDKIT=1
|
||||||
export COMPOSE_DOCKER_CLI_BUILD=1
|
export COMPOSE_DOCKER_CLI_BUILD=1
|
||||||
|
|
||||||
|
find_free_port() {
|
||||||
|
local port="$1"
|
||||||
|
while lsof -nP -iTCP:"${port}" -sTCP:LISTEN >/dev/null 2>&1; do
|
||||||
|
port=$((port + 1))
|
||||||
|
done
|
||||||
|
echo "${port}"
|
||||||
|
}
|
||||||
|
|
||||||
|
HOST_PORT="$(find_free_port 5180)"
|
||||||
|
export HOST_PORT
|
||||||
|
|
||||||
echo "==> 使用国内镜像源构建并启动 VFXdemo"
|
echo "==> 使用国内镜像源构建并启动 VFXdemo"
|
||||||
echo " - Node 基础镜像: docker.m.daocloud.io"
|
echo " - Node 基础镜像: docker.m.daocloud.io"
|
||||||
echo " - npm registry: registry.npmmirror.com"
|
echo " - npm registry: registry.npmmirror.com"
|
||||||
|
echo " - Host 端口: ${HOST_PORT}"
|
||||||
|
|
||||||
"${COMPOSE_CMD[@]}" build --pull
|
"${COMPOSE_CMD[@]}" build --pull
|
||||||
"${COMPOSE_CMD[@]}" up -d
|
"${COMPOSE_CMD[@]}" up -d
|
||||||
|
|
||||||
echo
|
echo
|
||||||
echo "启动完成: http://localhost:5180"
|
echo "启动完成: http://localhost:${HOST_PORT}"
|
||||||
echo "查看日志: ${COMPOSE_CMD[*]} logs -f"
|
echo "查看日志: ${COMPOSE_CMD[*]} logs -f"
|
||||||
echo "停止服务: ${COMPOSE_CMD[*]} down"
|
echo "停止服务: ${COMPOSE_CMD[*]} down"
|
||||||
|
|||||||
Reference in New Issue
Block a user