Update: JNLP file has been fixed. Should work now (me == stupid)…

I am sure most of you have seen the nice demo posted by Mark Anro Silva.

While this is a very nice demo it has a performance problem related to the slider that is very common in JavaFX.
The effectOpacity of the LightBulbEffect is bound to the value of the slider. While this is the correct logic, we run into problems, if those values are updated too often.
A slider is able to create several updates/events every second. When expensive effects are calculated every time, we will get a UI that is not very responsive (and feels slow).

Therefore it is a good idea to not to bind expensive actions to UI triggers directly. Instead you could use a timeline to delay the action for some milliseconds.

For that purpose I have created a DelayedAction.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
package com.cedarsoft.fx;

import javafx.animation.Timeline;
import javafx.animation.KeyFrame;

/**
 * A DeplayedAction.
 * This class can be used to delay actions. This is especially useful if used with sliders or other UI components
 * that update its values very fast.
 * When binding long running / processor intensive actions to those variables your application will start to feel
 * unresponsive.<p/>
 *
 * If maxDelay is set, the action is executed at least {@link #maxDelay} after the call of {@link #schedule()}.
 */

public class DelayedAction {

  /**
   * The action that is called after the delay has passed.
   */

  public-init var action: function(): Void;
  /**
   * The delay
   */

  public-init var delay: Duration = 50ms;
  /**
   * The maximum deplay the action is called after.
   * If the max delay
   */

  public-init var maxDelay: Duration;
  public-read def maxDelaySet = bind maxDelay > 0ms on replace {
    if ( not maxDelaySet ) {
      maxDeplayTimeLine.stop();
    }
  };
  /**
   * if set to true every call to {@link #schedule} will executed immediately.
   * Pending requests will be executed immediately.
   */

  public var executeImmediately: Boolean on replace {
    if ( executeImmediately and timeline.running or maxDeplayTimeLine.running ) {
      execute();
    }
  };
  def timeline: Timeline = Timeline {
      keyFrames: [
        KeyFrame {
          time: bind delay
          action: function () {
            execute();
          }
        }
      ]
    }
  def maxDeplayTimeLine: Timeline = Timeline {
      keyFrames: [
        KeyFrame {
          override var time= bind maxDelay on replace {
            maxDeplayTimeLine.evaluateKeyValues();
          }
          action: function () {
            execute();
          }
        }
      ]
    }

  /**
   * The action is called after the given delay. Every call to schedule restarts the delay.
   *
   */

  public function schedule() {
    if ( executeImmediately ) {
      execute();
      return ;
    }
    if ( maxDelaySet ) {
      maxDeplayTimeLine.play();
    }
    timeline.playFromStart();
  }

  /**
   * Executes the action immediately.
   */

  public function execute(): Void {
    maxDeplayTimeLine.stop();
    timeline.stop();
    action();
  }
}

That action is part of the fx.commons-library (snapshot can be downloaded from
http://nexus.cedarsoft.com/content/groups/public-snapshots/com/cedarsoft/fx/commons/1.0.0-SNAPSHOT/).

How to use DelayedAction

The usage of the DelayedAction is quite simple. Instead of directly binding the variable, you assign the var within a delayed action.

1
2
3
4
5
6
7
8
9
def lightOpacitySetAction: DelayedAction =  DelayedAction {
  delay: 50ms
  maxDelay: 500ms
  executeImmediately: true //this is only necessary for this demo. Will be changed to false as soon as the slider becomes visible
   action: function () {
    println( "---> SETTING NEW VALUE! {slider.value}" );
    lightOpacity2 = slider.value;
  }
};

That action is triggered if the value of the slider is changed. Therefore we introduce a temporary var.

1
2
3
var tmp = bind slider.value on replace{  
  lightOpacitySetAction.schedule();
}

Of course it is necessary to remove the binding of lightOpacity2 to slider.value.

Updated Demo

I have updated the demo application accordingly:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
package com.cedarsoft.fx;

import javafx.scene.shape.QuadCurveTo;
import javafx.scene.shape.LineTo;
import javafx.scene.shape.MoveTo;
import javafx.scene.image.ImageView;
import javafx.scene.Group;
import javafx.scene.shape.Rectangle;
import javafx.scene.paint.Color;
import javafx.scene.effect.GaussianBlur;
import javafx.scene.effect.Glow;
import javafx.scene.shape.Path;
import javafx.scene.control.Button;
import javafx.scene.input.MouseEvent;
import javafx.scene.control.Slider;
import javafx.scene.control.Label;
import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.scene.CustomNode;
import javafx.animation.Timeline;
import javafx.animation.KeyFrame;
import javafx.animation.Interpolator;
import java.lang.UnsupportedOperationException;

function run(_ARGS:String[ ]){
  var buttonstext ="On" on replace {
    if ( buttonstext == "Off" ) {
      lightOpacity1 = .1;
      lightOpacity3 = .3;
      lightOpacity4 = 1;
      controlVisible = true;
      slider.value = .3;
      lightOpacitySetAction.executeImmediately = false;
    } else {
      lightOpacity1 = 0;
      lightOpacity3 = 0;
      lightOpacity4 = 0;
      controlVisible = false;
      lightOpacitySetAction.executeImmediately = true;
      slider.value = 0;
    }
  };

var lightOpacity1: Number = 0;
var lightOpacity2: Number = 0;
var lightOpacity3: Number = 0;
var lightOpacity4: Number = 0;
var lightbulbImg = ImageView {
     image: Image {
       url: "{__DIR__}resources/lightbulb.jpg"
    }
  }


def lightOpacitySetAction: DelayedAction =  DelayedAction {
  delay: 50ms
  maxDelay: 500ms
  executeImmediately: true
   action: function () {
    println( "---> SETTING NEW VALUE! {slider.value}" );
    lightOpacity2 = slider.value;
  }
};


var controlVisible = false;
var effects = Group {
     content: [
      Rectangle {
         width: 412
        height: 520
        fill: Color.YELLOW
        opacity: bind lightOpacity1
      }
      LightBulbEffect {  effectOpacity: bind lightOpacity2 }
      LightBulbEffect {  effectOpacity: bind lightOpacity3 }
      Rectangle {
         effect: GaussianBlur {
           radius: 10
          input: Glow { }
        }
        translateY: 178
        translateX: 165
        width: 65
        height: 8
        arcWidth: 8
        arcHeight: 8
        fill: Color.WHITE
        stroke: Color.ORANGE
        strokeWidth: 1.8
        opacity: bind lightOpacity4
      }
      Path {
         effect: GaussianBlur {
           radius: 1
          input: Glow { }
        }
        stroke: Color.WHITE
        strokeWidth: 1.8
        opacity: bind lightOpacity4
        elements: [
          MoveTo {  x: 172 y: 183 },
          LineTo {  x: 183 y: 182 },
          QuadCurveTo {
             controlX: 198 controlY: 183
            x: 213 y: 182
          }
          QuadCurveTo {
             controlX: 214 controlY: 183
            x: 225 y: 185
          }
        ]
      }
    ]
  }
var togglebutton = Button {
     translateY: 350
    translateX: 260
    text: bind buttonstext
    onMousePressed: function ( ev: MouseEvent ): Void {
      if ( buttonstext == "Off" ) {
        buttonstext = "On";
      } else {
        buttonstext = "Off";
      }
    }
  }
var slider: Slider = Slider {
    translateY: 420
    translateX: 128
    max: .4
    min: 0
    visible: bind controlVisible
  }

var tmp = bind slider.value on replace{  
  lightOpacitySetAction.schedule();
}

var controls = Group {
     content: [
      togglebutton,
      slider,
      Label {  translateX: 120 translateY: 419 text: "-" visible: bind controlVisible }
      Label {  translateX: 268 translateY: 419 text: "+" visible: bind controlVisible }
    ]
  }

Stage {
   title: "JavaFX Light Bulb"
  scene: Scene {
     width: 402
    height: 499
    fill: Color.BLACK
    content: [
      lightbulbImg,
      effects,
      controls
    ]
  }
}
}


public class LightBulbEffect extends CustomNode {

    public var effectOpacity: Number;

    override function create() {
        Path {
            translateX: -10
            translateY: -8
            effect: GaussianBlur {input: Glow {level: 1} radius: 63}
            fill: Color.YELLOW
            stroke: Color.YELLOW
            strokeWidth: 5
            opacity: bind effectOpacity
            elements: [
                MoveTo {x: 127 y: 132},
                QuadCurveTo {
                    controlX: 206 controlY: 48
                    x: 287 y: 132
                }
                QuadCurveTo {
                    controlX: 320 controlY: 180
                    x: 295 y: 230
                }
                QuadCurveTo {
                    controlX: 290 controlY: 240
                    x: 270 y: 280
                }
                QuadCurveTo {
                    controlX: 270 controlY: 295
                    x: 260 y: 320
                }
                LineTo {x: 153 y: 320}
                QuadCurveTo {
                    controlX: 143 controlY: 295
                    x: 143 y: 280
                }
                QuadCurveTo {
                    controlX: 118 controlY: 240
                    x: 115 y: 230
                }
                QuadCurveTo {
                    controlX: 95 controlY: 180
                    x: 127 y: 132
                }
            ]
        }
    }
}