From 0828c75e5762fcbbc601293b6daba2fb95536a9c Mon Sep 17 00:00:00 2001 From: Davipb Date: Sun, 20 Jun 2021 10:41:47 -0300 Subject: [PATCH] PixelPaint: Add a Mask class The Mask class represents an opacity mask over a rectangular section of an image, linking every pixel to an alpha value ranging from 0 (not selected) to 255 (fully selected). "Partially selected" pixels can be used to simulate anti-aliased curves. This class will be used as the basis for the new non-rectangular selection feature. --- .../Applications/PixelPaint/CMakeLists.txt | 1 + Userland/Applications/PixelPaint/Mask.cpp | 118 ++++++++++++++++++ Userland/Applications/PixelPaint/Mask.h | 105 ++++++++++++++++ 3 files changed, 224 insertions(+) create mode 100644 Userland/Applications/PixelPaint/Mask.cpp create mode 100644 Userland/Applications/PixelPaint/Mask.h diff --git a/Userland/Applications/PixelPaint/CMakeLists.txt b/Userland/Applications/PixelPaint/CMakeLists.txt index 5fba876951..117dda935b 100644 --- a/Userland/Applications/PixelPaint/CMakeLists.txt +++ b/Userland/Applications/PixelPaint/CMakeLists.txt @@ -28,6 +28,7 @@ set(SOURCES PixelPaintWindowGML.h RectangleTool.cpp RectangleSelectTool.cpp + Mask.cpp Selection.cpp SprayTool.cpp ToolboxWidget.cpp diff --git a/Userland/Applications/PixelPaint/Mask.cpp b/Userland/Applications/PixelPaint/Mask.cpp new file mode 100644 index 0000000000..51fbcf4664 --- /dev/null +++ b/Userland/Applications/PixelPaint/Mask.cpp @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2021, Davipb + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include "Mask.h" + +namespace PixelPaint { + +Mask::Mask(Gfx::IntRect bounding_rect, u8 default_value) + : m_bounding_rect(bounding_rect) +{ + auto data_size = bounding_rect.size().area(); + m_data.resize(data_size); + + for (auto& x : m_data) { + x = default_value; + } +} + +size_t Mask::to_index(int x, int y) const +{ + VERIFY(m_bounding_rect.contains(x, y)); + + int dx = x - m_bounding_rect.x(); + int dy = y - m_bounding_rect.y(); + return dy * m_bounding_rect.width() + dx; +} + +u8 Mask::get(int x, int y) const +{ + if (is_null() || !m_bounding_rect.contains(x, y)) { + return 0; + } + + return m_data[to_index(x, y)]; +} + +void Mask::set(int x, int y, u8 value) +{ + VERIFY(!is_null()); + VERIFY(m_bounding_rect.contains(x, y)); + + m_data[to_index(x, y)] = value; +} + +Mask Mask::with_bounding_rect(Gfx::IntRect inner_rect) const +{ + auto result = Mask::empty(inner_rect); + + result.for_each_pixel([&](int x, int y) { + result.set(x, y, get(x, y)); + }); + + return result; +} + +void Mask::shrink_to_fit() +{ + int topmost = NumericLimits::max(); + int bottommost = NumericLimits::min(); + int leftmost = NumericLimits::max(); + int rightmost = NumericLimits::min(); + + bool empty = true; + for_each_pixel([&](auto x, auto y) { + if (get(x, y) == 0) { + return; + } + + empty = false; + + topmost = min(topmost, y); + bottommost = max(bottommost, y); + + leftmost = min(leftmost, x); + rightmost = max(rightmost, x); + }); + + if (empty) { + m_bounding_rect = {}; + m_data.clear(); + return; + } + + Gfx::IntRect new_bounding_rect( + leftmost, + topmost, + rightmost - leftmost + 1, + bottommost - topmost + 1); + + *this = with_bounding_rect(new_bounding_rect); +} + +void Mask::invert() +{ + for_each_pixel([&](int x, int y) { + set(x, y, 0xFF - get(x, y)); + }); +} + +void Mask::add(Mask const& other) +{ + combine(other, [](int a, int b) { return a + b; }); +} + +void Mask::subtract(Mask const& other) +{ + combine(other, [](int a, int b) { return a - b; }); +} + +void Mask::intersect(Mask const& other) +{ + combinef(other, [](float a, float b) { return a * b; }); +} + +} diff --git a/Userland/Applications/PixelPaint/Mask.h b/Userland/Applications/PixelPaint/Mask.h new file mode 100644 index 0000000000..1d70c26cb1 --- /dev/null +++ b/Userland/Applications/PixelPaint/Mask.h @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2021, Davipb + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include + +namespace PixelPaint { + +class Mask { +public: + Mask() = default; + + Mask(Mask const&) = default; + Mask& operator=(Mask const&) = default; + + Mask(Mask&&) = default; + Mask& operator=(Mask&&) = default; + + [[nodiscard]] static Mask empty(Gfx::IntRect rect) { return { rect, 0x00 }; } + [[nodiscard]] static Mask full(Gfx::IntRect rect) { return { rect, 0xFF }; } + + [[nodiscard]] bool is_null() const { return m_data.is_empty(); } + [[nodiscard]] Gfx::IntRect bounding_rect() const { return m_bounding_rect; } + + [[nodiscard]] u8 get(int x, int y) const; + [[nodiscard]] u8 get(Gfx::IntPoint point) const { return get(point.x(), point.y()); } + [[nodiscard]] float getf(int x, int y) const { return (float)get(x, y) / 255.0f; } + [[nodiscard]] float getf(Gfx::IntPoint point) const { return getf(point.x(), point.y()); } + + void set(int x, int y, u8); + void set(Gfx::IntPoint point, u8 value) { set(point.x(), point.y(), value); } + void setf(int x, int y, float value) { set(x, y, (u8)clamp(value * 255.0f, 0.0f, 255.0f)); } + void setf(Gfx::IntPoint point, float value) { setf(point.x(), point.y(), value); } + + void shrink_to_fit(); + [[nodiscard]] Mask with_bounding_rect(Gfx::IntRect) const; + + void invert(); + void add(Mask const& other); + void subtract(Mask const& other); + void intersect(Mask const& other); + + template + void for_each_pixel(Func func) const + { + for (int x = m_bounding_rect.left(); x <= m_bounding_rect.right(); x++) { + for (int y = m_bounding_rect.top(); y <= m_bounding_rect.bottom(); y++) { + func(x, y); + } + } + } + +private: + Gfx::IntRect m_bounding_rect {}; + Vector m_data {}; + + Mask(Gfx::IntRect, u8 default_value); + + [[nodiscard]] size_t to_index(int x, int y) const; + + template + void combine(Mask const& other, Func func) + { + auto new_bounding_rect = m_bounding_rect.united(other.m_bounding_rect); + auto new_me = Mask::empty(new_bounding_rect); + + new_me.for_each_pixel([&](auto x, auto y) { + // Widen to int then clamp before narrowing back to avoid annoying overflow checks in the combine functions + auto my_alpha = static_cast(get(x, y)); + auto other_alpha = static_cast(other.get(x, y)); + auto new_alpha = static_cast(clamp(func(my_alpha, other_alpha), 0, 0xFF)); + + new_me.set(x, y, new_alpha); + }); + + *this = move(new_me); + shrink_to_fit(); + } + + template + void combinef(Mask const& other, Func func) + { + auto new_bounding_rect = m_bounding_rect.united(other.m_bounding_rect); + auto new_me = Mask::empty(new_bounding_rect); + + new_me.for_each_pixel([&](auto x, auto y) { + auto my_alpha = getf(x, y); + auto other_alpha = other.getf(x, y); + auto new_alpha = func(my_alpha, other_alpha); + + new_me.setf(x, y, new_alpha); + }); + + *this = move(new_me); + shrink_to_fit(); + } +}; + +}