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);
}
type ShaderPass = {
type EffectItem = {
id: string;
name: string;
fragment: string;
locked?: boolean;
templateName: string;
params: EffectParams;
shaderDraft: string;
shaderApplied: string;
};
const initialPasses: ShaderPass[] = templates.map((t) => ({
id: t.name,
name: t.name,
fragment: buildShader(t, t.defaultParams),
locked: true
}));
function makeEffect(templateName: string, serial: number): EffectItem {
const template = templateMap.get(templateName) ?? templates[0];
const shader = buildShader(template, template.defaultParams);
return {
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() {
const defaultTemplate = templates[0];
const [templateName, setTemplateName] = useState<string>(templates[0].name);
const [effects, setEffects] = useState<EffectItem[]>([makeEffect(templates[0].name, 1)]);
const [activeEffectId, setActiveEffectId] = useState<string>("");
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 previewWrapRef = useRef<HTMLDivElement | null>(null);
const template = templateMap.get(templateName) ?? templates[0];
const activePass = useMemo(
() => passes.find((p) => p.id === activePassId) ?? passes[0],
[passes, activePassId]
);
const activeEffect = useMemo(() => {
const direct = effects.find((effect) => effect.id === activeEffectId);
return direct ?? effects[0];
}, [effects, activeEffectId]);
const [uniforms, setUniforms] = useState(() => createUniforms(effects[0].params));
useEffect(() => {
updateUniformValues(uniforms, isPlaying ? params : { ...params, speed: 0 });
}, [uniforms, params, isPlaying]);
if (!activeEffectId && effects[0]) {
setActiveEffectId(effects[0].id);
}
}, [activeEffectId, effects]);
const onParamChange = (key: keyof EffectParams, value: number) => {
const next = { ...params, [key]: value };
setParams(next);
useEffect(() => {
if (activeEffect) {
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 selected = templateMap.get(name);
const onCreateEffect = () => {
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) {
return;
}
setTemplateName(name);
setActivePassId(selected.name);
setAppliedShader(passes.find((p) => p.id === selected.name)?.fragment ?? appliedShader);
setParams(selected.defaultParams);
const nextShader = buildShader(selected, selected.defaultParams);
updateActiveEffect((current) => ({
...current,
templateName: selected.name,
params: { ...selected.defaultParams },
shaderDraft: nextShader,
shaderApplied: nextShader
}));
setUniforms(createUniforms(selected.defaultParams));
};
@@ -76,68 +117,43 @@ export default function App() {
if (!selected) {
return;
}
setTemplateName(selected.name);
setActivePassId(selected.name);
setAppliedShader(passes.find((p) => p.id === selected.name)?.fragment ?? appliedShader);
setParams(result.params);
const nextShader = buildShader(selected, result.params);
updateActiveEffect((current) => ({
...current,
templateName: selected.name,
params: { ...result.params },
shaderDraft: nextShader,
shaderApplied: nextShader
}));
setUniforms(createUniforms(result.params));
};
const onActiveShaderChange = (value: string) => {
setPasses((prev) => prev.map((p) => (p.id === activePass.id ? { ...p, fragment: value } : p)));
const onParamChange = (key: keyof EffectParams, value: number) => {
updateActiveEffect((current) => ({
...current,
params: { ...current.params, [key]: value }
}));
};
const onApplyShader = () => {
setAppliedShader(activePass.fragment);
};
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?.();
updateActiveEffect((current) => ({ ...current, shaderApplied: current.shaderDraft }));
};
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(
"material.json",
JSON.stringify(
{
type: "ShaderMaterial",
template: activePass.name,
effect: activeEffect.name,
template: activeEffect.templateName,
uniforms: {
u_time: 0,
u_speed: params.speed,
u_scale: params.scale,
u_intensity: params.intensity
u_speed: activeEffect.params.speed,
u_scale: activeEffect.params.scale,
u_intensity: activeEffect.params.intensity
}
},
null,
@@ -147,23 +163,30 @@ export default function App() {
);
};
const onFullscreen = () => {
previewWrapRef.current?.requestFullscreen?.();
};
return (
<main className="app-shell">
<header className="topbar">
<h1>AI VFX Shader Tool</h1>
<p> Shadertoy + + Shader /</p>
</header>
<section className="content-grid">
<section className="left-column">
<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">
<span className="status-text">
Active: <strong>{activePass.name}</strong>
Active Effect: <strong>{activeEffect.name}</strong>
</span>
<div className="player-actions">
<button type="button" className="secondary" onClick={onTogglePlay}>
<button type="button" className="secondary" onClick={() => setIsPlaying((prev) => !prev)}>
{isPlaying ? "Pause" : "Play"}
</button>
<button type="button" onClick={onFullscreen}>
@@ -174,43 +197,43 @@ export default function App() {
</article>
<article className="card editor-shell">
<div className="pass-manager">
{passes.map((pass) => (
<div className="effect-manager">
{effects.map((effect) => (
<button
key={pass.id}
key={effect.id}
type="button"
className={`pass-tab ${activePass.id === pass.id ? "active" : ""}`}
className={`effect-tab ${activeEffect.id === effect.id ? "active" : ""}`}
onClick={() => {
setActivePassId(pass.id);
setAppliedShader(pass.fragment);
if (templateMap.has(pass.id)) {
setTemplateName(pass.id);
}
setActiveEffectId(effect.id);
setUniforms(createUniforms(effect.params));
}}
>
{pass.name}
{effect.name}
</button>
))}
<button type="button" className="small secondary" onClick={onAddShader}>
+ Add Shader
</button>
<button
type="button"
className="small secondary"
onClick={onDeleteShader}
disabled={activePass.locked}
title={activePass.locked ? "内置模板不可删除" : "删除当前自定义 Shader"}
>
Delete
</button>
<div className="effect-actions">
<button type="button" className="small secondary" onClick={onCreateEffect}>
+ New Effect
</button>
<button
type="button"
className="small secondary"
onClick={onDeleteEffect}
disabled={effects.length <= 1}
>
Delete Effect
</button>
</div>
</div>
<label className="field shader-editor">
<span>Fragment Shader (Editable)</span>
<textarea
value={activePass.fragment}
value={activeEffect.shaderDraft}
rows={16}
onChange={(e) => onActiveShaderChange(e.target.value)}
onChange={(e) =>
updateActiveEffect((current) => ({ ...current, shaderDraft: e.target.value }))
}
/>
</label>
@@ -226,11 +249,20 @@ export default function App() {
</section>
<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">
<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) => (
<option key={t.name} value={t.name}>
{t.name}
@@ -266,10 +298,10 @@ export default function App() {
min={min}
max={max}
step={step}
value={params[key]}
value={activeEffect.params[key]}
onChange={(e) => onParamChange(key, Number(e.target.value))}
/>
<strong>{params[key].toFixed(2)}</strong>
<strong>{activeEffect.params[key].toFixed(2)}</strong>
</label>
);
})}

View File

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