JavaFX Light Bulb with improved UI performance
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]

April 27th, 2010 at 22:11
Nice post, I noticed that the slider was really unresponsive when I first tried that demo a few times ago – I thought it was due to poor performance of the JavaFX runtime rather than inefficient application code.
The launch button on this page does not work at this time:
com.sun.deploy.net.FailedDownloadException: Unable to load resource: file:/home/johannes/NetBeansProjects/fx-commons/dist/fx-commons.jnlp
April 28th, 2010 at 10:09
Thanks for the hint. I just used a JNLP created by Netbeans. Obviously I should have looked at it first… Thanks.
April 28th, 2010 at 16:58
Another way to do it is to round the slider value to the nearest X. Where X depends on how many stops you want between min and max. Below is some example code showing how to limit a slider to 10 stops between min and max. This should be much lighter weight than using a Timer.
import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.text.Text;
import javafx.scene.text.Font;
import javafx.scene.control.Slider;
var text:Text;
var slider:Slider;
def numberOfStops = 10;
def factor = bind 1/ ((slider.max – slider.min)/numberOfStops);
var sliderValue = bind ((slider.value*factor) as Integer) / factor;
Stage {
scene: Scene {
width: 250
height: 150
content: [
text = Text {
font : Font { size : 16 }
x: 10
y: 30
content: bind "Slider value = {slider.value}\n sliderValue={sliderValue}"
}
slider = Slider {
layoutX: 20
layoutY: 80
min: 0
max: 0.4
majorTickUnit: 2
minorTickCount: 1
snapToTicks: true
}
]
}
}
April 29th, 2010 at 19:43
This great technique should work for Swing too.
May 1st, 2010 at 13:38
Very nice idea! I will take a closer look at your code. At least for sliders this seems to be a very good approach.
But I think for this demo particular demo it won’t work. The change of the opacity takes too long.