ff_render/nodes/composite/composite_op.rs
1//! `CompositeOp`: the Porter-Duff alpha-compositing operators.
2//!
3//! # Reference
4//!
5//! These are the operators from W3C Compositing and Blending Level 1, whose
6//! general form is
7//!
8//! ```text
9//! Co = as * Fa * Cs + ab * Fb * Cb
10//! ao = as * Fa + ab * Fb
11//! ```
12//!
13//! with `Fa` and `Fb` chosen per operator. Every doc comment below gives the
14//! premultiplied form the shader and [`blend_math`](super::blend_math) actually
15//! evaluate, where `s = as * Cs` is the premultiplied source, `d = ab * Cb` the
16//! premultiplied backdrop, and `sa` / `da` their alphas. The two are the same
17//! thing: substituting `s` and `d` into `co = s * Fa + d * Fb` reproduces the
18//! spec's expression. Skia states the same set as one-liners on premultiplied
19//! colour (`kSrcIn: r = s * da`), and Natron's compositor cites these sections
20//! directly.
21//!
22//! # Relationship to `BlendMode`
23//!
24//! [`BlendMode`](super::BlendMode) is a *colour* function of two pixels;
25//! `CompositeOp` is *alpha* algebra deciding how much of each side survives.
26//! W3C applies them in that order, blend then composite, which is how
27//! `shaders/blend.wgsl` is written. avio's editing model does not combine them:
28//! a layer with a non-`Over` composite has its blend mode ignored, so
29//! `avio::gpu::map_scene` emits `Normal` for those layers.
30//!
31//! # Why the CPU path differs
32//!
33//! `ff_filter::CompositeOp` builds In/Out/Atop/Xor from `blend`'s `all_expr`,
34//! which is per-channel arithmetic rather than alpha compositing: `FFmpeg` has
35//! no Porter-Duff filter, and `all_expr` can only reference the same plane of
36//! both inputs, so it cannot express a colour term that depends on the other
37//! input's alpha. The GPU implements the real operators; the divergence is
38//! recorded on #1670.
39
40/// Porter-Duff alpha-compositing operator.
41///
42/// The discriminant is the value written into the shader's `composite` uniform,
43/// so variants are only ever **appended**, never renumbered. Each doc comment
44/// gives the premultiplied output colour `co` and alpha `ao`.
45#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
46#[repr(u32)]
47pub enum CompositeOp {
48 /// Source over destination. `co = s + d(1-sa)`, `ao = sa + da(1-sa)`.
49 #[default]
50 Over = 0,
51 /// Destination over source. `co = d + s(1-da)`, `ao = da + sa(1-da)`.
52 Under = 1,
53 /// Source shown only where the destination is opaque.
54 /// `co = s·da`, `ao = sa·da`.
55 In = 2,
56 /// Source shown only where the destination is transparent.
57 /// `co = s(1-da)`, `ao = sa(1-da)`.
58 Out = 3,
59 /// Source atop the destination; the destination's shape is kept.
60 /// `co = s·da + d(1-sa)`, `ao = da`.
61 Atop = 4,
62 /// Whichever side the other does not cover.
63 /// `co = s(1-da) + d(1-sa)`, `ao = sa(1-da) + da(1-sa)`.
64 Xor = 5,
65}
66
67#[cfg(test)]
68mod tests {
69 use super::CompositeOp;
70
71 /// The discriminant is the shader's `composite` uniform value, so
72 /// renumbering a variant silently changes what `blend.wgsl` composites. A
73 /// new variant needs a row here and a matching `case` in the shader.
74 #[test]
75 fn composite_op_discriminants_should_match_the_shader_codes() {
76 let expected = [
77 (CompositeOp::Over, 0),
78 (CompositeOp::Under, 1),
79 (CompositeOp::In, 2),
80 (CompositeOp::Out, 3),
81 (CompositeOp::Atop, 4),
82 (CompositeOp::Xor, 5),
83 ];
84 for (op, code) in expected {
85 assert_eq!(op as u32, code, "{op:?} moved to a different code");
86 }
87 assert_eq!(expected.len(), 6);
88 }
89
90 #[test]
91 fn composite_op_should_default_to_over() {
92 assert_eq!(CompositeOp::default(), CompositeOp::Over);
93 }
94}