Skip to main content

alpha2_model/
generator.rs

1//! Sources the engine draws itself: fills, gradients, shapes, and text.
2
3use crate::{
4    GeneratorIssue, LayoutNode, MAX_TEXT_CONTENT_BYTES, MAX_TEXT_GENERATOR_FONT_SIZE, ShapeItem,
5    TextStyle, Vec2,
6};
7use serde::{Deserialize, Serialize};
8
9/// Most stops in one gradient.
10pub const MAX_GRADIENT_STOPS: usize = 8;
11
12/// A straight (not premultiplied), sRGB-encoded color with alpha, each
13/// component in `0..=1`.
14///
15/// Written as the object, or as CSS hex: `"#rgb"`, `"#rgba"`, `"#rrggbb"` or
16/// `"#rrggbbaa"`, any case, each digit pair a component times 255. It is
17/// written back as the object: a component need not be a multiple of 1/255.
18#[derive(Copy, Clone, Debug, Deserialize, PartialEq, Serialize)]
19#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
20#[cfg_attr(
21    feature = "schema",
22    schemars(rename = "Rgba", transform = crate::schema::with_hex_form)
23)]
24#[serde(remote = "Self", deny_unknown_fields)]
25pub struct Rgba {
26    /// Red component.
27    pub red: f32,
28    /// Green component.
29    pub green: f32,
30    /// Blue component.
31    pub blue: f32,
32    /// Coverage.
33    pub alpha: f32,
34}
35
36impl Rgba {
37    /// Creates a color from sRGB-encoded components and alpha.
38    #[must_use]
39    pub const fn new(red: f32, green: f32, blue: f32, alpha: f32) -> Self {
40        Self {
41            red,
42            green,
43            blue,
44            alpha,
45        }
46    }
47
48    pub(crate) fn is_valid(self) -> bool {
49        [self.red, self.green, self.blue, self.alpha]
50            .iter()
51            .all(|value| (0.0..=1.0).contains(value))
52    }
53
54    /// The color a CSS hex string names, if it is one.
55    #[must_use]
56    pub fn from_hex(text: &str) -> Option<Self> {
57        let digits = text.strip_prefix('#')?.as_bytes();
58        let nibble = |index: usize| char::from(digits[index]).to_digit(16);
59        let component = |value: u32| u8::try_from(value).ok().map(|byte| f32::from(byte) / 255.0);
60        let channels = match digits.len() {
61            3 | 4 => (0..digits.len())
62                .map(|index| component(nibble(index)? * 17))
63                .collect::<Option<Vec<_>>>()?,
64            6 | 8 => (0..digits.len() / 2)
65                .map(|index| component(nibble(index * 2)? * 16 + nibble(index * 2 + 1)?))
66                .collect::<Option<Vec<_>>>()?,
67            _ => return None,
68        };
69        Some(Self::new(
70            channels[0],
71            channels[1],
72            channels[2],
73            channels.get(3).copied().unwrap_or(1.0),
74        ))
75    }
76}
77
78/// Reads a hex string or the object.
79struct RgbaVisitor;
80
81impl<'de> serde::de::Visitor<'de> for RgbaVisitor {
82    type Value = Rgba;
83
84    fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
85        formatter.write_str("a color: the {red, green, blue, alpha} object or a CSS hex string")
86    }
87
88    fn visit_str<E: serde::de::Error>(self, value: &str) -> Result<Rgba, E> {
89        Rgba::from_hex(value).ok_or_else(|| {
90            E::custom(format!(
91                "invalid color \"{value}\": write #rgb, #rgba, #rrggbb, or #rrggbbaa"
92            ))
93        })
94    }
95
96    fn visit_map<A: serde::de::MapAccess<'de>>(self, map: A) -> Result<Rgba, A::Error> {
97        Rgba::deserialize(serde::de::value::MapAccessDeserializer::new(map))
98    }
99}
100
101impl<'de> Deserialize<'de> for Rgba {
102    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
103        deserializer.deserialize_any(RgbaVisitor)
104    }
105}
106
107impl Serialize for Rgba {
108    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
109        Self::serialize(self, serializer)
110    }
111}
112
113/// The picture of a generated source.
114///
115/// A generated picture has the canvas's size and aspect, like a source that
116/// fills the frame at scale 1. Positions are in its normalized space, `(0, 0)`
117/// top-left to `(1, 1)` bottom-right; lengths (radii, stroke widths) are
118/// fractions of its shorter side, so the picture is the same at any render
119/// resolution. A fill or text does not change over time (a clip animates it
120/// with its transform, effects, and masks); shapes and layouts may.
121#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
122#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
123#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
124pub enum Generator {
125    /// Paint over the whole picture.
126    Fill {
127        /// What covers the picture.
128        paint: Paint,
129    },
130    /// Shapes drawn from an ordered content tree, as an After Effects shape
131    /// layer or a Lottie shape layer draws them. Unlike the other
132    /// generators, its picture changes over time: every value in the tree
133    /// may be keyframed, in the picture's time.
134    Shapes {
135        /// The content tree, front first. See [`ShapeItem`].
136        items: Vec<ShapeItem>,
137    },
138    /// Text on a transparent picture, laid out and placed by its style as a
139    /// caption track's cues are: its size and wrapping width fractions of
140    /// the canvas, its anchor at `style.position`. The engine draws it with
141    /// the fonts its host registered.
142    Text {
143        /// The text; `\n` starts a line. Non-empty, at most
144        /// [`MAX_TEXT_CONTENT_BYTES`] bytes.
145        text: String,
146        /// How it looks and where it lies. The font size is at most
147        /// [`MAX_TEXT_GENERATOR_FONT_SIZE`].
148        #[serde(default)]
149        style: TextStyle,
150    },
151    /// Boxes laid out by CSS flexbox around their text and shapes. Its
152    /// picture changes over time when any value in the tree is keyframed,
153    /// in the picture's time. The engine lays it out with the fonts it
154    /// draws with. See [`LayoutNode`].
155    Layout {
156        /// The root box: the whole picture.
157        root: Box<LayoutNode>,
158    },
159}
160
161/// A color or gradient.
162///
163/// Gradients interpolate between stops premultiplied, in linear light: the
164/// engine's working space, so a gradient between two colors passes through
165/// the physical mix of them, and a stop fading to transparent does not darken
166/// its neighbour.
167///
168/// A color (hex or object) is read as a solid paint of it; a paint is
169/// written back tagged.
170#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
171#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
172#[cfg_attr(
173    feature = "schema",
174    schemars(rename = "Paint", transform = crate::schema::with_color_form)
175)]
176#[serde(
177    remote = "Self",
178    tag = "type",
179    rename_all = "snake_case",
180    deny_unknown_fields
181)]
182pub enum Paint {
183    /// One color.
184    Solid {
185        /// The color.
186        color: Rgba,
187    },
188    /// Colors along the line from `start` to `end`, constant across it.
189    LinearGradient {
190        /// Where offset 0 lies.
191        start: Vec2,
192        /// Where offset 1 lies. Distinct from `start`.
193        end: Vec2,
194        /// Two to [`MAX_GRADIENT_STOPS`] stops.
195        stops: Vec<GradientStop>,
196        /// What lies beyond the first and last offsets.
197        #[serde(default)]
198        spread: Spread,
199    },
200    /// Colors along the radius of a circle.
201    RadialGradient {
202        /// Where offset 0 lies.
203        center: Vec2,
204        /// The distance of offset 1 from the centre, as a fraction of the
205        /// picture's shorter side. Positive.
206        radius: f32,
207        /// Two to [`MAX_GRADIENT_STOPS`] stops.
208        stops: Vec<GradientStop>,
209        /// What lies beyond the first and last offsets.
210        #[serde(default)]
211        spread: Spread,
212    },
213    /// Colors around a point: offsets 0 to 1 are one clockwise turn.
214    ConicGradient {
215        /// The point the gradient turns around.
216        center: Vec2,
217        /// Where offset 0 lies, in degrees clockwise from straight up.
218        #[serde(default)]
219        angle_degrees: f32,
220        /// Two to [`MAX_GRADIENT_STOPS`] stops.
221        stops: Vec<GradientStop>,
222    },
223}
224
225impl<'de> Deserialize<'de> for Paint {
226    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
227        use serde::de::Error as _;
228        // Which form it is shows only in whether it has a tag.
229        let value = serde_json::Value::deserialize(deserializer)?;
230        if value.get("type").is_some() {
231            Self::deserialize(value).map_err(D::Error::custom)
232        } else {
233            <Rgba as Deserialize>::deserialize(value)
234                .map(|color| Self::Solid { color })
235                .map_err(D::Error::custom)
236        }
237    }
238}
239
240impl Serialize for Paint {
241    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
242        Self::serialize(self, serializer)
243    }
244}
245
246/// One color of a gradient.
247#[derive(Copy, Clone, Debug, Deserialize, PartialEq, Serialize)]
248#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
249#[serde(deny_unknown_fields)]
250pub struct GradientStop {
251    /// Position along the gradient, in `0..=1`, not before the previous stop.
252    pub offset: f32,
253    /// Color at that position.
254    pub color: Rgba,
255}
256
257/// What a linear or radial gradient shows outside its first and last stops.
258#[derive(Copy, Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
259#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
260#[serde(rename_all = "snake_case")]
261pub enum Spread {
262    /// The end colors continue.
263    #[default]
264    Pad,
265    /// The gradient starts again.
266    Repeat,
267    /// The gradient runs back and forth.
268    Reflect,
269}
270
271impl Generator {
272    /// Whether every field is in range, as a project needs it to be.
273    #[must_use]
274    pub fn is_valid(&self) -> bool {
275        self.validate().is_ok()
276    }
277
278    /// Checks every field. A shapes or layout generator's problem names
279    /// where in its tree it lies; another generator's is
280    /// [`GeneratorIssue::Invalid`]. A layout's pixel font sizes are not
281    /// held to the limit here: that takes the canvas ([`Self::validate_on`]).
282    ///
283    /// # Errors
284    /// Returns the first problem, if any.
285    pub fn validate(&self) -> Result<(), GeneratorIssue> {
286        self.validate_canvas(None)
287    }
288
289    /// [`Self::validate`] for a generator drawn on a `width` by `height`
290    /// canvas, which a layout's pixel lengths are pixels of.
291    ///
292    /// # Errors
293    /// Returns the first problem, if any.
294    pub fn validate_on(&self, width: u32, height: u32) -> Result<(), GeneratorIssue> {
295        self.validate_canvas(Some((width, height)))
296    }
297
298    fn validate_canvas(&self, canvas: Option<(u32, u32)>) -> Result<(), GeneratorIssue> {
299        let valid = match self {
300            Self::Fill { paint } => paint.is_valid(),
301            Self::Shapes { items } => {
302                return crate::shapes::validate_shape_items(items).map_err(GeneratorIssue::Shape);
303            }
304            Self::Layout { root } => return crate::layout::validate_layout(root, canvas),
305            Self::Text { text, style } => {
306                !text.is_empty()
307                    && text.len() <= MAX_TEXT_CONTENT_BYTES
308                    && style.is_valid(MAX_TEXT_GENERATOR_FONT_SIZE)
309            }
310        };
311        valid.then_some(()).ok_or(GeneratorIssue::Invalid)
312    }
313}
314
315impl Paint {
316    pub(crate) fn is_valid(&self) -> bool {
317        match self {
318            Self::Solid { color } => color.is_valid(),
319            Self::LinearGradient {
320                start, end, stops, ..
321            } => finite(*start) && finite(*end) && start != end && valid_stops(stops),
322            Self::RadialGradient {
323                center,
324                radius,
325                stops,
326                ..
327            } => finite(*center) && radius.is_finite() && *radius > 0.0 && valid_stops(stops),
328            Self::ConicGradient {
329                center,
330                angle_degrees,
331                stops,
332            } => finite(*center) && angle_degrees.is_finite() && valid_stops(stops),
333        }
334    }
335}
336
337fn finite(value: Vec2) -> bool {
338    value.x.is_finite() && value.y.is_finite()
339}
340
341fn valid_stops(stops: &[GradientStop]) -> bool {
342    (2..=MAX_GRADIENT_STOPS).contains(&stops.len())
343        && stops
344            .iter()
345            .all(|stop| (0.0..=1.0).contains(&stop.offset) && stop.color.is_valid())
346        && stops
347            .windows(2)
348            .all(|pair| pair[0].offset <= pair[1].offset)
349}