From 8c681a160375793bc7992eb9ea49bd80c14466ad Mon Sep 17 00:00:00 2001 From: Torstennator Date: Fri, 23 Jun 2023 12:43:59 +0200 Subject: [PATCH] PixelPaint: Add color-masking for editing masks This patch adds the ability to refine a editing mask by a color-range based on the pixels of the content image. This is useful for image editing where mask restirction via luminosity might not fit or just certain color ranges should be edited. --- .../Applications/PixelPaint/CMakeLists.txt | 2 + .../Applications/PixelPaint/ColorMasking.gml | 77 +++ .../Applications/PixelPaint/ImageMasking.cpp | 447 +++++++++++++++--- .../Applications/PixelPaint/ImageMasking.h | 47 +- .../Applications/PixelPaint/MainWidget.cpp | 30 +- Userland/Applications/PixelPaint/MainWidget.h | 1 + 6 files changed, 520 insertions(+), 84 deletions(-) create mode 100644 Userland/Applications/PixelPaint/ColorMasking.gml diff --git a/Userland/Applications/PixelPaint/CMakeLists.txt b/Userland/Applications/PixelPaint/CMakeLists.txt index dcf0808eff..8c7bc4457d 100644 --- a/Userland/Applications/PixelPaint/CMakeLists.txt +++ b/Userland/Applications/PixelPaint/CMakeLists.txt @@ -11,6 +11,7 @@ compile_gml(FilterGallery.gml FilterGalleryGML.h filter_gallery_gml) compile_gml(ResizeImageDialog.gml ResizeImageDialogGML.h resize_image_dialog_gml) compile_gml(LevelsDialog.gml LevelsDialogGML.h levels_dialog_gml) compile_gml(LuminosityMasking.gml LuminosityMaskingGML.h luminosity_masking_gml) +compile_gml(ColorMasking.gml ColorMaskingGML.h color_masking_gml) compile_gml(Filters/MedianSettings.gml Filters/MedianSettingsGML.h median_settings_gml) set(SOURCES @@ -80,6 +81,7 @@ set(SOURCES set(GENERATED_SOURCES EditGuideDialogGML.h + ColorMaskingGML.h FilterGalleryGML.h Filters/MedianSettingsGML.h LevelsDialogGML.h diff --git a/Userland/Applications/PixelPaint/ColorMasking.gml b/Userland/Applications/PixelPaint/ColorMasking.gml new file mode 100644 index 0000000000..544c9e7f99 --- /dev/null +++ b/Userland/Applications/PixelPaint/ColorMasking.gml @@ -0,0 +1,77 @@ +@GUI::Frame { + fill_with_background_color: true + layout: @GUI::VerticalBoxLayout { + margins: [4] + } + + @GUI::Widget { + layout: @GUI::VerticalBoxLayout {} + + @GUI::Label { + name: "hint_label" + enabled: true + fixed_height: 20 + visible: true + text: "Restrict mask to colors:" + text_alignment: "CenterLeft" + } + + @GUI::Widget { + layout: @GUI::HorizontalBoxLayout {} + + @GUI::VerticalSlider { + name: "color_range" + max: 180 + min: 0 + value: 15 + page_step: 10 + } + + @GUI::Widget { + name: "color_wheel_container" + } + + @GUI::VerticalSlider { + name: "hardness" + max: 100 + min: 0 + value: 50 + page_step: 10 + } + } + + @GUI::HorizontalRangeSlider { + name: "saturation_value" + max: 100 + min: -100 + lower_range: -100 + upper_range: 100 + page_step: 5 + show_label: false + } + + @GUI::CheckBox { + name: "mask_visibility" + text: "Show layer mask" + } + + @GUI::HorizontalSeparator {} + } + + @GUI::Widget { + layout: @GUI::HorizontalBoxLayout {} + fixed_height: 22 + + @GUI::Layout::Spacer {} + + @GUI::DialogButton { + name: "apply_button" + text: "OK" + } + + @GUI::DialogButton { + name: "cancel_button" + text: "Cancel" + } + } +} diff --git a/Userland/Applications/PixelPaint/ImageMasking.cpp b/Userland/Applications/PixelPaint/ImageMasking.cpp index 92f5d30ef2..d2bcc61f72 100644 --- a/Userland/Applications/PixelPaint/ImageMasking.cpp +++ b/Userland/Applications/PixelPaint/ImageMasking.cpp @@ -5,73 +5,136 @@ */ #include "ImageMasking.h" +#include #include #include #include #include #include #include +#include #include #include namespace PixelPaint { -ImageMasking::ImageMasking(GUI::Window* parent_window, ImageEditor* editor) +ImageMasking::ImageMasking(GUI::Window* parent_window, ImageEditor* editor, MaskingType masking_type) : GUI::Dialog(parent_window) + , m_masking_type(masking_type) + , m_editor(editor) { - set_title("Luminosity Mask"); set_icon(parent_window->icon()); auto main_widget = set_main_widget().release_value_but_fixme_should_propagate_errors(); - main_widget->load_from_gml(luminosity_masking_gml).release_value_but_fixme_should_propagate_errors(); - resize(300, 170); set_resizable(false); + m_previous_edit_mode = m_editor->active_layer()->edit_mode(); + m_editor->active_layer()->set_edit_mode(Layer::EditMode::Mask); - m_editor = editor; + if (m_masking_type == MaskingType::Luminosity) { + main_widget->load_from_gml(luminosity_masking_gml).release_value_but_fixme_should_propagate_errors(); + set_title("Luminosity Mask"); + resize(300, 170); + + m_full_masking_slider = main_widget->find_descendant_of_type_named("full_masking"); + m_edge_masking_slider = main_widget->find_descendant_of_type_named("edge_masking"); + auto range_illustration_container = main_widget->find_descendant_of_type_named("range_illustration"); + VERIFY(m_full_masking_slider); + VERIFY(m_edge_masking_slider); + VERIFY(range_illustration_container); + + m_full_masking_slider->set_gradient_color(Color(0, 0, 0, 255), Color(255, 255, 255, 255)); + m_edge_masking_slider->set_gradient_color(Color(0, 0, 0, 255), Color(255, 255, 255, 255)); + + auto illustration_widget = range_illustration_container->try_add(m_edge_masking_slider, m_full_masking_slider).release_value(); + illustration_widget->set_width(range_illustration_container->width()); + illustration_widget->set_height(range_illustration_container->height()); + + // check that edges of full and edge masking are not intersecting, and refine the mask with the updated values + m_full_masking_slider->on_range_change = [this, illustration_widget](int lower, int upper) { + if (lower < m_edge_masking_slider->lower_range()) + m_full_masking_slider->set_lower_range(AK::max(lower, m_edge_masking_slider->lower_range())); + if (upper > m_edge_masking_slider->upper_range()) + m_full_masking_slider->set_upper_range(AK::min(upper, m_edge_masking_slider->upper_range())); + + illustration_widget->update(); + generate_new_mask(); + }; + m_edge_masking_slider->on_range_change = [this, illustration_widget](int lower, int upper) { + if (lower > m_full_masking_slider->lower_range()) + m_edge_masking_slider->set_lower_range(AK::min(lower, m_full_masking_slider->lower_range())); + if (upper < m_full_masking_slider->upper_range()) + m_edge_masking_slider->set_upper_range(AK::max(upper, m_full_masking_slider->upper_range())); + + illustration_widget->update(); + generate_new_mask(); + }; + } + + if (m_masking_type == MaskingType::Color) { + main_widget->load_from_gml(color_masking_gml).release_value_but_fixme_should_propagate_errors(); + + set_title("Color Mask"); + resize(300, 250); + + m_saturation_value_masking_slider = main_widget->find_descendant_of_type_named("saturation_value"); + auto color_range_slider = main_widget->find_descendant_of_type_named("color_range"); + auto hardness_slider = main_widget->find_descendant_of_type_named("hardness"); + auto color_wheel_container = main_widget->find_descendant_of_type_named("color_wheel_container"); + + VERIFY(m_saturation_value_masking_slider); + VERIFY(color_wheel_container); + VERIFY(color_range_slider); + VERIFY(hardness_slider); + + m_color_wheel_widget = color_wheel_container->try_add().release_value(); + m_color_wheel_widget->set_width(color_wheel_container->width()); + m_color_wheel_widget->set_height(color_wheel_container->height()); + + auto update_control_gradients = [this, color_range_slider, hardness_slider]() { + auto selected_color = Gfx::Color::from_hsv(m_color_wheel_widget->hue(), 1, 1); + m_saturation_value_masking_slider->set_gradient_colors(Vector { + Gfx::ColorStop { Color(0, 0, 0, 255), 0 }, + Gfx::ColorStop { selected_color, 0.5 }, + Gfx::ColorStop { Color(255, 255, 255, 255), 1 } }); + color_range_slider->set_value(m_color_wheel_widget->color_range()); + hardness_slider->set_value(m_color_wheel_widget->hardness()); + }; + + auto hsv = editor->primary_color().to_hsv(); + m_color_wheel_widget->set_hue(hsv.hue); + m_color_wheel_widget->set_color_range(15); + update_control_gradients(); + + m_saturation_value_masking_slider->on_range_change = [this](int, int) { + generate_new_mask(); + }; + + color_range_slider->on_change = [this](int value) { + m_color_wheel_widget->set_color_range(value); + }; + + hardness_slider->on_change = [this](int value) { + m_color_wheel_widget->set_hardness(value); + }; + + m_color_wheel_widget->on_change = [this, update_control_gradients, color_range_slider, hardness_slider](double, double color_range, int hardness) { + color_range_slider->set_value(color_range); + hardness_slider->set_value(hardness); + update_control_gradients(); + generate_new_mask(); + }; + } - m_full_masking_slider = main_widget->find_descendant_of_type_named("full_masking"); - m_edge_masking_slider = main_widget->find_descendant_of_type_named("edge_masking"); - auto range_illustration_container = main_widget->find_descendant_of_type_named("range_illustration"); auto mask_visibility = main_widget->find_descendant_of_type_named("mask_visibility"); auto apply_button = main_widget->find_descendant_of_type_named("apply_button"); auto cancel_button = main_widget->find_descendant_of_type_named("cancel_button"); - VERIFY(m_full_masking_slider); - VERIFY(m_edge_masking_slider); - VERIFY(range_illustration_container); VERIFY(mask_visibility); VERIFY(apply_button); VERIFY(cancel_button); VERIFY(m_editor->active_layer()); - m_full_masking_slider->set_gradient_color(Color(0, 0, 0, 255), Color(255, 255, 255, 255)); - m_edge_masking_slider->set_gradient_color(Color(0, 0, 0, 255), Color(255, 255, 255, 255)); - - auto illustration_widget = range_illustration_container->try_add(m_edge_masking_slider, m_full_masking_slider).release_value(); - illustration_widget->set_width(range_illustration_container->width()); - illustration_widget->set_height(range_illustration_container->height()); - - // check that edges of full and edge masking are not intersecting, and refine the mask with the updated values - m_full_masking_slider->on_range_change = [this, illustration_widget](int lower, int upper) { - if (lower < m_edge_masking_slider->lower_range()) - m_full_masking_slider->set_lower_range(AK::max(lower, m_edge_masking_slider->lower_range())); - if (upper > m_edge_masking_slider->upper_range()) - m_full_masking_slider->set_upper_range(AK::min(upper, m_edge_masking_slider->upper_range())); - - illustration_widget->update(); - generate_new_mask(); - }; - m_edge_masking_slider->on_range_change = [this, illustration_widget](int lower, int upper) { - if (lower > m_full_masking_slider->lower_range()) - m_edge_masking_slider->set_lower_range(AK::min(lower, m_full_masking_slider->lower_range())); - if (upper < m_full_masking_slider->upper_range()) - m_edge_masking_slider->set_upper_range(AK::max(upper, m_full_masking_slider->upper_range())); - - illustration_widget->update(); - generate_new_mask(); - }; - mask_visibility->set_checked(m_editor->active_layer()->mask_visibility()); mask_visibility->on_checked = [this](auto checked) { m_editor->active_layer()->set_mask_visibility(checked); @@ -80,9 +143,8 @@ ImageMasking::ImageMasking(GUI::Window* parent_window, ImageEditor* editor) apply_button->on_click = [this](auto) { if (m_did_change) - m_editor->did_complete_action("Luminosity Masking"sv); + m_editor->did_complete_action("Image Masking"sv); - cleanup_resources(); done(ExecResult::OK); }; @@ -93,15 +155,6 @@ ImageMasking::ImageMasking(GUI::Window* parent_window, ImageEditor* editor) generate_new_mask(); } -void ImageMasking::revert_possible_changes() -{ - if (m_did_change && m_reference_mask) { - MUST(m_editor->active_layer()->set_bitmaps(m_editor->active_layer()->content_bitmap(), m_reference_mask.release_nonnull())); - m_editor->layers_did_change(); - } - cleanup_resources(); -} - void ImageMasking::generate_new_mask() { ensure_reference_mask().release_value_but_fixme_should_propagate_errors(); @@ -109,35 +162,104 @@ void ImageMasking::generate_new_mask() if (m_reference_mask.is_null()) return; - int min_luminosity_start = m_edge_masking_slider->lower_range(); - int min_luminosity_full = m_full_masking_slider->lower_range(); - int max_luminosity_full = m_full_masking_slider->upper_range(); - int max_luminosity_end = m_edge_masking_slider->upper_range(); - int current_content_luminosity, approximation_alpha; - bool has_start_range = min_luminosity_start != min_luminosity_full; - bool has_end_range = max_luminosity_end != max_luminosity_full; - Gfx::Color reference_mask_pixel, content_pixel; + if (m_masking_type == MaskingType::Luminosity) { + int min_luminosity_start = m_edge_masking_slider->lower_range(); + int min_luminosity_full = m_full_masking_slider->lower_range(); + int max_luminosity_full = m_full_masking_slider->upper_range(); + int max_luminosity_end = m_edge_masking_slider->upper_range(); + int current_content_luminosity, approximation_alpha; + bool has_start_range = min_luminosity_start != min_luminosity_full; + bool has_end_range = max_luminosity_end != max_luminosity_full; + Gfx::Color reference_mask_pixel, content_pixel; - for (int y = 0; y < m_reference_mask->height(); y++) { - for (int x = 0; x < m_reference_mask->width(); x++) { - reference_mask_pixel = m_reference_mask->get_pixel(x, y); - if (!reference_mask_pixel.alpha()) - continue; + for (int y = 0; y < m_reference_mask->height(); y++) { + for (int x = 0; x < m_reference_mask->width(); x++) { + reference_mask_pixel = m_reference_mask->get_pixel(x, y); + if (!reference_mask_pixel.alpha()) + continue; - content_pixel = m_editor->active_layer()->content_bitmap().get_pixel(x, y); - current_content_luminosity = content_pixel.luminosity(); + content_pixel = m_editor->active_layer()->content_bitmap().get_pixel(x, y); + current_content_luminosity = content_pixel.luminosity(); - if (!content_pixel.alpha() || current_content_luminosity < min_luminosity_start || current_content_luminosity > max_luminosity_end) { - reference_mask_pixel.set_alpha(0); - } else if (current_content_luminosity >= min_luminosity_start && current_content_luminosity < min_luminosity_full && has_start_range) { - approximation_alpha = reference_mask_pixel.alpha() * static_cast((current_content_luminosity - min_luminosity_start)) / (min_luminosity_full - min_luminosity_start); - reference_mask_pixel.set_alpha(approximation_alpha); - } else if (current_content_luminosity > max_luminosity_full && current_content_luminosity <= max_luminosity_end && has_end_range) { - approximation_alpha = reference_mask_pixel.alpha() * (1 - static_cast((current_content_luminosity - max_luminosity_full)) / (max_luminosity_end - max_luminosity_full)); - reference_mask_pixel.set_alpha(approximation_alpha); + if (!content_pixel.alpha() || current_content_luminosity < min_luminosity_start || current_content_luminosity > max_luminosity_end) { + reference_mask_pixel.set_alpha(0); + } else if (current_content_luminosity >= min_luminosity_start && current_content_luminosity < min_luminosity_full && has_start_range) { + approximation_alpha = reference_mask_pixel.alpha() * static_cast((current_content_luminosity - min_luminosity_start)) / (min_luminosity_full - min_luminosity_start); + reference_mask_pixel.set_alpha(approximation_alpha); + } else if (current_content_luminosity > max_luminosity_full && current_content_luminosity <= max_luminosity_end && has_end_range) { + approximation_alpha = reference_mask_pixel.alpha() * (1 - static_cast((current_content_luminosity - max_luminosity_full)) / (max_luminosity_end - max_luminosity_full)); + reference_mask_pixel.set_alpha(approximation_alpha); + } + + m_editor->active_layer()->mask_bitmap()->set_pixel(x, y, reference_mask_pixel); } + } + } - m_editor->active_layer()->mask_bitmap()->set_pixel(x, y, reference_mask_pixel); + if (m_masking_type == MaskingType::Color) { + double lower_saturation = 1; + double upper_saturation = 1; + double lower_value = 1; + double upper_value = 1; + + // m_saturation_value_masking_slider value description: + // - saturation part in the positive range + // - value part in the negative range + if (m_saturation_value_masking_slider->upper_range() <= 0) { + lower_value = (100 + m_saturation_value_masking_slider->lower_range()) / 100.0; + upper_value = (100 + m_saturation_value_masking_slider->upper_range()) / 100.0; + } else if (m_saturation_value_masking_slider->lower_range() >= 0) { + lower_saturation = 1.0 - (m_saturation_value_masking_slider->upper_range() / 100.0); + upper_saturation = 1.0 - (m_saturation_value_masking_slider->lower_range() / 100.0); + } else { + lower_value = (100 + m_saturation_value_masking_slider->lower_range()) / 100.0; + upper_value = 1.0; + lower_saturation = 1.0 - m_saturation_value_masking_slider->upper_range() / 100.0; + upper_saturation = 1; + } + + double full_masking_edge = m_color_wheel_widget->hardness(); + double gradient_masking_length = m_color_wheel_widget->color_range() - m_color_wheel_widget->hardness(); + double corrected_current_hue; + double distance_to_selected_color = 0; + Gfx::Color reference_mask_pixel; + Gfx::HSV content_pixel_hsv; + + for (int y = 0; y < m_reference_mask->height(); y++) { + for (int x = 0; x < m_reference_mask->width(); x++) { + reference_mask_pixel = m_reference_mask->get_pixel(x, y); + if (!reference_mask_pixel.alpha()) + continue; + + content_pixel_hsv = m_editor->active_layer()->content_bitmap().get_pixel(x, y).to_hsv(); + + // check against saturation + if (!(lower_saturation <= content_pixel_hsv.saturation && upper_saturation >= content_pixel_hsv.saturation)) { + m_editor->active_layer()->mask_bitmap()->set_pixel(x, y, reference_mask_pixel.with_alpha(0)); + continue; + } + + // check against value + if (!(lower_value <= content_pixel_hsv.value && upper_value >= content_pixel_hsv.value)) { + m_editor->active_layer()->mask_bitmap()->set_pixel(x, y, reference_mask_pixel.with_alpha(0)); + continue; + } + + // check against hue + corrected_current_hue = content_pixel_hsv.hue - m_color_wheel_widget->hue(); + distance_to_selected_color = AK::min(AK::abs(corrected_current_hue), AK::min(AK::abs(corrected_current_hue - 360), AK::abs(corrected_current_hue + 360))); + if (distance_to_selected_color > m_color_wheel_widget->color_range()) { + m_editor->active_layer()->mask_bitmap()->set_pixel(x, y, reference_mask_pixel.with_alpha(0)); + continue; + } + + if (distance_to_selected_color < full_masking_edge) { + m_editor->active_layer()->mask_bitmap()->set_pixel(x, y, reference_mask_pixel); + continue; + } + + m_editor->active_layer()->mask_bitmap()->set_pixel(x, y, reference_mask_pixel.with_alpha(reference_mask_pixel.alpha() - (((distance_to_selected_color - full_masking_edge) * reference_mask_pixel.alpha()) / gradient_masking_length))); + } } } @@ -153,10 +275,15 @@ ErrorOr ImageMasking::ensure_reference_mask() return {}; } -void ImageMasking::cleanup_resources() +void ImageMasking::on_done(GUI::Dialog::ExecResult result) { + if (result != GUI::Dialog::ExecResult::OK && m_did_change && m_reference_mask) + m_editor->active_layer()->set_bitmaps(m_editor->active_layer()->content_bitmap(), m_reference_mask.release_nonnull()).release_value_but_fixme_should_propagate_errors(); + if (m_reference_mask) m_reference_mask = nullptr; + + m_editor->active_layer()->set_edit_mode(m_previous_edit_mode); } void RangeIllustrationWidget::paint_event(GUI::PaintEvent&) @@ -174,4 +301,176 @@ void RangeIllustrationWidget::paint_event(GUI::PaintEvent&) painter.fill_path(illustration, Color::MidGray); } + +void ColorWheelWidget::paint_event(GUI::PaintEvent&) +{ + GUI::Painter painter(*this); + painter.save(); + + auto wedge_edge = Gfx::FloatPoint(0, -height() / 2); + + float deg_as_radians = 10.0f * (AK::Pi / 180); + Gfx::AffineTransform transform; + transform.rotate_radians(deg_as_radians); + + painter.translate(width() / 2, height() / 2); + + for (int deg = 0; deg < 360; deg += 10) { + auto rotated_edge = wedge_edge.transformed(transform); + Gfx::Path wedge; + wedge.move_to({ + 0, + 0, + }); + wedge.line_to(wedge_edge); + wedge.line_to(rotated_edge); + wedge.line_to({ + 0, + 0, + }); + wedge.close(); + + painter.fill_path(wedge, Color::from_hsv(deg, 1, 1)); + + wedge_edge = rotated_edge; + } + + transform.rotate_radians(-deg_as_radians); + deg_as_radians = static_cast(hue()) * (AK::Pi / 180); + transform.rotate_radians(deg_as_radians); + auto selected_color = Gfx::FloatPoint(0, -height() / 2); + selected_color.transform_by(transform); + + deg_as_radians = static_cast(color_range()) * (AK::Pi / 180); + + auto selected_color_edge_1 = Gfx::FloatPoint(0, -height() / 2); + transform.rotate_radians(deg_as_radians); + selected_color_edge_1.transform_by(transform); + + auto selected_color_edge_2 = Gfx::FloatPoint(0, -height() / 2); + transform.rotate_radians(-deg_as_radians); + transform.rotate_radians(-deg_as_radians); + selected_color_edge_2.transform_by(transform); + + transform.rotate_radians(deg_as_radians); + deg_as_radians = static_cast(color_range() * static_cast(hardness()) / 100.0) * (AK::Pi / 180); + + auto hardness_edge_1 = Gfx::FloatPoint(0, -height() / 2); + transform.rotate_radians(deg_as_radians); + hardness_edge_1.transform_by(transform); + + auto hardness_edge_2 = Gfx::FloatPoint(0, -height() / 2); + transform.rotate_radians(-deg_as_radians); + transform.rotate_radians(-deg_as_radians); + hardness_edge_2.transform_by(transform); + + Gfx::AntiAliasingPainter aa_painter = Gfx::AntiAliasingPainter(painter); + + aa_painter.draw_line(Gfx::IntPoint(0, 0), selected_color_edge_1.to_type(), Color::White, 2); + aa_painter.draw_line(Gfx::IntPoint(0, 0), selected_color_edge_2.to_type(), Color::White, 2); + aa_painter.draw_line(Gfx::IntPoint(0, 0), hardness_edge_1.to_type(), Color::LightGray, 1); + aa_painter.draw_line(Gfx::IntPoint(0, 0), hardness_edge_2.to_type(), Color::LightGray, 1); + aa_painter.draw_line(Gfx::IntPoint(0, 0), selected_color.to_type(), Color::Black, 3); + aa_painter.fill_circle({ 0, 0 }, height() / 4, Color(Color::LightGray)); + aa_painter.fill_circle({ 0, 0 }, (height() - 4) / 4, Color::from_hsv(hue(), 1, 1)); + + painter.restore(); + auto hue_text = DeprecatedString::formatted("hue: {:.0}", hue()); + painter.draw_text(rect().translated(1, 1), hue_text, Gfx::TextAlignment::Center, Color::Black); + painter.draw_text(rect(), hue_text, Gfx::TextAlignment::Center, Color::White); +} + +void ColorWheelWidget::mousedown_event(GUI::MouseEvent& event) +{ + if (event.button() == GUI::MouseButton::Primary) + m_mouse_pressed = true; +} + +void ColorWheelWidget::mouseup_event(GUI::MouseEvent& event) +{ + if (m_mouse_pressed) + calc_hue(event.position()); + m_mouse_pressed = false; +} + +void ColorWheelWidget::mousemove_event(GUI::MouseEvent& event) +{ + if (!m_mouse_pressed) + return; + + calc_hue(event.position()); +} + +void ColorWheelWidget::mousewheel_event(GUI::MouseEvent& event) +{ + if (event.ctrl()) + set_color_range(color_range() + event.wheel_delta_y()); + else if (event.shift()) + set_hardness(hardness() + event.wheel_delta_y()); + else + set_hue(hue() + event.wheel_delta_y()); +} + +void ColorWheelWidget::set_hue(double value) +{ + if (value < 0) + value += 360.0; + + value = AK::fmod(value, 360.0); + if (m_hue != value) { + m_hue = value; + update(); + + if (on_change) + on_change(hue(), color_range(), hardness()); + } +} + +double ColorWheelWidget::hue() +{ + return m_hue; +} + +void ColorWheelWidget::calc_hue(Gfx::IntPoint const& position) +{ + auto center = Gfx::IntPoint(width() / 2, height() / 2); + + auto angle = AK::atan2(static_cast(position.y() - center.y()), static_cast(position.x() - center.x())) * 180 / AK::Pi; + set_hue(angle + 90); +} + +double ColorWheelWidget::color_range() +{ + return m_color_range; +} + +void ColorWheelWidget::set_color_range(double value) +{ + value = clamp(value, 0.0, 180.0); + if (m_color_range != value) { + m_color_range = value; + update(); + + if (on_change) + on_change(hue(), color_range(), hardness()); + } +} + +void ColorWheelWidget::set_hardness(int value) +{ + value = clamp(value, 0, 100); + if (m_hardness != value) { + m_hardness = value; + update(); + + if (on_change) + on_change(hue(), color_range(), hardness()); + } +} + +int ColorWheelWidget::hardness() +{ + return m_hardness; +} + } diff --git a/Userland/Applications/PixelPaint/ImageMasking.h b/Userland/Applications/PixelPaint/ImageMasking.h index f5bc9ab137..f0e2a018b1 100644 --- a/Userland/Applications/PixelPaint/ImageMasking.h +++ b/Userland/Applications/PixelPaint/ImageMasking.h @@ -10,29 +10,41 @@ #include "Layer.h" #include #include +#include #include namespace PixelPaint { +class ColorWheelWidget; + class ImageMasking final : public GUI::Dialog { C_OBJECT(ImageMasking); public: - void revert_possible_changes(); + enum class MaskingType { + Luminosity, + Color, + }; + +protected: + void on_done(GUI::Dialog::ExecResult) override; private: - ImageMasking(GUI::Window* parent_window, ImageEditor*); + explicit ImageMasking(GUI::Window* parent_window, ImageEditor*, MaskingType masking_type); + MaskingType m_masking_type; + Layer::EditMode m_previous_edit_mode; ImageEditor* m_editor { nullptr }; RefPtr m_reference_mask { nullptr }; bool m_did_change = false; RefPtr m_full_masking_slider = { nullptr }; RefPtr m_edge_masking_slider = { nullptr }; + RefPtr m_color_wheel_widget = { nullptr }; + RefPtr m_saturation_value_masking_slider = { nullptr }; ErrorOr ensure_reference_mask(); void generate_new_mask(); - void cleanup_resources(); }; class RangeIllustrationWidget final : public GUI::Widget { @@ -53,4 +65,33 @@ private: RefPtr m_full_mask_values; }; +class ColorWheelWidget final : public GUI::Widget { + C_OBJECT(ColorWheelWidget) +public: + virtual ~ColorWheelWidget() override = default; + double hue(); + void set_hue(double); + double color_range(); + void set_color_range(double); + int hardness(); + void set_hardness(int); + Function on_change; + +protected: + virtual void paint_event(GUI::PaintEvent&) override; + virtual void mousedown_event(GUI::MouseEvent&) override; + virtual void mousemove_event(GUI::MouseEvent&) override; + virtual void mouseup_event(GUI::MouseEvent&) override; + virtual void mousewheel_event(GUI::MouseEvent&) override; + +private: + ColorWheelWidget() = default; + double m_hue = 0; + double m_color_range = 0; + int m_hardness = 0; + bool m_mouse_pressed = false; + + void calc_hue(Gfx::IntPoint const&); +}; + } diff --git a/Userland/Applications/PixelPaint/MainWidget.cpp b/Userland/Applications/PixelPaint/MainWidget.cpp index 68728d99eb..3b0a7d2e21 100644 --- a/Userland/Applications/PixelPaint/MainWidget.cpp +++ b/Userland/Applications/PixelPaint/MainWidget.cpp @@ -869,18 +869,33 @@ ErrorOr MainWidget::initialize_menubar(GUI::Window& window) TRY(m_layer_menu->try_add_action(*m_toggle_mask_visibility_action)); m_open_luminosity_masking_action = GUI::Action::create( - "Luminosity Masking", create_layer_mask_callback("Luminosity Masking", [&](Layer* active_layer) { - VERIFY(active_layer->mask_type() == Layer::MaskType::EditingMask); - + "Luminosity Masking", [&](auto&) { auto* editor = current_image_editor(); VERIFY(editor); - auto dialog = PixelPaint::ImageMasking::construct(&window, editor); - if (dialog->exec() != GUI::Dialog::ExecResult::OK) - dialog->revert_possible_changes(); - })); + if (!editor->active_layer()) + return; + VERIFY(editor->active_layer()->mask_type() == Layer::MaskType::EditingMask); + + PixelPaint::ImageMasking::construct(&window, editor, ImageMasking::MaskingType::Luminosity)->exec(); + m_layer_list_widget->repaint(); + }); TRY(m_layer_menu->try_add_action(*m_open_luminosity_masking_action)); + m_open_color_masking_action = GUI::Action::create( + "Color Masking", [&](auto&) { + auto* editor = current_image_editor(); + VERIFY(editor); + if (!editor->active_layer()) + return; + VERIFY(editor->active_layer()->mask_type() == Layer::MaskType::EditingMask); + + PixelPaint::ImageMasking::construct(&window, editor, ImageMasking::MaskingType::Color)->exec(); + m_layer_list_widget->repaint(); + }); + + TRY(m_layer_menu->try_add_action(*m_open_color_masking_action)); + TRY(m_layer_menu->try_add_separator()); TRY(m_layer_menu->try_add_action(GUI::Action::create( @@ -1263,6 +1278,7 @@ void MainWidget::set_mask_actions_for_layer(Layer* layer) m_toggle_mask_visibility_action->set_visible(layer->mask_type() == Layer::MaskType::EditingMask); m_toggle_mask_visibility_action->set_checked(layer->mask_visibility()); m_open_luminosity_masking_action->set_visible(layer->mask_type() == Layer::MaskType::EditingMask); + m_open_color_masking_action->set_visible(layer->mask_type() == Layer::MaskType::EditingMask); } void MainWidget::open_image(FileSystemAccessClient::File file) diff --git a/Userland/Applications/PixelPaint/MainWidget.h b/Userland/Applications/PixelPaint/MainWidget.h index 1accb63272..46c6771693 100644 --- a/Userland/Applications/PixelPaint/MainWidget.h +++ b/Userland/Applications/PixelPaint/MainWidget.h @@ -119,6 +119,7 @@ private: RefPtr m_clear_mask_action; RefPtr m_toggle_mask_visibility_action; RefPtr m_open_luminosity_masking_action; + RefPtr m_open_color_masking_action; Gfx::IntPoint m_last_image_editor_mouse_position; };