-
-
Notifications
You must be signed in to change notification settings - Fork 491
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[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.
- Loading branch information
Showing
14 changed files
with
295 additions
and
694 deletions.
There are no files selected for viewing
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 <http://www.gnu.org/licenses/>. | ||
|
||
#include "gui/item_color_picker_2d.hpp" | ||
|
||
#include <vector> | ||
|
||
#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<int>(pos.x * static_cast<float>(surface->w - 1) + 0.5f), | ||
0, | ||
surface->w - 1 | ||
); | ||
int y = math::clamp( | ||
static_cast<int>(pos.y * static_cast<float>(surface->h - 1) + 0.5f), | ||
0, | ||
surface->h - 1 | ||
); | ||
uint8_t *pixel = static_cast<uint8_t *>(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 */ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 <http://www.gnu.org/licenses/>. | ||
|
||
#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 */ |
Oops, something went wrong.