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.
[cc lang="c" lines="5" tab_size="2" lines="20"]
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.
*
* 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();
}
}
[/cc]
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.
[cc lang="c"]
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;
}
};
[/cc]
That action is triggered if the value of the slider is changed. Therefore we introduce a temporary var.
[cc lang="c"]var tmp = bind slider.value on replace{
lightOpacitySetAction.schedule();
}
[/cc]
Of course it is necessary to remove the binding of lightOpacity2 to slider.value.
Updated Demo
I have updated the demo application accordingly:
[cc lang="c"]
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
}
]
}
}
}[/cc]
