Additons
This commit is contained in:
@@ -94,6 +94,15 @@
|
|||||||
"min": 0.0,
|
"min": 0.0,
|
||||||
"max": 0.2,
|
"max": 0.2,
|
||||||
"step": 0.005
|
"step": 0.005
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "noiseSize",
|
||||||
|
"label": "Noise Size",
|
||||||
|
"type": "float",
|
||||||
|
"default": 1.0,
|
||||||
|
"min": 0.25,
|
||||||
|
"max": 6.0,
|
||||||
|
"step": 0.05
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,13 +44,23 @@ float noiseHash(float2 p)
|
|||||||
return frac(sin(dot(p, float2(127.1, 311.7))) * 43758.5453123);
|
return frac(sin(dot(p, float2(127.1, 311.7))) * 43758.5453123);
|
||||||
}
|
}
|
||||||
|
|
||||||
float3 chromaSpeckle(float2 uv, float framecount)
|
float grainScalar(float2 uv)
|
||||||
{
|
{
|
||||||
float2 coarseUv = floor(uv);
|
return frac(sin(dot(uv, float2(12.9898, 78.233))) * 43758.5453);
|
||||||
float r = noiseHash(coarseUv + float2(framecount * 19.0, framecount * 11.0));
|
}
|
||||||
float g = noiseHash(coarseUv + float2(framecount * 23.0 + 17.0, framecount * 7.0 + 31.0));
|
|
||||||
float b = noiseHash(coarseUv + float2(framecount * 13.0 + 47.0, framecount * 29.0 + 9.0));
|
float3 animatedChromaGrain(float2 uv, float time, float2 outputResolution, float grainSize)
|
||||||
return float3(r, g, b) - 0.5;
|
{
|
||||||
|
float safeGrainSize = max(grainSize, 0.001);
|
||||||
|
float2 baseUv = uv * outputResolution * float2(0.85, 0.95) / safeGrainSize;
|
||||||
|
float2 grainUv = floor(baseUv) + 0.5;
|
||||||
|
float2 drift = float2(time * 19.7, time * 23.3);
|
||||||
|
|
||||||
|
float r = grainScalar(grainUv + drift + float2(13.1, 71.7));
|
||||||
|
float g = grainScalar(grainUv * float2(1.03, 0.97) + drift * 1.11 + float2(47.2, 19.4));
|
||||||
|
float b = grainScalar(grainUv * float2(0.96, 1.05) + drift * 0.91 + float2(83.6, 53.8));
|
||||||
|
|
||||||
|
return float3(r, g, b) * 2.0 - 1.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
float3 softBloom(float2 uv, float2 outputResolution, float radius)
|
float3 softBloom(float2 uv, float2 outputResolution, float radius)
|
||||||
@@ -145,14 +155,14 @@ float4 shadeVideo(ShaderContext context)
|
|||||||
color = lerp(color, bloomSource, bloomAmount * 0.18);
|
color = lerp(color, bloomSource, bloomAmount * 0.18);
|
||||||
color += bloomSource * float3(1.0, 0.96, 0.92) * bloomMask * 0.24;
|
color += bloomSource * float3(1.0, 0.96, 0.92) * bloomMask * 0.24;
|
||||||
|
|
||||||
float2 noiseUv = context.uv * context.outputResolution * float2(0.55, 1.1);
|
float3 speckle = animatedChromaGrain(context.uv, context.time, context.outputResolution, noiseSize);
|
||||||
float3 speckle = chromaSpeckle(noiseUv, framecount);
|
|
||||||
float luma = dot(color, float3(0.299, 0.587, 0.114));
|
float luma = dot(color, float3(0.299, 0.587, 0.114));
|
||||||
float noiseMask = lerp(0.65, 1.0, 1.0 - saturate(luma));
|
float noiseMask = lerp(0.65, 1.0, 1.0 - saturate(luma));
|
||||||
float3 chromaNoise = float3(speckle.x * 1.1, speckle.y * 0.45, speckle.z * 1.2);
|
float chunkiness = lerp(1.0, 2.4, saturate((noiseSize - 1.0) / 5.0));
|
||||||
color += chromaNoise * noiseAmount * noiseMask;
|
float3 chromaNoise = float3(speckle.x * 1.2, speckle.y * 0.28, speckle.z * 1.35);
|
||||||
color.rg = lerp(color.rg, float2(color.r, color.g) + speckle.xy * noiseAmount * 0.18, 0.35);
|
color += chromaNoise * noiseAmount * noiseMask * chunkiness;
|
||||||
color.b = lerp(color.b, color.b + speckle.z * noiseAmount * 0.22, 0.45);
|
color.rg = lerp(color.rg, float2(color.r, color.g) + speckle.xy * noiseAmount * 0.2 * chunkiness, 0.35);
|
||||||
|
color.b = lerp(color.b, color.b + speckle.z * noiseAmount * 0.28 * chunkiness, 0.5);
|
||||||
|
|
||||||
float3 grayscale = float3(luma, luma, luma);
|
float3 grayscale = float3(luma, luma, luma);
|
||||||
color = lerp(color, grayscale, fadeAmount * 0.18);
|
color = lerp(color, grayscale, fadeAmount * 0.18);
|
||||||
|
|||||||
130
ui/src/App.jsx
130
ui/src/App.jsx
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { GripVertical, Trash2 } from "lucide-react";
|
import { GripVertical, Trash2 } from "lucide-react";
|
||||||
|
|
||||||
function KvList({ values }) {
|
function KvList({ values }) {
|
||||||
@@ -62,19 +62,105 @@ function valuesMatch(left, right) {
|
|||||||
|
|
||||||
function ParameterField({ parameter, onParameterChange }) {
|
function ParameterField({ parameter, onParameterChange }) {
|
||||||
const [draftValue, setDraftValue] = useState(parameter.value);
|
const [draftValue, setDraftValue] = useState(parameter.value);
|
||||||
|
const [appliedValue, setAppliedValue] = useState(parameter.value);
|
||||||
|
const pendingTimeoutRef = useRef(null);
|
||||||
|
const latestDraftRef = useRef(parameter.value);
|
||||||
|
const lastSentAtRef = useRef(0);
|
||||||
|
const isInteractingRef = useRef(false);
|
||||||
|
const isDirtyRef = useRef(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setDraftValue(parameter.value);
|
setDraftValue(parameter.value);
|
||||||
}, [parameter.value]);
|
setAppliedValue(parameter.value);
|
||||||
|
latestDraftRef.current = parameter.value;
|
||||||
|
lastSentAtRef.current = 0;
|
||||||
|
isInteractingRef.current = false;
|
||||||
|
isDirtyRef.current = false;
|
||||||
|
}, [parameter.id]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setAppliedValue(parameter.value);
|
||||||
|
latestDraftRef.current = draftValue;
|
||||||
|
if (isDirtyRef.current && valuesMatch(parameter.value, latestDraftRef.current)) {
|
||||||
|
isDirtyRef.current = false;
|
||||||
|
}
|
||||||
|
if (!isInteractingRef.current && !isDirtyRef.current) {
|
||||||
|
setDraftValue(parameter.value);
|
||||||
|
}
|
||||||
|
}, [draftValue, parameter.value]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (pendingTimeoutRef.current) {
|
||||||
|
clearTimeout(pendingTimeoutRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
const sendValue = (value) => {
|
const sendValue = (value) => {
|
||||||
setDraftValue(value);
|
setDraftValue(value);
|
||||||
onParameterChange(parameter.id, value);
|
latestDraftRef.current = value;
|
||||||
|
isDirtyRef.current = true;
|
||||||
|
lastSentAtRef.current = Date.now();
|
||||||
|
const request = onParameterChange(parameter.id, value);
|
||||||
|
if (request && typeof request.then === "function") {
|
||||||
|
request
|
||||||
|
.then((response) => {
|
||||||
|
if (response?.ok) {
|
||||||
|
setAppliedValue(value);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// Keep showing the last confirmed value if the update failed.
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const scheduleSendValue = (value, immediate = false) => {
|
||||||
|
setDraftValue(value);
|
||||||
|
latestDraftRef.current = value;
|
||||||
|
isDirtyRef.current = true;
|
||||||
|
|
||||||
|
if (pendingTimeoutRef.current) {
|
||||||
|
clearTimeout(pendingTimeoutRef.current);
|
||||||
|
pendingTimeoutRef.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
const throttleMs = 90;
|
||||||
|
const elapsed = now - lastSentAtRef.current;
|
||||||
|
|
||||||
|
if (immediate || elapsed >= throttleMs) {
|
||||||
|
sendValue(value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingTimeoutRef.current = setTimeout(() => {
|
||||||
|
pendingTimeoutRef.current = null;
|
||||||
|
sendValue(latestDraftRef.current);
|
||||||
|
}, throttleMs - elapsed);
|
||||||
|
};
|
||||||
|
|
||||||
|
const flushScheduledValue = () => {
|
||||||
|
if (pendingTimeoutRef.current) {
|
||||||
|
clearTimeout(pendingTimeoutRef.current);
|
||||||
|
pendingTimeoutRef.current = null;
|
||||||
|
}
|
||||||
|
sendValue(latestDraftRef.current);
|
||||||
|
};
|
||||||
|
|
||||||
|
const beginInteraction = () => {
|
||||||
|
isInteractingRef.current = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const endInteraction = () => {
|
||||||
|
isInteractingRef.current = false;
|
||||||
|
flushScheduledValue();
|
||||||
};
|
};
|
||||||
|
|
||||||
const label = <label>{parameter.label}</label>;
|
const label = <label>{parameter.label}</label>;
|
||||||
const isPending = !valuesMatch(draftValue, parameter.value);
|
const isPending = !valuesMatch(draftValue, appliedValue);
|
||||||
const appliedValueText = formatParameterValue(parameter.type, parameter.value);
|
const appliedValueText = formatParameterValue(parameter.type, appliedValue);
|
||||||
|
|
||||||
if (parameter.type === "float") {
|
if (parameter.type === "float") {
|
||||||
return (
|
return (
|
||||||
@@ -87,7 +173,16 @@ function ParameterField({ parameter, onParameterChange }) {
|
|||||||
max={parameter.max?.[0] ?? 1}
|
max={parameter.max?.[0] ?? 1}
|
||||||
step={parameter.step?.[0] ?? 0.01}
|
step={parameter.step?.[0] ?? 0.01}
|
||||||
value={draftValue}
|
value={draftValue}
|
||||||
onChange={(event) => sendValue(Number(event.target.value))}
|
onMouseDown={beginInteraction}
|
||||||
|
onPointerDown={beginInteraction}
|
||||||
|
onTouchStart={beginInteraction}
|
||||||
|
onChange={(event) => scheduleSendValue(Number(event.target.value))}
|
||||||
|
onMouseUp={endInteraction}
|
||||||
|
onTouchEnd={endInteraction}
|
||||||
|
onPointerUp={endInteraction}
|
||||||
|
onKeyDown={beginInteraction}
|
||||||
|
onKeyUp={endInteraction}
|
||||||
|
onBlur={endInteraction}
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
@@ -95,7 +190,9 @@ function ParameterField({ parameter, onParameterChange }) {
|
|||||||
max={parameter.max?.[0] ?? ""}
|
max={parameter.max?.[0] ?? ""}
|
||||||
step={parameter.step?.[0] ?? 0.01}
|
step={parameter.step?.[0] ?? 0.01}
|
||||||
value={draftValue}
|
value={draftValue}
|
||||||
|
onFocus={beginInteraction}
|
||||||
onChange={(event) => sendValue(Number(event.target.value))}
|
onChange={(event) => sendValue(Number(event.target.value))}
|
||||||
|
onBlur={endInteraction}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={`parameter__value${isPending ? " parameter__value--pending" : ""}`}>
|
<div className={`parameter__value${isPending ? " parameter__value--pending" : ""}`}>
|
||||||
@@ -124,11 +221,13 @@ function ParameterField({ parameter, onParameterChange }) {
|
|||||||
max={parameter.max?.[index] ?? ""}
|
max={parameter.max?.[index] ?? ""}
|
||||||
step={parameter.step?.[index] ?? 0.01}
|
step={parameter.step?.[index] ?? 0.01}
|
||||||
value={values[index]}
|
value={values[index]}
|
||||||
|
onFocus={beginInteraction}
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
const next = [...values];
|
const next = [...values];
|
||||||
next[index] = Number(event.target.value);
|
next[index] = Number(event.target.value);
|
||||||
sendValue(next);
|
sendValue(next);
|
||||||
}}
|
}}
|
||||||
|
onBlur={endInteraction}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -147,7 +246,9 @@ function ParameterField({ parameter, onParameterChange }) {
|
|||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={Boolean(draftValue)}
|
checked={Boolean(draftValue)}
|
||||||
|
onFocus={beginInteraction}
|
||||||
onChange={(event) => sendValue(event.target.checked)}
|
onChange={(event) => sendValue(event.target.checked)}
|
||||||
|
onBlur={endInteraction}
|
||||||
/>
|
/>
|
||||||
<span>{draftValue ? "Enabled" : "Disabled"}</span>
|
<span>{draftValue ? "Enabled" : "Disabled"}</span>
|
||||||
</label>
|
</label>
|
||||||
@@ -162,7 +263,12 @@ function ParameterField({ parameter, onParameterChange }) {
|
|||||||
return (
|
return (
|
||||||
<section className="parameter">
|
<section className="parameter">
|
||||||
{label}
|
{label}
|
||||||
<select value={draftValue} onChange={(event) => sendValue(event.target.value)}>
|
<select
|
||||||
|
value={draftValue}
|
||||||
|
onFocus={beginInteraction}
|
||||||
|
onChange={(event) => sendValue(event.target.value)}
|
||||||
|
onBlur={endInteraction}
|
||||||
|
>
|
||||||
{parameter.options.map((option) => (
|
{parameter.options.map((option) => (
|
||||||
<option key={option.value} value={option.value}>
|
<option key={option.value} value={option.value}>
|
||||||
{option.label}
|
{option.label}
|
||||||
@@ -192,6 +298,7 @@ function LayerCard({
|
|||||||
onDragOver,
|
onDragOver,
|
||||||
onDrop,
|
onDrop,
|
||||||
onRemove,
|
onRemove,
|
||||||
|
onLayerParameterChange,
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -299,11 +406,9 @@ function LayerCard({
|
|||||||
<div className="parameter-grid">
|
<div className="parameter-grid">
|
||||||
{layer.parameters.map((parameter) => (
|
{layer.parameters.map((parameter) => (
|
||||||
<ParameterField
|
<ParameterField
|
||||||
key={`${layer.id}:${parameter.id}:${JSON.stringify(parameter.value)}`}
|
key={`${layer.id}:${parameter.id}`}
|
||||||
parameter={parameter}
|
parameter={parameter}
|
||||||
onParameterChange={(parameterId, value) =>
|
onParameterChange={(parameterId, value) => onLayerParameterChange(layer.id, parameterId, value)}
|
||||||
updateLayerParameterOptimistically(layer.id, parameterId, value)
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -405,7 +510,7 @@ function App() {
|
|||||||
const expandedSet = useMemo(() => new Set(expandedLayerIds), [expandedLayerIds]);
|
const expandedSet = useMemo(() => new Set(expandedLayerIds), [expandedLayerIds]);
|
||||||
|
|
||||||
function updateLayerParameterOptimistically(layerId, parameterId, value) {
|
function updateLayerParameterOptimistically(layerId, parameterId, value) {
|
||||||
postJson("/api/layers/update-parameter", {
|
return postJson("/api/layers/update-parameter", {
|
||||||
layerId,
|
layerId,
|
||||||
parameterId,
|
parameterId,
|
||||||
value,
|
value,
|
||||||
@@ -590,6 +695,7 @@ function App() {
|
|||||||
onDragOver={setDropTargetLayerId}
|
onDragOver={setDropTargetLayerId}
|
||||||
onDrop={handleDrop}
|
onDrop={handleDrop}
|
||||||
onRemove={removeLayer}
|
onRemove={removeLayer}
|
||||||
|
onLayerParameterChange={updateLayerParameterOptimistically}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user