1use cranpose_ui::{Brush, Color, text::Shadow};
5use cranpose_ui_graphics::{BrushSampler, ImageBitmap, Point, Rect, whole};
6
7use crate::{
8 brush_sampling::{color_to_rgba, sample_brush_rgba},
9 glyph_mask::GlyphMask,
10 rgb565::{GlyphPixel, Rgb565Be, Rgb565Le},
11 text_mask_gamma::{MaskGammaCorrection, TextLuminance},
12 text_shadow::draw_shadow_mask,
13};
14
15pub struct TextPixels {
19 pub width: u32,
21 pub height: u32,
23 pub pixels: Vec<u8>,
25}
26
27impl TextPixels {
28 pub fn into_image(self) -> Option<ImageBitmap> {
30 ImageBitmap::from_rgba8(self.width, self.height, self.pixels).ok()
31 }
32}
33
34pub struct TextPixelWindow<'a> {
37 pub pixels: TextPixelRows<'a>,
39 pub stride: usize,
41 pub width: u32,
43 pub height: u32,
45}
46
47pub enum TextPixelRows<'a> {
49 Rgba(&'a mut [[u8; 4]]),
51 Rgb565Le(&'a mut [Rgb565Le]),
53 Rgb565Be(&'a mut [Rgb565Be]),
55}
56
57#[derive(Clone, Copy)]
59pub(crate) struct PlacedMask<'a> {
60 pub(crate) mask: &'a GlyphMask,
61 pub(crate) x: i32,
62 pub(crate) y: i32,
63}
64
65pub(crate) struct CanvasPaint<'a> {
70 pub(crate) brush: &'a Brush,
71 pub(crate) alpha: f32,
72 pub(crate) solid: Option<SolidInk>,
74 pub(crate) shadow: Option<Shadow>,
75 pub(crate) brush_rect: Rect,
76 pub(crate) canvas_origin: Point,
77 pub(crate) scale: f32,
78 pub(crate) static_motion: bool,
79}
80
81#[derive(Clone, Copy)]
84pub(crate) struct SolidInk {
85 color: [f32; 4],
87 red: u8,
88 green: u8,
89 blue: u8,
90 alpha_scale: f32,
93 correction: MaskGammaCorrection,
94}
95
96impl SolidInk {
97 pub(crate) fn of(brush: &Brush, alpha: f32) -> Option<Self> {
100 let Brush::Solid(color) = brush else {
101 return None;
102 };
103 let color = color_to_rgba(*color);
104 let alpha_scale = color[3].clamp(0.0, 1.0) * alpha.clamp(0.0, 1.0);
105 if alpha_scale <= 0.0 {
106 return None;
107 }
108 let byte = |channel: f32| whole::round(channel.clamp(0.0, 1.0) * 255.0) as u8;
109 Some(Self {
110 color,
111 red: byte(color[0]),
112 green: byte(color[1]),
113 blue: byte(color[2]),
114 alpha_scale,
115 correction: TextLuminance::of_color(Color(color[0], color[1], color[2], 1.0))
116 .correction(),
117 })
118 }
119}
120
121enum CanvasPixels<'a> {
122 Solid(Vec<u8>),
123 Blended(Vec<[f32; 4]>),
124 Window(TextPixelWindow<'a>),
127}
128
129pub(crate) struct TextCanvas<'a> {
131 pub(crate) width: u32,
132 pub(crate) height: u32,
133 pixels: CanvasPixels<'a>,
134}
135
136impl TextCanvas<'static> {
137 pub(crate) fn solid(width: u32, height: u32) -> Self {
140 Self {
141 width,
142 height,
143 pixels: CanvasPixels::Solid(vec![0; width as usize * height as usize * 4]),
144 }
145 }
146
147 pub(crate) fn blended(width: u32, height: u32) -> Self {
150 Self {
151 width,
152 height,
153 pixels: CanvasPixels::Blended(vec![[0.0; 4]; width as usize * height as usize]),
154 }
155 }
156}
157
158impl<'a> TextCanvas<'a> {
159 pub(crate) fn over(window: TextPixelWindow<'a>) -> Self {
161 Self {
162 width: window.width,
163 height: window.height,
164 pixels: CanvasPixels::Window(window),
165 }
166 }
167
168 pub(crate) fn paint(&mut self, placed: PlacedMask<'_>, paint: &CanvasPaint<'_>) {
169 let area = (self.width, self.height);
170 match &mut self.pixels {
171 CanvasPixels::Solid(pixels) => {
172 if let Some(ink) = &paint.solid {
173 draw_mask_glyph_solid(
174 pixels.as_chunks_mut::<4>().0,
175 self.width as usize,
176 area,
177 placed,
178 ink,
179 CanvasBlend { color: ink.color },
180 );
181 }
182 }
183 CanvasPixels::Window(window) => {
184 if let Some(ink) = &paint.solid {
185 let stride = window.stride;
186 match &mut window.pixels {
187 TextPixelRows::Rgba(pixels) => {
188 draw_mask_glyph_solid(pixels, stride, area, placed, ink, OverBlend);
189 }
190 TextPixelRows::Rgb565Le(pixels) => {
191 draw_mask_glyph_solid(pixels, stride, area, placed, ink, OverBlend);
192 }
193 TextPixelRows::Rgb565Be(pixels) => {
194 draw_mask_glyph_solid(pixels, stride, area, placed, ink, OverBlend);
195 }
196 }
197 }
198 }
199 CanvasPixels::Blended(pixels) => {
200 if let Some(shadow) = paint.shadow {
201 draw_shadow_mask(
202 pixels,
203 area,
204 placed,
205 shadow,
206 (paint.scale, paint.static_motion),
207 );
208 }
209 draw_mask_glyph(pixels, area, placed, paint);
210 }
211 }
212 }
213
214 pub(crate) fn into_pixels(self) -> Option<TextPixels> {
215 if self.width == 0 || self.height == 0 {
216 return None;
217 }
218 let pixels = match self.pixels {
219 CanvasPixels::Solid(pixels) => pixels,
220 CanvasPixels::Window(_) => return None,
221 CanvasPixels::Blended(canvas) => {
222 let mut pixels = Vec::with_capacity(canvas.len() * 4);
223 for pixel in canvas {
224 pixels.extend(
225 pixel.map(|channel| whole::round(channel.clamp(0.0, 1.0) * 255.0) as u8),
226 );
227 }
228 pixels
229 }
230 };
231 Some(TextPixels {
232 width: self.width,
233 height: self.height,
234 pixels,
235 })
236 }
237}
238
239pub(crate) fn blend_src_over(dst: &mut [f32; 4], src: [f32; 4]) {
240 let src_alpha = src[3].clamp(0.0, 1.0);
241 if src_alpha <= 0.0 {
242 return;
243 }
244
245 let dst_alpha = dst[3].clamp(0.0, 1.0);
246 let out_alpha = src_alpha + dst_alpha * (1.0 - src_alpha);
247
248 if out_alpha <= f32::EPSILON {
249 *dst = [0.0, 0.0, 0.0, 0.0];
250 return;
251 }
252
253 for channel in 0..3 {
254 let src_premult = src[channel].clamp(0.0, 1.0) * src_alpha;
255 let dst_premult = dst[channel].clamp(0.0, 1.0) * dst_alpha;
256 dst[channel] =
257 ((src_premult + dst_premult * (1.0 - src_alpha)) / out_alpha).clamp(0.0, 1.0);
258 }
259 dst[3] = out_alpha;
260}
261
262#[inline(never)]
264fn draw_mask_glyph(
265 canvas: &mut [[f32; 4]],
266 (width, height): (u32, u32),
267 placed: PlacedMask<'_>,
268 paint: &CanvasPaint<'_>,
269) {
270 let mask = placed.mask;
271 let correction = TextLuminance::of_brush(paint.brush).correction();
272 let brush = BrushSampler::new(paint.brush, paint.brush_rect);
273 for y in 0..mask.height {
274 let py = placed.y + y as i32;
275 if py < 0 || py >= height as i32 {
276 continue;
277 }
278
279 for x in 0..mask.width {
280 let px = placed.x + x as i32;
281 if px < 0 || px >= width as i32 {
282 continue;
283 }
284
285 let coverage = correction.apply_unit(mask.alpha[y * mask.width + x]);
286 if coverage <= 0.0 {
287 continue;
288 }
289
290 let sample = sample_brush_rgba(
291 &brush,
292 paint.canvas_origin.x + px as f32 + 0.5,
293 paint.canvas_origin.y + py as f32 + 0.5,
294 Point::default(),
295 );
296 let alpha = coverage * sample[3] * paint.alpha;
297 if alpha <= 0.0 {
298 continue;
299 }
300 let idx = (py as u32 * width + px as u32) as usize;
301 blend_src_over(
302 &mut canvas[idx],
303 [sample[0], sample[1], sample[2], alpha.clamp(0.0, 1.0)],
304 );
305 }
306 }
307}
308
309fn blend_src_over_u8(dst: &mut [u8], src: [f32; 4]) {
310 let src_alpha = src[3].clamp(0.0, 1.0);
311 if src_alpha <= 0.0 {
312 return;
313 }
314
315 let dst_alpha = dst[3] as f32 / 255.0;
316 if dst_alpha <= 0.0 {
317 dst[0] = whole::round(src[0].clamp(0.0, 1.0) * 255.0) as u8;
318 dst[1] = whole::round(src[1].clamp(0.0, 1.0) * 255.0) as u8;
319 dst[2] = whole::round(src[2].clamp(0.0, 1.0) * 255.0) as u8;
320 dst[3] = whole::round(src_alpha * 255.0) as u8;
321 return;
322 }
323
324 let out_alpha = src_alpha + dst_alpha * (1.0 - src_alpha);
325 if out_alpha <= f32::EPSILON {
326 dst.fill(0);
327 return;
328 }
329
330 for channel in 0..3 {
331 let src_premult = src[channel].clamp(0.0, 1.0) * src_alpha;
332 let dst_premult = (dst[channel] as f32 / 255.0) * dst_alpha;
333 dst[channel] =
334 whole::round((src_premult + dst_premult * (1.0 - src_alpha)) / out_alpha * 255.0) as u8;
335 }
336 dst[3] = whole::round(out_alpha.clamp(0.0, 1.0) * 255.0) as u8;
337}
338
339trait SolidGlyphBlend<P> {
341 fn blend(&self, dst: &mut P, src: [u8; 4], alpha: impl FnOnce() -> f32);
344}
345
346struct CanvasBlend {
349 color: [f32; 4],
350}
351
352impl SolidGlyphBlend<[u8; 4]> for CanvasBlend {
353 fn blend(&self, dst: &mut [u8; 4], src: [u8; 4], alpha: impl FnOnce() -> f32) {
354 if dst[3] == 0 {
355 *dst = src;
356 } else {
357 blend_src_over_u8(dst, [self.color[0], self.color[1], self.color[2], alpha()]);
358 }
359 }
360}
361
362struct OverBlend;
365
366impl<P: GlyphPixel> SolidGlyphBlend<P> for OverBlend {
367 fn blend(&self, dst: &mut P, src: [u8; 4], _alpha: impl FnOnce() -> f32) {
368 dst.blend_bytes(src);
369 }
370}
371
372#[inline(never)]
377fn draw_mask_glyph_solid<P>(
378 pixels: &mut [P],
379 stride: usize,
380 (width, height): (u32, u32),
381 placed: PlacedMask<'_>,
382 ink: &SolidInk,
383 blend: impl SolidGlyphBlend<P>,
384) {
385 let mask = placed.mask;
386 let SolidInk {
387 red,
388 green,
389 blue,
390 alpha_scale,
391 correction,
392 ..
393 } = *ink;
394 if width == 0 || mask.width == 0 || stride == 0 {
395 return;
396 }
397 let columns = canvas_span(placed.x, mask.width, width);
401 let rows = canvas_span(placed.y, mask.height, height);
402 let left = columns.start.saturating_add_signed(placed.x as isize);
403 let top = rows.start.saturating_add_signed(placed.y as isize);
404 for (row, mask_y) in (top..).zip(rows) {
405 let start = mask_y.saturating_mul(mask.width);
406 let coverage = mask
407 .alpha
408 .get(start + columns.start..start + columns.end)
409 .unwrap_or_default();
410 let area_row = row_pixels(pixels, stride, row, (left, columns.len()));
411 for (dst, &mask_coverage) in area_row.iter_mut().zip(coverage) {
412 let corrected = correction.apply(mask_coverage);
413 if corrected == 0 {
414 continue;
415 }
416 let alpha =
419 || (crate::text_mask_gamma::unit_coverage(corrected) * alpha_scale).clamp(0.0, 1.0);
420 let alpha_u8 = if alpha_scale == 1.0 {
421 corrected
422 } else {
423 whole::round(alpha() * 255.0) as u8
424 };
425 if alpha_u8 == 0 {
426 continue;
427 }
428 blend.blend(dst, [red, green, blue, alpha_u8], alpha);
429 }
430 }
431}
432
433fn row_pixels<P>(
436 pixels: &mut [P],
437 stride: usize,
438 row: usize,
439 (left, len): (usize, usize),
440) -> &mut [P] {
441 let start = row.saturating_mul(stride);
442 let end = start.saturating_add(stride).min(pixels.len());
443 let from = start.saturating_add(left).min(end);
444 let to = from.saturating_add(len).min(end);
445 pixels.get_mut(from..to).unwrap_or_default()
446}
447
448fn canvas_span(origin: i32, len: usize, extent: u32) -> std::ops::Range<usize> {
451 let start = usize::try_from(-i64::from(origin)).unwrap_or(0).min(len);
452 let end = usize::try_from(i64::from(extent) - i64::from(origin))
453 .unwrap_or(0)
454 .clamp(start, len);
455 start..end
456}