Skip to main content

alpha2_render/
generator.rs

1//! The internal generator behind fill sources: a color or gradient over the
2//! whole picture, drawn from parameters alone. Shapes are drawn by the vector
3//! pass ([`crate::vector`]), with the same paint code.
4
5use std::fmt::Write as _;
6
7use alpha2_gpu::CapabilityTier;
8
9use crate::{EffectDef, EffectKind, ParamSpec, ParamType, ParamUnit, ParamValue};
10
11/// Internal identifier of the generator pass.
12pub const GENERATOR_EFFECT_ID: &str = "alpha2.internal.generator";
13
14/// Stops one paint carries. The project model allows as many.
15pub const MAX_GENERATOR_STOPS: usize = 8;
16
17/// Enum values of a paint's `kind` parameter.
18pub const PAINT_KINDS: [&str; 5] = ["none", "solid", "linear", "radial", "conic"];
19
20/// Enum values of a paint's `spread` parameter.
21pub const PAINT_SPREADS: [&str; 3] = ["pad", "repeat", "reflect"];
22
23/// The parameter names of one of the generator's paints, as
24/// [`generator_definition`] declares them.
25#[derive(Copy, Clone, Debug, Eq, PartialEq)]
26pub struct PaintParams {
27    /// `{paint}_kind`.
28    pub kind: &'static str,
29    /// `{paint}_a`.
30    pub a: &'static str,
31    /// `{paint}_b`.
32    pub b: &'static str,
33    /// `{paint}_spread`.
34    pub spread: &'static str,
35    /// `{paint}_count`.
36    pub count: &'static str,
37    /// `{paint}_offsets0`.
38    pub offsets0: &'static str,
39    /// `{paint}_offsets1`.
40    pub offsets1: &'static str,
41    /// `{paint}_c0` to `{paint}_c7`, by stop.
42    pub colors: [&'static str; MAX_GENERATOR_STOPS],
43}
44
45macro_rules! paint_params {
46    ($paint:literal) => {
47        PaintParams {
48            kind: concat!($paint, "_kind"),
49            a: concat!($paint, "_a"),
50            b: concat!($paint, "_b"),
51            spread: concat!($paint, "_spread"),
52            count: concat!($paint, "_count"),
53            offsets0: concat!($paint, "_offsets0"),
54            offsets1: concat!($paint, "_offsets1"),
55            colors: [
56                concat!($paint, "_c0"),
57                concat!($paint, "_c1"),
58                concat!($paint, "_c2"),
59                concat!($paint, "_c3"),
60                concat!($paint, "_c4"),
61                concat!($paint, "_c5"),
62                concat!($paint, "_c6"),
63                concat!($paint, "_c7"),
64            ],
65        }
66    };
67}
68
69/// The fill paint's parameter names: static, so a caller sets a generator's
70/// paints without formatting a name.
71pub const FILL_PAINT_PARAMS: PaintParams = paint_params!("fill");
72
73fn param(name: String, param_type: ParamType, default: ParamValue) -> ParamSpec {
74    ParamSpec {
75        name,
76        param_type,
77        default,
78        min: None,
79        max: None,
80        step: None,
81        unit: ParamUnit::Normalized,
82        enum_values: Vec::new(),
83    }
84}
85
86fn enum_param(name: String, values: &[&str]) -> ParamSpec {
87    ParamSpec {
88        unit: ParamUnit::Enum,
89        enum_values: values.iter().map(|value| (*value).to_owned()).collect(),
90        ..param(name, ParamType::Enum, ParamValue::Enum(0))
91    }
92}
93
94/// The generator pass: the fill paint over the whole picture.
95///
96/// The paint (`fill_*`) has a `kind`, two points `a` and `b` (a linear
97/// gradient's start and end; a radial gradient's centre and, in `b.x`,
98/// radius; a conic gradient's centre and, in `b.x`, start angle in radians
99/// clockwise from up), a `spread`, a stop `count`, stop offsets in
100/// `offsets0` (first four) and `offsets1`, and straight sRGB stop colors
101/// `c0` to `c7`. Stops interpolate premultiplied in linear light; the output
102/// is premultiplied and sRGB-encoded.
103#[must_use]
104pub fn generator_definition() -> EffectDef {
105    let names = FILL_PAINT_PARAMS;
106    let mut params = vec![
107        enum_param(names.kind.into(), &PAINT_KINDS),
108        param(
109            names.a.into(),
110            ParamType::Vec2,
111            ParamValue::Vec2([0.0, 0.0]),
112        ),
113        param(
114            names.b.into(),
115            ParamType::Vec2,
116            ParamValue::Vec2([1.0, 0.0]),
117        ),
118        enum_param(names.spread.into(), &PAINT_SPREADS),
119        param(names.count.into(), ParamType::Float, ParamValue::Float(0.0)),
120        param(
121            names.offsets0.into(),
122            ParamType::Vec4,
123            ParamValue::Vec4([0.0; 4]),
124        ),
125        param(
126            names.offsets1.into(),
127            ParamType::Vec4,
128            ParamValue::Vec4([0.0; 4]),
129        ),
130    ];
131    params.extend(names.colors.map(|name| ParamSpec {
132        unit: ParamUnit::Color,
133        ..param(name.into(), ParamType::Color, ParamValue::Color([0.0; 4]))
134    }));
135    EffectDef {
136        id: GENERATOR_EFFECT_ID.into(),
137        version: 1,
138        kind: EffectKind::Generator,
139        inputs: Vec::new(),
140        params,
141        resources: Vec::new(),
142        wgsl_body: generator_body(),
143        tier_min: CapabilityTier::TierB,
144        license: Some(crate::builtins::ALPHA2_EFFECT_LICENSE.into()),
145    }
146}
147
148fn paint_constructor(paint: &str) -> String {
149    let mut colors = String::new();
150    for stop in 0..MAX_GENERATOR_STOPS {
151        let separator = if stop + 1 == MAX_GENERATOR_STOPS {
152            ""
153        } else {
154            ", "
155        };
156        write!(colors, "params.{paint}_c{stop}{separator}")
157            .expect("writing to a String cannot fail");
158    }
159    format!(
160        "fn builtin_{paint}_paint() -> AlphaPaint {{
161    let o0 = params.{paint}_offsets0;
162    let o1 = params.{paint}_offsets1;
163    return AlphaPaint(
164        params.{paint}_kind,
165        params.{paint}_a,
166        params.{paint}_b,
167        params.{paint}_spread,
168        u32(clamp(params.{paint}_count, 0.0, {MAX_GENERATOR_STOPS}.0)),
169        array<f32, {MAX_GENERATOR_STOPS}>(o0.x, o0.y, o0.z, o0.w, o1.x, o1.y, o1.z, o1.w),
170        array<vec4f, {MAX_GENERATOR_STOPS}>({colors}),
171    );
172}}
173"
174    )
175}
176
177fn generator_body() -> String {
178    let paint = paint_constructor("fill");
179    format!(
180        r"{paint_wgsl}
181{paint}
182fn generate(uv: vec2f) -> vec4f {{
183    return builtin_generator_store(builtin_paint(builtin_fill_paint(), uv, resolution()));
184}}
185",
186        paint_wgsl = crate::paint_wgsl::paint_wgsl(),
187    )
188}
189
190#[cfg(test)]
191mod tests {
192    use super::{FILL_PAINT_PARAMS, generator_definition};
193
194    #[test]
195    fn the_paints_static_names_are_the_ones_the_pass_declares() {
196        let declared = generator_definition()
197            .params
198            .into_iter()
199            .map(|spec| spec.name)
200            .collect::<Vec<_>>();
201        let names = FILL_PAINT_PARAMS;
202        let mut expected = Vec::new();
203        let fields = [
204            ("kind", names.kind),
205            ("a", names.a),
206            ("b", names.b),
207            ("spread", names.spread),
208            ("count", names.count),
209            ("offsets0", names.offsets0),
210            ("offsets1", names.offsets1),
211        ];
212        let stops = names
213            .colors
214            .iter()
215            .enumerate()
216            .map(|(stop, name)| (format!("c{stop}"), *name));
217        for (field, name) in fields
218            .into_iter()
219            .map(|(field, name)| (field.to_owned(), name))
220            .chain(stops)
221        {
222            assert_eq!(name, format!("fill_{field}"));
223            expected.push(name.to_owned());
224        }
225        assert_eq!(declared, expected);
226    }
227}