Control ui adjsutments
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 1m54s
CI / Windows Release Package (push) Successful in 2m9s

This commit is contained in:
2026-05-08 13:54:02 +10:00
parent 7035cde8c8
commit bc536bd751
10 changed files with 135 additions and 120 deletions

View File

@@ -240,6 +240,6 @@ If `SLANG_ROOT` is not set, the workflow falls back to the repo-local default un
- More comprehensive greenscreen shader - More comprehensive greenscreen shader
- linear compositing? - linear compositing?
- compute shaders or a small 1x1 or nx1 RGBA16f render target for abritary data store - compute shaders or a small 1x1 or nx1 RGBA16f render target for abritary data store
- allow shaders to read other shaders data store based on name? or putput over OSC - allow shaders to read other shaders data store based on name? or output over OSC
- Mipmappong needed? - Mipmappong needed?
- unwrap a fish eyelens and mirror it and map it to equirectangulr for environmnet map purposes - unwrap a fish eyelens and mirror it and map it to equirectangulr for environmnet map purposes

14
ui/package-lock.json generated
View File

@@ -10,6 +10,7 @@
"dependencies": { "dependencies": {
"@uiw/color-convert": "^2.10.1", "@uiw/color-convert": "^2.10.1",
"@uiw/react-color-wheel": "^2.10.1", "@uiw/react-color-wheel": "^2.10.1",
"fuse.js": "^7.3.0",
"lucide-react": "^0.511.0", "lucide-react": "^0.511.0",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1" "react-dom": "^18.3.1"
@@ -1429,6 +1430,19 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0" "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
} }
}, },
"node_modules/fuse.js": {
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-7.3.0.tgz",
"integrity": "sha512-plz8RVjfcDedTGfVngWH1jmJvBvAwi1v2jecfDerbEnMcmOYUEEwKFTHbNoCiYyzaK2Ws8lABkTCcRSqCY1q4w==",
"license": "Apache-2.0",
"engines": {
"node": ">=10"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/krisk"
}
},
"node_modules/gensync": { "node_modules/gensync": {
"version": "1.0.0-beta.2", "version": "1.0.0-beta.2",
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",

View File

@@ -11,6 +11,7 @@
"dependencies": { "dependencies": {
"@uiw/color-convert": "^2.10.1", "@uiw/color-convert": "^2.10.1",
"@uiw/react-color-wheel": "^2.10.1", "@uiw/react-color-wheel": "^2.10.1",
"fuse.js": "^7.3.0",
"lucide-react": "^0.511.0", "lucide-react": "^0.511.0",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1" "react-dom": "^18.3.1"

View File

@@ -23,7 +23,6 @@ function AppFooter() {
function App() { function App() {
const [appState, setAppState] = useRuntimeState(); const [appState, setAppState] = useRuntimeState();
const [pendingShaderId, setPendingShaderId] = useState("");
const [presetName, setPresetName] = useState(""); const [presetName, setPresetName] = useState("");
const [selectedPresetName, setSelectedPresetName] = useState(""); const [selectedPresetName, setSelectedPresetName] = useState("");
const [expandedLayerIds, setExpandedLayerIds] = useState([]); const [expandedLayerIds, setExpandedLayerIds] = useState([]);
@@ -38,14 +37,6 @@ function App() {
const app = appState?.app ?? {}; const app = appState?.app ?? {};
const stackPresets = appState?.stackPresets ?? []; const stackPresets = appState?.stackPresets ?? [];
useEffect(() => {
if (!pendingShaderId && shaders.length > 0) {
setPendingShaderId(shaders[0].id);
} else if (pendingShaderId && !shaders.some((shader) => shader.id === pendingShaderId)) {
setPendingShaderId(shaders[0]?.id ?? "");
}
}, [pendingShaderId, shaders]);
useEffect(() => { useEffect(() => {
if (!selectedPresetName && stackPresets.length > 0) { if (!selectedPresetName && stackPresets.length > 0) {
setSelectedPresetName(stackPresets[0]); setSelectedPresetName(stackPresets[0]);
@@ -123,12 +114,10 @@ function App() {
dropTargetLayerId={dropTargetLayerId} dropTargetLayerId={dropTargetLayerId}
expandedLayerIds={expandedLayerIds} expandedLayerIds={expandedLayerIds}
layers={layers} layers={layers}
pendingShaderId={pendingShaderId}
setAppState={setAppState} setAppState={setAppState}
setDragLayerId={setDragLayerId} setDragLayerId={setDragLayerId}
setDropTargetLayerId={setDropTargetLayerId} setDropTargetLayerId={setDropTargetLayerId}
setExpandedLayerIds={setExpandedLayerIds} setExpandedLayerIds={setExpandedLayerIds}
setPendingShaderId={setPendingShaderId}
shaders={shaders} shaders={shaders}
/> />

View File

@@ -1,4 +1,4 @@
import { GripVertical, Trash2 } from "lucide-react"; import { EyeOff, GripVertical, RotateCcw, SlidersHorizontal, Trash2 } from "lucide-react";
import { postJson } from "../api/controlApi"; import { postJson } from "../api/controlApi";
import { ParameterField } from "./ParameterField"; import { ParameterField } from "./ParameterField";
@@ -71,11 +71,13 @@ export function LayerCard({
}) })
} }
/> />
<EyeOff size={15} strokeWidth={1.8} aria-hidden="true" />
<span>Bypass</span> <span>Bypass</span>
</label> </label>
<button type="button" onClick={() => onToggleExpanded(layer.id)}> <button type="button" className="button-with-icon" onClick={() => onToggleExpanded(layer.id)}>
{expanded ? "Hide" : "Controls"} <SlidersHorizontal size={16} strokeWidth={1.9} aria-hidden="true" />
<span>{expanded ? "Hide" : "Controls"}</span>
</button> </button>
<button <button
type="button" type="button"
@@ -116,10 +118,12 @@ export function LayerCard({
<h3>Parameters</h3> <h3>Parameters</h3>
<button <button
type="button" type="button"
className="button-with-icon"
disabled={layer.parameters.length === 0} disabled={layer.parameters.length === 0}
onClick={() => postJson("/api/layers/reset-parameters", { layerId: layer.id })} onClick={() => postJson("/api/layers/reset-parameters", { layerId: layer.id })}
> >
Reset <RotateCcw size={16} strokeWidth={1.9} aria-hidden="true" />
<span>Reset</span>
</button> </button>
</div> </div>

View File

@@ -18,12 +18,10 @@ export function LayerStack({
dropTargetLayerId, dropTargetLayerId,
expandedLayerIds, expandedLayerIds,
layers, layers,
pendingShaderId,
setAppState, setAppState,
setDragLayerId, setDragLayerId,
setDropTargetLayerId, setDropTargetLayerId,
setExpandedLayerIds, setExpandedLayerIds,
setPendingShaderId,
shaders, shaders,
}) { }) {
const expandedSet = new Set(expandedLayerIds); const expandedSet = new Set(expandedLayerIds);
@@ -118,27 +116,13 @@ export function LayerStack({
<span className="layer-card__index">+</span> <span className="layer-card__index">+</span>
<div className="layer-card__title layer-card__title--static">Add Layer</div> <div className="layer-card__title layer-card__title--static">Add Layer</div>
</div> </div>
<div className="layer-card__actions">
<button
type="button"
disabled={!pendingShaderId}
onClick={() => {
if (pendingShaderId) {
postJson("/api/layers/add", { shaderId: pendingShaderId });
}
}}
>
Add
</button>
</div>
</div> </div>
<div className="layer-card__body"> <div className="layer-card__body">
<div className="layer-card__field"> <div className="layer-card__field">
<ShaderPicker <ShaderPicker
id="add-layer" id="add-layer"
shaders={shaders} shaders={shaders}
value={pendingShaderId} onAdd={(shaderId) => postJson("/api/layers/add", { shaderId })}
onChange={setPendingShaderId}
/> />
</div> </div>
</div> </div>

View File

@@ -1,6 +1,6 @@
import Wheel from "@uiw/react-color-wheel"; import Wheel from "@uiw/react-color-wheel";
import { hsvaToRgba, rgbaToHsva } from "@uiw/color-convert"; import { hsvaToRgba, rgbaToHsva } from "@uiw/color-convert";
import { Copy, RotateCcw } from "lucide-react"; import { Copy, RotateCcw, Zap } from "lucide-react";
import { useThrottledParameterValue } from "../hooks/useThrottledParameterValue"; import { useThrottledParameterValue } from "../hooks/useThrottledParameterValue";
import { ParameterValueDisplay } from "./ParameterValueDisplay"; import { ParameterValueDisplay } from "./ParameterValueDisplay";
@@ -325,10 +325,11 @@ export function ParameterField({ layer, parameter, onParameterChange }) {
{header} {header}
<button <button
type="button" type="button"
className="parameter__trigger" className="button-with-icon parameter__trigger"
onClick={() => sendValue(triggerCount + 1)} onClick={() => sendValue(triggerCount + 1)}
> >
Trigger <Zap size={16} strokeWidth={1.9} aria-hidden="true" />
<span>Trigger</span>
</button> </button>
<ParameterValueDisplay parameterType={parameter.type} value={appliedValue} pending={isPending} /> <ParameterValueDisplay parameterType={parameter.type} value={appliedValue} pending={isPending} />
</section> </section>

View File

@@ -1,16 +1,19 @@
import { ChevronDown, Search } from "lucide-react"; import Fuse from "fuse.js";
import { Plus, Search } from "lucide-react";
import { useMemo, useState } from "react"; import { useMemo, useState } from "react";
function matchesShader(shader, query) { const shaderSearchOptions = {
const normalizedQuery = query.trim().toLowerCase(); threshold: 0.38,
if (!normalizedQuery) { ignoreLocation: true,
return true; minMatchCharLength: 2,
} keys: [
{ name: "name", weight: 0.45 },
return [shader.name, shader.id, shader.category, shader.description, shader.error] { name: "id", weight: 0.25 },
.filter(Boolean) { name: "category", weight: 0.15 },
.some((value) => value.toLowerCase().includes(normalizedQuery)); { name: "description", weight: 0.1 },
} { name: "error", weight: 0.05 },
],
};
function shaderSummary(shader) { function shaderSummary(shader) {
if (!shader) { if (!shader) {
@@ -37,86 +40,78 @@ function ShaderOptionContent({ shader }) {
); );
} }
export function ShaderPicker({ id, label = "Shader", shaders, value, onChange }) { export function ShaderPicker({ id, label = "Shader", shaders, onAdd }) {
const [query, setQuery] = useState(""); const [query, setQuery] = useState("");
const [open, setOpen] = useState(false);
const filteredShaders = useMemo( const shaderSearch = useMemo(
() => shaders.filter((shader) => matchesShader(shader, query)), () => new Fuse(shaders, shaderSearchOptions),
[query, shaders], [shaders],
); );
const selectedShader = shaders.find((shader) => shader.id === value); const filteredShaders = useMemo(
() => {
const normalizedQuery = query.trim();
if (!normalizedQuery) {
return shaders;
}
return shaderSearch.search(normalizedQuery).map((result) => result.item);
},
[query, shaderSearch, shaders],
);
return ( return (
<div className="shader-picker"> <div className="shader-picker">
<div className="shader-picker__topline"> <div className="shader-picker__topline">
<label id={`${id}-label`}>{label}</label> <label id={`${id}-label`}>{label}</label>
{selectedShader ? <span className="shader-picker__selected">{selectedShader.name}</span> : null}
</div> </div>
<button <div className="shader-picker__popover">
type="button" <div className="shader-picker__search">
className="shader-picker__trigger" <Search size={16} strokeWidth={1.75} aria-hidden="true" />
aria-labelledby={`${id}-label`} <input
aria-expanded={open} id={`${id}-search`}
onClick={() => setOpen((current) => !current)} type="text"
> value={query}
<span> placeholder="Search shaders"
<span className="shader-picker__option-head"> onChange={(event) => setQuery(event.target.value)}
<span className="shader-picker__name">{selectedShader?.name ?? "Choose shader"}</span> />
{selectedShader?.available === false ? ( </div>
<span className="shader-picker__category shader-picker__category--error">Error</span>
) : selectedShader?.category ? (
<span className="shader-picker__category">{selectedShader.category}</span>
) : null}
</span>
<span className="shader-picker__meta">{shaderSummary(selectedShader)}</span>
</span>
<ChevronDown size={16} strokeWidth={1.75} aria-hidden="true" />
</button>
{open ? ( <div className="shader-picker__list" role="list" aria-labelledby={`${id}-label`}>
<div className="shader-picker__popover"> {filteredShaders.length > 0 ? (
<div className="shader-picker__search"> filteredShaders.map((shader) => (
<Search size={16} strokeWidth={1.75} aria-hidden="true" /> <div
<input key={shader.id}
id={`${id}-search`} className={`shader-picker__option${shader.available === false ? " shader-picker__option--unavailable" : ""}`}
type="text" role="listitem"
value={query} >
placeholder="Search shaders" <span className="shader-picker__option-copy">
onChange={(event) => setQuery(event.target.value)} <ShaderOptionContent shader={shader} />
/> </span>
</div>
<div className="shader-picker__list" role="listbox" aria-label={label}>
{filteredShaders.length > 0 ? (
filteredShaders.map((shader) => (
<button <button
key={shader.id}
type="button" type="button"
className={`shader-picker__option${shader.id === value ? " shader-picker__option--selected" : ""}${shader.available === false ? " shader-picker__option--unavailable" : ""}`} className="icon-button shader-picker__add"
role="option" title={`Add ${shader.name}`}
aria-selected={shader.id === value} aria-label={`Add ${shader.name}`}
disabled={shader.available === false} disabled={shader.available === false}
onClick={() => { onClick={() => {
if (shader.available === false) { if (shader.available === false) {
return; return;
} }
onChange(shader.id); onAdd(shader.id);
setOpen(false);
setQuery(""); setQuery("");
}} }}
> >
<ShaderOptionContent shader={shader} /> <Plus size={16} strokeWidth={1.9} />
</button> </button>
)) </div>
) : ( ))
<div className="shader-picker__empty">No shaders found</div> ) : (
)} <div className="shader-picker__empty">No shaders found</div>
</div> )}
</div> </div>
) : null} </div>
</div> </div>
); );
} }

View File

@@ -1,3 +1,5 @@
import { FolderOpen, RefreshCw, Save } from "lucide-react";
import { postJson } from "../api/controlApi"; import { postJson } from "../api/controlApi";
export function StackPresetToolbar({ export function StackPresetToolbar({
@@ -14,8 +16,13 @@ export function StackPresetToolbar({
<h3>Stack presets</h3> <h3>Stack presets</h3>
<p className="muted">Save or recall the current layer chain.</p> <p className="muted">Save or recall the current layer chain.</p>
</div> </div>
<button type="button" className="stack-panel__reload" onClick={() => postJson("/api/reload", {})}> <button
Reload shader type="button"
className="button-with-icon stack-panel__reload"
onClick={() => postJson("/api/reload", {})}
>
<RefreshCw size={16} strokeWidth={1.9} aria-hidden="true" />
<span>Reload shader</span>
</button> </button>
</div> </div>
@@ -32,6 +39,7 @@ export function StackPresetToolbar({
/> />
<button <button
type="button" type="button"
className="button-with-icon"
disabled={!presetName.trim()} disabled={!presetName.trim()}
onClick={() => { onClick={() => {
const trimmedName = presetName.trim(); const trimmedName = presetName.trim();
@@ -44,7 +52,8 @@ export function StackPresetToolbar({
); );
}} }}
> >
Save <Save size={16} strokeWidth={1.9} aria-hidden="true" />
<span>Save</span>
</button> </button>
</div> </div>
</div> </div>
@@ -66,6 +75,7 @@ export function StackPresetToolbar({
</select> </select>
<button <button
type="button" type="button"
className="button-with-icon"
disabled={!selectedPresetName} disabled={!selectedPresetName}
onClick={() => { onClick={() => {
if (selectedPresetName) { if (selectedPresetName) {
@@ -73,7 +83,8 @@ export function StackPresetToolbar({
} }
}} }}
> >
Recall <FolderOpen size={16} strokeWidth={1.9} aria-hidden="true" />
<span>Recall</span>
</button> </button>
</div> </div>
</div> </div>

View File

@@ -156,6 +156,17 @@ button:active:not(:disabled) {
transform: translateY(1px); transform: translateY(1px);
} }
.button-with-icon {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.45rem;
}
.button-with-icon svg {
flex: 0 0 auto;
}
button:disabled, button:disabled,
input:disabled, input:disabled,
select:disabled { select:disabled {
@@ -746,7 +757,7 @@ pre {
.shader-picker__list { .shader-picker__list {
display: grid; display: grid;
gap: 0.45rem; gap: 0.45rem;
max-height: 250px; max-height: min(52vh, 520px);
overflow-y: auto; overflow-y: auto;
padding: 0.5rem; padding: 0.5rem;
border: 1px solid var(--app-border); border: 1px solid var(--app-border);
@@ -754,22 +765,22 @@ pre {
} }
.shader-picker__option { .shader-picker__option {
width: 100%;
display: grid; display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 0.25rem; gap: 0.25rem;
align-items: center;
min-height: 4.25rem; min-height: 4.25rem;
padding: 0.65rem 0.75rem; padding: 0.65rem 0.75rem;
border: 1px solid var(--app-border);
text-align: left; text-align: left;
background: #182232; background: #182232;
border-color: var(--app-border);
align-content: start;
line-height: 1.25; line-height: 1.25;
} }
.shader-picker__option--selected { .shader-picker__option-copy {
background: #203b54; display: grid;
border-color: var(--app-primary); gap: 0.25rem;
box-shadow: inset 0 0 0 1px var(--app-primary-soft); min-width: 0;
} }
.shader-picker__option--unavailable { .shader-picker__option--unavailable {
@@ -778,16 +789,16 @@ pre {
opacity: 1; opacity: 1;
} }
.shader-picker__option:disabled {
cursor: not-allowed;
opacity: 1;
}
.shader-picker__option--unavailable .shader-picker__name, .shader-picker__option--unavailable .shader-picker__name,
.shader-picker__option--unavailable .shader-picker__meta { .shader-picker__option--unavailable .shader-picker__meta {
color: #ffd0cf; color: #ffd0cf;
} }
.shader-picker__add {
flex: 0 0 auto;
margin-left: 0.5rem;
}
.shader-picker__name, .shader-picker__name,
.shader-picker__meta { .shader-picker__meta {
min-width: 0; min-width: 0;
@@ -1040,6 +1051,11 @@ pre {
min-height: 36px; min-height: 36px;
} }
.toggle svg {
flex: 0 0 auto;
color: var(--app-muted);
}
.toggle--compact { .toggle--compact {
min-height: auto; min-height: auto;
} }