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, Vec2};
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`) are
14/// fractions of that space's shorter side, so a mask covers the same part of
15/// 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.
21    pub shape: MaskShape,
22    /// Centre of the shape.
23    pub center: Animated<Vec2>,
24    /// Full width and height of the shape, before rotation. Non-negative.
25    pub size: Animated<Vec2>,
26    /// Clockwise rotation about the centre, in degrees.
27    #[serde(default = "zero")]
28    pub rotation_degrees: Animated<f32>,
29    /// Width of the soft edge, centred on the outline. Non-negative; zero is
30    /// a one-pixel antialiased edge.
31    #[serde(default = "zero")]
32    pub feather: Animated<f32>,
33    /// Whether the mask keeps the outside of the shape instead of its inside.
34    #[serde(default)]
35    pub invert: bool,
36    /// How this mask combines with the ones before it.
37    #[serde(default)]
38    pub mode: MaskMode,
39}
40
41fn zero() -> Animated<f32> {
42    Animated::Constant { value: 0.0 }
43}
44
45/// The outline of a [`ShapeMask`].
46#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
47#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
48#[serde(tag = "type", rename_all = "snake_case", from = "MaskShapeWire")]
49pub enum MaskShape {
50    /// A rectangle, optionally with rounded corners.
51    Rectangle {
52        /// Corner radius, as a fraction of the shorter side of source
53        /// space. Non-negative; a radius past half the rectangle is capped.
54        corner_radius: Animated<f32>,
55    },
56    /// The ellipse inscribed in the size.
57    Ellipse,
58}
59
60/// The wire form of [`MaskShape`]: an empty struct variant rejects extra
61/// fields where serde's unit variant would ignore them.
62#[derive(Deserialize)]
63#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
64#[cfg_attr(feature = "schema", schemars(rename = "MaskShape"))]
65#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
66pub(crate) enum MaskShapeWire {
67    /// A rectangle, optionally with rounded corners.
68    Rectangle {
69        /// Corner radius, as a fraction of the shorter side of source
70        /// space. Non-negative; a radius past half the rectangle is capped.
71        #[serde(default = "zero")]
72        corner_radius: Animated<f32>,
73    },
74    /// The ellipse inscribed in the size.
75    Ellipse {},
76}
77
78impl From<MaskShapeWire> for MaskShape {
79    fn from(wire: MaskShapeWire) -> Self {
80        match wire {
81            MaskShapeWire::Rectangle { corner_radius } => Self::Rectangle { corner_radius },
82            MaskShapeWire::Ellipse {} => Self::Ellipse,
83        }
84    }
85}
86
87/// How a mask combines with the coverage of the masks before it.
88///
89/// Coverage starts empty when the first mask adds, and full otherwise, so a
90/// clip whose only mask subtracts is visible outside the shape.
91#[derive(Copy, Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
92#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
93#[serde(rename_all = "snake_case")]
94pub enum MaskMode {
95    /// Union: `c + m - c * m`.
96    #[default]
97    Add,
98    /// Difference: `c * (1 - m)`.
99    Subtract,
100    /// Intersection: `c * m`.
101    Intersect,
102}
103
104/// Another visual clip whose picture sets where this clip is visible.
105///
106/// The matte clip is drawn, with its own transform and effects, only into the
107/// matte; a clip that is any clip's matte is never composited itself. Where
108/// the matte clip is not active, its coverage is zero.
109#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
110#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
111#[serde(deny_unknown_fields)]
112pub struct Matte {
113    /// The clip whose picture is the matte.
114    pub source: ClipId,
115    /// What of that picture is the coverage.
116    pub mode: MatteMode,
117}
118
119/// Which property of the matte clip's picture is coverage.
120#[derive(Copy, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
121#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
122#[serde(rename_all = "snake_case")]
123pub enum MatteMode {
124    /// Its alpha.
125    Alpha,
126    /// One minus its alpha.
127    AlphaInverted,
128    /// Its luminance over black: BT.709 weights on linear light, times alpha.
129    Luma,
130    /// One minus that luminance.
131    LumaInverted,
132}
133
134impl ShapeMask {
135    pub(crate) fn is_valid(&self) -> bool {
136        let non_negative = |animation: &Animated<f32>| {
137            crate::animation::valid_scalar_values(animation, |value| value >= 0.0)
138        };
139        let sizes = crate::animation::valid_vec2_values(&self.size, |value| {
140            value.x >= 0.0 && value.y >= 0.0
141        });
142        let shape = match &self.shape {
143            MaskShape::Rectangle { corner_radius } => non_negative(corner_radius),
144            MaskShape::Ellipse => true,
145        };
146        shape
147            && sizes
148            && crate::animation::valid_vec2_values(&self.center, |_| true)
149            && crate::animation::valid_scalar_values(&self.rotation_degrees, |_| true)
150            && non_negative(&self.feather)
151    }
152}