1use std::f32::consts::TAU;
6
7#[derive(Clone, Debug, PartialEq)]
10pub struct TextGradient {
11 pub kind: TextGradientKind,
13 pub stops: Vec<TextGradientStop>,
15 pub spread: TextSpread,
17}
18
19#[derive(Copy, Clone, Debug, PartialEq)]
21pub enum TextGradientKind {
22 Linear {
24 start: [f32; 2],
26 end: [f32; 2],
28 },
29 Radial {
31 center: [f32; 2],
33 radius: f32,
35 },
36 Conic {
38 center: [f32; 2],
40 angle: f32,
42 },
43}
44
45#[derive(Copy, Clone, Debug, PartialEq)]
47pub struct TextGradientStop {
48 pub offset: f32,
50 pub color: [f32; 4],
52}
53
54#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
56pub enum TextSpread {
57 #[default]
59 Pad,
60 Repeat,
62 Reflect,
64}
65
66pub(crate) const MAX_STOPS: usize = 16;
68
69impl TextGradient {
70 #[allow(clippy::float_cmp)]
72 pub(crate) fn is_valid(&self) -> bool {
73 let finite = |point: [f32; 2]| point.iter().all(|value| value.is_finite());
74 let geometry = match self.kind {
75 TextGradientKind::Linear { start, end } => finite(start) && finite(end) && start != end,
76 TextGradientKind::Radial { center, radius } => {
77 finite(center) && radius.is_finite() && radius > 0.0
78 }
79 TextGradientKind::Conic { center, angle } => finite(center) && angle.is_finite(),
80 };
81 geometry
82 && (2..=MAX_STOPS).contains(&self.stops.len())
83 && self.stops.iter().all(|stop| {
84 (0.0..=1.0).contains(&stop.offset)
85 && stop.color.iter().all(|value| (0.0..=1.0).contains(value))
86 })
87 && self
88 .stops
89 .windows(2)
90 .all(|pair| pair[0].offset <= pair[1].offset)
91 }
92
93 pub(crate) fn prepared(&self, size: [f32; 2]) -> PreparedGradient<'_> {
95 let at = |point: [f32; 2]| [point[0] * size[0], point[1] * size[1]];
96 let kind = match self.kind {
97 TextGradientKind::Linear { start, end } => TextGradientKind::Linear {
98 start: at(start),
99 end: at(end),
100 },
101 TextGradientKind::Radial { center, radius } => TextGradientKind::Radial {
102 center: at(center),
103 radius: radius * size[0].min(size[1]),
104 },
105 TextGradientKind::Conic { center, angle } => TextGradientKind::Conic {
106 center: at(center),
107 angle,
108 },
109 };
110 PreparedGradient {
111 gradient: self,
112 kind,
113 colors: self
114 .stops
115 .iter()
116 .map(|stop| premultiplied_linear(stop.color))
117 .collect(),
118 }
119 }
120}
121
122pub(crate) struct PreparedGradient<'a> {
124 gradient: &'a TextGradient,
125 kind: TextGradientKind,
127 colors: Vec<[f32; 4]>,
128}
129
130impl PreparedGradient<'_> {
131 pub(crate) fn rgba8_at(&self, point: [f32; 2]) -> [u8; 4] {
133 stored_rgba8(self.linear_at(point))
134 }
135
136 pub(crate) fn linear_at(&self, point: [f32; 2]) -> [f32; 4] {
137 let gradient = self.gradient;
138 let t = spread(self.kind, gradient.spread, position(self.kind, point));
139 let stops = &gradient.stops;
140 if t <= stops[0].offset {
141 return self.colors[0];
142 }
143 for index in 1..stops.len() {
144 if t <= stops[index].offset {
145 let span = stops[index].offset - stops[index - 1].offset;
146 let weight = if span <= 0.0 {
147 1.0
148 } else {
149 (t - stops[index - 1].offset) / span
150 };
151 let (from, to) = (self.colors[index - 1], self.colors[index]);
152 return std::array::from_fn(|channel| {
154 from[channel] * (1.0 - weight) + to[channel] * weight
155 });
156 }
157 }
158 self.colors[stops.len() - 1]
159 }
160}
161
162fn position(kind: TextGradientKind, point: [f32; 2]) -> f32 {
163 match kind {
164 TextGradientKind::Linear { start, end } => {
165 let direction = [end[0] - start[0], end[1] - start[1]];
166 let from = [point[0] - start[0], point[1] - start[1]];
167 dot(from, direction) / dot(direction, direction).max(0.000_001)
168 }
169 TextGradientKind::Radial { center, radius } => {
170 let from = [point[0] - center[0], point[1] - center[1]];
171 dot(from, from).sqrt() / radius.max(0.000_001)
172 }
173 TextGradientKind::Conic { center, angle } => {
174 let from = [point[0] - center[0], point[1] - center[1]];
175 if dot(from, from) <= 0.0 {
176 return 0.0;
177 }
178 let turn = (libm::atan2f(from[0], -from[1]) - angle) / TAU;
180 turn - turn.floor()
181 }
182 }
183}
184
185fn spread(kind: TextGradientKind, spread: TextSpread, t: f32) -> f32 {
186 if matches!(kind, TextGradientKind::Conic { .. }) {
187 return t;
188 }
189 match spread {
190 TextSpread::Pad => t.clamp(0.0, 1.0),
191 TextSpread::Repeat => t - t.floor(),
192 TextSpread::Reflect => {
193 let turn = t - 2.0 * (t * 0.5).floor();
194 if turn > 1.0 { 2.0 - turn } else { turn }
195 }
196 }
197}
198
199fn dot(left: [f32; 2], right: [f32; 2]) -> f32 {
200 left[0] * right[0] + left[1] * right[1]
201}
202
203pub(crate) fn premultiplied_linear(color: [f32; 4]) -> [f32; 4] {
205 let alpha = color[3].clamp(0.0, 1.0);
206 let linear = |value: f32| {
207 let value = value.clamp(0.0, 1.0);
208 if value >= 1.0 {
209 1.0
210 } else if value <= 0.040_45 {
211 value / 12.92
212 } else {
213 libm::powf((value + 0.055) / 1.055, 2.4)
214 }
215 };
216 [
217 linear(color[0]) * alpha,
218 linear(color[1]) * alpha,
219 linear(color[2]) * alpha,
220 alpha,
221 ]
222}
223
224#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
226pub(crate) fn stored_rgba8(color: [f32; 4]) -> [u8; 4] {
227 let alpha = color[3].clamp(0.0, 1.0);
228 if alpha <= 0.0 {
229 return [0; 4];
230 }
231 let encode = |value: f32| {
232 let value = (value / alpha).clamp(0.0, 1.0);
233 if value >= 1.0 {
234 1.0
235 } else if value <= 0.003_130_8 {
236 value * 12.92
237 } else {
238 1.055 * libm::powf(value, 1.0 / 2.4) - 0.055
239 }
240 };
241 let byte = |value: f32| (value.clamp(0.0, 1.0) * 255.0).round() as u8;
242 [
243 byte(encode(color[0])),
244 byte(encode(color[1])),
245 byte(encode(color[2])),
246 byte(alpha),
247 ]
248}