Coverage Report

Created: 2026-06-07 07:41

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/serenity/Userland/Libraries/LibGfx/AntiAliasingPainter.cpp
Line
Count
Source
1
/*
2
 * Copyright (c) 2021, Ali Mohammad Pur <mpfard@serenityos.org>
3
 * Copyright (c) 2022, Ben Maxwell <macdue@dueutil.tech>
4
 * Copyright (c) 2022, Torsten Engelmann <engelTorsten@gmx.de>
5
 *
6
 * SPDX-License-Identifier: BSD-2-Clause
7
 */
8
9
#if defined(AK_COMPILER_GCC)
10
#    pragma GCC optimize("O3")
11
#endif
12
13
#include <AK/Function.h>
14
#include <AK/NumericLimits.h>
15
#include <LibGfx/AntiAliasingPainter.h>
16
#include <LibGfx/Line.h>
17
#include <LibGfx/Painter.h>
18
19
namespace Gfx {
20
21
void AntiAliasingPainter::draw_line(IntPoint actual_from, IntPoint actual_to, Color color, float thickness, LineStyle style, Color alternate_color)
22
0
{
23
0
    draw_line(actual_from.to_type<float>(), actual_to.to_type<float>(), color, thickness, style, alternate_color);
24
0
}
25
26
static Path::StrokeStyle line_style_to_stroke_style(LineStyle style, float thickness)
27
0
{
28
0
    switch (style) {
29
0
    case LineStyle::Solid:
30
0
        return Path::StrokeStyle { .thickness = thickness, .cap_style = Path::CapStyle::Butt, .join_style = Path::JoinStyle::Miter };
31
0
    case LineStyle::Dotted:
32
0
        return Path::StrokeStyle { .thickness = thickness, .cap_style = Path::CapStyle::Round, .join_style = Path::JoinStyle::Round, .dash_pattern = { 0, ceil(thickness / 2) * 4 } };
33
0
    case LineStyle::Dashed:
34
0
        return Path::StrokeStyle { .thickness = thickness, .cap_style = Path::CapStyle::Butt, .join_style = Path::JoinStyle::Miter, .dash_pattern = { thickness, thickness } };
35
0
    }
36
0
    VERIFY_NOT_REACHED();
37
0
}
38
39
void AntiAliasingPainter::draw_line(FloatPoint actual_from, FloatPoint actual_to, Color color, float thickness, LineStyle style, Color)
40
0
{
41
0
    if (color.alpha() == 0)
42
0
        return;
43
44
0
    Path line;
45
0
    line.move_to(actual_from);
46
0
    line.line_to(actual_to);
47
0
    stroke_path(line, color, line_style_to_stroke_style(style, thickness));
48
0
}
49
50
void AntiAliasingPainter::stroke_path(Path const& path, Color color, Path::StrokeStyle const& stroke_style)
51
38.1k
{
52
38.1k
    if (stroke_style.thickness <= 0 || color.alpha() == 0)
53
853
        return;
54
    // FIXME: Cache this? Probably at a higher level such as in LibWeb?
55
37.2k
    fill_path(path.stroke_to_fill(stroke_style), color);
56
37.2k
}
57
58
void AntiAliasingPainter::stroke_path(Path const& path, Gfx::PaintStyle const& paint_style, Path::StrokeStyle const& stroke_style, float opacity)
59
6.46k
{
60
6.46k
    if (stroke_style.thickness <= 0 || opacity <= 0)
61
418
        return;
62
    // FIXME: Cache this? Probably at a higher level such as in LibWeb?
63
6.04k
    fill_path(path.stroke_to_fill(stroke_style), paint_style, opacity);
64
6.04k
}
65
66
void AntiAliasingPainter::fill_rect(FloatRect const& float_rect, Color color)
67
0
{
68
    // Draw the integer part of the rectangle:
69
0
    float right_x = float_rect.x() + float_rect.width();
70
0
    float bottom_y = float_rect.y() + float_rect.height();
71
0
    int x1 = ceilf(float_rect.x());
72
0
    int y1 = ceilf(float_rect.y());
73
0
    int x2 = floorf(right_x);
74
0
    int y2 = floorf(bottom_y);
75
0
    auto solid_rect = Gfx::IntRect::from_two_points({ x1, y1 }, { x2, y2 });
76
0
    m_underlying_painter.fill_rect(solid_rect, color);
77
78
0
    if (float_rect == solid_rect)
79
0
        return;
80
81
    // Draw the rest:
82
0
    float left_subpixel = x1 - float_rect.x();
83
0
    float top_subpixel = y1 - float_rect.y();
84
0
    float right_subpixel = right_x - x2;
85
0
    float bottom_subpixel = bottom_y - y2;
86
0
    float top_left_subpixel = top_subpixel * left_subpixel;
87
0
    float top_right_subpixel = top_subpixel * right_subpixel;
88
0
    float bottom_left_subpixel = bottom_subpixel * left_subpixel;
89
0
    float bottom_right_subpixel = bottom_subpixel * right_subpixel;
90
91
0
    auto subpixel = [&](float alpha) {
92
0
        return color.with_alpha(color.alpha() * alpha);
93
0
    };
94
95
0
    auto set_pixel = [&](int x, int y, float alpha) {
96
0
        m_underlying_painter.set_pixel(x, y, subpixel(alpha), true);
97
0
    };
98
99
0
    auto line_to_rect = [&](int x1, int y1, int x2, int y2) {
100
0
        return IntRect::from_two_points({ x1, y1 }, { x2 + 1, y2 + 1 });
101
0
    };
102
103
0
    set_pixel(x1 - 1, y1 - 1, top_left_subpixel);
104
0
    set_pixel(x2, y1 - 1, top_right_subpixel);
105
0
    set_pixel(x2, y2, bottom_right_subpixel);
106
0
    set_pixel(x1 - 1, y2, bottom_left_subpixel);
107
0
    m_underlying_painter.fill_rect(line_to_rect(x1, y1 - 1, x2 - 1, y1 - 1), subpixel(top_subpixel));
108
0
    m_underlying_painter.fill_rect(line_to_rect(x1, y2, x2 - 1, y2), subpixel(bottom_subpixel));
109
0
    m_underlying_painter.fill_rect(line_to_rect(x1 - 1, y1, x1 - 1, y2 - 1), subpixel(left_subpixel));
110
0
    m_underlying_painter.fill_rect(line_to_rect(x2, y1, x2, y2 - 1), subpixel(right_subpixel));
111
0
}
112
113
void AntiAliasingPainter::draw_ellipse(IntRect const& a_rect, Color color, int thickness)
114
0
{
115
    // FIXME: Come up with an allocation-free version of this!
116
    // Using draw_line() for segments of an ellipse was attempted but gave really poor results :^(
117
    // There probably is a way to adjust the fill of draw_ellipse_part() to do this, but getting it rendering correctly is tricky.
118
    // The outline of the steps required to paint it efficiently is:
119
    //     - Paint the outer ellipse without the fill (from the fill() lambda in draw_ellipse_part())
120
    //     - Paint the inner ellipse, but in the set_pixel() invert the alpha values
121
    //     - Somehow fill in the gap between the two ellipses (the tricky part to get right)
122
    //          - Have to avoid overlapping pixels and accidentally painting over some of the edge pixels
123
124
0
    auto color_no_alpha = color;
125
0
    color_no_alpha.set_alpha(255);
126
0
    auto outline_ellipse_bitmap = ({
127
0
        auto bitmap = Bitmap::create(BitmapFormat::BGRA8888, a_rect.size());
128
0
        if (bitmap.is_error())
129
0
            return warnln("Failed to allocate temporary bitmap for antialiased outline ellipse!");
130
0
        bitmap.release_value();
131
0
    });
132
133
0
    auto outer_rect = a_rect;
134
0
    outer_rect.set_location({ 0, 0 });
135
0
    auto inner_rect = outer_rect.shrunken(thickness * 2, thickness * 2);
136
0
    Painter painter { outline_ellipse_bitmap };
137
0
    AntiAliasingPainter aa_painter { painter };
138
0
    aa_painter.fill_ellipse(outer_rect, color_no_alpha);
139
0
    aa_painter.fill_ellipse(inner_rect, color_no_alpha, BlendMode::AlphaSubtract);
140
0
    m_underlying_painter.blit(a_rect.location(), outline_ellipse_bitmap, outline_ellipse_bitmap->rect(), color.alpha() / 255.);
141
0
}
142
143
void AntiAliasingPainter::fill_circle(IntPoint center, int radius, Color color, BlendMode blend_mode)
144
0
{
145
0
    if (radius <= 0)
146
0
        return;
147
0
    draw_ellipse_part(center, radius, radius, color, false, {}, blend_mode);
148
0
}
149
150
void AntiAliasingPainter::fill_ellipse(IntRect const& a_rect, Color color, BlendMode blend_mode)
151
0
{
152
0
    auto center = a_rect.center();
153
0
    auto radius_a = a_rect.width() / 2;
154
0
    auto radius_b = a_rect.height() / 2;
155
0
    if (radius_a <= 0 || radius_b <= 0)
156
0
        return;
157
0
    if (radius_a == radius_b)
158
0
        return fill_circle(center, radius_a, color, blend_mode);
159
0
    auto x_paint_range = draw_ellipse_part(center, radius_a, radius_b, color, false, {}, blend_mode);
160
    // FIXME: This paints some extra fill pixels that are clipped
161
0
    draw_ellipse_part(center, radius_b, radius_a, color, true, x_paint_range, blend_mode);
162
0
}
163
164
FLATTEN AntiAliasingPainter::Range AntiAliasingPainter::draw_ellipse_part(
165
    IntPoint center, int radius_a, int radius_b, Color color, bool flip_x_and_y, Optional<Range> x_clip, BlendMode blend_mode)
166
0
{
167
    /*
168
    Algorithm from: https://cs.uwaterloo.ca/research/tr/1984/CS-84-38.pdf
169
170
    This method can draw a whole circle with a whole circle in one call using
171
    8-way symmetry, or an ellipse in two calls using 4-way symmetry.
172
    */
173
174
0
    center *= m_underlying_painter.scale();
175
0
    radius_a *= m_underlying_painter.scale();
176
0
    radius_b *= m_underlying_painter.scale();
177
178
    // If this is a ellipse everything can be drawn in one pass with 8 way symmetry
179
0
    bool const is_circle = radius_a == radius_b;
180
181
    // These happen to be the same here, but are treated separately in the paper:
182
    // intensity is the fill alpha
183
0
    int const intensity = 255;
184
    // 0 to subpixel_resolution is the range of alpha values for the circle edges
185
0
    int const subpixel_resolution = intensity;
186
187
    // Current pixel address
188
0
    int i = 0;
189
0
    int q = radius_b;
190
191
    // 1st and 2nd order differences of y
192
0
    int delta_y = 0;
193
0
    int delta2_y = 0;
194
195
0
    int const a_squared = radius_a * radius_a;
196
0
    int const b_squared = radius_b * radius_b;
197
198
    // Exact and predicted values of f(i) -- the ellipse equation scaled by subpixel_resolution
199
0
    int y = subpixel_resolution * radius_b;
200
0
    int y_hat = 0;
201
202
    // The value of f(i)*f(i)
203
0
    int f_squared = y * y;
204
205
    // 1st and 2nd order differences of f(i)*f(i)
206
0
    int delta_f_squared = (static_cast<int64_t>(b_squared) * subpixel_resolution * subpixel_resolution) / a_squared;
207
0
    int delta2_f_squared = -delta_f_squared - delta_f_squared;
208
209
    // edge_intersection_area/subpixel_resolution = percentage of pixel intersected by circle
210
    // (aka the alpha for the pixel)
211
0
    int edge_intersection_area = 0;
212
0
    int old_area = edge_intersection_area;
213
214
0
    auto predict = [&] {
215
0
        delta_y += delta2_y;
216
        // y_hat is the predicted value of f(i)
217
0
        y_hat = y + delta_y;
218
0
    };
219
220
0
    auto minimize = [&] {
221
        // Initialize the minimization
222
0
        delta_f_squared += delta2_f_squared;
223
0
        f_squared += delta_f_squared;
224
225
0
        int min_squared_error = y_hat * y_hat - f_squared;
226
0
        int prediction_overshot = 1;
227
0
        y = y_hat;
228
229
        // Force error negative
230
0
        if (min_squared_error > 0) {
231
0
            min_squared_error = -min_squared_error;
232
0
            prediction_overshot = -1;
233
0
        }
234
235
        // Minimize
236
0
        int previous_error = min_squared_error;
237
0
        while (min_squared_error < 0) {
238
0
            y += prediction_overshot;
239
0
            previous_error = min_squared_error;
240
0
            min_squared_error += y + y - prediction_overshot;
241
0
        }
242
243
0
        if (min_squared_error + previous_error > 0)
244
0
            y -= prediction_overshot;
245
0
    };
246
247
0
    auto correct = [&] {
248
0
        int error = y - y_hat;
249
250
0
        delta2_y += error;
251
0
        delta_y += error;
252
0
    };
253
254
0
    int min_paint_x = NumericLimits<int>::max();
255
0
    int max_paint_x = NumericLimits<int>::min();
256
0
    auto pixel = [&](int x, int y, int alpha) {
257
0
        if (alpha <= 0 || alpha > 255)
258
0
            return;
259
0
        if (flip_x_and_y)
260
0
            swap(x, y);
261
0
        if (x_clip.has_value() && x_clip->contains_inclusive(x))
262
0
            return;
263
0
        min_paint_x = min(x, min_paint_x);
264
0
        max_paint_x = max(x, max_paint_x);
265
0
        alpha = (alpha * color.alpha()) / 255;
266
0
        if (blend_mode == BlendMode::AlphaSubtract)
267
0
            alpha = ~alpha;
268
0
        auto pixel_color = color;
269
0
        pixel_color.set_alpha(alpha);
270
0
        m_underlying_painter.set_pixel(center + IntPoint { x, y }, pixel_color, blend_mode == BlendMode::Normal);
271
0
    };
272
273
0
    auto fill = [&](int x, int ymax, int ymin, int alpha) {
274
0
        while (ymin <= ymax) {
275
0
            pixel(x, ymin, alpha);
276
0
            ymin += 1;
277
0
        }
278
0
    };
279
280
0
    auto symmetric_pixel = [&](int x, int y, int alpha) {
281
0
        pixel(x, y, alpha);
282
0
        pixel(x, -y - 1, alpha);
283
0
        pixel(-x - 1, -y - 1, alpha);
284
0
        pixel(-x - 1, y, alpha);
285
0
        if (is_circle) {
286
0
            pixel(y, x, alpha);
287
0
            pixel(y, -x - 1, alpha);
288
0
            pixel(-y - 1, -x - 1, alpha);
289
0
            pixel(-y - 1, x, alpha);
290
0
        }
291
0
    };
292
293
    // These are calculated incrementally (as it is possibly a tiny bit faster)
294
0
    int ib_squared = 0;
295
0
    int qa_squared = q * a_squared;
296
297
0
    auto in_symmetric_region = [&] {
298
        // Main fix two stop cond here
299
0
        return is_circle ? i < q : ib_squared < qa_squared;
300
0
    };
301
302
    // Draws a 8 octants for a circle or 4 quadrants for a (partial) ellipse
303
0
    while (in_symmetric_region()) {
304
0
        predict();
305
0
        minimize();
306
0
        correct();
307
0
        old_area = edge_intersection_area;
308
0
        edge_intersection_area += delta_y;
309
0
        if (edge_intersection_area >= 0) {
310
            // Single pixel on perimeter
311
0
            symmetric_pixel(i, q, (edge_intersection_area + old_area) / 2);
312
0
            fill(i, q - 1, -q, intensity);
313
0
            fill(-i - 1, q - 1, -q, intensity);
314
0
        } else {
315
            // Two pixels on perimeter
316
0
            edge_intersection_area += subpixel_resolution;
317
0
            symmetric_pixel(i, q, old_area / 2);
318
0
            q -= 1;
319
0
            qa_squared -= a_squared;
320
0
            fill(i, q - 1, -q, intensity);
321
0
            fill(-i - 1, q - 1, -q, intensity);
322
0
            if (!is_circle || in_symmetric_region()) {
323
0
                symmetric_pixel(i, q, (edge_intersection_area + subpixel_resolution) / 2);
324
0
                if (is_circle) {
325
0
                    fill(q, i - 1, -i, intensity);
326
0
                    fill(-q - 1, i - 1, -i, intensity);
327
0
                }
328
0
            } else {
329
0
                edge_intersection_area += subpixel_resolution;
330
0
            }
331
0
        }
332
0
        i += 1;
333
0
        ib_squared += b_squared;
334
0
    }
335
336
0
    if (is_circle) {
337
0
        int alpha = edge_intersection_area / 2;
338
0
        pixel(q, q, alpha);
339
0
        pixel(-q - 1, q, alpha);
340
0
        pixel(-q - 1, -q - 1, alpha);
341
0
        pixel(q, -q - 1, alpha);
342
0
    }
343
344
0
    return Range { min_paint_x, max_paint_x };
345
0
}
346
347
void AntiAliasingPainter::fill_rect_with_rounded_corners(IntRect const& a_rect, Color color, int radius)
348
0
{
349
0
    fill_rect_with_rounded_corners(a_rect, color, radius, radius, radius, radius);
350
0
}
351
352
void AntiAliasingPainter::fill_rect_with_rounded_corners(IntRect const& a_rect, Color color, int top_left_radius, int top_right_radius, int bottom_right_radius, int bottom_left_radius)
353
0
{
354
0
    fill_rect_with_rounded_corners(a_rect, color,
355
0
        { top_left_radius, top_left_radius },
356
0
        { top_right_radius, top_right_radius },
357
0
        { bottom_right_radius, bottom_right_radius },
358
0
        { bottom_left_radius, bottom_left_radius });
359
0
}
360
361
void AntiAliasingPainter::fill_rect_with_rounded_corners(IntRect const& a_rect, Color color, CornerRadius top_left, CornerRadius top_right, CornerRadius bottom_right, CornerRadius bottom_left, BlendMode blend_mode)
362
0
{
363
0
    if (!top_left && !top_right && !bottom_right && !bottom_left) {
364
0
        if (blend_mode == BlendMode::Normal)
365
0
            return m_underlying_painter.fill_rect(a_rect, color);
366
0
        else if (blend_mode == BlendMode::AlphaSubtract)
367
0
            return m_underlying_painter.clear_rect(a_rect, Color());
368
0
    }
369
370
0
    if (color.alpha() == 0)
371
0
        return;
372
373
0
    IntPoint top_left_corner {
374
0
        a_rect.x() + top_left.horizontal_radius,
375
0
        a_rect.y() + top_left.vertical_radius,
376
0
    };
377
0
    IntPoint top_right_corner {
378
0
        a_rect.x() + a_rect.width() - top_right.horizontal_radius,
379
0
        a_rect.y() + top_right.vertical_radius,
380
0
    };
381
0
    IntPoint bottom_left_corner {
382
0
        a_rect.x() + bottom_left.horizontal_radius,
383
0
        a_rect.y() + a_rect.height() - bottom_left.vertical_radius
384
0
    };
385
0
    IntPoint bottom_right_corner {
386
0
        a_rect.x() + a_rect.width() - bottom_right.horizontal_radius,
387
0
        a_rect.y() + a_rect.height() - bottom_right.vertical_radius
388
0
    };
389
390
    // All corners are centered at the same point, so this can be painted as a single ellipse.
391
0
    if (top_left_corner == top_right_corner && top_right_corner == bottom_left_corner && bottom_left_corner == bottom_right_corner)
392
0
        return fill_ellipse(a_rect, color, blend_mode);
393
394
0
    IntRect top_rect {
395
0
        a_rect.x() + top_left.horizontal_radius,
396
0
        a_rect.y(),
397
0
        a_rect.width() - top_left.horizontal_radius - top_right.horizontal_radius,
398
0
        top_left.vertical_radius
399
0
    };
400
0
    IntRect right_rect {
401
0
        a_rect.x() + a_rect.width() - top_right.horizontal_radius,
402
0
        a_rect.y() + top_right.vertical_radius,
403
0
        top_right.horizontal_radius,
404
0
        a_rect.height() - top_right.vertical_radius - bottom_right.vertical_radius
405
0
    };
406
0
    IntRect bottom_rect {
407
0
        a_rect.x() + bottom_left.horizontal_radius,
408
0
        a_rect.y() + a_rect.height() - bottom_right.vertical_radius,
409
0
        a_rect.width() - bottom_left.horizontal_radius - bottom_right.horizontal_radius,
410
0
        bottom_right.vertical_radius
411
0
    };
412
0
    IntRect left_rect {
413
0
        a_rect.x(),
414
0
        a_rect.y() + top_left.vertical_radius,
415
0
        bottom_left.horizontal_radius,
416
0
        a_rect.height() - top_left.vertical_radius - bottom_left.vertical_radius
417
0
    };
418
419
0
    IntRect inner = {
420
0
        left_rect.x() + left_rect.width(),
421
0
        left_rect.y(),
422
0
        a_rect.width() - left_rect.width() - right_rect.width(),
423
0
        a_rect.height() - top_rect.height() - bottom_rect.height()
424
0
    };
425
426
0
    if (blend_mode == BlendMode::Normal) {
427
0
        m_underlying_painter.fill_rect(top_rect, color);
428
0
        m_underlying_painter.fill_rect(right_rect, color);
429
0
        m_underlying_painter.fill_rect(bottom_rect, color);
430
0
        m_underlying_painter.fill_rect(left_rect, color);
431
0
        m_underlying_painter.fill_rect(inner, color);
432
0
    } else if (blend_mode == BlendMode::AlphaSubtract) {
433
0
        m_underlying_painter.clear_rect(top_rect, Color());
434
0
        m_underlying_painter.clear_rect(right_rect, Color());
435
0
        m_underlying_painter.clear_rect(bottom_rect, Color());
436
0
        m_underlying_painter.clear_rect(left_rect, Color());
437
0
        m_underlying_painter.clear_rect(inner, Color());
438
0
    }
439
440
0
    auto fill_corner = [&](auto const& ellipse_center, auto const& corner_point, CornerRadius const& corner) {
441
0
        PainterStateSaver save { m_underlying_painter };
442
0
        m_underlying_painter.add_clip_rect(IntRect::from_two_points(ellipse_center, corner_point));
443
0
        fill_ellipse(IntRect::centered_on(ellipse_center, { corner.horizontal_radius * 2, corner.vertical_radius * 2 }), color, blend_mode);
444
0
    };
445
446
0
    auto bounding_rect = a_rect.inflated(0, 1, 1, 0);
447
0
    if (top_left)
448
0
        fill_corner(top_left_corner, bounding_rect.top_left(), top_left);
449
0
    if (top_right)
450
0
        fill_corner(top_right_corner, bounding_rect.top_right().moved_left(1), top_right);
451
0
    if (bottom_left)
452
0
        fill_corner(bottom_left_corner, bounding_rect.bottom_left().moved_up(1), bottom_left);
453
0
    if (bottom_right)
454
0
        fill_corner(bottom_right_corner, bounding_rect.bottom_right().translated(-1), bottom_right);
455
0
}
456
457
}