1use std::fmt::Write as _;
7
8use alpha2_gpu::CapabilityTier;
9
10use crate::{EffectDef, EffectInputs, EffectKind, ParamSpec, ParamType, ParamUnit, ParamValue};
11
12pub const SHAPE_MASK_EFFECT_ID: &str = "alpha2.internal.shape-mask";
14
15pub const TRACK_MATTE_EFFECT_ID: &str = "alpha2.internal.track-matte";
17
18pub const MAX_SHAPE_MASKS: usize = 8;
20
21pub const MASK_MODES: [&str; 3] = ["add", "subtract", "intersect"];
23
24#[derive(Copy, Clone, Debug, Eq, PartialEq)]
27pub struct ShapeMaskParams {
28 pub center: &'static str,
30 pub size: &'static str,
32 pub rotation: &'static str,
34 pub radius: &'static str,
36 pub feather: &'static str,
38 pub ellipse: &'static str,
40 pub invert: &'static str,
42 pub mode: &'static str,
44}
45
46macro_rules! shape_mask_params {
47 ($($index:literal),*) => {
48 [$(ShapeMaskParams {
49 center: concat!("m", $index, "_center"),
50 size: concat!("m", $index, "_size"),
51 rotation: concat!("m", $index, "_rotation"),
52 radius: concat!("m", $index, "_radius"),
53 feather: concat!("m", $index, "_feather"),
54 ellipse: concat!("m", $index, "_ellipse"),
55 invert: concat!("m", $index, "_invert"),
56 mode: concat!("m", $index, "_mode"),
57 }),*]
58 };
59}
60
61pub const SHAPE_MASK_PARAMS: [ShapeMaskParams; MAX_SHAPE_MASKS] =
64 shape_mask_params!(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]
95pub fn shape_mask_definition() -> EffectDef {
96 let mut params = vec![param(
97 "count".into(),
98 ParamType::Float,
99 ParamValue::Float(0.0),
100 )];
101 for names in SHAPE_MASK_PARAMS {
102 params.extend([
103 param(
104 names.center.into(),
105 ParamType::Vec2,
106 ParamValue::Vec2([0.5, 0.5]),
107 ),
108 param(
109 names.size.into(),
110 ParamType::Vec2,
111 ParamValue::Vec2([1.0, 1.0]),
112 ),
113 param(
114 names.rotation.into(),
115 ParamType::Float,
116 ParamValue::Float(0.0),
117 ),
118 param(
119 names.radius.into(),
120 ParamType::Float,
121 ParamValue::Float(0.0),
122 ),
123 param(
124 names.feather.into(),
125 ParamType::Float,
126 ParamValue::Float(0.0),
127 ),
128 param(
129 names.ellipse.into(),
130 ParamType::Bool,
131 ParamValue::Bool(false),
132 ),
133 param(
134 names.invert.into(),
135 ParamType::Bool,
136 ParamValue::Bool(false),
137 ),
138 param(names.mode.into(), ParamType::Enum, ParamValue::Enum(0)),
139 ]);
140 }
141 EffectDef {
142 id: SHAPE_MASK_EFFECT_ID.into(),
143 version: 1,
144 kind: EffectKind::Effect,
145 inputs: EffectInputs::One,
146 params,
147 resources: Vec::new(),
148 wgsl_body: shape_mask_body(),
149 tier_min: CapabilityTier::TierB,
150 license: Some(crate::builtins::ALPHA2_EFFECT_LICENSE.into()),
151 }
152}
153
154pub(crate) const SHAPE_WGSL: &str = r"// Pixel offset of `uv` from `center`, in the frame of a shape turned
158// `rotation` radians clockwise.
159fn builtin_shape_local(uv: vec2f, center: vec2f, rotation: f32) -> vec2f {
160 let offset = (uv - center) * resolution();
161 let c = cos(rotation);
162 let s = sin(rotation);
163 return vec2f(c * offset.x + s * offset.y, -s * offset.x + c * offset.y);
164}
165
166// Signed distance, in pixels, from `p` (relative to the centre, unrotated)
167// to the outline of a shape of half extent `half`: negative inside.
168fn builtin_shape_distance(p: vec2f, half: vec2f, radius: f32, ellipse: bool) -> f32 {
169 if (ellipse) {
170 // The gradient-normalized approximation, exact on the axes and
171 // within a fraction of a pixel near the outline, where it matters.
172 let h = max(half, vec2f(0.0001));
173 let k0 = length(p / h);
174 let k1 = length(p / (h * h));
175 if (k1 <= 0.0) { return -min(h.x, h.y); }
176 return k0 * (k0 - 1.0) / k1;
177 }
178 let r = min(radius, min(half.x, half.y));
179 let q = abs(p) - half + vec2f(r);
180 return length(max(q, vec2f(0.0))) + min(max(q.x, q.y), 0.0) - r;
181}
182
183// The share of its edge-ramp coverage a shape of full `extent` (pixels) keeps
184// with an antialiased edge `edge` pixels wide: all of it once each side spans
185// the edge, and in proportion to each side below that. The ramp, centred on
186// the outline, alone keeps half coverage at the centre of a shape of no size;
187// with this, a shape shrinking to nothing fades out with its area instead of
188// ending on a half-covered dot. Exactly one from `edge` up.
189fn builtin_shape_extent_weight(extent: vec2f, edge: f32) -> f32 {
190 let w = clamp(extent / max(edge, 0.0001), vec2f(0.0), vec2f(1.0));
191 return w.x * w.y;
192}
193";
194
195fn shape_mask_body() -> String {
196 let mut masks = String::new();
197 for index in 0..MAX_SHAPE_MASKS {
198 let separator = if index + 1 == MAX_SHAPE_MASKS {
199 ""
200 } else {
201 ","
202 };
203 writeln!(
204 masks,
205 " AlphaMask(params.m{index}_center, params.m{index}_size, \
206 params.m{index}_rotation, params.m{index}_radius, params.m{index}_feather, \
207 params.m{index}_ellipse, params.m{index}_invert, params.m{index}_mode){separator}"
208 )
209 .expect("writing to a String cannot fail");
210 }
211 format!(
212 r"struct AlphaMask {{
213 center: vec2f,
214 size: vec2f,
215 rotation: f32,
216 radius: f32,
217 feather: f32,
218 ellipse: u32,
219 invert: u32,
220 mode: u32,
221}};
222
223{SHAPE_WGSL}
224fn builtin_mask_coverage(uv: vec2f, mask: AlphaMask) -> f32 {{
225 let res = resolution();
226 let unit = min(res.x, res.y);
227 let p = builtin_shape_local(uv, mask.center, mask.rotation);
228 let extent = abs(mask.size) * res;
229 let d = builtin_shape_distance(p, extent * 0.5, max(mask.radius, 0.0) * unit, mask.ellipse != 0u);
230 // A one-pixel edge at least, so a sharp mask is still antialiased.
231 let width = max(max(mask.feather, 0.0) * unit, 1.0);
232 var coverage = clamp(0.5 - d / width, 0.0, 1.0) * builtin_shape_extent_weight(extent, width);
233 if (mask.invert != 0u) {{ coverage = 1.0 - coverage; }}
234 return coverage;
235}}
236
237fn effect(uv: vec2f, src: vec4f) -> vec4f {{
238 var masks = array<AlphaMask, {MAX_SHAPE_MASKS}>(
239{masks} );
240 let count = u32(clamp(params.count, 0.0, {MAX_SHAPE_MASKS}.0));
241 // Empty when the first mask adds, full otherwise.
242 var coverage = select(1.0, 0.0, masks[0].mode == 0u);
243 for (var index = 0u; index < {MAX_SHAPE_MASKS}u; index++) {{
244 if (index >= count) {{ break; }}
245 let mask = masks[index];
246 let m = builtin_mask_coverage(uv, mask);
247 if (mask.mode == 1u) {{
248 coverage = coverage * (1.0 - m);
249 }} else if (mask.mode == 2u) {{
250 coverage = coverage * m;
251 }} else {{
252 coverage = coverage + m - coverage * m;
253 }}
254 }}
255 if (count == 0u) {{ return src; }}
256 return src * coverage;
257}}
258"
259 )
260}
261
262#[must_use]
269pub fn track_matte_definition() -> EffectDef {
270 EffectDef {
271 id: TRACK_MATTE_EFFECT_ID.into(),
272 version: 1,
273 kind: EffectKind::Transition,
274 inputs: EffectInputs::Two,
275 params: vec![
276 ParamSpec {
277 unit: ParamUnit::Bool,
278 ..param("luma".into(), ParamType::Bool, ParamValue::Bool(false))
279 },
280 ParamSpec {
281 unit: ParamUnit::Bool,
282 ..param("inverted".into(), ParamType::Bool, ParamValue::Bool(false))
283 },
284 ],
285 resources: Vec::new(),
286 wgsl_body: r"fn builtin_matte_linear(value: f32) -> f32 {
287 if (value <= 0.04045) { return value / 12.92; }
288 return pow((value + 0.055) / 1.055, 2.4);
289}
290
291fn transition(uv: vec2f) -> vec4f {
292 let picture = from_color(uv);
293 let matte = to_color(uv);
294 var coverage = matte.a;
295 if (params.luma != 0u) {
296 let straight = clamp(matte.rgb / max(matte.a, 0.000001), vec3f(0.0), vec3f(1.0));
297 let linear = vec3f(
298 builtin_matte_linear(straight.r),
299 builtin_matte_linear(straight.g),
300 builtin_matte_linear(straight.b),
301 );
302 coverage = dot(linear, vec3f(0.2126, 0.7152, 0.0722)) * matte.a;
303 }
304 if (params.inverted != 0u) { coverage = 1.0 - coverage; }
305 return picture * clamp(coverage, 0.0, 1.0);
306}
307"
308 .into(),
309 tier_min: CapabilityTier::TierB,
310 license: Some(crate::builtins::ALPHA2_EFFECT_LICENSE.into()),
311 }
312}
313
314#[cfg(test)]
315mod tests {
316 use super::{MAX_SHAPE_MASKS, SHAPE_MASK_PARAMS, shape_mask_definition};
317
318 #[test]
319 fn each_masks_static_names_are_the_ones_the_pass_declares() {
320 let declared = shape_mask_definition()
321 .params
322 .into_iter()
323 .map(|spec| spec.name)
324 .collect::<Vec<_>>();
325 let mut expected = vec!["count".to_owned()];
326 for (index, names) in SHAPE_MASK_PARAMS.iter().enumerate() {
327 let fields = [
328 ("center", names.center),
329 ("size", names.size),
330 ("rotation", names.rotation),
331 ("radius", names.radius),
332 ("feather", names.feather),
333 ("ellipse", names.ellipse),
334 ("invert", names.invert),
335 ("mode", names.mode),
336 ];
337 for (field, name) in fields {
338 assert_eq!(name, format!("m{index}_{field}"));
339 expected.push(name.to_owned());
340 }
341 }
342 assert_eq!(declared, expected);
343 assert_eq!(declared.len(), 1 + 8 * MAX_SHAPE_MASKS);
344 }
345}