Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to use ColorAdjust to set a target Color?

Tags:

javafx

Situation

I have an Image with a Circle that is filled with a radial gradient which ranges from white to black/transparency. It serves as a particle and needs to be colorized during its lifecycle.

Problem

I'm using ColorAdjust to apply different colors to the image. Problem is that the colors aren't the way I want them to be. If I use GREEN as target color, I get a pink ball.

Question

How do you calculate ColorAdjust's hue value to match a given target color? Or is there a better way to colorize the images? I can't use the shape itself because using an ImageView is way faster than using the shape.

Code

Here's the code. The problem is most probably that the hue is only the difference to the current color:

import javafx.application.Application;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.SnapshotParameters;
import javafx.scene.effect.ColorAdjust;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.image.WritableImage;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.scene.paint.CycleMethod;
import javafx.scene.paint.RadialGradient;
import javafx.scene.paint.Stop;
import javafx.scene.shape.Circle;
import javafx.stage.Stage;


public class Main extends Application {

    @Override
    public void start(Stage primaryStage) {

            HBox root = new HBox();

            // create alpha masked image
            Image image = createImage(createAlphaMaskedBall(200));

            // create original imageview
            ImageView original = new ImageView( image);

            // create imageview with color adjustment
            ImageView modified = new ImageView( image);

            // colorAdjust effect
            ColorAdjust colorAdjust = new ColorAdjust();

            // set saturation to 1, otherwise hue won't have an effect
            colorAdjust.setSaturation(1); 

            // define target color
            Color targetColor = Color.GREEN; 

            // calculate hue: map from [0,360] to [-1,1]; TODO: here's the problem
            double hue = map( targetColor.getHue(), 0, 360, -1, 1);

            colorAdjust.setHue(hue); 

            // apply color adjustment
            modified.setEffect(colorAdjust);

            root.getChildren().addAll( original, modified);

            Scene scene = new Scene(root,1024,500, Color.BLACK);
            primaryStage.setScene(scene);
            primaryStage.show();

    }

    /**
     * Snapshot an image out of a node, consider transparency.
     * @param node
     * @return
     */
    public static Image createImage( Node node) {

        WritableImage wi;

        SnapshotParameters parameters = new SnapshotParameters();
        parameters.setFill(Color.TRANSPARENT); 

        int imageWidth = (int) node.getBoundsInLocal().getWidth();
        int imageHeight = (int) node.getBoundsInLocal().getHeight();

        wi = new WritableImage( imageWidth, imageHeight);
        node.snapshot(parameters, wi);

        return wi;

    }   

    /**
     * Create an alpha masked ball with gradient colors from White to Black/Transparent. Used e. g. for particles.
     *
     * @param radius
     * @return
     */
    public static Node createAlphaMaskedBall( double radius) {

        Circle ball = new Circle(radius);

        RadialGradient gradient1 = new RadialGradient(0,
            .1,
            0,
            0,
            radius,
            false,
            CycleMethod.NO_CYCLE,
            new Stop(0, Color.WHITE.deriveColor(1,1,1,1)),
            new Stop(1, Color.BLACK.deriveColor(1,1,1,0)));

        ball.setFill(gradient1);

        return ball;
    }

   public static double map(double value, double start, double stop, double targetStart, double targetStop) {
        return targetStart + (targetStop - targetStart) * ((value - start) / (stop - start));
   }

    public static void main(String[] args) {
        launch(args);
    }
}

And a screenshot:

enter image description here

Thank you very much for the help!


Edit:

Solution

Working extended version with RGB sliders and José's solution applied, for the ones who'd like to toy around with it:

import javafx.application.Application;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.SnapshotParameters;
import javafx.scene.control.ComboBox;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.control.Slider;
import javafx.scene.effect.ColorAdjust;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.image.WritableImage;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.scene.paint.CycleMethod;
import javafx.scene.paint.RadialGradient;
import javafx.scene.paint.Stop;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
import javafx.util.Callback;

/**
 * Modify ColorAdjust so that a given target color is matched.
 */
public class Main extends Application {

    Color[] presetColors = new Color[] {Color.RED, Color.GREEN, Color.BLUE, Color.YELLOW, Color.MAGENTA, Color.CYAN, Color.DODGERBLUE, Color.LIGHTGREY, Color.DARKGRAY, Color.BLACK, Color.WHITE, Color.BROWN};

    Color targetColor;

    Rectangle referenceColorRectangle; // display target color as reference (rgb) 
    ColorAdjust colorAdjust;
    Slider redSlider;
    Slider greenSlider;
    Slider blueSlider;
    ComboBox<Color> colorComboBox;

    @Override
    public void start(Stage primaryStage) {

            BorderPane root = new BorderPane();

            // content
            // -------------------------

            HBox content = new HBox();
            content.setStyle("-fx-background-color:black");

            // create alpha masked image
            Image image = createImage(createAlphaMaskedBall(100));

            // create original imageview
            ImageView original = new ImageView( image);

            // create imageview with color adjustment
            ImageView modified = new ImageView( image);

            // colorAdjust effect
            colorAdjust = new ColorAdjust();
            modified.setEffect(colorAdjust);

            content.getChildren().addAll( original, modified);

            // toolbar
            // -------------------------
            GridPane toolbar = new GridPane();

            // presets: show colors as rectangles in the combobox list, as hex color in the combobox selection
            colorComboBox = new ComboBox<>();
            colorComboBox.getItems().addAll( presetColors);

            colorComboBox.setOnAction(new EventHandler<ActionEvent>() {

                @Override
                public void handle(ActionEvent event) {

                    updateSliders();

                }

            });
            colorComboBox.setCellFactory(new Callback<ListView<Color>, ListCell<Color>>() {
                @Override
                public ListCell<Color> call(ListView<Color> p) {
                  return new ListCell<Color>() {
                    private final Rectangle rectangle;
                    {
                      setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
                      rectangle = new Rectangle(100, 10);
                    }

                    @Override
                    protected void updateItem(Color item, boolean empty) {
                      super.updateItem(item, empty);

                      if (item == null || empty) {
                        setGraphic(null);
                      } else {
                        rectangle.setFill(item);
                        setGraphic(rectangle);
                      }
                    }
                  };
                }
              });

            // sliders, value is initialized later
            redSlider = createSlider( 0,255, 0);
            greenSlider = createSlider( 0,255, 0);
            blueSlider = createSlider( 0,255, 0);

            // reference rectangle in rgb
            referenceColorRectangle = new Rectangle( 0, 0, 100, 100);
            referenceColorRectangle.setStroke(Color.BLACK);

            // listener: get new target color from sliders and apply it 
            ChangeListener<Number> listener = new ChangeListener<Number>() {

                @Override
                public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) {

                    updateColor();

                }

            }; 

            redSlider.valueProperty().addListener(listener);
            greenSlider.valueProperty().addListener(listener);
            blueSlider.valueProperty().addListener(listener);

            // add nodes to gridpane
            toolbar.addRow(0, new Label( "Preset"), colorComboBox);
            toolbar.addRow(1, new Label( "Red"), redSlider);
            toolbar.addRow(2, new Label( "Green"), greenSlider);
            toolbar.addRow(3, new Label( "Blue"), blueSlider);

            toolbar.add(referenceColorRectangle, 2, 0, 1, 4);

            // margin for all gridpane nodes
            for( Node node: toolbar.getChildren()) {
                GridPane.setMargin(node, new Insets(5,5,5,5));
            }

            // layout
            root.setTop(toolbar);
            root.setCenter(content);

            // create scene
            Scene scene = new Scene(root,800,400, Color.BLACK);

            primaryStage.setScene(scene);
            primaryStage.show();

            // set height of combobox list
            colorComboBox.lookup(".list-view").setStyle("-fx-pref-height: 200");

            // select 1st color and implicitly initialize the sliders and colors
            colorComboBox.getSelectionModel().selectFirst();

    }

    private void updateColor() {

        // create target color
        targetColor = Color.rgb( (int) redSlider.getValue(), (int) greenSlider.getValue(), (int) blueSlider.getValue());

        // update reference
        referenceColorRectangle.setFill(targetColor);

        // update colorAdjust
        // see http://stackoverflow.com/questions/31587092/how-to-use-coloradjust-to-set-a-target-color
        double hue = map( (targetColor.getHue() + 180) % 360, 0, 360, -1, 1);
        colorAdjust.setHue(hue);

        // use saturation as it is
        double saturation = targetColor.getSaturation();
        colorAdjust.setSaturation(saturation);

        // we use WHITE in the masked ball creation => inverse brightness
        double brightness = map( targetColor.getBrightness(), 0, 1, -1, 0);
        colorAdjust.setBrightness(brightness);

        // System.out.println("Target color: " + targetColor + ", hue 0..360: " + targetColor.getHue() + ", hue 0..1: " + hue);

    }

    private void updateSliders() {

        Color referenceColor = colorComboBox.getValue();

        redSlider.setValue( map( referenceColor.getRed(), 0, 1, 0, 255));
        greenSlider.setValue( map( referenceColor.getGreen(), 0, 1, 0, 255));
        blueSlider.setValue( map( referenceColor.getBlue(), 0, 1, 0, 255));

    }

    private Slider createSlider( double min, double max, double value) {

        Slider slider = new Slider( min, max, value);
        slider.setPrefWidth(600);
        slider.setShowTickLabels(true);
        slider.setShowTickMarks(true);

        return slider;
    }

    /**
     * Snapshot an image out of a node, consider transparency.
     * @param node
     * @return
     */
    public static Image createImage( Node node) {

        WritableImage wi;

        SnapshotParameters parameters = new SnapshotParameters();
        parameters.setFill(Color.TRANSPARENT); 

        int imageWidth = (int) node.getBoundsInLocal().getWidth();
        int imageHeight = (int) node.getBoundsInLocal().getHeight();

        wi = new WritableImage( imageWidth, imageHeight);
        node.snapshot(parameters, wi);

        return wi;

    }   

    /**
     * Create an alpha masked ball with gradient colors from White to Black/Transparent. Used e. g. for particles.
     *
     * @param radius
     * @return
     */
    public static Node createAlphaMaskedBall( double radius) {

        Circle ball = new Circle(radius);

        RadialGradient gradient1 = new RadialGradient(0,
            0,
            0,
            0,
            radius,
            false,
            CycleMethod.NO_CYCLE,
            new Stop(0, Color.WHITE.deriveColor(1,1,1,1)),
            new Stop(1, Color.WHITE.deriveColor(1,1,1,0)));

        ball.setFill(gradient1);

        return ball;
    }

   public static double map(double value, double start, double stop, double targetStart, double targetStop) {
        return targetStart + (targetStop - targetStart) * ((value - start) / (stop - start));
   }

    public static void main(String[] args) {
        launch(args);
    }
}

enter image description here

or the short version of the solution:

double hue = map( (targetColor.getHue() + 180) % 360, 0, 360, -1, 1);
colorAdjust.setHue(hue);

// use saturation as it is
double saturation = targetColor.getSaturation();
colorAdjust.setSaturation(saturation);

// we use WHITE in the masked ball creation => inverse brightness
double brightness = map( targetColor.getBrightness(), 0, 1, -1, 0);
colorAdjust.setBrightness(brightness);
like image 399
Roland Avatar asked Jul 23 '15 12:07

Roland


1 Answers

If you have a look at the color on the center of the circle (for pure white, 100% opacity), as a result of applying the ColorAdust effect, you have magenta #ff00ffff.

The color you are applying has a hue value of 120 (Color.GREEN.getHue()), and if you create this color:

Color color = Color.hsb(120,1,1);
System.out.println(color);

it will print #00ff00ff, which is green (100% opacity), and precisely the opposite color in terms of R,G,B.

Based on this, you could do it the other way around: if you want green as a result, you need to set the inital color as magenta, with a hue value of 300:

targetColor = Color.web("#ff00ffff");

If you have a look on a color wheel:

color hue

you will notice they are diametrically opposite colors, separated 180º.

So as a general rule you can add 180º to the hue color you want to display.

double hue = map( targetColor.getHue()+180, 0, 360, -1, 1);

In my test I've added a slider to check it:

enter image description here

like image 53
José Pereda Avatar answered Oct 03 '22 05:10

José Pereda