Skip to main content

alpha2_render/
generator.rs

1//! The internal generator behind generated sources: fills, gradients, and
2//! shapes drawn from parameters alone.
3
4use std::fmt::Write as _;
5
6use alpha2_gpu::CapabilityTier;
7
8use crate::{EffectDef, EffectInputs, EffectKind, ParamSpec, ParamType, ParamUnit, ParamValue};
9
10/// Internal identifier of the generator pass.
11pub const GENERATOR_EFFECT_ID: &str = "alpha2.internal.generator";
12
13/// Stops one paint carries. The project model allows as many.
14pub const MAX_GENERATOR_STOPS: usize = 8;
15
16/// Enum values of the generator's `shape` parameter.
17pub const GENERATOR_SHAPES: [&str; 3] = ["fill", "rectangle", "ellipse"];
18
19/// Enum values of a paint's `kind` parameter.
20pub const PAINT_KINDS: [&str; 5] = ["none", "solid", "linear", "radial", "conic"];
21
22/// Enum values of a paint's `spread` parameter.
23pub const PAINT_SPREADS: [&str; 3] = ["pad", "repeat", "reflect"];
24
25/// The two paints a generator draws with, in drawing order.
26const PAINTS: [&str; 2] = ["fill", "stroke"];
27
28/// The parameter names of one of the generator's paints, as
29/// [`generator_definition`] declares them.
30#[derive(Copy, Clone, Debug, Eq, PartialEq)]
31pub struct PaintParams {
32    /// `{paint}_kind`.
33    pub kind: &'static str,
34    /// `{paint}_a`.
35    pub a: &'static str,
36    /// `{paint}_b`.
37    pub b: &'static str,
38    /// `{paint}_spread`.
39    pub spread: &'static str,
40    /// `{paint}_count`.
41    pub count: &'static str,
42    /// `{paint}_offsets0`.
43    pub offsets0: &'static str,
44    /// `{paint}_offsets1`.
45    pub offsets1: &'static str,
46    /// `{paint}_c0` to `{paint}_c7`, by stop.
47    pub colors: [&'static str; MAX_GENERATOR_STOPS],
48}
49
50macro_rules! paint_params {
51    ($paint:literal) => {
52        PaintParams {
53            kind: concat!($paint, "_kind"),
54            a: concat!($paint, "_a"),
55            b: concat!($paint, "_b"),
56            spread: concat!($paint, "_spread"),
57            count: concat!($paint, "_count"),
58            offsets0: concat!($paint, "_offsets0"),
59            offsets1: concat!($paint, "_offsets1"),
60            colors: [
61                concat!($paint, "_c0"),
62                concat!($paint, "_c1"),
63                concat!($paint, "_c2"),
64                concat!($paint, "_c3"),
65                concat!($paint, "_c4"),
66                concat!($paint, "_c5"),
67                concat!($paint, "_c6"),
68                concat!($paint, "_c7"),
69            ],
70        }
71    };
72}
73
74/// The fill paint's parameter names: static, so a caller sets a generator's
75/// paints without formatting a name.
76pub const FILL_PAINT_PARAMS: PaintParams = paint_params!("fill");
77
78/// The stroke paint's parameter names.
79pub const STROKE_PAINT_PARAMS: PaintParams = paint_params!("stroke");
80
81fn param(name: String, param_type: ParamType, default: ParamValue) -> ParamSpec {
82    ParamSpec {
83        name,
84        param_type,
85        default,
86        min: None,
87        max: None,
88        step: None,
89        unit: ParamUnit::Normalized,
90        enum_values: Vec::new(),
91    }
92}
93
94fn enum_param(name: String, values: &[&str]) -> ParamSpec {
95    ParamSpec {
96        unit: ParamUnit::Enum,
97        enum_values: values.iter().map(|value| (*value).to_owned()).collect(),
98        ..param(name, ParamType::Enum, ParamValue::Enum(0))
99    }
100}
101
102/// The generator pass.
103///
104/// `shape` is `fill` (the fill paint everywhere) or a `rectangle` or
105/// `ellipse` at `center` with full `size` (normalized), turned `rotation`
106/// radians clockwise, with corner `radius` and `stroke_width` as fractions
107/// of the shorter side. Each paint (`fill_*`, `stroke_*`) has a `kind`, two
108/// points `a` and `b` (a linear gradient's start and end; a radial
109/// gradient's centre and, in `b.x`, radius; a conic gradient's centre and,
110/// in `b.x`, start angle in radians clockwise from up), a `spread`, a stop
111/// `count`, stop offsets in `offsets0` (first four) and `offsets1`, and
112/// straight sRGB stop colors `c0` to `c7`. Stops interpolate premultiplied in
113/// linear light; the output is premultiplied and sRGB-encoded.
114#[must_use]
115pub fn generator_definition() -> EffectDef {
116    let mut params = vec![
117        enum_param("shape".into(), &GENERATOR_SHAPES),
118        param(
119            "center".into(),
120            ParamType::Vec2,
121            ParamValue::Vec2([0.5, 0.5]),
122        ),
123        param("size".into(), ParamType::Vec2, ParamValue::Vec2([1.0, 1.0])),
124        param("rotation".into(), ParamType::Float, ParamValue::Float(0.0)),
125        param("radius".into(), ParamType::Float, ParamValue::Float(0.0)),
126        param(
127            "stroke_width".into(),
128            ParamType::Float,
129            ParamValue::Float(0.0),
130        ),
131    ];
132    for names in [FILL_PAINT_PARAMS, STROKE_PAINT_PARAMS] {
133        params.extend([
134            enum_param(names.kind.into(), &PAINT_KINDS),
135            param(
136                names.a.into(),
137                ParamType::Vec2,
138                ParamValue::Vec2([0.0, 0.0]),
139            ),
140            param(
141                names.b.into(),
142                ParamType::Vec2,
143                ParamValue::Vec2([1.0, 0.0]),
144            ),
145            enum_param(names.spread.into(), &PAINT_SPREADS),
146            param(names.count.into(), ParamType::Float, ParamValue::Float(0.0)),
147            param(
148                names.offsets0.into(),
149                ParamType::Vec4,
150                ParamValue::Vec4([0.0; 4]),
151            ),
152            param(
153                names.offsets1.into(),
154                ParamType::Vec4,
155                ParamValue::Vec4([0.0; 4]),
156            ),
157        ]);
158        params.extend(names.colors.map(|name| ParamSpec {
159            unit: ParamUnit::Color,
160            ..param(name.into(), ParamType::Color, ParamValue::Color([0.0; 4]))
161        }));
162    }
163    EffectDef {
164        id: GENERATOR_EFFECT_ID.into(),
165        version: 1,
166        kind: EffectKind::Generator,
167        inputs: EffectInputs::None,
168        params,
169        resources: Vec::new(),
170        wgsl_body: generator_body(),
171        tier_min: CapabilityTier::TierB,
172        license: Some(crate::builtins::ALPHA2_EFFECT_LICENSE.into()),
173    }
174}
175
176fn paint_constructor(paint: &str) -> String {
177    let mut colors = String::new();
178    for stop in 0..MAX_GENERATOR_STOPS {
179        let separator = if stop + 1 == MAX_GENERATOR_STOPS {
180            ""
181        } else {
182            ", "
183        };
184        write!(colors, "params.{paint}_c{stop}{separator}")
185            .expect("writing to a String cannot fail");
186    }
187    format!(
188        "fn builtin_{paint}_paint() -> AlphaPaint {{
189    let o0 = params.{paint}_offsets0;
190    let o1 = params.{paint}_offsets1;
191    return AlphaPaint(
192        params.{paint}_kind,
193        params.{paint}_a,
194        params.{paint}_b,
195        params.{paint}_spread,
196        u32(clamp(params.{paint}_count, 0.0, {MAX_GENERATOR_STOPS}.0)),
197        array<f32, {MAX_GENERATOR_STOPS}>(o0.x, o0.y, o0.z, o0.w, o1.x, o1.y, o1.z, o1.w),
198        array<vec4f, {MAX_GENERATOR_STOPS}>({colors}),
199    );
200}}
201"
202    )
203}
204
205fn generator_body() -> String {
206    let paints = PAINTS.map(paint_constructor).concat();
207    format!(
208        r"struct AlphaPaint {{
209    kind: u32,
210    a: vec2f,
211    b: vec2f,
212    spread: u32,
213    count: u32,
214    offsets: array<f32, {MAX_GENERATOR_STOPS}>,
215    // Straight sRGB, as authored.
216    colors: array<vec4f, {MAX_GENERATOR_STOPS}>,
217}};
218
219{shape_wgsl}
220// Straight sRGB to premultiplied linear light; 1 stays exactly 1.
221fn builtin_stop_color(color: vec4f) -> vec4f {{
222    let c = clamp(color.rgb, vec3f(0.0), vec3f(1.0));
223    let curve = pow((c + vec3f(0.055)) / 1.055, vec3f(2.4));
224    let linear = select(select(curve, c / 12.92, c <= vec3f(0.04045)), vec3f(1.0), c >= vec3f(1.0));
225    let alpha = clamp(color.a, 0.0, 1.0);
226    return vec4f(linear * alpha, alpha);
227}}
228
229{paints}
230// Where `uv` lies along the paint's gradient, before spread.
231fn builtin_paint_position(paint: AlphaPaint, uv: vec2f) -> f32 {{
232    let res = resolution();
233    let p = uv * res;
234    if (paint.kind == 2u) {{
235        let start = paint.a * res;
236        let direction = paint.b * res - start;
237        return dot(p - start, direction) / max(dot(direction, direction), 0.000001);
238    }}
239    if (paint.kind == 3u) {{
240        return length(p - paint.a * res) / max(paint.b.x * min(res.x, res.y), 0.000001);
241    }}
242    // Conic: clockwise from straight up (-y), a whole turn from 0 to 1.
243    let offset = p - paint.a * res;
244    if (dot(offset, offset) <= 0.0) {{ return 0.0; }}
245    let angle = atan2(offset.x, -offset.y) - paint.b.x;
246    let turn = angle / 6.283185307179586;
247    return turn - floor(turn);
248}}
249
250fn builtin_paint_spread(paint: AlphaPaint, t: f32) -> f32 {{
251    if (paint.kind == 4u) {{ return t; }}
252    if (paint.spread == 1u) {{ return t - floor(t); }}
253    if (paint.spread == 2u) {{
254        let m = t - 2.0 * floor(t * 0.5);
255        return select(m, 2.0 - m, m > 1.0);
256    }}
257    return clamp(t, 0.0, 1.0);
258}}
259
260// Premultiplied linear-light color of the paint at `uv`.
261fn builtin_paint(paint: AlphaPaint, uv: vec2f) -> vec4f {{
262    if (paint.kind == 0u || paint.count == 0u) {{ return vec4f(0.0); }}
263    var colors = paint.colors;
264    var offsets = paint.offsets;
265    if (paint.kind == 1u || paint.count == 1u) {{ return builtin_stop_color(colors[0]); }}
266    let t = builtin_paint_spread(paint, builtin_paint_position(paint, uv));
267    if (t <= offsets[0]) {{ return builtin_stop_color(colors[0]); }}
268    for (var stop = 1u; stop < {MAX_GENERATOR_STOPS}u; stop++) {{
269        if (stop >= paint.count) {{ break; }}
270        if (t <= offsets[stop]) {{
271            let span = offsets[stop] - offsets[stop - 1u];
272            let f = select((t - offsets[stop - 1u]) / span, 1.0, span <= 0.0);
273            return mix(builtin_stop_color(colors[stop - 1u]), builtin_stop_color(colors[stop]), f);
274        }}
275    }}
276    return builtin_stop_color(colors[paint.count - 1u]);
277}}
278
279// Premultiplied linear light to the premultiplied, sRGB-encoded storage
280// value; 1 stays exactly 1.
281fn builtin_generator_store(color: vec4f) -> vec4f {{
282    if (color.a <= 0.0) {{ return vec4f(0.0); }}
283    let c = clamp(color.rgb / color.a, vec3f(0.0), vec3f(1.0));
284    let curve = 1.055 * pow(c, vec3f(1.0 / 2.4)) - vec3f(0.055);
285    let encoded = select(select(curve, c * 12.92, c <= vec3f(0.0031308)), vec3f(1.0), c >= vec3f(1.0));
286    return vec4f(encoded * color.a, color.a);
287}}
288
289fn generate(uv: vec2f) -> vec4f {{
290    let fill = builtin_paint(builtin_fill_paint(), uv);
291    if (params.shape == 0u) {{ return builtin_generator_store(fill); }}
292    let res = resolution();
293    let unit = min(res.x, res.y);
294    let p = builtin_shape_local(uv, params.center, params.rotation);
295    let extent = abs(params.size) * res;
296    let d = builtin_shape_distance(p, extent * 0.5, max(params.radius, 0.0) * unit, params.shape == 2u);
297    // One-pixel antialiased edges, centred on the outline. A shape or a stroke
298    // narrower than that pixel fades with its width rather than keeping the
299    // ramp's half coverage down to nothing.
300    let inside = clamp(0.5 - d, 0.0, 1.0) * builtin_shape_extent_weight(extent, 1.0);
301    let half_width = max(params.stroke_width, 0.0) * unit * 0.5;
302    let on_stroke = select(0.0, clamp(0.5 - (abs(d) - half_width), 0.0, 1.0) * min(half_width * 2.0, 1.0), half_width > 0.0);
303    let stroke = builtin_paint(builtin_stroke_paint(), uv) * on_stroke;
304    return builtin_generator_store(stroke + fill * inside * (1.0 - stroke.a));
305}}
306",
307        shape_wgsl = crate::masking::SHAPE_WGSL,
308    )
309}
310
311#[cfg(test)]
312mod tests {
313    use super::{FILL_PAINT_PARAMS, STROKE_PAINT_PARAMS, generator_definition};
314
315    #[test]
316    fn each_paints_static_names_are_the_ones_the_pass_declares() {
317        let declared = generator_definition()
318            .params
319            .into_iter()
320            .map(|spec| spec.name)
321            .collect::<Vec<_>>();
322        let mut expected = [
323            "shape",
324            "center",
325            "size",
326            "rotation",
327            "radius",
328            "stroke_width",
329        ]
330        .map(str::to_owned)
331        .to_vec();
332        for (paint, names) in [("fill", FILL_PAINT_PARAMS), ("stroke", STROKE_PAINT_PARAMS)] {
333            let fields = [
334                ("kind", names.kind),
335                ("a", names.a),
336                ("b", names.b),
337                ("spread", names.spread),
338                ("count", names.count),
339                ("offsets0", names.offsets0),
340                ("offsets1", names.offsets1),
341            ];
342            let stops = names
343                .colors
344                .iter()
345                .enumerate()
346                .map(|(stop, name)| (format!("c{stop}"), *name));
347            for (field, name) in fields
348                .into_iter()
349                .map(|(field, name)| (field.to_owned(), name))
350                .chain(stops)
351            {
352                assert_eq!(name, format!("{paint}_{field}"));
353                expected.push(name.to_owned());
354            }
355        }
356        assert_eq!(declared, expected);
357    }
358}