1use std::fmt::Write as _;
8
9use alpha2_gpu::CapabilityTier;
10
11use crate::{EffectDef, EffectKind, InputSpec, ParamSpec, ParamType, ParamUnit, ParamValue};
12
13pub const TRACK_MATTE_EFFECT_ID: &str = "alpha2.internal.track-matte";
15
16pub const MAX_SHAPE_MASKS: usize = 8;
18
19const _: () = assert!(MAX_SHAPE_MASKS <= crate::MAX_EFFECT_INPUTS);
20
21pub const MASK_MODES: [&str; 3] = ["add", "subtract", "intersect"];
23
24pub const MASK_COMBINE_EFFECT_IDS: [&str; MAX_SHAPE_MASKS] = [
26 "alpha2.internal.mask-combine-1",
27 "alpha2.internal.mask-combine-2",
28 "alpha2.internal.mask-combine-3",
29 "alpha2.internal.mask-combine-4",
30 "alpha2.internal.mask-combine-5",
31 "alpha2.internal.mask-combine-6",
32 "alpha2.internal.mask-combine-7",
33 "alpha2.internal.mask-combine-8",
34];
35
36#[derive(Copy, Clone, Debug, Eq, PartialEq)]
38pub struct MaskCombineNames {
39 pub input: &'static str,
41 pub invert: &'static str,
43 pub mode: &'static str,
45}
46
47macro_rules! mask_combine_names {
48 ($($index:literal),*) => {
49 [$(MaskCombineNames {
50 input: concat!("m", $index),
51 invert: concat!("invert", $index),
52 mode: concat!("mode", $index),
53 }),*]
54 };
55}
56
57pub const MASK_COMBINE_NAMES: [MaskCombineNames; MAX_SHAPE_MASKS] =
59 mask_combine_names!(0, 1, 2, 3, 4, 5, 6, 7);
60
61fn param(name: String, param_type: ParamType, default: ParamValue) -> ParamSpec {
62 ParamSpec {
63 name,
64 param_type,
65 default,
66 min: None,
67 max: None,
68 step: None,
69 unit: match param_type {
70 ParamType::Bool => ParamUnit::Bool,
71 ParamType::Enum => ParamUnit::Enum,
72 _ => ParamUnit::Normalized,
73 },
74 enum_values: if param_type == ParamType::Enum {
75 MASK_MODES.map(str::to_owned).to_vec()
76 } else {
77 Vec::new()
78 },
79 }
80}
81
82#[must_use]
91pub fn mask_combine_definition(count: usize) -> EffectDef {
92 assert!(
93 (1..=MAX_SHAPE_MASKS).contains(&count),
94 "a mask-combine pass takes 1 to {MAX_SHAPE_MASKS} masks"
95 );
96 let names = &MASK_COMBINE_NAMES[..count];
97 let mut params = Vec::with_capacity(count * 2);
98 let mut steps = String::new();
99 for names in names {
100 params.push(param(
101 names.invert.into(),
102 ParamType::Bool,
103 ParamValue::Bool(false),
104 ));
105 params.push(param(
106 names.mode.into(),
107 ParamType::Enum,
108 ParamValue::Enum(0),
109 ));
110 writeln!(
111 steps,
112 " coverage = builtin_mask_step(coverage, sample_{input}(uv).a, params.{invert}, params.{mode});",
113 input = names.input,
114 invert = names.invert,
115 mode = names.mode,
116 )
117 .expect("writing to a String cannot fail");
118 }
119 EffectDef {
120 id: MASK_COMBINE_EFFECT_IDS[count - 1].into(),
121 version: 1,
122 kind: EffectKind::Effect,
123 inputs: names
124 .iter()
125 .map(|names| InputSpec {
126 name: names.input.into(),
127 })
128 .collect(),
129 params,
130 resources: Vec::new(),
131 wgsl_body: format!(
132 r"fn builtin_mask_step(coverage: f32, mask: f32, invert: u32, mode: u32) -> f32 {{
133 var m = clamp(mask, 0.0, 1.0);
134 if (invert != 0u) {{ m = 1.0 - m; }}
135 if (mode == 1u) {{ return coverage * (1.0 - m); }}
136 if (mode == 2u) {{ return coverage * m; }}
137 return coverage + m - coverage * m;
138}}
139
140fn effect(uv: vec2f, src: vec4f) -> vec4f {{
141 // Empty when the first mask adds, full otherwise.
142 var coverage = select(1.0, 0.0, params.mode0 == 0u);
143{steps} return src * clamp(coverage, 0.0, 1.0);
144}}
145"
146 ),
147 tier_min: CapabilityTier::TierB,
148 license: Some(crate::builtins::ALPHA2_EFFECT_LICENSE.into()),
149 }
150}
151
152#[must_use]
154pub fn mask_combine_definitions() -> Vec<EffectDef> {
155 (1..=MAX_SHAPE_MASKS).map(mask_combine_definition).collect()
156}
157
158#[must_use]
163pub fn track_matte_definition() -> EffectDef {
164 EffectDef {
165 id: TRACK_MATTE_EFFECT_ID.into(),
166 version: 1,
167 kind: EffectKind::Transition,
168 inputs: Vec::new(),
169 params: vec![
170 ParamSpec {
171 unit: ParamUnit::Bool,
172 ..param("luma".into(), ParamType::Bool, ParamValue::Bool(false))
173 },
174 ParamSpec {
175 unit: ParamUnit::Bool,
176 ..param("inverted".into(), ParamType::Bool, ParamValue::Bool(false))
177 },
178 ],
179 resources: Vec::new(),
180 wgsl_body: r"fn builtin_matte_linear(value: f32) -> f32 {
181 if (value <= 0.04045) { return value / 12.92; }
182 return pow((value + 0.055) / 1.055, 2.4);
183}
184
185fn transition(uv: vec2f) -> vec4f {
186 let picture = from_color(uv);
187 let matte = to_color(uv);
188 var coverage = matte.a;
189 if (params.luma != 0u) {
190 let straight = clamp(matte.rgb / max(matte.a, 0.000001), vec3f(0.0), vec3f(1.0));
191 let linear = vec3f(
192 builtin_matte_linear(straight.r),
193 builtin_matte_linear(straight.g),
194 builtin_matte_linear(straight.b),
195 );
196 coverage = dot(linear, vec3f(0.2126, 0.7152, 0.0722)) * matte.a;
197 }
198 if (params.inverted != 0u) { coverage = 1.0 - coverage; }
199 return picture * clamp(coverage, 0.0, 1.0);
200}
201"
202 .into(),
203 tier_min: CapabilityTier::TierB,
204 license: Some(crate::builtins::ALPHA2_EFFECT_LICENSE.into()),
205 }
206}
207
208#[cfg(test)]
209mod tests {
210 use super::{
211 MASK_COMBINE_EFFECT_IDS, MASK_COMBINE_NAMES, MAX_SHAPE_MASKS, mask_combine_definition,
212 };
213
214 #[test]
215 fn each_masks_static_names_are_the_ones_the_pass_declares() {
216 for count in 1..=MAX_SHAPE_MASKS {
217 let definition = mask_combine_definition(count);
218 assert_eq!(definition.id, MASK_COMBINE_EFFECT_IDS[count - 1]);
219 let params = definition
220 .params
221 .iter()
222 .map(|spec| spec.name.clone())
223 .collect::<Vec<_>>();
224 let inputs = definition
225 .inputs
226 .iter()
227 .map(|input| input.name.clone())
228 .collect::<Vec<_>>();
229 let mut expected_params = Vec::new();
230 let mut expected_inputs = Vec::new();
231 for (index, names) in MASK_COMBINE_NAMES[..count].iter().enumerate() {
232 assert_eq!(names.input, format!("m{index}"));
233 assert_eq!(names.invert, format!("invert{index}"));
234 assert_eq!(names.mode, format!("mode{index}"));
235 expected_inputs.push(names.input.to_owned());
236 expected_params.push(names.invert.to_owned());
237 expected_params.push(names.mode.to_owned());
238 }
239 assert_eq!(inputs, expected_inputs);
240 assert_eq!(params, expected_params);
241 }
242 }
243}