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(); }; }