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