Video Puzzle

By Josh Marinacci, October 27th, 2008

JavaFX는 매우 강력한 미디어 기능을 가지고 있다. 단지 비디오를 보여주는 것 뿐만 아니라, 변형하고, 다른 컨텐츠와 혼합하고, 조각조각 낼 수조차 있다. 이 예제는 비디오 클립 재생기능을 사용한 퍼즐을 만드는 방법을 보여준다.

Java Webstart로 예제 실행

video from Elephant's Dream

코드 이해하기

이 예제가 많은 양의 코드를 포함하고 있지만, 대부분이 사용자 인터페이스를 위한 부분이다. 실제로 비디오를 잘라내는 부분은 아주 간단하다. 핵심 구조에는 MediaPlayer class에는 단일 instance를 사용했지만, MediaView class에는 다중 instance를 사용했다. 각각의 보기는 화면의 다른 부분에서 전체 비디오 클립을 보여 줄 수 있다. 각각의 MediaView에서 viewport 를 설정함으로, 비디오의 부분만 보여 줄 수 있고, 그것이 퍼즐의 하나의 조각이 된다. 여러개의 조각을 하나로 합치면, 퍼즐을 완성한 것이다.

그림 1처럼, 첫번째 class는 Puzzle class다. 변수를 사용하여, 퍼즐의 전체 크기(widthheight), 각각의 퍼즐의 크기(pieceWidthpieceHeight), 각각의 열과 행의 갯수(pieceRowspieceCols)를 정의한다. 이제, pieces 변수에는 퍼즐 조각의 목록들이 저장됐다.

또한 Puzzle class는 두가지 기능이 있는데, 하나는 조각을 생성하고(generatePieces()) 하나는 화면에 불규칙하게 조각들을 흩어버린다(scatter()). generatePieces() 함수가 퍼즐 조각을 생성하려 열과 행을 반복문으로 도는 것에 주목하라. 각각의 조각들은 다른 위치가 주어져(pxpy), 공유된 MediaPlayer instance에 포함되어 있다: video(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;
        }
    }
}

그림 1: Puzzle.fx Class

그림 2.처럼, 퍼즐 조각들은 Piece class에의해 정의되었다. 각각의 Piece 는 그림자 글씨 효과를 위한 Rectangle, 비디오의 작은 조각에서 사용할 MediaView, 선택 테두리를 위한 다른 rectangle, 그리고 비디오가 선택됐을때 사용할 조준점 이미지로 구성되어 있다. 비디오의 일부만 보여지려면, MediaView에 viewport가 주어지면 된다. viewport는 단지 보여질 전체 비디오의 부분을 정의하는 javafx.geometry.Rectangle instance이다. 비디오를 잘라낼 마법을 부리는 것이 이 MediaView.viewport 변수다.

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
                }
            ]
        }
    }

그림 2: Puzzle.fx, 퍼즐 조각 생성

마지막으로, 퍼즐 조각들을 드래깅 하기 위해, Puzzle class에 드래깅을 할 수 있게 몇가지 event handler들을 정의한다. Puzzle class는 또한 퍼즐 조각이 정확한 위치 근처에 위치하는지 판단하는 isNearDropSpot 함수와 장소에 조각을 끼워넣는 snapToDropSpot 함수를 정의한다. 이 함수들은, 그림 3처럼 각각 조각들의 최종 위치를 결정하는 Puzzle class 안에 설정된 pxpy 의 원래 값을 사용한다.

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;
    }
}

그림 3: Piece.fx 에서 Event Handler들