From 7f9cf6f45c04a331212a3ca4c84eedff1b277059 Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Mon, 30 Aug 2021 23:58:37 +0200 Subject: [PATCH] UIDefaultsLoader: added contrast() color function (inspired by Less CSS), which is useful to choose a foreground color that is readable, based on the luma (perceptual brightness) of a background color --- .../com/formdev/flatlaf/UIDefaultsLoader.java | 37 +++++++++++++--- .../formdev/flatlaf/util/ColorFunctions.java | 28 ++++++++++++ .../flatlaf/util/TestColorFunctions.java | 44 +++++++++++++++++++ .../themeeditor/FlatCompletionProvider.java | 6 +++ .../themeeditor/FlatThemeEditorOverlay.java | 24 +++++++--- .../themeeditor/FlatThemeFileEditor.java | 24 +++++++--- .../themeeditor/FlatThemeFileEditor.jfd | 5 +++ .../themeeditor/FlatThemeTokenMaker.java | 1 + .../theme-editor-test.properties | 29 ++++++++++++ 9 files changed, 180 insertions(+), 18 deletions(-) create mode 100644 flatlaf-core/src/test/java/com/formdev/flatlaf/util/TestColorFunctions.java diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/UIDefaultsLoader.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/UIDefaultsLoader.java index 1b6d1ab3..a63fd6bd 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/UIDefaultsLoader.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/UIDefaultsLoader.java @@ -611,6 +611,7 @@ class UIDefaultsLoader case "mix": return parseColorMix( null, params, resolver, reportError ); case "tint": return parseColorMix( "#fff", params, resolver, reportError ); case "shade": return parseColorMix( "#000", params, resolver, reportError ); + case "contrast": return parseColorContrast( params, resolver, reportError ); } } finally { parseColorDepth--; @@ -804,14 +805,10 @@ class UIDefaultsLoader if( color1Str == null ) color1Str = params.get( i++ ); String color2Str = params.get( i++ ); - int weight = 50; - - if( params.size() > i ) - weight = parsePercentage( params.get( i++ ) ); + int weight = (params.size() > i) ? parsePercentage( params.get( i ) ) : 50; // parse second color - String resolvedColor2Str = resolver.apply( color2Str ); - ColorUIResource color2 = (ColorUIResource) parseColorOrFunction( resolvedColor2Str, resolver, reportError ); + ColorUIResource color2 = (ColorUIResource) parseColorOrFunction( resolver.apply( color2Str ), resolver, reportError ); if( color2 == null ) return null; @@ -822,6 +819,34 @@ class UIDefaultsLoader return parseFunctionBaseColor( color1Str, function, false, resolver, reportError ); } + /** + * Syntax: contrast(color,dark,light[,threshold]) + * - color: a color to compare against + * - dark: a designated dark color (e.g. #000) or a color function + * - light: a designated light color (e.g. #fff) or a color function + * - threshold: the threshold (in range 0-100%) to specify where the transition + * from "dark" to "light" is (default is 43%) + */ + private static Object parseColorContrast( List params, Function resolver, boolean reportError ) { + String colorStr = params.get( 0 ); + String darkStr = params.get( 1 ); + String lightStr = params.get( 2 ); + int threshold = (params.size() > 3) ? parsePercentage( params.get( 3 ) ) : 43; + + // parse color to compare against + ColorUIResource color = (ColorUIResource) parseColorOrFunction( resolver.apply( colorStr ), resolver, reportError ); + if( color == null ) + return null; + + // check luma and determine whether to use dark or light color + String darkOrLightColor = (ColorFunctions.luma( color ) * 100 < threshold) + ? lightStr + : darkStr; + + // parse dark or light color + return parseColorOrFunction( resolver.apply( darkOrLightColor ), resolver, reportError ); + } + private static Object parseFunctionBaseColor( String colorStr, ColorFunction function, boolean derived, Function resolver, boolean reportError ) { diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/util/ColorFunctions.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/util/ColorFunctions.java index 0b7e99f9..14fe2ce0 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/util/ColorFunctions.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/util/ColorFunctions.java @@ -79,6 +79,34 @@ public class ColorFunctions Math.round( a2 + ((a1 - a2) * weight) ) ); } + /** + * Calculates the luma (perceptual brightness) of the given color. + *

+ * Uses SMPTE C / Rec. 709 coefficients, as recommended in + * WCAG 2.0. + * + * @param color a color + * @return the luma (in range 0-1) + * + * @see https://en.wikipedia.org/wiki/Luma_(video) + * @since 1.6 + */ + public static float luma( Color color ) { + // see https://en.wikipedia.org/wiki/Luma_(video) + // see https://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef + // see https://github.com/less/less.js/blob/master/packages/less/src/less/tree/color.js + float r = gammaCorrection( color.getRed() / 255f ); + float g = gammaCorrection( color.getGreen() / 255f ); + float b = gammaCorrection( color.getBlue() / 255f ); + return (0.2126f * r) + (0.7152f * g) + (0.0722f * b); + } + + private static float gammaCorrection( float value ) { + return (value <= 0.03928f) + ? value / 12.92f + : (float) Math.pow( (value + 0.055) / 1.055, 2.4 ); + } + //---- interface ColorFunction -------------------------------------------- public interface ColorFunction { diff --git a/flatlaf-core/src/test/java/com/formdev/flatlaf/util/TestColorFunctions.java b/flatlaf-core/src/test/java/com/formdev/flatlaf/util/TestColorFunctions.java new file mode 100644 index 00000000..74843273 --- /dev/null +++ b/flatlaf-core/src/test/java/com/formdev/flatlaf/util/TestColorFunctions.java @@ -0,0 +1,44 @@ +/* + * Copyright 2021 FormDev Software GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.formdev.flatlaf.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import java.awt.Color; +import org.junit.jupiter.api.Test; + +/** + * @author Karl Tauber + */ +public class TestColorFunctions +{ + @Test + void luma() { + assertEquals( 0, ColorFunctions.luma( Color.black ) ); + assertEquals( 1, ColorFunctions.luma( Color.white ) ); + + assertEquals( 0.2126f, ColorFunctions.luma( Color.red ) ); + assertEquals( 0.7152f, ColorFunctions.luma( Color.green ) ); + assertEquals( 0.0722f, ColorFunctions.luma( Color.blue ) ); + + assertEquals( 0.9278f, ColorFunctions.luma( Color.yellow ) ); + assertEquals( 0.7874f, ColorFunctions.luma( Color.cyan ) ); + + assertEquals( 0.051269464f, ColorFunctions.luma( Color.darkGray ) ); + assertEquals( 0.21586052f, ColorFunctions.luma( Color.gray ) ); + assertEquals( 0.52711517f, ColorFunctions.luma( Color.lightGray ) ); + } +} diff --git a/flatlaf-theme-editor/src/main/java/com/formdev/flatlaf/themeeditor/FlatCompletionProvider.java b/flatlaf-theme-editor/src/main/java/com/formdev/flatlaf/themeeditor/FlatCompletionProvider.java index c9522891..2f333a13 100644 --- a/flatlaf-theme-editor/src/main/java/com/formdev/flatlaf/themeeditor/FlatCompletionProvider.java +++ b/flatlaf-theme-editor/src/main/java/com/formdev/flatlaf/themeeditor/FlatCompletionProvider.java @@ -471,6 +471,12 @@ class FlatCompletionProvider addFunction( "shade", "color", colorParamDesc, "weight", weightParamDesc ); + + addFunction( "contrast", + "color", colorParamDesc, + "dark", colorParamDesc, + "light", colorParamDesc, + "threshold", "(optional) 0-100%, default is 43%" ); } private void addFunction( String name, String... paramNamesAndDescs ) { diff --git a/flatlaf-theme-editor/src/main/java/com/formdev/flatlaf/themeeditor/FlatThemeEditorOverlay.java b/flatlaf-theme-editor/src/main/java/com/formdev/flatlaf/themeeditor/FlatThemeEditorOverlay.java index e769a37c..f3584a5f 100644 --- a/flatlaf-theme-editor/src/main/java/com/formdev/flatlaf/themeeditor/FlatThemeEditorOverlay.java +++ b/flatlaf-theme-editor/src/main/java/com/formdev/flatlaf/themeeditor/FlatThemeEditorOverlay.java @@ -31,6 +31,7 @@ import org.fife.ui.rsyntaxtextarea.Token; import com.formdev.flatlaf.FlatLaf; import com.formdev.flatlaf.UIDefaultsLoaderAccessor; import com.formdev.flatlaf.ui.FlatUIUtils; +import com.formdev.flatlaf.util.ColorFunctions; import com.formdev.flatlaf.util.HSLColor; import com.formdev.flatlaf.util.UIScale; @@ -46,6 +47,7 @@ class FlatThemeEditorOverlay static boolean showHSL = true; static boolean showRGB; + static boolean showLuma; private Font font; private Font baseFont; @@ -80,19 +82,21 @@ class FlatThemeEditorOverlay } FontMetrics fm = c.getFontMetrics( font ); + int space = fm.stringWidth( " " ); int maxTextWidth = 0; if( showHSL ) - maxTextWidth += fm.stringWidth( "HSL 360 100 100" ); + maxTextWidth += fm.stringWidth( "HSL 360 100 100" ) + space; if( showRGB ) - maxTextWidth += fm.stringWidth( "#ffffff" ); - if( showHSL && showRGB ) - maxTextWidth += fm.stringWidth( " " ); + maxTextWidth += fm.stringWidth( "#ffffff" ) + space; + if( showLuma ) + maxTextWidth += fm.stringWidth( "100" ) + space; + maxTextWidth = Math.max( maxTextWidth - space, 0 ); int textHeight = fm.getAscent() - fm.getLeading(); int width = c.getWidth(); int previewWidth = UIScale.scale( COLOR_PREVIEW_WIDTH ); int gap = UIScale.scale( 4 ); - int textGap = (showHSL || showRGB) ? UIScale.scale( 6 ) : 0; + int textGap = (showHSL || showRGB || showLuma) ? UIScale.scale( 6 ) : 0; // check whether preview is outside of clip bounds if( clipBounds.x + clipBounds.width < width - previewWidth - maxTextWidth - gap - textGap ) @@ -123,7 +127,7 @@ class FlatThemeEditorOverlay } // paint text - if( showHSL || showRGB ) { + if( showHSL || showRGB || showLuma ) { int textX = px - textGap - maxTextWidth; if( textX > r.x + gap) { String colorStr = null; @@ -141,6 +145,14 @@ class FlatThemeEditorOverlay else colorStr = rgbStr; } + if( showLuma ) { + String lumaStr = String.format( "%3d", + Math.round( ColorFunctions.luma( color ) * 100 ) ); + if( colorStr != null ) + colorStr += " " + lumaStr; + else + colorStr = lumaStr; + } int textWidth = fm.stringWidth( colorStr ); if( textWidth > maxTextWidth ) diff --git a/flatlaf-theme-editor/src/main/java/com/formdev/flatlaf/themeeditor/FlatThemeFileEditor.java b/flatlaf-theme-editor/src/main/java/com/formdev/flatlaf/themeeditor/FlatThemeFileEditor.java index c3b53a64..4acbe91f 100644 --- a/flatlaf-theme-editor/src/main/java/com/formdev/flatlaf/themeeditor/FlatThemeFileEditor.java +++ b/flatlaf-theme-editor/src/main/java/com/formdev/flatlaf/themeeditor/FlatThemeFileEditor.java @@ -83,8 +83,9 @@ class FlatThemeFileEditor private static final String KEY_PREVIEW = "preview"; private static final String KEY_LAF = "laf"; private static final String KEY_FONT_SIZE_INCR = "fontSizeIncr"; - private static final String KEY_HSL_COLORS = "hslColors"; - private static final String KEY_RGB_COLORS = "rgbColors"; + private static final String KEY_SHOW_HSL_COLORS = "showHslColors"; + private static final String KEY_SHOW_RGB_COLORS = "showRgbColors"; + private static final String KEY_SHOW_COLOR_LUMA = "showColorLuma"; private File dir; private Preferences state; @@ -701,9 +702,11 @@ class FlatThemeFileEditor private void colorModelChanged() { FlatThemeEditorOverlay.showHSL = showHSLColorsMenuItem.isSelected(); FlatThemeEditorOverlay.showRGB = showRGBColorsMenuItem.isSelected(); + FlatThemeEditorOverlay.showLuma = showColorLumaMenuItem.isSelected(); - putPrefsBoolean( state, KEY_HSL_COLORS, FlatThemeEditorOverlay.showHSL, true ); - putPrefsBoolean( state, KEY_RGB_COLORS, FlatThemeEditorOverlay.showRGB, false ); + putPrefsBoolean( state, KEY_SHOW_HSL_COLORS, FlatThemeEditorOverlay.showHSL, true ); + putPrefsBoolean( state, KEY_SHOW_RGB_COLORS, FlatThemeEditorOverlay.showRGB, false ); + putPrefsBoolean( state, KEY_SHOW_COLOR_LUMA, FlatThemeEditorOverlay.showLuma, false ); repaint(); } @@ -755,13 +758,15 @@ class FlatThemeFileEditor directoryField.setModel( model ); // restore overlay color models - FlatThemeEditorOverlay.showHSL = state.getBoolean( KEY_HSL_COLORS, true ); - FlatThemeEditorOverlay.showRGB = state.getBoolean( KEY_RGB_COLORS, false ); + FlatThemeEditorOverlay.showHSL = state.getBoolean( KEY_SHOW_HSL_COLORS, true ); + FlatThemeEditorOverlay.showRGB = state.getBoolean( KEY_SHOW_RGB_COLORS, false ); + FlatThemeEditorOverlay.showLuma = state.getBoolean( KEY_SHOW_COLOR_LUMA, false ); // restore menu item selection previewMenuItem.setSelected( state.getBoolean( KEY_PREVIEW, true ) ); showHSLColorsMenuItem.setSelected( FlatThemeEditorOverlay.showHSL ); showRGBColorsMenuItem.setSelected( FlatThemeEditorOverlay.showRGB ); + showColorLumaMenuItem.setSelected( FlatThemeEditorOverlay.showLuma ); } private void saveState() { @@ -877,6 +882,7 @@ class FlatThemeFileEditor resetFontSizeMenuItem = new JMenuItem(); showHSLColorsMenuItem = new JCheckBoxMenuItem(); showRGBColorsMenuItem = new JCheckBoxMenuItem(); + showColorLumaMenuItem = new JCheckBoxMenuItem(); windowMenu = new JMenu(); activateEditorMenuItem = new JMenuItem(); nextEditorMenuItem = new JMenuItem(); @@ -1024,6 +1030,11 @@ class FlatThemeFileEditor showRGBColorsMenuItem.setText("Show RGB colors (hex)"); showRGBColorsMenuItem.addActionListener(e -> colorModelChanged()); viewMenu.add(showRGBColorsMenuItem); + + //---- showColorLumaMenuItem ---- + showColorLumaMenuItem.setText("Show color luma"); + showColorLumaMenuItem.addActionListener(e -> colorModelChanged()); + viewMenu.add(showColorLumaMenuItem); } menuBar.add(viewMenu); @@ -1133,6 +1144,7 @@ class FlatThemeFileEditor private JMenuItem resetFontSizeMenuItem; private JCheckBoxMenuItem showHSLColorsMenuItem; private JCheckBoxMenuItem showRGBColorsMenuItem; + private JCheckBoxMenuItem showColorLumaMenuItem; private JMenu windowMenu; private JMenuItem activateEditorMenuItem; private JMenuItem nextEditorMenuItem; diff --git a/flatlaf-theme-editor/src/main/java/com/formdev/flatlaf/themeeditor/FlatThemeFileEditor.jfd b/flatlaf-theme-editor/src/main/java/com/formdev/flatlaf/themeeditor/FlatThemeFileEditor.jfd index f6f2f336..1b080599 100644 --- a/flatlaf-theme-editor/src/main/java/com/formdev/flatlaf/themeeditor/FlatThemeFileEditor.jfd +++ b/flatlaf-theme-editor/src/main/java/com/formdev/flatlaf/themeeditor/FlatThemeFileEditor.jfd @@ -177,6 +177,11 @@ new FormModel { "text": "Show RGB colors (hex)" addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "colorModelChanged", false ) ) } ) + add( new FormComponent( "javax.swing.JCheckBoxMenuItem" ) { + name: "showColorLumaMenuItem" + "text": "Show color luma" + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "colorModelChanged", false ) ) + } ) } ) add( new FormContainer( "javax.swing.JMenu", new FormLayoutManager( class javax.swing.JMenu ) ) { name: "windowMenu" diff --git a/flatlaf-theme-editor/src/main/java/com/formdev/flatlaf/themeeditor/FlatThemeTokenMaker.java b/flatlaf-theme-editor/src/main/java/com/formdev/flatlaf/themeeditor/FlatThemeTokenMaker.java index 6f540cdf..f8dbcea9 100644 --- a/flatlaf-theme-editor/src/main/java/com/formdev/flatlaf/themeeditor/FlatThemeTokenMaker.java +++ b/flatlaf-theme-editor/src/main/java/com/formdev/flatlaf/themeeditor/FlatThemeTokenMaker.java @@ -71,6 +71,7 @@ public class FlatThemeTokenMaker tokenMap.put( "mix", TOKEN_FUNCTION ); tokenMap.put( "tint", TOKEN_FUNCTION ); tokenMap.put( "shade", TOKEN_FUNCTION ); + tokenMap.put( "contrast", TOKEN_FUNCTION ); tokenMap.put( "lazy", TOKEN_FUNCTION ); // function options diff --git a/flatlaf-theme-editor/theme-editor-test.properties b/flatlaf-theme-editor/theme-editor-test.properties index 66a11c82..46a84560 100644 --- a/flatlaf-theme-editor/theme-editor-test.properties +++ b/flatlaf-theme-editor/theme-editor-test.properties @@ -70,3 +70,32 @@ Prop.colorFunc42 = tint(#f0f,75%) Prop.colorFunc45 = shade(#f0f,25%) Prop.colorFunc46 = shade(#f0f) Prop.colorFunc47 = shade(#f0f,75%) + +Prop.colorFunc50 = contrast(#111,#000,#fff) +Prop.colorFunc51 = contrast(#eee,#000,#fff) +Prop.colorFunc52 = contrast(#111,$Prop.colorFunc1,$Prop.colorFunc3) +Prop.colorFunc53 = contrast(#eee,$Prop.colorFunc1,$Prop.colorFunc3) +Prop.colorFunc54 = contrast(lighten(#111,5%),$Prop.colorFunc1,$Prop.colorFunc3) +Prop.colorFunc55 = contrast(lighten(#eee,5%),$Prop.colorFunc1,$Prop.colorFunc3) +Prop.colorFunc56 = contrast(contrast(#222,#111,#eee),contrast(#eee,#000,#fff),contrast(#111,#000,#fff)) + +Prop.1.selectionBackground = #2675BF +Prop.1.selectionForeground = contrast($Prop.1.selectionBackground,#000,#fff) + +Prop.2.selectionBackground = #95c0e9 +Prop.2.selectionForeground = contrast($Prop.2.selectionBackground,#000,#fff) + +Prop.3.selectionBackground = #f00 +Prop.3.selectionForeground = contrast($Prop.3.selectionBackground,#000,#fff) + +Prop.4.selectionBackground = #0f0 +Prop.4.selectionForeground = contrast($Prop.4.selectionBackground,#000,#fff) + +Prop.5.selectionBackground = #00f +Prop.5.selectionForeground = contrast($Prop.5.selectionBackground,#000,#fff) + +Prop.6.selectionBackground = #FFCC00 +Prop.6.selectionForeground = contrast($Prop.6.selectionBackground,#000,#fff) + +Prop.7.selectionBackground = #FF9500 +Prop.7.selectionForeground = contrast($Prop.7.selectionBackground,#000,#fff)