1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
|
/*
* Copyright (c) 2021, Ali Mohammad Pur <mpfard@serenityos.org>
* Copyright (c) 2022, Ben Maxwell <macdue@dueutil.tech>
* Copyright (c) 2022, Torsten Engelmann <engelTorsten@gmx.de>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#if defined(AK_COMPILER_GCC)
# pragma GCC optimize("O3")
#endif
#include "FillPathImplementation.h"
#include <AK/Function.h>
#include <AK/NumericLimits.h>
#include <LibGfx/AntiAliasingPainter.h>
#include <LibGfx/Line.h>
namespace Gfx {
template<AntiAliasingPainter::FixmeEnableHacksForBetterPathPainting path_hacks>
void AntiAliasingPainter::draw_anti_aliased_line(FloatPoint actual_from, FloatPoint actual_to, Color color, float thickness, Painter::LineStyle style, Color)
{
// FIXME: Implement this :P
VERIFY(style == Painter::LineStyle::Solid);
if (color.alpha() == 0)
return;
// FIMXE:
// This is not a proper line drawing algorithm.
// It's hack-ish AA rotated rectangle painting.
// There's probably more optimal ways to achieve this
// (though this still runs faster than the previous AA-line code)
//
// If you, reading this comment, know a better way that:
// 1. Does not overpaint (i.e. painting a line with transparency looks correct)
// 2. Has square end points (i.e. the line is a rectangle)
// 3. Has good anti-aliasing
// 4. Is less hacky than this
//
// Please delete this code and implement it!
int int_thickness = AK::ceil(thickness);
auto mapped_from = m_transform.map(actual_from);
auto mapped_to = m_transform.map(actual_to);
auto distance = mapped_to.distance_from(mapped_from);
auto length = distance + 1;
// Axis-aligned lines:
if (mapped_from.y() == mapped_to.y()) {
auto start_point = (mapped_from.x() < mapped_to.x() ? mapped_from : mapped_to).translated(0, -int_thickness / 2);
if constexpr (path_hacks == FixmeEnableHacksForBetterPathPainting::Yes) {
// FIXME: SVG fill_path() hack:
// SVG asks for 1px scanlines at floating point y values, if they're not snapped to a pixel they look faint.
start_point.set_y(floorf(start_point.y()));
}
return fill_rect(Gfx::FloatRect(start_point, { length, thickness }), color);
}
if (mapped_from.x() == mapped_to.x()) {
auto start_point = (mapped_from.y() < mapped_to.y() ? mapped_from : mapped_to).translated(-int_thickness / 2, 0);
return fill_rect(Gfx::FloatRect(start_point, { thickness, length }), color);
}
if constexpr (path_hacks == FixmeEnableHacksForBetterPathPainting::Yes) {
// FIXME: SVG stoke_path() hack:
// When painting stokes SVG asks for many very short lines...
// These look better just painted as dots/AA rectangles
// (Technically this should be rotated or a circle, but that currently gives worse results)
if (distance < 1.0f)
return fill_rect(Gfx::FloatRect::centered_at(mapped_from, { thickness, thickness }), color);
}
// The painting only works for the positive XY quadrant (because that is easier).
// So flip things around until we're there:
bool flip_x = false;
bool flip_y = false;
if (mapped_to.x() < mapped_from.x() && mapped_to.y() < mapped_from.y())
swap(mapped_to, mapped_from);
if ((flip_x = mapped_to.x() < mapped_from.x()))
mapped_to.set_x(2 * mapped_from.x() - mapped_to.x());
if ((flip_y = mapped_to.y() < mapped_from.y()))
mapped_to.set_y(2 * mapped_from.y() - mapped_to.y());
auto delta = mapped_to - mapped_from;
auto line_angle_radians = AK::atan2(delta.y(), delta.x()) - 0.5f * AK::Pi<float>;
float sin_inverse_angle;
float cos_inverse_angle;
AK::sincos(-line_angle_radians, sin_inverse_angle, cos_inverse_angle);
auto inverse_rotate_point = [=](FloatPoint point) {
return Gfx::FloatPoint(
point.x() * cos_inverse_angle - point.y() * sin_inverse_angle,
point.y() * cos_inverse_angle + point.x() * sin_inverse_angle);
};
Gfx::FloatRect line_rect({ -(thickness * 255) / 2.0f, 0 }, Gfx::FloatSize(thickness * 255, length * 255));
auto gradient = delta.y() / delta.x();
// Work out how long we need to scan along the X-axis to reach the other side of the line.
// E.g. for a vertical line this would be `thickness', in general it is this:
int scan_line_length = AK::ceil(AK::sqrt((gradient * gradient + 1) * thickness * thickness) / gradient);
auto x_gradient = 1 / gradient;
int x_step = floorf(x_gradient);
float x_error = 0;
float x_error_per_y = x_gradient - x_step;
auto y_offset = int_thickness;
auto x_offset = int(x_gradient * y_offset);
int const line_start_x = mapped_from.x();
int const line_start_y = mapped_from.y();
int const line_end_x = mapped_to.x();
int const line_end_y = mapped_to.y();
auto set_pixel = [=, this](int x, int y, Gfx::Color color) {
// FIXME: The lines seem slightly off (<= 1px) when flipped.
if (flip_x)
x = 2 * line_start_x - x;
if (flip_y)
y = 2 * line_start_y - y;
m_underlying_painter.set_pixel(x, y, color, true);
};
// Scan a bit extra to avoid issues from the x_error:
int const overscan = max(x_step, 1) * 2 + 1;
int x = line_start_x - x_offset;
int const center_offset = (scan_line_length + 1) / 2;
for (int y = line_start_y - y_offset; y < line_end_y + y_offset; y += 1) {
for (int i = -overscan; i < scan_line_length + overscan; i++) {
int scan_x_pos = x + i - center_offset;
// Avoid scanning over pixels definitely outside the line:
int dx = (line_start_x - int_thickness) - (scan_x_pos + 1);
if (dx > 0) {
i += dx;
continue;
}
if (line_end_x + int_thickness <= scan_x_pos - 1)
break;
auto sample = inverse_rotate_point(Gfx::FloatPoint(scan_x_pos - line_start_x, y - line_start_y));
Gfx::FloatRect sample_px(sample * 255, Gfx::FloatSize(255, 255));
sample_px.intersect(line_rect);
auto alpha = (sample_px.width() * sample_px.height()) / 255.0f;
alpha = (alpha * color.alpha()) / 255;
set_pixel(scan_x_pos, y, color.with_alpha(alpha));
}
x += x_step;
x_error += x_error_per_y;
if (x_error > 1.0f) {
x_error -= 1.0f;
x += 1;
}
}
}
void AntiAliasingPainter::draw_line_for_path(FloatPoint const& actual_from, FloatPoint const& actual_to, Color color, float thickness, Painter::LineStyle style, Color alternate_color)
{
draw_anti_aliased_line<FixmeEnableHacksForBetterPathPainting::Yes>(actual_from, actual_to, color, thickness, style, alternate_color);
}
void AntiAliasingPainter::draw_dotted_line(IntPoint point1, IntPoint point2, Color color, int thickness)
{
// AA circles don't really work below a radius of 2px.
if (thickness < 4)
return m_underlying_painter.draw_line(point1, point2, color, thickness, Painter::LineStyle::Dotted);
auto draw_spaced_dots = [&](int start, int end, auto to_point) {
int step = thickness * 2;
if (start > end)
swap(start, end);
int delta = end - start;
int dots = delta / step;
if (dots == 0)
return;
int fudge_per_dot = 0;
int extra_fudge = 0;
if (dots > 3) {
// Fudge the numbers so the last dot is drawn at the `end' point (otherwise you can get lines cuts short).
// You need at least a handful of dots to do this.
int fudge = delta % step;
fudge_per_dot = fudge / dots;
extra_fudge = fudge % dots;
}
for (int dot = start; dot <= end; dot += (step + fudge_per_dot + (extra_fudge > 0))) {
fill_circle(to_point(dot), thickness / 2, color);
--extra_fudge;
}
};
if (point1.y() == point2.y()) {
draw_spaced_dots(point1.x(), point2.x(), [&](int dot_x) {
return IntPoint { dot_x, point1.y() };
});
} else if (point1.x() == point2.x()) {
draw_spaced_dots(point1.y(), point2.y(), [&](int dot_y) {
return IntPoint { point1.x(), dot_y };
});
} else {
TODO();
}
}
void AntiAliasingPainter::draw_line(FloatPoint const& actual_from, FloatPoint const& actual_to, Color color, float thickness, Painter::LineStyle style, Color alternate_color)
{
if (style == Painter::LineStyle::Dotted)
return draw_dotted_line(actual_from.to_rounded<int>(), actual_to.to_rounded<int>(), color, static_cast<int>(round(thickness)));
draw_anti_aliased_line<FixmeEnableHacksForBetterPathPainting::No>(actual_from, actual_to, color, thickness, style, alternate_color);
}
void AntiAliasingPainter::fill_path(Path& path, Color color, Painter::WindingRule rule)
{
Detail::fill_path<Detail::FillPathMode::AllowFloatingPoints>(*this, path, color, rule);
}
void AntiAliasingPainter::stroke_path(Path const& path, Color color, float thickness)
{
FloatPoint cursor;
bool previous_was_line = false;
FloatLine last_line;
Optional<FloatLine> first_line;
for (auto& segment : path.segments()) {
switch (segment.type()) {
case Segment::Type::Invalid:
VERIFY_NOT_REACHED();
case Segment::Type::MoveTo:
cursor = segment.point();
break;
case Segment::Type::LineTo:
draw_line(cursor, segment.point(), color, thickness);
if (thickness > 1) {
if (!first_line.has_value())
first_line = FloatLine(cursor, segment.point());
if (previous_was_line)
stroke_segment_intersection(cursor, segment.point(), last_line, color, thickness);
last_line.set_a(cursor);
last_line.set_b(segment.point());
}
cursor = segment.point();
break;
case Segment::Type::QuadraticBezierCurveTo: {
auto& through = static_cast<QuadraticBezierCurveSegment const&>(segment).through();
draw_quadratic_bezier_curve(through, cursor, segment.point(), color, thickness);
cursor = segment.point();
break;
}
case Segment::Type::CubicBezierCurveTo: {
auto& curve = static_cast<CubicBezierCurveSegment const&>(segment);
auto& through_0 = curve.through_0();
auto& through_1 = curve.through_1();
draw_cubic_bezier_curve(through_0, through_1, cursor, segment.point(), color, thickness);
cursor = segment.point();
break;
}
case Segment::Type::EllipticalArcTo:
auto& arc = static_cast<EllipticalArcSegment const&>(segment);
draw_elliptical_arc(cursor, segment.point(), arc.center(), arc.radii(), arc.x_axis_rotation(), arc.theta_1(), arc.theta_delta(), color, thickness);
cursor = segment.point();
break;
}
previous_was_line = segment.type() == Segment::Type::LineTo;
}
// Check if the figure was started and closed as line at the same position.
if (thickness > 1 && previous_was_line && path.segments().size() >= 2 && path.segments().first().point() == cursor && (path.segments().first().type() == Segment::Type::LineTo || (path.segments().first().type() == Segment::Type::MoveTo && path.segments()[1].type() == Segment::Type::LineTo)))
stroke_segment_intersection(first_line.value().a(), first_line.value().b(), last_line, color, thickness);
}
void AntiAliasingPainter::draw_elliptical_arc(FloatPoint const& p1, FloatPoint const& p2, FloatPoint const& center, FloatPoint const& radii, float x_axis_rotation, float theta_1, float theta_delta, Color color, float thickness, Painter::LineStyle style)
{
Painter::for_each_line_segment_on_elliptical_arc(p1, p2, center, radii, x_axis_rotation, theta_1, theta_delta, [&](FloatPoint const& fp1, FloatPoint const& fp2) {
draw_line_for_path(fp1, fp2, color, thickness, style);
});
}
void AntiAliasingPainter::draw_quadratic_bezier_curve(FloatPoint const& control_point, FloatPoint const& p1, FloatPoint const& p2, Color color, float thickness, Painter::LineStyle style)
{
Painter::for_each_line_segment_on_bezier_curve(control_point, p1, p2, [&](FloatPoint const& fp1, FloatPoint const& fp2) {
draw_line_for_path(fp1, fp2, color, thickness, style);
});
}
void AntiAliasingPainter::draw_cubic_bezier_curve(FloatPoint const& control_point_0, FloatPoint const& control_point_1, FloatPoint const& p1, FloatPoint const& p2, Color color, float thickness, Painter::LineStyle style)
{
Painter::for_each_line_segment_on_cubic_bezier_curve(control_point_0, control_point_1, p1, p2, [&](FloatPoint const& fp1, FloatPoint const& fp2) {
draw_line_for_path(fp1, fp2, color, thickness, style);
});
}
void AntiAliasingPainter::fill_rect(FloatRect const& float_rect, Color color)
{
// Draw the integer part of the rectangle:
float right_x = float_rect.x() + float_rect.width();
float bottom_y = float_rect.y() + float_rect.height();
int x1 = ceilf(float_rect.x());
int y1 = ceilf(float_rect.y());
int x2 = floorf(right_x);
int y2 = floorf(bottom_y);
auto solid_rect = Gfx::IntRect::from_two_points({ x1, y1 }, { x2, y2 });
m_underlying_painter.fill_rect(solid_rect, color);
if (float_rect == solid_rect)
return;
// Draw the rest:
float left_subpixel = x1 - float_rect.x();
float top_subpixel = y1 - float_rect.y();
float right_subpixel = right_x - x2;
float bottom_subpixel = bottom_y - y2;
float top_left_subpixel = top_subpixel * left_subpixel;
float top_right_subpixel = top_subpixel * right_subpixel;
float bottom_left_subpixel = bottom_subpixel * left_subpixel;
float bottom_right_subpixel = bottom_subpixel * right_subpixel;
auto subpixel = [&](float alpha) {
return color.with_alpha(color.alpha() * alpha);
};
auto set_pixel = [&](int x, int y, float alpha) {
m_underlying_painter.set_pixel(x, y, subpixel(alpha), true);
};
auto line_to_rect = [&](int x1, int y1, int x2, int y2) {
return IntRect::from_two_points({ x1, y1 }, { x2 + 1, y2 + 1 });
};
set_pixel(x1 - 1, y1 - 1, top_left_subpixel);
set_pixel(x2, y1 - 1, top_right_subpixel);
set_pixel(x2, y2, bottom_right_subpixel);
set_pixel(x1 - 1, y2, bottom_left_subpixel);
m_underlying_painter.fill_rect(line_to_rect(x1, y1 - 1, x2 - 1, y1 - 1), subpixel(top_subpixel));
m_underlying_painter.fill_rect(line_to_rect(x1, y2, x2 - 1, y2), subpixel(bottom_subpixel));
m_underlying_painter.fill_rect(line_to_rect(x1 - 1, y1, x1 - 1, y2 - 1), subpixel(left_subpixel));
m_underlying_painter.fill_rect(line_to_rect(x2, y1, x2, y2 - 1), subpixel(right_subpixel));
}
void AntiAliasingPainter::draw_ellipse(IntRect const& a_rect, Color color, int thickness)
{
// FIXME: Come up with an allocation-free version of this!
// Using draw_line() for segments of an ellipse was attempted but gave really poor results :^(
// There probably is a way to adjust the fill of draw_ellipse_part() to do this, but getting it rendering correctly is tricky.
// The outline of the steps required to paint it efficiently is:
// - Paint the outer ellipse without the fill (from the fill() lambda in draw_ellipse_part())
// - Paint the inner ellipse, but in the set_pixel() invert the alpha values
// - Somehow fill in the gap between the two ellipses (the tricky part to get right)
// - Have to avoid overlapping pixels and accidentally painting over some of the edge pixels
auto color_no_alpha = color;
color_no_alpha.set_alpha(255);
auto outline_ellipse_bitmap = ({
auto bitmap = Bitmap::try_create(BitmapFormat::BGRA8888, a_rect.size());
if (bitmap.is_error())
return warnln("Failed to allocate temporary bitmap for antialiased outline ellipse!");
bitmap.release_value();
});
auto outer_rect = a_rect;
outer_rect.set_location({ 0, 0 });
auto inner_rect = outer_rect.shrunken(thickness * 2, thickness * 2);
Painter painter { outline_ellipse_bitmap };
AntiAliasingPainter aa_painter { painter };
aa_painter.fill_ellipse(outer_rect, color_no_alpha);
aa_painter.fill_ellipse(inner_rect, color_no_alpha, BlendMode::AlphaSubtract);
m_underlying_painter.blit(a_rect.location(), outline_ellipse_bitmap, outline_ellipse_bitmap->rect(), color.alpha() / 255.);
}
void AntiAliasingPainter::fill_circle(IntPoint const& center, int radius, Color color, BlendMode blend_mode)
{
if (radius <= 0)
return;
draw_ellipse_part(center, radius, radius, color, false, {}, blend_mode);
}
void AntiAliasingPainter::fill_ellipse(IntRect const& a_rect, Color color, BlendMode blend_mode)
{
auto center = a_rect.center();
auto radius_a = a_rect.width() / 2;
auto radius_b = a_rect.height() / 2;
if (radius_a <= 0 || radius_b <= 0)
return;
if (radius_a == radius_b)
return fill_circle(center, radius_a, color, blend_mode);
auto x_paint_range = draw_ellipse_part(center, radius_a, radius_b, color, false, {}, blend_mode);
// FIXME: This paints some extra fill pixels that are clipped
draw_ellipse_part(center, radius_b, radius_a, color, true, x_paint_range, blend_mode);
}
FLATTEN AntiAliasingPainter::Range AntiAliasingPainter::draw_ellipse_part(
IntPoint center, int radius_a, int radius_b, Color color, bool flip_x_and_y, Optional<Range> x_clip, BlendMode blend_mode)
{
/*
Algorithm from: https://cs.uwaterloo.ca/research/tr/1984/CS-84-38.pdf
This method can draw a whole circle with a whole circle in one call using
8-way symmetry, or an ellipse in two calls using 4-way symmetry.
*/
center *= m_underlying_painter.scale();
radius_a *= m_underlying_painter.scale();
radius_b *= m_underlying_painter.scale();
// If this is a ellipse everything can be drawn in one pass with 8 way symmetry
bool const is_circle = radius_a == radius_b;
// These happen to be the same here, but are treated separately in the paper:
// intensity is the fill alpha
int const intensity = 255;
// 0 to subpixel_resolution is the range of alpha values for the circle edges
int const subpixel_resolution = intensity;
// Current pixel address
int i = 0;
int q = radius_b;
// 1st and 2nd order differences of y
int delta_y = 0;
int delta2_y = 0;
int const a_squared = radius_a * radius_a;
int const b_squared = radius_b * radius_b;
// Exact and predicted values of f(i) -- the ellipse equation scaled by subpixel_resolution
int y = subpixel_resolution * radius_b;
int y_hat = 0;
// The value of f(i)*f(i)
int f_squared = y * y;
// 1st and 2nd order differences of f(i)*f(i)
int delta_f_squared = -(static_cast<int64_t>(b_squared) * subpixel_resolution * subpixel_resolution) / a_squared;
int delta2_f_squared = 2 * delta_f_squared;
// edge_intersection_area/subpixel_resolution = percentage of pixel intersected by circle
// (aka the alpha for the pixel)
int edge_intersection_area = 0;
int old_area = edge_intersection_area;
auto predict = [&] {
delta_y += delta2_y;
// y_hat is the predicted value of f(i)
y_hat = y + delta_y;
};
auto minimize = [&] {
// Initialize the minimization
delta_f_squared += delta2_f_squared;
f_squared += delta_f_squared;
int min_squared_error = y_hat * y_hat - f_squared;
int prediction_overshot = 1;
y = y_hat;
// Force error negative
if (min_squared_error > 0) {
min_squared_error = -min_squared_error;
prediction_overshot = -1;
}
// Minimize
int previous_error = min_squared_error;
while (min_squared_error < 0) {
y += prediction_overshot;
previous_error = min_squared_error;
min_squared_error += y + y - prediction_overshot;
}
if (min_squared_error + previous_error > 0)
y -= prediction_overshot;
};
auto correct = [&] {
int error = y - y_hat;
// FIXME: The alpha values seem too low, which makes things look
// overly pointy. This fixes that, though there's probably a better
// solution to be found. (This issue seems to exist in the base algorithm)
error /= 4;
delta2_y += error;
delta_y += error;
};
int min_paint_x = NumericLimits<int>::max();
int max_paint_x = NumericLimits<int>::min();
auto pixel = [&](int x, int y, int alpha) {
if (alpha <= 0 || alpha > 255)
return;
if (flip_x_and_y)
swap(x, y);
if (x_clip.has_value() && x_clip->contains_inclusive(x))
return;
min_paint_x = min(x, min_paint_x);
max_paint_x = max(x, max_paint_x);
alpha = (alpha * color.alpha()) / 255;
if (blend_mode == BlendMode::AlphaSubtract)
alpha = ~alpha;
auto pixel_color = color;
pixel_color.set_alpha(alpha);
m_underlying_painter.set_pixel(center + IntPoint { x, y }, pixel_color, blend_mode == BlendMode::Normal);
};
auto fill = [&](int x, int ymax, int ymin, int alpha) {
while (ymin <= ymax) {
pixel(x, ymin, alpha);
ymin += 1;
}
};
auto symmetric_pixel = [&](int x, int y, int alpha) {
pixel(x, y, alpha);
pixel(x, -y - 1, alpha);
pixel(-x - 1, -y - 1, alpha);
pixel(-x - 1, y, alpha);
if (is_circle) {
pixel(y, x, alpha);
pixel(y, -x - 1, alpha);
pixel(-y - 1, -x - 1, alpha);
pixel(-y - 1, x, alpha);
}
};
// These are calculated incrementally (as it is possibly a tiny bit faster)
int ib_squared = 0;
int qa_squared = q * a_squared;
auto in_symmetric_region = [&] {
// Main fix two stop cond here
return is_circle ? i < q : ib_squared < qa_squared;
};
// Draws a 8 octants for a circle or 4 quadrants for a (partial) ellipse
while (in_symmetric_region()) {
predict();
minimize();
correct();
old_area = edge_intersection_area;
edge_intersection_area += delta_y;
if (edge_intersection_area >= 0) {
// Single pixel on perimeter
symmetric_pixel(i, q, (edge_intersection_area + old_area) / 2);
fill(i, q - 1, -q, intensity);
fill(-i - 1, q - 1, -q, intensity);
} else {
// Two pixels on perimeter
edge_intersection_area += subpixel_resolution;
symmetric_pixel(i, q, old_area / 2);
q -= 1;
qa_squared -= a_squared;
fill(i, q - 1, -q, intensity);
fill(-i - 1, q - 1, -q, intensity);
if (!is_circle || in_symmetric_region()) {
symmetric_pixel(i, q, (edge_intersection_area + subpixel_resolution) / 2);
if (is_circle) {
fill(q, i - 1, -i, intensity);
fill(-q - 1, i - 1, -i, intensity);
}
} else {
edge_intersection_area += subpixel_resolution;
}
}
i += 1;
ib_squared += b_squared;
}
if (is_circle) {
int alpha = edge_intersection_area / 2;
pixel(q, q, alpha);
pixel(-q - 1, q, alpha);
pixel(-q - 1, -q - 1, alpha);
pixel(q, -q - 1, alpha);
}
return Range { min_paint_x, max_paint_x };
}
void AntiAliasingPainter::fill_rect_with_rounded_corners(IntRect const& a_rect, Color color, int radius)
{
fill_rect_with_rounded_corners(a_rect, color, radius, radius, radius, radius);
}
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)
{
fill_rect_with_rounded_corners(a_rect, color,
{ top_left_radius, top_left_radius },
{ top_right_radius, top_right_radius },
{ bottom_right_radius, bottom_right_radius },
{ bottom_left_radius, bottom_left_radius });
}
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)
{
if (!top_left && !top_right && !bottom_right && !bottom_left) {
if (blend_mode == BlendMode::Normal)
return m_underlying_painter.fill_rect(a_rect, color);
else if (blend_mode == BlendMode::AlphaSubtract)
return m_underlying_painter.clear_rect(a_rect, Color());
}
if (color.alpha() == 0)
return;
IntPoint top_left_corner {
a_rect.x() + top_left.horizontal_radius,
a_rect.y() + top_left.vertical_radius,
};
IntPoint top_right_corner {
a_rect.x() + a_rect.width() - top_right.horizontal_radius,
a_rect.y() + top_right.vertical_radius,
};
IntPoint bottom_left_corner {
a_rect.x() + bottom_left.horizontal_radius,
a_rect.y() + a_rect.height() - bottom_left.vertical_radius
};
IntPoint bottom_right_corner {
a_rect.x() + a_rect.width() - bottom_right.horizontal_radius,
a_rect.y() + a_rect.height() - bottom_right.vertical_radius
};
// All corners are centered at the same point, so this can be painted as a single ellipse.
if (top_left_corner == top_right_corner && top_right_corner == bottom_left_corner && bottom_left_corner == bottom_right_corner)
return fill_ellipse(a_rect, color, blend_mode);
IntRect top_rect {
a_rect.x() + top_left.horizontal_radius,
a_rect.y(),
a_rect.width() - top_left.horizontal_radius - top_right.horizontal_radius,
top_left.vertical_radius
};
IntRect right_rect {
a_rect.x() + a_rect.width() - top_right.horizontal_radius,
a_rect.y() + top_right.vertical_radius,
top_right.horizontal_radius,
a_rect.height() - top_right.vertical_radius - bottom_right.vertical_radius
};
IntRect bottom_rect {
a_rect.x() + bottom_left.horizontal_radius,
a_rect.y() + a_rect.height() - bottom_right.vertical_radius,
a_rect.width() - bottom_left.horizontal_radius - bottom_right.horizontal_radius,
bottom_right.vertical_radius
};
IntRect left_rect {
a_rect.x(),
a_rect.y() + top_left.vertical_radius,
bottom_left.horizontal_radius,
a_rect.height() - top_left.vertical_radius - bottom_left.vertical_radius
};
IntRect inner = {
left_rect.x() + left_rect.width(),
left_rect.y(),
a_rect.width() - left_rect.width() - right_rect.width(),
a_rect.height() - top_rect.height() - bottom_rect.height()
};
if (blend_mode == BlendMode::Normal) {
m_underlying_painter.fill_rect(top_rect, color);
m_underlying_painter.fill_rect(right_rect, color);
m_underlying_painter.fill_rect(bottom_rect, color);
m_underlying_painter.fill_rect(left_rect, color);
m_underlying_painter.fill_rect(inner, color);
} else if (blend_mode == BlendMode::AlphaSubtract) {
m_underlying_painter.clear_rect(top_rect, Color());
m_underlying_painter.clear_rect(right_rect, Color());
m_underlying_painter.clear_rect(bottom_rect, Color());
m_underlying_painter.clear_rect(left_rect, Color());
m_underlying_painter.clear_rect(inner, Color());
}
auto fill_corner = [&](auto const& ellipse_center, auto const& corner_point, CornerRadius const& corner) {
PainterStateSaver save { m_underlying_painter };
m_underlying_painter.add_clip_rect(IntRect::from_two_points(ellipse_center, corner_point));
fill_ellipse(IntRect::centered_at(ellipse_center, { corner.horizontal_radius * 2, corner.vertical_radius * 2 }), color, blend_mode);
};
auto bounding_rect = a_rect.inflated(0, 1, 1, 0);
if (top_left)
fill_corner(top_left_corner, bounding_rect.top_left(), top_left);
if (top_right)
fill_corner(top_right_corner, bounding_rect.top_right(), top_right);
if (bottom_left)
fill_corner(bottom_left_corner, bounding_rect.bottom_left(), bottom_left);
if (bottom_right)
fill_corner(bottom_right_corner, bounding_rect.bottom_right(), bottom_right);
}
void AntiAliasingPainter::stroke_segment_intersection(FloatPoint const& current_line_a, FloatPoint const& current_line_b, FloatLine const& previous_line, Color color, float thickness)
{
// FIXME: This is currently drawn in slightly the wrong place most of the time.
// FIXME: This is sometimes drawn when the intersection would not be visible anyway.
// Starting point of the current line is where the last line ended... this is an intersection.
auto intersection = current_line_a;
auto previous_line_b = (previous_line.a());
// If both are straight lines we can simply draw a rectangle at the intersection.
if ((current_line_a.x() == current_line_b.x() || current_line_a.y() == current_line_b.y()) && (previous_line.a().x() == previous_line.b().x() || previous_line.a().y() == previous_line.b().y())) {
intersection = m_transform.map(current_line_a);
// Adjust coordinates to handle rounding offsets.
auto intersection_rect = IntSize(thickness, thickness);
float drawing_edge_offset = fmodf(thickness, 2.0f) < 0.5f && thickness > 3 ? 1 : 0;
auto integer_part = [](float x) { return floorf(x); };
auto round = [&](float x) { return integer_part(x + 0.5f); };
if (thickness == 1)
drawing_edge_offset = -1;
if (current_line_a.x() == current_line_b.x() && previous_line.a().x() == previous_line.b().x()) {
intersection_rect.set_height(1);
}
if (current_line_a.y() == current_line_b.y() && previous_line.a().y() == previous_line.b().y()) {
intersection_rect.set_width(1);
drawing_edge_offset = thickness == 1 ? -1 : 0;
intersection.set_x(intersection.x() - 1 + (thickness == 1 ? 1 : 0));
intersection.set_y(intersection.y() + (thickness > 3 && fmodf(thickness, 2.0f) < 0.5f ? 1 : 0));
}
m_underlying_painter.fill_rect(IntRect::centered_on({ round(intersection.x()) + drawing_edge_offset, round(intersection.y()) + drawing_edge_offset }, intersection_rect), color);
return;
}
float scale_to_move_current = (thickness / 2) / intersection.distance_from(current_line_b);
float scale_to_move_previous = (thickness / 2) / intersection.distance_from(previous_line_b);
// Move the point on the line by half of the thickness.
double offset_current_edge_x = scale_to_move_current * (current_line_b.x() - intersection.x());
double offset_current_edge_y = scale_to_move_current * (current_line_b.y() - intersection.y());
double offset_prev_edge_x = scale_to_move_previous * (previous_line_b.x() - intersection.x());
double offset_prev_edge_y = scale_to_move_previous * (previous_line_b.y() - intersection.y());
// Rotate the point by 90 and 270 degrees to get the points for both edges.
double rad_90deg = 0.5 * M_PI;
FloatPoint current_rotated_90deg = { (offset_current_edge_x * cos(rad_90deg) - offset_current_edge_y * sin(rad_90deg)), (offset_current_edge_x * sin(rad_90deg) + offset_current_edge_y * cos(rad_90deg)) };
FloatPoint current_rotated_270deg = intersection - current_rotated_90deg;
FloatPoint previous_rotated_90deg = { (offset_prev_edge_x * cos(rad_90deg) - offset_prev_edge_y * sin(rad_90deg)), (offset_prev_edge_x * sin(rad_90deg) + offset_prev_edge_y * cos(rad_90deg)) };
FloatPoint previous_rotated_270deg = intersection - previous_rotated_90deg;
// Translate coordinates to the intersection point.
current_rotated_90deg += intersection;
previous_rotated_90deg += intersection;
FloatLine outer_line_current_90 = FloatLine({ current_rotated_90deg, current_line_b - static_cast<FloatPoint>(intersection - current_rotated_90deg) });
FloatLine outer_line_current_270 = FloatLine({ current_rotated_270deg, current_line_b - static_cast<FloatPoint>(intersection - current_rotated_270deg) });
FloatLine outer_line_prev_270 = FloatLine({ previous_rotated_270deg, previous_line_b - static_cast<FloatPoint>(intersection - previous_rotated_270deg) });
FloatLine outer_line_prev_90 = FloatLine({ previous_rotated_90deg, previous_line_b - static_cast<FloatPoint>(intersection - previous_rotated_90deg) });
Optional<FloatPoint> edge_spike_90 = outer_line_current_90.intersected(outer_line_prev_270);
Optional<FloatPoint> edge_spike_270;
if (edge_spike_90.has_value()) {
edge_spike_270 = intersection + (intersection - edge_spike_90.value());
} else {
edge_spike_270 = outer_line_current_270.intersected(outer_line_prev_90);
if (edge_spike_270.has_value()) {
edge_spike_90 = intersection + (intersection - edge_spike_270.value());
}
}
Path intersection_edge_path;
intersection_edge_path.move_to(current_rotated_90deg);
if (edge_spike_90.has_value())
intersection_edge_path.line_to(edge_spike_90.value());
intersection_edge_path.line_to(previous_rotated_270deg);
intersection_edge_path.line_to(current_rotated_270deg);
if (edge_spike_270.has_value())
intersection_edge_path.line_to(edge_spike_270.value());
intersection_edge_path.line_to(previous_rotated_90deg);
intersection_edge_path.close();
fill_path(intersection_edge_path, color);
}
}
|