From 411ffb79543406ec17a4fe5fdbefe8290deef6a6 Mon Sep 17 00:00:00 2001 From: Torstennator Date: Sun, 23 Jul 2023 16:41:54 +0200 Subject: [PATCH] PixelPaint: Add simple dodge and burn function to BrushTool This patch adds three new modes to the brush-tool where it is now possible to use a dodge or burn function with the brush and a soft mode where the overdraw is reduced so that the stroke looks much softer. The dodge and burn functions are used to brighten or darken the colors in the affected area of the brush. The user can decide if the highlights, midtones or shadows should be prioritized by the brush. --- .../PixelPaint/Tools/BrushTool.cpp | 210 +++++++++++++++++- .../Applications/PixelPaint/Tools/BrushTool.h | 22 ++ 2 files changed, 224 insertions(+), 8 deletions(-) diff --git a/Userland/Applications/PixelPaint/Tools/BrushTool.cpp b/Userland/Applications/PixelPaint/Tools/BrushTool.cpp index 43636a4faf..228ad96532 100644 --- a/Userland/Applications/PixelPaint/Tools/BrushTool.cpp +++ b/Userland/Applications/PixelPaint/Tools/BrushTool.cpp @@ -11,6 +11,8 @@ #include "../Layer.h" #include #include +#include +#include #include #include #include @@ -78,6 +80,9 @@ void BrushTool::on_mousemove(Layer* layer, MouseEvent& event) void BrushTool::on_mouseup(Layer*, MouseEvent&) { + m_is_drawing_line = false; + m_last_draw_rect = {}; + if (m_was_drawing) { m_editor->did_complete_action(tool_name()); m_was_drawing = false; @@ -94,28 +99,66 @@ void BrushTool::draw_point(Gfx::Bitmap& bitmap, Gfx::Color color, Gfx::IntPoint if (ensure_brush_reference_bitmap(color).is_error()) return; - if (m_editor->active_layer()->mask_type() != Layer::MaskType::EditingMask || m_editor->active_layer()->edit_mode() == Layer::EditMode::Mask) { + if ((m_mode == BrushMode::Normal && m_editor->active_layer()->mask_type() != Layer::MaskType::EditingMask) || m_editor->active_layer()->edit_mode() == Layer::EditMode::Mask) { Gfx::Painter painter = Gfx::Painter(bitmap); painter.blit(point.translated(-size()), *m_brush_reference, m_brush_reference->rect()); return; } - // if we have to deal with an EditingMask we need to set the pixel individually - int ref_x, ref_y; + auto current_draw_rect = Gfx::IntRect(point.x() - size(), point.y() - size(), size() * 2, size() * 2); + + if (current_draw_rect.location() == m_last_draw_rect.location()) + return; + + auto intersection = Gfx::IntRect::intersection(m_last_draw_rect, current_draw_rect); + auto offset_current_to_last_draw = current_draw_rect.location() - m_last_draw_rect.location(); + int last_reference_x, last_reference_y, reference_x, reference_y; + Gfx::Color brush_color_used, last_drawn_pixel, bitmap_color; + for (int y = point.y() - size(); y < point.y() + size(); y++) { for (int x = point.x() - size(); x < point.x() + size(); x++) { - ref_x = x + size() - point.x(); - ref_y = y + size() - point.y(); if (x < 0 || x >= bitmap.width() || y < 0 || y >= bitmap.height()) continue; - auto pixel_color = m_brush_reference->get_pixel(ref_x, ref_y); - if (!pixel_color.alpha()) + reference_x = x - point.x() + size(); + reference_y = y - point.y() + size(); + + if (reference_x < 0 || reference_y < 0) continue; - set_pixel_with_possible_mask(x, y, bitmap.get_pixel(x, y).blend(pixel_color), bitmap); + brush_color_used = m_brush_reference->get_pixel(reference_x, reference_y); + if (!brush_color_used.alpha()) + continue; + + if (m_mode != BrushMode::Normal && intersection.contains({ x, y })) { + last_reference_x = reference_x + offset_current_to_last_draw.x(); + last_reference_y = reference_y + offset_current_to_last_draw.y(); + last_drawn_pixel = m_brush_reference->get_pixel(last_reference_x, last_reference_y); + + if (last_drawn_pixel.alpha() < brush_color_used.alpha()) + brush_color_used.set_alpha(brush_color_used.alpha() - last_drawn_pixel.alpha()); + else + continue; + } + + if (m_mode == BrushMode::Dodge || m_mode == BrushMode::Burn) { + bitmap_color = bitmap.get_pixel(x, y); + + if (!bitmap_color.alpha()) + continue; + + brush_color_used = Gfx::Color( + m_precomputed_color_values[bitmap_color.red()], + m_precomputed_color_values[bitmap_color.green()], + m_precomputed_color_values[bitmap_color.blue()], + (static_cast(bitmap_color.alpha()) / 255) * brush_color_used.alpha()); + } + + set_pixel_with_possible_mask(x, y, bitmap.get_pixel(x, y).blend(brush_color_used), bitmap); } } + + m_last_draw_rect = current_draw_rect; } void BrushTool::draw_line(Gfx::Bitmap& bitmap, Gfx::Color color, Gfx::IntPoint start, Gfx::IntPoint end) @@ -138,6 +181,9 @@ void BrushTool::draw_line(Gfx::Bitmap& bitmap, Gfx::Color color, Gfx::IntPoint s swap(start_x, end_x); swap(start_y, end_y); } + if (!m_is_drawing_line) + m_last_draw_rect = {}; + m_is_drawing_line = true; float y = start_y; for (int x = start_x; x <= end_x; x++) { @@ -157,6 +203,112 @@ ErrorOr BrushTool::get_properties_widget() auto properties_widget = TRY(GUI::Widget::try_create()); properties_widget->set_layout(); + auto mode_container = TRY(properties_widget->try_add()); + mode_container->set_fixed_height(20); + mode_container->set_layout(); + auto mode_label = TRY(mode_container->try_add("Mode:"_string)); + mode_label->set_text_alignment(Gfx::TextAlignment::CenterLeft); + mode_label->set_fixed_size(60, 20); + + static constexpr auto s_mode_names = [] { + Array names; + for (size_t i = 0; i < names.size(); i++) { + switch ((BrushMode)i) { + case BrushMode::Normal: + names[i] = "Normal"sv; + break; + case BrushMode::Soft: + names[i] = "Soft"sv; + break; + case BrushMode::Burn: + names[i] = "Burn"sv; + break; + case BrushMode::Dodge: + names[i] = "Dodge"sv; + break; + default: + break; + } + } + return names; + }(); + + auto mode_combobox = TRY(mode_container->try_add()); + mode_combobox->set_only_allow_values_from_model(true); + mode_combobox->set_model(*GUI::ItemListModel::create(s_mode_names)); + mode_combobox->set_selected_index((int)m_mode, GUI::AllowCallback::No); + + auto priority_container = TRY(properties_widget->try_add()); + priority_container->set_fixed_height(20); + priority_container->set_visible(false); + priority_container->set_layout(); + auto priority_label = TRY(priority_container->try_add("Priority:"_string)); + priority_label->set_text_alignment(Gfx::TextAlignment::CenterLeft); + priority_label->set_fixed_size(60, 20); + + static constexpr auto s_priority_names = [] { + Array names; + for (size_t i = 0; i < names.size(); i++) { + switch ((PriorityMode)i) { + case PriorityMode::Highlights: + names[i] = "Highlights"sv; + break; + case PriorityMode::Midtones: + names[i] = "Midtones"sv; + break; + case PriorityMode::Shadows: + names[i] = "Shadows"sv; + break; + default: + break; + } + } + return names; + }(); + + auto priority_combobox = TRY(priority_container->try_add()); + priority_combobox->set_only_allow_values_from_model(true); + priority_combobox->set_model(*GUI::ItemListModel::create(s_priority_names)); + priority_combobox->set_selected_index((int)m_priority, GUI::AllowCallback::No); + + auto exposure_container = TRY(properties_widget->try_add()); + exposure_container->set_fixed_height(20); + exposure_container->set_visible(false); + exposure_container->set_layout(); + + auto exposure_label = TRY(exposure_container->try_add("Exposure:"_string)); + exposure_label->set_text_alignment(Gfx::TextAlignment::CenterLeft); + exposure_label->set_fixed_size(60, 20); + + auto exposure_slider = TRY(exposure_container->try_add(Orientation::Horizontal, "%"_string)); + exposure_slider->set_range(1, 100); + exposure_slider->set_value(m_exposure * 100); + + mode_combobox->on_change = [this, priority_container, exposure_container](auto&, auto& model_index) { + VERIFY(model_index.row() >= 0); + VERIFY(model_index.row() < (int)BrushMode::__Count); + + m_mode = (BrushMode)model_index.row(); + priority_container->set_visible(m_mode == BrushMode::Dodge || m_mode == BrushMode::Burn); + exposure_container->set_visible(m_mode == BrushMode::Dodge || m_mode == BrushMode::Burn); + + if (m_mode == BrushMode::Dodge || m_mode == BrushMode::Burn) + update_precomputed_color_values(); + }; + + priority_combobox->on_change = [this](auto&, auto& model_index) { + VERIFY(model_index.row() >= 0); + VERIFY(model_index.row() < (int)PriorityMode::__Count); + + m_priority = (PriorityMode)model_index.row(); + update_precomputed_color_values(); + }; + + exposure_slider->on_change = [this](int value) { + m_exposure = value / 100.0f; + update_precomputed_color_values(); + }; + auto size_container = TRY(properties_widget->try_add()); size_container->set_fixed_height(20); size_container->set_layout(); @@ -268,4 +420,46 @@ float BrushTool::max_allowed_cursor_size() { return m_editor ? Gfx::IntPoint(0, 0).distance_from({ m_editor->width(), m_editor->height() }) * 1.1f : 500; } + +void BrushTool::update_precomputed_color_values() +{ + float dodge_burn_factor = 0; + switch (m_priority) { + case PriorityMode::Highlights: + if (m_mode == BrushMode::Dodge) + dodge_burn_factor = 1.0f + (m_exposure / 3.0f); + else + dodge_burn_factor = 1.0f - (m_exposure / 3.0f); + break; + case PriorityMode::Midtones: + if (m_mode == BrushMode::Dodge) + dodge_burn_factor = 1.0f / (1.0f + m_exposure); + else + dodge_burn_factor = 1.0f / (1.0f - m_exposure); + break; + case PriorityMode::Shadows: + dodge_burn_factor = (1.0f - m_exposure * 0.5f); + break; + case PriorityMode::__Count: + VERIFY_NOT_REACHED(); + break; + } + + float scaled_color = 0; + for (int color_val = 0; color_val < 256; color_val++) { + scaled_color = static_cast(color_val) / 255.0f; + if (m_priority == PriorityMode::Highlights) + m_precomputed_color_values[color_val] = AK::min(color_val * dodge_burn_factor, 255); + + if (m_priority == PriorityMode::Midtones) + m_precomputed_color_values[color_val] = AK::min(AK::pow(static_cast(scaled_color), dodge_burn_factor) * 255, 255); + + if (m_priority == PriorityMode::Shadows) { + if (m_mode == BrushMode::Dodge) + m_precomputed_color_values[color_val] = AK::min((dodge_burn_factor * scaled_color + (1.0f - dodge_burn_factor)) * 255, 255); + else + m_precomputed_color_values[color_val] = AK::max((scaled_color + m_exposure * (1.0f - AK::exp(1.0f - scaled_color))) * 255, 0); + } + } +} } diff --git a/Userland/Applications/PixelPaint/Tools/BrushTool.h b/Userland/Applications/PixelPaint/Tools/BrushTool.h index c53db41822..6d06b8aa6b 100644 --- a/Userland/Applications/PixelPaint/Tools/BrushTool.h +++ b/Userland/Applications/PixelPaint/Tools/BrushTool.h @@ -55,9 +55,27 @@ protected: float m_scale_last_created_cursor = 0; private: + enum class BrushMode { + Normal, + Soft, + Dodge, + Burn, + __Count, + }; + + enum class PriorityMode { + Highlights, + Midtones, + Shadows, + __Count, + }; + + BrushMode m_mode = BrushMode::Normal; + PriorityMode m_priority = PriorityMode::Highlights; RefPtr m_properties_widget; int m_size { 20 }; int m_hardness { 80 }; + float m_exposure = 0.2f; bool m_was_drawing { false }; bool m_has_clicked { false }; Gfx::IntPoint m_last_position; @@ -65,7 +83,11 @@ private: RefPtr m_brush_reference = nullptr; Gfx::Color m_ensured_color {}; int m_ensured_hardness = 0; + int m_precomputed_color_values[256]; + Gfx::IntRect m_last_draw_rect; + bool m_is_drawing_line { false }; ErrorOr ensure_brush_reference_bitmap(Gfx::Color); + void update_precomputed_color_values(); }; }