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);
|
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}>
|
||||||
</button>
|
+ New Effect
|
||||||
<button
|
</button>
|
||||||
type="button"
|
<button
|
||||||
className="small secondary"
|
type="button"
|
||||||
onClick={onDeleteShader}
|
className="small secondary"
|
||||||
disabled={activePass.locked}
|
onClick={onDeleteEffect}
|
||||||
title={activePass.locked ? "内置模板不可删除" : "删除当前自定义 Shader"}
|
disabled={effects.length <= 1}
|
||||||
>
|
>
|
||||||
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>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -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
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