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