This commit is contained in:
Daniel
2026-04-01 19:57:47 +08:00
parent 1d0e0430ab
commit 53d17883ac
4505 changed files with 1659892 additions and 6 deletions

View File

@@ -1,4 +1,5 @@
import { useEffect, useMemo, useRef, useState } from "react";
import * as THREE from "three";
import Preview from "./components/Preview";
import { generateEffect } from "./engine/aiMock";
import { buildShader } from "./engine/shaderBuilder";
@@ -40,6 +41,8 @@ function makeEffect(templateName: string, serial: number): EffectItem {
}
export default function App() {
const channelIndices = [0, 1, 2, 3] as const;
const channelKeys = ["iChannel0", "iChannel1", "iChannel2", "iChannel3"] as const;
const [effects, setEffects] = useState<EffectItem[]>([makeEffect(templates[0].name, 1)]);
const [activeEffectId, setActiveEffectId] = useState<string>("");
const [prompt, setPrompt] = useState<string>("hot fire");
@@ -52,6 +55,14 @@ export default function App() {
}, [effects, activeEffectId]);
const [uniforms, setUniforms] = useState(() => createUniforms(effects[0].params));
const [channelTextures, setChannelTextures] = useState<Array<THREE.Texture | null>>([
null,
null,
null,
null
]);
const [channelFileNames, setChannelFileNames] = useState<string[]>(["None", "None", "None", "None"]);
const channelTexturesRef = useRef<Array<THREE.Texture | null>>([null, null, null, null]);
useEffect(() => {
if (!activeEffectId && effects[0]) {
@@ -72,6 +83,29 @@ export default function App() {
updateUniformValues(uniforms, isPlaying ? activeEffect.params : { ...activeEffect.params, speed: 0 });
}, [uniforms, activeEffect, isPlaying]);
useEffect(() => {
channelTexturesRef.current = channelTextures;
channelIndices.forEach((idx) => {
const channelKey = channelKeys[idx];
const texture = channelTextures[idx];
uniforms[channelKey].value = texture;
if (texture && texture.image) {
const image = texture.image as { width?: number; height?: number };
const width = image.width ?? 1;
const height = image.height ?? 1;
uniforms.iChannelResolution.value[idx].set(width, height, 1);
} else {
uniforms.iChannelResolution.value[idx].set(1, 1, 1);
}
});
}, [channelTextures, uniforms]);
useEffect(() => {
return () => {
channelTexturesRef.current.forEach((texture) => texture?.dispose());
};
}, []);
const updateActiveEffect = (updater: (current: EffectItem) => EffectItem) => {
setEffects((prev) => prev.map((effect) => (effect.id === activeEffect.id ? updater(effect) : effect)));
};
@@ -167,6 +201,51 @@ export default function App() {
previewWrapRef.current?.requestFullscreen?.();
};
const onUploadChannelTexture = (channelIndex: number, file: File) => {
const objectUrl = URL.createObjectURL(file);
const loader = new THREE.TextureLoader();
loader.load(
objectUrl,
(texture) => {
texture.needsUpdate = true;
setChannelTextures((prev) => {
const next = [...prev];
next[channelIndex]?.dispose();
next[channelIndex] = texture;
return next;
});
setChannelFileNames((prev) => {
const next = [...prev];
next[channelIndex] = file.name;
return next;
});
URL.revokeObjectURL(objectUrl);
},
undefined,
() => {
URL.revokeObjectURL(objectUrl);
}
);
};
const onClearChannelTexture = (channelIndex: number) => {
setChannelTextures((prev) => {
const next = [...prev];
next[channelIndex]?.dispose();
next[channelIndex] = null;
return next;
});
setChannelFileNames((prev) => {
const next = [...prev];
next[channelIndex] = "None";
return next;
});
};
const onSampleRateChange = (value: number) => {
uniforms.iSampleRate.value = Number.isFinite(value) && value > 0 ? value : 44100;
};
return (
<main className="app-shell">
<header className="topbar">
@@ -305,6 +384,43 @@ export default function App() {
</label>
);
})}
<label className="field">
<span>iSampleRate</span>
<input
type="number"
min={1}
value={uniforms.iSampleRate.value}
onChange={(e) => onSampleRateChange(Number(e.target.value))}
/>
</label>
<div className="channel-panel">
<h4>Shadertoy Channels</h4>
{channelIndices.map((channelIndex) => (
<div key={channelIndex} className="channel-row">
<span>{`iChannel${channelIndex}`}</span>
<input
type="file"
accept="image/*"
onChange={(e) => {
const file = e.target.files?.[0];
if (file) {
onUploadChannelTexture(channelIndex, file);
}
}}
/>
<small>{channelFileNames[channelIndex]}</small>
<button
type="button"
className="small secondary"
onClick={() => onClearChannelTexture(channelIndex)}
>
Clear
</button>
</div>
))}
</div>
</aside>
</section>
</main>