From afd52e25769218ac9d38adc1c0d84853ee5c642c Mon Sep 17 00:00:00 2001 From: BenJilks Date: Thu, 15 Oct 2020 17:57:07 +0000 Subject: [PATCH] PixelPaint: Tool properties panel Each tool can have its own set of properties that can be modified through a panel on the right side. The tools I've added properties for are: Pen: Thickness Brush: Size Hardness Spray: Thickness Density Bucket: Threshold --- Applications/PixelPaint/BrushTool.cpp | 54 +++++++++++++++- Applications/PixelPaint/BrushTool.h | 5 +- Applications/PixelPaint/BucketTool.cpp | 49 ++++++++++++++- Applications/PixelPaint/BucketTool.h | 4 ++ Applications/PixelPaint/CMakeLists.txt | 1 + Applications/PixelPaint/PenTool.cpp | 32 ++++++++++ Applications/PixelPaint/PenTool.h | 2 + Applications/PixelPaint/SprayTool.cpp | 55 ++++++++++++++++- Applications/PixelPaint/SprayTool.h | 6 +- Applications/PixelPaint/Tool.h | 1 + .../PixelPaint/ToolPropertiesWidget.cpp | 61 +++++++++++++++++++ .../PixelPaint/ToolPropertiesWidget.h | 54 ++++++++++++++++ Applications/PixelPaint/main.cpp | 12 ++-- 13 files changed, 324 insertions(+), 12 deletions(-) create mode 100644 Applications/PixelPaint/ToolPropertiesWidget.cpp create mode 100644 Applications/PixelPaint/ToolPropertiesWidget.h diff --git a/Applications/PixelPaint/BrushTool.cpp b/Applications/PixelPaint/BrushTool.cpp index 6e89eab88b..3caebcb60f 100644 --- a/Applications/PixelPaint/BrushTool.cpp +++ b/Applications/PixelPaint/BrushTool.cpp @@ -28,9 +28,13 @@ #include "ImageEditor.h" #include "Layer.h" #include +#include +#include #include +#include #include #include +#include namespace PixelPaint { @@ -70,7 +74,7 @@ void BrushTool::draw_point(Gfx::Bitmap& bitmap, const Gfx::Color& color, const G if (distance >= m_size) continue; - auto falloff = (1.0 - (distance / (float)m_size)) * 0.2; + auto falloff = (1.0 - (distance / (float)m_size)) * (1.0f / (100 - m_hardness)); auto pixel_color = color; pixel_color.set_alpha(falloff * 255); bitmap.set_pixel(x, y, bitmap.get_pixel(x, y).blend(pixel_color)); @@ -111,4 +115,52 @@ void BrushTool::draw_line(Gfx::Bitmap& bitmap, const Gfx::Color& color, const Gf } } +GUI::Widget* BrushTool::get_properties_widget() +{ + if (!m_properties_widget) { + m_properties_widget = GUI::Widget::construct(); + m_properties_widget->set_layout(); + + auto& size_container = m_properties_widget->add(); + size_container.set_size_policy(GUI::SizePolicy::Fill, GUI::SizePolicy::Fixed); + size_container.set_preferred_size(0, 20); + size_container.set_layout(); + + auto& size_label = size_container.add("Size:"); + size_label.set_text_alignment(Gfx::TextAlignment::CenterLeft); + size_label.set_size_policy(GUI::SizePolicy::Fixed, GUI::SizePolicy::Fixed); + size_label.set_preferred_size(80, 20); + + auto& size_slider = size_container.add(); + size_slider.set_size_policy(GUI::SizePolicy::Fill, GUI::SizePolicy::Fixed); + size_slider.set_preferred_size(0, 20); + size_slider.set_range(1, 100); + size_slider.set_value(m_size); + size_slider.on_value_changed = [this](int value) { + m_size = value; + }; + + auto& hardness_container = m_properties_widget->add(); + hardness_container.set_size_policy(GUI::SizePolicy::Fill, GUI::SizePolicy::Fixed); + hardness_container.set_preferred_size(0, 20); + hardness_container.set_layout(); + + auto& hardness_label = hardness_container.add("Hardness:"); + hardness_label.set_text_alignment(Gfx::TextAlignment::CenterLeft); + hardness_label.set_size_policy(GUI::SizePolicy::Fixed, GUI::SizePolicy::Fixed); + hardness_label.set_preferred_size(80, 20); + + auto& hardness_slider = hardness_container.add(); + hardness_slider.set_size_policy(GUI::SizePolicy::Fill, GUI::SizePolicy::Fixed); + hardness_slider.set_preferred_size(0, 20); + hardness_slider.set_range(1, 99); + hardness_slider.set_value(m_hardness); + hardness_slider.on_value_changed = [this](int value) { + m_hardness = value; + }; + } + + return m_properties_widget.ptr(); +} + } diff --git a/Applications/PixelPaint/BrushTool.h b/Applications/PixelPaint/BrushTool.h index 68d25385d2..33b27fce4e 100644 --- a/Applications/PixelPaint/BrushTool.h +++ b/Applications/PixelPaint/BrushTool.h @@ -37,9 +37,12 @@ public: virtual void on_mousedown(Layer&, GUI::MouseEvent& layer_event, GUI::MouseEvent& image_event) override; virtual void on_mousemove(Layer&, GUI::MouseEvent& layer_event, GUI::MouseEvent& image_event) override; + virtual GUI::Widget* get_properties_widget() override; private: - int m_size { 10 }; + RefPtr m_properties_widget; + int m_size { 20 }; + int m_hardness { 80 }; Gfx::IntPoint m_last_position; virtual const char* class_name() const override { return "BrushTool"; } diff --git a/Applications/PixelPaint/BucketTool.cpp b/Applications/PixelPaint/BucketTool.cpp index 1ed56b2008..11d0cf639d 100644 --- a/Applications/PixelPaint/BucketTool.cpp +++ b/Applications/PixelPaint/BucketTool.cpp @@ -28,7 +28,10 @@ #include "ImageEditor.h" #include "Layer.h" #include +#include +#include #include +#include #include #include @@ -42,7 +45,15 @@ BucketTool::~BucketTool() { } -static void flood_fill(Gfx::Bitmap& bitmap, const Gfx::IntPoint& start_position, Color target_color, Color fill_color) +static float color_distance_squared(const Gfx::Color& lhs, const Gfx::Color& rhs) +{ + int a = rhs.red() - lhs.red(); + int b = rhs.green() - lhs.green(); + int c = rhs.blue() - lhs.blue(); + return (a * a + b * b + c * c) / (255.0f * 255.0f); +} + +static void flood_fill(Gfx::Bitmap& bitmap, const Gfx::IntPoint& start_position, Color target_color, Color fill_color, int threshold) { ASSERT(bitmap.bpp() == 32); @@ -52,12 +63,15 @@ static void flood_fill(Gfx::Bitmap& bitmap, const Gfx::IntPoint& start_position, if (!bitmap.rect().contains(start_position)) return; + float threshold_normalized_squared = (threshold / 100.0f) * (threshold / 100.0f); + Queue queue; queue.enqueue(start_position); while (!queue.is_empty()) { auto position = queue.dequeue(); - if (bitmap.get_pixel(position.x(), position.y()) != target_color) + auto pixel_color = bitmap.get_pixel(position.x(), position.y()); + if (color_distance_squared(pixel_color, target_color) > threshold_normalized_squared) continue; bitmap.set_pixel(position.x(), position.y(), fill_color); @@ -84,9 +98,38 @@ void BucketTool::on_mousedown(Layer& layer, GUI::MouseEvent& event, GUI::MouseEv GUI::Painter painter(layer.bitmap()); auto target_color = layer.bitmap().get_pixel(event.x(), event.y()); - flood_fill(layer.bitmap(), event.position(), target_color, m_editor->color_for(event)); + flood_fill(layer.bitmap(), event.position(), target_color, m_editor->color_for(event), m_threshold); layer.did_modify_bitmap(*m_editor->image()); } +GUI::Widget* BucketTool::get_properties_widget() +{ + if (!m_properties_widget) { + m_properties_widget = GUI::Widget::construct(); + m_properties_widget->set_layout(); + + auto& threshold_container = m_properties_widget->add(); + threshold_container.set_size_policy(GUI::SizePolicy::Fill, GUI::SizePolicy::Fixed); + threshold_container.set_preferred_size(0, 20); + threshold_container.set_layout(); + + auto& threshold_label = threshold_container.add("Threshold:"); + threshold_label.set_text_alignment(Gfx::TextAlignment::CenterLeft); + threshold_label.set_size_policy(GUI::SizePolicy::Fixed, GUI::SizePolicy::Fixed); + threshold_label.set_preferred_size(80, 20); + + auto& threshold_slider = threshold_container.add(); + threshold_slider.set_size_policy(GUI::SizePolicy::Fill, GUI::SizePolicy::Fixed); + threshold_slider.set_preferred_size(0, 20); + threshold_slider.set_range(0, 100); + threshold_slider.set_value(m_threshold); + threshold_slider.on_value_changed = [this](int value) { + m_threshold = value; + }; + } + + return m_properties_widget.ptr(); +} + } diff --git a/Applications/PixelPaint/BucketTool.h b/Applications/PixelPaint/BucketTool.h index 0d17f0699d..2760302c44 100644 --- a/Applications/PixelPaint/BucketTool.h +++ b/Applications/PixelPaint/BucketTool.h @@ -36,9 +36,13 @@ public: virtual ~BucketTool() override; virtual void on_mousedown(Layer&, GUI::MouseEvent& layer_event, GUI::MouseEvent& image_event) override; + virtual GUI::Widget* get_properties_widget() override; private: virtual const char* class_name() const override { return "BucketTool"; } + + RefPtr m_properties_widget; + int m_threshold { 0 }; }; } diff --git a/Applications/PixelPaint/CMakeLists.txt b/Applications/PixelPaint/CMakeLists.txt index ca317ae46f..c1ea822c45 100644 --- a/Applications/PixelPaint/CMakeLists.txt +++ b/Applications/PixelPaint/CMakeLists.txt @@ -18,6 +18,7 @@ set(SOURCES RectangleTool.cpp SprayTool.cpp ToolboxWidget.cpp + ToolPropertiesWidget.cpp Tool.cpp ) diff --git a/Applications/PixelPaint/PenTool.cpp b/Applications/PixelPaint/PenTool.cpp index 833453385d..63512b4010 100644 --- a/Applications/PixelPaint/PenTool.cpp +++ b/Applications/PixelPaint/PenTool.cpp @@ -28,8 +28,11 @@ #include "ImageEditor.h" #include "Layer.h" #include +#include +#include #include #include +#include namespace PixelPaint { @@ -94,4 +97,33 @@ void PenTool::on_tool_button_contextmenu(GUI::ContextMenuEvent& event) m_context_menu->popup(event.screen_position()); } +GUI::Widget* PenTool::get_properties_widget() +{ + if (!m_properties_widget) { + m_properties_widget = GUI::Widget::construct(); + m_properties_widget->set_layout(); + + auto& thickness_container = m_properties_widget->add(); + thickness_container.set_size_policy(GUI::SizePolicy::Fill, GUI::SizePolicy::Fixed); + thickness_container.set_preferred_size(0, 20); + thickness_container.set_layout(); + + auto& thickness_label = thickness_container.add("Thickness:"); + thickness_label.set_text_alignment(Gfx::TextAlignment::CenterLeft); + thickness_label.set_size_policy(GUI::SizePolicy::Fixed, GUI::SizePolicy::Fixed); + thickness_label.set_preferred_size(80, 20); + + auto& thickness_slider = thickness_container.add(); + thickness_slider.set_size_policy(GUI::SizePolicy::Fill, GUI::SizePolicy::Fixed); + thickness_slider.set_preferred_size(0, 20); + thickness_slider.set_range(1, 20); + thickness_slider.set_value(m_thickness); + thickness_slider.on_value_changed = [this](int value) { + m_thickness = value; + }; + } + + return m_properties_widget.ptr(); +} + } diff --git a/Applications/PixelPaint/PenTool.h b/Applications/PixelPaint/PenTool.h index ff7d4555f6..e574099307 100644 --- a/Applications/PixelPaint/PenTool.h +++ b/Applications/PixelPaint/PenTool.h @@ -41,12 +41,14 @@ public: virtual void on_mousemove(Layer&, GUI::MouseEvent& layer_event, GUI::MouseEvent& image_event) override; virtual void on_mouseup(Layer&, GUI::MouseEvent& layer_event, GUI::MouseEvent& image_event) override; virtual void on_tool_button_contextmenu(GUI::ContextMenuEvent&) override; + virtual GUI::Widget* get_properties_widget() override; private: virtual const char* class_name() const override { return "PenTool"; } Gfx::IntPoint m_last_drawing_event_position { -1, -1 }; RefPtr m_context_menu; + RefPtr m_properties_widget; int m_thickness { 1 }; GUI::ActionGroup m_thickness_actions; }; diff --git a/Applications/PixelPaint/SprayTool.cpp b/Applications/PixelPaint/SprayTool.cpp index e56132562d..72e6fc7f03 100644 --- a/Applications/PixelPaint/SprayTool.cpp +++ b/Applications/PixelPaint/SprayTool.cpp @@ -29,8 +29,11 @@ #include "Layer.h" #include #include +#include +#include #include #include +#include #include #include #include @@ -65,9 +68,9 @@ void SprayTool::paint_it() GUI::Painter painter(bitmap); ASSERT(bitmap.bpp() == 32); m_editor->update(); - const double minimal_radius = 10; + const double minimal_radius = 2; const double base_radius = minimal_radius * m_thickness; - for (int i = 0; i < 100 + (nrand() * 800); i++) { + for (int i = 0; i < M_PI * base_radius * base_radius * (m_density / 100.0f); i++) { double radius = base_radius * nrand(); double angle = 2 * M_PI * nrand(); const int xpos = m_last_pos.x() + radius * cos(angle); @@ -125,4 +128,52 @@ void SprayTool::on_tool_button_contextmenu(GUI::ContextMenuEvent& event) m_context_menu->popup(event.screen_position()); } +GUI::Widget* SprayTool::get_properties_widget() +{ + if (!m_properties_widget) { + m_properties_widget = GUI::Widget::construct(); + m_properties_widget->set_layout(); + + auto& thickness_container = m_properties_widget->add(); + thickness_container.set_size_policy(GUI::SizePolicy::Fill, GUI::SizePolicy::Fixed); + thickness_container.set_preferred_size(0, 20); + thickness_container.set_layout(); + + auto& thickness_label = thickness_container.add("Thickness:"); + thickness_label.set_text_alignment(Gfx::TextAlignment::CenterLeft); + thickness_label.set_size_policy(GUI::SizePolicy::Fixed, GUI::SizePolicy::Fixed); + thickness_label.set_preferred_size(80, 20); + + auto& thickness_slider = thickness_container.add(); + thickness_slider.set_size_policy(GUI::SizePolicy::Fill, GUI::SizePolicy::Fixed); + thickness_slider.set_preferred_size(0, 20); + thickness_slider.set_range(1, 20); + thickness_slider.set_value(m_thickness); + thickness_slider.on_value_changed = [this](int value) { + m_thickness = value; + }; + + auto& density_container = m_properties_widget->add(); + density_container.set_size_policy(GUI::SizePolicy::Fill, GUI::SizePolicy::Fixed); + density_container.set_preferred_size(0, 20); + density_container.set_layout(); + + auto& density_label = density_container.add("Density:"); + density_label.set_text_alignment(Gfx::TextAlignment::CenterLeft); + density_label.set_size_policy(GUI::SizePolicy::Fixed, GUI::SizePolicy::Fixed); + density_label.set_preferred_size(80, 20); + + auto& density_slider = density_container.add(); + density_slider.set_size_policy(GUI::SizePolicy::Fill, GUI::SizePolicy::Fixed); + density_slider.set_preferred_size(0, 30); + density_slider.set_range(1, 100); + density_slider.set_value(m_density); + density_slider.on_value_changed = [this](int value) { + m_density = value; + }; + } + + return m_properties_widget.ptr(); +} + } diff --git a/Applications/PixelPaint/SprayTool.h b/Applications/PixelPaint/SprayTool.h index 32c1c5f5f4..827832bc4c 100644 --- a/Applications/PixelPaint/SprayTool.h +++ b/Applications/PixelPaint/SprayTool.h @@ -42,16 +42,20 @@ public: virtual void on_mouseup(Layer&, GUI::MouseEvent& layer_event, GUI::MouseEvent& image_event) override; virtual void on_mousemove(Layer&, GUI::MouseEvent& layer_event, GUI::MouseEvent& image_event) override; virtual void on_tool_button_contextmenu(GUI::ContextMenuEvent&) override; + virtual GUI::Widget* get_properties_widget() override; private: virtual const char* class_name() const override { return "SprayTool"; } void paint_it(); + + RefPtr m_properties_widget; RefPtr m_timer; Gfx::IntPoint m_last_pos; Color m_color; RefPtr m_context_menu; GUI::ActionGroup m_thickness_actions; - int m_thickness { 1 }; + int m_thickness { 10 }; + int m_density { 40 }; }; } diff --git a/Applications/PixelPaint/Tool.h b/Applications/PixelPaint/Tool.h index 9c70f218f9..812e0edd2a 100644 --- a/Applications/PixelPaint/Tool.h +++ b/Applications/PixelPaint/Tool.h @@ -48,6 +48,7 @@ public: virtual void on_second_paint(const Layer&, GUI::PaintEvent&) { } virtual void on_keydown(GUI::KeyEvent&) { } virtual void on_keyup(GUI::KeyEvent&) { } + virtual GUI::Widget* get_properties_widget() { return nullptr; } virtual bool is_move_tool() const { return false; } diff --git a/Applications/PixelPaint/ToolPropertiesWidget.cpp b/Applications/PixelPaint/ToolPropertiesWidget.cpp new file mode 100644 index 0000000000..a27103d3e7 --- /dev/null +++ b/Applications/PixelPaint/ToolPropertiesWidget.cpp @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2020, Ben Jilks + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "ToolPropertiesWidget.h" +#include "Tool.h" +#include +#include + +namespace PixelPaint { + +ToolPropertiesWidget::ToolPropertiesWidget() +{ + set_layout(); + + m_group_box = add("Tool properties"); + auto& layout = m_group_box->set_layout(); + layout.set_margins({ 10, 20, 10, 10 }); +} + +void ToolPropertiesWidget::set_active_tool(Tool* tool) +{ + if (tool == m_active_tool) + return; + + if (m_active_tool_widget != nullptr) + m_group_box->remove_child(*m_active_tool_widget); + + m_active_tool = tool; + m_active_tool_widget = tool->get_properties_widget(); + if (m_active_tool_widget != nullptr) + m_group_box->add_child(*m_active_tool_widget); +} + +ToolPropertiesWidget::~ToolPropertiesWidget() +{ +} + +} diff --git a/Applications/PixelPaint/ToolPropertiesWidget.h b/Applications/PixelPaint/ToolPropertiesWidget.h new file mode 100644 index 0000000000..41c1675cfa --- /dev/null +++ b/Applications/PixelPaint/ToolPropertiesWidget.h @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2020, Ben Jilks + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include +#include +#include + +namespace PixelPaint { + +class Tool; + +class ToolPropertiesWidget final : public GUI::Widget { + C_OBJECT(ToolPropertiesWidget); + +public: + virtual ~ToolPropertiesWidget() override; + + void set_active_tool(Tool*); + +private: + ToolPropertiesWidget(); + + RefPtr m_group_box; + + Tool* m_active_tool { nullptr }; + GUI::Widget* m_active_tool_widget { nullptr }; +}; + +} diff --git a/Applications/PixelPaint/main.cpp b/Applications/PixelPaint/main.cpp index 519b58cf98..9c57483762 100644 --- a/Applications/PixelPaint/main.cpp +++ b/Applications/PixelPaint/main.cpp @@ -33,6 +33,7 @@ #include "LayerPropertiesWidget.h" #include "PaletteWidget.h" #include "Tool.h" +#include "ToolPropertiesWidget.h" #include "ToolboxWidget.h" #include #include @@ -84,10 +85,6 @@ int main(int argc, char** argv) auto& image_editor = vertical_container.add(); image_editor.set_focus(true); - toolbox.on_tool_selection = [&](auto* tool) { - image_editor.set_active_tool(tool); - }; - vertical_container.add(image_editor); auto& right_panel = horizontal_container.add(); @@ -100,6 +97,13 @@ int main(int argc, char** argv) auto& layer_properties_widget = right_panel.add(); + auto& tool_properties_widget = right_panel.add(); + + toolbox.on_tool_selection = [&](auto* tool) { + image_editor.set_active_tool(tool); + tool_properties_widget.set_active_tool(tool); + }; + window->show(); auto menubar = GUI::MenuBar::construct();