1use serde_json::{Value, json};
2
3use crate::{
4 EffectDef, EffectKind, EffectResourceKind, ParamSpec, ParamType, ParamUnit, ParamValue,
5 builtin_effect_definitions, builtin_transition_definitions,
6};
7
8#[must_use]
13pub fn builtin_catalog() -> Value {
14 let effects = builtin_effect_definitions()
15 .iter()
16 .map(catalog_entry)
17 .collect::<Vec<_>>();
18 let transitions = builtin_transition_definitions()
19 .iter()
20 .map(catalog_entry)
21 .collect::<Vec<_>>();
22 json!({ "effects": effects, "transitions": transitions })
23}
24
25#[must_use]
27pub fn builtin_catalog_json() -> String {
28 builtin_catalog().to_string()
29}
30
31fn catalog_entry(definition: &EffectDef) -> Value {
32 let (label, group) = presentation(&definition.id, definition.kind);
33 json!({
34 "id": definition.id,
35 "version": definition.version,
36 "kind": match definition.kind {
37 EffectKind::Effect => "effect",
38 EffectKind::Transition => "transition",
39 EffectKind::Generator => "generator",
40 },
41 "inputs": definition
42 .inputs
43 .iter()
44 .map(|input| json!({ "name": input.name }))
45 .collect::<Vec<_>>(),
46 "label": label,
47 "group": group,
48 "params": definition.params.iter().map(param).collect::<Vec<_>>(),
49 "resources": definition.resources.iter().map(|resource| json!({
50 "name": resource.name,
51 "kind": match resource.kind { EffectResourceKind::Lut3d => "lut3d" },
52 })).collect::<Vec<_>>(),
53 })
54}
55
56fn param(spec: &ParamSpec) -> Value {
57 let value = match &spec.default {
58 ParamValue::Float(value) => json!(value),
59 ParamValue::Vec2(value) => json!(value),
60 ParamValue::Vec4(value) | ParamValue::Color(value) => json!(value),
61 ParamValue::Enum(value) => json!(value),
62 ParamValue::Bool(value) => json!(value),
63 };
64 json!({
65 "name": spec.name,
66 "label": humanize(&spec.name),
67 "type": match spec.param_type {
68 ParamType::Float => "float",
69 ParamType::Vec2 => "vec2",
70 ParamType::Vec4 => "vec4",
71 ParamType::Color => "color",
72 ParamType::Enum => "enum",
73 ParamType::Bool => "bool",
74 },
75 "default": value,
76 "min": spec.min,
77 "max": spec.max,
78 "step": spec.step,
79 "unit": match spec.unit {
80 ParamUnit::Normalized => "normalized",
81 ParamUnit::Degrees => "degrees",
82 ParamUnit::Color => "color",
83 ParamUnit::Enum => "enum",
84 ParamUnit::Bool => "bool",
85 },
86 "enumValues": spec.enum_values,
87 "animatable": matches!(
90 spec.param_type,
91 ParamType::Float | ParamType::Vec2 | ParamType::Vec4 | ParamType::Color
92 ),
93 })
94}
95
96fn presentation(id: &str, kind: EffectKind) -> (String, &'static str) {
97 let short = id.rsplit('.').next().unwrap_or(id);
98 let label = if short == "lut-3d" {
99 "3D LUT".into()
100 } else {
101 humanize(short)
102 };
103 let group = match kind {
104 EffectKind::Effect if matches!(short, "gaussian-blur" | "sharpen") => "Blur & Sharpen",
105 EffectKind::Effect if short == "chroma-key" => "Keying",
106 EffectKind::Effect if matches!(short, "color-controls" | "lut-3d") => "Color",
107 EffectKind::Effect if matches!(short, "crop" | "mirror" | "pixelate") => "Distort",
108 EffectKind::Effect => "Stylize",
109 EffectKind::Transition
110 if matches!(
111 short,
112 "wipe-up"
113 | "wipe-down"
114 | "wipe-left"
115 | "wipe-right"
116 | "window-blinds"
117 | "window-slice"
118 | "wind"
119 | "pinwheel"
120 | "heart"
121 ) =>
122 {
123 "Wipes"
124 }
125 EffectKind::Transition
126 if matches!(
127 short,
128 "fade"
129 | "directional"
130 | "circle-open"
131 | "angular"
132 | "pixelize"
133 | "directional-wipe"
134 | "circle"
135 | "cross-warp"
136 | "dreamy"
137 | "radial"
138 ) =>
139 {
140 "Classic"
141 }
142 EffectKind::Transition => "Stylized",
143 EffectKind::Generator => "Generate",
144 };
145 (label, group)
146}
147
148fn humanize(value: &str) -> String {
149 value
150 .split(['-', '_'])
151 .filter(|part| !part.is_empty())
152 .map(|part| {
153 let mut chars = part.chars();
154 chars.next().map_or_else(String::new, |first| {
155 first.to_uppercase().collect::<String>() + chars.as_str()
156 })
157 })
158 .collect::<Vec<_>>()
159 .join(" ")
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165
166 #[test]
167 fn catalog_exposes_lut_resource_without_shader_source() {
168 let catalog = builtin_catalog();
169 let lut = catalog["effects"]
170 .as_array()
171 .unwrap()
172 .iter()
173 .find(|entry| entry["id"] == "alpha2.effect.lut-3d")
174 .unwrap();
175 assert_eq!(lut["resources"][0]["kind"], "lut3d");
176 assert_eq!(lut["label"], "3D LUT");
177 assert!(lut.get("wgslBody").is_none());
178
179 let crop = catalog["effects"]
180 .as_array()
181 .unwrap()
182 .iter()
183 .find(|entry| entry["id"] == "alpha2.effect.crop")
184 .unwrap();
185 assert_eq!(crop["params"][0]["min"], 0.0);
186 assert_eq!(crop["params"][0]["max"], 1.0);
187 assert!((crop["params"][0]["step"].as_f64().unwrap() - 0.005).abs() < 1e-8);
188 }
189
190 #[test]
191 fn every_built_in_lists_its_named_inputs_and_none_has_any() {
192 let catalog = builtin_catalog();
193 for entry in catalog["effects"]
194 .as_array()
195 .unwrap()
196 .iter()
197 .chain(catalog["transitions"].as_array().unwrap())
198 {
199 assert_eq!(entry["inputs"], json!([]), "{}", entry["id"]);
200 }
201 }
202
203 #[test]
204 fn continuous_parameters_are_animatable_and_discrete_ones_hold() {
205 let catalog = builtin_catalog();
206 let mut seen = std::collections::BTreeSet::new();
207 for entry in catalog["effects"]
208 .as_array()
209 .unwrap()
210 .iter()
211 .chain(catalog["transitions"].as_array().unwrap())
212 {
213 for param in entry["params"].as_array().unwrap() {
214 let kind = param["type"].as_str().unwrap();
215 let animatable = param["animatable"].as_bool().unwrap();
216 assert_eq!(
217 animatable,
218 matches!(kind, "float" | "vec2" | "vec4" | "color"),
219 "{}.{}",
220 entry["id"],
221 param["name"]
222 );
223 seen.insert(kind.to_owned());
224 }
225 }
226 assert!(seen.contains("float") && seen.contains("enum"), "{seen:?}");
227 }
228
229 #[test]
230 fn a_color_parameter_is_a_color_and_thresholds_are_a_vec4() {
231 let catalog = builtin_catalog();
232 for entry in catalog["effects"]
233 .as_array()
234 .unwrap()
235 .iter()
236 .chain(catalog["transitions"].as_array().unwrap())
237 {
238 for param in entry["params"].as_array().unwrap() {
239 if param["type"] == "color" {
240 assert_eq!(param["unit"], "color", "{}.{}", entry["id"], param["name"]);
241 }
242 }
243 }
244 let phase = catalog["transitions"]
247 .as_array()
248 .unwrap()
249 .iter()
250 .find(|entry| entry["id"] == "alpha2.transition.color-phase")
251 .unwrap();
252 for param in phase["params"].as_array().unwrap() {
253 assert_eq!(param["type"], "vec4", "{}", param["name"]);
254 assert_eq!(
255 (param["min"].as_f64(), param["max"].as_f64()),
256 (Some(0.0), Some(1.0))
257 );
258 }
259 }
260}