1use vyre_foundation::ir::model::expr::GeneratorRef;
7use vyre_foundation::ir::{BufferAccess, BufferDecl, DataType, Expr, Node, Program};
8
9const OP_ID: &str = "vyre-libs::visual::gradient";
10
11#[derive(Clone, Copy, Debug)]
13pub struct ColorStop {
14 pub position: f32,
16 pub color: u32,
18}
19
20#[must_use]
26pub fn linear_gradient(
27 output: &str,
28 width: u32,
29 height: u32,
30 angle_deg: f32,
31 stops: &[ColorStop],
32) -> Program {
33 try_linear_gradient(output, width, height, angle_deg, stops).unwrap_or_else(|error| {
34 crate::builder::invalid_builder_trap_program(
35 OP_ID,
36 output,
37 DataType::U32,
38 format!("Fix: {error}"),
39 )
40 })
41}
42
43pub fn try_linear_gradient(
50 output: &str,
51 width: u32,
52 height: u32,
53 angle_deg: f32,
54 stops: &[ColorStop],
55) -> Result<Program, String> {
56 let count = width.saturating_mul(height);
57
58 if !(2..=16).contains(&stops.len()) {
59 return Err(format!(
60 "linear_gradient needs 2..=16 stops, got {}. Fix: provide at least two color stops and at most sixteen.",
61 stops.len()
62 ));
63 }
64
65 let angle_rad = angle_deg.to_radians();
72 let dx = angle_rad.sin();
73 let dy = -angle_rad.cos();
74
75 let dx_fp = (dx * 65536.0).round() as i32;
77 let dy_fp = (dy * 65536.0).round() as i32;
78
79 let width_extent = width.saturating_sub(1) as i64;
80 let height_extent = height.saturating_sub(1) as i64;
81 let corner_projections = [
82 0i64,
83 width_extent * i64::from(dx_fp),
84 height_extent * i64::from(dy_fp),
85 width_extent * i64::from(dx_fp) + height_extent * i64::from(dy_fp),
86 ];
87 let min_projection = corner_projections.into_iter().min().unwrap_or(0);
88 let max_projection = corner_projections.into_iter().max().unwrap_or(0);
89 let projection_offset = min_projection
90 .saturating_neg()
91 .clamp(0, i64::from(u32::MAX)) as u32;
92 let projection_range = (max_projection - min_projection).max(1);
93 let projection_range_pixels =
94 ((projection_range + 65_535) / 65_536).clamp(1, i64::from(u32::MAX)) as u32;
95
96 let stop_positions: Vec<u32> = stops
98 .iter()
99 .map(|s| (s.position.clamp(0.0, 1.0) * 65536.0).round() as u32)
100 .collect();
101
102 let stop_r: Vec<u32> = stops.iter().map(|s| s.color & 0xFF).collect();
103 let stop_g: Vec<u32> = stops.iter().map(|s| (s.color >> 8) & 0xFF).collect();
104 let stop_b: Vec<u32> = stops.iter().map(|s| (s.color >> 16) & 0xFF).collect();
105 let stop_a: Vec<u32> = stops.iter().map(|s| s.color >> 24).collect();
106
107 let mut body = vec![Node::let_bind("idx", Expr::gid_x())];
113
114 body.push(Node::if_then(
115 Expr::lt(Expr::var("idx"), Expr::u32(count)),
116 {
117 let mut inner = vec![
118 Node::let_bind("px", Expr::rem(Expr::var("idx"), Expr::u32(width.max(1)))),
119 Node::let_bind("py", Expr::div(Expr::var("idx"), Expr::u32(width.max(1)))),
120 ];
121
122 let dp_x = if dx_fp >= 0 {
125 Expr::mul(Expr::var("px"), Expr::u32(dx_fp as u32))
126 } else {
127 Expr::mul(Expr::var("px"), Expr::u32((-dx_fp) as u32))
130 };
131 let dp_y = if dy_fp >= 0 {
132 Expr::mul(Expr::var("py"), Expr::u32(dy_fp as u32))
133 } else {
134 Expr::mul(Expr::var("py"), Expr::u32((-dy_fp) as u32))
135 };
136
137 let pos_part = Expr::add(
140 if dx_fp >= 0 {
141 dp_x.clone()
142 } else {
143 Expr::u32(0)
144 },
145 if dy_fp >= 0 {
146 dp_y.clone()
147 } else {
148 Expr::u32(0)
149 },
150 );
151 let neg_part = Expr::add(
152 if dx_fp < 0 { dp_x } else { Expr::u32(0) },
153 if dy_fp < 0 { dp_y } else { Expr::u32(0) },
154 );
155
156 inner.push(Node::let_bind("pos_dp", pos_part));
157 inner.push(Node::let_bind("neg_dp", neg_part));
158
159 let shifted_pos = Expr::add(Expr::var("pos_dp"), Expr::u32(projection_offset));
164 inner.push(Node::let_bind(
165 "raw_dp",
166 Expr::select(
167 Expr::ge(shifted_pos.clone(), Expr::var("neg_dp")),
168 Expr::sub(shifted_pos, Expr::var("neg_dp")),
169 Expr::u32(0),
170 ),
171 ));
172 inner.push(Node::let_bind(
173 "t",
174 Expr::select(
175 Expr::gt(
176 Expr::div(Expr::var("raw_dp"), Expr::u32(projection_range_pixels)),
177 Expr::u32(65536),
178 ),
179 Expr::u32(65536),
180 Expr::div(Expr::var("raw_dp"), Expr::u32(projection_range_pixels)),
181 ),
182 ));
183
184 inner.push(Node::let_bind("out_r", Expr::u32(stop_r[0])));
188 inner.push(Node::let_bind("out_g", Expr::u32(stop_g[0])));
189 inner.push(Node::let_bind("out_b", Expr::u32(stop_b[0])));
190 inner.push(Node::let_bind("out_a", Expr::u32(stop_a[0])));
191
192 for i in 0..stops.len() - 1 {
193 let t0 = stop_positions[i];
194 let t1 = stop_positions[i + 1];
195 let span = if t1 > t0 { t1 - t0 } else { 1 }; let lerp_ch = |ch: &str, c0: u32, c1: u32| -> Node {
200 let stop_delta = Expr::sub(Expr::var("t"), Expr::u32(t0));
201 let rounded_delta = |delta: u32| {
202 Expr::div(
203 Expr::add(
204 Expr::mul(Expr::u32(delta), stop_delta.clone()),
205 Expr::u32(span / 2),
206 ),
207 Expr::u32(span),
208 )
209 };
210 Node::assign(
211 ch,
212 Expr::select(
213 Expr::and(
214 Expr::ge(Expr::var("t"), Expr::u32(t0)),
215 Expr::lt(Expr::var("t"), Expr::u32(t1)),
216 ),
217 if c1 >= c0 {
219 Expr::add(Expr::u32(c0), rounded_delta(c1 - c0))
220 } else {
221 Expr::sub(Expr::u32(c0), rounded_delta(c0 - c1))
222 },
223 Expr::var(ch),
224 ),
225 )
226 };
227
228 inner.push(lerp_ch("out_r", stop_r[i], stop_r[i + 1]));
229 inner.push(lerp_ch("out_g", stop_g[i], stop_g[i + 1]));
230 inner.push(lerp_ch("out_b", stop_b[i], stop_b[i + 1]));
231 inner.push(lerp_ch("out_a", stop_a[i], stop_a[i + 1]));
232 }
233
234 let last = stops.len() - 1;
236 inner.push(Node::assign(
237 "out_r",
238 Expr::select(
239 Expr::ge(Expr::var("t"), Expr::u32(stop_positions[last])),
240 Expr::u32(stop_r[last]),
241 Expr::var("out_r"),
242 ),
243 ));
244 inner.push(Node::assign(
245 "out_g",
246 Expr::select(
247 Expr::ge(Expr::var("t"), Expr::u32(stop_positions[last])),
248 Expr::u32(stop_g[last]),
249 Expr::var("out_g"),
250 ),
251 ));
252 inner.push(Node::assign(
253 "out_b",
254 Expr::select(
255 Expr::ge(Expr::var("t"), Expr::u32(stop_positions[last])),
256 Expr::u32(stop_b[last]),
257 Expr::var("out_b"),
258 ),
259 ));
260 inner.push(Node::assign(
261 "out_a",
262 Expr::select(
263 Expr::ge(Expr::var("t"), Expr::u32(stop_positions[last])),
264 Expr::u32(stop_a[last]),
265 Expr::var("out_a"),
266 ),
267 ));
268
269 inner.push(Node::let_bind(
271 "packed",
272 Expr::bitor(
273 Expr::bitor(
274 Expr::var("out_r"),
275 Expr::shl(Expr::var("out_g"), Expr::u32(8)),
276 ),
277 Expr::bitor(
278 Expr::shl(Expr::var("out_b"), Expr::u32(16)),
279 Expr::shl(Expr::var("out_a"), Expr::u32(24)),
280 ),
281 ),
282 ));
283 inner.push(Node::let_bind(
284 "oidx",
285 Expr::add(
286 Expr::mul(Expr::var("py"), Expr::u32(width)),
287 Expr::var("px"),
288 ),
289 ));
290 inner.push(Node::store(output, Expr::var("oidx"), Expr::var("packed")));
291 inner
292 },
293 ));
294
295 Ok(Program::wrapped(
296 vec![
297 BufferDecl::storage(output, 0, BufferAccess::ReadWrite, DataType::U32)
298 .with_count(count),
299 ],
300 super::PIXEL_WORKGROUP_SIZE,
301 vec![crate::region::wrap_anonymous(
302 OP_ID,
303 vec![crate::region::wrap_child(
304 vyre_primitives::visual::packed_rgba_map::OP_ID,
305 GeneratorRef {
306 name: OP_ID.to_string(),
307 },
308 body,
309 )],
310 )],
311 ))
312}
313
314inventory::submit! {
315 vyre_foundation::operation::OperationRegistration {
316 semantic_version: 1,
317 signature: None,
318 tier: vyre_foundation::operation::OperationTier::Library,
319 laws: &[],
320 tolerance: vyre_foundation::operation::TolerancePolicy::EXACT,
321 id: OP_ID,
322 build: Some(|| linear_gradient(
323 "output", 4, 1, 90.0,
324 &[
325 ColorStop { position: 0.0, color: 0xFF_0000FF }, ColorStop { position: 1.0, color: 0xFF_FF0000 }, ],
328 )),
329 test_inputs: Some(|| {
330 vec![vec![vec![0u8; 16]]] }),
332 expected_output: Some(|| {
333 let expected = [0xFF_0000FFu32, 0xFF_5500AAu32, 0xFF_AA0055u32, 0xFF_FF0000u32];
337 vec![vec![crate::visual::byte_helpers::u32_words_to_le_bytes(&expected)]]
338 }),
339 category: Some("visual"),
340 }
341}