Skip to main content

cranpose_render_common/
text_canvas.rs

1//! The pixels a text's glyph masks are painted into: an image of the text's
2//! own, or a renderer's pixels, and how a glyph's coverage blends there.
3
4use 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
15/// Straight-alpha RGBA8 pixels, row by row, that a text was rasterized
16/// into: what an image of it holds, without the content hash a bitmap
17/// computes over every pixel.
18pub struct TextPixels {
19    /// Width in pixels.
20    pub width: u32,
21    /// Height in pixels.
22    pub height: u32,
23    /// Four bytes a pixel, `width` pixels a row.
24    pub pixels: Vec<u8>,
25}
26
27impl TextPixels {
28    /// The pixels as a bitmap, hashed for caches keyed by content.
29    pub fn into_image(self) -> Option<ImageBitmap> {
30        ImageBitmap::from_rgba8(self.width, self.height, self.pixels).ok()
31    }
32}
33
34/// Pixels that a text paints over, as a renderer's target holds them:
35/// `width` by `height` of them from the first, a row every `stride` pixels.
36pub struct TextPixelWindow<'a> {
37    /// The pixels from the window's top left corner on.
38    pub pixels: TextPixelRows<'a>,
39    /// Pixels from the start of one row to the start of the next.
40    pub stride: usize,
41    /// Width of the window in pixels.
42    pub width: u32,
43    /// Height of the window in pixels.
44    pub height: u32,
45}
46
47/// The pixels of a [`TextPixelWindow`], in the format the renderer draws.
48pub enum TextPixelRows<'a> {
49    /// Straight-alpha RGBA bytes.
50    Rgba(&'a mut [[u8; 4]]),
51    /// 16-bit pixels stored low byte first.
52    Rgb565Le(&'a mut [Rgb565Le]),
53    /// 16-bit pixels stored high byte first.
54    Rgb565Be(&'a mut [Rgb565Be]),
55}
56
57/// A glyph's mask placed on a canvas, its top left corner at `x`, `y`.
58#[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
65/// What a glyph's pixels paint with: its brush, sampled across
66/// `brush_rect`, an alpha the brush's is multiplied by and the shadow under
67/// it, on a canvas whose top left corner sits at `canvas_origin` in device
68/// pixels, `scale` of them a layout pixel.
69pub(crate) struct CanvasPaint<'a> {
70    pub(crate) brush: &'a Brush,
71    pub(crate) alpha: f32,
72    // `brush` at `alpha` as a solid glyph's pixels take it.
73    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// A solid paint as a glyph's pixels take it, worked out once for all the
82// glyphs it paints.
83#[derive(Clone, Copy)]
84pub(crate) struct SolidInk {
85    // The color's channels, each between 0 and 1.
86    color: [f32; 4],
87    red: u8,
88    green: u8,
89    blue: u8,
90    // What a mask's coverage is multiplied by: the color's alpha times the
91    // paint's.
92    alpha_scale: f32,
93    correction: MaskGammaCorrection,
94}
95
96impl SolidInk {
97    // `brush` at `alpha`; `None` for a brush that is no solid color or that
98    // paints nothing.
99    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    /// A renderer's pixels, which solid glyphs blend over as its own
125    /// drawing does.
126    Window(TextPixelWindow<'a>),
127}
128
129/// Pixels a text's glyphs are painted into.
130pub(crate) struct TextCanvas<'a> {
131    pub(crate) width: u32,
132    pub(crate) height: u32,
133    pixels: CanvasPixels<'a>,
134}
135
136impl TextCanvas<'static> {
137    // An image of its own, `width` by `height`, of bytes, for a text whose
138    // glyphs all paint solid colors.
139    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    // An image of its own, `width` by `height`, of floats, for a text where a
148    // gradient or a shadow blends.
149    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    /// A canvas over `window`, for a text whose paint is solid.
160    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// Out of line, as is a glyph's shadow: most text paints solid colors.
263#[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
339/// How a solid glyph's pixel goes over the pixel `P` under it.
340trait SolidGlyphBlend<P> {
341    /// Puts `src`, the glyph's color with its coverage as alpha, over
342    /// `dst`; `alpha` gives that coverage as a unit value.
343    fn blend(&self, dst: &mut P, src: [u8; 4], alpha: impl FnOnce() -> f32);
344}
345
346/// A text's own canvas: a pixel no glyph covered yet takes the glyph's,
347/// and glyphs that overlap combine in floats before the canvas is drawn.
348struct 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
362/// A renderer's pixels: each glyph pixel goes over them as a text canvas's
363/// pixel would, so a glyph that overlaps no other draws the same bytes.
364struct 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/// Paints the solid glyph `placed` in `ink` over an `area` of `pixels`,
373/// width by height, a row every `stride` pixels.
374// Out of line, one function a format: a display takes one byte order, and a
375// build that keeps its drawing in fast memory keeps only that one.
376#[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    // The mask columns and rows that land in the area, and the area's column
398    // and row of the first of them. A row is found by its index: walking
399    // rows through iterator adapters cost more than its pixels.
400    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            // At full alpha the byte is the corrected coverage itself: a
417            // byte over 255 multiplied back by 255 rounds to that byte.
418            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
433// The pixels of `row` of `pixels`, a row every `stride` of them, from
434// column `left` on, `len` of them or fewer where the pixels end.
435fn 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
448/// The indices of a mask's `len` columns or rows, the first at `origin`
449/// on the canvas, that land in the canvas's `extent`.
450fn 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}