mirror of
https://github.com/RGBCube/serenity
synced 2025-05-31 07:08:10 +00:00

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.
476 lines
19 KiB
C++
476 lines
19 KiB
C++
/*
|
|
* Copyright (c) 2023, Torsten Engelmann <engelTorsten@gmx.de>
|
|
*
|
|
* SPDX-License-Identifier: BSD-2-Clause
|
|
*/
|
|
|
|
#include "ImageMasking.h"
|
|
#include <Applications/PixelPaint/ColorMaskingGML.h>
|
|
#include <Applications/PixelPaint/LuminosityMaskingGML.h>
|
|
#include <LibGUI/Button.h>
|
|
#include <LibGUI/CheckBox.h>
|
|
#include <LibGUI/Label.h>
|
|
#include <LibGUI/Painter.h>
|
|
#include <LibGUI/RangeSlider.h>
|
|
#include <LibGfx/AntiAliasingPainter.h>
|
|
#include <LibGfx/Palette.h>
|
|
#include <LibGfx/Path.h>
|
|
|
|
namespace PixelPaint {
|
|
|
|
ImageMasking::ImageMasking(GUI::Window* parent_window, ImageEditor* editor, MaskingType masking_type)
|
|
: GUI::Dialog(parent_window)
|
|
, m_masking_type(masking_type)
|
|
, m_editor(editor)
|
|
{
|
|
set_icon(parent_window->icon());
|
|
|
|
auto main_widget = set_main_widget<GUI::Widget>().release_value_but_fixme_should_propagate_errors();
|
|
|
|
set_resizable(false);
|
|
m_previous_edit_mode = m_editor->active_layer()->edit_mode();
|
|
m_editor->active_layer()->set_edit_mode(Layer::EditMode::Mask);
|
|
|
|
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<GUI::RangeSlider>("full_masking");
|
|
m_edge_masking_slider = main_widget->find_descendant_of_type_named<GUI::RangeSlider>("edge_masking");
|
|
auto range_illustration_container = main_widget->find_descendant_of_type_named<GUI::Widget>("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<RangeIllustrationWidget>(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<GUI::RangeSlider>("saturation_value");
|
|
auto color_range_slider = main_widget->find_descendant_of_type_named<GUI::VerticalSlider>("color_range");
|
|
auto hardness_slider = main_widget->find_descendant_of_type_named<GUI::VerticalSlider>("hardness");
|
|
auto color_wheel_container = main_widget->find_descendant_of_type_named<GUI::Widget>("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<ColorWheelWidget>().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();
|
|
};
|
|
}
|
|
|
|
auto mask_visibility = main_widget->find_descendant_of_type_named<GUI::CheckBox>("mask_visibility");
|
|
auto apply_button = main_widget->find_descendant_of_type_named<GUI::Button>("apply_button");
|
|
auto cancel_button = main_widget->find_descendant_of_type_named<GUI::Button>("cancel_button");
|
|
|
|
VERIFY(mask_visibility);
|
|
VERIFY(apply_button);
|
|
VERIFY(cancel_button);
|
|
VERIFY(m_editor->active_layer());
|
|
|
|
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);
|
|
m_editor->update();
|
|
};
|
|
|
|
apply_button->on_click = [this](auto) {
|
|
if (m_did_change)
|
|
m_editor->did_complete_action("Image Masking"sv);
|
|
|
|
done(ExecResult::OK);
|
|
};
|
|
|
|
cancel_button->on_click = [this](auto) {
|
|
done(ExecResult::Cancel);
|
|
};
|
|
|
|
generate_new_mask();
|
|
}
|
|
|
|
void ImageMasking::generate_new_mask()
|
|
{
|
|
ensure_reference_mask().release_value_but_fixme_should_propagate_errors();
|
|
|
|
if (m_reference_mask.is_null())
|
|
return;
|
|
|
|
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;
|
|
|
|
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<float>((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<float>((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);
|
|
}
|
|
}
|
|
}
|
|
|
|
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)));
|
|
}
|
|
}
|
|
}
|
|
|
|
m_editor->active_layer()->did_modify_bitmap();
|
|
m_did_change = true;
|
|
}
|
|
|
|
ErrorOr<void> ImageMasking::ensure_reference_mask()
|
|
{
|
|
if (m_reference_mask.is_null())
|
|
m_reference_mask = TRY(m_editor->active_layer()->mask_bitmap()->clone());
|
|
|
|
return {};
|
|
}
|
|
|
|
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&)
|
|
{
|
|
GUI::Painter painter(*this);
|
|
painter.fill_rect(Gfx::IntRect(0, 0, width(), height()), palette().color(background_role()));
|
|
float fraction = width() / 255.0f;
|
|
|
|
Gfx::Path illustration;
|
|
illustration.move_to({ fraction * m_edge_mask_values->lower_range(), static_cast<float>(height()) });
|
|
illustration.line_to({ fraction * m_full_mask_values->lower_range(), 0 });
|
|
illustration.line_to({ fraction * m_full_mask_values->upper_range(), 0 });
|
|
illustration.line_to({ fraction * m_edge_mask_values->upper_range(), static_cast<float>(height()) });
|
|
illustration.close();
|
|
|
|
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<float> / 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<float>(hue()) * (AK::Pi<float> / 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<float>(color_range()) * (AK::Pi<float> / 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<float>(color_range() * static_cast<double>(hardness()) / 100.0) * (AK::Pi<float> / 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<int>(), Color::White, 2);
|
|
aa_painter.draw_line(Gfx::IntPoint(0, 0), selected_color_edge_2.to_type<int>(), Color::White, 2);
|
|
aa_painter.draw_line(Gfx::IntPoint(0, 0), hardness_edge_1.to_type<int>(), Color::LightGray, 1);
|
|
aa_painter.draw_line(Gfx::IntPoint(0, 0), hardness_edge_2.to_type<int>(), Color::LightGray, 1);
|
|
aa_painter.draw_line(Gfx::IntPoint(0, 0), selected_color.to_type<int>(), 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<float>(position.y() - center.y()), static_cast<float>(position.x() - center.x())) * 180 / AK::Pi<float>;
|
|
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;
|
|
}
|
|
|
|
}
|