diff --git a/sampler/src/main/java/atlantafx/sampler/page/AbstractPage.java b/sampler/src/main/java/atlantafx/sampler/page/AbstractPage.java index 996639f..e1bc882 100755 --- a/sampler/src/main/java/atlantafx/sampler/page/AbstractPage.java +++ b/sampler/src/main/java/atlantafx/sampler/page/AbstractPage.java @@ -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); diff --git a/sampler/src/main/java/atlantafx/sampler/page/general/ColorBlock.java b/sampler/src/main/java/atlantafx/sampler/page/general/ColorBlock.java new file mode 100644 index 0000000..786d664 --- /dev/null +++ b/sampler/src/main/java/atlantafx/sampler/page/general/ColorBlock.java @@ -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 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 actionHandler) { + this.actionHandler = actionHandler; + } + + private Text description(String text) { + var t = new Text(text); + t.getStyleClass().addAll("description", Styles.TEXT_SMALL); + return t; + } +} \ No newline at end of file diff --git a/sampler/src/main/java/atlantafx/sampler/page/general/ColorContrastChecker.java b/sampler/src/main/java/atlantafx/sampler/page/general/ColorContrastChecker.java new file mode 100644 index 0000000..bab6100 --- /dev/null +++ b/sampler/src/main/java/atlantafx/sampler/page/general/ColorContrastChecker.java @@ -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 values = FXCollections.observableArrayList(0f, 0f, 0f, 0f); + private final ReadOnlyObjectWrapper color = new ReadOnlyObjectWrapper<>() { }; + + public ObservableHSLAColor(Color initialColor) { + color.set(initialColor); + values.addListener((ListChangeListener) 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 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(); + } + } +} diff --git a/sampler/src/main/java/atlantafx/sampler/page/general/ColorPalette.java b/sampler/src/main/java/atlantafx/sampler/page/general/ColorPalette.java new file mode 100644 index 0000000..e635f09 --- /dev/null +++ b/sampler/src/main/java/atlantafx/sampler/page/general/ColorPalette.java @@ -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 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() { + @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()); + } +} diff --git a/sampler/src/main/java/atlantafx/sampler/page/general/ThemePage.java b/sampler/src/main/java/atlantafx/sampler/page/general/ThemePage.java index 15b995e..ff9765c 100755 --- a/sampler/src/main/java/atlantafx/sampler/page/general/ThemePage.java +++ b/sampler/src/main/java/atlantafx/sampler/page/general/ThemePage.java @@ -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 themeSelector = themeSelector(); themeSelector.setPrefWidth(200); - Spinner 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 fontSizeSpinner() { - var spinner = new Spinner(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; - } } diff --git a/sampler/src/main/java/atlantafx/sampler/page/general/TypographyPage.java b/sampler/src/main/java/atlantafx/sampler/page/general/TypographyPage.java index 358f1ed..843d7ed 100755 --- a/sampler/src/main/java/atlantafx/sampler/page/general/TypographyPage.java +++ b/sampler/src/main/java/atlantafx/sampler/page/general/TypographyPage.java @@ -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 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 fontSizeSpinner() { + var spinner = new Spinner(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()) - )); - } - } - } } diff --git a/sampler/src/main/java/atlantafx/sampler/util/Containers.java b/sampler/src/main/java/atlantafx/sampler/util/Containers.java index 9de631c..e418899 100755 --- a/sampler/src/main/java/atlantafx/sampler/util/Containers.java +++ b/sampler/src/main/java/atlantafx/sampler/util/Containers.java @@ -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, diff --git a/sampler/src/main/java/atlantafx/sampler/util/JColor.java b/sampler/src/main/java/atlantafx/sampler/util/JColor.java new file mode 100644 index 0000000..82f4f46 --- /dev/null +++ b/sampler/src/main/java/atlantafx/sampler/util/JColor.java @@ -0,0 +1,992 @@ +/** + * MIT License + *

+ * Copyright (c) 2022 National Geospatial-Intelligence Agency + * Source: https://github.com/ngageoint/color-java + *

+ * 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: + *

+ * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + *

+ * 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); + } + +} \ No newline at end of file diff --git a/sampler/src/main/java/atlantafx/sampler/util/JColorUtils.java b/sampler/src/main/java/atlantafx/sampler/util/JColorUtils.java new file mode 100644 index 0000000..18c5820 --- /dev/null +++ b/sampler/src/main/java/atlantafx/sampler/util/JColorUtils.java @@ -0,0 +1,900 @@ +/** + * MIT License + *

+ * Copyright (c) 2022 National Geospatial-Intelligence Agency + * Source: https://github.com/ngageoint/color-java + *

+ * 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: + *

+ * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + *

+ * 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: + *

    + *
  • 0.14285 (7.0:1) for small text in AAA-level
  • + *
  • 0.22222 (4.5:1) for small text in AA-level, or large text in AAA-level
  • + *
  • 0.33333 (3.0:1) for large text in AA-level
  • + *
+ * WCAG defines large text as text that is 18pt and larger, or 14pt and larger if it is bold. + *
+ * More info. + */ + 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 + * W3C. + *
+ * 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. + *

+ * 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. + *

+ * 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. + *

+ * 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. + *
+ * Source. + */ + 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(), + }; + } +} \ No newline at end of file diff --git a/sampler/src/main/resources/assets/styles/index.css b/sampler/src/main/resources/assets/styles/index.css index 6296622..f12d970 100755 --- a/sampler/src/main/resources/assets/styles/index.css +++ b/sampler/src/main/resources/assets/styles/index.css @@ -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;