JavaFX Applets

September 19, 2008

Using the Java Deployment Toolkit with JavaFX Applets

First, let me apologize for resurrecting the very humble JavaFX program shown below, but I want to keep this example very succinct.  This will enable you to use it as "starter code" for JavaFX applet deployment.  Note: To see more functional JavaFX programs, please see articles in the JFX Custom Nodes category.

BindToFunctionApplet_SDK_Preview

Note: Thanks to reader "mbien" (see comments) for pointing our that the colors of the original applet in this post were hideous (my words).  I then consulted graphics designer Mark Dingman of Malden Labs who gave me a graphical mock-up from which I created the above applet.  Here's the code for this applet, updated for the JavaFX SDK preview:

/*
 *  BindToFunctionApplet.fx - A compiled JavaFX program that demonstrates
 *                            how to create JavaFX applets.
 *                            It also demonstrates binding to a function.
 *
 *  Developed 2008 by Jim Weaver (development) and Mark Dingman (graphic design)
 *  to serve as a JavaFX Script example.
 */
package com.javafxpert.bind_to_function;

import javafx.application.*;
import javafx.ext.swing.*;
import javafx.scene.*;
import javafx.scene.geometry.*;
import javafx.scene.paint.*;
import javafx.scene.text.*;
import javafx.scene.transform.*;
import java.lang.Math;

class CircleModel {
  attribute diameter:Integer;
  
  bound function getArea():Number {
    Math.PI * Math.pow(diameter / 2, 2);
  }
}

Application {
  var cModel = CircleModel {};
  var componentViewRef:ComponentView;
  var stageRef:Stage;
  stage: 
    stageRef = Stage {
      var labelFont = Font {
        name: "Sans Serif"
        style: FontStyle.PLAIN
        size: 32
      }
      fill:
        LinearGradient {
          startX: 0.0
          startY: 0.0
          endX: 0.0
          endY: 1.0
          stops: [
            Stop { 
              offset: 0.0 
              color: Color.rgb(0, 168, 255) 
            },
            Stop { 
              offset: 1.0 
              color: Color.rgb(0, 65, 103) 
            }
          ]
        }
      content: [
        Circle {
          centerX: 250
          centerY: 250
          radius: bind cModel.diameter / 2
          fill:
            LinearGradient {
              startX: 0.0
              startY: 0.0
              endX: 0.0
              endY: 1.0
              stops: [
                Stop { 
                  offset: 0.0 
                  color: Color.rgb(74, 74, 74) 
                },
                Stop { 
                  offset: 1.0 
                  color: Color.rgb(9, 9, 9) 
                }
              ]
            }
        },
        Text {
          font: labelFont
          x: 30
          y: 70
          fill: Color.BLACK
          content: bind "Diameter: {cModel.diameter}"
        },
        Text {
          font: labelFont
          x: 260
          y: 70
          fill: Color.BLACK
          content: bind "Area: {%3.2f cModel.getArea()}"
        },
        componentViewRef = ComponentView {
          transform: bind 
            Translate.translate(40, stageRef.height - 30 -
                                   componentViewRef.getHeight())
          component:
            Slider {
              minimum: 0
              maximum: 400
              preferredSize: bind [stageRef.width - 80, 20]
              value: bind cModel.diameter with inverse
            }
        }
      ]
    }
}

Why Use the Java Deployment Toolkit for Java Applets?

According to Sun's Java Deployment Toolkit overview page, "Desktop clients have a wide variety of Java Platforms installed, from the Microsft VM to Sun's latest Java SE 6 updates. They run various operating systems from Sun, Microsoft, Apple, Red Hat, and others, and are connected to the internet at a wide range of connection speeds. How are content providers to deliver Java content to all of these clients with the best possible user experience?

Various sources have published JavaScript techniques for detecting and deploying the Java Platform for use by Java Plug-In applets and Java Web Start applications. These scripts generally have serious limitations and fail to support the varied combinations of browser, OS, and configuration options found on today's clients.

The Java Deployment Toolkit allows developers to easily deploy applets and applications to a large variety of clients with JavaScripts. It also provides advice on using some of the most powerful features available in Java Web Start and Java Plug-In, and an outline of the differences between these two deployment vehicles.
"

In a nutshell, the Java Deployment Toolkit is a JavaScript library maintained by Sun and always available at runtime by your HTML code.  This library has several methods that perform tasks such as sensing Java-related infrastructure and installing the JRE on client machines.  We'll use one of these methods, namely runApplet, to run a JavaFX applet with a specified minimum JRE version.  Here's the HTML and JavaScript code I'm using to deploy today's example applet:

<html>
<script src="http://java.com/js/deployJava.js"></script>
<script>
  var attributes = {codebase:'http://jmentor.com/JFX/BindToFunctionApplet',
    code:'javafx.application.Applet.class',
    archive:'BindToFunctionApplet.jar, javafxrt.jar, Scenario.jar, javafxgui.jar, javafx-swing.jar',
    width:500, height:500, java_arguments:'-Djnlp.packEnabled=true'};
  var parameters = {"ApplicationClass":"com.javafxpert.bind_to_function.BindToFunctionApplet",
                    "draggable":"true"};
  var version = '1.6.0' ;
  deployJava.runApplet(attributes, parameters, version);
</script>
</html>


Notice that the above code enables dragging the applet onto the desktop, as well as using Pack200 formatted JAR files, if the client machine has Java SE 6 update 10 installed.  Give the applet a whirl to see its deployment behavior on your machine.  By the way, according to the Java SE 6 Update 10 plug-in docs, "by default, the gesture to drag the applet out of the web browser is Alt + Left click + Drag."

Thanks,
Jim Weaver
JavaFXpert.com weblog

September 04, 2008

JavaFX Applets Meet Google Chrome

In the JFX Custom Nodes category of this blog, graphics designer Mark Dingman of Malden Labs and I have been collaborating on an imaginary Sound Beans application. This category contains a growing series of posts in which we are demonstrating how to create JavaFX UI custom controls.  This series also provide a case study in how a graphics designer and an application developer can work together effectively in developing JavaFX applications.  Today I'd like to highlight the recent Google Chrome browser announcement by showing you how to create and run a JavaFX applet in Chrome.  Here's a screenshot of the TableNode example from an earlier post running as a JavaFX applet in Chrome:


TableNodeExampleApplet

To try this out, first obtain Google Chrome and install it.  Then obtain Java SE 6 Update 10 and install it as well.  By the way, installing Java SE 6 update 10 will enable this JavaFX applet to run on Firefox 3 and Internet Explorer as well.  Go ahead and run this example, being sure to scroll the custom TableNode control and to click on its rows.  Also, select the Burn icon and move the slider to demonstrate the custom ProgressNode control.


Looking at the Code

In addition to the ButtonNode.fx, MenuNode.fx, DeckNode.fx, ProgressNode.fx and TableNode.fx files from previous posts in this series, you'll need the following files:

TableNodeExampleApplet.fx:

/*
 *  TableNodeExampleApplet.fx -
 *  An example of using the TableNode custom node in an Applet.  It also
 *  demonstrates the ProgressNode, DeckNode, MenuNode and ButtonNode
 *  custom nodes
 *
 *  Developed 2008 by James L. Weaver (jim.weaver at lat-inc.com)
 *  to demonstrate how to create custom nodes and applets in JavaFX
 */
package com.javafxpert.table_node_example.ui;

import javafx.application.*;
import javafx.ext.swing.*;
import javafx.scene.*;
import javafx.scene.geometry.*;
import javafx.scene.image.*;
import javafx.scene.layout.*;
import javafx.scene.paint.*;
import javafx.scene.text.*;
import javafx.scene.transform.*;
import java.lang.Object;
import java.lang.System;
import com.javafxpert.custom_node.DeckNode;
import com.javafxpert.custom_node.TableNode;
import com.javafxpert.custom_node.ProgressNode;
import com.javafxpert.custom_node.ButtonNode;
import com.javafxpert.custom_node.MenuNode;
import com.javafxpert.table_node_example.model.TableNodeExampleModel;

var deckRef:DeckNode;

Application {
  var model = TableNodeExampleModel.getInstance();
  var stageRef:Stage;
  var menuRef:MenuNode;
  stage:
    stageRef = Stage {
      fill: Color.BLACK
      content: [
        deckRef = DeckNode {
          fadeInDur: 700ms
          content: [
            // The "Splash" page
            Group {
              var vboxRef:VBox;
              var splashFont =
                Font {
                  name: "Sans serif"
                  style: FontStyle.BOLD
                  size: 12
                };
              id: "Splash"
              content: [
                ImageView {
                  image:
                    Image {
                      url: "{__DIR__}images/splashpage.png"
                    }
                },
                vboxRef = VBox {
                  translateX: bind stageRef.width - vboxRef.getWidth() - 10
                  translateY: 215
                  spacing: 1
                  content: [
                    Text {
                      content: "A Fictitious Audio Application that Demonstrates"
                      fill: Color.WHITE
                      font: splashFont
                    },
                    Text {
                      content: "Creating JavaFX Custom Nodes"
                      fill: Color.WHITE
                      font: splashFont
                    },
                    Text {
                      content: "Application Developer: Jim Weaver"
                      fill: Color.WHITE
                      font: splashFont
                    },
                    Text {
                      content: "Graphics Designer: Mark Dingman"
                      fill: Color.WHITE
                      font: splashFont
                    },
                  ]
                }
              ]
            },
            // The "Play" page
            VBox {
              var tableNode:TableNode
              id: "Play"
              spacing: 4
              content: [
                Group {
                  content: [
                    ImageView {
                      image:
                        Image {
                          url: "{__DIR__}images/playing_currently.png"
                        }
                    },
                    Text {
                      textOrigin: TextOrigin.TOP
                      content: bind "{tableNode.selectedIndex}"
                      font: Font {
                        size: 24
                      }
                    }
                  ]
                },
                tableNode = TableNode {
                  height: 135
                  rowHeight: 25
                  rowSpacing: 2
                  columnWidths: [150, 247, 25, 70]
                  tableFill: Color.BLACK
                  rowFill: Color.#1c1c1c
                  selectedRowFill: Color.#2d2d2d
                  selectedIndex: -1
                  vertScrollbarWidth: 20
                  vertScrollbarFill: LinearGradient {
                    startX: 0.0
                    startY: 0.0
                    endX: 1.0
                    endY: 0.0
                    stops: [
                      Stop {
                        offset: 0.0
                        color: Color.#0b0b0b
                      },
                      Stop {
                        offset: 1.0
                        color: Color.#343434
                      }
                    ]
                  }
                  vertScrollbarThumbFill: Color.#efefef
                  content: bind
                    for (obj in model.playlistObjects) {
                      if (obj instanceof String)
                        Text {
                          textOrigin: TextOrigin.TOP
                          fill: Color.#b7b7b7
                          content: obj as String
                          font:
                            Font {
                              size: 11
                            }
                        }
                      else if (obj instanceof Image)
                        ImageView {
                          image: obj as Image
                        }
                      else
                        null
                    }
                  onSelectionChange:
                    function(row:Integer):Void {
                      System.out.println("Table row #{row} selected");
                    }
                }
              ]
            },
            // The "Burn" page
            Group {
              var vboxRef:VBox;
              id: "Burn"
              content: [
                vboxRef = VBox {
                  translateX: bind stageRef.width / 2 - vboxRef.getWidth() / 2
                  translateY: bind stageRef.height / 2 - vboxRef.getHeight() / 2
                  spacing: 15
                  content: [
                    Text {
                      textOrigin: TextOrigin.TOP
                      content: "Burning custom playlist to CD..."
                      font:
                        Font {
                          name: "Sans serif"
                          style: FontStyle.PLAIN
                          size: 22
                        }
                      fill: Color.#d3d3d3
                    },
                    ProgressNode {
                      width: 430
                      height: 15
                      progressPercentColor: Color.#bfdfef
                      progressTextColor: Color.#0c1515
                      progressText: bind "{model.remainingBurnTime} Remaining"
                      progressFill:
                        LinearGradient {
                          startX: 0.0
                          startY: 0.0
                          endX: 0.0
                          endY: 1.0
                          stops: [
                            Stop {
                              offset: 0.0
                              color: Color.#00c0ff
                            },
                            Stop {
                              offset: 0.20
                              color: Color.#00acea
                            },
                            Stop {
                              offset: 1.0
                              color: Color.#0070ae
                            },
                          ]
                        }
                      barFill:
                        LinearGradient {
                          startX: 0.0
                          startY: 0.0
                          endX: 0.0
                          endY: 1.0
                          stops: [
                            Stop {
                              offset: 0.0
                              color: Color.#707070
                            },
                            Stop {
                              offset: 1.0
                              color: Color.#585858
                            },
                          ]
                        }
                      progress: bind model.burnProgressPercent / 100.0
                    },
                    ComponentView {
                      component:
                        FlowPanel {
                          background: Color.BLACK
                          content: [
                            Label {
                              text: "Slide to simulate burn progress:"
                              foreground: Color.#d3d3d3
                            },
                            Slider {
                              orientation: Orientation.HORIZONTAL
                              minimum: 0
                              maximum: 100
                              value: bind model.burnProgressPercent with inverse
                              preferredSize: [200, 20]
                            }
                          ]
                        }
                    }
                  ]
                }
              ]
            },
            // The "Config" page
            Group {
              id: "Config"
              content: [
                ImageView {
                  image:
                    Image {
                      url: "{__DIR__}images/config.png"
                    }
                }
              ]
            },
            // The "Help" page
            Group {
              id: "Help"
              content: [
                ImageView {
                  image:
                    Image {
                      url: "{__DIR__}images/help.png"
                    }
                }
              ]
            }
          ]
        },
        menuRef = MenuNode {
          translateX: bind stageRef.width / 2 - menuRef.getWidth() / 2
          translateY: bind stageRef.height - menuRef.getHeight()
          buttons: [
            ButtonNode {
              title: "Play"
              imageURL: "{__DIR__}icons/play.png"
              action:
                function():Void {
                  deckRef.visibleNodeId = "Play";
                }
            },
            ButtonNode {
              title: "Burn"
              imageURL: "{__DIR__}icons/burn.png"
              action:
                function():Void {
                  deckRef.visibleNodeId = "Burn";
                }
            },
            ButtonNode {
              title: "Config"
              imageURL: "{__DIR__}icons/config.png"
              action:
                function():Void {
                  deckRef.visibleNodeId = "Config";
                }
            },
            ButtonNode {
              title: "Help"
              imageURL: "{__DIR__}icons/help.png"
              action:
                function():Void {
                  deckRef.visibleNodeId = "Help";
                }
            },
          ]
        }
      ]
    }
}


Note that the Application class has a stage attribute just as the Frame had in previous examples.  Here's the TableNodeExamplePage.html file that you'll open in your browser.  The draggable param, by the way, enables that neat "pull the applet out of the browser" trick that I'll show you in a bit:

<html>
  <body bgcolor="Black">
    <center>
      <applet code="javafx.application.Applet" width=500 height=400
          archive="javafxrt.jar, Scenario.jar, javafxgui.jar, javafx-swing.jar, TableNodeExample.jar">
        <param name="ApplicationClass" value="com.javafxpert.table_node_example.ui.TableNodeExampleApplet"/>
        <param name="jnlp_href" value="TableNodeExampleApplet.jnlp"/>
        <param name="draggable" value="true">
      </applet>
    </center>
  </body>
</html>


Finally, here's the Java Web Start TableNodeExampleApplet.jnlp file that is used by the HTML file above:

<?xml version="1.0" encoding="UTF-8"?>
<jnlp spec="1.0+" href="TableNodeExampleApplet.jnlp">
  <information>
    <title>TableNodeExampleApplet</title>
    <vendor>JMentor</vendor>
    <description>TableNodeExampleApplet</description>
    <description kind="short">TableNodeExampleApplet</description>
    <homepage href="http://jmentor.com"/>
    <offline-allowed />
  </information>
  <security>
    <all-permissions/>
  </security>
  <resources>
    <property name="jnlp.packEnabled" value="true"/>
    <j2se version="1.6+" href="http://java.sun.com/products/autodl/j2se" java-vm-args="-Xmx800m" />
    <!--j2se version="1.6+" java-vm-args="-Xmx800m" /-->
    <jar href="TableNodeExample.jar" main="true" download="eager"/>
    <jar href="Scenario.jar"/>
    <jar href="javafxrt.jar"/>
    <jar href="javafxgui.jar"/>
    <jar href="javafx-swing.jar"/>
  </resources>
  <applet-desc
    name="TableNodeExampleApplet"
    main-class="javafx.application.Applet"
    width="500"
    height="400">
  </applet-desc>
</jnlp>


Dragging the Applet out of the Browser and onto the Desktop

As shown in the following screenshot, one of the cool features of Java SE 6 update 10 is that you can drag a Java or JavaFX applet out of the browser and onto the desktop.  By default, you press the Alt key while dragging the applet:

TableNodeExampleApplet-Drag

Here is our JavaFX Applet living happily on the desktop after the browser has been closed, and the user has selected the Burn page:

TableNodeExampleApplet-Dragged

Google Chrome will be a Driving Force for RIA

According to Google, Java SE 6 Update 10 is the version that must be used in order to run Java in the Chrome browser.  As I've mentioned previously, one of the objectives of Java SE 6 Update 10 is to solve the JRE and Java/JavaFX deployment issues.  Because Google Chrome is destined to be a great, cross-platform browser, and because it requires the version of Java that makes rich-client Java/JavaFX programs feasible, this will increase the adoption rate of JavaFX applets and applications.

Thanks Google!

Jim Weaver
JavaFXpert.com

April 29, 2008

Game Over: Improving upon the Compiled JavaFX Tetris Program

In the Knowing the State of a JavaFX Script Animation post I showed you the more succinct syntax for controlling animations.  Now I'd like to show you some improvements to the TetrisJFX program that I've been developing in this blog, including switching to the new animation syntax.  Here is a screenshot of the TetrisJFX game after the blocks stacked up to the top and the game is over.

Tetrisjfx_4_gameover

By the way, Bruno Ghisi (a fellow Java Champion) and Lucas Torri have developed a Java Bluetooth Framework called Marge.  Using that framework they developed a mobile phone game controller for this TetrisJFX program.  You can see a video of this in action in one of Bruno's blog posts.

As you can see, I've added some functionality since the previous post, but here are some highlights:

  • A preview area in the upper right corner that shows the next tetromino shape that will drop.
  • Scoring is more Tetris-like, in that each tetromino is worth 25 points when it is finished dropping, and each row that fills up is worth 100 points.  Full rows are removed from the playing field and the tetrominoes above are moved down.
  • The button on the left toggles between Play and Stop.  If you choose to Stop the game, "Game Over" will appear in the background, followed by which pressing Play will start a new game.
  • When the tetrominoes stack up to the top, as shown in the sceenshot above,  "Game Over" appears in the background and the game stops.

Here is the code for this version of TetrisJFX.  Note that I've added a new class named TetrisNextShapeNode that shows the next tetromino shape to drop.

TetrisMain.fx:

/*
*  TetrisMain.fx - The main program for a compiled JavaFX Script Tetris game
*
*  Developed 2008 by James L. Weaver (jim.weaver at lat-inc.com)
*  to serve as a compiled JavaFX Script example.
*/
package tetris_ui;

import javafx.ui.*;
import javafx.ui.canvas.*;
import java.lang.System;
import tetris_model.*;

Frame {
  var model = TetrisModel {}
  var canvas:Canvas
  width: 350
  height: 500
  title: "TetrisJFX"
  background: Color.LIGHTGREY
  content:
    StackPanel {
      content: [
        Canvas {
          content: [
            ImageView {
              image:
                Image {
                  url: "{__DIR__}images/background.jpg"
                }
            },
            Text {
              transform: bind [
                Translate.translate(30, 350),
                Rotate.rotate(290, 0, 0),
              ]
              content: "Game Over"
              fill: Color.WHITE
              opacity: bind if (model.timeline.running) 0
                            else 1
              font:
                Font {
                  face: FontFace.SANSSERIF
                  size: 60
                  style: FontStyle.BOLD
                }
            }
          ]
        },
        BorderPanel {
          center:
            FlowPanel {
              alignment: Alignment.LEADING
              vgap: 10
              hgap: 10
              content: [
                Canvas {
                  content:
                    TetrisPlayingField {
                      model: model
                    }
                }
              ]
            }
          right:
            GridPanel {
            var sansSerif24 =
              Font {
                face: FontFace.SANSSERIF
                size: 24
                style: FontStyle.BOLD
              }
              rows: 2
              columns: 1
              cells: [
                BorderPanel {
                  top:
                    FlowPanel {
                      vgap: 10
                      alignment: Alignment.LEADING
                      content:
                        Label {
                          foreground: Color.LIGHTBLUE
                          text: "NEXT:"
                          font: sansSerif24
                        }
                    }
                  center: 
                    Canvas {
                      content:
                        TetrisNextShapeNode {
                          model: model
                        }
                    }
                },
                BorderPanel {
                  top:
                    Label {
                      foreground: Color.LIGHTBLUE
                      text: "SCORE:"
                      font: sansSerif24
                    }
                  center:
                    FlowPanel {
                      alignment: Alignment.TRAILING
                      content:
                        Label {
                          foreground: Color.LIGHTBLUE
                          text: bind "{model.score}"
                          font: sansSerif24
                        }
                    }
                }
              ]
            }
          bottom:
            FlowPanel {
              alignment: Alignment.CENTER
              content: [
                Button {
                  text: bind
                    if (model.timeline.running)
                      "Stop"
                    else
                      "Play"
                  action:
                    function():Void {
                      if (model.timeline.running) {
                        model.stopGame();
                      }
                      else {
                        model.startGame();
                      }
                    }
                },
                Button {
                  text: "Rotate"
                  enabled: bind not model.stopDropping
                  action:
                    function():Void {
                      model.rotate90();
                    }
                },
                Button {
                  text: "Left"
                  enabled: bind not model.stopDropping
                  action:
                    function():Void {
                      model.moveLeft();
                    }
                },
                Button {
                  text: "Right"
                  enabled: bind not model.stopDropping
                  action:
                    function():Void {
                      model.moveRight();
                    }
                }
              ]
            }
        }
      ]
    }
  visible: true
  onClose:
    function():Void {
      System.exit(0);
    }
}

TetrisPlayingField.fx: 

/*
*  TetrisPlayingField.fx -
*  A custom graphical component that is the UI for the
*  playing field.
*
*  Developed 2008 by James L. Weaver (jim.weaver at lat-inc.com)
*  to serve as a compiled JavaFX Script example.

*/
package tetris_ui;

import javafx.ui.*;
import javafx.ui.canvas.*;
import java.lang.System;
import tetris_model.*;

class TetrisPlayingField extends CompositeNode {
  private static attribute squareOutlineColor = Color.BLACK;
  private static attribute squareOutlineWidth = 1;
  public attribute model:TetrisModel;
  private attribute numHiddenTopRows = 2;
  public function composeNode():Node {
    Group {
      content: bind [
        Rect {
          x: 0
          y: 0
          width: model.NUM_COLS * model.SQUARE_SIZE
          height: (model.NUM_ROWS - numHiddenTopRows) * model.SQUARE_SIZE
          strokeWidth: 1
          stroke: Color.BLACK
          fill: Color.BLUE
          opacity: bind if (model.timeline.running) .5
                        else .2
        },
        for (cell in model.fieldCells
               where cell <> TetrisShapeType.NONE) {
          Rect {
            var yPos:Integer = indexof cell / model.NUM_COLS
            //x: (indexof cell % model.NUM_COLS).intValue() * model.SQUARE_SIZE
            x: indexof cell % model.NUM_COLS * model.SQUARE_SIZE
            y: (yPos - numHiddenTopRows) * model.SQUARE_SIZE
            width: model.SQUARE_SIZE
            height: model.SQUARE_SIZE
            fill: cell.squareColor
            stroke: squareOutlineColor
            strokeWidth: squareOutlineWidth
          }
        }
      ]
    }
  }
}

TetrisNextShapeNode.fx: 

/*
*  TetrisNextShapeNode.fx -
*  A custom graphical component that shows the next shape to fall
*
*  Developed 2008 by James L. Weaver (jim.weaver at lat-inc.com)
*  to serve as a compiled JavaFX Script example.

*/
package tetris_ui;

import javafx.ui.*;
import javafx.ui.canvas.*;
import java.lang.System;
import tetris_model.*;

class TetrisNextShapeNode extends CompositeNode {
  private static attribute squareOutlineColor = Color.BLACK;
  private static attribute squareOutlineWidth = 1;
  public attribute model:TetrisModel;
  public function composeNode():Node {
    Group {
      content: bind [
        Rect {
          x: 0
          y: 0
          width: model.PREVIEW_NUM_COLS * model.SQUARE_SIZE
          height: model.PREVIEW_NUM_ROWS * model.SQUARE_SIZE
          strokeWidth: 1
          stroke: Color.BLACK
          fill: Color.BLUE
          opacity: bind if (model.timeline.running) .7
                        else .2
        },
        for (cell in model.nextShapePreviewCells
               where cell <> TetrisShapeType.NONE) {
          Rect {
            var yPos:Integer = indexof cell / model.PREVIEW_NUM_COLS
            x: indexof cell % model.PREVIEW_NUM_COLS * model.SQUARE_SIZE
            y: yPos * model.SQUARE_SIZE
            width: model.SQUARE_SIZE
            height: model.SQUARE_SIZE
            fill: cell.squareColor
            stroke: squareOutlineColor
            strokeWidth: squareOutlineWidth
          }
        }
      ]
    }
  }
}

TetrisModel.fx: 

/*
*  TetrisModel.fx - The model behind the Tetris UI
*
*  Developed 2008 by James L. Weaver (jim.weaver at lat-inc.com)
*  to serve as a compiled JavaFX Script example.

*/
package tetris_model;

import javafx.animation.*;
import javafx.lang.*;
import java.lang.System;

public class TetrisModel {
  /** Size of each tetromino in pixels */
  public static attribute SQUARE_SIZE = 19;
 
  /** Number of rows in the playing field */
  public static attribute NUM_ROWS = 23;
 
  /** Number of columns in the playing field */
  public static attribute NUM_COLS = 10;
 
  /** Number of rows in the next shape viewing area */
  public static attribute PREVIEW_NUM_ROWS = 4;
 
  /** Number of columns in the next shape viewing area  */
  public static attribute PREVIEW_NUM_COLS = 6;
 
  public attribute stopDropping:Boolean;

  /**
   * Sequence of objects that represent the
   * type of shapes (including no shape) in each playing field cell
   */
  public attribute fieldCells:TetrisShapeType[];

  /**
   * Sequence of objects that represent the
   * type of shapes (including no shape) in each next shape preview cell
   */
  public attribute nextShapePreviewCells:TetrisShapeType[];
   
  /**
   * The active tetromino shape type.
   */
  public attribute activeShapeType:TetrisShapeType;
  public attribute nextShapeType:TetrisShapeType;
 
  public attribute score:Integer = 0;
  private attribute pieceScore = 25;
  private attribute rowScore = 100;
 
  private attribute startDropRow = 2;
  private attribute stopDropRow = NUM_ROWS;
  private attribute pieceAppearRow = 3;
  private attribute fieldFullRow = 5;
 
  /**
   * This value is incremented via the KeyFrame animation mechanism,
   * and represents the row in which the pivotal block is currently residing.
   */
  public attribute a:Integer on replace oldVal {
    if (a == pieceAppearRow) {
      activeShapeType = nextShapeType;
      
      // Remove the current "next shape type" from the preview area
      updateNextShapePreviewCells(true);
      
      nextShapeType = TetrisShapeType.randomShapeType();
      // Load the new "next shape type" into the preview area
      updateNextShapePreviewCells(false);
      
      tetrominoHorzPos = NUM_COLS / 2 - 1 as Integer;
      stopDropping = false;
    }
    if (not stopDropping) {
      // Remove the tetromino from the playing field
      updateFieldCells(tetrominoHorzPos, oldVal, tetrominoAngle, true);

      if (canMoveDown(tetrominoHorzPos, a, tetrominoAngle)) {   
        // Was able to move down, so place the tetromino accordingly
        updateFieldCells(tetrominoHorzPos, a, tetrominoAngle, false);
      }
      else {
        score += pieceScore;
        updateFieldCells(tetrominoHorzPos, oldVal, tetrominoAngle, false);
        stopDropping = true;
        if (a <= fieldFullRow) {
          // The blocks have stacked up too high, and game is over
          timeline.stop();
        }
      }
      removeFilledRows();
    }
  };
  public attribute timeline =
    Timeline {
      keyFrames: [
        KeyFrame {
          time: 0s
          values: a => startDropRow tween Interpolator.LINEAR
        },
        KeyFrame {
          time: 10s
          values: a => stopDropRow tween Interpolator.LINEAR
        }
      ]
      repeatCount: Timeline.INDEFINITE
    };
  public attribute tetrominoAngle:Number;
  public attribute tetrominoHorzPos:Number;

  public function startGame():Void {
    fieldCells = for (i in [1..(NUM_ROWS * NUM_COLS)]) {
      TetrisShapeType.NONE
    };
    nextShapePreviewCells = for (i in [1..(PREVIEW_NUM_ROWS * PREVIEW_NUM_COLS)]) {
      TetrisShapeType.NONE
    };
    nextShapeType = TetrisShapeType.randomShapeType();
    activeShapeType = null;
    score = 0;
    timeline.start();
  }

  public function stopGame():Void {
    timeline.stop();
    stopDropping = true;
  }

  public function rotate90():Void {
    if (stopDropping) {return;} // Don't try to process if a piece isn't falling
    updateFieldCells(tetrominoHorzPos, a, tetrominoAngle, true);
    for (oldCell in TetrisShapeType.squarePositionsForRotatedShape(activeShapeType,
                                              computeNewAngle(tetrominoAngle, 90))) {
      // First check to see if the rotated tetromino is past the
      // left side of the playing field
      if (tetrominoHorzPos + oldCell.x < 0) {
        updateFieldCells(tetrominoHorzPos, a, tetrominoAngle, false);
        return;
      }
      // Then check to see if the tetromino is at the right of
      // the playing field
      if (tetrominoHorzPos + oldCell.x > NUM_COLS - 1) {
        updateFieldCells(tetrominoHorzPos, a, tetrominoAngle, false);
        return;
      }
      // Now check to see if another tetromino is preventing it from rotating
      if (fieldCells[((a + oldCell.y) * NUM_COLS + tetrominoHorzPos - 1 + oldCell.x) as Integer] <>
                TetrisShapeType.NONE) {
        updateFieldCells(tetrominoHorzPos, a, tetrominoAngle, false);
        return;
      }
    }
    tetrominoAngle = computeNewAngle(tetrominoAngle, 90);
    updateFieldCells(tetrominoHorzPos, a, tetrominoAngle, false);
  }

  public function computeNewAngle(originalRotationAngle:Integer, degreesToRotate):Integer {
    ((originalRotationAngle + degreesToRotate) % (activeShapeType.rotateStates * 90)) as Integer;
  }

  public function moveLeft():Void {
    if (stopDropping) {return;} // Don't try to process if a piece isn't falling
    updateFieldCells(tetrominoHorzPos, a, tetrominoAngle, true);
    for (oldCell in TetrisShapeType.squarePositionsForRotatedShape(activeShapeType,
                                                                   tetrominoAngle)) {
      // First check to see if the tetromino is at the left of
      // the playing field
      if (tetrominoHorzPos + oldCell.x <= 0) {
        updateFieldCells(tetrominoHorzPos, a, tetrominoAngle, false);
        return;
      }
      // Now check to see if another tetromino is preventing it from moving left
      if (fieldCells[((a + oldCell.y) * NUM_COLS + tetrominoHorzPos - 1 + oldCell.x) as Integer] <>
                TetrisShapeType.NONE) {
        updateFieldCells(tetrominoHorzPos, a, tetrominoAngle, false);
        return;
      }
    }
    tetrominoHorzPos--;
    updateFieldCells(tetrominoHorzPos, a, tetrominoAngle, false);
  }
  // TODO: Refactor with moveLeft method
  public function moveRight():Void {
    if (stopDropping) {return;} // Don't try to process if a piece isn't falling
    updateFieldCells(tetrominoHorzPos, a, tetrominoAngle, true);
    for (oldCell in TetrisShapeType.squarePositionsForRotatedShape(activeShapeType,
                                                                   tetrominoAngle)) {
      // First check to see if the tetromino is at the left of
      // the playing field
      if (tetrominoHorzPos + oldCell.x >= NUM_COLS - 1) {
        updateFieldCells(tetrominoHorzPos, a, tetrominoAngle, false);
        return;
      }
      // Now check to see if another tetromino is preventing it from moving left
      if (fieldCells[((a + oldCell.y) * NUM_COLS + tetrominoHorzPos + 1 + oldCell.x) as Integer] <>
                TetrisShapeType.NONE) {
        updateFieldCells(tetrominoHorzPos, a, tetrominoAngle, false);
        return;
      }
    }
    tetrominoHorzPos++;
    updateFieldCells(tetrominoHorzPos, a, tetrominoAngle, false);
  }

  /**
   * Keeps the fieldCells sequence (that represents the
   * cells in the playing field) updated to reflect reality.
   * pass in the row and column that the pivotal block was
   * located, as well as the former angle of the tetromino.
   * The new row, column, angle, as well as the shape type, is
   * held in the model, so don't have to be passed in. 
   */
  public function updateFieldCells(oldX:Integer, oldY:Integer, oldAngle:Integer, remove:Boolean):Void {
    // Place (or remove) the shape on the playing field
    for (oldCell in TetrisShapeType.squarePositionsForRotatedShape(activeShapeType,
                                                                   oldAngle)) {
      fieldCells[(oldY + oldCell.y) * NUM_COLS + oldX + oldCell.x] =
        if (remove) TetrisShapeType.NONE else activeShapeType;
    }
  }

  /**
   * Keeps the nextShapePreviewCells sequence (that represents the
   * cells in the next shape preview area) updated to reflect reality.
   */
  public function updateNextShapePreviewCells(remove:Boolean):Void {
    // Place (or remove) the shape on the next shape preview field
    var pivotPosX = 2;
    var pivotPosY = 1;
   
    for (oldCell in TetrisShapeType.squarePositionsForRotatedShape(nextShapeType, 0)) {
      nextShapePreviewCells[(pivotPosY + oldCell.y) * PREVIEW_NUM_COLS + pivotPosX + oldCell.x] =
        if (remove) TetrisShapeType.NONE else nextShapeType;
    }
  }

  public function canMoveDown(oldX:Integer, oldY:Integer, oldAngle:Integer):Boolean {
    var retVal = true;
    for (oldCell in TetrisShapeType.squarePositionsForRotatedShape(activeShapeType,
                                                                   oldAngle)) {
      // First check to see if the tetromino is at the bottom of
      // the playing field
      if (oldY + oldCell.y >= NUM_ROWS) {
        return false;
      }
      // Now check to see if another tetromino is preventing it from moving down
      if (fieldCells[(oldY + oldCell.y) * NUM_COLS + oldX + oldCell.x] <>
                TetrisShapeType.NONE) {
        return false;
      }
    }
    return true;
  }
 
  private function removeFilledRows():Void {
    if (not stopDropping) {
      return;
    }
    for (row in [0..NUM_ROWS - 1]) {
      // First see if the beginning of the row is empty
      // before doing the more expensive check of the whole row
      //TODO: Consider not doing this, as perhaps the indexOf isn't expensive
      if (fieldCells[row * NUM_COLS] <> TetrisShapeType.NONE) {
        var fieldRowSlice = fieldCells[row * NUM_COLS .. (row + 1) * NUM_COLS -1];
        if (Sequences.indexOf(fieldRowSlice, TetrisShapeType.NONE) < 0) {
          score += rowScore;
          stopDropping = true;
          //TODO: Use fieldRowSlice in delete statement
          delete fieldCells[row * NUM_COLS .. (row + 1) * NUM_COLS -1];
          insert
            for (i in [1..NUM_COLS]) {
              TetrisShapeType.NONE
            }
            before fieldCells[0];
        }
      }
    }
  }
}

TetrisShapeType.fx:

/*
*  TetrisShapeType.fx - A Tetris shape type, which are
*                       I, J, L, O, S, T, and Z
*
*  Developed 2008 by James L. Weaver (jim.weaver at lat-inc.com)
*  to serve as a compiled JavaFX Script example.

*/
package tetris_model;

import javafx.ui.*;
import java.awt.Point;
import java.lang.Math;
import java.lang.System;

/**
* This class contains the model information for each type
* of tetromino (Tetris piece).
*/
public class TetrisShapeType {
  public attribute id: Integer;
  public attribute name: String;
 
  /**
   * A sequence containing positions of each square in a
   * tetromino type.  The first element in the sequence is
   * the one around which the tetromino will rotate.
   * Note that the "O" tetromino type doesn't rotate.
   */
  public attribute squarePositions:Point[];
 
  //public attribute allowRotate:Boolean = true;
 
  /**
   * Valid values are 1 (for "O" shape, 2 (for "S", "Z", and "I" shapes)
   * and 4 (for "L", "J", and "T" shapes)
   */
  public attribute rotateStates:Integer =  4;
 
  public attribute squareColor:Color;

  /** The "NONE" shape (represents the absence of a shape) */
  public static attribute NONE =
    TetrisShapeType {
      id: 0
      name: "."
      squarePositions: [
        new Point(0, 0),
        new Point(0, 0),
        new Point(0, 0),
        new Point(0, 0)
      ]
      squareColor: Color.SILVER
    };

  /** The "I" shape (four squares in a straight line) */
  public static attribute I =
    TetrisShapeType {
      id: 1
      name: "I"
      squarePositions: [
        new Point(0, 0),
        new Point(-1, 0),
        new Point(1, 0),
        new Point(2, 0)
      ]
      squareColor: Color.SILVER
      rotateStates: 2
    };

  /** The "T" shape (looks like a stout "T") */
  public static attribute T =
    TetrisShapeType {
      id: 2
      name: "T"
      squarePositions: [
        new Point(0, 0),
        new Point(-1, 0),
        new Point(1, 0),
        new Point(0, 1)
      ]
      squareColor: Color.HOTPINK
    };

  /** The "L" shape (looks like an "L") */
  public static attribute L =
    TetrisShapeType {
      id: 3
      name: "L"
      squarePositions: [
        new Point(0, 0),
        new Point(-1, 0),
        new Point(1, 0),
        new Point(-1, 1)
      ]
      squareColor: Color.LIGHTBLUE
    };

  /** The "J" shape (looks sort of like a "J", but
   *  more like a backwards "L") */
  public static attribute J =
    TetrisShapeType {
      id: 4
      name: "J"
      squarePositions: [
        new Point(0, 0),
        new Point(-1, 0),
        new Point(1, 0),
        new Point(1, 1)
      ]
     &n