Skip to main content

motion_core/drawing/
blend.rs

1//! Blending, fill and mask interpretation.
2
3/// Linear-light layer blending, with explicit compositing space.
4///
5/// Separable modes evaluate straight linear channels with source-over alpha.
6/// Extended RGB is not gamut-clipped. Plus instead adds premultiplied RGB and
7/// clamps the summed alpha to one. Dodge/Burn retain their formula's intrinsic
8/// endpoints and saturation; other modes extend their piecewise formulas without
9/// input clipping. Nonseparable modes use hue/saturation/luminosity replacement.
10#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
11#[cfg_attr(
12    feature = "serde",
13    derive(serde::Serialize, serde::Deserialize),
14    serde(rename_all = "snake_case")
15)]
16#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
17pub enum BlendMode {
18    /// Premultiplied source-over; the default.
19    #[default]
20    SourceOver,
21    /// Unclipped additive RGB with saturated additive alpha.
22    Plus,
23    /// Product of straight source and backdrop channels.
24    Multiply,
25    /// Source plus backdrop minus their product.
26    Screen,
27    /// Minimum of straight source and backdrop channels.
28    Darken,
29    /// Maximum of straight source and backdrop channels.
30    Lighten,
31    /// Absolute difference of straight channels.
32    Difference,
33    /// Source plus backdrop minus twice their product.
34    Exclusion,
35    /// Multiply or screen, selected by the backdrop channel at one half.
36    Overlay,
37    /// Multiply or screen, selected by the source channel at one half.
38    HardLight,
39    /// Piecewise soft lighting, with the low-backdrop cubic and upper square root.
40    SoftLight,
41    /// Backdrop divided by one minus source, saturated above at one.
42    ///
43    /// Nonpositive backdrop returns zero before source at or above one returns one.
44    ColorDodge,
45    /// One minus the saturated ratio of one minus backdrop to source.
46    ///
47    /// Backdrop at or above one returns one before nonpositive source returns zero.
48    ColorBurn,
49    /// Source hue with backdrop saturation and luminosity.
50    Hue,
51    /// Source saturation with backdrop hue and luminosity.
52    Saturation,
53    /// Source hue/saturation with backdrop luminosity.
54    Color,
55    /// Source luminosity with backdrop hue/saturation.
56    Luminosity,
57    /// Internal linear-only additive capture used by sample accumulation.
58    #[doc(hidden)]
59    LinearPlus,
60}
61
62/// Color space used for compositing, independent of stored linear pixels.
63#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
64pub enum BlendSpace {
65    /// Composite straight linear RGB, retaining the default.
66    #[default]
67    LinearRgb,
68    /// Composite encoded sRGB, then return to linear-premultiplied storage.
69    Srgb,
70}
71
72/// How closed contours determine the filled interior.
73///
74/// Recording retains both rules without silently substituting one for
75/// the default NonZero rule.
76///
77/// ```
78/// use motion_core::prelude::*;
79/// use motion_core::{
80///     DisplayList, RasterLimits, ResourceStore, ValidatedDisplayList, MAX_PATH_VERBS,
81///     MAX_STROKE_VERBS, MAX_SVG_PATH_BYTES, sample_offsets,
82/// };
83/// let path = Path2D::from_rect(Rect::new(0.0, 0.0, 10.0, 20.0))?;
84/// let fill = path.fill(Color::WHITE);
85/// let native = DrawContext::default().record(&fill, ValidationLimits::default())?;
86/// assert!(matches!(
87///     native[0],
88///     DrawCommand::Fill {
89///         rule: FillRule::NonZero,
90///         ..
91///     }
92/// ));
93/// let even = DrawContext::default().record(
94///     &fill.fill_rule(FillRule::EvenOdd),
95///     ValidationLimits::default(),
96/// )?;
97/// assert!(matches!(
98///     even[0],
99///     DrawCommand::Fill {
100///         rule: FillRule::EvenOdd,
101///         ..
102///     }
103/// ));
104/// # Ok::<(), motion_core::ValidationError>(())
105/// ```
106#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
107#[cfg_attr(
108    feature = "serde",
109    derive(serde::Serialize, serde::Deserialize),
110    serde(rename_all = "snake_case")
111)]
112#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
113pub enum FillRule {
114    /// Nonzero signed winding count; the default.
115    #[default]
116    NonZero,
117    /// Odd crossing count.
118    EvenOdd,
119}
120
121/// How linear-premultiplied content becomes a layer's mask coverage.
122///
123/// Coverage is computed in the compositing color space and clamped
124/// to 0..=1 before optional inversion, without altering working colors.
125/// Alpha-zero pixels have zero ordinary coverage, even with hidden extended
126/// RGB; inverted modes therefore retain source content at those pixels.
127#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
128#[cfg_attr(
129    feature = "serde",
130    derive(serde::Serialize, serde::Deserialize),
131    serde(rename_all = "snake_case")
132)]
133#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
134pub enum MaskMode {
135    /// Use only the completed mask's alpha.
136    #[default]
137    Alpha,
138    /// One minus the completed mask's alpha, including transparent artwork.
139    InvertedAlpha,
140    /// Use Rec.709 luminance of linear premultiplied RGB, already alpha-weighted.
141    Luminance,
142    /// One minus clamped linear-premultiplied luminance; alpha is not inverted first.
143    InvertedLuminance,
144}