Slicing Video Into Puzzle Pieces

By Josh Marinacci, October 27th, 2008

JavaFX has very powerful media capabilities. Not only can you display video, but you can also transform it, mix it with other content, and even chop it up into pieces. This example shows how to create a puzzle using a streaming video clip.

Video from Elephant's Dream

Understanding the Code

Though this example includes a sizeable amount of code, most of it is for the user interface. The actual code that slices up the video is fairly compact. The core principle is to use a single instance of the MediaPlayer class but multiple instances of the MediaView class. Each view can show the entire video clip in a different part of the screen. By setting a viewport on each MediaView, you can make it show just a portion of the video, resulting in a single piece of the puzzle. By combining multiple pieces together, you have a complete puzzle.

The first class is the Puzzle class shown in Figure 1. Using variables, it defines the overall size of the puzzle (width and height), the size of each piece (pieceWidth and pieceHeight), the number of pieces in each row and column (pieceRows and pieceCols). Finally, it has a list of puzzle pieces stored in the pieces variable.

The Puzzle class also has two functions, one for creating the pieces (generatePieces()) and one for randomly scattering the pieces on the screen (scatter()). Notice that the generatePieces() function loops over the rows and columns, creating puzzle pieces. Each piece is given a different location (px and py), as well as attaching it to the shared MediaPlayer instance: video.

Source Code
public class Puzzle {
    public var pieces:Piece[];
    public var video:MediaPlayer;
    public var width  = 300;
    public var height = 300;
    public var pieceWidth  = 300;
    public var pieceHeight = 300;
    public var pieceRows = 3;
    public var pieceCols = 3;
    public var scatterBounds:Rectangle2D = Rectangle2D { width: 300 height: 300 }
    public var selectedPiece:Piece = null;
    public-init var dragTargetImage:Image;

    init {
        generatePieces();
        scatter();
    }

    public function generatePieces() {
        pieces = for (x in [0..pieceCols-1]) {
                    for(y in [0..pieceRows-1]) {
                        Piece {
                            px:x*pieceWidth py: y*pieceHeight
                            translateX: x*(pieceWidth+10) translateY:y*(pieceHeight+10)
                            video: video
                            puzzle: this
                            pw: pieceWidth ph: pieceHeight
                        }
                    }
                };
    }

    public function scatter() {
        for(piece in pieces) {
            piece.translateX = Math.random() * (scatterBounds.width-pieceWidth) + scatterBounds.minX;
            piece.translateY = Math.random() * (scatterBounds.height-pieceHeight) + scatterBounds.minY;
            piece.placed = false;
        }
    }
}

Figure 1: Puzzle.fx Class

The puzzle pieces themselves are defined by the Piece class, as shown in Figure 2. Each Piece is composed of a Rectangle for the drop shadow, the MediaView for the tiny slice of video, another rectangle for the selection border, and a crosshair image for when the video is selected. To show just a portion of the video, the MediaView is given a viewport. A viewport is just a javafx.geometry.Rectangle instance defining the part of the full video to show. It is this MediaView.viewport variable that does the magic of chopping up video.

Source Code
public class Piece extends CustomNode {
    public var px = 0.0;
    public var py = 0.0;
    public var pw = 100;
    public var ph = 100;
    public var video: MediaPlayer;
    public var puzzle:Puzzle;
    public override var blocksMouse = true;
    var startX = 0.0;
    var startY = 0.0;
    var active = false;
    var near = false;
    public var placed = false;

    override public function create():Node {
        var bds = Rectangle2D {
            minX: px minY: py
            width: pw height: ph
        };
        return Group {
            content: [
                
                Rectangle {
                    width: pw-1
                    height: ph-1
                    fill: Color.RED
                    cache: true
                    opacity: bind if(active) { 1.0 } else { 0.0 }
                    effect: DropShadow { radius:20 offsetX:10 offsetY: 10 }
                }
                
                MediaView {
                    mediaPlayer: bind video
                    viewport: bds
                },
                Rectangle {
                    width: pw-1
                    height: ph-1
                    stroke: bind Color.rgb(255,255,0, if(near) { 0.7 } else { 0.0 })
                    strokeWidth: 5
                    fill: bind Color.rgb(255,255,100, if(active) {0.3} else {0.0})
                }
                ImageView {
                    image: puzzle.dragTargetImage
                    visible: bind (this == puzzle.selectedPiece)
                    translateX: (pw - puzzle.dragTargetImage.width)/2
                    translateY: (ph - puzzle.dragTargetImage.height)/2
                }
            ]
        }
    }

Figure 2: Puzzle.fx, Creating the Puzzle Piece

Finally, to make the puzzle pieces draggable, the Puzzle class defines several event handlers to implement the dragging. The Puzzle class also defines the isNearDropSpot function to determine if a puzzle piece is near its proper spot and the snapToDropSpot function to snap the piece into place. These functions use the original px and py variables set in the Puzzle class to determine each piece's final resting place, as shown in Figure 3.

Source Code
    override var onMousePressed = function(e:MouseEvent):Void {
        if(placed) return;
        startX = e.sceneX-translateX;
        startY = e.sceneY-translateY;
        active = true;
        delete this from puzzle.pieces;
        insert this into puzzle.pieces;
        puzzle.selectedPiece = this;
    }

    override var onMouseDragged = function(e:MouseEvent):Void {
        if(placed) return;
        translateX = e.sceneX-startX;
        translateY = e.sceneY-startY;
        if(isNearDropSpot()) {
            near = true;
        } else {
            near = false;
        }
    }

    override var onMouseReleased = function(e:MouseEvent):Void {
        if(placed) return;
        active = false;
        if(isNearDropSpot()) {
            snapToDropSpot();
            near = false;
            delete this from puzzle.pieces;
            insert this before puzzle.pieces[0];
            placed = true;
        }
    }

    function isNearDropSpot():Boolean {
        var xdiff = Math.abs(translateX - px);
        var ydiff = Math.abs(translateY - py);
        if(xdiff  < 10 and ydiff < 10) {
            return true;
        }
        return false;
    }

    function snapToDropSpot() {
        translateX = px;
        translateY = py;
    }
}

Figure 3: Event Handlers in Piece.fx