Improve color contrast checker
Add option to remove color opacity (flatten).
This commit is contained in:
parent
197618ef2e
commit
2d6393cc2b
@ -1,7 +1,9 @@
|
||||
/* SPDX-License-Identifier: MIT */
|
||||
package atlantafx.sampler.page.general;
|
||||
|
||||
import atlantafx.base.theme.Styles;
|
||||
import atlantafx.sampler.util.Containers;
|
||||
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.AnchorPane;
|
||||
@ -23,6 +25,7 @@ class ColorBlock extends VBox {
|
||||
private final String fgColorName;
|
||||
private final String bgColorName;
|
||||
private final String borderColorName;
|
||||
private final ReadOnlyObjectProperty<Color> bgBaseColor;
|
||||
|
||||
private final AnchorPane colorBox;
|
||||
private final Text fgText;
|
||||
@ -32,10 +35,14 @@ class ColorBlock extends VBox {
|
||||
|
||||
private Consumer<ColorBlock> actionHandler;
|
||||
|
||||
public ColorBlock(String fgColorName, String bgColorName, String borderColorName) {
|
||||
public ColorBlock(String fgColorName,
|
||||
String bgColorName,
|
||||
String borderColorName,
|
||||
ReadOnlyObjectProperty<Color> bgBaseColor) {
|
||||
this.fgColorName = validateColorName(fgColorName);
|
||||
this.bgColorName = validateColorName(bgColorName);
|
||||
this.borderColorName = validateColorName(borderColorName);
|
||||
this.bgBaseColor = bgBaseColor;
|
||||
|
||||
fgText = new Text();
|
||||
fgText.setStyle("-fx-fill:" + fgColorName + ";");
|
||||
@ -58,12 +65,16 @@ class ColorBlock extends VBox {
|
||||
colorBox.getStyleClass().add("box");
|
||||
colorBox.getChildren().setAll(fgText, wsagLabel, expandIcon);
|
||||
colorBox.setOnMouseEntered(e -> {
|
||||
toggleHover(true);
|
||||
var bgFill = getBgColor();
|
||||
|
||||
// this happens when css isn't updated yet
|
||||
if (bgFill == null) { return; }
|
||||
|
||||
toggleHover(true);
|
||||
// doesn't play quite well with transparency, because we not calc
|
||||
// actual underlying background color to flatten bgFill
|
||||
expandIcon.setFill(getColorLuminance(flattenColor(Color.WHITE, bgFill)) < LUMINANCE_THRESHOLD ?
|
||||
Color.WHITE : Color.BLACK
|
||||
expandIcon.setFill(getColorLuminance(flattenColor(bgBaseColor.get(), bgFill)) < LUMINANCE_THRESHOLD ?
|
||||
Color.WHITE : Color.BLACK
|
||||
);
|
||||
});
|
||||
colorBox.setOnMouseExited(e -> toggleHover(false));
|
||||
@ -94,6 +105,12 @@ class ColorBlock extends VBox {
|
||||
wsagLabel.setOpacity(state ? 0.5 : 1);
|
||||
}
|
||||
|
||||
private Text description(String text) {
|
||||
var t = new Text(text);
|
||||
t.getStyleClass().addAll("description", Styles.TEXT_SMALL);
|
||||
return t;
|
||||
}
|
||||
|
||||
public void update() {
|
||||
var fgFill = getFgColor();
|
||||
var bgFill = getBgColor();
|
||||
@ -105,7 +122,7 @@ class ColorBlock extends VBox {
|
||||
return;
|
||||
}
|
||||
|
||||
double contrastRatio = 1 / getContrastRatioOpacityAware(bgFill, fgFill);
|
||||
double contrastRatio = 1 / getContrastRatioOpacityAware(bgFill, fgFill, bgBaseColor.get());
|
||||
colorBox.pseudoClassStateChanged(PASSED, contrastRatio >= 4.5);
|
||||
|
||||
wsagIcon.setIconCode(contrastRatio >= 4.5 ? Material2AL.CHECK : Material2AL.CLOSE);
|
||||
@ -119,9 +136,8 @@ class ColorBlock extends VBox {
|
||||
}
|
||||
|
||||
public Color getBgColor() {
|
||||
return colorBox.getBackground() != null & !colorBox.getBackground().isEmpty() ?
|
||||
(Color) colorBox.getBackground().getFills().get(0).getFill() :
|
||||
null;
|
||||
return colorBox.getBackground() != null && !colorBox.getBackground().isEmpty() ?
|
||||
(Color) colorBox.getBackground().getFills().get(0).getFill() : null;
|
||||
}
|
||||
|
||||
public String getFgColorName() {
|
||||
@ -139,10 +155,4 @@ class ColorBlock extends VBox {
|
||||
public void setOnAction(Consumer<ColorBlock> actionHandler) {
|
||||
this.actionHandler = actionHandler;
|
||||
}
|
||||
|
||||
private Text description(String text) {
|
||||
var t = new Text(text);
|
||||
t.getStyleClass().addAll("description", Styles.TEXT_SMALL);
|
||||
return t;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
/* SPDX-License-Identifier: MIT */
|
||||
package atlantafx.sampler.page.general;
|
||||
|
||||
import atlantafx.base.controls.CustomTextField;
|
||||
@ -12,11 +13,14 @@ import javafx.collections.FXCollections;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.css.PseudoClass;
|
||||
import javafx.event.Event;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.ContentDisplay;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.Slider;
|
||||
import javafx.scene.input.ContextMenuEvent;
|
||||
import javafx.scene.layout.GridPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.VBox;
|
||||
@ -36,6 +40,7 @@ public class ColorContrastChecker extends GridPane {
|
||||
static final PseudoClass PASSED = PseudoClass.getPseudoClass("passed");
|
||||
static final float[] COLOR_WHITE = new float[] { 255f, 255f, 255f, 1f };
|
||||
static final float[] COLOR_BLACK = new float[] { 0f, 0f, 0f, 1f };
|
||||
static final double CONTRAST_RATIO_THRESHOLD = 1.5;
|
||||
static final double LUMINANCE_THRESHOLD = 0.55;
|
||||
|
||||
private static final int SLIDER_WIDTH = 300;
|
||||
@ -45,11 +50,8 @@ public class ColorContrastChecker extends GridPane {
|
||||
|
||||
private final ObservableHSLAColor bgColor = new ObservableHSLAColor(Color.WHITE);
|
||||
private final ObservableHSLAColor fgColor = new ObservableHSLAColor(Color.BLACK);
|
||||
private final DoubleBinding contrastRatio = Bindings.createDoubleBinding(
|
||||
() -> 1 / getContrastRatioOpacityAware(bgColor.getColor(), fgColor.getColor()),
|
||||
bgColor.colorProperty(),
|
||||
fgColor.colorProperty()
|
||||
);
|
||||
private final ReadOnlyObjectProperty<Color> bgBaseColor;
|
||||
private final DoubleBinding contrastRatio;
|
||||
|
||||
private Label bgColorNameLabel;
|
||||
private Label fgColorNameLabel;
|
||||
@ -62,8 +64,17 @@ public class ColorContrastChecker extends GridPane {
|
||||
private Slider fgLightnessSlider;
|
||||
private Slider fgAlphaSlider;
|
||||
|
||||
public ColorContrastChecker() {
|
||||
public ColorContrastChecker(ReadOnlyObjectProperty<Color> bgBaseColor) {
|
||||
super();
|
||||
|
||||
this.bgBaseColor = bgBaseColor;
|
||||
this.contrastRatio = Bindings.createDoubleBinding(
|
||||
() -> 1 / getContrastRatioOpacityAware(bgColor.getColor(), fgColor.getColor(), bgBaseColor.get()),
|
||||
bgColor.colorProperty(),
|
||||
fgColor.colorProperty(),
|
||||
bgBaseColor
|
||||
);
|
||||
|
||||
createView();
|
||||
}
|
||||
|
||||
@ -155,6 +166,7 @@ public class ColorContrastChecker extends GridPane {
|
||||
bgTextField.textProperty().bind(Bindings.createStringBinding(
|
||||
() -> bgColor.getColorHexWithAlpha().substring(1), bgColor.colorProperty()
|
||||
));
|
||||
bgTextField.addEventFilter(ContextMenuEvent.CONTEXT_MENU_REQUESTED, Event::consume);
|
||||
|
||||
fgColorNameLabel = new Label("Foreground Color");
|
||||
fgColorNameLabel.setPadding(new Insets(-15, 0, 0, 0));
|
||||
@ -166,6 +178,7 @@ public class ColorContrastChecker extends GridPane {
|
||||
fgTextField.textProperty().bind(Bindings.createStringBinding(
|
||||
() -> fgColor.getColorHexWithAlpha().substring(1), fgColor.colorProperty()
|
||||
));
|
||||
fgTextField.addEventFilter(ContextMenuEvent.CONTEXT_MENU_REQUESTED, Event::consume);
|
||||
|
||||
bgHueSlider = slider(1, 360, 1, 1);
|
||||
bgHueSlider.valueProperty().addListener((obs, old, val) -> {
|
||||
@ -243,6 +256,17 @@ public class ColorContrastChecker extends GridPane {
|
||||
|
||||
// ~
|
||||
|
||||
var flattenButton = new Button("Flatten");
|
||||
flattenButton.setOnAction(e -> {
|
||||
double[] flatBg = flattenColor(bgBaseColor.get(), bgColor.getColor());
|
||||
setBackground(Color.color(flatBg[0], flatBg[1], flatBg[2]));
|
||||
|
||||
double[] flatFg = flattenColor(bgBaseColor.get(), fgColor.getColor());
|
||||
setForeground(Color.color(flatFg[0], flatFg[1], flatFg[2]));
|
||||
});
|
||||
|
||||
// ~
|
||||
|
||||
getStyleClass().add("contrast-checker");
|
||||
|
||||
// column 0
|
||||
@ -259,6 +283,8 @@ public class ColorContrastChecker extends GridPane {
|
||||
add(bgAlphaLabel, 0, 10);
|
||||
add(bgAlphaSlider, 0, 11);
|
||||
|
||||
add(flattenButton, 0, 12);
|
||||
|
||||
// column 1
|
||||
add(wsagBox, 1, 0);
|
||||
add(new Label("Foreground Color"), 1, 1);
|
||||
@ -287,14 +313,28 @@ public class ColorContrastChecker extends GridPane {
|
||||
float[] fg = fgColor.getRGBAColor();
|
||||
|
||||
// use fallback color if contrast ratio is too low
|
||||
if (contrastRatio.get() <= LUMINANCE_THRESHOLD) {
|
||||
fg = getColorLuminance(flattenColor(Color.WHITE, bgColor.getColor())) < 0.55 ? COLOR_WHITE : COLOR_BLACK;
|
||||
if (contrastRatio.get() <= CONTRAST_RATIO_THRESHOLD) {
|
||||
fg = getColorLuminance(flattenColor(bgBaseColor.get(), bgColor.getColor())) < LUMINANCE_THRESHOLD ?
|
||||
COLOR_WHITE :
|
||||
COLOR_BLACK;
|
||||
}
|
||||
|
||||
setStyle(String.format("-color-contrast-checker-bg:rgba(%.0f,%.0f,%.0f,%.2f);-color-contrast-checker-fg:rgba(%.0f,%.0f,%.0f,%.2f);",
|
||||
bg[0], bg[1], bg[2], bg[3],
|
||||
fg[0], fg[1], fg[2], fg[3]
|
||||
));
|
||||
// flat colors are necessary for controls that use reverse styling (bg color over fg color),
|
||||
// it won't be readable if we not remove transparency first
|
||||
double[] bgFlat = flattenColor(bgBaseColor.get(), bgColor.getColor());
|
||||
double[] fgFlat = flattenColor(bgBaseColor.get(), fgColor.getColor());
|
||||
|
||||
var style = String.format("-color-contrast-checker-bg:rgba(%.0f,%.0f,%.0f,%.2f);" +
|
||||
"-color-contrast-checker-fg:rgba(%.0f,%.0f,%.0f,%.2f);" +
|
||||
"-color-contrast-checker-bg-flat:%s;" +
|
||||
"-color-contrast-checker-fg-flat:%s;",
|
||||
bg[0], bg[1], bg[2], bg[3],
|
||||
fg[0], fg[1], fg[2], fg[3],
|
||||
JColor.color((float) bgFlat[0], (float) bgFlat[1], (float) bgFlat[2]).getColorHex(),
|
||||
JColor.color((float) fgFlat[0], (float) fgFlat[1], (float) fgFlat[2]).getColorHex()
|
||||
);
|
||||
|
||||
setStyle(style);
|
||||
}
|
||||
|
||||
private void setBackground(Color color) {
|
||||
@ -357,9 +397,9 @@ public class ColorContrastChecker extends GridPane {
|
||||
return slider;
|
||||
}
|
||||
|
||||
static double getContrastRatioOpacityAware(Color bgColor, Color fgColor) {
|
||||
double luminance1 = getColorLuminance(flattenColor(Color.WHITE, bgColor));
|
||||
double luminance2 = getColorLuminance(flattenColor(Color.WHITE, fgColor));
|
||||
static double getContrastRatioOpacityAware(Color bgColor, Color fgColor, Color bgBaseColor) {
|
||||
double luminance1 = getColorLuminance(flattenColor(bgBaseColor, bgColor));
|
||||
double luminance2 = getColorLuminance(flattenColor(bgBaseColor, fgColor));
|
||||
return getContrastRatio(luminance1, luminance2);
|
||||
}
|
||||
|
||||
@ -371,11 +411,11 @@ public class ColorContrastChecker extends GridPane {
|
||||
private final ReadOnlyObjectWrapper<Color> color = new ReadOnlyObjectWrapper<>() { };
|
||||
|
||||
public ObservableHSLAColor(Color initialColor) {
|
||||
color.set(initialColor);
|
||||
values.addListener((ListChangeListener<Float>) c -> {
|
||||
float[] rgb = getRGBAArithmeticColor();
|
||||
color.set(Color.color(rgb[0], rgb[1], rgb[2], getAlpha()));
|
||||
});
|
||||
setColor(initialColor);
|
||||
}
|
||||
|
||||
public Color getColor() {
|
||||
|
@ -1,8 +1,12 @@
|
||||
/* SPDX-License-Identifier: MIT */
|
||||
package atlantafx.sampler.page.general;
|
||||
|
||||
import atlantafx.base.controls.Spacer;
|
||||
import atlantafx.base.theme.Styles;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.ReadOnlyBooleanProperty;
|
||||
import javafx.beans.property.ReadOnlyBooleanWrapper;
|
||||
import javafx.beans.property.ReadOnlyObjectWrapper;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.Label;
|
||||
@ -10,6 +14,7 @@ import javafx.scene.layout.GridPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.scene.paint.Color;
|
||||
import org.kordamp.ikonli.feather.Feather;
|
||||
import org.kordamp.ikonli.javafx.FontIcon;
|
||||
|
||||
@ -22,14 +27,35 @@ import java.util.function.Consumer;
|
||||
|
||||
class ColorPalette extends VBox {
|
||||
|
||||
private final List<ColorBlock> blocks = new ArrayList<>();
|
||||
|
||||
private Label headerLabel;
|
||||
private Button backBtn;
|
||||
private GridPane colorGrid;
|
||||
private ColorContrastChecker contrastChecker;
|
||||
private VBox contrastCheckerArea;
|
||||
|
||||
private final List<ColorBlock> blocks = new ArrayList<>();
|
||||
private final Consumer<ColorBlock> colorBlockActionHandler = colorBlock -> {
|
||||
ColorContrastChecker c = getOrCreateContrastChecker();
|
||||
c.setValues(colorBlock.getFgColorName(),
|
||||
colorBlock.getFgColor(),
|
||||
colorBlock.getBgColorName(),
|
||||
colorBlock.getBgColor()
|
||||
);
|
||||
|
||||
if (contrastCheckerArea.getChildren().isEmpty()) {
|
||||
contrastCheckerArea.getChildren().setAll(c);
|
||||
}
|
||||
|
||||
showContrastChecker();
|
||||
};
|
||||
|
||||
private final ReadOnlyBooleanWrapper contrastCheckerActive = new ReadOnlyBooleanWrapper(false);
|
||||
private final ReadOnlyObjectWrapper<Color> bgBaseColor = new ReadOnlyObjectWrapper<>(Color.WHITE);
|
||||
|
||||
public ReadOnlyBooleanProperty contrastCheckerActiveProperty() {
|
||||
return contrastCheckerActive.getReadOnlyProperty();
|
||||
}
|
||||
|
||||
public ColorPalette() {
|
||||
super();
|
||||
createView();
|
||||
@ -43,22 +69,22 @@ class ColorPalette extends VBox {
|
||||
backBtn.getStyleClass().add(Styles.FLAT);
|
||||
backBtn.setVisible(false);
|
||||
backBtn.setManaged(false);
|
||||
backBtn.setOnAction(e -> {
|
||||
headerLabel.setText("Color Palette");
|
||||
backBtn.setVisible(false);
|
||||
backBtn.setManaged(false);
|
||||
getChildren().set(1, colorGrid);
|
||||
});
|
||||
backBtn.setOnAction(e -> showColorPalette());
|
||||
|
||||
var headerBox = new HBox();
|
||||
headerBox.getChildren().setAll(headerLabel, new Spacer(), backBtn);
|
||||
headerBox.setAlignment(Pos.CENTER_LEFT);
|
||||
headerBox.getStyleClass().add("header");
|
||||
|
||||
contrastCheckerArea = new VBox();
|
||||
contrastCheckerArea.getStyleClass().add("contrast-checker-area");
|
||||
|
||||
colorGrid = colorGrid();
|
||||
|
||||
backgroundProperty().addListener((obs, old, val) -> bgBaseColor.set(
|
||||
val != null && !val.getFills().isEmpty() ? (Color) val.getFills().get(0).getFill() : Color.WHITE
|
||||
));
|
||||
|
||||
getChildren().setAll(headerBox, colorGrid);
|
||||
setId("color-palette");
|
||||
}
|
||||
@ -100,39 +126,34 @@ class ColorPalette extends VBox {
|
||||
}
|
||||
|
||||
private ColorBlock colorBlock(String fgColor, String bgColor, String borderColor) {
|
||||
var actionHandler = new Consumer<ColorBlock>() {
|
||||
@Override
|
||||
public void accept(ColorBlock colorBlock) {
|
||||
ColorContrastChecker c = getOrCreateContrastChecker();
|
||||
c.setValues(colorBlock.getFgColorName(),
|
||||
colorBlock.getFgColor(),
|
||||
colorBlock.getBgColorName(),
|
||||
colorBlock.getBgColor()
|
||||
);
|
||||
|
||||
if (contrastCheckerArea.getChildren().isEmpty()) {
|
||||
contrastCheckerArea.getChildren().setAll(c);
|
||||
}
|
||||
|
||||
headerLabel.setText("Contrast Checker");
|
||||
backBtn.setVisible(true);
|
||||
backBtn.setManaged(true);
|
||||
getChildren().set(1, contrastCheckerArea);
|
||||
}
|
||||
};
|
||||
|
||||
var block = new ColorBlock(fgColor, bgColor, borderColor);
|
||||
block.setOnAction(actionHandler);
|
||||
var block = new ColorBlock(fgColor, bgColor, borderColor, bgBaseColor.getReadOnlyProperty());
|
||||
block.setOnAction(colorBlockActionHandler);
|
||||
blocks.add(block);
|
||||
return block;
|
||||
}
|
||||
|
||||
private ColorContrastChecker getOrCreateContrastChecker() {
|
||||
if (contrastChecker == null) { contrastChecker = new ColorContrastChecker(); }
|
||||
if (contrastChecker == null) { contrastChecker = new ColorContrastChecker(bgBaseColor.getReadOnlyProperty()); }
|
||||
VBox.setVgrow(contrastChecker, Priority.ALWAYS);
|
||||
return contrastChecker;
|
||||
}
|
||||
|
||||
private void showColorPalette() {
|
||||
headerLabel.setText("Color Palette");
|
||||
backBtn.setVisible(false);
|
||||
backBtn.setManaged(false);
|
||||
getChildren().set(1, colorGrid);
|
||||
contrastCheckerActive.set(false);
|
||||
}
|
||||
|
||||
private void showContrastChecker() {
|
||||
headerLabel.setText("Contrast Checker");
|
||||
backBtn.setVisible(true);
|
||||
backBtn.setManaged(true);
|
||||
getChildren().set(1, contrastCheckerArea);
|
||||
contrastCheckerActive.set(true);
|
||||
}
|
||||
|
||||
// To calculate contrast ratio, we have to obtain all components colors first.
|
||||
// Unfortunately, JavaFX doesn't provide an API to observe when stylesheet changes has been applied.
|
||||
// The timer is introduced to defer widget update to a time when scene changes supposedly will be finished.
|
||||
|
@ -43,6 +43,7 @@ public class ThemePage extends AbstractPage {
|
||||
private GridPane optionsGrid() {
|
||||
ChoiceBox<Theme> themeSelector = themeSelector();
|
||||
themeSelector.setPrefWidth(200);
|
||||
themeSelector.disableProperty().bind(colorPalette.contrastCheckerActiveProperty());
|
||||
|
||||
// ~
|
||||
|
||||
|
@ -101,6 +101,11 @@
|
||||
|
||||
#color-palette {
|
||||
-fx-spacing: 20px;
|
||||
/* mandatory, base bg color for flatten feature */
|
||||
-fx-background-color: -color-bg-default;
|
||||
}
|
||||
#color-palette > .header {
|
||||
-fx-pref-height: 40px;
|
||||
}
|
||||
#color-palette > .grid {
|
||||
-fx-vgap: 20px;
|
||||
@ -150,6 +155,23 @@
|
||||
-fx-border-color: -color-contrast-checker-fg;
|
||||
-fx-border-width: 0 0 1 0;
|
||||
}
|
||||
#color-palette > .contrast-checker-area > .contrast-checker .button {
|
||||
-color-button-bg: -color-contrast-checker-fg-flat;
|
||||
-color-button-fg: -color-contrast-checker-bg-flat;
|
||||
-color-button-border: -color-contrast-checker-bg-flat;
|
||||
|
||||
-color-button-bg-hover: -color-contrast-checker-fg-flat;
|
||||
-color-button-fg-hover: -color-contrast-checker-bg-flat;
|
||||
-color-button-border-hover: -color-contrast-checker-bg-flat;
|
||||
|
||||
-color-button-bg-focused: -color-contrast-checker-fg-flat;
|
||||
-color-button-fg-focused: -color-contrast-checker-bg-flat;
|
||||
-color-button-border-focused: -color-contrast-checker-bg-flat;
|
||||
|
||||
-color-button-bg-pressed: -color-contrast-checker-bg-flat;
|
||||
-color-button-fg-pressed: -color-contrast-checker-fg-flat;
|
||||
-color-button-border-pressed: -color-contrast-checker-fg-flat;
|
||||
}
|
||||
#color-palette > .contrast-checker-area > .contrast-checker .ikonli-font-icon {
|
||||
-fx-icon-color: -color-contrast-checker-fg;
|
||||
-fx-fill: -color-contrast-checker-fg;
|
||||
|
Loading…
Reference in New Issue
Block a user