diff --git a/README.md b/README.md index c056681..effddb8 100644 --- a/README.md +++ b/README.md @@ -234,12 +234,11 @@ If your Windows runner stores the Blackmagic SDK outside the repo, configure `GP Audio improve text rendering genlock +find a better UI libary Logs anamorphic desqueeze -solid color layer refactor, cleanup of source files display URL (Maybe clicakable) for control in the windows app (Not on the output) Sound shader as seperate .slang in shader package? runtime date time UTC and offset from PCs internal clock -Add a value control to the color wheels ![alt text](image.png) \ No newline at end of file diff --git a/shaders/anamorphic-desqueeze/shader.json b/shaders/anamorphic-desqueeze/shader.json new file mode 100644 index 0000000..3a41ae1 --- /dev/null +++ b/shaders/anamorphic-desqueeze/shader.json @@ -0,0 +1,46 @@ +{ + "id": "anamorphic-desqueeze", + "name": "Anamorphic Desqueeze", + "description": "Desqueezes anamorphic footage by 1.3x, 1.33x, 1.5x, or 2x with fit or fill framing.", + "category": "Transform", + "entryPoint": "shadeVideo", + "parameters": [ + { + "id": "desqueezeFactor", + "label": "Desqueeze", + "type": "enum", + "default": "x1_33", + "options": [ + { "value": "x1_3", "label": "1.3x" }, + { "value": "x1_33", "label": "1.33x" }, + { "value": "x1_5", "label": "1.5x" }, + { "value": "x2_0", "label": "2x" } + ] + }, + { + "id": "framing", + "label": "Framing", + "type": "enum", + "default": "fit", + "options": [ + { "value": "fit", "label": "Fit" }, + { "value": "fill", "label": "Fill" } + ] + }, + { + "id": "pan", + "label": "Pan", + "type": "vec2", + "default": [0.0, 0.0], + "min": [-1.0, -1.0], + "max": [1.0, 1.0], + "step": [0.001, 0.001] + }, + { + "id": "outsideColor", + "label": "Outside Color", + "type": "color", + "default": [0.0, 0.0, 0.0, 1.0] + } + ] +} diff --git a/shaders/anamorphic-desqueeze/shader.slang b/shaders/anamorphic-desqueeze/shader.slang new file mode 100644 index 0000000..871cef0 --- /dev/null +++ b/shaders/anamorphic-desqueeze/shader.slang @@ -0,0 +1,32 @@ +float selectedDesqueezeFactor() +{ + if (desqueezeFactor == 0) + return 1.3; + if (desqueezeFactor == 1) + return 1.3333333; + if (desqueezeFactor == 2) + return 1.5; + return 2.0; +} + +float4 shadeVideo(ShaderContext context) +{ + float factor = selectedDesqueezeFactor(); + float2 centered = context.uv - 0.5; + + if (framing == 0) + { + centered.y *= factor; + } + else + { + centered.x /= factor; + } + + float2 sourceUv = centered + 0.5 - pan; + bool inside = sourceUv.x >= 0.0 && sourceUv.x <= 1.0 && sourceUv.y >= 0.0 && sourceUv.y <= 1.0; + if (!inside) + return outsideColor; + + return sampleVideo(sourceUv); +} diff --git a/shaders/smpte-color-bars/shader.json b/shaders/smpte-color-bars/shader.json new file mode 100644 index 0000000..50c44ea --- /dev/null +++ b/shaders/smpte-color-bars/shader.json @@ -0,0 +1,8 @@ +{ + "id": "smpte-color-bars", + "name": "SMPTE Color Bars", + "description": "Generates a procedural SMPTE RP 219-style 16:9 color bar test pattern matching the common Wikimedia 1920x1080 reference layout.", + "category": "Calibration", + "entryPoint": "shadeVideo", + "parameters": [] +} diff --git a/shaders/smpte-color-bars/shader.slang b/shaders/smpte-color-bars/shader.slang new file mode 100644 index 0000000..00cad5d --- /dev/null +++ b/shaders/smpte-color-bars/shader.slang @@ -0,0 +1,90 @@ +float3 hexColor(float r, float g, float b) +{ + return float3(r, g, b) / 255.0; +} + +float3 smpteTop(float x) +{ + if (x < 240.0) + return hexColor(102.0, 102.0, 102.0); + if (x < 445.0) + return hexColor(191.0, 191.0, 191.0); + if (x < 651.0) + return hexColor(191.0, 191.0, 0.0); + if (x < 857.0) + return hexColor(0.0, 191.0, 191.0); + if (x < 1063.0) + return hexColor(0.0, 191.0, 0.0); + if (x < 1269.0) + return hexColor(191.0, 0.0, 191.0); + if (x < 1475.0) + return hexColor(191.0, 0.0, 0.0); + if (x < 1680.0) + return hexColor(0.0, 0.0, 191.0); + return hexColor(102.0, 102.0, 102.0); +} + +float3 smpteMiddleA(float x) +{ + if (x < 240.0) + return hexColor(0.0, 255.0, 255.0); + if (x < 445.0) + return hexColor(0.0, 63.0, 105.0); + if (x < 1680.0) + return hexColor(191.0, 191.0, 191.0); + return hexColor(0.0, 0.0, 255.0); +} + +float3 smpteMiddleB(float x) +{ + if (x < 240.0) + return hexColor(255.0, 255.0, 0.0); + if (x < 445.0) + return hexColor(65.0, 0.0, 119.0); + if (x < 1475.0) + { + float ramp = saturate((x - 445.0) / (1475.0 - 445.0)); + return float3(ramp, ramp, ramp); + } + if (x < 1680.0) + return float3(1.0, 1.0, 1.0); + return hexColor(255.0, 0.0, 0.0); +} + +float3 smpteBottom(float x) +{ + if (x < 240.0) + return hexColor(38.0, 38.0, 38.0); + if (x < 549.0) + return float3(0.0, 0.0, 0.0); + if (x < 960.0) + return float3(1.0, 1.0, 1.0); + if (x < 1268.0) + return float3(0.0, 0.0, 0.0); + if (x < 1337.0) + return hexColor(5.0, 5.0, 5.0); + if (x < 1405.0) + return float3(0.0, 0.0, 0.0); + if (x < 1474.0) + return hexColor(10.0, 10.0, 10.0); + if (x < 1680.0) + return float3(0.0, 0.0, 0.0); + return hexColor(38.0, 38.0, 38.0); +} + +float4 shadeVideo(ShaderContext context) +{ + float2 uv = saturate(context.uv); + float2 pixel = float2(uv.x, 1.0 - uv.y) * float2(1920.0, 1080.0); + + if (pixel.y < 630.0) + return float4(smpteTop(pixel.x), 1.0); + + if (pixel.y < 720.0) + return float4(smpteMiddleA(pixel.x), 1.0); + + if (pixel.y < 810.0) + return float4(smpteMiddleB(pixel.x), 1.0); + + return float4(smpteBottom(pixel.x), 1.0); +} diff --git a/shaders/solid-color/shader.json b/shaders/solid-color/shader.json new file mode 100644 index 0000000..e752d6f --- /dev/null +++ b/shaders/solid-color/shader.json @@ -0,0 +1,15 @@ +{ + "id": "solid-color", + "name": "Solid Color", + "description": "Fills the frame with a single user-selected color.", + "category": "Color", + "entryPoint": "shadeVideo", + "parameters": [ + { + "id": "fillColor", + "label": "Fill", + "type": "color", + "default": [1.0, 1.0, 1.0, 1.0] + } + ] +} diff --git a/shaders/solid-color/shader.slang b/shaders/solid-color/shader.slang new file mode 100644 index 0000000..744a995 --- /dev/null +++ b/shaders/solid-color/shader.slang @@ -0,0 +1,4 @@ +float4 shadeVideo(ShaderContext context) +{ + return saturate(fillColor); +} diff --git a/ui/src/components/ParameterField.jsx b/ui/src/components/ParameterField.jsx index 348941a..b8d51a3 100644 --- a/ui/src/components/ParameterField.jsx +++ b/ui/src/components/ParameterField.jsx @@ -190,43 +190,70 @@ export function ParameterField({ layer, parameter, onParameterChange }) { while (values.length < 4) { values.push(values.length === 3 ? 1 : 0); } + const hsva = colorValueToHsva(values); + const wheelHsva = { ...hsva, v: 100 }; + const sendHsva = (nextHsva) => scheduleSendValue(hsvaToColorValue(nextHsva, values[3])); return (
{header}
-
- scheduleSendValue(hsvaToColorValue(color.hsva, values[3]))} - /> -
-
diff --git a/ui/src/styles.css b/ui/src/styles.css index d0b1f83..a7fba78 100644 --- a/ui/src/styles.css +++ b/ui/src/styles.css @@ -858,14 +858,23 @@ pre { .parameter__wheel-row { display: grid; - grid-template-columns: auto minmax(5.25rem, 1fr); - gap: 0.5rem; + grid-template-columns: minmax(0, 196px); + gap: 0.625rem; align-items: start; + justify-content: center; +} + +.parameter__color-stack { + display: grid; + gap: 0.625rem; + width: 196px; } .parameter__wheel { - width: 132px; - height: 132px; + width: 196px; + height: 196px; + overflow: hidden; + border-radius: 50%; } .parameter__wheel [class*="react-colorful"], @@ -873,14 +882,87 @@ pre { max-width: 100%; } +.parameter__wheel svg, +.parameter__wheel canvas { + display: block; +} + +.parameter__color-bottom { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(4.5rem, 0.85fr); + gap: 0.625rem; + align-items: end; + width: 196px; +} + .parameter__swatch { - grid-column: 2; width: 100%; - min-height: 28px; + min-height: 38px; border: 1px solid var(--app-border); border-radius: var(--app-radius-sm); } +.parameter__value-slider { + display: block; + width: 196px; +} + +.parameter__value-slider input[type="range"] { + --value-thumb-size: 18px; + display: block; + width: 196px; + height: 22px; + margin: 0; + accent-color: #f2f6fb; + background: linear-gradient(90deg, #000 0%, #fff 100%); + border-radius: 999px; + cursor: pointer; + appearance: none; + -webkit-appearance: none; +} + +.parameter__value-slider input[type="range"]::-webkit-slider-runnable-track { + height: 0.75rem; + border: 1px solid rgba(255, 255, 255, 0.28); + border-radius: 999px; + background: linear-gradient(90deg, #000 0%, #fff 100%); +} + +.parameter__value-slider input[type="range"]::-webkit-slider-thumb { + width: var(--value-thumb-size); + height: var(--value-thumb-size); + margin-top: calc((0.75rem - var(--value-thumb-size)) / 2); + border: 2px solid #f7fbff; + border-radius: 999px; + background: #f7fbff; + box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.65), 0 1px 4px rgba(0, 0, 0, 0.45); + -webkit-appearance: none; +} + +.parameter__value-slider input[type="range"]::-moz-range-track { + height: 0.75rem; + border: 1px solid rgba(255, 255, 255, 0.28); + border-radius: 999px; + background: linear-gradient(90deg, #000 0%, #fff 100%); +} + +.parameter__value-slider input[type="range"]::-moz-range-thumb { + width: var(--value-thumb-size); + height: var(--value-thumb-size); + border: 2px solid #f7fbff; + border-radius: 999px; + background: #f7fbff; + box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.65), 0 1px 4px rgba(0, 0, 0, 0.45); +} + +.parameter__value-slider strong { + text-align: right; + min-height: 1rem; + color: var(--app-text); + font-size: 0.74rem; + line-height: 1; +} + .parameter__alpha { display: grid; gap: 0.25rem; @@ -975,6 +1057,13 @@ pre { grid-column: auto; } + .parameter__color-stack, + .parameter__color-bottom, + .parameter__value-slider, + .parameter__value-slider input[type="range"] { + width: 100%; + } + .kv-row { grid-template-columns: minmax(6.25rem, 0.6fr) minmax(0, 1fr); }