From b7e8f32323c9fd8bc1fcd2058e717d4307fa8f1f Mon Sep 17 00:00:00 2001 From: Torstennator Date: Mon, 18 Apr 2022 15:24:49 +0200 Subject: PixelPaint: Add a histogram widget This adds a simple histogram widget that visualizes the rgb-channels and brightness for a given image. When hovering over the image it will indicate what brightness level the pixel at the mouse position has. --- Userland/Applications/PixelPaint/CMakeLists.txt | 1 + .../Applications/PixelPaint/HistogramWidget.cpp | 172 +++++++++++++++++++++ Userland/Applications/PixelPaint/HistogramWidget.h | 45 ++++++ Userland/Applications/PixelPaint/MainWidget.cpp | 8 + Userland/Applications/PixelPaint/MainWidget.h | 2 + .../Applications/PixelPaint/PixelPaintWindow.gml | 13 ++ 6 files changed, 241 insertions(+) create mode 100644 Userland/Applications/PixelPaint/HistogramWidget.cpp create mode 100644 Userland/Applications/PixelPaint/HistogramWidget.h (limited to 'Userland') diff --git a/Userland/Applications/PixelPaint/CMakeLists.txt b/Userland/Applications/PixelPaint/CMakeLists.txt index 65aeef7394..e8c1f5f7e1 100644 --- a/Userland/Applications/PixelPaint/CMakeLists.txt +++ b/Userland/Applications/PixelPaint/CMakeLists.txt @@ -31,6 +31,7 @@ set(SOURCES Filters/LaplaceDiagonal.cpp Filters/Sepia.cpp Filters/Sharpen.cpp + HistogramWidget.cpp IconBag.cpp Image.cpp ImageEditor.cpp diff --git a/Userland/Applications/PixelPaint/HistogramWidget.cpp b/Userland/Applications/PixelPaint/HistogramWidget.cpp new file mode 100644 index 0000000000..d84cd3cf36 --- /dev/null +++ b/Userland/Applications/PixelPaint/HistogramWidget.cpp @@ -0,0 +1,172 @@ +/* + * Copyright (c) 2022, Torsten Engelmann + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include "HistogramWidget.h" +#include "Image.h" +#include "ImageEditor.h" +#include "Layer.h" +#include +#include +#include + +REGISTER_WIDGET(PixelPaint, HistogramWidget); + +namespace PixelPaint { + +HistogramWidget::HistogramWidget() +{ + set_height(65); +} + +HistogramWidget::~HistogramWidget() +{ + if (m_image) + m_image->remove_client(*this); +} + +void HistogramWidget::set_image(Image* image) +{ + if (m_image == image) + return; + if (m_image) + m_image->remove_client(*this); + m_image = image; + if (m_image) + m_image->add_client(*this); + + (void)rebuild_histogram_data(); +} + +ErrorOr HistogramWidget::rebuild_histogram_data() +{ + if (!m_image) + return {}; + + auto full_bitmap = TRY(m_image->try_compose_bitmap(Gfx::BitmapFormat::BGRA8888)); + + m_data.red.clear_with_capacity(); + m_data.green.clear_with_capacity(); + m_data.blue.clear_with_capacity(); + m_data.brightness.clear_with_capacity(); + + for (int i = 0; i < 256; i++) { + m_data.red.append(0); + m_data.green.append(0); + m_data.blue.append(0); + m_data.brightness.append(0); + } + + Color pixel_color; + for (int x = 0; x < full_bitmap->width(); x++) { + for (int y = 0; y < full_bitmap->height(); y++) { + pixel_color = full_bitmap->get_pixel(x, y); + if (!pixel_color.alpha()) + continue; + + m_data.red[pixel_color.red()]++; + m_data.green[pixel_color.green()]++; + m_data.blue[pixel_color.blue()]++; + m_data.brightness[pixel_color.luminosity()]++; + } + } + int max_brightness_frequency = 0; + int max_color_frequency = 0; + for (int i = 0; i < 256; i++) { + if (m_data.red[i] > max_color_frequency) + max_color_frequency = m_data.red[i]; + if (m_data.green[i] > max_color_frequency) + max_color_frequency = m_data.green[i]; + if (m_data.blue[i] > max_color_frequency) + max_color_frequency = m_data.blue[i]; + if (m_data.brightness[i] > max_brightness_frequency) + max_brightness_frequency = m_data.brightness[i]; + } + + // Scale the frequency values to fit the widgets height. + m_widget_height = height(); + + for (int i = 0; i < 256; i++) { + m_data.red[i] = (static_cast(m_data.red[i]) / max_color_frequency) * m_widget_height; + m_data.green[i] = (static_cast(m_data.green[i]) / max_color_frequency) * m_widget_height; + m_data.blue[i] = (static_cast(m_data.blue[i]) / max_color_frequency) * m_widget_height; + m_data.brightness[i] = (static_cast(m_data.brightness[i]) / max_brightness_frequency) * m_widget_height; + } + + update(); + return {}; +} + +void HistogramWidget::paint_event(GUI::PaintEvent& event) +{ + GUI::Painter painter(*this); + painter.add_clip_rect(event.rect()); + + if (!m_image) + return; + + int bottom_line = m_widget_height - 1; + float step_width = static_cast(width()) / 256; + + Gfx::Path brightness_path; + Gfx::Path red_channel_path; + Gfx::Path green_channel_path; + Gfx::Path blue_channel_path; + red_channel_path.move_to({ 0, bottom_line - m_data.red[0] }); + green_channel_path.move_to({ 0, bottom_line - m_data.green[0] }); + blue_channel_path.move_to({ 0, bottom_line - m_data.blue[0] }); + brightness_path.move_to({ 0, bottom_line }); + brightness_path.line_to({ 0, bottom_line }); + + float current_x_as_float = 0; + int current_x_as_int = 0; + int last_drawn_x = -1; + + for (int data_column = 0; data_column < 256; data_column++) { + current_x_as_int = static_cast(current_x_as_float); + // we would like to skip values that map to the same x position as it does not look so good in the final result + if (current_x_as_int == last_drawn_x) { + current_x_as_float += step_width; + continue; + } + + red_channel_path.line_to({ current_x_as_int, bottom_line - m_data.red[data_column] }); + green_channel_path.line_to({ current_x_as_int, bottom_line - m_data.green[data_column] }); + blue_channel_path.line_to({ current_x_as_int, bottom_line - m_data.blue[data_column] }); + brightness_path.line_to({ current_x_as_int, bottom_line - m_data.brightness[data_column] }); + + current_x_as_float += step_width; + last_drawn_x = current_x_as_int; + } + + brightness_path.line_to({ last_drawn_x, bottom_line }); + brightness_path.close(); + + painter.fill_path(brightness_path, Color::MidGray, Gfx::Painter::WindingRule::EvenOdd); + painter.stroke_path(red_channel_path, Color(Color::NamedColor::Red).with_alpha(90), 2); + painter.stroke_path(green_channel_path, Color(Color::NamedColor::Green).with_alpha(90), 2); + painter.stroke_path(blue_channel_path, Color(Color::NamedColor::Blue).with_alpha(90), 2); + + if (m_color_at_mouseposition != Color::Transparent) { + int x = m_color_at_mouseposition.luminosity() * step_width; + painter.draw_line({ x, 0 }, { x, bottom_line }, Color::from_hsl(45, 1, .7), 1); + } +} + +void HistogramWidget::image_changed() +{ + (void)rebuild_histogram_data(); +} + +void HistogramWidget::set_color_at_mouseposition(Color color) +{ + if (m_color_at_mouseposition == color) + return; + + m_color_at_mouseposition = color; + update(); +} + +} diff --git a/Userland/Applications/PixelPaint/HistogramWidget.h b/Userland/Applications/PixelPaint/HistogramWidget.h new file mode 100644 index 0000000000..72d0c1cff5 --- /dev/null +++ b/Userland/Applications/PixelPaint/HistogramWidget.h @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2022, Torsten Engelmann + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include "Image.h" +#include + +namespace PixelPaint { + +class HistogramWidget final + : public GUI::Frame + , ImageClient { + C_OBJECT(HistogramWidget); + +public: + virtual ~HistogramWidget() override; + + void set_image(Image*); + void image_changed(); + void set_color_at_mouseposition(Color); + +private: + HistogramWidget(); + + virtual void paint_event(GUI::PaintEvent&) override; + + ErrorOr rebuild_histogram_data(); + int m_widget_height = 0; + Color m_color_at_mouseposition = Color::Transparent; + RefPtr m_image; + + struct HistogramData { + Vector red; + Vector green; + Vector blue; + Vector brightness; + }; + HistogramData m_data; +}; + +} diff --git a/Userland/Applications/PixelPaint/MainWidget.cpp b/Userland/Applications/PixelPaint/MainWidget.cpp index 625e7df559..27c52473c1 100644 --- a/Userland/Applications/PixelPaint/MainWidget.cpp +++ b/Userland/Applications/PixelPaint/MainWidget.cpp @@ -46,6 +46,7 @@ MainWidget::MainWidget() m_palette_widget = *find_descendant_of_type_named("palette_widget"); + m_histogram_widget = *find_descendant_of_type_named("histogram_widget"); m_layer_list_widget = *find_descendant_of_type_named("layer_list_widget"); m_layer_list_widget->on_layer_select = [&](auto* layer) { auto* editor = current_image_editor(); @@ -73,6 +74,7 @@ MainWidget::MainWidget() m_tab_widget->deferred_invoke([&] { m_tab_widget->remove_tab(image_editor); if (m_tab_widget->children().size() == 0) { + m_histogram_widget->set_image(nullptr); m_layer_list_widget->set_image(nullptr); m_layer_properties_widget->set_layer(nullptr); m_palette_widget->set_image_editor(nullptr); @@ -86,11 +88,13 @@ MainWidget::MainWidget() m_tab_widget->on_change = [&](auto& widget) { auto& image_editor = verify_cast(widget); m_palette_widget->set_image_editor(&image_editor); + m_histogram_widget->set_image(&image_editor.image()); m_layer_list_widget->set_image(&image_editor.image()); m_layer_properties_widget->set_layer(image_editor.active_layer()); window()->set_modified(image_editor.is_modified()); image_editor.on_modified_change = [this](bool modified) { window()->set_modified(modified); + m_histogram_widget->image_changed(); }; if (auto* active_tool = m_toolbox->active_tool()) image_editor.set_active_tool(active_tool); @@ -125,6 +129,7 @@ void MainWidget::initialize_menubar(GUI::Window& window) editor.set_title(image_title.is_empty() ? "Untitled" : image_title); editor.undo_stack().set_current_unmodified(); + m_histogram_widget->set_image(image); m_layer_list_widget->set_image(image); m_layer_list_widget->set_selected_layer(bg_layer); } @@ -853,13 +858,16 @@ ImageEditor& MainWidget::create_new_editor(NonnullRefPtr image) auto image_rectangle = Gfx::IntRect { 0, 0, image_size.width(), image_size.height() }; if (image_rectangle.contains(mouse_position)) { m_statusbar->set_override_text(mouse_position.to_string()); + m_histogram_widget->set_color_at_mouseposition(current_image_editor()->image().color_at(mouse_position)); } else { m_statusbar->set_override_text({}); + m_histogram_widget->set_color_at_mouseposition(Color::Transparent); } }; image_editor.on_leave = [&]() { m_statusbar->set_override_text({}); + m_histogram_widget->set_color_at_mouseposition(Color::Transparent); }; image_editor.on_set_guide_visibility = [&](bool show_guides) { diff --git a/Userland/Applications/PixelPaint/MainWidget.h b/Userland/Applications/PixelPaint/MainWidget.h index 440b977cf0..c817abfa92 100644 --- a/Userland/Applications/PixelPaint/MainWidget.h +++ b/Userland/Applications/PixelPaint/MainWidget.h @@ -7,6 +7,7 @@ #pragma once #include "Guide.h" +#include "HistogramWidget.h" #include "IconBag.h" #include "Image.h" #include "ImageEditor.h" @@ -58,6 +59,7 @@ private: RefPtr m_toolbox; RefPtr m_palette_widget; + RefPtr m_histogram_widget; RefPtr m_layer_list_widget; RefPtr m_layer_properties_widget; RefPtr m_tool_properties_widget; diff --git a/Userland/Applications/PixelPaint/PixelPaintWindow.gml b/Userland/Applications/PixelPaint/PixelPaintWindow.gml index e7d429de9e..59e721b07e 100644 --- a/Userland/Applications/PixelPaint/PixelPaintWindow.gml +++ b/Userland/Applications/PixelPaint/PixelPaintWindow.gml @@ -61,6 +61,19 @@ max_height: 94 } + @GUI::GroupBox { + title: "Histogram" + max_height: 90 + layout: @GUI::VerticalBoxLayout { + margins: [6] + } + + @PixelPaint::HistogramWidget { + name: "histogram_widget" + max_height: 65 + } + } + @PixelPaint::ToolPropertiesWidget { name: "tool_properties_widget" max_height: 144 -- cgit v1.2.3