1use cranpose_ui_graphics::{Brush, Color, Point, Rect, TileMode};
2
3const TRANSPARENT: Color = Color(0.0, 0.0, 0.0, 0.0);
4
5#[doc(hidden)]
6pub fn color_to_rgba(color: Color) -> [f32; 4] {
7 [
8 color.0.clamp(0.0, 1.0),
9 color.1.clamp(0.0, 1.0),
10 color.2.clamp(0.0, 1.0),
11 color.3.clamp(0.0, 1.0),
12 ]
13}
14
15const LEVEL: f32 = 1.0 / 255.0;
16
17pub fn gradient_dither_offset(x: f32, y: f32) -> f32 {
68 let px = x.floor().max(0.0) as u32 + 1;
69 let py = y.floor().max(0.0) as u32 + 1;
70 let m = ((py & 1) << 3) | ((px & 1) << 2) | (py & 2) | ((px & 2) >> 1);
71 m as f32 * (1.0 / 16.0) - (15.0 / 32.0)
72}
73
74fn dither_gradient(rgba: [f32; 4], x: f32, y: f32) -> [f32; 4] {
75 if rgba[3] <= 0.0 {
76 return rgba;
77 }
78 let offset = gradient_dither_offset(x, y) * LEVEL;
79 [
80 (rgba[0] + offset).clamp(0.0, 1.0),
81 (rgba[1] + offset).clamp(0.0, 1.0),
82 (rgba[2] + offset).clamp(0.0, 1.0),
83 rgba[3],
84 ]
85}
86
87fn sample_tiled_gradient_rgba(
88 t: f32,
89 tile_mode: TileMode,
90 colors: &[Color],
91 stops: Option<&[f32]>,
92 x: f32,
93 y: f32,
94) -> [f32; 4] {
95 match normalize_gradient_t(t, tile_mode) {
96 Some(sample_t) => dither_gradient(
97 color_to_rgba(interpolate_colors(colors, stops, sample_t)),
98 x,
99 y,
100 ),
101 None => color_to_rgba(TRANSPARENT),
102 }
103}
104
105#[doc(hidden)]
106pub fn sample_brush_rgba(
107 brush: &Brush,
108 rect: Rect,
109 x: f32,
110 y: f32,
111 dither_origin: Point,
112) -> [f32; 4] {
113 let dither = Point::new(x - dither_origin.x, y - dither_origin.y);
114 match brush {
115 Brush::Solid(color) => color_to_rgba(*color),
116 Brush::LinearGradient {
117 colors,
118 stops,
119 start,
120 end,
121 tile_mode,
122 } => {
123 let sx = resolve_gradient_point(rect.x, rect.width, start.x);
124 let sy = resolve_gradient_point(rect.y, rect.height, start.y);
125 let ex = resolve_gradient_point(rect.x, rect.width, end.x);
126 let ey = resolve_gradient_point(rect.y, rect.height, end.y);
127 let dx = ex - sx;
128 let dy = ey - sy;
129 let denom = (dx * dx + dy * dy).max(f32::EPSILON);
130 let t = ((x - sx) * dx + (y - sy) * dy) / denom;
131 sample_tiled_gradient_rgba(t, *tile_mode, colors, stops.as_deref(), dither.x, dither.y)
132 }
133 Brush::RadialGradient {
134 colors,
135 stops,
136 center,
137 radius,
138 tile_mode,
139 } => {
140 let cx = rect.x + center.x;
141 let cy = rect.y + center.y;
142 let radius = (*radius).max(f32::EPSILON);
143 let dx = x - cx;
144 let dy = y - cy;
145 let distance = (dx * dx + dy * dy).sqrt();
146 let t = distance / radius;
147 sample_tiled_gradient_rgba(t, *tile_mode, colors, stops.as_deref(), dither.x, dither.y)
148 }
149 Brush::SweepGradient {
150 colors,
151 stops,
152 center,
153 } => {
154 let cx = rect.x + center.x;
155 let cy = rect.y + center.y;
156 let dx = x - cx;
157 let dy = y - cy;
158 let angle = dy.atan2(dx);
159 let t = (angle / std::f32::consts::TAU + 0.5).clamp(0.0, 1.0);
160 dither_gradient(
161 color_to_rgba(interpolate_colors(colors, stops.as_deref(), t)),
162 dither.x,
163 dither.y,
164 )
165 }
166 }
167}
168
169fn resolve_gradient_point(origin: f32, extent: f32, value: f32) -> f32 {
170 if value.is_finite() {
171 origin + value
172 } else if value.is_sign_positive() {
173 origin + extent
174 } else {
175 origin
176 }
177}
178
179#[doc(hidden)]
180pub fn normalize_gradient_t(t: f32, tile_mode: TileMode) -> Option<f32> {
181 match tile_mode {
182 TileMode::Clamp => Some(t.clamp(0.0, 1.0)),
183 TileMode::Decal => {
184 if (0.0..=1.0).contains(&t) {
185 Some(t)
186 } else {
187 None
188 }
189 }
190 TileMode::Repeated => Some(t.rem_euclid(1.0)),
191 TileMode::Mirror => {
192 let wrapped = t.rem_euclid(2.0);
193 if wrapped <= 1.0 {
194 Some(wrapped)
195 } else {
196 Some(2.0 - wrapped)
197 }
198 }
199 }
200}
201
202fn interpolate_colors(colors: &[Color], stops: Option<&[f32]>, t: f32) -> Color {
203 if colors.is_empty() {
204 return TRANSPARENT;
205 }
206 if colors.len() == 1 {
207 return colors[0];
208 }
209 let clamped = t.clamp(0.0, 1.0);
210
211 if let Some(stops) = stops
212 && stops.len() == colors.len()
213 {
214 if clamped <= stops[0] {
215 return colors[0];
216 }
217 for index in 0..(stops.len() - 1) {
218 let start = stops[index];
219 let end = stops[index + 1];
220 if clamped <= end {
221 let span = (end - start).max(f32::EPSILON);
222 let frac = ((clamped - start) / span).clamp(0.0, 1.0);
223 return lerp_color(colors[index], colors[index + 1], frac);
224 }
225 }
226 return last_color(colors);
227 }
228
229 let segments = (colors.len() - 1) as f32;
230 let scaled = clamped * segments;
231 let index = scaled.floor() as usize;
232 if index >= colors.len() - 1 {
233 return last_color(colors);
234 }
235 let frac = scaled - index as f32;
236 lerp_color(colors[index], colors[index + 1], frac)
237}
238
239fn last_color(colors: &[Color]) -> Color {
240 colors.last().copied().unwrap_or(TRANSPARENT)
241}
242
243fn lerp_color(a: Color, b: Color, t: f32) -> Color {
244 let lerp = |start: f32, end: f32| start + (end - start) * t;
245 Color(
246 lerp(a.0, b.0),
247 lerp(a.1, b.1),
248 lerp(a.2, b.2),
249 lerp(a.3, b.3),
250 )
251}
252
253#[cfg(test)]
254#[path = "tests/brush_sampling_tests.rs"]
255mod tests;