1
Fork 0
mirror of https://github.com/RGBCube/serenity synced 2025-07-25 14:47:44 +00:00

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.
This commit is contained in:
Torstennator 2023-07-23 16:41:54 +02:00 committed by Sam Atkins
parent b9b4ca064f
commit 411ffb7954
2 changed files with 224 additions and 8 deletions

View file

@ -11,6 +11,8 @@
#include "../Layer.h"
#include <LibGUI/Action.h>
#include <LibGUI/BoxLayout.h>
#include <LibGUI/ComboBox.h>
#include <LibGUI/ItemListModel.h>
#include <LibGUI/Label.h>
#include <LibGUI/MessageBox.h>
#include <LibGUI/Painter.h>
@ -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<Gfx::StorageFormat::BGRA8888>(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<Gfx::StorageFormat::BGRA8888>(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<Gfx::StorageFormat::BGRA8888>(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<float>(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<GUI::Widget*> BrushTool::get_properties_widget()
auto properties_widget = TRY(GUI::Widget::try_create());
properties_widget->set_layout<GUI::VerticalBoxLayout>();
auto mode_container = TRY(properties_widget->try_add<GUI::Widget>());
mode_container->set_fixed_height(20);
mode_container->set_layout<GUI::HorizontalBoxLayout>();
auto mode_label = TRY(mode_container->try_add<GUI::Label>("Mode:"_string));
mode_label->set_text_alignment(Gfx::TextAlignment::CenterLeft);
mode_label->set_fixed_size(60, 20);
static constexpr auto s_mode_names = [] {
Array<StringView, (int)BrushMode::__Count> 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<GUI::ComboBox>());
mode_combobox->set_only_allow_values_from_model(true);
mode_combobox->set_model(*GUI::ItemListModel<StringView, decltype(s_mode_names)>::create(s_mode_names));
mode_combobox->set_selected_index((int)m_mode, GUI::AllowCallback::No);
auto priority_container = TRY(properties_widget->try_add<GUI::Widget>());
priority_container->set_fixed_height(20);
priority_container->set_visible(false);
priority_container->set_layout<GUI::HorizontalBoxLayout>();
auto priority_label = TRY(priority_container->try_add<GUI::Label>("Priority:"_string));
priority_label->set_text_alignment(Gfx::TextAlignment::CenterLeft);
priority_label->set_fixed_size(60, 20);
static constexpr auto s_priority_names = [] {
Array<StringView, (int)PriorityMode::__Count> 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<GUI::ComboBox>());
priority_combobox->set_only_allow_values_from_model(true);
priority_combobox->set_model(*GUI::ItemListModel<StringView, decltype(s_priority_names)>::create(s_priority_names));
priority_combobox->set_selected_index((int)m_priority, GUI::AllowCallback::No);
auto exposure_container = TRY(properties_widget->try_add<GUI::Widget>());
exposure_container->set_fixed_height(20);
exposure_container->set_visible(false);
exposure_container->set_layout<GUI::HorizontalBoxLayout>();
auto exposure_label = TRY(exposure_container->try_add<GUI::Label>("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<GUI::ValueSlider>(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<GUI::Widget>());
size_container->set_fixed_height(20);
size_container->set_layout<GUI::HorizontalBoxLayout>();
@ -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<float>(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<float>(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);
}
}
}
}