2

Chaining two effects in JavaFX is easy with .setInput()

if (isDropShadowEnabled)
    innerShadow.setInput(dropShadow);
content.setEffect(innerShadow);

But how do I chain multiple effects when any one of those effects may not be enabled?

Say a third effect is Bloom. So I want a Dropshadow, InnerShadow and Bloom, but at other times just a DropShadow and Bloom and no InnerShadow. I'm trying to avoid lots of 'if's and I've been looking for something along the lines of effect.getChildren().add('lots of effects'). But nothing has jumped out after hours of Googling.

Frank
  • 521
  • 7
  • 19

1 Answers1

4

Perhaps you could use some ideas from this code. Sorry it is a bit complicated and I don't have time to explain it right now.

s1 s2 s3 s4

import javafx.application.Application;
import javafx.beans.property.*;
import javafx.geometry.*;
import javafx.scene.Scene;
import javafx.scene.control.CheckBox;
import javafx.scene.effect.*;
import javafx.scene.image.*;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;

public class Effector extends Application{

    @Override
    public void start(Stage stage) throws Exception {
        ImageView imageView = new ImageView(
                new Image(
                        "http://icons.iconarchive.com/icons/designbolts/smurfs-movie/128/smurfette-icon.png"
                )
        );

        VBox effectControls = new VBox(5);
        effectControls.setPadding(new Insets(5));
        effectControls.getChildren().setAll(
                new EffectController(
                        "Drop Shadow",
                        new DropShadow()
                ),
                new EffectController(
                        "Inner Shadow",
                        new InnerShadow()
                ),
                new EffectController(
                        "Bloom",
                        new Bloom()
                )
        );

        EffectPipeline pipeline = new EffectPipeline(
                effectControls.getChildren().stream().map(
                        node -> ((EffectController) node).getChainableEffect()
                ).toArray(ChainableEffect[]::new)
        );

        imageView.effectProperty().bind(
                pipeline.chainedEffectProperty()
        );

        VBox layout = new VBox(
                5,
                effectControls,
                imageView
        );
        layout.setPadding(new Insets(5));
        layout.setAlignment(
                Pos.CENTER
        );

        stage.setScene(
                new Scene(
                        layout
                )
        );
        stage.setResizable(false);
        stage.show();
    }

    class EffectController extends CheckBox {
        private final ChainableEffect chainableEffect;

        public EffectController(
                String text,
                Effect effect
        ) {
            super(text);

            chainableEffect = new ChainableEffect(
                    effect
            );

            this.setSelected(!chainableEffect.isDisabled());
            this.selectedProperty().addListener(
                    (observable, oldValue, newValue) -> {
                        chainableEffect.disabledProperty().set(!newValue);
                    }
            );
        }

        public ChainableEffect getChainableEffect() {
            return chainableEffect;
        }
    }

    class EffectPipeline {
        private List<ChainableEffect> effects;

        private ReadOnlyObjectWrapper<Effect> chainedEffect = new ReadOnlyObjectWrapper<>();

        public EffectPipeline(ChainableEffect... effects) {
            this.effects = Arrays.asList(effects);

            for (ChainableEffect chainableEffect: effects) {
                chainableEffect.disabledProperty().addListener((observable, oldValue, newValue) -> {
                    refreshChainedEffect();
                });
            }

            refreshChainedEffect();
        }

        public void refreshChainedEffect() {
            ChainableEffect firstEffect = null, lastEffect = null;

            for (ChainableEffect nextEffect : effects) {
                nextEffect.setInput(null);
                if (nextEffect.isDisabled()) {
                    continue;
                }

                if (firstEffect == null) {
                    firstEffect = nextEffect;
                    lastEffect = firstEffect;
                    continue;
                }

                lastEffect.setInput(nextEffect);
                lastEffect = nextEffect;
            }

            chainedEffect.setValue(
                    firstEffect == null
                            ? null
                            : firstEffect.getEffect()
            );
        }

        public Effect getChainedEffect() {
            return chainedEffect.get();
        }

        public ReadOnlyObjectProperty<Effect> chainedEffectProperty() {
            return chainedEffect.getReadOnlyProperty();
        }
    }

    class ChainableEffect {
        private final Effect effect;
        private final Method inputMethod;
        private final BooleanProperty disabled = new SimpleBooleanProperty(
                false
        );

        public ChainableEffect(Effect effect) {
            if (effect == null) {
                throw new IllegalArgumentException("Effect for chaining must not be null");
            }

            this.effect = effect;

            try {
                inputMethod = effect.getClass().getMethod(
                        "setInput",
                        Effect.class
                );
            } catch (NoSuchMethodException e) {
                throw new IllegalArgumentException("Effect for chaining must implement the setInput method", e);
            } catch (SecurityException e) {
                throw new IllegalStateException("Creating chainable effects requires a reflection capable security environment", e);
            }
        }

        public ChainableEffect setInput(ChainableEffect chainableEffect) {
            try {
                inputMethod.invoke(
                        this.getEffect(),
                        chainableEffect != null
                                ? chainableEffect.getEffect()
                                : null
                );

                return this;
            } catch (IllegalAccessException e) {
                throw new IllegalStateException("Chainable effect does not support access rights for setInput", e);
            } catch (InvocationTargetException e) {
                throw new IllegalStateException("Unable to set the input for a chainable effect", e);
            }
        }

        public Effect getEffect() {
            return effect;
        }

        public boolean isDisabled() {
            return disabled.get();
        }

        public BooleanProperty disabledProperty() {
            return disabled;
        }

        public void setDisabled(boolean disabled) {
            this.disabled.set(disabled);
        }
    }

    public static void main(String[] args) {
        launch(args);
    }
}
jewelsea
  • 150,031
  • 14
  • 366
  • 406
  • This is going to need some serious study, so it'll be a few hours, but thanks! – Frank Aug 15 '15 at 02:53
  • Great! This works well. I use text rather than an image and I have also added lighting to the text. The problem with lighting is that it doesn't have a setInput() method so it's not chainable. I solved that problem by overlaying the text with itself and applying the chained effects to the bottom text and the lighting to the top text. The final result is fantastic. Thanks jewelsea. – Frank Aug 15 '15 at 19:38