fix:优化代码
This commit is contained in:
250
src/App.tsx
250
src/App.tsx
@@ -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>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
24326
test/Untitled
Normal file
24326
test/Untitled
Normal file
File diff suppressed because one or more lines are too long
0
test/x.html
Normal file
0
test/x.html
Normal file
1
test/x2.html
Normal file
1
test/x2.html
Normal file
@@ -0,0 +1 @@
|
||||
</body></html>
|
||||
22806
test/x3.html
Normal file
22806
test/x3.html
Normal file
File diff suppressed because it is too large
Load Diff
24326
test/x4.html
Normal file
24326
test/x4.html
Normal file
File diff suppressed because one or more lines are too long
0
test/x5.html
Normal file
0
test/x5.html
Normal file
Reference in New Issue
Block a user