Skip to main content

alpha2_model/
mask.rs

1//! Shape masks and track mattes: what limits where a clip is visible.
2
3use crate::{Animated, ClipId, FillRule, ShapeGeometry, ShapeIssue, ShapeProblem};
4use serde::{Deserialize, Serialize};
5
6/// Most shape masks on one clip.
7pub const MAX_CLIP_MASKS: usize = 8;
8
9/// A shape that limits where a clip is visible.
10///
11/// Geometry is in the clip's source space, before its transform, so the mask
12/// moves with the clip: `(0, 0)` is the source's top-left corner and `(1, 1)`
13/// its bottom-right. Distances (`feather`, a rectangle's `corner_radius`, a
14/// polystar's radii) are fractions of that space's shorter side, so a mask
15/// covers the same part of the picture at any render resolution.
16#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
17#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
18#[serde(deny_unknown_fields)]
19pub struct ShapeMask {
20    /// The shape's outline, as a shapes generator's geometry.
21    pub shape: ShapeGeometry,
22    /// How soft the edge is. Non-negative; zero is the outline's
23    /// antialiased edge. The coverage is blurred by a Gaussian of standard
24    /// deviation `0.3125 * feather`, which spreads its 10 to 90 percent ramp
25    /// over `0.8 * feather` across the outline.
26    #[serde(default = "zero")]
27    pub feather: Animated<f32>,
28    /// Whether the mask keeps the outside of the shape instead of its inside.
29    #[serde(default)]
30    pub invert: bool,
31    /// How this mask combines with the ones before it.
32    #[serde(default)]
33    pub mode: MaskMode,
34    /// Which regions of the outline are inside it.
35    #[serde(default)]
36    pub fill_rule: FillRule,
37}
38
39fn zero() -> Animated<f32> {
40    Animated::Constant { value: 0.0 }
41}
42
43/// How a mask combines with the coverage of the masks before it.
44///
45/// Coverage starts empty when the first mask adds, and full otherwise, so a
46/// clip whose only mask subtracts is visible outside the shape.
47#[derive(Copy, Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
48#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
49#[serde(rename_all = "snake_case")]
50pub enum MaskMode {
51    /// Union: `c + m - c * m`.
52    #[default]
53    Add,
54    /// Difference: `c * (1 - m)`.
55    Subtract,
56    /// Intersection: `c * m`.
57    Intersect,
58}
59
60/// Another visual clip whose picture sets where this clip is visible.
61///
62/// The matte clip is drawn, with its own transform and effects, only into the
63/// matte; a clip that is any clip's matte is never composited itself. Where
64/// the matte clip is not active, its coverage is zero.
65#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
66#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
67#[serde(deny_unknown_fields)]
68pub struct Matte {
69    /// The clip whose picture is the matte.
70    pub source: ClipId,
71    /// What of that picture is the coverage.
72    pub mode: MatteMode,
73}
74
75/// Which property of the matte clip's picture is coverage.
76#[derive(Copy, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
77#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
78#[serde(rename_all = "snake_case")]
79pub enum MatteMode {
80    /// Its alpha.
81    Alpha,
82    /// One minus its alpha.
83    AlphaInverted,
84    /// Its luminance over black: BT.709 weights on linear light, times alpha.
85    Luma,
86    /// One minus that luminance.
87    LumaInverted,
88}
89
90impl ShapeMask {
91    /// Checks the outline and feather; `at` names the mask in the issue.
92    pub(crate) fn validate(&self, at: &str) -> Result<(), ShapeIssue> {
93        self.shape.validate(&format!("{at}.shape"))?;
94        if crate::animation::valid_scalar_values(&self.feather, |value| value >= 0.0) {
95            Ok(())
96        } else {
97            Err(ShapeIssue {
98                at: format!("{at}.feather"),
99                problem: ShapeProblem::OutOfRange,
100            })
101        }
102    }
103}
104
105/// Checks each of `masks`, naming the first problem by its index.
106pub(crate) fn validate_masks(masks: &[ShapeMask]) -> Result<(), ShapeIssue> {
107    for (index, mask) in masks.iter().enumerate() {
108        mask.validate(&format!("masks[{index}]"))?;
109    }
110    Ok(())
111}