fix:优化代码

This commit is contained in:
Daniel
2026-04-01 18:22:28 +08:00
parent 0bf1299908
commit 1d0e0430ab
8 changed files with 71611 additions and 113 deletions

View File

@@ -17,56 +17,97 @@ function downloadFile(filename: string, content: string, type: string): void {
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
} }
type ShaderPass = { type EffectItem = {
id: string; id: string;
name: string; name: string;
fragment: string; templateName: string;
locked?: boolean; params: EffectParams;
shaderDraft: string;
shaderApplied: string;
}; };
const initialPasses: ShaderPass[] = templates.map((t) => ({ function makeEffect(templateName: string, serial: number): EffectItem {
id: t.name, const template = templateMap.get(templateName) ?? templates[0];
name: t.name, const shader = buildShader(template, template.defaultParams);
fragment: buildShader(t, t.defaultParams), return {
locked: true id: `effect-${Date.now()}-${Math.floor(Math.random() * 10000)}`,
})); name: `effect_${serial}`,
templateName: template.name,
params: { ...template.defaultParams },
shaderDraft: shader,
shaderApplied: shader
};
}
export default function App() { export default function App() {
const defaultTemplate = templates[0]; const [effects, setEffects] = useState<EffectItem[]>([makeEffect(templates[0].name, 1)]);
const [templateName, setTemplateName] = useState<string>(templates[0].name); const [activeEffectId, setActiveEffectId] = useState<string>("");
const [prompt, setPrompt] = useState<string>("hot fire"); const [prompt, setPrompt] = useState<string>("hot fire");
const [passes, setPasses] = useState<ShaderPass[]>(initialPasses);
const [activePassId, setActivePassId] = useState<string>(initialPasses[0].id);
const [params, setParams] = useState<EffectParams>(defaultTemplate.defaultParams);
const [uniforms, setUniforms] = useState(() => createUniforms(defaultTemplate.defaultParams));
const [appliedShader, setAppliedShader] = useState<string>(initialPasses[0].fragment);
const [isPlaying, setIsPlaying] = useState<boolean>(true); const [isPlaying, setIsPlaying] = useState<boolean>(true);
const previewWrapRef = useRef<HTMLDivElement | null>(null); const previewWrapRef = useRef<HTMLDivElement | null>(null);
const template = templateMap.get(templateName) ?? templates[0]; const activeEffect = useMemo(() => {
const activePass = useMemo( const direct = effects.find((effect) => effect.id === activeEffectId);
() => passes.find((p) => p.id === activePassId) ?? passes[0], return direct ?? effects[0];
[passes, activePassId] }, [effects, activeEffectId]);
);
const [uniforms, setUniforms] = useState(() => createUniforms(effects[0].params));
useEffect(() => { useEffect(() => {
updateUniformValues(uniforms, isPlaying ? params : { ...params, speed: 0 }); if (!activeEffectId && effects[0]) {
}, [uniforms, params, isPlaying]); setActiveEffectId(effects[0].id);
}
}, [activeEffectId, effects]);
const onParamChange = (key: keyof EffectParams, value: number) => { useEffect(() => {
const next = { ...params, [key]: value }; if (activeEffect) {
setParams(next); setUniforms(createUniforms(activeEffect.params));
}
}, [activeEffect?.id]);
useEffect(() => {
if (!activeEffect) {
return;
}
updateUniformValues(uniforms, isPlaying ? activeEffect.params : { ...activeEffect.params, speed: 0 });
}, [uniforms, activeEffect, isPlaying]);
const updateActiveEffect = (updater: (current: EffectItem) => EffectItem) => {
setEffects((prev) => prev.map((effect) => (effect.id === activeEffect.id ? updater(effect) : effect)));
}; };
const onTemplateSelect = (name: string) => { const onCreateEffect = () => {
const selected = templateMap.get(name); const next = makeEffect(templates[0].name, effects.length + 1);
setEffects((prev) => [...prev, next]);
setActiveEffectId(next.id);
setUniforms(createUniforms(next.params));
};
const onDeleteEffect = () => {
if (!activeEffect || effects.length <= 1) {
return;
}
const currentIndex = effects.findIndex((effect) => effect.id === activeEffect.id);
const nextEffects = effects.filter((effect) => effect.id !== activeEffect.id);
const fallback = nextEffects[Math.max(0, currentIndex - 1)] ?? nextEffects[0];
setEffects(nextEffects);
setActiveEffectId(fallback.id);
setUniforms(createUniforms(fallback.params));
};
const onSelectTemplate = (templateName: string) => {
const selected = templateMap.get(templateName);
if (!selected) { if (!selected) {
return; return;
} }
setTemplateName(name); const nextShader = buildShader(selected, selected.defaultParams);
setActivePassId(selected.name); updateActiveEffect((current) => ({
setAppliedShader(passes.find((p) => p.id === selected.name)?.fragment ?? appliedShader); ...current,
setParams(selected.defaultParams); templateName: selected.name,
params: { ...selected.defaultParams },
shaderDraft: nextShader,
shaderApplied: nextShader
}));
setUniforms(createUniforms(selected.defaultParams)); setUniforms(createUniforms(selected.defaultParams));
}; };
@@ -76,68 +117,43 @@ export default function App() {
if (!selected) { if (!selected) {
return; return;
} }
setTemplateName(selected.name); const nextShader = buildShader(selected, result.params);
setActivePassId(selected.name); updateActiveEffect((current) => ({
setAppliedShader(passes.find((p) => p.id === selected.name)?.fragment ?? appliedShader); ...current,
setParams(result.params); templateName: selected.name,
params: { ...result.params },
shaderDraft: nextShader,
shaderApplied: nextShader
}));
setUniforms(createUniforms(result.params)); setUniforms(createUniforms(result.params));
}; };
const onActiveShaderChange = (value: string) => { const onParamChange = (key: keyof EffectParams, value: number) => {
setPasses((prev) => prev.map((p) => (p.id === activePass.id ? { ...p, fragment: value } : p))); updateActiveEffect((current) => ({
...current,
params: { ...current.params, [key]: value }
}));
}; };
const onApplyShader = () => { const onApplyShader = () => {
setAppliedShader(activePass.fragment); updateActiveEffect((current) => ({ ...current, shaderApplied: current.shaderDraft }));
};
const onAddShader = () => {
const customCount = passes.filter((p) => p.id.startsWith("custom-")).length + 1;
const id = `custom-${Date.now()}`;
const next: ShaderPass = {
id,
name: `custom_${customCount}`,
fragment: activePass.fragment
};
setPasses((prev) => [...prev, next]);
setActivePassId(id);
setAppliedShader(next.fragment);
};
const onDeleteShader = () => {
if (!activePass || activePass.locked || passes.length <= 1) {
return;
}
const index = passes.findIndex((p) => p.id === activePass.id);
const nextPasses = passes.filter((p) => p.id !== activePass.id);
const fallback = nextPasses[Math.max(0, index - 1)];
setPasses(nextPasses);
setActivePassId(fallback.id);
setAppliedShader(fallback.fragment);
};
const onTogglePlay = () => {
setIsPlaying((prev) => !prev);
};
const onFullscreen = () => {
previewWrapRef.current?.requestFullscreen?.();
}; };
const onExport = () => { const onExport = () => {
downloadFile("fragment.glsl", appliedShader, "text/plain;charset=utf-8"); downloadFile("fragment.glsl", activeEffect.shaderApplied, "text/plain;charset=utf-8");
downloadFile("vertex.glsl", baseVertexShader, "text/plain;charset=utf-8"); downloadFile("vertex.glsl", baseVertexShader, "text/plain;charset=utf-8");
downloadFile( downloadFile(
"material.json", "material.json",
JSON.stringify( JSON.stringify(
{ {
type: "ShaderMaterial", type: "ShaderMaterial",
template: activePass.name, effect: activeEffect.name,
template: activeEffect.templateName,
uniforms: { uniforms: {
u_time: 0, u_time: 0,
u_speed: params.speed, u_speed: activeEffect.params.speed,
u_scale: params.scale, u_scale: activeEffect.params.scale,
u_intensity: params.intensity u_intensity: activeEffect.params.intensity
} }
}, },
null, null,
@@ -147,23 +163,30 @@ export default function App() {
); );
}; };
const onFullscreen = () => {
previewWrapRef.current?.requestFullscreen?.();
};
return ( return (
<main className="app-shell"> <main className="app-shell">
<header className="topbar"> <header className="topbar">
<h1>AI VFX Shader Tool</h1> <h1>AI VFX Shader Tool</h1>
<p> Shadertoy + + Shader /</p>
</header> </header>
<section className="content-grid"> <section className="content-grid">
<section className="left-column"> <section className="left-column">
<article className="card player" ref={previewWrapRef}> <article className="card player" ref={previewWrapRef}>
<Preview vertexShader={baseVertexShader} fragmentShader={appliedShader} uniforms={uniforms} /> <Preview
vertexShader={baseVertexShader}
fragmentShader={activeEffect.shaderApplied}
uniforms={uniforms}
/>
<div className="player-bar"> <div className="player-bar">
<span className="status-text"> <span className="status-text">
Active: <strong>{activePass.name}</strong> Active Effect: <strong>{activeEffect.name}</strong>
</span> </span>
<div className="player-actions"> <div className="player-actions">
<button type="button" className="secondary" onClick={onTogglePlay}> <button type="button" className="secondary" onClick={() => setIsPlaying((prev) => !prev)}>
{isPlaying ? "Pause" : "Play"} {isPlaying ? "Pause" : "Play"}
</button> </button>
<button type="button" onClick={onFullscreen}> <button type="button" onClick={onFullscreen}>
@@ -174,43 +197,43 @@ export default function App() {
</article> </article>
<article className="card editor-shell"> <article className="card editor-shell">
<div className="pass-manager"> <div className="effect-manager">
{passes.map((pass) => ( {effects.map((effect) => (
<button <button
key={pass.id} key={effect.id}
type="button" type="button"
className={`pass-tab ${activePass.id === pass.id ? "active" : ""}`} className={`effect-tab ${activeEffect.id === effect.id ? "active" : ""}`}
onClick={() => { onClick={() => {
setActivePassId(pass.id); setActiveEffectId(effect.id);
setAppliedShader(pass.fragment); setUniforms(createUniforms(effect.params));
if (templateMap.has(pass.id)) {
setTemplateName(pass.id);
}
}} }}
> >
{pass.name} {effect.name}
</button> </button>
))} ))}
<button type="button" className="small secondary" onClick={onAddShader}> <div className="effect-actions">
+ Add Shader <button type="button" className="small secondary" onClick={onCreateEffect}>
+ New Effect
</button> </button>
<button <button
type="button" type="button"
className="small secondary" className="small secondary"
onClick={onDeleteShader} onClick={onDeleteEffect}
disabled={activePass.locked} disabled={effects.length <= 1}
title={activePass.locked ? "内置模板不可删除" : "删除当前自定义 Shader"}
> >
Delete Delete Effect
</button> </button>
</div> </div>
</div>
<label className="field shader-editor"> <label className="field shader-editor">
<span>Fragment Shader (Editable)</span> <span>Fragment Shader (Editable)</span>
<textarea <textarea
value={activePass.fragment} value={activeEffect.shaderDraft}
rows={16} rows={16}
onChange={(e) => onActiveShaderChange(e.target.value)} onChange={(e) =>
updateActiveEffect((current) => ({ ...current, shaderDraft: e.target.value }))
}
/> />
</label> </label>
@@ -226,11 +249,20 @@ export default function App() {
</section> </section>
<aside className="right-column card panel"> <aside className="right-column card panel">
<h3>Interaction</h3> <h3>Effect Settings</h3>
<label className="field">
<span>Effect Name</span>
<input
value={activeEffect.name}
onChange={(e) => updateActiveEffect((current) => ({ ...current, name: e.target.value }))}
placeholder="输入特效名称"
/>
</label>
<label className="field"> <label className="field">
<span>Template</span> <span>Template</span>
<select value={templateName} onChange={(e) => onTemplateSelect(e.target.value)}> <select value={activeEffect.templateName} onChange={(e) => onSelectTemplate(e.target.value)}>
{templates.map((t) => ( {templates.map((t) => (
<option key={t.name} value={t.name}> <option key={t.name} value={t.name}>
{t.name} {t.name}
@@ -266,10 +298,10 @@ export default function App() {
min={min} min={min}
max={max} max={max}
step={step} step={step}
value={params[key]} value={activeEffect.params[key]}
onChange={(e) => onParamChange(key, Number(e.target.value))} onChange={(e) => onParamChange(key, Number(e.target.value))}
/> />
<strong>{params[key].toFixed(2)}</strong> <strong>{activeEffect.params[key].toFixed(2)}</strong>
</label> </label>
); );
})} })}

View File

@@ -92,14 +92,15 @@ body {
gap: 10px; gap: 10px;
} }
.pass-manager { .effect-manager {
display: flex; display: flex;
gap: 8px; gap: 8px;
overflow-x: auto; overflow-x: auto;
padding-bottom: 2px; padding-bottom: 2px;
align-items: center;
} }
.pass-tab, .effect-tab,
button { button {
border: none; border: none;
border-radius: 8px; border-radius: 8px;
@@ -111,18 +112,24 @@ button {
white-space: nowrap; white-space: nowrap;
} }
.pass-tab { .effect-tab {
background: #2d3e5d; background: #2d3e5d;
border: 1px solid rgba(132, 168, 221, 0.24); border: 1px solid rgba(132, 168, 221, 0.24);
} }
.pass-tab.active { .effect-tab.active {
background: #f39a34; background: #f39a34;
color: #141a27; color: #141a27;
font-weight: 700; font-weight: 700;
border-color: transparent; border-color: transparent;
} }
.effect-actions {
margin-left: auto;
display: flex;
gap: 8px;
}
button:hover { button:hover {
filter: brightness(1.08); filter: brightness(1.08);
} }

24326
test/Untitled Normal file

File diff suppressed because one or more lines are too long

0
test/x.html Normal file
View File

1
test/x2.html Normal file
View File

@@ -0,0 +1 @@
</body></html>

22806
test/x3.html Normal file

File diff suppressed because it is too large Load Diff

24326
test/x4.html Normal file

File diff suppressed because one or more lines are too long

0
test/x5.html Normal file
View File