Flickr로 부터 사진들 움직이게 하기

By Anton Tarasov, November 17, 2008

샘플은 여러분이 입력한 태그에 기반하여 Filcker 웹 서비스에서 그림들을 가져옵니다. 그것은 서로 flock 이라는 공통 태그를 가지는 이러한 방법으로 사진들을 애니메이트(animates)합니다. 여러분은 마우스로 사진을 선택할수 있습니다. 사진은 곧 두배 크기가 될것이며 여러분은 그것과 연관된 태스를 볼수 있을 것입니다. 태그 리스트는 스크롤 될수 있습니다. 목록의 태크를 클릭함으로서 검색 필드안에 태그를 놓고 예제는 새로운 그림들을 가져옵니다. 여러분이 사진을 선택하고 클릭했을때 사진은 전체 화면으로 확장되고 큰 이미지가 로드됩니다. 아무키나 누르거나 이미지를 클릭하면 이미지는 작은 크기의 이미지로 다시 되돌아 갑니다.

이 샘플은 모마일 플랫폼이나 pre-6u10 JDK에서 작동하지 않을 것입니다.

코드 이해하기

샘플의 ScenePhotoFlockr 자식 노드를 포함하고 있습니다. Scene는 이 샘플에서 모든 다른 예제들이 사용한 루트 노드 입니다. PhotoFlockr는 아래 컨텐츠와 함께 Group 클래스의 인스턴스 입니다.

  • Sprite 노드들의 순서. 각각의 sprite들은 가져온 그림들을 표시합니다.
  • 사용자는 검색 태그들을 텍스트 필드 안에 타이핑합니다.
  • 화살표를 표현하는 커스텀 CrossHair 노드.
  • 투명한 Rectangle는 마우스 움직임을 교차하는데 사용됩니다.
  • 노드는 닫기 제어를 관리하는데 사용됩니다.

CrossHair 노드는 glass pane의 콜백 함수안에서 마우스 좌표에 의해서 결정되는 cxcy 속성을 가지고 있다는것에 주목하세요. Figure 1을 보세요

Source Code
    override function create(): Node {
        ...
        return Group {
            content:
                //
                // A sequence of nodes displaying fetched pictures.
                //
                [spriteGroup = Group {
                    content: sprites
                    visible: bind spriteGroupVisible
                },
                //
                // A tag search field.
                //
                Group {
                    translateX: 10
                    translateY: 160
                    content: tagSearchField
                },
                //
                // A node rendering crosshair cursor.
                //
                CrossHair {
                    w: width
                    h: height
                    cx: bind mouseX
                    cy: bind mouseY
                },
                //
                // A "glass pane".
                //
                Rectangle {
                    cursor: Cursor.NONE
                    height: height
                    width: width
                    fill: Color.rgb(0, 0, 0, 0)
                    onMouseMoved: function(e) {
                        mouseX = e.x;
                        mouseY = e.y;
                    }
                    onMouseDragged: function(e) {
                        mouseX = e.x;
                        mouseY = e.y;
                    }
                    smooth: false
                },
                //
                // A close control.
                //
                Group {
                    translateX: width - 20
                    content:
                        [ImageView {
                            translateX: 5
                            translateY: 5
                            image: Image {
                                url: "{__DIR__}images/close.png"
                            }
                            cache: true
                        },
                        Rectangle {
                            width: 20
                            height: 20
                            fill: Color.rgb(0, 0, 0, 0)
                            cursor: Cursor.HAND
                            cache: true
                            onMouseClicked: function(e) {
                                FX.exit();
                            }
                        }]
                }]
    }

Figure 1: Main.fx Script: PhotoFlockr.create Function

마우스가 Sprite 노드를 hover 그림은 선택되어 지고 Figure 2에서 보여지는 것처럼 reorderSprites 함수는 호촐됩니다. 함수는 선택된 sprite를 순서의 마지막으로 이동시키는 방법으로 sprite들의 순서를 다시 정렬합니다.spriteGroup의 내용은 그때 교체되고 선택된 sprite는 계층(hierarchy)의 맨 위(화면에 좀더 가까운)에 나타납니다.

Source Code
    package function reorderSprites() {
        var _sprites = [ sprites[s | not s.boid.selected], sprites[s | s.boid.selected] ];
        spriteGroup.content = _sprites;
    }

Figure 2: Main.fx Script: PhotoFlockr.reorderSprites Function

Figure 3는 Sprite 커스텀 노드와 그것의 자식 노드들 계층을 보여줍니다. transform의 두가지 단계여 여기에 사용됩니다. 최상급의 Transform.translate(spriteX, spriteY) transform은 전체 계층과 예제 화면 너머로 sprite를 이동시키곤 하는데 적용됩니다. transform의 순서는 한단계 아래에 위치해 있습니다. 그 순서는 작은 그림과 전경이 검정색인 사각형을 포함하는 ImageView에만 적용됩니다. 이것들의 transform은 그림이 선택될때나 전체화면으로 확장될때나 그림이 "flocks" 할때 그림을 회전하기 위해서 그림을 비교하는데 사용됩니다. 모든 transform들은 그것들의 인자가 변할수 있는 값들에 의해서 결정된다는 것에 주목하세요.

계층의 최상에 있는 두 Group들(the buttom of the code fragment)은 largeImage와 각각 사진의 태그들의 목록을 포함하고 있습니다. 큰 그림 Groupopacity 속성은 전체화면으로 확장하는 그림의fadeTimeline에 의해서 변경되는 largeImageFade에 의해서 결정됩니다. 또한 image 변수와 함께 초기화 되는 ImageViewlargeImage 가 나타나는 것처럼 전체 화면으로 확장되는 방식으로 결정되는 opacity 속성을 가진다는 것에 주목하세요.

Source Code
    override public function create(): Node {
        Group {
            transforms: bind Transform.translate(spriteX, spriteY)
            content:
                [Group {
                    transforms: bind [ Transform.scale(alpha, alpha),
                                       Transform.rotate(if (not selected) heading
                                                        else spriteHeading, radius, radius) ]
                    content: [
                        //
                        // The picture's background black rectangle.
                        //
                        Rectangle {
                            height: radius*2
                            width: radius*2
                            fill: Color.BLACK
                            smooth: false
                        },
                        //
                        // The small image of the picture.
                        //
                        ImageView {
                            opacity: bind 1.0 - largeImageFade
                            image: bind image
                            smooth: true
                            onMouseClicked: function(e) {
                                this.doSelect();
                            }
                        },
                        //
                        // The picture's gray frame.
                        //
                        Rectangle {
                            transforms: bind Transform.scale(1.0/alpha, 1.0/alpha);
                            stroke: Color.GRAY
                            height: bind radius*2*alpha
                            width: bind radius*2*alpha
                            fill: Color.color(0, 0, 0, 0);
                            strokeWidth: 1
                       }
                    ]
                },
                //
                // The large image of the picture.
                //
                Group {
                    opacity: bind largeImageFade
                    visible: bind largeImage != null
                    content:
                        [Rectangle {
                            height: screenHeight
                            width: screenWidth
                            fill: Color.BLACK
                            visible: bind largeImageFade == 1.0
                        },
                        ImageView {
                            image: bind largeImage
                            smooth: true
                        }]
                },
                //
                // The tag list.
                //
                Group {
                    visible: bind alpha == 2.0
                    content: bind tagList
                }]
        }
    }

Figure 3: Sprite.create Method

Figure 4는 큰 이밎-페이딩(image-fading) 효과를 보여줍니다. largeImageProgress> 변수는 largeImage.progress에 귀결되고 trigger 집함은 largeImage의 로딩이 완료되었을때 fadeTimeline의 실행이 시작되도록 합니다.

Source Code
    // Start the picture-fading effect when a large image loading is completed.
    var largeImageProgress = bind largeImage.progress on replace {
        if (largeImage != null and largeImageProgress == 100) {
            fadeTimeline.play();
        }
    }

    var fadeTimeline = Timeline {
        keyFrames:
            [ at (0s) { largeImageFade => 0.0 },
              at (1s) { largeImageFade => 1.0 } ]
    }

Figure 4: Sprite.fx Script: the Large Image-Fading Effect

sprite 노드의 선택을 위한 Listening은 매우 쉽습니다. Node.hover 속성을 오버라이드하고 거기에 트리거(trigger)를 설정하는 것만으로도 충분합니다(Figure 5). Node.blocksMouse 속성 또한 오버라이드 되었고 true로 설정된 점을 명심하세요. 이러한 설정은 scene 계층(hierarchy)에서 이 노드 밑의 다른 sprite 노드들에게 마우스 이벤트를 보내는것을 방지합니다.

Source Code
    override var blocksMouse = true;
    override var hover on replace {
        if (hover and not fullScreen) {
            onEnter();
        } else {
            onLeave();
        }
    }

Figure 5: Sprite.fx Script With Node's Attributes Overriden

Boid클래스는 예제 화면을 가로지르면서 움직이는 sprite들의 logic을 관리합니다. 몇몇의 force들은 속도와 움직임의 방향에 영향을 미칩니다.(Figure 6을 보세요):

  • 분리력 – 근처에 있는 두 sprite들은 멀리 나아가는 경향이 있습니다.
  • 결합력 – 근처에 있는 두 sprite들은 공통 태그들의 몇몇 특정한 계산으로 서로 나란히 움직이는 경향이 있습니다.
  • 정렬 – 근처에 있는 두 sprite들의 속도는 평준화 됩니다.

Vector2D클래스는 이러한 계산들을 실행하기 위해서 사용되곤 합니다.

Source Code
    // Calculate new cordinates and heading of the sprite
    public function run(boids: Boid[], cohere: function(b1: Boid, b2: Boid): Number): Void {
        if (not selected) {
            flock(boids, cohere);
            update();
            borders();
        }
    }

    // Accumulate a new acceleration each time based on three rules
    function flock(boids: Boid[], cohere: function(b1: Boid, b2: Boid): Number): Void {
        var sep: Vector2D = separate(boids);           // Separation
        var ali: Vector2D = align(boids);              // Alignment
        var coh: Vector2D = cohesion(boids, cohere);   // Cohesion
        // Arbitrarily weight these forces
        sep.mult(8.0);
        ali.mult(1.0);
        coh.mult(1.0);
        // Add the force vectors to acceleration
        acc.add(sep);
        acc.add(ali);
        acc.add(coh);
    }

    // Update location
    function update(): Void {
        vel.add(acc);           // Update velocity
        vel.limit(maxspeed);    // Limit speed
        vel.updateHeading2D();  // Update direction
        loc.add(vel);           // Finally update location
        acc.setXY(0, 0);        // Reset accelertion
    }

    // Wraparound
    function borders(): Void {
        if (loc.x < -radius*2) loc.x = width + radius*2;
        if (loc.y < -radius*2) loc.y = height + radius*2;
        if (loc.x > width + radius*2) loc.x = -radius*2;
        if (loc.y > height + radius*2) loc.y = -radius*2;
    }

Figure 6: Boid.fx Script: Vector Calculations

Model.fx는 Flicker로부터 XML 데이터를 fetch하는데 사용되곤 합니다, 특히 http request들을 비동기하는 javafx.io.http.HttpRequest와 XML 데이터를 parse하는 javafx.data.pull.PullParser. PhotoModel 클래스는 크고 작의 특정 사진들의 URL들과 그것과 연관있는 태그들의 문자열 목록을 캡슐화 합니다.

그리고 마지막으로 TagList 클래스는 CustomNode를 상속받고 사진이 선택되었을때 사진 태그들의 목록을 보여줍니다(Figure 7). 투명한 사각형 배경은 마우스 휠 이벤트와 목록의 스크롤을 교차 합니다. 또 다른 사각형은 특정 태크에 마우스를 클릭하는 것을 listen하기 위해서 listGroup에 포함되어 있습니다. 클릭은 선택된 태그에 상응하는 사진들을 위해 새로운 검색을 시작하게 합니다.

Source Code
    override public function create(): Node {
        Group {
            var margin = 5;
            var spacing = 6;
            clip: Rectangle {
                height: height
                width: width
            }
            content: [
                // The rectangle manages scrolling.
                Rectangle {
                    var h = bind sizeof tags * (font.size + spacing);
                    var dif = bind h - height
                    height: bind h
                    width: bind width;
                    fill: Color.color(0, 0, 0, 0);
                    smooth: false
                    onMouseWheelMoved: function(e) {
                        if ((scrollY >= 0 and e.wheelRotation < 0) or
                            (scrollY + dif <= 0 and e.wheelRotation > 0))
                        {
                            return;
                        }
                        if (scrollY - e.wheelRotation*5 > 0) {
                            scrollY = 0;
                        } else if (scrollY + dif - e.wheelRotation*5 < 0) {
                            scrollY = -dif;
                        } else {
                            scrollY -= e.wheelRotation*5;
                        }
                    }
                },
                // A list of tags.
                listGroup = Group {
                    translateY: bind scrollY
                    content: bind for (tag in tags)
                        Group {
                            translateY: indexof tag * (font.size + spacing)
                            var r: Rectangle;
                            content: [
                                // The rectangle manages tag selection.
                                r = Rectangle {
                                    height: 20
                                    width: 150
                                    smooth: false
                                    fill: Color.color(0, 0, 0, 0)
                                    onMouseClicked: function(e) {
                                        clickAction(tag);
                                    }
                                },
                                // A single tag.
                                Text {
                                    fill: bind if (r.hover) selectionColor else color
                                    translateX: 5
                                    translateY: 12
                                    content: tag
                                    font: font
                                }]
                        }
                }
            ]
        }
    }

Figure 7: TagList.create Method