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