Add color palette preview and color contrast checker

This commit is contained in:
mkpaz 2022-08-23 21:24:57 +04:00
parent a7255949ce
commit 05e278e56b
10 changed files with 2811 additions and 51 deletions

@ -135,7 +135,6 @@ public abstract class AbstractPage extends BorderPane implements Page {
// set syntax highlight theme according to JavaFX theme
ThemeManager tm = ThemeManager.getInstance();
System.out.println(tm.getMatchingHighlightJSTheme(tm.getTheme()).getBackground());
codeViewer.setContent(stream, tm.getMatchingHighlightJSTheme(tm.getTheme()));
graphic.setIconCode(ICON_SAMPLE);

@ -0,0 +1,148 @@
package atlantafx.sampler.page.general;
import atlantafx.base.theme.Styles;
import atlantafx.sampler.util.Containers;
import javafx.geometry.Insets;
import javafx.scene.control.Label;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.text.Text;
import org.kordamp.ikonli.feather.Feather;
import org.kordamp.ikonli.javafx.FontIcon;
import org.kordamp.ikonli.material2.Material2AL;
import java.util.function.Consumer;
import static atlantafx.sampler.page.general.ColorContrastChecker.*;
import static atlantafx.sampler.util.JColorUtils.flattenColor;
import static atlantafx.sampler.util.JColorUtils.getColorLuminance;
class ColorBlock extends VBox {
private final String fgColorName;
private final String bgColorName;
private final String borderColorName;
private final AnchorPane colorBox;
private final Text fgText;
private final FontIcon wsagIcon = new FontIcon();
private final Label wsagLabel = new Label();
private final FontIcon expandIcon = new FontIcon(Feather.MAXIMIZE_2);
private Consumer<ColorBlock> actionHandler;
public ColorBlock(String fgColorName, String bgColorName, String borderColorName) {
this.fgColorName = validateColorName(fgColorName);
this.bgColorName = validateColorName(bgColorName);
this.borderColorName = validateColorName(borderColorName);
fgText = new Text();
fgText.setStyle("-fx-fill:" + fgColorName + ";");
fgText.getStyleClass().addAll("text", Styles.TITLE_3);
Containers.setAnchors(fgText, new Insets(5, -1, -1, 5));
wsagLabel.setGraphic(wsagIcon);
wsagLabel.getStyleClass().add("wsag-label");
wsagLabel.setVisible(false);
Containers.setAnchors(wsagLabel, new Insets(-1, 3, 3, -1));
expandIcon.setIconSize(24);
expandIcon.getStyleClass().add("expand-icon");
expandIcon.setVisible(false);
expandIcon.setManaged(false);
Containers.setAnchors(expandIcon, new Insets(3, 3, -1, -1));
colorBox = new AnchorPane();
colorBox.setStyle("-fx-background-color:" + bgColorName + ";" + "-fx-border-color:" + borderColorName + ";");
colorBox.getStyleClass().add("box");
colorBox.getChildren().setAll(fgText, wsagLabel, expandIcon);
colorBox.setOnMouseEntered(e -> {
toggleHover(true);
var bgFill = getBgColor();
// 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
);
});
colorBox.setOnMouseExited(e -> toggleHover(false));
colorBox.setOnMouseClicked(e -> {
if (actionHandler != null) { actionHandler.accept(this); }
});
getChildren().addAll(
colorBox,
description(fgColorName),
description(bgColorName),
description(borderColorName)
);
getStyleClass().add("color-block");
}
static String validateColorName(String colorName) {
if (colorName == null || !colorName.startsWith("-color")) {
throw new IllegalArgumentException("Invalid color name: '" + colorName + "'.");
}
return colorName;
}
private void toggleHover(boolean state) {
expandIcon.setVisible(state);
expandIcon.setManaged(state);
fgText.setOpacity(state ? 0.5 : 1);
wsagLabel.setOpacity(state ? 0.5 : 1);
}
public void update() {
var fgFill = getFgColor();
var bgFill = getBgColor();
if (fgFill == null || bgFill == null) {
fgText.setText("");
wsagLabel.setText("");
wsagLabel.setVisible(false);
return;
}
double contrastRatio = 1 / getContrastRatioOpacityAware(bgFill, fgFill);
colorBox.pseudoClassStateChanged(PASSED, contrastRatio >= 4.5);
wsagIcon.setIconCode(contrastRatio >= 4.5 ? Material2AL.CHECK : Material2AL.CLOSE);
wsagLabel.setVisible(true);
wsagLabel.setText(contrastRatio >= 7 ? "AAA" : "AA");
fgText.setText(String.format("%.2f", contrastRatio));
}
public Color getFgColor() {
return (Color) fgText.getFill();
}
public Color getBgColor() {
return colorBox.getBackground() != null & !colorBox.getBackground().isEmpty() ?
(Color) colorBox.getBackground().getFills().get(0).getFill() :
null;
}
public String getFgColorName() {
return fgColorName;
}
public String getBgColorName() {
return bgColorName;
}
public String getBorderColorName() {
return borderColorName;
}
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;
}
}

@ -0,0 +1,457 @@
package atlantafx.sampler.page.general;
import atlantafx.base.controls.CustomTextField;
import atlantafx.base.theme.Styles;
import atlantafx.sampler.util.JColor;
import atlantafx.sampler.util.JColorUtils;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.DoubleBinding;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.css.PseudoClass;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.Label;
import javafx.scene.control.Slider;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import org.kordamp.ikonli.feather.Feather;
import org.kordamp.ikonli.javafx.FontIcon;
import org.kordamp.ikonli.material2.Material2AL;
import java.util.Objects;
import static atlantafx.sampler.page.general.ColorBlock.validateColorName;
import static atlantafx.sampler.util.JColorUtils.*;
// Inspired by the https://colourcontrast.cc/
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 LUMINANCE_THRESHOLD = 0.55;
private static final int SLIDER_WIDTH = 300;
private String bgColorName;
private String fgColorName;
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 Label bgColorNameLabel;
private Label fgColorNameLabel;
private Slider bgHueSlider;
private Slider bgSaturationSlider;
private Slider bgLightnessSlider;
private Slider bgAlphaSlider;
private Slider fgHueSlider;
private Slider fgSaturationSlider;
private Slider fgLightnessSlider;
private Slider fgAlphaSlider;
public ColorContrastChecker() {
super();
createView();
}
public void setValues(String fgColorName, Color fgColor,
String bgColorName, Color bgColor) {
this.bgColorName = validateColorName(bgColorName);
bgColorNameLabel.setText(bgColorName);
setBackground(bgColor);
this.fgColorName = validateColorName(fgColorName);
fgColorNameLabel.setText(fgColorName);
setForeground(fgColor);
}
public String getBgColorName() { return bgColorName; }
public String getFgColorName() { return fgColorName; }
public Color getBgColor() { return bgColor.colorProperty().get(); }
public Color getFgColor() { return fgColor.colorProperty().get(); }
private void createView() {
var textLabel = new Label("Aa");
textLabel.getStyleClass().add("text");
var ratioLabel = new Label("0.0");
ratioLabel.getStyleClass().add("ratio");
ratioLabel.textProperty().bind(Bindings.createStringBinding(
() -> String.format("%.2f", contrastRatio.get()), contrastRatio
));
var fontBox = new HBox(20, textLabel, ratioLabel);
fontBox.getStyleClass().add("font-box");
fontBox.setAlignment(Pos.BASELINE_LEFT);
// !
var aaNormalLabel = wsagLabel();
var aaNormalBox = wsagBox(aaNormalLabel, "AA Normal");
var aaLargeLabel = wsagLabel();
var aaLargeBox = wsagBox(aaLargeLabel, "AA Large");
var aaaNormalLabel = wsagLabel();
var aaaNormalBox = wsagBox(aaaNormalLabel, "AAA Normal");
var aaaLargeLabel = wsagLabel();
var aaaLargeBox = wsagBox(aaaLargeLabel, "AAA Large");
var wsagBox = new HBox(20, aaNormalBox, aaLargeBox, aaaNormalBox, aaaLargeBox);
wsagBox.getStyleClass().add("wsag-box");
contrastRatio.addListener((obs, old, val) -> {
if (val == null) { return; }
float ratio = val.floatValue();
if (ratio >= 7) {
updateWsagLabel(aaNormalLabel, true);
updateWsagLabel(aaLargeLabel, true);
updateWsagLabel(aaaNormalLabel, true);
updateWsagLabel(aaaLargeLabel, true);
} else if (ratio < 7 && ratio >= 4.5) {
updateWsagLabel(aaNormalLabel, true);
updateWsagLabel(aaLargeLabel, true);
updateWsagLabel(aaaNormalLabel, false);
updateWsagLabel(aaaLargeLabel, true);
} else if (ratio < 4.5 && ratio >= 3) {
updateWsagLabel(aaNormalLabel, false);
updateWsagLabel(aaLargeLabel, true);
updateWsagLabel(aaaNormalLabel, false);
updateWsagLabel(aaaLargeLabel, false);
} else {
updateWsagLabel(aaNormalLabel, false);
updateWsagLabel(aaLargeLabel, false);
updateWsagLabel(aaaNormalLabel, false);
updateWsagLabel(aaaLargeLabel, false);
}
});
// ~
bgColorNameLabel = new Label("Background Color");
bgColorNameLabel.setPadding(new Insets(-15, 0, 0, 0));
bgColorNameLabel.getStyleClass().add(Styles.TEXT_SMALL);
var bgTextField = new CustomTextField();
bgTextField.setEditable(false);
bgTextField.setLeft(new FontIcon(Feather.HASH));
bgTextField.textProperty().bind(Bindings.createStringBinding(
() -> bgColor.getColorHexWithAlpha().substring(1), bgColor.colorProperty()
));
fgColorNameLabel = new Label("Foreground Color");
fgColorNameLabel.setPadding(new Insets(-15, 0, 0, 0));
fgColorNameLabel.getStyleClass().add(Styles.TEXT_SMALL);
var fgTextField = new CustomTextField();
fgTextField.setEditable(false);
fgTextField.setLeft(new FontIcon(Feather.HASH));
fgTextField.textProperty().bind(Bindings.createStringBinding(
() -> fgColor.getColorHexWithAlpha().substring(1), fgColor.colorProperty()
));
bgHueSlider = slider(1, 360, 1, 1);
bgHueSlider.valueProperty().addListener((obs, old, val) -> {
if (val != null) { bgColor.setHue(val.floatValue()); }
});
var bgHueLabel = new Label("Hue °");
bgHueLabel.textProperty().bind(Bindings.createStringBinding(
() -> String.format("Hue %.0f °", bgHueSlider.getValue()), bgHueSlider.valueProperty())
);
bgSaturationSlider = slider(0, 1, 0, 0.01);
bgSaturationSlider.valueProperty().addListener((obs, old, val) -> {
if (val != null) { bgColor.setSaturation(val.floatValue()); }
});
var bgSaturationLabel = new Label("Saturation");
bgSaturationLabel.textProperty().bind(Bindings.createStringBinding(
() -> String.format("Saturation %.2f", bgSaturationSlider.getValue()), bgSaturationSlider.valueProperty())
);
bgLightnessSlider = slider(0, 1, 0, 0.01);
bgLightnessSlider.valueProperty().addListener((obs, old, val) -> {
if (val != null) { bgColor.setLightness(val.floatValue()); }
});
var bgLightnessLabel = new Label("Lightness");
bgLightnessLabel.textProperty().bind(Bindings.createStringBinding(
() -> String.format("Lightness %.2f", bgLightnessSlider.getValue()), bgLightnessSlider.valueProperty())
);
bgAlphaSlider = slider(0, 1, 0, 0.01);
bgAlphaSlider.valueProperty().addListener((obs, old, val) -> {
if (val != null) { bgColor.setAlpha(val.floatValue()); }
});
var bgAlphaLabel = new Label("Alpha");
bgAlphaLabel.textProperty().bind(Bindings.createStringBinding(
() -> String.format("Alpha %.2f", bgAlphaSlider.getValue()), bgAlphaSlider.valueProperty())
);
// ~
fgHueSlider = slider(1, 360, 1, 1);
fgHueSlider.valueProperty().addListener((obs, old, val) -> {
if (val != null) { fgColor.setHue(val.floatValue()); }
});
var fgHueLabel = new Label("Hue °");
fgHueLabel.textProperty().bind(Bindings.createStringBinding(
() -> String.format("Hue %.0f °", fgHueSlider.getValue()), fgHueSlider.valueProperty())
);
fgSaturationSlider = slider(0, 1, 0, 0.01);
fgSaturationSlider.valueProperty().addListener((obs, old, val) -> {
if (val != null) { fgColor.setSaturation(val.floatValue()); }
});
var fgSaturationLabel = new Label("Saturation");
fgSaturationLabel.textProperty().bind(Bindings.createStringBinding(
() -> String.format("Saturation %.2f", fgSaturationSlider.getValue()), fgSaturationSlider.valueProperty())
);
fgLightnessSlider = slider(0, 1, 0, 0.01);
fgLightnessSlider.valueProperty().addListener((obs, old, val) -> {
if (val != null) { fgColor.setLightness(val.floatValue()); }
});
var fgLightnessLabel = new Label("Lightness");
fgLightnessLabel.textProperty().bind(Bindings.createStringBinding(
() -> String.format("Lightness %.2f", fgLightnessSlider.getValue()), fgLightnessSlider.valueProperty())
);
fgAlphaSlider = slider(0, 1, 0, 0.01);
fgAlphaSlider.valueProperty().addListener((obs, old, val) -> {
if (val != null) { fgColor.setAlpha(val.floatValue()); }
});
var fgAlphaLabel = new Label("Alpha");
fgAlphaLabel.textProperty().bind(Bindings.createStringBinding(
() -> String.format("Alpha %.2f", fgAlphaSlider.getValue()), fgAlphaSlider.valueProperty())
);
// ~
getStyleClass().add("contrast-checker");
// column 0
add(fontBox, 0, 0);
add(new Label("Background Color"), 0, 1);
add(bgColorNameLabel, 0, 2);
add(bgTextField, 0, 3);
add(bgHueLabel, 0, 4);
add(bgHueSlider, 0, 5);
add(bgSaturationLabel, 0, 6);
add(bgSaturationSlider, 0, 7);
add(bgLightnessLabel, 0, 8);
add(bgLightnessSlider, 0, 9);
add(bgAlphaLabel, 0, 10);
add(bgAlphaSlider, 0, 11);
// column 1
add(wsagBox, 1, 0);
add(new Label("Foreground Color"), 1, 1);
add(fgColorNameLabel, 1, 2);
add(fgTextField, 1, 3);
add(fgHueLabel, 1, 4);
add(fgHueSlider, 1, 5);
add(fgSaturationLabel, 1, 6);
add(fgSaturationSlider, 1, 7);
add(fgLightnessLabel, 1, 8);
add(fgLightnessSlider, 1, 9);
add(fgAlphaLabel, 1, 10);
add(fgAlphaSlider, 1, 11);
bgColor.colorProperty().addListener((obs, old, val) -> {
if (val != null) { updateStyle(); }
});
fgColor.colorProperty().addListener((obs, old, val) -> {
if (val != null) { updateStyle(); }
});
}
private void updateStyle() {
float[] bg = bgColor.getRGBAColor();
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;
}
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]
));
}
private void setBackground(Color color) {
float[] hsl = JColorUtils.toHSL(
(float) color.getRed(),
(float) color.getGreen(),
(float) color.getBlue()
);
bgHueSlider.setValue(hsl[0]);
bgSaturationSlider.setValue(hsl[1]);
bgLightnessSlider.setValue(hsl[2]);
bgAlphaSlider.setValue(color.getOpacity());
}
private void setForeground(Color color) {
float[] hsl = JColorUtils.toHSL(
(float) color.getRed(),
(float) color.getGreen(),
(float) color.getBlue()
);
fgHueSlider.setValue(hsl[0]);
fgSaturationSlider.setValue(hsl[1]);
fgLightnessSlider.setValue(hsl[2]);
fgAlphaSlider.setValue(color.getOpacity());
}
private void updateWsagLabel(Label label, boolean success) {
FontIcon icon = Objects.requireNonNull((FontIcon) label.getGraphic());
if (success) {
label.setText("PASS");
icon.setIconCode(Material2AL.CHECK);
} else {
label.setText("FAIL");
icon.setIconCode(Material2AL.CLOSE);
}
label.pseudoClassStateChanged(PASSED, success);
}
private Label wsagLabel() {
var label = new Label("FAIL");
label.getStyleClass().add("wsag-label");
label.setContentDisplay(ContentDisplay.RIGHT);
label.setGraphic(new FontIcon(Material2AL.CLOSE));
return label;
}
private VBox wsagBox(Label label, String description) {
var box = new VBox(10, label, new Label(description));
box.setAlignment(Pos.CENTER);
return box;
}
private Slider slider(double min, double max, double value, double increment) {
var slider = new Slider(min, max, value);
slider.setMinWidth(SLIDER_WIDTH);
slider.setMajorTickUnit(increment);
slider.setBlockIncrement(increment);
slider.setMinorTickCount(0);
slider.setSnapToTicks(true);
return slider;
}
static double getContrastRatioOpacityAware(Color bgColor, Color fgColor) {
double luminance1 = getColorLuminance(flattenColor(Color.WHITE, bgColor));
double luminance2 = getColorLuminance(flattenColor(Color.WHITE, fgColor));
return getContrastRatio(luminance1, luminance2);
}
///////////////////////////////////////////////////////////////////////////
private static class ObservableHSLAColor {
private final ObservableList<Float> values = FXCollections.observableArrayList(0f, 0f, 0f, 0f);
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()));
});
}
public Color getColor() {
return color.get();
}
public void setColor(Color color) {
float[] hsl = JColorUtils.toHSL(
(float) color.getRed(),
(float) color.getGreen(),
(float) color.getBlue()
);
values.setAll(hsl[0], hsl[1], hsl[2], (float) color.getOpacity());
}
public ReadOnlyObjectProperty<Color> colorProperty() {
return color.getReadOnlyProperty();
}
public float getHue() {
return values.get(0);
}
public void setHue(float value) {
values.set(0, value);
}
public float getSaturation() {
return values.get(1);
}
public void setSaturation(float value) {
values.set(1, value);
}
public float getLightness() {
return values.get(2);
}
public void setLightness(float value) {
values.set(2, value);
}
public float getAlpha() {
return values.get(3);
}
public void setAlpha(float value) {
values.set(3, value);
}
public float[] getRGBAArithmeticColor() {
float[] hsl = new float[] { getHue(), getSaturation(), getLightness() };
var color = JColor.color(hsl, getAlpha());
return new float[] {
color.getRedArithmetic(),
color.getGreenArithmetic(),
color.getBlueArithmetic(),
getAlpha()
};
}
public float[] getRGBAColor() {
float[] hsl = new float[] { getHue(), getSaturation(), getLightness() };
var color = JColor.color(hsl, getAlpha());
return new float[] {
color.getRed(),
color.getGreen(),
color.getBlue(),
getAlpha()
};
}
public String getColorHexWithAlpha() {
float[] hsl = new float[] { getHue(), getSaturation(), getLightness() };
return JColor.color(hsl, getAlpha()).getColorHexWithAlpha();
}
}
}

@ -0,0 +1,147 @@
package atlantafx.sampler.page.general;
import atlantafx.base.controls.Spacer;
import atlantafx.base.theme.Styles;
import javafx.application.Platform;
import javafx.geometry.Pos;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import org.kordamp.ikonli.feather.Feather;
import org.kordamp.ikonli.javafx.FontIcon;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
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;
public ColorPalette() {
super();
createView();
}
private void createView() {
headerLabel = new Label("Color Palette");
headerLabel.getStyleClass().add(Styles.TITLE_4);
backBtn = new Button("Back", new FontIcon(Feather.CHEVRONS_LEFT));
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);
});
var headerBox = new HBox();
headerBox.getChildren().setAll(headerLabel, new Spacer(), backBtn);
headerBox.setAlignment(Pos.CENTER_LEFT);
contrastCheckerArea = new VBox();
contrastCheckerArea.getStyleClass().add("contrast-checker-area");
colorGrid = colorGrid();
getChildren().setAll(headerBox, colorGrid);
setId("color-palette");
}
private GridPane colorGrid() {
var grid = new GridPane();
grid.getStyleClass().add("grid");
grid.add(colorBlock("-color-fg-default", "-color-bg-default", "-color-border-default"), 0, 0);
grid.add(colorBlock("-color-fg-muted", "-color-bg-default", "-color-border-muted"), 1, 0);
grid.add(colorBlock("-color-fg-subtle", "-color-bg-default", "-color-border-subtle"), 2, 0);
grid.add(colorBlock("-color-fg-emphasis", "-color-accent-emphasis", "-color-accent-emphasis"), 0, 1);
grid.add(colorBlock("-color-accent-fg", "-color-bg-default", "-color-accent-emphasis"), 1, 1);
grid.add(colorBlock("-color-accent-fg", "-color-accent-muted", "-color-accent-muted"), 2, 1);
grid.add(colorBlock("-color-accent-fg", "-color-accent-subtle", "-color-accent-subtle"), 3, 1);
grid.add(colorBlock("-color-fg-emphasis", "-color-neutral-emphasis-plus", "-color-neutral-emphasis-plus"), 0, 2);
grid.add(colorBlock("-color-fg-emphasis", "-color-neutral-emphasis", "-color-neutral-emphasis"), 1, 2);
grid.add(colorBlock("-color-fg-muted", "-color-neutral-muted", "-color-neutral-muted"), 2, 2);
grid.add(colorBlock("-color-fg-subtle", "-color-neutral-subtle", "-color-neutral-subtle"), 3, 2);
grid.add(colorBlock("-color-fg-emphasis", "-color-success-emphasis", "-color-success-emphasis"), 0, 3);
grid.add(colorBlock("-color-success-fg", "-color-bg-default", "-color-success-emphasis"), 1, 3);
grid.add(colorBlock("-color-success-fg", "-color-success-muted", "-color-success-muted"), 2, 3);
grid.add(colorBlock("-color-success-fg", "-color-success-subtle", "-color-success-subtle"), 3, 3);
grid.add(colorBlock("-color-fg-emphasis", "-color-warning-emphasis", "-color-warning-emphasis"), 0, 4);
grid.add(colorBlock("-color-warning-fg", "-color-bg-default", "-color-warning-emphasis"), 1, 4);
grid.add(colorBlock("-color-warning-fg", "-color-warning-muted", "-color-warning-muted"), 2, 4);
grid.add(colorBlock("-color-warning-fg", "-color-warning-subtle", "-color-warning-subtle"), 3, 4);
grid.add(colorBlock("-color-fg-emphasis", "-color-danger-emphasis", "-color-danger-emphasis"), 0, 5);
grid.add(colorBlock("-color-danger-fg", "-color-bg-default", "-color-danger-emphasis"), 1, 5);
grid.add(colorBlock("-color-danger-fg", "-color-danger-muted", "-color-danger-muted"), 2, 5);
grid.add(colorBlock("-color-danger-fg", "-color-danger-subtle", "-color-danger-subtle"), 3, 5);
return grid;
}
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);
blocks.add(block);
return block;
}
private ColorContrastChecker getOrCreateContrastChecker() {
if (contrastChecker == null) { contrastChecker = new ColorContrastChecker(); }
VBox.setVgrow(contrastChecker, Priority.ALWAYS);
return contrastChecker;
}
// 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.
public void updateColorInfo(Duration delay) {
new Timer().schedule(new TimerTask() {
@Override
public void run() {
Platform.runLater(() -> blocks.forEach(ColorBlock::update));
}
}, delay.toMillis());
}
}

@ -6,16 +6,18 @@ import atlantafx.sampler.page.AbstractPage;
import atlantafx.sampler.theme.ThemeManager;
import javafx.scene.control.ChoiceBox;
import javafx.scene.control.Label;
import javafx.scene.control.Spinner;
import javafx.scene.layout.GridPane;
import javafx.util.StringConverter;
import java.time.Duration;
import java.util.Objects;
public class ThemePage extends AbstractPage {
public static final String NAME = "Theme";
private final ColorPalette colorPalette = new ColorPalette();
@Override
public String getName() { return NAME; }
@ -24,8 +26,17 @@ public class ThemePage extends AbstractPage {
createView();
}
@Override
protected void onRendered() {
super.onRendered();
colorPalette.updateColorInfo(Duration.ZERO);
}
private void createView() {
userContent.getChildren().add(optionsGrid());
userContent.getChildren().addAll(
optionsGrid(),
colorPalette
);
sourceCodeToggleBtn.setVisible(false);
}
@ -33,10 +44,6 @@ public class ThemePage extends AbstractPage {
ChoiceBox<Theme> themeSelector = themeSelector();
themeSelector.setPrefWidth(200);
Spinner<Integer> fontSizeSpinner = fontSizeSpinner();
fontSizeSpinner.getStyleClass().add(Spinner.STYLE_CLASS_SPLIT_ARROWS_HORIZONTAL);
fontSizeSpinner.setPrefWidth(200);
// ~
var grid = new GridPane();
@ -46,9 +53,6 @@ public class ThemePage extends AbstractPage {
grid.add(new Label("Color theme"), 0, 0);
grid.add(themeSelector, 1, 0);
grid.add(new Label("Font size"), 0, 1);
grid.add(fontSizeSpinner, 1, 1);
return grid;
}
@ -59,6 +63,7 @@ public class ThemePage extends AbstractPage {
selector.getSelectionModel().selectedItemProperty().addListener((obs, old, val) -> {
if (val != null && getScene() != null) {
ThemeManager.getInstance().setTheme(getScene(), val);
colorPalette.updateColorInfo(Duration.ofSeconds(1));
}
});
@ -70,7 +75,8 @@ public class ThemePage extends AbstractPage {
@Override
public Theme fromString(String themeName) {
return manager.getAvailableThemes().stream().filter(t -> Objects.equals(themeName, t.getName()))
return manager.getAvailableThemes().stream()
.filter(t -> Objects.equals(themeName, t.getName()))
.findFirst()
.orElse(null);
}
@ -86,23 +92,4 @@ public class ThemePage extends AbstractPage {
return selector;
}
private Spinner<Integer> fontSizeSpinner() {
var spinner = new Spinner<Integer>(10, 24, 14);
// Instead of this we should obtain font size from a rendered node.
// But since it's not trivial (thanks to JavaFX doesn't expose relevant API)
// we just keep current font size inside ThemeManager singleton.
// It works fine if ThemeManager default font size value matches
// default theme font size value.
spinner.getValueFactory().setValue(ThemeManager.getInstance().getFontSize());
spinner.valueProperty().addListener((obs, old, val) -> {
if (val != null && getScene() != null) {
ThemeManager.getInstance().setFontSize(getScene(), val);
}
});
return spinner;
}
}

@ -3,14 +3,22 @@ package atlantafx.sampler.page.general;
import atlantafx.sampler.page.AbstractPage;
import atlantafx.sampler.page.SampleBlock;
import atlantafx.sampler.theme.ThemeManager;
import javafx.application.Platform;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Hyperlink;
import javafx.scene.control.Label;
import javafx.scene.control.Spinner;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;
import java.time.Duration;
import java.util.Timer;
import java.util.TimerTask;
import static atlantafx.base.theme.Styles.*;
public class TypographyPage extends AbstractPage {
@ -20,7 +28,7 @@ public class TypographyPage extends AbstractPage {
@Override
public String getName() { return NAME; }
private GridPane fontSizeBox;
private GridPane fontSizeSampleContent;
public TypographyPage() {
super();
@ -28,10 +36,18 @@ public class TypographyPage extends AbstractPage {
}
private void createView() {
Spinner<Integer> fontSizeSpinner = fontSizeSpinner();
fontSizeSpinner.getStyleClass().add(Spinner.STYLE_CLASS_SPLIT_ARROWS_HORIZONTAL);
fontSizeSpinner.setPrefWidth(200);
var fontSizeBox = new HBox(20, new Label("Font size"), fontSizeSpinner);
fontSizeBox.setAlignment(Pos.CENTER_LEFT);
var fontSizeSample = fontSizeSample();
fontSizeBox = (GridPane) fontSizeSample.getContent();
fontSizeSampleContent = (GridPane) fontSizeSample.getContent();
userContent.getChildren().setAll(
fontSizeBox,
fontSizeSample.getRoot(),
fontWeightSample().getRoot(),
fontStyleSample().getRoot(),
@ -41,6 +57,50 @@ public class TypographyPage extends AbstractPage {
);
}
private Spinner<Integer> fontSizeSpinner() {
var spinner = new Spinner<Integer>(10, 24, 14);
// Instead of this we should obtain font size from a rendered node.
// But since it's not trivial (thanks to JavaFX doesn't expose relevant API)
// we just keep current font size inside ThemeManager singleton.
// It works fine if ThemeManager default font size value matches
// default theme font size value.
spinner.getValueFactory().setValue(ThemeManager.getInstance().getFontSize());
spinner.valueProperty().addListener((obs, old, val) -> {
if (val != null && getScene() != null) {
ThemeManager.getInstance().setFontSize(getScene(), val);
updateFontInfo(Duration.ofMillis(1000));
}
});
return spinner;
}
// font metrics can only be obtained by requesting from a rendered node
protected void onRendered() {
super.onRendered();
updateFontInfo(Duration.ZERO);
}
private void updateFontInfo(Duration delay) {
new Timer().schedule(new TimerTask() {
@Override
public void run() {
Platform.runLater(() -> {
for (Node node : fontSizeSampleContent.getChildren()) {
if (node instanceof Text textNode) {
var font = textNode.getFont();
textNode.setText(
String.format("%s = %.1fpx", textNode.getUserData(), Math.ceil(font.getSize()))
);
}
}
});
}
}, delay.toMillis());
}
private SampleBlock fontSizeSample() {
var grid = new GridPane();
grid.setHgap(40);
@ -116,6 +176,7 @@ public class TypographyPage extends AbstractPage {
private Text text(String text, String... styleClasses) {
var t = new Text(text);
t.getStyleClass().addAll(styleClasses);
t.setUserData(text);
return t;
}
@ -144,17 +205,4 @@ public class TypographyPage extends AbstractPage {
return new SampleBlock("Text flow", textFlow);
}
// font metrics can only be obtained by requesting from a rendered node
protected void onRendered() {
for (Node node : fontSizeBox.getChildren()) {
if (node instanceof Text textNode) {
var font = textNode.getFont();
textNode.setText(String.format("%s = %.1fpx",
textNode.getText(),
Math.ceil(font.getSize())
));
}
}
}
}

@ -2,7 +2,7 @@
package atlantafx.sampler.util;
import javafx.geometry.Insets;
import javafx.scene.Parent;
import javafx.scene.Node;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.ColumnConstraints;
@ -15,11 +15,11 @@ public final class Containers {
public static final ColumnConstraints H_GROW_NEVER = columnConstraints(Priority.NEVER);
public static void setAnchors(Parent parent, Insets insets) {
if (insets.getTop() >= 0) { AnchorPane.setTopAnchor(parent, insets.getTop()); }
if (insets.getRight() >= 0) { AnchorPane.setRightAnchor(parent, insets.getRight()); }
if (insets.getBottom() >= 0) { AnchorPane.setBottomAnchor(parent, insets.getBottom()); }
if (insets.getLeft() >= 0) { AnchorPane.setLeftAnchor(parent, insets.getLeft()); }
public static void setAnchors(Node node, Insets insets) {
if (insets.getTop() >= 0) { AnchorPane.setTopAnchor(node, insets.getTop()); }
if (insets.getRight() >= 0) { AnchorPane.setRightAnchor(node, insets.getRight()); }
if (insets.getBottom() >= 0) { AnchorPane.setBottomAnchor(node, insets.getBottom()); }
if (insets.getLeft() >= 0) { AnchorPane.setLeftAnchor(node, insets.getLeft()); }
}
public static void setScrollConstraints(ScrollPane scrollPane,

@ -0,0 +1,992 @@
/**
* MIT License
* <p>
* Copyright (c) 2022 National Geospatial-Intelligence Agency
* Source: https://github.com/ngageoint/color-java
* <p>
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* <p>
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
* <p>
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package atlantafx.sampler.util;
/**
* Color representation with support for hex, RGB, arithmetic RGB, HSL, and
* integer colors.
*
* @author osbornb
*/
public class JColor {
/**
* Red arithmetic color value
*/
private float red = 0.0f;
/**
* Green arithmetic color value
*/
private float green = 0.0f;
/**
* Blue arithmetic color value
*/
private float blue = 0.0f;
/**
* Opacity arithmetic value
*/
private float opacity = 1.0f;
/**
* Create the color in hex
*
* @param color hex color in format #RRGGBB, RRGGBB, #RGB, RGB, #AARRGGBB,
* AARRGGBB, #ARGB, or ARGB
*
* @return color
*/
public static JColor color(String color) {
return new JColor(color);
}
/**
* Create the color in hex with an opacity
*
* @param color hex color in format #RRGGBB, RRGGBB, #RGB, RGB, #AARRGGBB,
* AARRGGBB, #ARGB, or ARGB
* @param opacity opacity float inclusively between 0.0 and 1.0
*
* @return color
*/
public static JColor color(String color, float opacity) {
return new JColor(color, opacity);
}
/**
* Create the color in hex with an alpha
*
* @param color hex color in format #RRGGBB, RRGGBB, #RGB, RGB, #AARRGGBB,
* AARRGGBB, #ARGB, or ARGB
* @param alpha alpha integer color inclusively between 0 and 255
*
* @return color
*/
public static JColor color(String color, int alpha) {
return new JColor(color, alpha);
}
/**
* Create the color with individual hex colors
*
* @param red red hex color in format RR
* @param green green hex color in format GG
* @param blue blue hex color in format BB
*
* @return color
*/
public static JColor color(String red, String green, String blue) {
return new JColor(red, green, blue);
}
/**
* Create the color with individual hex colors and alpha
*
* @param red red hex color in format RR
* @param green green hex color in format GG
* @param blue blue hex color in format BB
* @param alpha alpha hex color in format AA
*
* @return color
*/
public static JColor color(String red, String green, String blue,
String alpha) {
return new JColor(red, green, blue, alpha);
}
/**
* Create the color with individual hex colors and opacity
*
* @param red red hex color in format RR
* @param green green hex color in format GG
* @param blue blue hex color in format BB
* @param opacity opacity float inclusively between 0.0 and 1.0
*
* @return color
*/
public static JColor color(String red, String green, String blue,
float opacity) {
return new JColor(red, green, blue, opacity);
}
/**
* Create the color with RGB values
*
* @param red red integer color inclusively between 0 and 255
* @param green green integer color inclusively between 0 and 255
* @param blue blue integer color inclusively between 0 and 255
*
* @return color
*/
public static JColor color(int red, int green, int blue) {
return new JColor(red, green, blue);
}
/**
* Create the color with RGBA values
*
* @param red red integer color inclusively between 0 and 255
* @param green green integer color inclusively between 0 and 255
* @param blue blue integer color inclusively between 0 and 255
* @param alpha alpha integer color inclusively between 0 and 255
*
* @return color
*/
public static JColor color(int red, int green, int blue, int alpha) {
return new JColor(red, green, blue, alpha);
}
/**
* Create the color with RGBA values
*
* @param red red integer color inclusively between 0 and 255
* @param green green integer color inclusively between 0 and 255
* @param blue blue integer color inclusively between 0 and 255
* @param opacity opacity float inclusively between 0.0 and 1.0
*
* @return color
*/
public static JColor color(int red, int green, int blue, float opacity) {
return new JColor(red, green, blue, opacity);
}
/**
* Create the color with arithmetic RGB values
*
* @param red red float color inclusively between 0.0 and 1.0
* @param green green float color inclusively between 0.0 and 1.0
* @param blue blue float color inclusively between 0.0 and 1.0
*
* @return color
*/
public static JColor color(float red, float green, float blue) {
return new JColor(red, green, blue);
}
/**
* Create the color with arithmetic RGB values
*
* @param red red float color inclusively between 0.0 and 1.0
* @param green green float color inclusively between 0.0 and 1.0
* @param blue blue float color inclusively between 0.0 and 1.0
* @param opacity opacity float inclusively between 0.0 and 1.0
*
* @return color
*/
public static JColor color(float red, float green, float blue,
float opacity) {
return new JColor(red, green, blue, opacity);
}
/**
* Create the color with HSL (hue, saturation, lightness) or HSL (alpha)
* values
*
* @param hsl HSL array where: 0 = hue, 1 = saturation, 2 = lightness,
* optional 3 = alpha
*
* @return color
*/
public static JColor color(float[] hsl) {
return new JColor(hsl);
}
/**
* Create the color with HSLA (hue, saturation, lightness, alpha) values
*
* @param hsl HSL array where: 0 = hue, 1 = saturation, 2 = lightness
* @param alpha alpha inclusively between 0.0 and 1.0
*
* @return color
*/
public static JColor color(float[] hsl, float alpha) {
return new JColor(hsl, alpha);
}
/**
* Create the color as a single integer
*
* @param color color integer
*
* @return color
*/
public static JColor color(int color) {
return new JColor(color);
}
/**
* Default color constructor, opaque black
*/
public JColor() {
}
/**
* Create the color in hex
*
* @param color hex color in format #RRGGBB, RRGGBB, #RGB, RGB, #AARRGGBB,
* AARRGGBB, #ARGB, or ARGB
*/
public JColor(String color) {
setColor(color);
}
/**
* Create the color in hex with an opacity
*
* @param color hex color in format #RRGGBB, RRGGBB, #RGB, RGB, #AARRGGBB,
* AARRGGBB, #ARGB, or ARGB
* @param opacity opacity float inclusively between 0.0 and 1.0
*/
public JColor(String color, float opacity) {
setColor(color, opacity);
}
/**
* Create the color in hex with an alpha
*
* @param color hex color in format #RRGGBB, RRGGBB, #RGB, RGB, #AARRGGBB,
* AARRGGBB, #ARGB, or ARGB
* @param alpha alpha integer color inclusively between 0 and 255
*/
public JColor(String color, int alpha) {
setColor(color, alpha);
}
/**
* Create the color with individual hex colors
*
* @param red red hex color in format RR
* @param green green hex color in format GG
* @param blue blue hex color in format BB
*/
public JColor(String red, String green, String blue) {
setColor(red, green, blue);
}
/**
* Create the color with individual hex colors and alpha
*
* @param red red hex color in format RR
* @param green green hex color in format GG
* @param blue blue hex color in format BB
* @param alpha alpha hex color in format AA
*/
public JColor(String red, String green, String blue, String alpha) {
setColor(red, green, blue, alpha);
}
/**
* Create the color with individual hex colors and opacity
*
* @param red red hex color in format RR
* @param green green hex color in format GG
* @param blue blue hex color in format BB
* @param opacity opacity float inclusively between 0.0 and 1.0
*/
public JColor(String red, String green, String blue, float opacity) {
setColor(red, green, blue, opacity);
}
/**
* Create the color with RGB values
*
* @param red red integer color inclusively between 0 and 255
* @param green green integer color inclusively between 0 and 255
* @param blue blue integer color inclusively between 0 and 255
*/
public JColor(int red, int green, int blue) {
setColor(red, green, blue);
}
/**
* Create the color with RGBA values
*
* @param red red integer color inclusively between 0 and 255
* @param green green integer color inclusively between 0 and 255
* @param blue blue integer color inclusively between 0 and 255
* @param alpha alpha integer color inclusively between 0 and 255
*/
public JColor(int red, int green, int blue, int alpha) {
setColor(red, green, blue, alpha);
}
/**
* Create the color with RGBA values
*
* @param red red integer color inclusively between 0 and 255
* @param green green integer color inclusively between 0 and 255
* @param blue blue integer color inclusively between 0 and 255
* @param opacity opacity float inclusively between 0.0 and 1.0
*/
public JColor(int red, int green, int blue, float opacity) {
setColor(red, green, blue, opacity);
}
/**
* Create the color with arithmetic RGB values
*
* @param red red float color inclusively between 0.0 and 1.0
* @param green green float color inclusively between 0.0 and 1.0
* @param blue blue float color inclusively between 0.0 and 1.0
*/
public JColor(float red, float green, float blue) {
setColor(red, green, blue);
}
/**
* Create the color with arithmetic RGB values
*
* @param red red float color inclusively between 0.0 and 1.0
* @param green green float color inclusively between 0.0 and 1.0
* @param blue blue float color inclusively between 0.0 and 1.0
* @param opacity opacity float inclusively between 0.0 and 1.0
*/
public JColor(float red, float green, float blue, float opacity) {
setColor(red, green, blue, opacity);
}
/**
* Create the color with HSL (hue, saturation, lightness) or HSL (alpha)
* values
*
* @param hsl HSL array where: 0 = hue, 1 = saturation, 2 = lightness,
* optional 3 = alpha
*/
public JColor(float[] hsl) {
if (hsl.length > 3) {
setColorByHSL(hsl[0], hsl[1], hsl[2], hsl[3]);
} else {
setColorByHSL(hsl[0], hsl[1], hsl[2]);
}
}
/**
* Create the color with HSLA (hue, saturation, lightness, alpha) values
*
* @param hsl HSL array where: 0 = hue, 1 = saturation, 2 = lightness
* @param alpha alpha inclusively between 0.0 and 1.0
*/
public JColor(float[] hsl, float alpha) {
setColorByHSL(hsl[0], hsl[1], hsl[2], alpha);
}
/**
* Create the color as a single integer
*
* @param color color integer
*/
public JColor(int color) {
setColor(color);
}
/**
* Copy constructor
*
* @param color color to copy
*/
public JColor(JColor color) {
this.red = color.red;
this.green = color.green;
this.blue = color.blue;
this.opacity = color.opacity;
}
/**
* Set the color in hex
*
* @param color hex color in format #RRGGBB, RRGGBB, #RGB, RGB, #AARRGGBB,
* AARRGGBB, #ARGB, or ARGB
*/
public void setColor(String color) {
setRed(JColorUtils.getRed(color));
setGreen(JColorUtils.getGreen(color));
setBlue(JColorUtils.getBlue(color));
String alpha = JColorUtils.getAlpha(color);
if (alpha != null) {
setAlpha(alpha);
}
}
/**
* Set the color in hex with an opacity
*
* @param color hex color in format #RRGGBB, RRGGBB, #RGB, RGB, #AARRGGBB,
* AARRGGBB, #ARGB, or ARGB
* @param opacity opacity float inclusively between 0.0 and 1.0
*/
public void setColor(String color, float opacity) {
setColor(color);
setOpacity(opacity);
}
/**
* Set the color in hex with an alpha
*
* @param color hex color in format #RRGGBB, RRGGBB, #RGB, RGB, #AARRGGBB,
* AARRGGBB, #ARGB, or ARGB
* @param alpha alpha integer color inclusively between 0 and 255
*/
public void setColor(String color, int alpha) {
setColor(color);
setAlpha(alpha);
}
/**
* Set the color with individual hex colors
*
* @param red red hex color in format RR
* @param green green hex color in format GG
* @param blue blue hex color in format BB
*/
public void setColor(String red, String green, String blue) {
setRed(red);
setGreen(green);
setBlue(blue);
}
/**
* Set the color with individual hex colors and alpha
*
* @param red red hex color in format RR
* @param green green hex color in format GG
* @param blue blue hex color in format BB
* @param alpha alpha hex color in format AA
*/
public void setColor(String red, String green, String blue, String alpha) {
setColor(red, green, blue);
setAlpha(alpha);
}
/**
* Set the color with individual hex colors and opacity
*
* @param red red hex color in format RR
* @param green green hex color in format GG
* @param blue blue hex color in format BB
* @param opacity opacity float inclusively between 0.0 and 1.0
*/
public void setColor(String red, String green, String blue, float opacity) {
setColor(red, green, blue);
setOpacity(opacity);
}
/**
* Set the color with RGB values
*
* @param red red integer color inclusively between 0 and 255
* @param green green integer color inclusively between 0 and 255
* @param blue blue integer color inclusively between 0 and 255
*/
public void setColor(int red, int green, int blue) {
setRed(red);
setGreen(green);
setBlue(blue);
}
/**
* Set the color with RGBA values
*
* @param red red integer color inclusively between 0 and 255
* @param green green integer color inclusively between 0 and 255
* @param blue blue integer color inclusively between 0 and 255
* @param alpha alpha integer color inclusively between 0 and 255
*/
public void setColor(int red, int green, int blue, int alpha) {
setColor(red, green, blue);
setAlpha(alpha);
}
/**
* Set the color with RGBA values
*
* @param red red integer color inclusively between 0 and 255
* @param green green integer color inclusively between 0 and 255
* @param blue blue integer color inclusively between 0 and 255
* @param opacity opacity float inclusively between 0.0 and 1.0
*/
public void setColor(int red, int green, int blue, float opacity) {
setColor(red, green, blue);
setOpacity(opacity);
}
/**
* Set the color with arithmetic RGB values
*
* @param red red float color inclusively between 0.0 and 1.0
* @param green green float color inclusively between 0.0 and 1.0
* @param blue blue float color inclusively between 0.0 and 1.0
*/
public void setColor(float red, float green, float blue) {
setRed(red);
setGreen(green);
setBlue(blue);
}
/**
* Set the color with arithmetic RGB values
*
* @param red red float color inclusively between 0.0 and 1.0
* @param green green float color inclusively between 0.0 and 1.0
* @param blue blue float color inclusively between 0.0 and 1.0
* @param opacity opacity float inclusively between 0.0 and 1.0
*/
public void setColor(float red, float green, float blue, float opacity) {
setColor(red, green, blue);
setOpacity(opacity);
}
/**
* Set the color with HSL (hue, saturation, lightness) values
*
* @param hue hue value inclusively between 0.0 and 360.0
* @param saturation saturation inclusively between 0.0 and 1.0
* @param lightness lightness inclusively between 0.0 and 1.0
*/
public void setColorByHSL(float hue, float saturation, float lightness) {
float[] arithmeticRGB = JColorUtils.toArithmeticRGB(hue, saturation,
lightness);
setRed(arithmeticRGB[0]);
setGreen(arithmeticRGB[1]);
setBlue(arithmeticRGB[2]);
}
/**
* Set the color with HSLA (hue, saturation, lightness, alpha) values
*
* @param hue hue value inclusively between 0.0 and 360.0
* @param saturation saturation inclusively between 0.0 and 1.0
* @param lightness lightness inclusively between 0.0 and 1.0
* @param alpha alpha inclusively between 0.0 and 1.0
*/
public void setColorByHSL(float hue, float saturation, float lightness,
float alpha) {
setColorByHSL(hue, saturation, lightness);
setAlpha(alpha);
}
/**
* Set the color as a single integer
*
* @param color color integer
*/
public void setColor(int color) {
setRed(JColorUtils.getRed(color));
setGreen(JColorUtils.getGreen(color));
setBlue(JColorUtils.getBlue(color));
if (color > 16777215 || color < 0) {
setAlpha(JColorUtils.getAlpha(color));
}
}
/**
* Set the red color in hex
*
* @param red red hex color in format RR or R
*/
public void setRed(String red) {
setRed(JColorUtils.toArithmeticRGB(red));
}
/**
* Set the green color in hex
*
* @param green green hex color in format GG or G
*/
public void setGreen(String green) {
setGreen(JColorUtils.toArithmeticRGB(green));
}
/**
* Set the blue color in hex
*
* @param blue blue hex color in format BB or B
*/
public void setBlue(String blue) {
setBlue(JColorUtils.toArithmeticRGB(blue));
}
/**
* Set the alpha color in hex
*
* @param alpha alpha hex color in format AA or A
*/
public void setAlpha(String alpha) {
setOpacity(JColorUtils.toArithmeticRGB(alpha));
}
/**
* Set the red color as an integer
*
* @param red red integer color inclusively between 0 and 255
*/
public void setRed(int red) {
setRed(JColorUtils.toHex(red));
}
/**
* Set the green color as an integer
*
* @param green green integer color inclusively between 0 and 255
*/
public void setGreen(int green) {
setGreen(JColorUtils.toHex(green));
}
/**
* Set the blue color as an integer
*
* @param blue blue integer color inclusively between 0 and 255
*/
public void setBlue(int blue) {
setBlue(JColorUtils.toHex(blue));
}
/**
* Set the alpha color as an integer
*
* @param alpha alpha integer color inclusively between 0 and 255
*/
public void setAlpha(int alpha) {
setOpacity(JColorUtils.toArithmeticRGB(alpha));
}
/**
* Set the red color as an arithmetic float
*
* @param red red float color inclusively between 0.0 and 1.0
*/
public void setRed(float red) {
JColorUtils.validateArithmeticRGB(red);
this.red = red;
}
/**
* Set the green color as an arithmetic float
*
* @param green green float color inclusively between 0.0 and 1.0
*/
public void setGreen(float green) {
JColorUtils.validateArithmeticRGB(green);
this.green = green;
}
/**
* Set the blue color as an arithmetic float
*
* @param blue blue float color inclusively between 0.0 and 1.0
*/
public void setBlue(float blue) {
JColorUtils.validateArithmeticRGB(blue);
this.blue = blue;
}
/**
* Set the opacity as an arithmetic float
*
* @param opacity opacity float color inclusively between 0.0 and 1.0
*/
public void setOpacity(float opacity) {
JColorUtils.validateArithmeticRGB(opacity);
this.opacity = opacity;
}
/**
* Set the alpha color as an arithmetic float
*
* @param alpha alpha float color inclusively between 0.0 and 1.0
*/
public void setAlpha(float alpha) {
setOpacity(alpha);
}
/**
* Check if the color is opaque (opacity or alpha of 1.0, 255, or x00)
*
* @return true if opaque
*/
public boolean isOpaque() {
return opacity == 1.0f;
}
/**
* Get the color as a hex string
*
* @return hex color in the format #RRGGBB
*/
public String getColorHex() {
return JColorUtils.toColor(getRedHex(), getGreenHex(), getBlueHex());
}
/**
* Get the color as a hex string with alpha
*
* @return hex color in the format #AARRGGBB
*/
public String getColorHexWithAlpha() {
return JColorUtils.toColorWithAlpha(getRedHex(), getGreenHex(),
getBlueHex(), getAlphaHex());
}
/**
* Get the color as a hex string, shorthanded when possible
*
* @return hex color in the format #RGB or #RRGGBB
*/
public String getColorHexShorthand() {
return JColorUtils.toColorShorthand(getRedHex(), getGreenHex(),
getBlueHex());
}
/**
* Get the color as a hex string with alpha, shorthanded when possible
*
* @return hex color in the format #ARGB or #AARRGGBB
*/
public String getColorHexShorthandWithAlpha() {
return JColorUtils.toColorShorthandWithAlpha(getRedHex(), getGreenHex(),
getBlueHex(), getAlphaHex());
}
/**
* Get the color as an integer
*
* @return integer color
*/
public int getColor() {
return JColorUtils.toColor(getRed(), getGreen(), getBlue());
}
/**
* Get the color as an integer including the alpha
*
* @return integer color
*/
public int getColorWithAlpha() {
return JColorUtils.toColorWithAlpha(getRed(), getGreen(), getBlue(),
getAlpha());
}
/**
* Get the red color in hex
*
* @return red hex color in format RR
*/
public String getRedHex() {
return JColorUtils.toHex(red);
}
/**
* Get the green color in hex
*
* @return green hex color in format GG
*/
public String getGreenHex() {
return JColorUtils.toHex(green);
}
/**
* Get the blue color in hex
*
* @return blue hex color in format BB
*/
public String getBlueHex() {
return JColorUtils.toHex(blue);
}
/**
* Get the alpha color in hex
*
* @return alpha hex color in format AA
*/
public String getAlphaHex() {
return JColorUtils.toHex(opacity);
}
/**
* Get the red color in hex, shorthand when possible
*
* @return red hex color in format R or RR
*/
public String getRedHexShorthand() {
return JColorUtils.shorthandHexSingle(getRedHex());
}
/**
* Get the green color in hex, shorthand when possible
*
* @return green hex color in format G or GG
*/
public String getGreenHexShorthand() {
return JColorUtils.shorthandHexSingle(getGreenHex());
}
/**
* Get the blue color in hex, shorthand when possible
*
* @return blue hex color in format B or BB
*/
public String getBlueHexShorthand() {
return JColorUtils.shorthandHexSingle(getBlueHex());
}
/**
* Get the alpha color in hex, shorthand when possible
*
* @return alpha hex color in format A or AA
*/
public String getAlphaHexShorthand() {
return JColorUtils.shorthandHexSingle(getAlphaHex());
}
/**
* Get the red color as an integer
*
* @return red integer color inclusively between 0 and 255
*/
public int getRed() {
return JColorUtils.toRGB(red);
}
/**
* Get the green color as an integer
*
* @return green integer color inclusively between 0 and 255
*/
public int getGreen() {
return JColorUtils.toRGB(green);
}
/**
* Get the blue color as an integer
*
* @return blue integer color inclusively between 0 and 255
*/
public int getBlue() {
return JColorUtils.toRGB(blue);
}
/**
* Get the alpha color as an integer
*
* @return alpha integer color inclusively between 0 and 255
*/
public int getAlpha() {
return JColorUtils.toRGB(opacity);
}
/**
* Get the red color as an arithmetic float
*
* @return red float color inclusively between 0.0 and 1.0
*/
public float getRedArithmetic() {
return red;
}
/**
* Get the green color as an arithmetic float
*
* @return green float color inclusively between 0.0 and 1.0
*/
public float getGreenArithmetic() {
return green;
}
/**
* Get the blue color as an arithmetic float
*
* @return blue float color inclusively between 0.0 and 1.0
*/
public float getBlueArithmetic() {
return blue;
}
/**
* Get the opacity as an arithmetic float
*
* @return opacity float inclusively between 0.0 and 1.0
*/
public float getOpacity() {
return opacity;
}
/**
* Get the alpha color as an arithmetic float
*
* @return alpha float color inclusively between 0.0 and 1.0
*/
public float getAlphaArithmetic() {
return getOpacity();
}
/**
* Get the HSL (hue, saturation, lightness) values
*
* @return HSL array where: 0 = hue, 1 = saturation, 2 = lightness
*/
public float[] getHSL() {
return JColorUtils.toHSL(red, green, blue);
}
/**
* Get the HSL hue value
*
* @return hue value
*/
public float getHue() {
return getHSL()[0];
}
/**
* Get the HSL saturation value
*
* @return saturation value
*/
public float getSaturation() {
return getHSL()[1];
}
/**
* Get the HSL lightness value
*
* @return lightness value
*/
public float getLightness() {
return getHSL()[2];
}
/**
* Copy the color
*
* @return color copy
*/
public JColor copy() {
return new JColor(this);
}
}

@ -0,0 +1,900 @@
/**
* MIT License
* <p>
* Copyright (c) 2022 National Geospatial-Intelligence Agency
* Source: https://github.com/ngageoint/color-java
* <p>
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* <p>
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
* <p>
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package atlantafx.sampler.util;
import javafx.scene.paint.Color;
import java.util.Arrays;
import java.util.regex.Pattern;
/**
* Color utilities with support for hex, RGB, arithmetic RGB, HSL, and integer colors.
*
* @author osbornb
*/
public class JColorUtils {
/**
* Hex color pattern
*/
private static final Pattern hexColorPattern = Pattern
.compile("^#?((\\p{XDigit}{3}){1,2}|(\\p{XDigit}{4}){1,2})$");
/**
* Hex single color pattern
*/
private static final Pattern hexSingleColorPattern = Pattern
.compile("^\\p{XDigit}{1,2}$");
/**
* Convert the hex color values to a hex color
*
* @param red red hex color in format RR or R
* @param green green hex color in format GG or G
* @param blue blue hex color in format BB or B
*
* @return hex color in format #RRGGBB
*/
public static String toColor(String red, String green, String blue) {
return toColorWithAlpha(red, green, blue, null);
}
/**
* Convert the hex color values to a hex color, shorthanded when possible
*
* @param red red hex color in format RR or R
* @param green green hex color in format GG or G
* @param blue blue hex color in format BB or B
*
* @return hex color in format #RGB or #RRGGBB
*/
public static String toColorShorthand(String red, String green,
String blue) {
return shorthandHex(toColor(red, green, blue));
}
/**
* Convert the hex color values to a hex color including an opaque alpha
* value of FF
*
* @param red red hex color in format RR or R
* @param green green hex color in format GG or G
* @param blue blue hex color in format BB or B
*
* @return hex color in format #AARRGGBB
*/
public static String toColorWithAlpha(String red, String green,
String blue) {
String defaultAlpha = "FF";
if (red != null && !red.isEmpty()
&& Character.isLowerCase(red.charAt(0))) {
defaultAlpha = defaultAlpha.toLowerCase();
}
return toColorWithAlpha(red, green, blue, defaultAlpha);
}
/**
* Convert the hex color values to a hex color including an opaque alpha
* value of FF or F, shorthanded when possible
*
* @param red red hex color in format RR or R
* @param green green hex color in format GG or G
* @param blue blue hex color in format BB or B
*
* @return hex color in format #ARGB or #AARRGGBB
*/
public static String toColorShorthandWithAlpha(String red, String green,
String blue) {
return shorthandHex(toColorWithAlpha(red, green, blue));
}
/**
* Convert the hex color values to a hex color
*
* @param red red hex color in format RR or R
* @param green green hex color in format GG or G
* @param blue blue hex color in format BB or B
* @param alpha alpha hex color in format AA or A, null to not include alpha
*
* @return hex color in format #AARRGGBB or #RRGGBB
*/
public static String toColorWithAlpha(String red, String green, String blue,
String alpha) {
validateHexSingle(red);
validateHexSingle(green);
validateHexSingle(blue);
StringBuilder color = new StringBuilder("#");
if (alpha != null) {
color.append(expandShorthandHexSingle(alpha));
}
color.append(expandShorthandHexSingle(red));
color.append(expandShorthandHexSingle(green));
color.append(expandShorthandHexSingle(blue));
return color.toString();
}
/**
* Convert the hex color values to a hex color, shorthanded when possible
*
* @param red red hex color in format RR or R
* @param green green hex color in format GG or G
* @param blue blue hex color in format BB or B
* @param alpha alpha hex color in format AA or A, null to not include alpha
*
* @return hex color in format #ARGB, #RGB, #AARRGGBB, or #RRGGBB
*/
public static String toColorShorthandWithAlpha(String red, String green,
String blue, String alpha) {
return shorthandHex(toColorWithAlpha(red, green, blue, alpha));
}
/**
* Convert the RGB values to a color integer
*
* @param red red integer color inclusively between 0 and 255
* @param green green integer color inclusively between 0 and 255
* @param blue blue integer color inclusively between 0 and 255
*
* @return integer color
*/
public static int toColor(int red, int green, int blue) {
return toColorWithAlpha(red, green, blue, -1);
}
/**
* Convert the RGB values to a color integer including an opaque alpha value
* of 255
*
* @param red red integer color inclusively between 0 and 255
* @param green green integer color inclusively between 0 and 255
* @param blue blue integer color inclusively between 0 and 255
*
* @return integer color
*/
public static int toColorWithAlpha(int red, int green, int blue) {
return toColorWithAlpha(red, green, blue, 255);
}
/**
* Convert the RGBA values to a color integer
*
* @param red red integer color inclusively between 0 and 255
* @param green green integer color inclusively between 0 and 255
* @param blue blue integer color inclusively between 0 and 255
* @param alpha alpha integer color inclusively between 0 and 255, -1 to not
* include alpha
*
* @return integer color
*/
public static int toColorWithAlpha(int red, int green, int blue,
int alpha) {
validateRGB(red);
validateRGB(green);
validateRGB(blue);
int color = (red & 0xff) << 16 | (green & 0xff) << 8 | (blue & 0xff);
if (alpha != -1) {
validateRGB(alpha);
color = (alpha & 0xff) << 24 | color;
}
return color;
}
/**
* Convert the RGB integer to a hex single color
*
* @param color integer color inclusively between 0 and 255
*
* @return hex single color in format FF
*/
public static String toHex(int color) {
validateRGB(color);
String hex = Integer.toHexString(color).toUpperCase();
if (hex.length() == 1) {
hex = "0" + hex;
}
return hex;
}
/**
* Convert the arithmetic RGB float to a hex single color
*
* @param color float color inclusively between 0.0 and 1.0
*
* @return hex single color in format FF
*/
public static String toHex(float color) {
return toHex(toRGB(color));
}
/**
* Convert the hex single color to a RGB integer
*
* @param color hex single color in format FF or F
*
* @return integer color inclusively between 0 and 255
*/
public static int toRGB(String color) {
validateHexSingle(color);
if (color.length() == 1) {
color += color;
}
return Integer.parseInt(color, 16);
}
/**
* Convert the arithmetic RGB float to a RGB integer
*
* @param color float color inclusively between 0.0 and 1.0
*
* @return integer color inclusively between 0 and 255
*/
public static int toRGB(float color) {
validateArithmeticRGB(color);
return Math.round(255 * color);
}
/**
* Convert the hex single color to an arithmetic RGB float
*
* @param color hex single color in format FF or F
*
* @return float color inclusively between 0.0 and 1.0
*/
public static float toArithmeticRGB(String color) {
return toArithmeticRGB(toRGB(color));
}
/**
* Convert the RGB integer to an arithmetic RGB float
*
* @param color integer color inclusively between 0 and 255
*
* @return float color inclusively between 0.0 and 1.0
*/
public static float toArithmeticRGB(int color) {
validateRGB(color);
return color / 255.0f;
}
/**
* Convert red, green, and blue arithmetic values to HSL (hue, saturation,
* lightness) values
*
* @param red red color inclusively between 0.0 and 1.0
* @param green green color inclusively between 0.0 and 1.0
* @param blue blue color inclusively between 0.0 and 1.0
*
* @return HSL array where: 0 = hue, 1 = saturation, 2 = lightness
*/
public static float[] toHSL(float red, float green, float blue) {
validateArithmeticRGB(red);
validateArithmeticRGB(green);
validateArithmeticRGB(blue);
float min = Math.min(Math.min(red, green), blue);
float max = Math.max(Math.max(red, green), blue);
float range = max - min;
float hue = 0.0f;
if (range > 0.0f) {
if (red >= green && red >= blue) {
hue = (green - blue) / range;
} else if (green >= blue) {
hue = 2 + (blue - red) / range;
} else {
hue = 4 + (red - green) / range;
}
}
hue *= 60.0f;
if (hue < 0.0f) {
hue += 360.0f;
}
float sum = min + max;
float lightness = sum / 2.0f;
float saturation;
if (min == max) {
saturation = 0.0f;
} else {
if (lightness < 0.5f) {
saturation = range / sum;
} else {
saturation = range / (2.0f - max - min);
}
}
return new float[] { hue, saturation, lightness };
}
/**
* Convert red, green, and blue integer values to HSL (hue, saturation,
* lightness) values
*
* @param red red color inclusively between 0 and 255
* @param green green color inclusively between 0 and 255
* @param blue blue color inclusively between 0 and 255
*
* @return HSL array where: 0 = hue, 1 = saturation, 2 = lightness
*/
public static float[] toHSL(int red, int green, int blue) {
return toHSL(toArithmeticRGB(red), toArithmeticRGB(green),
toArithmeticRGB(blue));
}
/**
* Convert HSL (hue, saturation, and lightness) values to RGB arithmetic
* values
*
* @param hue hue value inclusively between 0.0 and 360.0
* @param saturation saturation inclusively between 0.0 and 1.0
* @param lightness lightness inclusively between 0.0 and 1.0
*
* @return arithmetic RGB array where: 0 = red, 1 = green, 2 = blue
*/
public static float[] toArithmeticRGB(float hue, float saturation,
float lightness) {
validateHue(hue);
validateSaturation(saturation);
validateLightness(lightness);
hue /= 60.0f;
float t2;
if (lightness <= 0.5f) {
t2 = lightness * (saturation + 1);
} else {
t2 = lightness + saturation - (lightness * saturation);
}
float t1 = lightness * 2.0f - t2;
float red = hslConvert(t1, t2, hue + 2);
float green = hslConvert(t1, t2, hue);
float blue = hslConvert(t1, t2, hue - 2);
return new float[] { red, green, blue };
}
/**
* Convert HSL (hue, saturation, and lightness) values to RGB integer values
*
* @param hue hue value inclusively between 0.0 and 360.0
* @param saturation saturation inclusively between 0.0 and 1.0
* @param lightness lightness inclusively between 0.0 and 1.0
*
* @return RGB integer array where: 0 = red, 1 = green, 2 = blue
*/
public static int[] toRGB(float hue, float saturation, float lightness) {
float[] arithmeticRGB = toArithmeticRGB(hue, saturation, lightness);
return new int[] { toRGB(arithmeticRGB[0]), toRGB(arithmeticRGB[1]), toRGB(arithmeticRGB[2]) };
}
/**
* HSL convert helper method
*
* @param t1 t1
* @param t2 t2
* @param hue hue
*
* @return arithmetic RGB value
*/
private static float hslConvert(float t1, float t2, float hue) {
float value;
if (hue < 0) {
hue += 6;
}
if (hue >= 6) {
hue -= 6;
}
if (hue < 1) {
value = (t2 - t1) * hue + t1;
} else if (hue < 3) {
value = t2;
} else if (hue < 4) {
value = (t2 - t1) * (4 - hue) + t1;
} else {
value = t1;
}
return value;
}
/**
* Get the hex red color from the hex string
*
* @param hex hex color
*
* @return hex red color in format RR
*/
public static String getRed(String hex) {
return getHexSingle(hex, 0);
}
/**
* Get the hex green color from the hex string
*
* @param hex hex color
*
* @return hex green color in format GG
*/
public static String getGreen(String hex) {
return getHexSingle(hex, 1);
}
/**
* Get the hex blue color from the hex string
*
* @param hex hex color
*
* @return hex blue color in format BB
*/
public static String getBlue(String hex) {
return getHexSingle(hex, 2);
}
/**
* Get the hex alpha color from the hex string if it exists
*
* @param hex hex color
*
* @return hex alpha color in format AA or null
*/
public static String getAlpha(String hex) {
return getHexSingle(hex, -1);
}
/**
* Get the hex single color
*
* @param hex hex color
* @param colorIndex red=0, green=1, blue=2, alpha=-1
*
* @return hex single color in format FF or null
*/
private static String getHexSingle(String hex, int colorIndex) {
validateHex(hex);
if (hex.startsWith("#")) {
hex = hex.substring(1);
}
int colorCharacters = 1;
int numColors = hex.length();
if (numColors > 4) {
colorCharacters++;
numColors /= 2;
}
String color = null;
if (colorIndex >= 0 || numColors > 3) {
if (numColors > 3) {
colorIndex++;
}
int startIndex = colorIndex;
if (colorCharacters > 1) {
startIndex *= 2;
}
color = hex.substring(startIndex, startIndex + colorCharacters);
color = expandShorthandHexSingle(color);
}
return color;
}
/**
* Get the red color from color integer
*
* @param color color integer
*
* @return red color
*/
public static int getRed(int color) {
return (color >> 16) & 0xff;
}
/**
* Get the green color from color integer
*
* @param color color integer
*
* @return green color
*/
public static int getGreen(int color) {
return (color >> 8) & 0xff;
}
/**
* Get the blue color from color integer
*
* @param color color integer
*
* @return blue color
*/
public static int getBlue(int color) {
return color & 0xff;
}
/**
* Get the alpha color from color integer
*
* @param color color integer
*
* @return alpha color
*/
public static int getAlpha(int color) {
return (color >> 24) & 0xff;
}
/**
* Shorthand the hex color if possible
*
* @param color hex color
*
* @return shorthand hex color or original value
*/
public static String shorthandHex(String color) {
validateHex(color);
if (color.length() > 5) {
StringBuilder shorthandColor = new StringBuilder();
int startIndex = 0;
if (color.startsWith("#")) {
shorthandColor.append("#");
startIndex++;
}
for (; startIndex < color.length(); startIndex += 2) {
String shorthand = shorthandHexSingle(
color.substring(startIndex, startIndex + 2));
if (shorthand.length() > 1) {
shorthandColor = null;
break;
}
shorthandColor.append(shorthand);
}
if (shorthandColor != null) {
color = shorthandColor.toString();
}
}
return color;
}
/**
* Expand the hex if it is in shorthand
*
* @param color hex color
*
* @return expanded hex color or original value
*/
public static String expandShorthandHex(String color) {
validateHex(color);
if (color.length() < 6) {
StringBuilder expandColor = new StringBuilder();
int startIndex = 0;
if (color.startsWith("#")) {
expandColor.append("#");
startIndex++;
}
for (; startIndex < color.length(); startIndex++) {
String expand = expandShorthandHexSingle(
color.substring(startIndex, startIndex + 1));
expandColor.append(expand);
}
color = expandColor.toString();
}
return color;
}
/**
* Shorthand the hex single color if possible
*
* @param color hex single color
*
* @return shorthand hex color or original value
*/
public static String shorthandHexSingle(String color) {
validateHexSingle(color);
if (color.length() > 1
&& Character.toUpperCase(color.charAt(0)) == Character
.toUpperCase(color.charAt(1))) {
color = color.substring(0, 1);
}
return color;
}
/**
* Expand the hex single if it is in shorthand
*
* @param color hex single color
*
* @return expanded hex color or original value
*/
public static String expandShorthandHexSingle(String color) {
validateHexSingle(color);
if (color.length() == 1) {
color += color;
}
return color;
}
/**
* Check if the hex color value is valid
*
* @param color hex color
*
* @return true if valid
*/
public static boolean isValidHex(String color) {
return color != null && hexColorPattern.matcher(color).matches();
}
/**
* Validate the hex color value
*
* @param color hex color
*/
public static void validateHex(String color) {
if (!isValidHex(color)) {
throw new IllegalArgumentException(
"Hex color must be in format #RRGGBB, #RGB, #AARRGGBB, #ARGB, RRGGBB, RGB, AARRGGBB, or ARGB, invalid value: "
+ color);
}
}
/**
* Check if the hex single color value is valid
*
* @param color hex single color
*
* @return true if valid
*/
public static boolean isValidHexSingle(String color) {
return color != null && hexSingleColorPattern.matcher(color).matches();
}
/**
* Validate the hex single color value
*
* @param color hex single color
*/
public static void validateHexSingle(String color) {
if (!isValidHexSingle(color)) {
throw new IllegalArgumentException(
"Must be in format FF or F, invalid value: " + color);
}
}
/**
* Check if the RGB integer color is valid, inclusively between 0 and 255
*
* @param color decimal color
*
* @return true if valid
*/
public static boolean isValidRGB(int color) {
return color >= 0 && color <= 255;
}
/**
* Validate the RGB integer color is inclusively between 0 and 255
*
* @param color decimal color
*/
public static void validateRGB(int color) {
if (!isValidRGB(color)) {
throw new IllegalArgumentException(
"Must be inclusively between 0 and 255, invalid value: "
+ color);
}
}
/**
* Check if the arithmetic RGB float color is valid, inclusively between 0.0
* and 1.0
*
* @param color decimal color
*
* @return true if valid
*/
public static boolean isValidArithmeticRGB(float color) {
return color >= 0.0 && color <= 1.0;
}
/**
* Validate the arithmetic RGB float color is inclusively between 0.0 and
* 1.0
*
* @param color decimal color
*/
public static void validateArithmeticRGB(float color) {
if (!isValidArithmeticRGB(color)) {
throw new IllegalArgumentException(
"Must be inclusively between 0.0 and 1.0, invalid value: "
+ color);
}
}
/**
* Check if the HSL hue float value is valid, inclusively between 0.0 and
* 360.0
*
* @param hue hue value
*
* @return true if valid
*/
public static boolean isValidHue(float hue) {
return hue >= 0.0 && hue <= 360.0;
}
/**
* Validate the HSL hue float value is inclusively between 0.0 and 360.0
*
* @param hue hue value
*/
public static void validateHue(float hue) {
if (!isValidHue(hue)) {
throw new IllegalArgumentException(
"Must be inclusively between 0.0 and 360.0, invalid value: "
+ hue);
}
}
/**
* Check if the HSL saturation float value is valid, inclusively between 0.0
* and 1.0
*
* @param saturation saturation value
*
* @return true if valid
*/
public static boolean isValidSaturation(float saturation) {
return saturation >= 0.0 && saturation <= 1.0;
}
/**
* Validate the HSL saturation float value is inclusively between 0.0 and
* 1.0
*
* @param saturation saturation value
*/
public static void validateSaturation(float saturation) {
if (!isValidSaturation(saturation)) {
throw new IllegalArgumentException(
"Must be inclusively between 0.0 and 1.0, invalid value: "
+ saturation);
}
}
/**
* Check if the HSL lightness float value is valid, inclusively between 0.0
* and 1.0
*
* @param lightness lightness value
*
* @return true if valid
*/
public static boolean isValidLightness(float lightness) {
return lightness >= 0.0 && lightness <= 1.0;
}
/**
* Validate the HSL lightness float value is inclusively between 0.0 and 1.0
*
* @param lightness lightness value
*/
public static void validateLightness(float lightness) {
if (!isValidLightness(lightness)) {
throw new IllegalArgumentException(
"Must be inclusively between 0.0 and 1.0, invalid value: "
+ lightness);
}
}
///////////////////////////////////////////////////////////////////////////
// Additional Utils, mkpaz (c) //
///////////////////////////////////////////////////////////////////////////
/**
* The WCAG contrast measures the difference in brightness (luminance) between two colours.
* It ranges from 1:1 (white on white) to 21:1 (black on white). WCAG requirements are:
* <ul>
* <li>0.14285 (7.0:1) for small text in AAA-level</li>
* <li>0.22222 (4.5:1) for small text in AA-level, or large text in AAA-level</li>
* <li>0.33333 (3.0:1) for large text in AA-level</li>
* </ul>
* WCAG defines large text as text that is 18pt and larger, or 14pt and larger if it is bold.
* <br/>
* <a href="https://www.w3.org/TR/WCAG20-TECHS/G18.html">More info</a>.
*/
public static double getContrastRatio(Color color1, Color color2) {
return getContrastRatio(getColorLuminance(color1), getColorLuminance(color2));
}
/** @see JColorUtils#getContrastRatio(Color, Color) */
public static double getContrastRatio(double luminance1, double luminance2) {
return luminance1 > luminance2 ?
(luminance2 + 0.05) / (luminance1 + 0.05) :
(luminance1 + 0.05) / (luminance2 + 0.05);
}
/**
* Measures relative color luminance according to the
* <a href="https://www.w3.org/TR/WCAG20-TECHS/G18.html">W3C</a>.
* <br/>
* Note that JavaFX provides {@link Color#getBrightness()} which
* IS NOT the same thing as luminance.
*/
public static double getColorLuminance(double[] rgb) {
double[] tmp = Arrays.stream(rgb)
.map(v -> v <= 0.03928 ? (v / 12.92) : Math.pow((v + 0.055) / 1.055, 2.4))
.toArray();
return (tmp[0] * 0.2126) + (tmp[1] * 0.7152) + (tmp[2] * 0.0722);
}
/** @see JColorUtils#getColorLuminance(double[]) */
public static double getColorLuminance(Color color) {
return getColorLuminance(new double[] { color.getRed(), color.getGreen(), color.getBlue() });
}
/**
* Removes given color opacity, if present.
* <br/><br/>
* When implementing designs, you'll sometimes want to use a lighter shade
* of a color for a background. A simple way to achieve lightness is by
* increasing the transparency or reducing the opacity of the color
* (changing what is known as the alpha channel). Against a white background,
* the color will look lighter.
* <br/><br/>
* There are however several issues. Adding an alpha channel means that the
* rendered color depends on what color lies underneath. Your elements may
* look fine when drawn over a default white background, but if they end up
* over another color, the foreground will be affected. Even if a white
* background is enforced, if your elements ever overlap, you'll also run
* into a problem when using transparency: the overlapping regions will get
* darker than the individual elements.
* <br/><br/>
* To remove the transparency we need to blend the foreground color with the
* background color, using the transparency value to determine how much to
* weight the foreground layer.
* <br/>
* <a href="https://filosophy.org/code/online-tool-to-lighten-color-without-alpha-channel/">Source</a>.
*/
public static double[] flattenColor(Color bgColor, Color fgColor) {
var opacity = fgColor.getOpacity();
return opacity < 1 ?
new double[] {
opacity * fgColor.getRed() + (1 - opacity) * bgColor.getRed(),
opacity * fgColor.getGreen() + (1 - opacity) * bgColor.getGreen(),
opacity * fgColor.getBlue() + (1 - opacity) * bgColor.getBlue(),
} :
new double[] {
fgColor.getRed(),
fgColor.getGreen(),
fgColor.getBlue(),
};
}
}

@ -93,6 +93,88 @@
-fx-font-weight: bold;
}
#color-palette {
-fx-spacing: 20px;
}
#color-palette > .grid {
-fx-vgap: 20px;
-fx-hgap: 40px;
}
#color-palette > .grid > .color-block {
-fx-spacing: 5px;
}
#color-palette > .grid > .color-block > .box {
-fx-min-width: 12em;
-fx-min-height: 5em;
-fx-max-width: 12em;
-fx-max-height: 5em;
-fx-cursor: hand;
}
#color-palette > .grid > .color-block > .box > .wsag-label {
-fx-text-fill: white;
-fx-background-color: #ef5350;
-fx-background-radius: 6px;
-fx-padding: 3px;
}
#color-palette > .grid > .color-block > .box:passed > .wsag-label {
-fx-background-color: #388e3c;
}
#color-palette > .grid > .color-block > .box > .wsag-label > .ikonli-font-icon {
-fx-fill: white;
-fx-icon-color: white;
}
#color-palette > .contrast-checker-area {
-fx-padding: 0 0 0 -20px;
}
#color-palette > .contrast-checker-area > .contrast-checker {
-fx-background-color: -color-contrast-checker-bg;
-fx-hgap: 40px;
-fx-vgap: 20px;
-fx-padding: 20px 20px 40px 20px;
}
#color-palette > .contrast-checker-area > .contrast-checker .label {
-fx-text-fill: -color-contrast-checker-fg;
}
#color-palette > .contrast-checker-area > .contrast-checker .text-field {
-fx-background-insets: 0;
-fx-background-color: transparent;
-fx-background-radius: 0;
-fx-text-fill: -color-contrast-checker-fg;
-fx-border-color: -color-contrast-checker-fg;
-fx-border-width: 0 0 1 0;
}
#color-palette > .contrast-checker-area > .contrast-checker .ikonli-font-icon {
-fx-icon-color: -color-contrast-checker-fg;
-fx-fill: -color-contrast-checker-fg;
}
#color-palette > .contrast-checker-area > .contrast-checker .slider > .thumb {
-fx-background-color: -color-contrast-checker-fg;
}
#color-palette > .contrast-checker-area > .contrast-checker .slider > .track {
-fx-background-color: transparent, -color-contrast-checker-fg;
-fx-opacity: 0.5;
}
#color-palette > .contrast-checker-area > .contrast-checker > .font-box > .text {
-fx-font-size: 4em;
}
#color-palette > .contrast-checker-area > .contrast-checker > .font-box > .ratio {
-fx-font-size: 2em;
}
#color-palette > .contrast-checker-area > .contrast-checker > .wsag-box > * > .wsag-label {
-fx-padding: 0.5em 1em 0.5em 1em;
-fx-background-color: #ef5350;
-fx-background-radius: 6px;
-fx-text-fill: white;
}
#color-palette > .contrast-checker-area > .contrast-checker > .wsag-box > * > .wsag-label > .ikonli-font-icon {
-fx-fill: white;
-fx-icon-color: white;
}
#color-palette > .contrast-checker-area > .contrast-checker > .wsag-box > * > .wsag-label:passed {
-fx-background-color: #388e3c;
}
.bordered {
-fx-border-width: 1px;
-fx-border-color: -color-border-muted;