diff options
author | MacDue <macdue@dueutil.tech> | 2023-05-31 19:02:00 +0100 |
---|---|---|
committer | Andreas Kling <kling@serenityos.org> | 2023-06-01 06:25:00 +0200 |
commit | 48fa8f97d3bc90fa98ecb1883eba6ce19ec26b88 (patch) | |
tree | 5f0251f9881bfc4c59226c27c639a1ea3787066a /Userland | |
parent | e4adaa2d20e98df4e2bdac81df8da94eb935af38 (diff) | |
download | serenity-48fa8f97d3bc90fa98ecb1883eba6ce19ec26b88.zip |
LibGfx: Implement new antialiased filled path rasterizer
This is an implementation of the scanline edge-flag algorithm for
antialiased path filling described here:
https://mlab.taik.fi/~kkallio/antialiasing/EdgeFlagAA.pdf
The initial implementation does not try to implement every possible
optimization in favour of keeping things simple. However, it does
support:
- Both evenodd and nonzero fill rules
- Applying paint styles/gradients
- A range of samples per pixel (8, 16, 32)
- Very nice antialiasing :^)
This replaces the previous path filling code, that only really applied
antialiasing in the x-axis.
There's some very nice improvements around the web with this change,
especially for small icons. Strokes are still a bit wonky, as they don't
yet use this rasterizer, but I think it should be possible to convert
them to do so.
Diffstat (limited to 'Userland')
-rw-r--r-- | Userland/Libraries/LibGfx/AntiAliasingPainter.cpp | 13 | ||||
-rw-r--r-- | Userland/Libraries/LibGfx/CMakeLists.txt | 2 | ||||
-rw-r--r-- | Userland/Libraries/LibGfx/EdgeFlagPathRasterizer.cpp | 345 | ||||
-rw-r--r-- | Userland/Libraries/LibGfx/EdgeFlagPathRasterizer.h | 188 | ||||
-rw-r--r-- | Userland/Libraries/LibGfx/FillPathImplementation.cpp | 299 | ||||
-rw-r--r-- | Userland/Libraries/LibGfx/Painter.h | 13 |
6 files changed, 536 insertions, 324 deletions
diff --git a/Userland/Libraries/LibGfx/AntiAliasingPainter.cpp b/Userland/Libraries/LibGfx/AntiAliasingPainter.cpp index 28e6695e16..684784f2ea 100644 --- a/Userland/Libraries/LibGfx/AntiAliasingPainter.cpp +++ b/Userland/Libraries/LibGfx/AntiAliasingPainter.cpp @@ -207,19 +207,6 @@ void AntiAliasingPainter::draw_line(FloatPoint actual_from, FloatPoint actual_to draw_anti_aliased_line<FixmeEnableHacksForBetterPathPainting::No>(actual_from, actual_to, color, thickness, style, alternate_color, line_length_mode); } -// FIXME: In the fill_paths() m_transform.translation() throws away any other transforms -// this currently does not matter -- but may in future. - -void AntiAliasingPainter::fill_path(Path const& path, Color color, Painter::WindingRule rule) -{ - m_underlying_painter.antialiased_fill_path(path, color, rule, m_transform.translation()); -} - -void AntiAliasingPainter::fill_path(Path const& path, PaintStyle const& paint_style, Painter::WindingRule rule) -{ - m_underlying_painter.antialiased_fill_path(path, paint_style, rule, m_transform.translation()); -} - void AntiAliasingPainter::stroke_path(Path const& path, Color color, float thickness) { FloatPoint cursor; diff --git a/Userland/Libraries/LibGfx/CMakeLists.txt b/Userland/Libraries/LibGfx/CMakeLists.txt index ab64783da7..30193191e3 100644 --- a/Userland/Libraries/LibGfx/CMakeLists.txt +++ b/Userland/Libraries/LibGfx/CMakeLists.txt @@ -8,7 +8,7 @@ set(SOURCES Color.cpp CursorParams.cpp DeltaE.cpp - FillPathImplementation.cpp + EdgeFlagPathRasterizer.cpp Filters/ColorBlindnessFilter.cpp Filters/FastBoxBlurFilter.cpp Filters/LumaFilter.cpp diff --git a/Userland/Libraries/LibGfx/EdgeFlagPathRasterizer.cpp b/Userland/Libraries/LibGfx/EdgeFlagPathRasterizer.cpp new file mode 100644 index 0000000000..488e469f82 --- /dev/null +++ b/Userland/Libraries/LibGfx/EdgeFlagPathRasterizer.cpp @@ -0,0 +1,345 @@ +/* + * Copyright (c) 2023, MacDue <macdue@dueutil.tech> + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include <AK/Array.h> +#include <AK/IntegralMath.h> +#include <AK/Types.h> +#include <LibGfx/AntiAliasingPainter.h> +#include <LibGfx/EdgeFlagPathRasterizer.h> + +#if defined(AK_COMPILER_GCC) +# pragma GCC optimize("O3") +#endif + +// This a pretty naive implementation of edge-flag scanline AA. +// The paper lists many possible optimizations, maybe implement one? (FIXME!) +// https://mlab.taik.fi/~kkallio/antialiasing/EdgeFlagAA.pdf +// This currently implements: +// - The scanline buffer optimization (only allocate one scanline) +// Possible other optimizations according to the paper: +// - Using fixed point numbers +// - Edge tracking +// - Mask tracking +// - Loop unrolling (compilers might handle this better now, the paper is from 2007) +// Optimizations I think we could add: +// - Using fast_u32_fills() for runs of solid colors +// - Clipping the plotted edges earlier + +namespace Gfx { + +static Vector<Detail::Edge> prepare_edges(ReadonlySpan<Path::SplitLineSegment> lines, unsigned samples_per_pixel, FloatPoint origin) +{ + // FIXME: split_lines() gives similar information, but the form it's in is not that useful (and is const anyway). + Vector<Detail::Edge> edges; + edges.ensure_capacity(lines.size()); + + for (auto& line : lines) { + auto p0 = line.from - origin; + auto p1 = line.to - origin; + + p0.scale_by(1, samples_per_pixel); + p1.scale_by(1, samples_per_pixel); + + i8 winding = -1; + if (p0.y() > p1.y()) { + swap(p0, p1); + } else { + winding = 1; + } + + if (p0.y() == p1.y()) + continue; + + auto dx = p1.x() - p0.x(); + auto dy = p1.y() - p0.y(); + float dxdy = float(dx) / dy; + float x = p0.x(); + edges.unchecked_append(Detail::Edge { + x, + static_cast<int>(p0.y()), + static_cast<int>(p1.y()), + dxdy, + winding, + nullptr }); + } + return edges; +} + +template<unsigned SamplesPerPixel> +EdgeFlagPathRasterizer<SamplesPerPixel>::EdgeFlagPathRasterizer(IntSize size) + : m_size(size.width() + 1, size.height() + 1) +{ + m_scanline.resize(m_size.width()); + m_edge_table.resize(m_size.height()); +} + +template<unsigned SamplesPerPixel> +void EdgeFlagPathRasterizer<SamplesPerPixel>::fill(Painter& painter, Path const& path, Color color, Painter::WindingRule winding_rule, FloatPoint offset) +{ + fill_internal(painter, path, color, winding_rule, offset); +} + +template<unsigned SamplesPerPixel> +void EdgeFlagPathRasterizer<SamplesPerPixel>::fill(Painter& painter, Path const& path, PaintStyle const& style, Painter::WindingRule winding_rule, FloatPoint offset) +{ + style.paint(enclosing_int_rect(path.bounding_box()), [&](PaintStyle::SamplerFunction sampler) { + fill_internal(painter, path, move(sampler), winding_rule, offset); + }); +} + +template<unsigned SamplesPerPixel> +void EdgeFlagPathRasterizer<SamplesPerPixel>::fill_internal(Painter& painter, Path const& path, auto color_or_function, Painter::WindingRule winding_rule, FloatPoint offset) +{ + // FIXME: Figure out how painter scaling works here... + VERIFY(painter.scale() == 1); + + auto bounding_box = enclosing_int_rect(path.bounding_box().translated(offset)); + auto dest_rect = bounding_box.translated(painter.translation()); + auto origin = bounding_box.top_left().to_type<float>() - offset; + m_blit_origin = dest_rect.top_left(); + m_clip = dest_rect.intersected(painter.clip_rect()); + + if (m_clip.is_empty()) + return; + + auto& lines = path.split_lines(); + if (lines.is_empty()) + return; + + auto edges = prepare_edges(lines, SamplesPerPixel, origin); + + int min_scanline = m_size.height(); + int max_scanline = 0; + for (auto& edge : edges) { + int start_scanline = edge.min_y / SamplesPerPixel; + int end_scanline = edge.max_y / SamplesPerPixel; + + // Create a linked-list of edges starting on this scanline: + edge.next_edge = m_edge_table[start_scanline]; + m_edge_table[start_scanline] = &edge; + + min_scanline = min(min_scanline, start_scanline); + max_scanline = max(max_scanline, end_scanline); + } + + Detail::Edge* active_edges = nullptr; + + // FIXME: We could probably clip some of the egde plotting if we know it won't be shown. + // Though care would have to be taken to ensure the active edges are correct at the first drawn scaline. + if (winding_rule == Painter::WindingRule::EvenOdd) { + auto plot_edge = [&](Detail::Edge& edge, int start_subpixel_y, int end_subpixel_y) { + for (int y = start_subpixel_y; y < end_subpixel_y; y++) { + int xi = static_cast<int>(edge.x + SubpixelSample::nrooks_subpixel_offsets[y]); + SampleType sample = 1 << y; + m_scanline[xi] ^= sample; + edge.x += edge.dxdy; + } + }; + for (int scanline = min_scanline; scanline <= max_scanline; scanline++) { + active_edges = plot_edges_for_scanline(scanline, plot_edge, active_edges); + accumulate_even_odd_scanline(painter, scanline, color_or_function); + } + } else { + VERIFY(winding_rule == Painter::WindingRule::Nonzero); + // Only allocate the winding buffer if needed. + // NOTE: non-zero fills are a fair bit less efficient. So if you can do an even-odd fill do that :^) + if (m_windings.is_empty()) + m_windings.resize(m_size.width()); + + auto plot_edge = [&](Detail::Edge& edge, int start_subpixel_y, int end_subpixel_y) { + for (int y = start_subpixel_y; y < end_subpixel_y; y++) { + int xi = static_cast<int>(edge.x + SubpixelSample::nrooks_subpixel_offsets[y]); + SampleType sample = 1 << y; + m_scanline[xi] |= sample; + m_windings[xi].counts[y] += edge.winding; + edge.x += edge.dxdy; + } + }; + for (int scanline = min_scanline; scanline <= max_scanline; scanline++) { + active_edges = plot_edges_for_scanline(scanline, plot_edge, active_edges); + accumulate_non_zero_scanline(painter, scanline, color_or_function); + } + } +} + +template<unsigned SamplesPerPixel> +Color EdgeFlagPathRasterizer<SamplesPerPixel>::scanline_color(int scanline, int offset, u8 alpha, auto& color_or_function) +{ + using ColorOrFunction = decltype(color_or_function); + constexpr bool has_constant_color = IsSame<RemoveCVReference<ColorOrFunction>, Color>; + auto color = [&] { + if constexpr (has_constant_color) { + return color_or_function; + } else { + return color_or_function({ offset, scanline }); + } + }(); + return color.with_alpha(color.alpha() * alpha / 255); +} + +template<unsigned SamplesPerPixel> +Detail::Edge* EdgeFlagPathRasterizer<SamplesPerPixel>::plot_edges_for_scanline(int scanline, auto plot_edge, Detail::Edge* active_edges) +{ + auto y_subpixel = [](int y) { + return y & (SamplesPerPixel - 1); + }; + + auto* current_edge = active_edges; + Detail::Edge* prev_edge = nullptr; + + // First iterate over the edge in the active edge table, these are edges added on earlier scanlines, + // that have not yet reached their end scanline. + while (current_edge) { + int end_scanline = current_edge->max_y / SamplesPerPixel; + if (scanline == end_scanline) { + // This edge ends this scanline. + plot_edge(*current_edge, 0, y_subpixel(current_edge->max_y)); + // Remove this edge from the AET + current_edge = current_edge->next_edge; + if (prev_edge) + prev_edge->next_edge = current_edge; + else + active_edges = current_edge; + } else { + // This egde sticks around for a few more scanlines. + plot_edge(*current_edge, 0, SamplesPerPixel); + prev_edge = current_edge; + current_edge = current_edge->next_edge; + } + } + + // Next, iterate over new edges for this line. If active_edges was null this also becomes the new + // AET. Edges new will be appended here. + current_edge = m_edge_table[scanline]; + while (current_edge) { + int end_scanline = current_edge->max_y / SamplesPerPixel; + if (scanline == end_scanline) { + // This edge will end this scanlines (no need to add to AET). + plot_edge(*current_edge, y_subpixel(current_edge->min_y), y_subpixel(current_edge->max_y)); + } else { + // This edge will live on for a few more scanlines. + plot_edge(*current_edge, y_subpixel(current_edge->min_y), SamplesPerPixel); + // Add this edge to the AET + if (prev_edge) + prev_edge->next_edge = current_edge; + else + active_edges = current_edge; + prev_edge = current_edge; + } + current_edge = current_edge->next_edge; + } + + m_edge_table[scanline] = nullptr; + return active_edges; +} + +template<unsigned SamplesPerPixel> +void EdgeFlagPathRasterizer<SamplesPerPixel>::write_pixel(Painter& painter, int scanline, int offset, SampleType sample, auto& color_or_function) +{ + auto dest = IntPoint { offset, scanline } + m_blit_origin; + if (!m_clip.contains_horizontally(dest.x())) + return; + // FIXME: We could detect runs of full coverage and use fast_u32_fills for those rather than many set_pixel() calls. + auto coverage = SubpixelSample::compute_coverage(sample); + if (coverage) { + auto paint_color = scanline_color(scanline, offset, coverage_to_alpha(coverage), color_or_function); + painter.set_physical_pixel(dest, paint_color, true); + } +} + +template<unsigned SamplesPerPixel> +void EdgeFlagPathRasterizer<SamplesPerPixel>::accumulate_even_odd_scanline(Painter& painter, int scanline, auto& color_or_function) +{ + auto dest_y = m_blit_origin.y() + scanline; + if (!m_clip.contains_vertically(dest_y)) { + // FIXME: This memset only really needs to be done on transition from clipped to not clipped, + // or not at all if we properly clipped egde plotting. + memset(m_scanline.data(), 0, sizeof(SampleType) * m_scanline.size()); + return; + } + + SampleType sample = 0; + for (int x = 0; x < m_size.width(); x += 1) { + sample ^= m_scanline[x]; + write_pixel(painter, scanline, x, sample, color_or_function); + m_scanline[x] = 0; + } +} + +template<unsigned SamplesPerPixel> +void EdgeFlagPathRasterizer<SamplesPerPixel>::accumulate_non_zero_scanline(Painter& painter, int scanline, auto& color_or_function) +{ + // NOTE: Same FIXMEs apply from accumulate_even_odd_scanline() + auto dest_y = m_blit_origin.y() + scanline; + if (!m_clip.contains_vertically(dest_y)) { + memset(m_scanline.data(), 0, sizeof(SampleType) * m_scanline.size()); + memset(m_windings.data(), 0, sizeof(WindingCounts) * m_windings.size()); + return; + } + + SampleType sample = 0; + WindingCounts sum_winding = {}; + for (int x = 0; x < m_size.width(); x += 1) { + if (auto edges = m_scanline[x]) { + // We only need to process the windings when we hit some edges. + for (auto y_sub = 0u; y_sub < SamplesPerPixel; y_sub++) { + auto subpixel_bit = 1 << y_sub; + if (edges & subpixel_bit) { + auto winding = m_windings[x].counts[y_sub]; + auto previous_winding_count = sum_winding.counts[y_sub]; + sum_winding.counts[y_sub] += winding; + // Toggle fill on change to/from zero + if ((previous_winding_count == 0 && sum_winding.counts[y_sub] != 0) + || (sum_winding.counts[y_sub] == 0 && previous_winding_count != 0)) { + sample ^= subpixel_bit; + } + } + } + } + write_pixel(painter, scanline, x, sample, color_or_function); + m_scanline[x] = 0; + m_windings[x] = {}; + } +} + +static IntSize path_bounds(Gfx::Path const& path) +{ + return enclosing_int_rect(path.bounding_box()).size(); +} + +// Note: The AntiAliasingPainter and Painter now perform the same antialiasing, +// since it would be harder to turn it off for the standard painter. +// The samples are reduced to 8 for Gfx::Painter though as a "speedy" option. + +void Painter::fill_path(Path const& path, Color color, WindingRule winding_rule) +{ + EdgeFlagPathRasterizer<8> rasterizer(path_bounds(path)); + rasterizer.fill(*this, path, color, winding_rule); +} + +void Painter::fill_path(Path const& path, PaintStyle const& paint_style, Painter::WindingRule winding_rule) +{ + EdgeFlagPathRasterizer<8> rasterizer(path_bounds(path)); + rasterizer.fill(*this, path, paint_style, winding_rule); +} + +void AntiAliasingPainter::fill_path(Path const& path, Color color, Painter::WindingRule winding_rule) +{ + EdgeFlagPathRasterizer<32> rasterizer(path_bounds(path)); + rasterizer.fill(m_underlying_painter, path, color, winding_rule, m_transform.translation()); +} + +void AntiAliasingPainter::fill_path(Path const& path, PaintStyle const& paint_style, Painter::WindingRule winding_rule) +{ + EdgeFlagPathRasterizer<32> rasterizer(path_bounds(path)); + rasterizer.fill(m_underlying_painter, path, paint_style, winding_rule, m_transform.translation()); +} + +template class EdgeFlagPathRasterizer<8>; +template class EdgeFlagPathRasterizer<16>; +template class EdgeFlagPathRasterizer<32>; + +} diff --git a/Userland/Libraries/LibGfx/EdgeFlagPathRasterizer.h b/Userland/Libraries/LibGfx/EdgeFlagPathRasterizer.h new file mode 100644 index 0000000000..6c80e974f2 --- /dev/null +++ b/Userland/Libraries/LibGfx/EdgeFlagPathRasterizer.h @@ -0,0 +1,188 @@ +/* + * Copyright (c) 2023, MacDue <macdue@dueutil.tech> + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include <AK/Array.h> +#include <AK/GenericShorthands.h> +#include <AK/Vector.h> +#include <LibGfx/Bitmap.h> +#include <LibGfx/PaintStyle.h> +#include <LibGfx/Painter.h> +#include <LibGfx/Path.h> + +namespace Gfx { + +namespace Detail { + +static auto constexpr coverage_lut = [] { + Array<u8, 256> coverage_lut {}; + for (u32 sample = 0; sample <= 255; sample++) + coverage_lut[sample] = popcount(sample); + return coverage_lut; +}(); + +template<unsigned SamplesPerPixel> +struct Sample { + static_assert(!first_is_one_of(SamplesPerPixel, 8u, 16u, 32u), "EdgeFlagPathRasterizer: Invalid samples per pixel!"); +}; + +// See paper for diagrams for how these offsets work, but they allow for nicely spread out samples in each pixel. +template<> +struct Sample<8> { + using Type = u8; + static constexpr Array nrooks_subpixel_offsets { + (5.0f / 8.0f), + (0.0f / 8.0f), + (3.0f / 8.0f), + (6.0f / 8.0f), + (1.0f / 8.0f), + (4.0f / 8.0f), + (7.0f / 8.0f), + (2.0f / 8.0f), + }; + + static u8 compute_coverage(Type sample) + { + return coverage_lut[sample]; + } +}; + +template<> +struct Sample<16> { + using Type = u16; + static constexpr Array nrooks_subpixel_offsets { + (1.0f / 16.0f), + (8.0f / 16.0f), + (4.0f / 16.0f), + (15.0f / 16.0f), + (11.0f / 16.0f), + (2.0f / 16.0f), + (6.0f / 16.0f), + (14.0f / 16.0f), + (10.0f / 16.0f), + (3.0f / 16.0f), + (7.0f / 16.0f), + (12.0f / 16.0f), + (0.0f / 16.0f), + (9.0f / 16.0f), + (5.0f / 16.0f), + (13.0f / 16.0f), + }; + + static u8 compute_coverage(Type sample) + { + return ( + coverage_lut[(sample >> 0) & 0xff] + + coverage_lut[(sample >> 8) & 0xff]); + } +}; + +template<> +struct Sample<32> { + using Type = u32; + static constexpr Array nrooks_subpixel_offsets { + (28.0f / 32.0f), + (13.0f / 32.0f), + (6.0f / 32.0f), + (23.0f / 32.0f), + (0.0f / 32.0f), + (17.0f / 32.0f), + (10.0f / 32.0f), + (27.0f / 32.0f), + (4.0f / 32.0f), + (21.0f / 32.0f), + (14.0f / 32.0f), + (31.0f / 32.0f), + (8.0f / 32.0f), + (25.0f / 32.0f), + (18.0f / 32.0f), + (3.0f / 32.0f), + (12.0f / 32.0f), + (29.0f / 32.0f), + (22.0f / 32.0f), + (7.0f / 32.0f), + (16.0f / 32.0f), + (1.0f / 32.0f), + (26.0f / 32.0f), + (11.0f / 32.0f), + (20.0f / 32.0f), + (5.0f / 32.0f), + (30.0f / 32.0f), + (15.0f / 32.0f), + (24.0f / 32.0f), + (9.0f / 32.0f), + (2.0f / 32.0f), + (19.0f / 32.0f), + }; + + static u8 compute_coverage(Type sample) + { + return ( + coverage_lut[(sample >> 0) & 0xff] + + coverage_lut[(sample >> 8) & 0xff] + + coverage_lut[(sample >> 16) & 0xff] + + coverage_lut[(sample >> 24) & 0xff]); + } +}; + +struct Edge { + float x; + int min_y; + int max_y; + float dxdy; + i8 winding; + Edge* next_edge; +}; + +} + +template<unsigned SamplesPerPixel = 32> +class EdgeFlagPathRasterizer { +public: + EdgeFlagPathRasterizer(IntSize); + + void fill(Painter&, Path const&, Color, Painter::WindingRule, FloatPoint offset = {}); + void fill(Painter&, Path const&, PaintStyle const&, Painter::WindingRule, FloatPoint offset = {}); + +private: + using SubpixelSample = Detail::Sample<SamplesPerPixel>; + using SampleType = typename SubpixelSample::Type; + + static u8 coverage_to_alpha(u8 coverage) + { + constexpr auto alpha_shift = AK::log2(256 / SamplesPerPixel); + if (!coverage) + return 0; + return (coverage << alpha_shift) - 1; + } + + void fill_internal(Painter&, Path const&, auto color_or_function, Painter::WindingRule, FloatPoint offset); + Detail::Edge* plot_edges_for_scanline(int scanline, auto plot_edge, Detail::Edge* active_edges = nullptr); + void accumulate_even_odd_scanline(Painter&, int scanline, auto& color_or_function); + void accumulate_non_zero_scanline(Painter&, int scanline, auto& color_or_function); + Color scanline_color(int scanline, int offset, u8 alpha, auto& color_or_function); + void write_pixel(Painter&, int scanline, int offset, SampleType sample, auto& color_or_function); + + struct WindingCounts { + // NOTE: This only allows up to 256 winding levels. Increase this if required (i.e. to an i16). + i8 counts[SamplesPerPixel]; + }; + + IntSize m_size; + IntPoint m_blit_origin; + IntRect m_clip; + + Vector<SampleType> m_scanline; + Vector<WindingCounts> m_windings; + Vector<Detail::Edge*> m_edge_table; +}; + +extern template class EdgeFlagPathRasterizer<8>; +extern template class EdgeFlagPathRasterizer<16>; +extern template class EdgeFlagPathRasterizer<32>; + +} diff --git a/Userland/Libraries/LibGfx/FillPathImplementation.cpp b/Userland/Libraries/LibGfx/FillPathImplementation.cpp deleted file mode 100644 index cc89763835..0000000000 --- a/Userland/Libraries/LibGfx/FillPathImplementation.cpp +++ /dev/null @@ -1,299 +0,0 @@ -/* - * Copyright (c) 2021, Ali Mohammad Pur <mpfard@serenityos.org> - * Copyright (c) 2023, MacDue <macdue@dueutil.tech> - * - * SPDX-License-Identifier: BSD-2-Clause - */ - -#include <AK/Debug.h> -#include <AK/QuickSort.h> -#include <LibGfx/Color.h> -#include <LibGfx/Painter.h> -#include <LibGfx/Path.h> - -#if defined(AK_COMPILER_GCC) -# pragma GCC optimize("O3") -#endif - -namespace Gfx { - -template<typename T, typename TColorOrFunction> -ALWAYS_INLINE void Painter::draw_scanline_for_fill_path(int y, T x_start, T x_end, TColorOrFunction color) -{ - // Fill path should scale the scanlines before calling this. - VERIFY(scale() == 1); - - constexpr bool is_floating_point = IsSameIgnoringCV<T, float>; - constexpr bool has_constant_color = IsSameIgnoringCV<TColorOrFunction, Color>; - - int x1 = 0; - int x2 = 0; - u8 left_subpixel_alpha = 0; - u8 right_subpixel_alpha = 0; - if constexpr (is_floating_point) { - x1 = ceilf(x_start); - x2 = floorf(x_end); - left_subpixel_alpha = (x1 - x_start) * 255; - right_subpixel_alpha = (x_end - x2) * 255; - x1 -= left_subpixel_alpha > 0; - x2 += right_subpixel_alpha > 0; - } else { - x1 = x_start; - x2 = x_end; - } - - IntRect scanline(x1, y, x2 - x1, 1); - scanline = scanline.translated(translation()); - auto clipped = scanline.intersected(clip_rect()); - if (clipped.is_empty()) - return; - - auto get_color = [&](int offset) { - if constexpr (has_constant_color) { - return color; - } else { - return color(offset); - } - }; - - if constexpr (is_floating_point) { - // Paint left and right subpixels (then remove them from the scanline). - auto get_color_with_alpha = [&](int offset, u8 alpha) { - auto color_at_offset = get_color(offset); - u8 color_alpha = (alpha * color_at_offset.alpha()) / 255; - return color_at_offset.with_alpha(color_alpha); - }; - bool paint_left_subpixel = clipped.left() == scanline.left() && left_subpixel_alpha; - bool paint_right_subpixel = clipped.right() == scanline.right() && right_subpixel_alpha; - if (paint_left_subpixel) - set_physical_pixel(clipped.top_left(), get_color_with_alpha(0, left_subpixel_alpha), true); - if (paint_right_subpixel) - set_physical_pixel(clipped.top_right().moved_left(1), get_color_with_alpha(scanline.width(), right_subpixel_alpha), true); - clipped.shrink(0, paint_right_subpixel, 0, paint_left_subpixel); - if (clipped.is_empty()) - return; - } - - if constexpr (has_constant_color) { - if (color.alpha() == 255) { - // Speedy path: Constant color and no alpha blending. - fast_u32_fill(m_target->scanline(clipped.y()) + clipped.x(), color.value(), clipped.width()); - return; - } - } - - for (int x = clipped.x(); x < clipped.right(); x++) - set_physical_pixel({ x, clipped.y() }, get_color(x - scanline.x()), true); -} - -[[maybe_unused]] inline void approximately_place_on_int_grid(FloatPoint ffrom, FloatPoint fto, IntPoint& from, IntPoint& to, Optional<IntPoint> previous_to) -{ - auto diffs = fto - ffrom; - // Truncate all first (round down). - from = ffrom.to_type<int>(); - to = fto.to_type<int>(); - // There are 16 possible configurations, by deciding to round each - // coord up or down (and there are four coords, from.x from.y to.x to.y) - // we will simply choose one which most closely matches the correct slope - // with the following heuristic: - // - if the x diff is positive or zero (that is, a right-to-left slant), round 'from.x' up and 'to.x' down. - // - if the x diff is negative (that is, a left-to-right slant), round 'from.x' down and 'to.x' up. - // Note that we do not need to touch the 'y' attribute, as that is our scanline. - if (diffs.x() >= 0) { - from.set_x(from.x() + 1); - } else { - to.set_x(to.x() + 1); - } - if (previous_to.has_value() && from.x() != previous_to.value().x()) // The points have to line up, since we're using these lines to fill a shape. - from.set_x(previous_to.value().x()); -} - -template<Painter::FillPathMode fill_path_mode, typename ColorOrFunction> -void Painter::fill_path_impl(Path const& path, ColorOrFunction color, Gfx::Painter::WindingRule winding_rule, Optional<FloatPoint> offset) -{ - using GridCoordinateType = Conditional<fill_path_mode == FillPathMode::PlaceOnIntGrid, int, float>; - using PointType = Point<GridCoordinateType>; - - auto draw_scanline = [&](int y, GridCoordinateType x1, GridCoordinateType x2) { - const auto draw_offset = offset.value_or({ 0, 0 }); - // Note: .to_floored() is used here to be consistent with enclosing_int_rect() - const auto draw_origin = (path.bounding_box().top_left() + draw_offset).to_floored<int>(); - // FIMXE: Offset is added here to handle floating point translations in the AA painter, - // really this should be done there but this function is a bit too specialised. - y = floorf(y + draw_offset.y()); - x1 += draw_offset.x(); - x2 += draw_offset.x(); - if (x1 > x2) - swap(x1, x2); - if constexpr (IsSameIgnoringCV<ColorOrFunction, Color>) { - draw_scanline_for_fill_path(y, x1, x2, color); - } else { - draw_scanline_for_fill_path(y, x1, x2, [&](int offset) { - return color(IntPoint(x1 + offset, y) - draw_origin); - }); - } - }; - - auto const& segments = path.split_lines(); - - if (segments.size() == 0) - return; - - Vector<Path::SplitLineSegment> active_list; - active_list.ensure_capacity(segments.size()); - - // first, grab the segments for the very first scanline - GridCoordinateType first_y = path.bounding_box().bottom(); - GridCoordinateType last_y = path.bounding_box().top() - 1; - float scanline = first_y; - - size_t last_active_segment { 0 }; - - for (auto& segment : segments) { - if (segment.maximum_y != scanline) - break; - active_list.append(segment); - ++last_active_segment; - } - - auto is_inside_shape = [winding_rule](int winding_number) { - if (winding_rule == Gfx::Painter::WindingRule::Nonzero) - return winding_number != 0; - - if (winding_rule == Gfx::Painter::WindingRule::EvenOdd) - return winding_number % 2 == 0; - - VERIFY_NOT_REACHED(); - }; - - auto increment_winding = [winding_rule](int& winding_number, PointType const& from, PointType const& to) { - if (winding_rule == Gfx::Painter::WindingRule::EvenOdd) { - ++winding_number; - return; - } - - if (winding_rule == Gfx::Painter::WindingRule::Nonzero) { - if (from.dy_relative_to(to) < 0) - ++winding_number; - else - --winding_number; - return; - } - - VERIFY_NOT_REACHED(); - }; - - while (scanline >= last_y) { - Optional<PointType> previous_to; - if (active_list.size()) { - // sort the active list by 'x' from right to left - quick_sort(active_list, [](auto const& line0, auto const& line1) { - return line1.x < line0.x; - }); - if constexpr (fill_path_mode == FillPathMode::PlaceOnIntGrid && FILL_PATH_DEBUG) { - if ((int)scanline % 10 == 0) { - draw_text(Gfx::Rect<GridCoordinateType>(active_list.last().x - 20, scanline, 20, 10), DeprecatedString::number((int)scanline)); - } - } - - if (active_list.size() > 1) { - auto winding_number { winding_rule == Gfx::Painter::WindingRule::Nonzero ? 1 : 0 }; - for (size_t i = 1; i < active_list.size(); ++i) { - auto& previous = active_list[i - 1]; - auto& current = active_list[i]; - - PointType from, to; - PointType truncated_from { previous.x, scanline }; - PointType truncated_to { current.x, scanline }; - if constexpr (fill_path_mode == FillPathMode::PlaceOnIntGrid) { - approximately_place_on_int_grid({ previous.x, scanline }, { current.x, scanline }, from, to, previous_to); - } else { - from = truncated_from; - to = truncated_to; - } - - if (is_inside_shape(winding_number)) { - // The points between this segment and the previous are - // inside the shape - - dbgln_if(FILL_PATH_DEBUG, "y={}: {} at {}: {} -- {}", scanline, winding_number, i, from, to); - draw_scanline(floorf(scanline), from.x(), to.x()); - } - - auto is_passing_through_maxima = scanline == previous.maximum_y - || scanline == previous.minimum_y - || scanline == current.maximum_y - || scanline == current.minimum_y; - - auto is_passing_through_vertex = false; - - if (is_passing_through_maxima) { - is_passing_through_vertex = previous.x == current.x; - } - - if (!is_passing_through_vertex || previous.inverse_slope * current.inverse_slope < 0) - increment_winding(winding_number, truncated_from, truncated_to); - - // update the x coord - active_list[i - 1].x -= active_list[i - 1].inverse_slope; - } - active_list.last().x -= active_list.last().inverse_slope; - } else { - auto point = PointType(active_list[0].x, scanline); - draw_scanline(floorf(scanline), point.x(), point.x()); - - // update the x coord - active_list.first().x -= active_list.first().inverse_slope; - } - } - - --scanline; - // remove any edge that goes out of bound from the active list - for (size_t i = 0, count = active_list.size(); i < count; ++i) { - if (scanline <= active_list[i].minimum_y) { - active_list.remove(i); - --count; - --i; - } - } - for (size_t j = last_active_segment; j < segments.size(); ++j, ++last_active_segment) { - auto& segment = segments[j]; - if (segment.maximum_y < scanline) - break; - if (segment.minimum_y >= scanline) - continue; - - active_list.append(segment); - } - } -} - -void Painter::fill_path(Path const& path, Color color, WindingRule winding_rule) -{ - VERIFY(scale() == 1); // FIXME: Add scaling support. - fill_path_impl<FillPathMode::PlaceOnIntGrid>(path, color, winding_rule); -} - -void Painter::fill_path(Path const& path, PaintStyle const& paint_style, Painter::WindingRule rule) -{ - VERIFY(scale() == 1); // FIXME: Add scaling support. - paint_style.paint(enclosing_int_rect(path.bounding_box()), [&](PaintStyle::SamplerFunction sampler) { - fill_path_impl<FillPathMode::PlaceOnIntGrid>(path, move(sampler), rule); - }); -} - -void Painter::antialiased_fill_path(Path const& path, Color color, WindingRule rule, FloatPoint translation) -{ - VERIFY(scale() == 1); // FIXME: Add scaling support. - fill_path_impl<FillPathMode::AllowFloatingPoints>(path, color, rule, translation); -} - -void Painter::antialiased_fill_path(Path const& path, PaintStyle const& paint_style, WindingRule rule, FloatPoint translation) -{ - VERIFY(scale() == 1); // FIXME: Add scaling support. - paint_style.paint(enclosing_int_rect(path.bounding_box()), [&](PaintStyle::SamplerFunction sampler) { - fill_path_impl<FillPathMode::AllowFloatingPoints>(path, move(sampler), rule, translation); - }); -} - -} diff --git a/Userland/Libraries/LibGfx/Painter.h b/Userland/Libraries/LibGfx/Painter.h index 9ee3b68c16..cd668af928 100644 --- a/Userland/Libraries/LibGfx/Painter.h +++ b/Userland/Libraries/LibGfx/Painter.h @@ -187,6 +187,8 @@ public: protected: friend GradientLine; friend AntiAliasingPainter; + template<unsigned SamplesPerPixel> + friend class EdgeFlagPathRasterizer; IntRect to_physical(IntRect const& r) const { return r.translated(translation()) * scale(); } IntPoint to_physical(IntPoint p) const { return p.translated(translation()) * scale(); } @@ -219,17 +221,6 @@ private: bool text_contains_bidirectional_text(Utf8View const&, TextDirection); template<typename DrawGlyphFunction> void do_draw_text(FloatRect const&, Utf8View const& text, Font const&, TextAlignment, TextElision, TextWrapping, DrawGlyphFunction); - - void antialiased_fill_path(Path const&, Color, WindingRule rule, FloatPoint translation); - void antialiased_fill_path(Path const&, PaintStyle const& paint_style, WindingRule rule, FloatPoint translation); - enum class FillPathMode { - PlaceOnIntGrid, - AllowFloatingPoints, - }; - template<typename T, typename TColorOrFunction> - void draw_scanline_for_fill_path(int y, T x_start, T x_end, TColorOrFunction color); - template<FillPathMode fill_path_mode, typename ColorOrFunction> - void fill_path_impl(Path const& path, ColorOrFunction color, Gfx::Painter::WindingRule winding_rule, Optional<FloatPoint> offset = {}); }; class PainterStateSaver { |