Skip to main content

alpha2_render/
generator.rs

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