JavaFX で Merry Christmas!

このエントリーは JavaFX Advent Calendar の 25 日目のエントリーです。

昨日は @masafumi_ohta さんの Java FX on PI so far ‘beta’ to use でした。

明日が最終日で @aoetk さんです。

@aoetk さんには、初日だけでなくおおとりまで務めていただいて、ほんとありがとうございます。

さて、今日はクリスマスなので、クリスマスらしいものを考えてみました。動くクリスマスカードです。

でも、作るだけではなんなので、JavaFX 2.2 の新機能である Canvas を使ってみました。

とりあえず、完成形はこちら。雪が降っていて、ネオンっぽいイルミネーションはアニメーションでついたり消えたりするようになっています。

背景は写真で、白くて丸いのが雪です。この雪を Canvas で描いています。イルミネーションは Canvas ではなく、Shape で描いています。

さて、雪をどうやって描いているかということですが、たいしたことはしていません。単に Canvas に GraphicsContext で丸を描いているのですが、乱数で半径、透明度、ぼかし量を決めています。

    private double x;
    private double y;
    private double radius;
    private Color color;
    private BoxBlur blur;
    
    private Random random = new Random();
    
    public SnowParticle(double initX) {
        x = initX;
        y = 0.0;
        radius = random.nextDouble() * MAX_RADIUS;
        color = Color.rgb(255, 255, 255, random.nextDouble());
        double blurSize = random.nextDouble() * MAX_BLUR + MAX_BLUR;  
        blur = new BoxBlur(blurSize, blurSize, 2);
    }
    
    public void draw(GraphicsContext context) {
        context.setFill(color);
        context.setEffect(blur);
        
        context.fillOval(x, y, radius, radius);
    }

コンストラクタの引数の initX は一番始めの位置です。雪は y = 0 から降るので、x 座標だけになっています。

半径、透明度、ぼかし量をランダムにしていることで、雪の見え方がまちまちになり、それによって遠近感を醸し出すようにしています。

雪が降るようなアニメーションには Timeline や Transition ではなくて、AnimationTimer を使用しています。

AnimationTimer は適当な間隔で handle メソッドコールするので、そこで座標を再計算し、描画しています。

座標の再計算は SnowParticle クラスの update メソッドで行なっています。

    public double update() {
        x += random.nextDouble() * MAX_STAGGER - MAX_STAGGER / 2.0;
        y += random.nextDouble() * MAX_STAGGER;
        
        return y;
    }

雪をちらちら降らせるために、次の座標を決めるのにも乱数を使用しています。落ちる量も乱数、左右へのぶれも乱数で決めています。

Snowparticle クラスの update メソッドと draw メソッドをコールしているのが、ChristmasCard クラスの update メソッドです。

雪は 1 つぶではないので、List で保持させています。y 座標が画面からはみ出てしまったら、雪を削除しています。また、update がコールされたら、再び SnowParticle オブジェクトを生成させています (これも乱数で決めていますが)。

    private void update() {
        GraphicsContext context = snowCanvas.getGraphicsContext2D();
        
        context.setFill(Color.rgb(0, 0, 0, 0.0));
        context.fillRect(0, 0, snowCanvas.getWidth(), snowCanvas.getHeight());

        Iterator<SnowParticle> it = particles.iterator();
        while (it.hasNext()) {
            SnowParticle particle = it.next();
            
            double y = particle.update();
            if (y >= snowCanvas.getHeight()) {
                it.remove();
            } else {       
                particle.draw(context);
            }
        }
        
        if (random.nextInt(3) == 0) {
            particles.add(new SnowParticle(random.nextDouble() * snowCanvas.getWidth()));
        }
    }

Canvas に描く場合、そのまま描画すると前回の描画に上書きされてしまいます。そこで、はじめに透明色で塗りつぶしてから、雪を描くようにしています。

次のイルミネーションなんですが、Text クラスだけでやろうかと思ったのですが、ストロークとフィルを分けて扱いたかったので、SVG を使ってしまいました。

SVG をロードする部分は SVGLoader を使っています。SVGLoader については SVGLoader のエントリ をご覧ください。

SVG からロードした Shape はアニメーションでストロークとフィルを別々に色を変えるということを行なっています。文字が 14 文字あるので、ループで 14 文字分の KeyValue オブジェクトを作っておいて、後から KeyFrame に追加しようと思ったのですが、できませんでした。

というのも KeyFrame.getValues メソッドで返ってくる Set オブジェクトがイミュタブルで変更不可だからです。

そのため、ちょっと見づらいのですが、ループの中で KeyFrame オブジェクトを作って、Timeline オブジェクトに追加するという方法にしました。

    private void initIllumination() {
        Timeline timeline = new Timeline();
        
        SVGContent svgContent = SVGLoader.load(getClass().getResource("illumination.svg").toString());
        
        for (int i = 1; i < 15; i++ ) {
            Shape ch = (Shape)svgContent.getNode(String.format("merry%02d", i));
            ch.setEffect(new DropShadow(BlurType.GAUSSIAN, Color.YELLOW, 20.0, 0.4, 0.0, 0.0));
            ch.setStroke(TRANSPARENT);
            ch.setFill(TRANSPARENT);
            ch.setTranslateX(50);
            ch.setTranslateY(40);

            illuminationPane.getChildren().add(ch);

            KeyFrame frame0 = new KeyFrame(Duration.ZERO,
                                           new KeyValue(ch.strokeProperty(), TRANSPARENT),
                                           new KeyValue(ch.fillProperty(), TRANSPARENT));
            KeyFrame frame1 = new KeyFrame(Duration.seconds(2),
                                           new KeyValue(ch.strokeProperty(), TRANSPARENT),
                                           new KeyValue(ch.fillProperty(), TRANSPARENT));
            KeyFrame frame2 = new KeyFrame(Duration.seconds(5),
                                           new KeyValue(ch.strokeProperty(), Color.YELLOW),
                                           new KeyValue(ch.fillProperty(), TRANSPARENT));
            KeyFrame frame3 = new KeyFrame(Duration.seconds(6),
                                           new KeyValue(ch.strokeProperty(), Color.YELLOW),
                                           new KeyValue(ch.fillProperty(), Color.WHITE));
            KeyFrame frame4 = new KeyFrame(Duration.seconds(10),
                                           new KeyValue(ch.strokeProperty(), Color.YELLOW),
                                           new KeyValue(ch.fillProperty(), Color.WHITE));
            KeyFrame frame5 = new KeyFrame(Duration.seconds(12),
                                           new KeyValue(ch.strokeProperty(), Color.YELLOW),
                                           new KeyValue(ch.fillProperty(), TRANSPARENT));
            KeyFrame frame6 = new KeyFrame(Duration.seconds(15),
                                           new KeyValue(ch.strokeProperty(), TRANSPARENT),
                                           new KeyValue(ch.fillProperty(), TRANSPARENT));

            timeline.getKeyFrames().addAll(frame0, frame1, frame2, frame3, frame4, frame5, frame6);
        }
        
        timeline.setCycleCount(Timeline.INDEFINITE);
        timeline.play();
    }

文字が輝いているように見えるのはドロップシャドウを施しているからです。DropShadow ではなく、Bloom でも輝いているようなエフェクトをつけられるはずなのですが、いまいち。ですので、いろいろと調整のきく DropShadow にしてあります。

ソースは近いうちに GitHub に上げようと思います。 GitHub に上げました。JavaFXChristmasCardです。

というわけで、Merry Christmas!!