- Hello, Project Scene Graph
- Nodes
- Animation
- Effects
- The FX Layer
- Conclusion
The FX Layer
Project Scene Graph includes a com.sun.scenario.scenegraph.fx package whose classes have the FX prefix. If you're wondering why these classes contain capabilities that mirror the capabilities of equivalent SG-prefixed classes, you'll find an explanation for the FX classes in the message thread here.
Specifically, Hans Muller and Chris Campbell, another Sun developer involved in the design and implementation of Project Scene Graph, give reasons of efficiency, simplicity, safety, and less awkwardness. (Furthermore, these classes are used to implement some of JavaFX's APIs.) Chris's example, which I've taken from the message thread and repeat below, concisely illustrates these benefits:
// Go from here... SGShape shape = new SGShape(); shape.setShape(circle); // oh, now I need an Effect, so add another node to the chain SGEffect effect = new SGEffect(); effect.setEffect(blur); effect.setChild(shape); // to here: FXShape shape = new FXShape(); shape.setShape(circle); shape.setEffect(blur);
The resulting code is simpler and less awkward because you don't create an intermediate SGEffect node, invoke its setEffect() method to wrap the blur effect, and invoke this node's setChild() method to make shape a child of effect. The code is safer because you'll never forget to call setChild()—it's called automatically. Finally, the FX classes are optimized to create nodes only when needed.
The FX class hierarchy begins with SGWrapper, which I mentioned earlier. This class serves as the base class for nodes that maintain an internal graph of nodes. The hierarchy continues with the FXNode subclass, whose own subclasses include FXComponent, FXGroup, FXImage, FXShape, FXText, and FXAbstractShape (which is the parent of FXShape and FXText).
Glowing Images
I've created a version of ScaledImages.java that demonstrates FXGroup and FXImage, along with the Glow class for establishing an image's glow effect. Specifically, this application causes the image over which the mouse is hovering to glow. Check out Listing 5 for the GlowingImages.java source code.
Listing 5 GlowingImages.java
// GlowingImages.java import java.awt.*; import java.awt.event.*; import java.awt.geom.*; import java.awt.image.*; import java.io.*; import java.util.*; import javax.imageio.*; import javax.swing.*; import com.sun.scenario.animation.*; import com.sun.scenario.effect.*; import com.sun.scenario.scenegraph.*; import com.sun.scenario.scenegraph.event.*; import com.sun.scenario.scenegraph.fx.*; public class GlowingImages { final static int WIDTH = 650; final static int HEIGHT = 250; final static int GLOW_TIME = 250; final static int PADDING = 50; public static void main (String [] args) { Runnable r = new Runnable () { public void run () { createAndShowGUI (); } }; EventQueue.invokeLater (r); } public static void createAndShowGUI () { JFrame f = new JFrame ("Glowing Images"); f.setDefaultCloseOperation (JFrame.EXIT_ON_CLOSE); // JSGPanel is a JComponent that renders a scene graph. Instead of // instantiating this class, this method instantiates a subclass that // allows a scene graph to be properly centered. CenteringSGPanel panel = new CenteringSGPanel (); panel.setBackground (Color.BLACK); panel.setPreferredSize (new Dimension (WIDTH, HEIGHT)); // Create a group to hold three FXImage nodes that each present a single // image. FXGroup imageNodes = new FXGroup (); imageNodes.setID ("imageNodes: FXGroup"); // I've chosen to present images of three planets: Venus, Mars, and // Jupiter. String [] imageNames = { "venus.gif", "mars.gif", "jupiter.gif" }; // Populate the group. int xOffset = 0; for (String name: imageNames) { FXImage imageNode = new FXImage (); imageNode.setID ("imageNode: FXImage"); // Create and register a mouse listener with each image node. This // listener is responsible for animating an image node's glow or // fade. SGMouseListener listener; listener = new SGMouseAdapter () { public void mouseEntered (MouseEvent me, SGNode n) { // Obtain the node's animation clip for // performing a glow. Clip clip; clip = (Clip) n.getAttribute ("doGlow"); // An IllegalStateException is thrown if // clip.start() is invoked when the clip is // already running. The following test avoids // this exception. if (clip.isRunning ()) return; // Start the animation. clip.start (); } public void mouseExited (MouseEvent me, SGNode n) { // Obtain the node's animation clip for // performing a fade. Clip clip; clip = (Clip) n.getAttribute ("doFade"); // An IllegalStateException is thrown if // clip.start() is invoked when the clip is // already running. The following test avoids // this exception. if (clip.isRunning ()) return; // Start the animation. clip.start (); } }; imageNode.addMouseListener (listener); try { // Load next image. BufferedImage origImage = ImageIO.read (new File (name)); imageNode.setImage (origImage); // Give image node a glowing effect. Glow glow = new Glow (); glow.setLevel (0.0f); imageNode.setEffect (glow); // Make sure that image node is offset from previous image node // so that it isn't positioned over the previous node. imageNode.setTranslateX (xOffset); // Space image nodes appropriately. xOffset += origImage.getWidth ()+PADDING; // Create animation clips for glowing and fading. Clip clipGlow = Clip.create (GLOW_TIME, new BeanProperty<Float> (imageNode.getEffect (), "Level"), 0.0f, 1.0f); Clip clipFade = Clip.create (GLOW_TIME, new BeanProperty<Float> (imageNode.getEffect (), "Level"), 1.0f, 0.0f); // Store the animation clips for later access. imageNode.putAttribute ("doGlow", clipGlow); imageNode.putAttribute ("doFade", clipFade); // Add image node to the group. imageNodes.add (imageNode); } catch (Exception e) { System.err.println ("Image loading error: "+e); } } // Establish the scene. panel.setScene (imageNodes); // Dump the scene graph hierarchy to the standard output. dump (panel.getScene (), 0); // Install the panel into the Swing hierarchy, pack it to its preferred // size, and don't let it be resized. Furthermore, center and display // the main window on the screen. f.setContentPane (panel); f.pack (); f.setResizable (false); f.setLocationRelativeTo (null); f.setVisible (true); } public static void dump (SGNode node, int level) { for (int i = 0; i < level; i++) System.out.print (" "); System.out.println (node.getID ()); if (node instanceof SGParent) { java.util.List<SGNode> children = ((SGParent) node).getChildren (); Iterator<SGNode> it = children.iterator (); while (it.hasNext ()) dump (it.next (), level+1); } } } // JSGPanel has been subclassed in order to properly center the scene within // this Swing component. class CenteringSGPanel extends JSGPanel { private SGNode sceneNode; private SGTransform.Translate centeringTNode; @Override public void doLayout () { if (sceneNode != null) { Rectangle2D bounds = sceneNode.getBounds (); centeringTNode.setTranslateX (-bounds.getX ()+ (getWidth ()-bounds.getWidth ())/2.0); centeringTNode.setTranslateY (-bounds.getY ()+ (getHeight ()-bounds.getHeight ())/2.0); } } @Override public void setScene (SGNode sceneNode) { this.sceneNode = sceneNode; centeringTNode = SGTransform.createTranslation (0f, 0f, sceneNode); centeringTNode.setID ("centeringTNode: SGTransform.Translate"); super.setScene (centeringTNode); } }
I tried to keep the source code similar to ScaledImages.java so that you can see the benefits of working with FX classes—for consistency, I kept CenteringSGPanel as is, although I could have rewritten it in terms of FX classes.
Compile the source code via javac -cp Scenario-0.6.jar GlowingImages.java, and run the application via java -cp Scenario-0.6.jar;. GlowingImages. The user interface appears in Figure 5.
Figure 5 Mars is glowing.
This application also sends the following hierarchy to the standard output—the null values are unnamed nodes automatically created by FXNode and its subclasses:
centeringTNode: SGTransform.Translate imageNodes: FXGroup null imageNode: FXImage null null imageNode: FXImage null null null imageNode: FXImage null null null
If you replace the dump() method's System.out.println (node.getID ()); statement with System.out.println (node);, this hierarchy starts to make more sense:
centeringTNode: SGTransform.Translate imageNodes: FXGroup SGGroup imageNode: FXImage SGEffect SGImage imageNode: FXImage SGTransform.Translate SGEffect SGImage imageNode: FXImage SGTransform.Translate SGEffect SGImage
Creating an FXGroup results in an SGGroup being created. Similarly, calls to the FXImage setImage() and setEffect() methods result in the creation of SGImage and SGEffect nodes. As an example of FX efficiency, an SGTransform.Translate node isn't created when xOffset contains 0.