From 914c4de9bd22991a196a0950d1fcd0d36031b08a Mon Sep 17 00:00:00 2001 From: HybridDog <3192173+HybridDog@users.noreply.github.com> Date: Thu, 31 Oct 2024 20:22:32 +0100 Subject: [PATCH] [NOSQUASH] Editor: Replace the OKLab color picker by a 2D color picker (#2895) * Editor: Add a 2D color picker A two-dimensional image can enable a more convenient selection of colors than a one-dimensional slider. Under the assumption that users typically want to select a color with the highest saturation, a two-dimensional color picker where any RGB color with the highest saturation can be slected with one click could be suitable. For an independent adjustment of lightness and hue, which is considered a good property of a color picker, the two-dimensional color picker should use a perceptual color space such as OKLab. As a side effect for choosing a perceptual color space, a marker showing the lightness and hue of the current color can visualize how a change in RGB values affects the perceived color. Changes: * For the color selection in the editor, add the ItemColorPicker2D menu item, which displays an image and enables picking a color from it by clicking. The image contains the highest-saturation sRGB colors, so for the color picker we only need OKLab code to determine marker positions and no color clipping code since we can sample pixels from the image. * Add a method drawing a hexagon to Canvas * Add a helper method to ColorOKLCh which calculates a modified lightness * Add a missing include guard and constness to the code for ColorOKLCh * Editor: Remove the 1D OKLab color picker There are reports that the one-dimensional OKLab color selection is unpopular and difficult to use, so we can remove it to simplify the code and GUI. This commit removes ItemColorChannelOKLab and most of the OKLab color space code. Since the two-dimensional color picker uses some of the OKLab code, it is not removed completely. --- data/images/engine/editor/color_picker_2d.png | Bin 0 -> 68597 bytes src/gui/item_color_picker_2d.cpp | 143 ++++++++ src/gui/item_color_picker_2d.hpp | 91 +++++ src/gui/item_colorchannel_oklab.cpp | 227 ------------ src/gui/item_colorchannel_rgba.cpp | 2 +- ...channel.hpp => item_colorchannel_rgba.hpp} | 35 +- src/gui/menu.cpp | 12 +- src/gui/menu.hpp | 4 +- src/gui/menu_color.cpp | 4 +- src/util/colorspace_oklab.cpp | 347 +----------------- src/util/colorspace_oklab.hpp | 30 +- src/video/canvas.cpp | 21 ++ src/video/canvas.hpp | 10 + tests/unit/util/colorspace_oklab.cpp | 63 ---- 14 files changed, 295 insertions(+), 694 deletions(-) create mode 100644 data/images/engine/editor/color_picker_2d.png create mode 100644 src/gui/item_color_picker_2d.cpp create mode 100644 src/gui/item_color_picker_2d.hpp delete mode 100644 src/gui/item_colorchannel_oklab.cpp rename src/gui/{item_colorchannel.hpp => item_colorchannel_rgba.hpp} (65%) diff --git a/data/images/engine/editor/color_picker_2d.png b/data/images/engine/editor/color_picker_2d.png new file mode 100644 index 0000000000000000000000000000000000000000..64c2a26f0077ab842a8560ad7a043228a8485e13 GIT binary patch literal 68597 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 JPDHLkV1nRY@Cg6_ literal 0 HcmV?d00001 diff --git a/src/gui/item_color_picker_2d.cpp b/src/gui/item_color_picker_2d.cpp new file mode 100644 index 00000000000..198a9959fa5 --- /dev/null +++ b/src/gui/item_color_picker_2d.cpp @@ -0,0 +1,143 @@ +// SuperTux +// Copyright (C) 2024 HybridDog +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#include "gui/item_color_picker_2d.hpp" + +#include + +#include "math/util.hpp" +#include "video/drawing_context.hpp" +#include "video/video_system.hpp" +#include "video/surface.hpp" +#include "video/sdl_surface.hpp" + + +namespace { + +/** Get the color of a pixel from an 8-bit RGB image + * + * @param surface SDL2 surface containing the image + * @param pos Floating-point pixel coordinates in [0,1]^2 + * + * @return Color of the pixel at the integer position which corresponds to pos + */ +Color +get_pixel(const SDLSurfacePtr& surface, const Vector& pos) +{ + assert(surface->format->BytesPerPixel == 3); + int x = math::clamp( + static_cast(pos.x * static_cast(surface->w - 1) + 0.5f), + 0, + surface->w - 1 + ); + int y = math::clamp( + static_cast(pos.y * static_cast(surface->h - 1) + 0.5f), + 0, + surface->h - 1 + ); + uint8_t *pixel = static_cast(surface->pixels) + + y * surface->pitch + x * 3; + if constexpr (SDL_BYTEORDER == SDL_BIG_ENDIAN) + return Color::from_rgb888(pixel[2], pixel[1], pixel[0]); + return Color::from_rgb888(pixel[0], pixel[1], pixel[2]); +} + +} // namespace + + +ItemColorPicker2D::ItemColorPicker2D(Color& col) : + MenuItem(""), + m_image(Surface::from_file("images/engine/editor/color_picker_2d.png")), + m_image_with_pixels(SDLSurface::from_file( + "images/engine/editor/color_picker_2d.png")), + m_original_color(col), + m_color(col) +{ +} + +void +ItemColorPicker2D::draw_marker(Canvas& canvas, Color col, float radius) const +{ + ColorOKLCh col_oklab = col; + Vector pos_rel( + fmodf(col_oklab.h * 0.5f / math::PI + 1.0f, 1.0f), + 1.0f - col_oklab.get_modified_lightness() + ); + Vector pos( + m_image_rect.get_left() + pos_rel.x * (m_image_rect.get_right() + - m_image_rect.get_left()), + m_image_rect.get_top() + pos_rel.y * (m_image_rect.get_bottom() + - m_image_rect.get_top()) + ); + col.alpha = 1.0f; + canvas.draw_hexagon(pos, radius, Color::BLACK, LAYER_GUI+1); + canvas.draw_hexagon(pos, 0.86f * radius, Color::WHITE, LAYER_GUI+1); + canvas.draw_hexagon(pos, 0.7f * radius, col, LAYER_GUI+2); +} + +void +ItemColorPicker2D::draw(DrawingContext& context, const Vector& pos, + int menu_width, bool active) +{ + m_image_rect = Rectf( + pos + Vector(16, -get_height() / 2 + 8), + pos + Vector(menu_width - 16, get_height() / 2 - 8) + ); + context.color().draw_surface_scaled(m_image, m_image_rect, LAYER_GUI); + draw_marker(context.color(), m_original_color, 4.7f); + draw_marker(context.color(), m_color, 5.5f); +} + +void +ItemColorPicker2D::event(const SDL_Event& ev) +{ + bool is_mouseclick = ev.type == SDL_MOUSEBUTTONDOWN + && ev.button.button == SDL_BUTTON_LEFT; + bool is_hold_mousemove = ev.type == SDL_MOUSEMOTION + && (ev.motion.state & SDL_BUTTON_LMASK); + if (is_mouseclick) { + m_mousedown = true; + } else if (!is_hold_mousemove || !m_mousedown) { + m_mousedown = false; + return; + } + + Vector mouse_pos = VideoSystem::current()->get_viewport().to_logical( + ev.motion.x, ev.motion.y); + Vector pos( + (mouse_pos.x - m_image_rect.get_left()) + / (m_image_rect.get_right() - m_image_rect.get_left()), + (mouse_pos.y - m_image_rect.get_top()) + / (m_image_rect.get_bottom() - m_image_rect.get_top()) + ); + if (is_mouseclick + && (pos.x < 0.0f || pos.x > 1.0f || pos.y < 0.0f || pos.y > 1.0f)) { + m_mousedown = false; + return; + } + + // The hue is periodic -> go back to the start after the mouse leaves the + // corner + pos.x = fmodf(pos.x + 3.0f, 1.0f); + // The lightness is not periodic -> clamp + pos.y = math::clamp(pos.y, 0.0f, 1.0f); + float alpha = m_color.alpha; + m_color = get_pixel(m_image_with_pixels, pos); + m_color.alpha = alpha; +} + + +/* EOF */ diff --git a/src/gui/item_color_picker_2d.hpp b/src/gui/item_color_picker_2d.hpp new file mode 100644 index 00000000000..70159673411 --- /dev/null +++ b/src/gui/item_color_picker_2d.hpp @@ -0,0 +1,91 @@ +// SuperTux +// Copyright (C) 2024 HybridDog +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#ifndef HEADER_SUPERTUX_GUI_ITEM_COLOR_PICKER_2D_HPP +#define HEADER_SUPERTUX_GUI_ITEM_COLOR_PICKER_2D_HPP + +#include "gui/menu_item.hpp" + +#include "util/colorspace_oklab.hpp" +#include "video/color.hpp" +#include "video/sdl_surface_ptr.hpp" + +/** A two-dimensional color picker + * + * The color picker displays an image and enables picking a color from it by + * clicking. + * The image contains all fully-saturated sRGB colors, + * with the OKLab hue on the horizontal axis and modified OKLab lightness, Lr, + * on the vertical axis. + * Since the user can select the hue and lightness, and the chroma is fixed to + * the highest value, the color picker is only two-dimensional. + */ +class ItemColorPicker2D final : public MenuItem +{ +public: + ItemColorPicker2D(Color& col); + + /** Show an image with all fully-saturated sRGB colors, a marker for the + * currently selected color and a smaller marker for the initial color */ + virtual void draw(DrawingContext&, const Vector& pos, int menu_width, + bool active) override; + /// Determine the minimum width of the menu item + virtual int get_width() const override { return 280; } + virtual int get_height() const override { return get_width(); } + /** Handle mouse input */ + virtual void event(const SDL_Event& ev) override; + virtual bool changes_width() const override { return true; } + +private: + /** Draw a marker for a given color + * + * The position of the marker is calculated from col's OKLab Lr and hue + * values, and is unaffected by chroma. + * The inside of the marker shows col. + * The drawing uses the layers LAYER_GUI+1 and LAYER_GUI+2. + * + * @param canvas Target canvas where the marker is drawn onto + * @param col Color for which the marker is drawn + * @param radius Marker size + */ + void draw_marker(Canvas& canvas, Color col, float radius) const; + +private: + /// Image for drawing + SurfacePtr m_image; + /** The same image to sample pixels from it + * + * Since we cannot get pixel data from a Surface, we need this in addition to + * m_image. + */ + SDLSurfacePtr m_image_with_pixels; + /// Color during the initialisation of the menu item + Color m_original_color; + /// Color which the user dynamically modifies + Color& m_color; + /// Determines if the user is still holding down the left mouse button + bool m_mousedown = false; + /// Coordinates where m_image is currently drawn + Rectf m_image_rect{0, 0, 1, 1}; + +private: + ItemColorPicker2D(const ItemColorPicker2D&) = delete; + ItemColorPicker2D& operator=(const ItemColorPicker2D&) = delete; +}; + +#endif + +/* EOF */ diff --git a/src/gui/item_colorchannel_oklab.cpp b/src/gui/item_colorchannel_oklab.cpp deleted file mode 100644 index f9780eee62f..00000000000 --- a/src/gui/item_colorchannel_oklab.cpp +++ /dev/null @@ -1,227 +0,0 @@ -// SuperTux -// Copyright (C) 2021 -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -#include "gui/item_colorchannel.hpp" - -#include - -#include "math/util.hpp" -#include "video/drawing_context.hpp" -#include "video/video_system.hpp" -#include "video/viewport.hpp" - - -// This value affects the gamut clipping in the hue selection. -// A bigger value means more preserving of chroma instead of lightness. -constexpr float HUE_COLORFULNESS = 0.5f; - -ItemColorChannelOKLab::ItemColorChannelOKLab(Color* col, int channel, - Menu* menu) : - MenuItem(""), - m_color(col), - m_col_prev(0, 0, 0), - m_channel(ChannelType::CHANNEL_L), - m_menu(menu), - m_mousedown(false) -{ - if (channel == 1) - m_channel = ChannelType::CHANNEL_L; - else if (channel == 2) - m_channel = ChannelType::CHANNEL_C; - else - m_channel = ChannelType::CHANNEL_H; -} - -void -ItemColorChannelOKLab::draw(DrawingContext& context, const Vector& pos, - int menu_width, bool active) -{ - const float lw = static_cast(menu_width - 32); - ColorOKLCh col_oklch(0, 0, 0); - if (active) { - col_oklch = m_col_prev; - } else { - col_oklch = ColorOKLCh(*m_color); - } - - // Draw all possible colour values for the given component - float chroma_max_any_l = 1.0f; - if (m_channel == ChannelType::CHANNEL_C) - chroma_max_any_l = col_oklch.get_maximum_chroma_any_l(); - constexpr int NUM_RECTS = 128; - std::vector colors(NUM_RECTS+1); - for (int i = 0; i < NUM_RECTS+1; ++i) { - ColorOKLCh col_oklch_current = col_oklch; - float x = static_cast(i) / NUM_RECTS; - if (m_channel == ChannelType::CHANNEL_L) { - col_oklch_current.L = x; - } else if (m_channel == ChannelType::CHANNEL_C) { - col_oklch_current.C = x * chroma_max_any_l; - col_oklch_current.clip_lightness(); - } else { - col_oklch_current.h = (2.0f * x - 1.0f) * math::PI; - col_oklch_current.clip_adaptive_L0_L_cusp(HUE_COLORFULNESS); - } - colors[i] = col_oklch_current.to_srgb(); - } - for (int i = 0; i < NUM_RECTS; ++i) { - float x1 = 16 + static_cast(i) * lw / NUM_RECTS; - float x2 = x1 + lw / NUM_RECTS; - context.color().draw_gradient(colors[i], colors[i+1], LAYER_GUI-1, - GradientDirection::HORIZONTAL, - Rectf(pos + Vector(x1, -10), pos + Vector(x2, 10))); - } - - // Draw a marker for the current colour - float x_marker; - if (m_channel == ChannelType::CHANNEL_L) { - x_marker = col_oklch.L; - } else if (m_channel == ChannelType::CHANNEL_C) { - x_marker = chroma_max_any_l > 0.0f ? col_oklch.C / chroma_max_any_l : 0.0f; - } else { - x_marker = 0.5f * col_oklch.h / math::PI + 0.5f; - } - x_marker = pos.x + 16 + x_marker * lw; - context.color().draw_triangle(Vector(x_marker - 3, pos.y - 11), - Vector(x_marker + 3, pos.y - 11), Vector(x_marker, pos.y - 4), - Color::WHITE, LAYER_GUI-1); - context.color().draw_triangle(Vector(x_marker, pos.y + 4), - Vector(x_marker - 3, pos.y + 11), Vector(x_marker + 3, pos.y + 11), - Color::BLACK, LAYER_GUI-1); - - if (m_channel == ChannelType::CHANNEL_C && chroma_max_any_l > 0.0f) { - // Draw a marker where the lightness clipping starts - x_marker = col_oklch.get_maximum_chroma() / chroma_max_any_l; - x_marker = pos.x + 16 + x_marker * lw; - context.color().draw_triangle(Vector(x_marker - 2, pos.y - 11), - Vector(x_marker + 2, pos.y - 11), Vector(x_marker, pos.y), - Color(0.73f, 0.73f, 0.73f), LAYER_GUI-1); - } -} - -void -ItemColorChannelOKLab::process_action(const MenuAction& action) -{ - if (action == MenuAction::SELECT) { - m_col_prev = ColorOKLCh(*m_color); - return; - } - float increment; - if (action == MenuAction::LEFT) - increment = -0.1f; - else if (action == MenuAction::RIGHT) - increment = 0.1f; - else - return; - - ColorOKLCh col_oklch = m_col_prev; - ColorOKLCh col_oklch_clipped(0, 0, 0); - if (m_channel == ChannelType::CHANNEL_L) { - col_oklch.L = math::clamp(col_oklch.L + increment, 0.0f, 1.0f); - col_oklch_clipped = col_oklch; - } else if (m_channel == ChannelType::CHANNEL_C) { - float chroma_max = col_oklch.get_maximum_chroma_any_l(); - increment *= chroma_max; - col_oklch.C = math::clamp(col_oklch.C + increment, 0.0f, chroma_max); - col_oklch_clipped = col_oklch; - col_oklch_clipped.clip_lightness(); - } else { - increment *= 3.0f; - col_oklch.h = fmodf(col_oklch.h + increment + 3.0f * math::PI, - 2.0f * math::PI) - math::PI; - col_oklch_clipped = col_oklch; - col_oklch_clipped.clip_adaptive_L0_L_cusp(HUE_COLORFULNESS); - } - set_color(col_oklch_clipped, col_oklch); -} - -void -ItemColorChannelOKLab::event(const SDL_Event& ev) -{ - // Determine the new colour with the mouse position if either the mouse - // is clicked once or clicked and held down - bool is_mouseclick = ev.type == SDL_MOUSEBUTTONDOWN - && ev.button.button == SDL_BUTTON_LEFT; - bool is_hold_mousemove = ev.type == SDL_MOUSEMOTION - && (ev.motion.state & SDL_BUTTON_LMASK); - if (is_mouseclick) { - m_mousedown = true; - } else if (!is_hold_mousemove || !m_mousedown) { - m_mousedown = false; - return; - } - - Vector mouse_pos = VideoSystem::current()->get_viewport().to_logical( - ev.motion.x, ev.motion.y); - - // Calculate the menu item positions as passed in the draw method - Vector menu_centre = m_menu->get_center_pos(); - const float menu_width = m_menu->get_width(); - const float menu_height = m_menu->get_height(); - Vector pos( - menu_centre.x - menu_width / 2.0f, - menu_centre.y - + 24.0f * static_cast(m_menu->get_active_item_id()) - - menu_height / 2.0f + 12.0f - ); - - // Calculate the relative horizontal position - float x1 = pos.x + 16.0f; - float x2 = pos.x + menu_width - 16.0f; - float x = (mouse_pos.x - x1) / (x2 - x1); - if (m_channel != ChannelType::CHANNEL_H) { - x = math::clamp(x, 0.0f, 1.0f); - } else { - // The hue is periodic - x = fmodf(x + 3.0f, 1.0f); - } - - // Ignore distant mouse presses - if (x < -0.5f || x > 1.5f || mouse_pos.y > pos.y + menu_height / 2.0f - || mouse_pos.y < pos.y - menu_height / 2.0f) - return; - - ColorOKLCh col_oklch = m_col_prev; - ColorOKLCh col_oklch_clipped(0, 0, 0); - if (m_channel == ChannelType::CHANNEL_L) { - col_oklch.L = x; - col_oklch_clipped = col_oklch; - } else if (m_channel == ChannelType::CHANNEL_C) { - float chroma_max_any_l = col_oklch.get_maximum_chroma_any_l(); - col_oklch.C = x * chroma_max_any_l; - col_oklch_clipped = col_oklch; - col_oklch_clipped.clip_lightness(); - } else { - col_oklch.h = (2.0f * x - 1.0f) * math::PI; - col_oklch_clipped = col_oklch; - col_oklch_clipped.clip_adaptive_L0_L_cusp(HUE_COLORFULNESS); - } - set_color(col_oklch_clipped, col_oklch); -} - -void -ItemColorChannelOKLab::set_color(ColorOKLCh& col_oklch_clipped, - ColorOKLCh& col_oklch_store) -{ - // Save the current unclipped colour - m_col_prev = col_oklch_store; - // Convert the colour back to sRGB and clip if needed; preserve transparency - float alpha = m_color->alpha; - *m_color = col_oklch_clipped.to_srgb(); - m_color->alpha = alpha; -} - -/* EOF */ diff --git a/src/gui/item_colorchannel_rgba.cpp b/src/gui/item_colorchannel_rgba.cpp index 2448522e348..fae55b8639a 100644 --- a/src/gui/item_colorchannel_rgba.cpp +++ b/src/gui/item_colorchannel_rgba.cpp @@ -14,7 +14,7 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -#include "gui/item_colorchannel.hpp" +#include "gui/item_colorchannel_rgba.hpp" #include diff --git a/src/gui/item_colorchannel.hpp b/src/gui/item_colorchannel_rgba.hpp similarity index 65% rename from src/gui/item_colorchannel.hpp rename to src/gui/item_colorchannel_rgba.hpp index 4e234a1f913..c7264a0175d 100644 --- a/src/gui/item_colorchannel.hpp +++ b/src/gui/item_colorchannel_rgba.hpp @@ -14,8 +14,8 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -#ifndef HEADER_SUPERTUX_GUI_ITEM_COLORCHANNEL_HPP -#define HEADER_SUPERTUX_GUI_ITEM_COLORCHANNEL_HPP +#ifndef HEADER_SUPERTUX_GUI_ITEM_COLORCHANNEL_RGBA_HPP +#define HEADER_SUPERTUX_GUI_ITEM_COLORCHANNEL_RGBA_HPP #include "gui/menu_item.hpp" @@ -65,37 +65,6 @@ class ItemColorChannelRGBA final : public MenuItem ItemColorChannelRGBA& operator=(const ItemColorChannelRGBA&) = delete; }; - -class ItemColorChannelOKLab final : public MenuItem -{ -public: - enum class ChannelType { CHANNEL_L, CHANNEL_C, CHANNEL_H }; - -public: - ItemColorChannelOKLab(Color* col, int channel, Menu* menu); - virtual void draw(DrawingContext&, const Vector& pos, int menu_width, - bool active) override; - /** Returns the minimum width of the menu item. */ - virtual int get_width() const override { return 64; } - virtual void process_action(const MenuAction& action) override; - virtual void event(const SDL_Event& ev) override; - virtual bool changes_width() const override { return true; } - -private: - void set_color(ColorOKLCh& col_oklch_clipped, ColorOKLCh& col_oklch_store); - -private: - Color* m_color; - ColorOKLCh m_col_prev; - ChannelType m_channel; - Menu* m_menu; - bool m_mousedown; - -private: - ItemColorChannelOKLab(const ItemColorChannelOKLab&) = delete; - ItemColorChannelOKLab& operator=(const ItemColorChannelOKLab&) = delete; -}; - #endif /* EOF */ diff --git a/src/gui/menu.cpp b/src/gui/menu.cpp index 8c235b05dcf..fc4486925ac 100644 --- a/src/gui/menu.cpp +++ b/src/gui/menu.cpp @@ -20,8 +20,9 @@ #include "gui/item_action.hpp" #include "gui/item_back.hpp" #include "gui/item_color.hpp" -#include "gui/item_colorchannel.hpp" +#include "gui/item_colorchannel_rgba.hpp" #include "gui/item_colordisplay.hpp" +#include "gui/item_color_picker_2d.hpp" #include "gui/item_controlfield.hpp" #include "gui/item_floatfield.hpp" #include "gui/item_goto.hpp" @@ -267,10 +268,9 @@ Menu::add_color_channel_rgba(float* input, Color channel, int id, bool is_linear return add_item(input, channel, id, is_linear); } -ItemColorChannelOKLab& -Menu::add_color_channel_oklab(Color* color, int channel) -{ - return add_item(color, channel, this); +ItemColorPicker2D& +Menu::add_color_picker_2d(Color& color) { + return add_item(color); } ItemPaths& @@ -308,7 +308,7 @@ Menu::add_images(const std::vector& image_paths, int max_image_widt { return add_item(image_paths, max_image_width, max_image_height, id); } - + ItemList& Menu::add_list(const std::string& text, const std::vector& items, std::string* value_ptr, int id) { diff --git a/src/gui/menu.hpp b/src/gui/menu.hpp index 0e0a1d05053..59362245315 100644 --- a/src/gui/menu.hpp +++ b/src/gui/menu.hpp @@ -30,7 +30,7 @@ class ItemAction; class ItemBack; class ItemColor; class ItemColorChannelRGBA; -class ItemColorChannelOKLab; +class ItemColorPicker2D; class ItemColorDisplay; class ItemControlField; class ItemFloatField; @@ -102,7 +102,7 @@ class Menu ItemColorDisplay& add_color_display(Color* color, int id = -1); ItemColorChannelRGBA& add_color_channel_rgba(float* input, Color channel, int id = -1, bool is_linear = false); - ItemColorChannelOKLab& add_color_channel_oklab(Color* color, int channel); + ItemColorPicker2D& add_color_picker_2d(Color& color); ItemPaths& add_path_settings(const std::string& text, PathObject& target, const std::string& path_ref); ItemStringArray& add_string_array(const std::string& text, std::vector& items, int id = -1); ItemImages& add_images(const std::string& image_path, int max_image_width = 0, int max_image_height = 0, int id = -1); diff --git a/src/gui/menu_color.cpp b/src/gui/menu_color.cpp index a071989b932..af068748b14 100644 --- a/src/gui/menu_color.cpp +++ b/src/gui/menu_color.cpp @@ -24,9 +24,7 @@ ColorMenu::ColorMenu(Color* color_) : add_label(_("Mix the colour")); add_hl(); - add_color_channel_oklab(color, 1); - add_color_channel_oklab(color, 2); - add_color_channel_oklab(color, 3); + add_color_picker_2d(*color); add_color_channel_rgba(&(color->red), Color::RED); add_color_channel_rgba(&(color->green), Color::GREEN); add_color_channel_rgba(&(color->blue), Color::BLUE); diff --git a/src/util/colorspace_oklab.cpp b/src/util/colorspace_oklab.cpp index ffc17375a89..e7e35af8b24 100644 --- a/src/util/colorspace_oklab.cpp +++ b/src/util/colorspace_oklab.cpp @@ -40,16 +40,8 @@ namespace { struct ColorRGB { float r, g, b; - bool is_valid() const; }; -bool -ColorRGB::is_valid() const -{ - return r >= 0.0f && r <= 1.0f && g >= 0.0f && g <= 1.0f - && b >= 0.0f && b <= 1.0f; -} - struct ColorOKLab { float L, a, b; }; @@ -65,24 +57,6 @@ ColorRGB srgb_to_linear_srgb(const Color& c) return {to_linear(c.red), to_linear(c.green), to_linear(c.blue)}; } -Color linear_srgb_to_srgb(const ColorRGB& c) -{ - auto make_nonlinear = [&](float channel) -> float { - if (channel <= 0.0031308f) - return 12.92f * channel; - else - return (1.0f + 0.055f) * powf(channel, 1.0f / 2.4f) - 0.055f; - }; - float r = make_nonlinear(c.r); - float g = make_nonlinear(c.g); - float b = make_nonlinear(c.b); - // The clamping here is only for safety against numerical precision errors. - // r, g and b should already be in [0,1] (at least approximately) - // since they were clipped in the OKLab colourspace. - return Color(math::clamp(r, 0.0f, 1.0f), math::clamp(g, 0.0f, 1.0f), - math::clamp(b, 0.0f, 1.0f)); -} - ColorOKLab linear_srgb_to_oklab(const ColorRGB& c) { float l = 0.4122214708f * c.r + 0.5363325363f * c.g + 0.0514459929f * c.b; @@ -100,215 +74,15 @@ ColorOKLab linear_srgb_to_oklab(const ColorRGB& c) }; } -ColorRGB oklab_to_linear_srgb(const ColorOKLab& c) -{ - float l_ = c.L + 0.3963377774f * c.a + 0.2158037573f * c.b; - float m_ = c.L - 0.1055613458f * c.a - 0.0638541728f * c.b; - float s_ = c.L - 0.0894841775f * c.a - 1.2914855480f * c.b; - - float l = l_*l_*l_; - float m = m_*m_*m_; - float s = s_*s_*s_; - - return { - +4.0767416621f * l - 3.3077115913f * m + 0.2309699292f * s, - -1.2684380046f * l + 2.6097574011f * m - 0.3413193965f * s, - -0.0041960863f * l - 0.7034186147f * m + 1.7076147010f * s, - }; -} - ColorOKLCh lab_to_lch(const ColorOKLab& c) { return ColorOKLCh{c.L, sqrtf(c.a * c.a + c.b * c.b), atan2f(c.b, c.a)}; } -ColorOKLab lch_to_lab(const ColorOKLCh& c) -{ - return {c.L, c.C * cosf(c.h), c.C * sinf(c.h)}; -} - -// Finds the maximum saturation possible for a given hue that fits in sRGB -// Saturation here is defined as S = C/L -// a and b must be normalized so a^2 + b^2 == 1. -float compute_max_saturation(float a, float b) -{ - // Max saturation will be when one of r, g or b goes below zero. - // Select different coefficients depending on which component goes below zero first. - float k0, k1, k2, k3, k4, wl, wm, ws; - - if (-1.88170328f * a - 0.80936493f * b > 1) - { - // Red component. - k0 = +1.19086277f; k1 = +1.76576728f; k2 = +0.59662641f; k3 = +0.75515197f; k4 = +0.56771245f; - wl = +4.0767416621f; wm = -3.3077115913f; ws = +0.2309699292f; - } - else if (1.81444104f * a - 1.19445276f * b > 1) - { - // Green component. - k0 = +0.73956515f; k1 = -0.45954404f; k2 = +0.08285427f; k3 = +0.12541070f; k4 = +0.14503204f; - wl = -1.2681437731f; wm = +2.6097574011f; ws = -0.3413193965f; - } - else - { - // Blue component. - k0 = +1.35733652f; k1 = -0.00915799f; k2 = -1.15130210f; k3 = -0.50559606f; k4 = +0.00692167f; - wl = -0.0041960863f; wm = -0.7034186147f; ws = +1.7076147010f; - } - - // Approximate max saturation using a polynomial: - float S = k0 + k1 * a + k2 * b + k3 * a * a + k4 * a * b; - - // Do one step Halley's method to get closer - // this gives an error less than 10e6, except for some blue hues where the dS/dh is close to infinite - // this should be sufficient for most applications, otherwise do two/three steps. - - float k_l = +0.3963377774f * a + 0.2158037573f * b; - float k_m = -0.1055613458f * a - 0.0638541728f * b; - float k_s = -0.0894841775f * a - 1.2914855480f * b; - - { - float l_ = 1.f + S * k_l; - float m_ = 1.f + S * k_m; - float s_ = 1.f + S * k_s; - - float l = l_ * l_ * l_; - float m = m_ * m_ * m_; - float s = s_ * s_ * s_; - - float l_dS = 3.f * k_l * l_ * l_; - float m_dS = 3.f * k_m * m_ * m_; - float s_dS = 3.f * k_s * s_ * s_; - - float l_dS2 = 6.f * k_l * k_l * l_; - float m_dS2 = 6.f * k_m * k_m * m_; - float s_dS2 = 6.f * k_s * k_s * s_; - - float f = wl * l + wm * m + ws * s; - float f1 = wl * l_dS + wm * m_dS + ws * s_dS; - float f2 = wl * l_dS2 + wm * m_dS2 + ws * s_dS2; - - S = S - f * f1 / (f1*f1 - 0.5f * f * f2); - } - - return S; -} - -// Finds L_cusp and C_cusp for a given hue -// a and b must be normalized so a^2 + b^2 == 1. -struct OKLabCusp { - float L, C; -}; -OKLabCusp find_cusp(float a, float b) -{ - // First, find the maximum saturation (saturation S = C/L). - float S_cusp = compute_max_saturation(a, b); - - // Convert to linear sRGB to find the first point where at least one of r,g or b >= 1: - ColorOKLab c = {1, S_cusp * a, S_cusp * b}; - ColorRGB rgb_at_max = oklab_to_linear_srgb(c); - float L_cusp = cbrtf(1.f / std::max(std::max(rgb_at_max.r, rgb_at_max.g), - rgb_at_max.b)); - float C_cusp = L_cusp * S_cusp; - - return {L_cusp , C_cusp}; -} - -// Finds intersection of the line defined by -// L = L0 * (1 - t) + t * L1; -// C = t * C1; -// a and b must be normalized so a^2 + b^2 == 1. -float find_gamut_intersection(float a, float b, float L1, float C1, float L0) -{ - // Find the cusp of the gamut triangle. - OKLabCusp cusp = find_cusp(a, b); - - // Find the intersection for upper and lower half seprately. - float t; - if (((L1 - L0) * cusp.C - (cusp.L - L0) * C1) <= 0.f) - { - // Lower half. - - t = cusp.C * L0 / (C1 * cusp.L + cusp.C * (L0 - L1)); - } - else - { - // Upper half. - - // First intersect with triangle. - t = cusp.C * (L0 - 1.f) / (C1 * (cusp.L - 1.f) + cusp.C * (L0 - L1)); - - // Then one step Halley's method. - { - float dL = L1 - L0; - float dC = C1; - - float k_l = +0.3963377774f * a + 0.2158037573f * b; - float k_m = -0.1055613458f * a - 0.0638541728f * b; - float k_s = -0.0894841775f * a - 1.2914855480f * b; - - float l_dt = dL + dC * k_l; - float m_dt = dL + dC * k_m; - float s_dt = dL + dC * k_s; - - - // If higher accuracy is required, 2 or 3 iterations of the following block can be used: - { - float L = L0 * (1.f - t) + t * L1; - float C = t * C1; - - float l_ = L + C * k_l; - float m_ = L + C * k_m; - float s_ = L + C * k_s; - - float l = l_ * l_ * l_; - float m = m_ * m_ * m_; - float s = s_ * s_ * s_; - - float ldt = 3 * l_dt * l_ * l_; - float mdt = 3 * m_dt * m_ * m_; - float sdt = 3 * s_dt * s_ * s_; - - float ldt2 = 6 * l_dt * l_dt * l_; - float mdt2 = 6 * m_dt * m_dt * m_; - float sdt2 = 6 * s_dt * s_dt * s_; - - float r = 4.0767416621f * l - 3.3077115913f * m + 0.2309699292f * s - 1; - float r1 = 4.0767416621f * ldt - 3.3077115913f * mdt + 0.2309699292f * sdt; - float r2 = 4.0767416621f * ldt2 - 3.3077115913f * mdt2 + 0.2309699292f * sdt2; - - float u_r = r1 / (r1 * r1 - 0.5f * r * r2); - float t_r = -r * u_r; - - float g = -1.2681437731f * l + 2.6097574011f * m - 0.3413193965f * s - 1; - float g1 = -1.2681437731f * ldt + 2.6097574011f * mdt - 0.3413193965f * sdt; - float g2 = -1.2681437731f * ldt2 + 2.6097574011f * mdt2 - 0.3413193965f * sdt2; - - float u_g = g1 / (g1 * g1 - 0.5f * g * g2); - float t_g = -g * u_g; - - b = -0.0041960863f * l - 0.7034186147f * m + 1.7076147010f * s - 1; - float b1 = -0.0041960863f * ldt - 0.7034186147f * mdt + 1.7076147010f * sdt; - float b2 = -0.0041960863f * ldt2 - 0.7034186147f * mdt2 + 1.7076147010f * sdt2; - - float u_b = b1 / (b1 * b1 - 0.5f * b * b2); - float t_b = -b * u_b; - - t_r = u_r >= 0.f ? t_r : FLT_MAX; - t_g = u_g >= 0.f ? t_g : FLT_MAX; - t_b = u_b >= 0.f ? t_b : FLT_MAX; - - t += std::min(t_r, std::min(t_g, t_b)); - } - } - } - - return t; -} - } // namespace -ColorOKLCh::ColorOKLCh(Color& c) : +ColorOKLCh::ColorOKLCh(const Color& c) : L(0.0f), C(0.0f), h(0.0f) @@ -317,119 +91,20 @@ ColorOKLCh::ColorOKLCh(Color& c) : ColorOKLab lab = linear_srgb_to_oklab(rgb); *this = lab_to_lch(lab); if (C < 0.00001f) - // Deterministic behaviour when increasing chroma of greyscale colours. + // Deterministic behaviour for greyscale colors h = 0.0f; } -Color -ColorOKLCh::to_srgb() const -{ - ColorOKLCh c = *this; - ColorOKLab lab = lch_to_lab(c); - ColorRGB rgb = oklab_to_linear_srgb(lab); - if (!rgb.is_valid()) { - c.clip_chroma(); - // Gamut clipping; reduce chroma when needed. - lab = lch_to_lab(c); - rgb = oklab_to_linear_srgb(lab); - } - if (!(rgb.r > -0.001f && rgb.r < 1.001f && rgb.g > -0.001f && rgb.g < 1.001f - && rgb.b > -0.001f && rgb.b < 1.001f)) { - log_warning << "Colour out of bounds (after clipping): (" << rgb.r << - ", " << rgb.g << ", " << rgb.b << ")" << std::endl; - } - return linear_srgb_to_srgb(rgb); -} - float -ColorOKLCh::get_maximum_chroma() const -{ - if (C <= 0.0f || L <= 0.0f || L >= 1.0f) - return 0.0f; - return find_gamut_intersection(cosf(h), sinf(h), L, 1.0f, L); -} - -float -ColorOKLCh::get_maximum_chroma_any_l() const -{ - OKLabCusp cusp = find_cusp(cosf(h), sinf(h)); - return cusp.C; -} - -void -ColorOKLCh::clip_chroma() -{ - // Avoid numerical problems for certain hues of blue. - if (-1.67462f < h && h < -1.67460f) - h = -1.67462f; - - L = math::clamp(L, 0.0f, 1.0f); - C = math::clamp(C, 0.0f, get_maximum_chroma()); -} - -void -ColorOKLCh::clip_lightness() -{ - // Avoid numerical problems for certain hues of blue. - if (-1.67462f < h && h < -1.67460f) - h = -1.67462f; - - L = math::clamp(L, 0.0f, 1.0f); - ColorOKLab lab = lch_to_lab(*this); - ColorRGB rgb = oklab_to_linear_srgb(lab); - if (rgb.is_valid()) - return; - - OKLabCusp cusp = find_cusp(lab.a / C, lab.b / C); - if (C >= cusp.C) { - // The cusp is the most colourful point for the given hue. - C = cusp.C; - L = cusp.L; - return; - } - // Select a point inside the triangle defined by (L,C) in {(0,0), (1,0), cusp} - // and then move it further if it's not in the sRGB gamut. - if (L > cusp.L) { - // Reduce L so that it's on the triangle. - L = std::min(L, 1.0f + C * (cusp.L - 1.0f) / cusp.C); - // Reduce L so that it's in the sRGB gamut. - float L0 = -100.0f; - float t = find_gamut_intersection(lab.a / C, lab.b / C, L, C, L0); - L = (1.0f - t) * L0 + t * L; - C *= t; - } else { - // Here the triangle is accurate. - L = std::max(L, C * cusp.L / cusp.C); - } -} - -void -ColorOKLCh::clip_adaptive_L0_L_cusp(float alpha) -{ - // Avoid numerical problems for certain hues of blue. - if (-1.67462f < h && h < -1.67460f) - h = -1.67462f; - - ColorOKLab lab = lch_to_lab(*this); - ColorRGB rgb = oklab_to_linear_srgb(lab); - if (rgb.is_valid()) - return; - - float a_ = lab.a / C; - float b_ = lab.b / C; - OKLabCusp cusp = find_cusp(a_, b_); - - float Ld = L - cusp.L; - float k = 2.f * (Ld > 0 ? 1.f - cusp.L : cusp.L); - - float e1 = 0.5f * k + fabsf(Ld) + alpha * C / k; - float sgn = Ld < 0.0f ? -1.0f : 1.0f; - float L0 = cusp.L + 0.5f * (sgn * (e1 - sqrtf( - std::max(e1 * e1 - 2.f * k * fabsf(Ld), 0.0f)))); - - float t = find_gamut_intersection(a_, b_, L, C, L0); - L = (1.0f - t) * L0 + t * L; - C *= t; +ColorOKLCh::get_modified_lightness() const +{ + // The formula is from + // https://bottosson.github.io/posts/colorpicker/#intermission---a-new-lightness-estimate-for-oklab + constexpr float k_1 = 0.206f; + constexpr float k_2 = 0.03f; + constexpr float k_3 = (1.f + k_1) / (1.f + k_2); + return 0.5f * (k_3 * L - k_1 + sqrtf((k_3 * L - k_1) * (k_3 * L - k_1) + + 4 * k_2 * k_3 * L)); } /* EOF */ diff --git a/src/util/colorspace_oklab.hpp b/src/util/colorspace_oklab.hpp index 5c1d215f10d..0d65be50997 100644 --- a/src/util/colorspace_oklab.hpp +++ b/src/util/colorspace_oklab.hpp @@ -18,6 +18,8 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. +#ifndef HEADER_UTIL_COLORSPACE_OKLAB_HPP +#define HEADER_UTIL_COLORSPACE_OKLAB_HPP class Color; @@ -25,32 +27,14 @@ struct ColorOKLCh final { ColorOKLCh(float pL, float pC, float ph) : L(pL), C(pC), h(ph) {} // Convert an non-linear sRGB colour to OKLab's LCh - ColorOKLCh(Color& c); + ColorOKLCh(const Color& c); - // Convert to non-linear sRGB; clip_chroma is applied if required. - Color to_srgb() const; - - // Find the maximum chroma which is still representable in sRGB while the - // lightness and hue are preserved - float get_maximum_chroma() const; - - // Find the maximum chroma which is still representable in sRGB while the - // hue is preserved - float get_maximum_chroma_any_l() const; - - // Reduce the chroma so that the colour can be represented in sRGB. - // Also clamp the lightness if needed. - void clip_chroma(); - - // Change the lightness so that the colour can be represented in sRGB. - void clip_lightness(); - - // Changes both the lightness and chroma so that the colour can be represented - // in sRGB. The resulting colour should have less visual distance to the true - // colour than colour produced by clipping only chroma or lightness. - void clip_adaptive_L0_L_cusp(float alpha=0.05f); + // Calculate a different lightness estimate which has less dark values + float get_modified_lightness() const; float L, C, h; }; +#endif + /* EOF */ diff --git a/src/video/canvas.cpp b/src/video/canvas.cpp index 1fe2bf91a49..e627a6ed886 100644 --- a/src/video/canvas.cpp +++ b/src/video/canvas.cpp @@ -17,6 +17,7 @@ #include "video/canvas.hpp" #include +#include #include "supertux/globals.hpp" #include "util/log.hpp" @@ -331,6 +332,26 @@ Canvas::draw_triangle(const Vector& pos1, const Vector& pos2, const Vector& pos3 m_requests.push_back(request); } +void +Canvas::draw_hexagon(const Vector& pos, float radius, const Color& color, + int layer) +{ + float radius2 = radius * sqrtf(0.8f); + float x_off_small = radius * sqrtf(0.2f); + std::array offsets{ + Vector(-x_off_small, -radius2), + Vector(x_off_small, -radius2), + Vector(-radius, 0), + Vector(radius, 0), + Vector(-x_off_small, radius2), + Vector(x_off_small, radius2), + }; + for (size_t i = 0; i < offsets.size() - 2; ++i) { + draw_triangle(pos + offsets[i], pos + offsets[i + 1], pos + offsets[i + 2], + color, layer); + } +} + void Canvas::get_pixel(const Vector& position, const std::shared_ptr& color_out) { diff --git a/src/video/canvas.hpp b/src/video/canvas.hpp index cdccdd884f7..aee91aa2489 100644 --- a/src/video/canvas.hpp +++ b/src/video/canvas.hpp @@ -82,6 +82,16 @@ class Canvas final void draw_line(const Vector& pos1, const Vector& pos2, const Color& color, int layer); void draw_triangle(const Vector& pos1, const Vector& pos2, const Vector& pos3, const Color& color, int layer); + /** Draw a flat-topped regular hexagon + * + * @param pos Centre position + * @param radius Radius of the hexagon's circle hull + * @param color Color + * @param layer Layer + */ + void draw_hexagon(const Vector& pos, float radius, const Color& color, + int layer); + /** on next update, set color to lightmap's color at position */ void get_pixel(const Vector& position, const std::shared_ptr& color_out); diff --git a/tests/unit/util/colorspace_oklab.cpp b/tests/unit/util/colorspace_oklab.cpp index 005f37a3562..cfc8ac9342e 100644 --- a/tests/unit/util/colorspace_oklab.cpp +++ b/tests/unit/util/colorspace_oklab.cpp @@ -40,67 +40,4 @@ TEST(ColorOKLCh, ctor_Color) EXPECT_NEAR(color.h, -1.9393998f, 0.001f); } -TEST(ColorOKLCh, to_srgb) -{ - Color col(.1f, 1.f, 0.f); - ColorOKLCh color(col); - - col = color.to_srgb(); - - EXPECT_NEAR(col.red, .1f, 0.001f); - EXPECT_NEAR(col.green, 1.f, 0.001f); - EXPECT_NEAR(col.blue, 0.f, 0.001f); -} - -TEST(ColorOKLCh, get_maximum_chroma) -{ - ColorOKLCh color(.6f, 1.f, .3f); - - float chroma = color.get_maximum_chroma(); - - EXPECT_NEAR(chroma, .24032472f, 0.001f); -} - -TEST(ColorOKLCh, get_maximum_chroma_any_l) -{ - ColorOKLCh color(0.f, .1f, .05f); - - float chroma = color.get_maximum_chroma_any_l(); - - EXPECT_NEAR(chroma, .26009744f, 0.001f); -} - -TEST(ColorOKLCh, clip_chroma) -{ - ColorOKLCh color(.45f, .67f, .12f); - - color.clip_chroma(); - - EXPECT_NEAR(color.L, .45f, 0.001f); - EXPECT_NEAR(color.C, .1804768f, 0.001f); - EXPECT_NEAR(color.h, .12f, 0.001f); -} - -TEST(ColorOKLCh, clip_lightness) -{ - ColorOKLCh color(.45f, .67f, .12f); - - color.clip_lightness(); - - EXPECT_NEAR(color.L, .64147455f, 0.001f); - EXPECT_NEAR(color.C, .2572695f, 0.001f); - EXPECT_NEAR(color.h, .12f, 0.001f); -} - -TEST(ColorOKLCh, clip_adaptive_L0_L_cusp) -{ - ColorOKLCh color(1.f, 1.f, 1.f); - - color.clip_adaptive_L0_L_cusp(0.25f); - - EXPECT_NEAR(color.L, .83574224f, 0.001f); - EXPECT_NEAR(color.C, .10836758f, 0.001f); - EXPECT_NEAR(color.h, 1.f, 0.001f); -} - /* EOF */