From 78cf29ba29aede2f0463e1747dc728787428d543 Mon Sep 17 00:00:00 2001 From: Albrecht Schlosser Date: Wed, 28 Dec 2022 17:50:00 +0100 Subject: [PATCH] Improve and extend fl_contrast() (#370) - Add internal fl_contrast_cielab() as the new default. - Keep old function as internal fl_contrast_legacy(). - Add fl_contrast_mode() to switch between fl_contrast() functions. - Add fl_contrast_level() to fine tune fl_contrast() per mode. - Add option to register and use a custom contrast function. - Add test/contrast.cxx test program. - Move all fl_contrast() related code to a new file src/fl_contrast.cxx. - Add fl_lightness() convenience function for perceived lightness. - Add fl_luminance() convenience function for physical luminance. --- FL/Enumerations.H | 48 ++++- src/CMakeLists.txt | 1 + src/Makefile | 1 + src/fl_color.cxx | 30 --- src/fl_contrast.cxx | 488 ++++++++++++++++++++++++++++++++++++++++++++ src/makedepend | 9 + test/.gitignore | 1 + test/CMakeLists.txt | 1 + test/Makefile | 4 + test/contrast.cxx | 454 +++++++++++++++++++++++++++++++++++++++++ test/demo.menu | 11 +- test/makedepend | 45 ++++ 12 files changed, 1057 insertions(+), 36 deletions(-) create mode 100644 src/fl_contrast.cxx create mode 100644 test/contrast.cxx diff --git a/FL/Enumerations.H b/FL/Enumerations.H index 56b96f42d..9d40152d6 100644 --- a/FL/Enumerations.H +++ b/FL/Enumerations.H @@ -1118,7 +1118,53 @@ are free for the user to be given any value using Fl::set_color(). */ FL_EXPORT Fl_Color fl_inactive(Fl_Color c); -FL_EXPORT Fl_Color fl_contrast(Fl_Color fg, Fl_Color bg); +/** + Type of a custom fl_contrast() function. + + Use this signature to define your own custom fl_contrast() function together + with fl_contrast_mode(FL_CONTRAST_CUSTOM). + Example: + \code + Fl_Color my_contrast(Fl_Color fg, Fl_Color bg, Fl_Fontsize fs, int context) { + // calculate contrast and ... + return color; + } + // call this early in your main() program: + fl_contrast_function(my_contrast); + fl_contrast_mode(FL_CONTRAST_CUSTOM); + \endcode + + \see fl_contrast(Fl_Color, Fl_Color, Fl_Fontsize, int) + \see fl_contrast_mode(int) +*/ +typedef Fl_Color (Fl_Contrast_Function)(Fl_Color, Fl_Color, Fl_Fontsize, int); + +FL_EXPORT void fl_contrast_function(Fl_Contrast_Function *f); + +/** + Define the possible modes to calculate fl_contrast(). +*/ +enum Fl_Contrast_Mode { + FL_CONTRAST_NONE = 0, ///< always return foreground color + FL_CONTRAST_LEGACY, ///< legacy (FLTK 1.3.x) contrast function + FL_CONTRAST_CIELAB, ///< new (FLTK 1.4.0) default function + FL_CONTRAST_CUSTOM, ///< optional custom contrast function + FL_CONTRAST_LAST ///< internal use only (invalid contrast mode) +}; + +// The following functions are defined and documented in src/fl_contrast.cxx + +FL_EXPORT void fl_contrast_level(int level); +FL_EXPORT int fl_contrast_level(); +FL_EXPORT void fl_contrast_mode(int mode); +FL_EXPORT int fl_contrast_mode(); + +FL_EXPORT Fl_Color fl_contrast(Fl_Color fg, Fl_Color bg, Fl_Fontsize fs = 0, int context = 0); + +FL_EXPORT double fl_lightness(Fl_Color color); +FL_EXPORT double fl_luminance(Fl_Color color); + +// Other color functions are defined and documented in src/fl_color.cxx FL_EXPORT Fl_Color fl_color_average(Fl_Color c1, Fl_Color c2, float weight); diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 8994d63e3..c26363972 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -132,6 +132,7 @@ set (CPPFILES fl_ask.cxx fl_boxtype.cxx fl_color.cxx + fl_contrast.cxx fl_cursor.cxx fl_curve.cxx fl_diamond_box.cxx diff --git a/src/Makefile b/src/Makefile index 819cd7b9b..dddb7fcb4 100644 --- a/src/Makefile +++ b/src/Makefile @@ -135,6 +135,7 @@ CPPFILES = \ fl_ask.cxx \ fl_boxtype.cxx \ fl_color.cxx \ + fl_contrast.cxx \ fl_cursor.cxx \ fl_curve.cxx \ fl_diamond_box.cxx \ diff --git a/src/fl_color.cxx b/src/fl_color.cxx index d925a73ee..dd1f07066 100644 --- a/src/fl_color.cxx +++ b/src/fl_color.cxx @@ -168,36 +168,6 @@ Fl_Color fl_inactive(Fl_Color c) { return fl_color_average(c, FL_GRAY, .33f); } -/** - Returns a color that contrasts with the background color. - - This will be the foreground color if it contrasts sufficiently with the - background color. Otherwise, returns \p FL_WHITE or \p FL_BLACK depending - on which color provides the best contrast. - \param[in] fg,bg foreground and background colors - \return contrasting color - */ -Fl_Color fl_contrast(Fl_Color fg, Fl_Color bg) { - unsigned c1, c2; // RGB colors - int l1, l2; // Luminosities - - // Get the RGB values for each color... - if (fg & 0xffffff00) c1 = (unsigned)fg; - else c1 = fl_cmap[fg]; - - if (bg & 0xffffff00) c2 = (unsigned)bg; - else c2 = fl_cmap[bg]; - - // Compute the luminosity... - l1 = ((c1 >> 24) * 30 + ((c1 >> 16) & 255) * 59 + ((c1 >> 8) & 255) * 11) / 100; - l2 = ((c2 >> 24) * 30 + ((c2 >> 16) & 255) * 59 + ((c2 >> 8) & 255) * 11) / 100; - - // Compare and return the contrasting color... - if ((l1 - l2) > 99) return fg; - else if ((l2 - l1) > 99) return fg; - else if (l2 > 127) return FL_BLACK; - else return FL_WHITE; -} /** \} */ diff --git a/src/fl_contrast.cxx b/src/fl_contrast.cxx new file mode 100644 index 000000000..4f9718e52 --- /dev/null +++ b/src/fl_contrast.cxx @@ -0,0 +1,488 @@ +// +// Color contrast functions for the Fast Light Tool Kit (FLTK). +// +// Copyright 1998-2022 by Bill Spitzak and others. +// +// This library is free software. Distribution and use rights are outlined in +// the file "COPYING" which should have been included with this file. If this +// file is missing or damaged, see the license at: +// +// https://www.fltk.org/COPYING.php +// +// Please see the following page on how to report bugs and issues: +// +// https://www.fltk.org/bugs.php +// + +/** + \file fl_contrast.cxx + \brief Color contrast handling + + Implementation of fl_contrast() and its variants. +*/ + +#include +#include + +// Initial values of global/static variables defined by fl_contrast_* functions. + +// This defines the default contrast mode since FLTK 1.4.0 +static int fl_contrast_mode_ = FL_CONTRAST_CIELAB; + +// This defines the default contrast level per contrast mode +static int fl_contrast_level_[10] = { + 0, 50, 55, 0, 0, 0, 0, 0, 0, 0 +}; + +// There is no default custom contrast function +static Fl_Contrast_Function *fl_contrast_function_ = 0; + + +// The following function is (and must be!) the same as Fl::get_color() +// but can be inlined. We need this additional implementation because all contrast +// related functions have been moved from fl_color.cxx to fl_contrast.cxx +// or have been directly implemented in fl_contrast.cxx (new functions). +// Inlining will hopefully prevent an extra function call. + +extern unsigned fl_cmap[256]; // defined in fl_color.cxx + +inline unsigned get_color(Fl_Color i) { // see Fl::get_color() ! + if (i & 0xffffff00) return (i); + else return fl_cmap[i]; +} + + +/** \addtogroup fl_attributes + \{ */ + +/** + Return the raw / physical luminance of a color. + + This function calculates the physical luminance of Fl_Color \p color. + + The returned luminance value (aka \p Y) is the physical luminance of the + Fl_Color \p color. + + The result is in the range 0.0 (black) to 1.0 (white). + + \note This is probably not what you want if you are interested in perceived + contrast or lightness calculation because the luminance \p Y is \b not linear + with respect to human perception. + + See fl_lightness(Fl_Color) for a function that returns the perceived lightness + of a color which can be used directly for contrast calculation. + + \param[in] color Fl_Color value + \return Raw (physical) luminance (0.0 .. 1.0) + + \since 1.4.0 + + \see fl_lightness(Fl_Color) +*/ +double fl_luminance(Fl_Color color) { + + // Get the sRGB (0xrrggbb) components of the FLTK color + unsigned col = get_color(color) >> 8; + + int r = (col & 0xFF0000) >> 16; + int g = (col & 0x00FF00) >> 8; + int b = (col & 0x0000FF); + + return (0.2126729 * pow(r/255.0, 2.4) + + 0.7151522 * pow(g/255.0, 2.4) + + 0.0721750 * pow(b/255.0, 2.4)); +} + + +/** + Return the perceived lightness of a color. + + This function calculates the perceived lightness of Fl_Color \p color. + + The returned lightness value \p Lstar according to the CIELAB (L*a*b*) + color model is almost linear with respect to human perception. It is in + the range 0 (black) to 100 (white). + + The result values of two colors can be compared directly and the difference + is their perceived contrast. + + \param[in] color Fl_Color value + \return perceived lightness (0 .. 100) + + \since 1.4.0 +*/ +double fl_lightness(Fl_Color color) { + + // compute the raw luminance Y (0.0 .. 1.0) + + double Y = fl_luminance(color); + + // return the perceived lightness L* (Lstar) + + if (Y <= (216/24389.)) + return Y * (24389/27.); + else + return pow(Y, (1/3.)) * 116 - 16; +} + + +/** + Set the contrast level (sensitivity) of the fl_contrast() method. + + This can be used to tune the legacy fl_contrast() function to achieve + slightly better results. The default value is defined per contrast mode + (see below). Values between 50 and 70 are recommended but you can raise + it up to 100. Lower values than 50 are probably not useful. + + The contrast \p level affects not only the legacy (1.3.x) fl_contrast() + function but also the new CIELAB contrast mode which is the default since + FLTK 1.4.0. + + Other contrast modes are currently not affected by the contrast level. + + You may use the contrast level if you define your own custom contrast + function in mode FL_CONTRAST_CUSTOM. + + \note All contrast modes store their own contrast level because the + behavior is slightly different. You must change the contrast mode + fl_contrast_mode() \b before you set or get the contrast level. + + The default contrast level is + - 50 in mode FL_CONTRAST_LEGACY (compatible with FLTK 1.3.x) + - 55 in mode FL_CONTRAST_CIELAB + - 0 (undefined) for all other modes + + See the description of fl_contrast_mode(int mode) for more information about + the contrast level per mode. + + Example: + \code + fl_contrast_mode(FL_CONTRAST_LEGACY); + fl_contrast_level(65); + \endcode + + A \p level greater than 50 (probably best in the range 50 to 70) may achieve + better results of the legacy fl_contrast() function in some border cases of + low contrast between foreground and background colors but we recommend to + use the new default algorithm \c FL_CONTRAST_CIELAB unless you need strict + backwards compatibility or use a CPU constrained embedded system. + + \param[in] level valid range is 0 to 100 + + \since 1.4.0 +*/ +void fl_contrast_level(int level) { + if (level < 0) level = 0; + else if (level > 100) level = 100; + fl_contrast_level_[fl_contrast_mode_] = level; +} + +/** + Get the contrast level (sensitivity) of the fl_contrast() method. + + This returns the level of the currently selected contrast mode. + + \return The current contrast level. + + \see fl_contrast_level(int level) + \see fl_contrast_mode(int mode) + + \since 1.4.0 +*/ +int fl_contrast_level() { + return fl_contrast_level_[fl_contrast_mode_]; +} + +/** + Set the contrast algorithm (mode). + + You can use one of + + - FL_CONTRAST_NONE (not recommended: returns the foreground color) + - FL_CONTRAST_LEGACY (same as in FLTK 1.3.x) + - FL_CONTRAST_CIELAB (default since FLTK 1.4.0) + - FL_CONTRAST_CUSTOM (you must define your own contrast algorithm) + + If you set FL_CONTRAST_CUSTOM you must also register your custom + contrast function by calling fl_contrast_function(). + + You may set the contrast level fl_contrast_level(int) after setting + the contrast mode. This affects the contrast algorithm as described + below: + + - FL_CONTRAST_LEGACY: default level is 50 which is compatible with + FLTK 1.3.x and older. This mode is no longer the default and is + not recommended because it doesn't take human contrast perception + into account and doesn't properly handle sRGB color values. You + may get better contrasts if you set the level higher than 50. + Values in the range 50 to 70 may be useful. Higher values result + in higher contrast, i.e. the algorithm switches "earlier" to + black or white mode. + + - FL_CONTRAST_CIELAB: defaut level is 55 which appears to be a good + value. The higher the level is, the more contrast is to be expected. + Values in the range below 55 accept lower contrast and values above + 55 switch "earlier" to black or white. Values between 45 and 65 may + yield usable contrast experience. + + \param[in] mode if invalid, FL_CONTRAST_CIELAB will be selected + + \since 1.4.0 + + \see fl_contrast_function(Fl_Contrast_Function *) + \see fl_contrast_level(int) +*/ +void fl_contrast_mode(int mode) { + if (mode >= 0 && mode < FL_CONTRAST_LAST) + fl_contrast_mode_ = mode; + else + fl_contrast_mode_ = FL_CONTRAST_CIELAB; +} + +/** + Return the current contrast algorithm (mode). + + \return Contrast algorithm (mode). + + \since 1.4.0 + + \see fl_contrast_mode(int) +*/ +int fl_contrast_mode() { + return fl_contrast_mode_; +} + +/** + Register a custom contrast function. + + Your custom contrast function will be called when fl_contrast() is + called if and only if you previously registered your function and + called fl_contrast_mode(FL_CONTRAST_CUSTOM) . + + Your custom contrast function must provide the signature + \code + Fl_Color my_contrast_function(Fl_Color fg, Fl_Color bg, Fl_Fontsize fs, int context) + \endcode + + The arguments are the same as for the full fl_contrast() function since FLTK 1.4. + You can use the supplied fontsize \p fs to modify the result. Depending on the + caller the \p fs parameter can be 0 (default) or a valid fontsize. + + The \p context parameter is not yet used and will always be 0 unless included in + a call to fl_contrast(). The value 0 should be interpreted as text. + In the future the \p context argument will be used to supply a different context + than text (small icons, large icons, etc.). The exact usage is not yet specified. + + Your function may also use fl_contrast_level() to modify the result accordingly. + + \since 1.4.0 + + \see fl_contrast_mode(int) + \see fl_contrast_level(int) + \see fl_contrast() +*/ +void fl_contrast_function(Fl_Contrast_Function *f) { + fl_contrast_function_ = f; +} + +/* + Returns a color that contrasts with the background color. + + This is functionally identical to the algorithm used in FLTK 1.3.x, + modified only to utilize fl_contrast_level() (since 1.4.0). + + *** This function is intentionally not public and not documented. + *** Do not change this except for level adjustment (backwards compatibility). + + Note: this is fast but *inaccurate* WRT human contrast perception. + The default since FLTK 1.4 is to use fl_contrast_cielab(). + + \param[in] fg,bg foreground and background colors + \param[in] fs,context fontsize and context (unused) + \return contrasting color +*/ +static Fl_Color fl_contrast_legacy(Fl_Color fg, Fl_Color bg, Fl_Fontsize fs, int context) { + + (void) fs; // currently ignored + (void) context; // currently ignored + + // internal static variables, recalculated only if fl_contrast_level() is changed + + static int level = 50; // default, compatible with FLTK 1.3.x + static int tc = 99; // sufficient contrast threshold (99 <=> 38.82 %) + static int tbw = 127; // black/white threshold (127 <=> 49.80 %) + + // new in FLTK 1.4: adjust thresholds if fl_contrast_level() was changed + + if (fl_contrast_level() != level) { + level = fl_contrast_level(); + if (level == 100) + tc = 256; + else if (level == 0) + tc = 0; + else if (level > 50) + tc = 99 + ((level - 50) * (255 - 99) / 50); + else + tc = 99 - ((50 - level) * 99 / 50); + } + + // Get the real sRGB values for each color... + unsigned cfg = get_color(fg); + unsigned cbg = get_color(bg); + + // Compute the luminance for each color (0 .. 255) + // Note: FLTK 1.3 compatible, don't change this! + + int lfg = ((cfg >> 24) * 30 + ((cfg >> 16) & 255) * 59 + ((cfg >> 8) & 255) * 11) / 100; + int lbg = ((cbg >> 24) * 30 + ((cbg >> 16) & 255) * 59 + ((cbg >> 8) & 255) * 11) / 100; + + int lc = lfg - lbg; // calculated contrast (-255 .. 255) + + // Compare and return the contrasting color... + + if (lc > tc || lc < -tc) return fg; // sufficient contrast + if (lbg > tbw) return FL_BLACK; // light background + return FL_WHITE; // dark background +} + +/* + Returns a color that contrasts with the background color. + + ** This function is intentionally not public and not documented. ** + + This is an improved algorithm compared to the one used in FLTK 1.3.x. + It is slower (uses floating point and pow()) but is much more + accurate WRT human contrast perception. + + \param[in] fg,bg foreground and background colors + \param[in] fs,context unused: fontsize and context + \return contrasting color +*/ +static Fl_Color fl_contrast_cielab(Fl_Color fg, Fl_Color bg, Fl_Fontsize fs, int context) { + + (void) fs; // currently ignored + (void) context; // currently ignored + + double tc = (double)fl_contrast_level(); // sufficient contrast threshold + double tbw = 55.; // black/white threshold + + // Compute the perceived lightness L* (Lstar) and the contrast + + double lfg = fl_lightness(fg); + double lbg = fl_lightness(bg); + double lc = lfg - lbg; + + // Compare and return the contrasting color... + if (lc >= tc || lc <= -tc) return fg; // sufficient contrast + if (lbg > tbw) return (Fl_Color)FL_BLACK; // black + return (Fl_Color)FL_WHITE; // white +} + + +/** + Returns a color that contrasts with the background color. + + This will be the foreground color if it contrasts sufficiently with the + background color. Otherwise, returns \p FL_WHITE or \p FL_BLACK depending + on which color provides the best contrast. + + FLTK 1.4.0 uses a different default contrast function than earlier releases + (1.3.x) but you can use the old "legacy" contrast function by calling + \code + fl_contrast_mode(FL_CONTRAST_LEGACY); + \endcode + early in your main program. + + \note It is a known issue that static initialization using fl_contrast() may already + have been executed before you call this function in main(). You should be aware of + this and, if necessary, write your own (static) contrast initialization function. + This should rarely be necessary. + + You can change the behavior of fl_contrast() in several ways: + + - Change the "level" (sensitivity) for contrast calculation, see fl_contrast_level(). + Valid levels are 0 - 100, the default "medium" value is 50. If you raise the level + above 50 the overall contrast will generally be higher, i.e. the required contrast + to return the foreground color is raised and therefore the calculated color will + switch "earlier" to either black or white. + In other words, using the following values: + - 0 will always use the foreground color + - 50 will use the default, unmodified algorithm + - 100 will always use black or white + - values larger than 50 may yield slightly better results. + Changing the \p level is particularly useful and intended for the "legacy mode" + to improve the results partially. Values slightly above 50 (50 - 70) will likely + return the best results (50 is the default, as used in FLTK 1.3.x). + + \note Different contrast modes (algorithms) can use their own values and + defaults of fl_contrast_level(). + + - Change the used contrast calculation function. You can either use the old + (FLTK 1.3.x) function or use the better but slower function based on the + CIELAB (L*a*b*) color model, or you can define your own custom contrast + function if you need even better contrast results. + + The following contrast functions are available: + + - FL_CONTRAST_LEGACY, the old FLTK 1.3.x compatible function. This is the + fastest function (using integer arithmetic) but it provides worse results + in border cases. You may want to use this on embedded or otherwise CPU + constrained systems or if you need strict backwards compatibility. + For slightly better results you may utilize the new fl_contrast_level(int) + function (since 1.4.0) to increase the contrast sensitivity. This will + provide slightly better results than FLTK 1.3.x and earlier but we recommend + to use the new default function: + + - FL_CONTRAST_CIELAB, based on the CIELAB (L*a*b*) color model. This function + is superior regarding the visual contrast perception but may be slower. + This is the default since FLTK 1.4.0. + + - FL_CONTRAST_CUSTOM, your own contrast calculation function. + + In the future we \b may provide even more (and superior) contrast algorithms. + + The new parameters \p fs and \p context (since 1.4.0) are defined for future + extensions and are currently not used. Default values are 0. + + \note These new optional parameters must be provided in the custom contrast + function which is the reason why they are added now. In the future we may use + the fontsize to adjust the calculated contrast, and users defining their own + contrast functions may use them in their functions. + + \param[in] fg foreground (text) color + \param[in] bg background color + \param[in] fs font size (optional, default = 0 == undefined) + \param[in] context graphical context (optional, default = 0 == text) + + \return contrasting color: \p fg, \p FL_BLACK, or \p FL_WHITE + + \see fl_contrast_level(int) + \see fl_contrast_mode(int) + \see fl_contrast_function() +*/ +Fl_Color fl_contrast(Fl_Color fg, Fl_Color bg, Fl_Fontsize fs, int context) { + + switch (fl_contrast_mode_) { + + case FL_CONTRAST_LEGACY: + return fl_contrast_legacy(fg, bg, fs, context); + + case FL_CONTRAST_CUSTOM: + if (fl_contrast_function_) + return (fl_contrast_function_)(fg, bg, fs, context); + + // FALLTHROUGH + + case FL_CONTRAST_CIELAB: + return fl_contrast_cielab(fg, bg, fs, context); + + default: // unknown (none): return fg + break; + } + return fg; + +} // fl_contrast() + +/** + \} + */ diff --git a/src/makedepend b/src/makedepend index dfcd68586..5db28d5a8 100644 --- a/src/makedepend +++ b/src/makedepend @@ -1371,6 +1371,15 @@ Fl_compose.o: ../FL/Fl_Valuator.H Fl_compose.o: ../FL/Fl_Widget.H Fl_compose.o: ../FL/platform_types.h Fl_compose.o: Fl_Screen_Driver.H +fl_contrast.o: ../FL/Enumerations.H +fl_contrast.o: ../FL/Fl.H +fl_contrast.o: ../FL/Fl_Cairo.H +fl_contrast.o: ../FL/fl_casts.H +fl_contrast.o: ../FL/fl_config.h +fl_contrast.o: ../FL/Fl_Export.H +fl_contrast.o: ../FL/fl_types.h +fl_contrast.o: ../FL/fl_utf8.h +fl_contrast.o: ../FL/platform_types.h Fl_Copy_Surface.o: ../FL/Enumerations.H Fl_Copy_Surface.o: ../FL/Fl.H Fl_Copy_Surface.o: ../FL/Fl_Bitmap.H diff --git a/test/.gitignore b/test/.gitignore index 49b1817e0..1e43541ac 100644 --- a/test/.gitignore +++ b/test/.gitignore @@ -30,6 +30,7 @@ clipboard clock colbrowser color_chooser +contrast coordinates cube CubeView diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 05333ee81..fc8e26ed1 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -72,6 +72,7 @@ CREATE_EXAMPLE (clipboard clipboard.cxx "fltk_images;fltk") CREATE_EXAMPLE (clock clock.cxx fltk) CREATE_EXAMPLE (colbrowser colbrowser.cxx fltk) CREATE_EXAMPLE (color_chooser color_chooser.cxx fltk) +CREATE_EXAMPLE (contrast contrast.cxx fltk) CREATE_EXAMPLE (coordinates coordinates.cxx fltk) CREATE_EXAMPLE (cursor cursor.cxx fltk) CREATE_EXAMPLE (curve curve.cxx fltk) diff --git a/test/Makefile b/test/Makefile index 3b9afa5eb..df0aee017 100644 --- a/test/Makefile +++ b/test/Makefile @@ -65,6 +65,7 @@ CPPFILES =\ clock.cxx \ colbrowser.cxx \ color_chooser.cxx \ + contrast.cxx \ cube.cxx \ CubeMain.cxx \ CubeView.cxx \ @@ -159,6 +160,7 @@ ALL = \ clock$(EXEEXT) \ colbrowser$(EXEEXT) \ color_chooser$(EXEEXT) \ + contrast$(EXEEXT) \ cursor$(EXEEXT) \ curve$(EXEEXT) \ demo$(EXEEXT) \ @@ -394,6 +396,8 @@ colbrowser$(EXEEXT): colbrowser.o color_chooser$(EXEEXT): color_chooser.o +contrast$(EXEEXT): contrast.o + cursor$(EXEEXT): cursor.o curve$(EXEEXT): curve.o diff --git a/test/contrast.cxx b/test/contrast.cxx new file mode 100644 index 000000000..5cd340d13 --- /dev/null +++ b/test/contrast.cxx @@ -0,0 +1,454 @@ +// +// Contrast function test program for the Fast Light Tool Kit (FLTK). +// +// Copyright 2022 by Bill Spitzak and others. +// +// This library is free software. Distribution and use rights are outlined in +// the file "COPYING" which should have been included with this file. If this +// file is missing or damaged, see the license at: +// +// https://www.fltk.org/COPYING.php +// +// Please see the following page on how to report bugs and issues: +// +// https://www.fltk.org/bugs.php +// + +// Note: this test and demo program is work in progress. It is published +// because it is helpful but it needs some more work to be "perfect" ;-) +// AlbrechtS + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +// program version + +const char *version = "0.9.0"; + +// prototypes and forward declarations + +static void button_cb(Fl_Widget *w, void *v); +static Fl_Color calc_contrast(Fl_Color fg, Fl_Color bg, Fl_Fontsize fs); + +// class Button + +class Button : public Fl_Button { + char lbuf[20]; // private label buffer + Fl_Color ocol_; // "original" (label) color + int idx_; // button index (0 - 255) +public: + Button(int X, int Y, int W, int H, int n) + : Fl_Button(X, Y, W, H, "") { + idx_ = n; + box(FL_THIN_DOWN_BOX); + label(lbuf); + callback(button_cb); + color((Fl_Color)n); + sprintf(lbuf, "%03d", n); + set_labelcolor(n); + labelsize(15); + labelfont(FL_HELVETICA); + } + + void set_labelcolor(Fl_Color col) { + ocol_ = col; + labelcolor(calc_contrast(col, color(), labelsize())); + } + + Fl_Color ocol() { + return ocol_; + } + + int idx() { + return idx_; + } + + virtual void draw() { + draw_box(); + // draw small filled rectangle with "original" color + fl_color(ocol_); + fl_rectf(x() + 5, y() + 5, 10, h() - 10); + // measure and draw label + int lw = 0, lh = 0; + fl_font(labelfont(), labelsize()); + fl_measure(lbuf, lw, lh); + fl_color(labelcolor()); + fl_draw(lbuf, x() + 15 + (w() - lw - 15) / 2, y() + h() - (h() - lh) / 2 - lh/4); + fl_color(FL_BLACK); + } + +}; // class Button + +// global variables + +Fl_Simple_Terminal *term = 0; + +double g_lfg; // perceived lightness of foreground color +double g_lbg; // perceived lightness of background color +double g_lcref; // calculated contrast reference (CIELAB, L*a*b*) +int g_selected = -1; // selected button: -1 = none, 0 - 255 = valid button + +Fl_Fontsize g_fs = 15; // fontsize for button labels +int g_level = 0; // *init* fl_contrast_level (sensitivity) + +int g_algo = FL_CONTRAST_LEGACY;// contrast algorithm: 0 = none, 1 = legacy (1.3.x), 2 = CIELAB, 3 = custom +const char *alch = ""; // algorithm as char: "LEGACY", "CIELAB" , or "CUSTOM" + +Fl_Color lcolor = FL_BLACK; // label color, set by slider callback +Button *buttons[256]; // array of color buttons +Fl_Value_Slider *sliders[6]; // array of sliders (gray, red, green, blue, level, fontsize) +Fl_Output *color_out = 0; // color output (RRGGBB) + +// Custom contrast algorithm: currently a dummy function (returns fg). +// This may be used to define a "better" contrast function in user code + +static Fl_Color custom_contrast(Fl_Color fg, Fl_Color bg, Fl_Fontsize fs, int) { + return fg; +} + +/* + Local function to calculate the contrast and store it in some + global variables for display purposes and logging. + + This function is a wrapper around fl_contrast() in this demo program. +*/ +static Fl_Color calc_contrast(Fl_Color fg, Fl_Color bg, Fl_Fontsize fs) { + + // Compute and set global *perceived* lightness L* (Lstar) and contrast for display + + g_lfg = fl_lightness(fg); + g_lbg = fl_lightness(bg); + g_lcref = g_lfg - g_lbg; // perceived contrast (light on dark = positive) + + switch (g_algo) { + case FL_CONTRAST_NONE: // 0 = none (return fg) + case FL_CONTRAST_LEGACY: // 1 = legacy (FLTK 1.3.x) + case FL_CONTRAST_CIELAB: // 2 = CIELAB (L*a*b*) + case FL_CONTRAST_CUSTOM: // 3 = Custom + return fl_contrast(fg, bg, fs); + default: + break; + } + return fg; +} + +// set all button label colors and adjust fontsize (labelsize) + +static void update_labels() { + for (int i = 0; i < 256; i++) { + buttons[i]->set_labelcolor(lcolor); + buttons[i]->labelsize(g_fs); + } +} + +static void button_cb(Fl_Widget *w, void *v) { + Button *b = (Button *)w; + g_selected = b->idx(); // selected button index + Fl_Color ocol = Fl::get_color(b->ocol()); // button's "original" label color (RGB0) + Fl_Color fg = Fl::get_color(b->labelcolor()); // button's label color (RGB0) + Fl_Color bg = Fl::get_color(b->color()); // button's background color (RGB0) + calc_contrast(ocol, bg, g_fs); // calculate values to be displayed + const char *color = ""; // calculated label color (text) + if (fg == ocol) color = "fg"; + else if (fg == 0xffffff00) color = "WHITE"; + else if (fg == 0x0) color = "BLACK"; + term->printf("[%s] fg: %06x, bg: %06x, lfg: %6.2f, lbg: %6.2f, lc: %7.2f, %s => %-5s", + b->label(), ocol >> 8, bg >> 8, g_lfg, g_lbg, g_lcref, alch, color); + if (g_algo == FL_CONTRAST_LEGACY || + g_algo == FL_CONTRAST_CIELAB) + term->printf(" (level = %3d)\n", g_level); + else + term->printf("\n"); +} + +void lf_cb(Fl_Widget *w, void *v) { + term->printf("\n"); +} + +// callback for color (gray and R, G, B) sliders + +void color_slider_cb(Fl_Widget *w, void *v) { + int n = fl_int(v); // slider type: 0 = gray, 1 = color + unsigned int r, g, b; + if (n == 0) { // gray slider + int val = (int)sliders[0]->value(); + lcolor = fl_rgb_color(val, val, val); // set gray value + sliders[1]->value(val); // set r/g/b values as well + sliders[2]->value(val); + sliders[3]->value(val); + r = g = b = val; + } else { // any color slider + r = (unsigned int)sliders[1]->value(); + g = (unsigned int)sliders[2]->value(); + b = (unsigned int)sliders[3]->value(); + lcolor = fl_rgb_color(r, g, b); // set color value + } + // update button label colors + update_labels(); + // output label color + char color_buf[10]; + sprintf(color_buf, "%02X %02X %02X", r, g, b); + color_out->value(color_buf); + w->window()->redraw(); +} + +// callback for "level" and "fontsize" sliders + +void slider_cb(Fl_Widget *w, void *v) { + int n = fl_int(v); // slider type: 1 = level, 2 = fontsize + switch (n) { + case 1: // fl_contrast_level() + g_level = (int)sliders[n + 3]->value(); + fl_contrast_level(g_level); // set/store current contrast level + break; + case 2: // 2nd slider: fontsize (labelsize) + g_fs = (int)sliders[n + 3]->value(); + break; + default: + break; + } + // update button label colors + update_labels(); + w->window()->redraw(); +} + +// callback for contrast algorithm (radio buttons) + +void algo_cb(Fl_Widget *w, void *v) { + int val = fl_int(v); + g_algo = val; + switch(val) { + case FL_CONTRAST_LEGACY: alch = "LEGACY"; fl_contrast_mode(val); break; // legacy 1.3.x + case FL_CONTRAST_CIELAB: alch = "CIELAB"; fl_contrast_mode(val); break; // CIELAB L*a*b* + case FL_CONTRAST_CUSTOM: alch = "CUSTOM"; fl_contrast_mode(val); break; // custom + case FL_CONTRAST_NONE: + default: + alch = "none "; + fl_contrast_mode(FL_CONTRAST_NONE); + break; + } + g_level = fl_contrast_level(); // get current contrast level (per mode) + sliders[4]->value(g_level); // set level slider value + update_labels(); // update all button labels + + // print selected button's attributes + if (g_selected >= 0) { + button_cb(buttons[g_selected], (void *)0); + } + if (w) + w->window()->redraw(); +} + +// color chooser callback +void color_cb(Fl_Widget *w, void *v) { + Fl_Color_Chooser *cc = (Fl_Color_Chooser *)w; + int r = (int)(cc->r() * 255); + int g = (int)(cc->g() * 255); + int b = (int)(cc->b() * 255); + Fl_Color c = fl_rgb_color(r, g, b); + Button *bt = buttons[255]; // last button + bt->color(c); + bt->set_labelcolor(lcolor); + bt->redraw(); +} + +// =============================================================== +// ====================== main() program ====================== +// =============================================================== + +int main(int argc, char **argv) { + + const int bw = 58; + const int bh = 30; + int cw = 16 * bw + 10; + int ch = 16 * bh + 10; + int ww = cw + 10; + int wh = 16 * bh + 135 + 10 + 150 /* terminal */ + 10; + Fl_Double_Window window(ww, wh, "fl_contrast test"); + + int n = 0; + Button **b = buttons; + for (int y = 0; y < 16; y++) { + for (int x = 0; x < 16; x++) { + *b = new Button(x * bw + 10, y * bh + 10, bw, bh, n); + (*b)->set_labelcolor(n); + b++; + n++; + } + } + + // sliders for label color (gray, red, green, blue) + + const int sx = 10 + bw; + const int sw = 5 * bw; + const int sh = 25; + int sy = ch + 10; + + Fl_Hor_Value_Slider *gray = new Fl_Hor_Value_Slider(sx, sy, sw, sh, "gray"); + gray->color(0xdddddd00); + gray->textsize(13); + gray->align(FL_ALIGN_LEFT); + gray->value(0); + gray->bounds(0, 255); + gray->step(1); + gray->callback(color_slider_cb, (void *)0); + sy += sh + 10; + + Fl_Hor_Value_Slider *red = new Fl_Hor_Value_Slider(sx, sy, sw, sh, "red"); + red->color(FL_RED); + red->textcolor(FL_WHITE); + red->textsize(13); + red->align(FL_ALIGN_LEFT); + red->value(0); + red->bounds(0, 255); + red->step(1); + red->callback(color_slider_cb, (void *)1); + sy += sh + 5; + + Fl_Hor_Value_Slider *green = new Fl_Hor_Value_Slider(sx, sy, sw, sh, "green"); + green->color(FL_GREEN); + green->textsize(13); + green->align(FL_ALIGN_LEFT); + green->value(0); + green->bounds(0, 255); + green->step(1); + green->callback(color_slider_cb, (void *)1); + sy += sh + 5; + + Fl_Hor_Value_Slider *blue = new Fl_Hor_Value_Slider(sx, sy, sw, sh, "blue"); + blue->color(FL_BLUE); + blue->textcolor(FL_WHITE); + blue->textsize(13); + blue->align(FL_ALIGN_LEFT); + blue->value(0); + blue->bounds(0, 255); + blue->step(1); + blue->callback(color_slider_cb, (void *)1); + + sliders[0] = gray; + sliders[1] = red; + sliders[2] = green; + sliders[3] = blue; + + // contrast algorithm selection group + + int cgx = 10 + 6*bw + 10; + int cgy = ch + 30; + int cgw = 90; + int cgh = 100; + int abh = 25; + + Fl_Group *cg = new Fl_Group(cgx, cgy, cgw, cgh, "fl_contrast:"); + cg->align(FL_ALIGN_TOP); + cg->box(FL_FRAME); + + Fl_Radio_Round_Button *anon = new Fl_Radio_Round_Button(cgx, cgy, cgw, abh, "none"); + Fl_Radio_Round_Button *aleg = new Fl_Radio_Round_Button(cgx, cgy + 25, cgw, abh, "LEGACY"); + Fl_Radio_Round_Button *acie = new Fl_Radio_Round_Button(cgx, cgy + 50, cgw, abh, "CIELAB"); + Fl_Radio_Round_Button *aapc = new Fl_Radio_Round_Button(cgx, cgy + 75, cgw, abh, "CUSTOM"); + aleg->value(1); + anon->callback(algo_cb, (void *)0); + aleg->callback(algo_cb, (void *)1); + acie->callback(algo_cb, (void *)2); + aapc->callback(algo_cb, (void *)3); + + cg->end(); + + color_out = new Fl_Output(10 + 10 * bw, ch + 10, 100, 30, "label color:"); + color_out->align(FL_ALIGN_LEFT); + color_out->textfont(FL_COURIER); + color_out->textsize(16); + color_out->value("00 00 00"); + + // light blue "level" slider + + Fl_Hor_Value_Slider *s_level = new Fl_Hor_Value_Slider(10 + 9 * bw, red->y(), 3 * bw - 15, sh, "level"); + s_level->color(231); + s_level->textcolor(224); + s_level->textsize(13); + s_level->align(FL_ALIGN_LEFT); + s_level->step(1); + s_level->bounds(0, 100); + s_level->value(g_level); + s_level->callback(slider_cb, (void *)1); + s_level->tooltip("set contrast sensitivity level (0-100), default: 50"); + + // labelsize slider + + Fl_Hor_Value_Slider *s_fs = new Fl_Hor_Value_Slider(10 + 9 * bw, green->y(), 3 * bw - 15, sh, "labelsize"); + s_fs->color(231); + s_fs->textcolor(224); + s_fs->textsize(13); + s_fs->align(FL_ALIGN_LEFT); + s_fs->step(1); + s_fs->bounds(8, 24); + s_fs->value(15); + s_fs->callback(slider_cb, (void *)2); + s_fs->tooltip("set label/text fontsize"); + + sliders[4] = s_level; + sliders[5] = s_fs; + + // line feed (LF) button + + Fl_Button *lf = new Fl_Button(10 + 8 * bw, blue->y(), bw, sh, "LF"); + lf->tooltip("Click to output a linefeed to the log."); + lf->callback(lf_cb); + + // color chooser for field #255 + + int ccx = 10 + 12 * bw; + int ccy = ch + 10; + int ccw = 4 * bw; + int cch = 120; + + Fl_Color_Chooser *color_chooser = new Fl_Color_Chooser(ccx, ccy, ccw, cch); + color_chooser->callback(color_cb); + color_chooser->label("bg color [255] @->"); + color_chooser->rgb(1, 1, 1); + color_chooser->mode(1); // byte mode + color_chooser->align(FL_ALIGN_LEFT_BOTTOM); + + // simple terminal for output (FLTK 1.4 only) + + int ttx = 10; + int tty = color_chooser->y() + cch + 10; + int ttw = window.w() - 20; + int tth = window.h() - tty - 10; + + term = new Fl_Simple_Terminal(ttx, tty, ttw, tth); + term->color(FL_WHITE); + term->textcolor(FL_BLACK); + term->textsize(13); + + term->printf("FLTK fl_contrast() test program with different contrast algorithms, version %s\n", version); + term->printf("FLTK version %d.%d.%d\n", FL_MAJOR_VERSION, FL_MINOR_VERSION, FL_PATCH_VERSION); + term->printf(" - Select a foreground (text) color with the gray or red/green/blue sliders (displayed inside each field).\n"); + term->printf(" - Select an arbitrary background color for field #255 with the color chooser.\n"); + term->printf(" - Select a colored field (by clicking on it) to display its attributes.\n"); + term->printf(" - Select the contrast algorithm by clicking on the radio buttons.\n"); + term->printf(" - Tune the contrast algorithm with the light blue \"level\" slider (default: 50).\n"); + + // set contrast mode and level, update button label colors + + fl_contrast_mode(g_algo); + fl_contrast_function(custom_contrast); // dummy function + algo_cb(NULL, fl_voidptr(g_algo)); // updates button labels + + window.resizable(window); + window.end(); + window.show(argc, argv); + return Fl::run(); +} diff --git a/test/demo.menu b/test/demo.menu index 2d5750634..16cce02da 100644 --- a/test/demo.menu +++ b/test/demo.menu @@ -61,7 +61,6 @@ @main:Fluid\n(UI design tool):fluid valuators.fl @main:Cool\nDemos...:@e - @e:X Color\nBrowser:colbrowser rgb.txt @e:Mandelbrot:mandelbrot @e:Fractals:fractals @e:Puzzle:glpuzzle @@ -72,17 +71,19 @@ @e:Clipboard\nViewer:clipboard @main:Other\nTests...:@o - @o:Color Choosers:color_chooser @o:File Chooser:file_chooser @o:Native File Chooser:native-filechooser - @o:Font Tests...:@of - @of:Fonts:fonts - @of:UTF-8:utf8 @o:HelpDialog:help_dialog help_dialog.html @o:Input Choice:input_choice @o:Preferences:preferences @o:Threading:threads @o:XForms Emulation:forms + @o:Colors and Fonts...:@of + @of:X Color\nBrowser:colbrowser rgb.txt + @of:Color Chooser:color_chooser + @of:Color Contrast:contrast + @of:Fonts:fonts + @of:UTF-8 Fonts:utf8 @main:Tutorial\nfrom\nManual...:@j @j:ask\n(modified):ask diff --git a/test/makedepend b/test/makedepend index a013aae50..f51a7416a 100644 --- a/test/makedepend +++ b/test/makedepend @@ -459,6 +459,51 @@ color_chooser.o: ../FL/platform.H color_chooser.o: ../FL/platform_types.h color_chooser.o: ../FL/x11.H color_chooser.o: list_visuals.cxx +contrast.o: ../FL/Enumerations.H +contrast.o: ../FL/Fl.H +contrast.o: ../FL/Fl_Bitmap.H +contrast.o: ../FL/Fl_Box.H +contrast.o: ../FL/Fl_Button.H +contrast.o: ../FL/Fl_Cairo.H +contrast.o: ../FL/fl_casts.H +contrast.o: ../FL/Fl_Choice.H +contrast.o: ../FL/Fl_Color_Chooser.H +contrast.o: ../FL/fl_config.h +contrast.o: ../FL/Fl_Device.H +contrast.o: ../FL/Fl_Double_Window.H +contrast.o: ../FL/fl_draw.H +contrast.o: ../FL/Fl_Export.H +contrast.o: ../FL/Fl_Graphics_Driver.H +contrast.o: ../FL/Fl_Group.H +contrast.o: ../FL/Fl_Hor_Value_Slider.H +contrast.o: ../FL/Fl_Image.H +contrast.o: ../FL/Fl_Input.H +contrast.o: ../FL/Fl_Input_.H +contrast.o: ../FL/Fl_Light_Button.H +contrast.o: ../FL/Fl_Menu_.H +contrast.o: ../FL/Fl_Menu_Item.H +contrast.o: ../FL/Fl_Output.H +contrast.o: ../FL/Fl_Pixmap.H +contrast.o: ../FL/Fl_Plugin.H +contrast.o: ../FL/Fl_Preferences.H +contrast.o: ../FL/Fl_Radio_Round_Button.H +contrast.o: ../FL/Fl_Rect.H +contrast.o: ../FL/Fl_Return_Button.H +contrast.o: ../FL/Fl_RGB_Image.H +contrast.o: ../FL/Fl_Round_Button.H +contrast.o: ../FL/Fl_Scrollbar.H +contrast.o: ../FL/Fl_Simple_Terminal.H +contrast.o: ../FL/Fl_Slider.H +contrast.o: ../FL/Fl_Text_Buffer.H +contrast.o: ../FL/Fl_Text_Display.H +contrast.o: ../FL/fl_types.h +contrast.o: ../FL/fl_utf8.h +contrast.o: ../FL/Fl_Valuator.H +contrast.o: ../FL/Fl_Value_Input.H +contrast.o: ../FL/Fl_Value_Slider.H +contrast.o: ../FL/Fl_Widget.H +contrast.o: ../FL/Fl_Window.H +contrast.o: ../FL/platform_types.h cube.o: ../config.h cube.o: ../FL/Enumerations.H cube.o: ../FL/Fl.H