mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2025-04-23 04:55:15 +00:00
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.
This commit is contained in:
parent
e4adaa2d20
commit
48fa8f97d3
Notes:
sideshowbarker
2024-07-17 05:18:58 +09:00
Author: https://github.com/MacDue Commit: https://github.com/SerenityOS/serenity/commit/48fa8f97d3 Pull-request: https://github.com/SerenityOS/serenity/pull/19191
6 changed files with 536 additions and 324 deletions
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
345
Userland/Libraries/LibGfx/EdgeFlagPathRasterizer.cpp
Normal file
345
Userland/Libraries/LibGfx/EdgeFlagPathRasterizer.cpp
Normal file
|
@ -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>;
|
||||
|
||||
}
|
188
Userland/Libraries/LibGfx/EdgeFlagPathRasterizer.h
Normal file
188
Userland/Libraries/LibGfx/EdgeFlagPathRasterizer.h
Normal file
|
@ -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>;
|
||||
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
|
@ -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 {
|
||||
|
|
Loading…
Add table
Reference in a new issue