Duke で Swing

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

昨日は @Yucchi_jp さんの力作、JavaFX の標準機能だけでシンプルな 3D トイピアノをつくろう でした。

明日は @peko_kun さんです。

そして、このエントリーは Java Advent Calendar 2014 の 12 日目でもあります。

昨日は @dk_masu さんの プログラミング初心者がじゃんけんのプログラムを書いてみた でした。

明日は @susumuis さんです。


以前、ITpro の JavaFX 連載で振り子のアニメーションを取りあげたことがありました。もし、よろしければ、要登録なのですが、ごらんください。

JavaFX 2ではじめる、GUI開発 第13回 タイムラインを使ったアニメーション

この時は、javafx.animation.AnimationTimer クラスを使用して、振り子のアニメーションを実現させました。

アニメーションではなく、静止画ですが、実行例はこちら。

http://itpro.nikkeibp.co.jp/article/COLUMN/20130719/492504/pendulums01.gif

これは、これでキレイなのでいいのですが... でも、いまいちリアリティにとぼしいわけです。

なぜ、リアルさが足りないのかをつらつら考えるに、糸がまっすぐなのがイヤなのかなぁと。実際の振り子などは、角度が大きくなったときにしなるじゃないですか。あれがないんです。

上の振り子の軌跡を描画したのが次の図です。

これに対して、しなりを入れてみたのが、こちら。

違いが分かりますか? 分かりますよね。

で、後者の方がそれっぽいわけです。

物理で等時性のとれた振り子はサイクロイド振り子と呼びます。重りの軌跡がサイクロイド曲線になっているので、サイクロイド振り子といいます。このサイクロイド振り子はちゃんとしなっているわけです。

今回は見た目重視なので、正しくサイクロイド振り子を表すつもりはまったくないのです。それっぽく見えれば OK。

ちなみに、上の直線で記述した振り子の糸の軌跡を描くには、次のメソッドを使用しました。ここでは Group クラスを使ってますけど、それを適当なコンテナに貼ってあげれば軌跡が表示できます。

    private void drawString(Group root) {
        DoubleStream.iterate(-180.0, t -> t + 10.0)
                    .limit(37)
                    .forEach(t -> {
                        double alpha = Math.PI * Math.sin(t * Math.PI / 180.0) / 6.0;
                        double endX = XPOSITION + LENGTH * Math.sin(alpha);
                        double endY = LENGTH * Math.cos(alpha);

                        Line line = new Line(XPOSITION, 0, endX, endY);
                        line.setStroke(Color.WHITE);

                        root.getChildren().add(line);
                    });
    }

まぁ、それはさておき、しなった糸を描画するのですから、曲線を使わなくてはいけません。

CG で曲線といったら、やっぱりベジェ曲線ですよね。

ベジェ曲線は端点と制御点から構成される曲線です。制御点が 1 つのもの 2 次ベジェ曲線、2 つのものを 3 次ベジェ曲線と呼びます。

図の赤い点が制御点です。

端点と制御点を結んだ直線が、端点での曲線の接線になります。

自由度が高いのは 3 次ベジェ曲線で、図のような S 字の曲線は 2 次ベジェ曲線では描けません (もちろん、複数の 2 次ベジェ曲線を使えば描けますけど)。

JavaFX の場合、2 次ベジェ曲線と 3 次ベジェ曲線の両方をサポートしています。それぞれ、QuadCurve クラス、CubicCurve クラスで表します。双方とも javafx.scene.shape パッケージで提供されているクラスです。

ここでは自由度の高い CubicCurve クラスを使用します。

では、とりあえずやってみましょう。

上のメソッドでは Line クラスを使っていた部分を CubicCurve に置き換えてみます。制御点はとりあえず端点と同じ場所にしてみましょう。

CubicCurve クラスはコンストラクタに開始点、第 1 制御点、第 2 制御点、終点の順に指定します。

public class DukeDeSwing extends Application {

    private static final double XPOSITION = 400.0;
    private static final double LENGTH = 500.0;

    private CubicCurve string = new CubicCurve(
            XPOSITION, 0.0,     // 開始点
            XPOSITION, 0.0,     // 第 1 制御点
            XPOSITION, LENGTH,  // 第 2 制御点
            XPOSITION, LENGTH); // 終点
    private Rotate rotate = new Rotate(0.0, 0.0, 0.0);

    @Override
    public void start(Stage stage) {
        Group root = new Group();

        string.setStroke(Color.WHITE);
        string.setStrokeWidth(1.0);
        string.setFill(null);
        root.getChildren().add(string);

        Scene scene = new Scene(root, XPOSITION * 2.0, LENGTH + 100.0);
        scene.setFill(Color.BLACK);

        stage.setScene(scene);
        stage.setTitle("Pendulum");
        stage.show();

        startSwingAnimation();
    }

    private void startSwingAnimation() {
        AnimationTimer timer = new AnimationTimer() {
            @Override
            public void handle(long now) {
                double alpha = Math.PI * Math.sin(now / 500_000_000.0) / 6.0;

                // 終点の位置の更新
                double endX = XPOSITION + LENGTH * Math.sin(alpha);
                double endY = LENGTH * Math.cos(alpha);
                string.setEndX(endX);
                string.setEndY(endY);
            }
        };

        timer.start();
    }

    public static void main(String... args) {
        launch(args);
    }

}

実行してみると、こんな感じになりました。

なんか思っていたのと違う!

というか、しなりすぎ!!

という予定調和のツッコミをしてみました。

これはなぜかというと制御点を動かしていないからです。下の図のように、開始点に制御点はあるままなので、終点がそこに引っ張られるようになってしまうのです。

では、どうすればよいでしょうか。

答えは簡単で制御点も動かせばいいのです。

でも、どうやって?

とりあえず、振り子っぽく動けばいいので、下の図のようにしたいと思います。

終点は円運動しているのですから、第 1 制御点、第 2 制御点とも円運動をさせてみます。ただし、その角度は徐々に減らすようにしてみました。

これを行うのは簡単です。改造した startSwingAnimation メソッドを下に示します。

    private void startSwingAnimation() {
        AnimationTimer timer = new AnimationTimer() {
            @Override
            public void handle(long now) {
                double alpha = Math.PI * Math.sin(now / 500_000_000.0) / 6.0;

                // 終点の位置の更新
                double endX = XPOSITION + LENGTH * Math.sin(alpha);
                double endY = LENGTH * Math.cos(alpha);
                string.setEndX(endX);
                string.setEndY(endY);

                // 第2制御点の更新
                double controlX2 = XPOSITION + 300 * Math.sin(alpha * .8);
                double controlY2 = 300 * Math.cos(alpha * .8);
                string.setControlX2(controlX2);
                string.setControlY2(controlY2);

                // 第1制御点の更新
                double controlX1 = XPOSITION + 100 * Math.sin(alpha * .4);
                double controlY1 = 100 * Math.cos(alpha * .4);
                string.setControlX1(controlX1);
                string.setControlY1(controlY1);
            }
        };

        timer.start();
    }

第 2 制御点が終点の 80% の角度、第 1 制御点が 40% の角度に制限してみました。

では、実行してみましょう。

なかなかいい感じではないですか。

と、ここまでが前振りです。長すぎですね。

ブランコ

このエントリーのタイトルが Duke で Swing なのに、どこにも Swing が出てこないじゃないかと文句を言われている方も多いかもしれません。

でも、この Swing はあの Swing ではなくて、ブランコの Swing のことなのです。そう、あの Swing も、もとをただせばブランコの Swing なのです。

下の図はサンタクララにある往年の Sun の本社の Swing チームの壁に描かれた Duke です。

Sun Microsystems, Santa Clara

そう、Swing はブランコだったのです。

で、JavaFXDuke の Swing を描画させてしまおうというのが、このエントリーの趣旨なわけです。

さて、今までは振り子として描画させてきたわけですが、これをブランコにしましょう。

といっても、線を太くするぐらいですが。

で、Duke の絵を用意しましょう。ここでは Illustrator で下のような Duke の絵を描きました。

ブランコの板に座っている Duke です。

これをイメージとしてエクスポートして、読み込んでもいいのですが、せっかくのベクター画像なのでそのまま JavaFX にインポートしようと思います。

そう、SVG です。

以前作成した SVGLoader を使用して読み込んであげましょう。

SVG の使い方については以前書いたのでこちらをご参照ください。

SVGLoader

この時は JavaFX 2.x ですが、使い方はそのまま同じです。この Duke de Swing のプロジェクトにも SVGLoader を同胞したので、そのまま使うことができます。

さて、SVG を使うと、Illustrator のレイヤーをそのまま使用することができます。

レイヤーなしに Duke のノードを描画させてからブランコの紐を描画すると、紐を持たない Duke になってしまいます。

そこで、ここでは Duke の本体と手の部分をレイヤーに分け、奥から Duke 本体、ブランコの紐、Duke の手となるように描画させます。

このようにすることで、Duke が紐を握っているように見えるわけです。

もう 1 つ工夫があります。

Illustrator の原点 (0, 0) を Duke の下のブランコの板の中心にしてあります。原点を Duke の左上などにしてしまうと、紐の端点と一緒に動かすときに補正をする必要が出てきてしまいます。

それを避けるためにも、はじめからブランコの板を原点に持ってきているわけです。

では、Duke のノードをロードするところです。これは start メソッドで行いました。

    public void start(Stage stage) {
        Group root = new Group();

        SVGContent content = SVGLoader.load(getClass().getResource("duke.svg").toString());

        // Duke のロード
        Node duke = content.getNode("duke");
        root.getChildren().add(duke);
        
        // Duke の位置を紐の終点とバインドする
        duke.translateXProperty().bind(string.endXProperty());
        duke.translateYProperty().bind(string.endYProperty());

        // ブランコの紐
        string.setStroke(Color.PERU);
        string.setStrokeWidth(10.0);
        string.setFill(null);
        root.getChildren().add(string);

        // Duke の手のロード
        Node hand = content.getNode("hand");
        root.getChildren().add(hand);

        // Duke の手の位置を紐の終点とバインドする
        hand.translateXProperty().bind(string.endXProperty());
        hand.translateYProperty().bind(string.endYProperty());

        Scene scene = new Scene(root, XPOSITION * 2.0, LENGTH + 100.0);
        scene.setFill(Color.BLACK);

        stage.setScene(scene);
        stage.setTitle("Duke de Swing");
        stage.show();

        startSwingAnimation();
    }

javaFX では、コンテナにはじめに追加したものが奥に描画されます。そのため、Duke 本体、紐、手の順番にコンテナに追加しています。

では、紐と一緒に一緒に場所を更新する処理を書かなくては... と思うかもしれませんが、必要ありません。

そう、ここで、バインドです!

duke や hand の移動量を表すプロパティ translateX/translateY を紐の終点にバインドすれば OK。これで紐の終点と一緒に動きます。

先ほど、原点について言及しましたけど、このバインドの処理が単純になるわけです。

では実行してみましょう。

えっ...

Duke が回転していない ><

ということで、回転させましょう。

ここではブランコの紐の終点から第 2 制御点に至る直線を使用して Duke の傾きを決めたいと思います。というのもこの直線が終点での紐の接線になるからです。

それと直角になるように Duke を傾ければいいわけです。つまり Duke から見ると、その直線は法線になるわけです。

この直線の傾きを出すには、アークタンジェントです。

アークタンジェントが何かを忘れている方も多いと思いますが、角度が分かるわけです。

ここでは Math クラスの atan2 メソッドを使用します。atan2 メソッドは第 1 引数に y 軸方向の差分、第 2 引数に x 軸方向の差分を指定します。

回転にはノードの setRotate を使いたいところですが、これは使えません。なぜかというと、setRotate はノードの真ん中を中心にして回転させるからです。

ここでは、ブランコの板を中心にして回転させたいのです。こういう場合には javafx.scene.transform.Rotate クラスを使用します。Rotate クラスでは回転の中心や、回転軸を指定することができます。

ここでは次のような rotate 変数をフィールドとして定義しました。

    // Duke の回転用
    private Rotate rotate = new Rotate(0.0, 0.0, 0.0);

Rotate クラスのコンストラクタの第 1 引数は回転角度、第 2、第 3 引数で回転の中心を指定します。ここでも、Duke の原点をブランコの板にしたのが効いてきます。つまり、回転の中心も原点でいいわけです。

次に duke と hand にこの rotate をセットしなくてはなりません。これは start メソッドの中で行っています。

        duke.getTransforms().add(rotate);

        hand.getTransforms().add(rotate);

Roate クラスのような変換は複数指定することができるので、getTransforms メソッドでリストを取り出して、そこに追加するようにします。

最後に角度の更新です。これは startSwingAnimation メソッドで行っています。

                // Dukeの回転の更新
                double theta = 180.0 * Math.atan2(endY - controlY2, endX - controlX2) / Math.PI - 90.0;
                rotate.setAngle(theta);

Rotate クラスの回転角は度なのに対し、atan2 の返り値はラジアンなので、変換が必要です。90 度引いているのは、法線だからです。

これで完璧なはず。さっそく実行してみましょう。

というわけで、Duke で Swing 完成です!!

ソースや DukeIllustrator のファイルは GitHub で公開しています。

Duke de Swing

それにしても、Duke 見てると癒やされるなあ。1 日中眺めていたいww

追うしょぼちむ

このエントリーは しょぼちむ Advent Calendar 2014 の 8 日目です

昨日は @shiget84 さんの しょぼちむさんをきっかけに価値を届けることについて考えた でした。

明日は @cocoatomo さんです。


はるか昔、neko というアプリケーションがあったのを皆さんごぞんじでしょうか?

wikipedia:Neko (ソフトウェア)

一瞬かわいいのですが、使っているとうざくてしかたないアプリケーションです。

なぜ、こんなアプリケーションを思い出したかというと、うらがみさんにかまって、かまってと言い続けるしょぼちむの存在があったからこそです。

そこで、かつての neko を現代によみがえらせた syobo を作成してみたいと思います。

もちろん、そこは櫻庭が作るものですから、JavaFX です。

ところが、JavaFX では 1 つだけ問題があります。マウスカーソルの位置を検出できないという問題です。

アプリケーションを表示している領域であれば、マウス移動のイベントが取得できるので、マウスカーソルの位置も取得できるのですが、何も表示していない場所ではこのイベントが発生しないのです。

しかたないので、AWT です ><

AWT には MouseInfo というクラスがあり、いつでもマウスカーソルの位置を取得することができます。そこで、タイマで定期的にカーソル位置を取得し、しょぼちむの位置を更新するということを行っていきます。

JavaFX で Swing を扱うのですが、ここでは直接 Swing のコンポーネントを扱うわけではないので、Swing の EDT を使用するだけで大丈夫です。

Swing の Timer クラスで 50 ミリ秒ごとにマウスカーソルの位置を検出するには次のように記述します。

    private void startSwingEDT() {
        // AWTでマウスの位置を 50 秒ごとに検出
        SwingUtilities.invokeLater(() -> {
            Timer timer = new Timer(50, e -> {
                PointerInfo info = MouseInfo.getPointerInfo();
                Platform.runLater(() -> updateLocation(info.getLocation().getX(), info.getLocation().getY()));
            });
            timer.start();
        });
    }

SwingUtilities.invokeLater メソッドで Swing の EDT を起動し、そこでタイマを生成して 50 ミリ秒ごとに処理を行わせています。

MouseInfo クラスの getPinterInfo メソッドマウスカーソルの位置が取得できるので、後は JavaFX のスレッドで位置を更新します。JavaFX のスレッドで処理するためには Platform クラスの runLater メソッドを使用します。

Swing と JavaFX を両方使っていると、スレッドの違いがうざいのですが、しかたありません。

updateLocation メソッドではカーソルの位置に追従して、しょぼちむを表示させます。たいした数学ではないので、分かるでしょう。

    private void updateLocation(double mx, double my) {
        cursor.setTranslateX(mx);
        cursor.setTranslateY(my);

        double tx = syobochim.getTranslateX();
        double ty = syobochim.getTranslateY();

        // 近傍であれば位置の更新を行わない
        double d = (mx - tx) * (mx - tx) + (my - ty) * (my - ty);
        if (d < DISTANCE * DISTANCE) {
            return;
        }

        // カーソルとsyobochimの角度を算出
        double theta = Math.atan2(my - ty, mx - tx);

        // 移動分を算出
        double dx = DISTANCE * Math.cos(theta);
        double dy = DISTANCE * Math.sin(theta);
        syobochim.setTranslateX(tx + dx);
        syobochim.setTranslateY(ty + dy);

        // 角度に応じて回転
        // カーソルの右側にsyobocimが位置している場合は反転
        syobochim.getTransforms().removeIf(trans -> trans.equals(rotate));
        if (theta > PI / 2.0 || theta < -PI / 2.0) {
            syobochim.getTransforms().add(rotate);
            syobochim.setRotate(theta * 180.0 / PI - 180.0);
        } else {
            syobochim.setRotate(theta * 180.0 / PI);
        }
    }

後は、透明ステージにするとか、常にトップにもってくるなどを行ってから表示を行っています。

    @Override
    public void start(Stage stage) {
        Group root = new Group();
        initImage(root);

        // Scene をスクリーンと同サイズに設定
        Screen screen = Screen.getPrimary();
        Scene scene = new Scene(root, screen.getBounds().getWidth(), screen.getBounds().getHeight());
        // 背景を透過にする
        scene.setFill(null);
        // カーソルを表示しない
        scene.setCursor(Cursor.NONE);

        stage.setScene(scene);
        // 透明ステージにする
        stage.initStyle(StageStyle.TRANSPARENT);
        stage.setAlwaysOnTop(true);
        stage.show();

        // Swing の EDT でタイマ処理を行う
        startSwingEDT();
    }

さて、これで完成です。実行してみましょう。

f:id:skrb:20141208211357p:plain

これで、ずっとうらがみさんを追い続けるしょぼちむのできあがりです。

ただ、しょぼちむのイメージはもっと大きくてもよかったなぁ。

それに今回は手抜きなのでちゃんとアニメーションしていないし。時間があったら手や足を動かして、追うようにします。

ついでに、カーソル動かなかったら、寝てしまうというのも取り込みたいなぁ...

ソースは GitHub で公開しています!

GitHub syobo

JavaFX Night でバインドの発表をしてきた

このエントリーは、2014 年の JavaFX Advent Calendar の 1 日目のエントリーです。

明日は alchemicalogic さんです。

blog も半年ぶりなわけですが、それよりも久しぶりの JavaFX の勉強会である JavaFX Night を 11/25 に開催しました。ほぼ 1 年ぶりです ^ ^;;

雨という悪コンディションの中、約 50 人の方にご参加いただきました。もっとキャンセルが多いかと思っていたのですが、うれしい誤算でした。

櫻庭は、バインドについての発表を行いました。

実をいうと資料は使いまわし。7/11 に大阪で よなよな JavaFX というイベントを開催したのですが、そこで話した内容と同じです。

資料はこちら。

Property

バインドは JavaFX 1.x 時代の JavaFX Script では言語でサポートされた機能でした。その当時のバインドは簡単に使えるものだったのですが、JavaFX 2 で JavaFX Script が廃止されたため JavaAPI として書き直されています。

そのため、簡単に使えたバインドが、結構めんどくさくなってしまいました。

その一番のめんどくさいところが、Property。

JavaFX 2 からはバインドできる対象を Property としたのです。Property は値を 1 つだけもてるコレクションのようなものです。Property を使うと、保持している値が変更されたときにイベントが発生します。

このイベントの受け取り方は 2 種類あり、一方が ChangeListner、もう一方が InvalidationListener です。

ChangeListener は値が変更すると、即座にコールされます。それに対して、InvalidationListener は値が変更しても、その値を使わなければコールされません。ようするにイベントの発生が遅延されます。

そして、バインドはこちらの InvalidationListener を使用して実装されています。

バインド

バインドは bind メソッドで行い、両方向のバインドは bindBiDirectional メソッドで行います。

バインドを行うことで、Property 同士の値を自動的に同期させることができます。

その他、Bindings クラスが提供しているさまざまな演算を組み合わせることで複雑な演算や、論理演算もバインドで可能になります。

バインドのユースケース

最後にバインドのユースケースを 4 つ紹介しました。

  • 知らないうちにバインドを使用している
  • View と Controller や、Controller と Model をバインドさせる
  • バリデーションや制約
  • アニメーション

最初の知らないうちというのは TableView などのクラスに関してです。作法に則って記述していれば、いつの間にバインドを使っているはずです。

そして、一番多く使われるのが、2 番目の使い方だと思います。まぁ、普通の使い方ですね。

3 番目の使い方が、意外に便利です。テキストフィールドになにか入力がないとボタンを押せないとか、数値しか入力させないようにするとか、レイアウトにつかうなどです。この用途は、ほんとに使い道いろいろなので、自分でも発掘していこうと思ってます。

最後のアニメーションはおまけです。おととしの Advent Calendar で書いた、irof さんの絵描き歌 を紹介しました。

講演後、バインド使ってみますという感想をいただけたので、よかったのではないかと思います。まだ、バインド使いこなしている人はすくないようなので、ぜひみなさんも使ってみてください!!

ページめくりのアニメーション

福岡でのプレゼンではいつも通り JavaFX のプレゼンツールで行いました。

Slideshare の表紙を見ていただければ分かるかもしれませんが、今回は本をテーマにしたプレゼン資料を作成しました。

なので、資料のページ送りも、紙のページをめくったようなアニメーションで行いました。で、ページ送りの部分だけ独立させてみました。

この動画だと、ページを 3D で動かしているように見えるかもしれませんが、全部 2D です ^ ^;;

そのせいで、いろいろとめんどうなことやってます。

ページのクリッピングのアニメーション

ページは重ねて表示してあり、上に現在表示しているページ、下に次に表示するページとなっています。

まず、行うのは上の現在表示しているページをクリッピングすること。

クリッピングというのは任意のノードで表示領域を制限することです。

単にクリッピングしただけではしかたないので、クリッピングした領域をアニメーションで左に移動させています。こうすることで、ページの右側からだんだんと見えなくなるということが可能になります。

一番上の四角がクリッピング領域でその下のイメージがそれによって右側がクリッピングされます。そして、このクリッピング領域を左の方に移動させるわけです。

このアニメーションは簡単。単に TranslateTransition を使うだけです。

        Node present = group.getChildren().get(group.getChildren().size() - 1);

        // 現在のページのクリップ
        Rectangle presentClip = new Rectangle(0, 0, width, height);
        present.setClip(presentClip);

        // 現在のページのクリップの移動アニメーション
        TranslateTransition presentClipAnim 
            = new TranslateTransition(Duration.millis(DURATION), presentClip);
        presentClipAnim.setToX(-width);
        presentClipAnim.setInterpolator(Interpolator.EASE_IN);

group はページを表示しているコンテナです。

レイアウトされるといろいろと支障があるので、今回のページ送りは Group 限定です。ページの中身は何でもいいので、そちらは適当にレイアウトでも何でもやってください。

で、現在表示されているページが present です。

present をクリップするのは四角系である Rectangle オブジェクト presentClip で、present と同じ大きさで、初期位置は present と同じ場所です。

それを、イメージの幅分だけ左に移動させます。

ポイントは補間方法を EASE IN にすること。

ページをめくる時、めくりはじめはゆっくり動きます。でも、ちょうどページが消える時は、ページが上を向いている時なので、一番速度が速くなっているはず。

なので、はじめゆっくり、後は速くという EASE IN にします。

これを EASE BOTH にしてしまうと、終わりもゆっくりになってしまって、ちぐはぐした印象になります。

これで上のイメージが徐々に消えて、下のイメージが表示されるようになります。

ページの裏

次にやるのは、ページにすけたイメージを表示させること。

ページにすけているので、鏡像になっています。

JavaFX Script の頃は、ノードを複製する機能があったので、簡単に鏡像ができたのですが、JavaFX 2.x になったらなくなってしまいました。

でも、Reflection のエフェクトでは鏡像ができているので、どうにかすればできるはず。

で、どうやったら鏡像ができるかというと、スナップショットを撮りました。それまではノードを複製することばかり考えていたのですが、別にノードでなくてもよくて、イメージさえとれればいいことに気がついたわけです。

スナップショットは Node の snapshot メソッドで取得します。Node クラスのメソッドなので、任意のノードのスナップショットが取得可能です。

戻り値が WritableImage で、引数はスナップショットを撮る時のパラメータと、WritableImage。パラメータは何もしていせず、WritableImage は null で大丈夫です。

        ImageView flipImage = new ImageView(present.snapshot(new SnapshotParameters(), null));

スナップショットをページの裏を作っていきます。

まず、鏡像にするために y 軸を中心に 180 度回転させ、透明度も調整します。透明度を設定しているため、そのままだと下の絵が透けてしまうので、白い四角の上に描画するようにしています。

        // ページ裏
        Group flip = new Group();

        // ページ裏の下地
        Rectangle flipRect = new Rectangle(0, 0, width, height);
        flipRect.setFill(Color.WHITE);
        flip.getChildren().add(flipRect);

        // ページを反転したイメージ
        ImageView flipImage = new ImageView(present.snapshot(new SnapshotParameters(), null));
        flipImage.setRotationAxis(new Point3D(0.0, 1.0, 0.0));
        flipImage.setRotate(180.0);
        flipImage.setOpacity(0.4);
        flip.getChildren().add(flipImage);

        // ページ裏用のクリップ
        Rectangle flipClip = new Rectangle(0, 0, width, height);
        flip.setClip(flipClip);

        group.getChildren().add(flip);

ページの裏のアニメーション

ページ裏が準備できたので、次はページの裏をめくるアニメーションです。

でも、実際はめくっているわけではなくて、単に右から左へ移動させているだけです。しかし、単に移動させるだけでは、めくっているように見えないので、ここでもクリッピングと合わせて移動させます。

つまり、裏返したページの移動 + クリッピング領域の移動 の 2 つのアニメーションでめくっているように見せます。

ポイントは移動の速度が違うこと。裏返しページはクリッピング領域の 2 倍の速度で動かします。

といのも裏返しページの初期位置が異なっているためです。クリッピング領域は始め x 座標が 0 の位置にあります。それに対し、裏返しページの x 座標は width です。

アニメーション後はどちらも、-width の位置になります。つまり、クリッピング領域は width だけ移動しますが、裏返しページは 2×width 移動するわけです。

一番上の四角がページ裏のクリッピング領域、その下がページ裏です。

ページ裏のクリッピング領域は、元のページのクリッピング領域と同じ速度で移動するので、見切れた部分が折り返しているように見えるわけです。

2 つのアニメーションはどちらも TranslateTransition です。

        // ページ裏のアニメーション
        TranslateTransition flipAnim = new TranslateTransition(Duration.millis(DURATION), flip);
        flipAnim.setFromX(width);
        flipAnim.setToX(-width);
        flipAnim.setInterpolator(Interpolator.EASE_IN);

        // ページ裏のクリップのアニメーション
        TranslateTransition clipAnim = new TranslateTransition(Duration.millis(DURATION), flipClip);
        clipAnim.setFromX(-width);
        clipAnim.setToX(0);
        clipAnim.setInterpolator(Interpolator.EASE_IN);

ページ裏のアニメーションは特に問題ないはずです。右側から左側に抜けるので、fromX が width で、toX が -width になります。

問題はクリップ領域のアニメーションです。

fromX が -width で、toX が 0 なのがくせものです。どういうことかというと、クリップする対象のページ裏からの相対位置になるからです。

このアニメーションだけだと右方向に移動しますが、ページ裏が左に移動しているので、クリッピング領域も左に移動します。

さて、ここまででとりあえずページをめくったアニメーションはできました。

もちろん、これで終わりにしてしまってもいいのですが、よりリアリティを持たせるために影を作りましょう。ページとページの間にできる影です。

影は端が透明、真ん中がグレーになっている四角を用意して、それをページの境目に描画することで実現しています。

これは Rectangle と LinearGradient で描画します。

        // ページの間にできる影
        Rectangle shadow = new Rectangle(width - 30, 0, 60, height);
        LinearGradient gradient = new LinearGradient(0, 0, 1, 0, true, CycleMethod.NO_CYCLE,
                new Stop(0.0, Color.rgb(0, 0, 0, 0.0)),
                new Stop(0.5, Color.rgb(0, 0, 0, 0.5)),
                new Stop(1.0, Color.rgb(0, 0, 0, 0.0)));
        shadow.setFill(gradient);
        group.getChildren().add(shadow);

もちろん、ページが動くのと同期して影も動かします。

また、ページのめくり始めは影はないはずです。なので、影を細くしておいて徐々に太くし、さらにページめくりが終わりそうになると再び補足するというアニメーションも行っています。

        // 影が太るアニメーション
        ScaleTransition shadowAnim1 = new ScaleTransition(Duration.millis(DURATION / 5), shadow);
        shadowAnim1.setFromX(0.0);
        shadowAnim1.setToX(1.0);
        shadowAnim1.setInterpolator(Interpolator.EASE_IN);

        // 影の移動アニメーション
        TranslateTransition shadowAnim2 = new TranslateTransition(Duration.millis(DURATION), shadow);
        shadowAnim2.setToX(-width);
        shadowAnim2.setInterpolator(Interpolator.EASE_IN);

        // 影が痩せるアニメーション
        ScaleTransition shadowAnim3 = new ScaleTransition(Duration.millis(DURATION / 5), shadow);
        shadowAnim3.setFromX(1.0);
        shadowAnim3.setToX(0.0);
        shadowAnim3.setInterpolator(Interpolator.EASE_OUT);
        SequentialTransition seqAnim
                = new SequentialTransition(new PauseTransition(Duration.millis(DURATION * 4 / 5)),
                        shadowAnim3);

移動は、今までと同じように TranslateTransition で行います。

影を太らせたり、細くするのは ScaleTransition を使います。ScaleTransition は拡大・縮小を行うアニメーションですが、軸ごとに拡大率・縮小率を設定できます。そのため、ここでは x 軸方向だけ拡大・縮小するということをやっています。

また、影を細くするのはページ送りの最後の方なので、はじめにポースしておいて、その後に細くなるアニメーションを行います。これは指定したアニメーションをシーケンシャルに行う SequentialTransition を使います。

アニメーションの制御

最後にこれらのアニメーションを同時に行うように設定し、アニメーションを開始します。

        ParallelTransition animation = new ParallelTransition(
                presentClipAnim,
                flipAnim,
                clipAnim,
                shadowAnim1,
                shadowAnim2,
                seqAnim);
        
        // アニメーションが終了した時の処理
        animation.setOnFinished(e -> {
            group.getChildren().remove(flip);
            group.getChildren().remove(shadow);

            present.setTranslateX(0);
            present.setClip(null);
            group.getChildren().remove(present);

            group.getChildren().add(0, present);
        });

        animation.play();

アニメーションを同時に行わせるのは ParallelTransition を使用します。コンストラクタで指定したアニメーションを同時に行います。

また、setOnFinished メソッドでは、アニメーションの終了時にコールされるコールバックメソッドをセットできます。

ここでは、ページ裏と影を削除します。

また、現ページも削除します。その際に、移動量を 0 にクリアしておき、またクリップ領域もクリアしておきます。これは削除したページを再利用するためです。再利用しないのであれば、移動量やクリップ領域をクリアする必要はありません。

最後に play メソッドでアニメーションを開始します。

これでページがめくれるわけです。

コードは gist で公開してあります。

https://gist.github.com/skrb/1c62b77ef7ddb3c7adf4


Page Flipping Demonstration

8 True Stories about JavaFX

福岡JavaFX のプレゼンをしてきました。

いまさらですが、8 個は多かった。時間オーバーしてしまいました ><

でも、8 はゴロがいいんですよね。Java SE 8 勉強会だし。

はじめは JavaFX を知らない人向けでだんだんとコード多くする方向で説明しました。でも、最後の Lambda Ready はとってつけたような感じでしたね。

そこそこ楽しんでいただけたようでよかったです。

さて、次は大阪

JavaFX 8 で 3D その 1

このエントリーは、2013 年の JavaFX Advent Calendar の 24 日目のエントリーです。

昨日は id:tikemin さんの iOS7 + RoboVM + JavaFX のイロイロ でした。

明日のクリスマスは小出先生です。

12/13 に久しぶりに JavaFX 勉強会を開催しました。今回は櫻庭は 3D の話をしました。

JavaFX の 3D は Windows, Mac, Linux のいずれでも動作し、2D と 3D を同様にシームレスに扱うことも可能です。しかも、 Java 3D で弱かった、Maya などのツールのファイルの読み込みをサポートしています。

JavaFX の 3D は 2.x でも限定的に使えたのですが、本格的に使えるようになるのは JavaFX 8 からです。すでに 3D が動作する Java SE 8 の Early Access が公開されているので、すぐに試すことができます。

セッションでは 30 分しかなかったので、かなり話題を絞って以下の 3 つの話題にしました。

  • 座標
  • Model
  • Light

とりあえず、ここらへんを押さえておけば、なんとかなるでしょう。

座標

3D を行うのに欠かせないのが、座標です。もちろん、JavaFX でも 3 次元の座標を扱うのですが、ちょっとだけ注意が必要です。

というのも、通常の 3D の CG Tool は y 軸が数学と同じで上方向を向いています。ところが、JavaFX では 2D と 3D を区別なく使えるようにしているためなのか、y 軸が下を向いています。

ツールで作ったモデルをインポートしたら、なぜかモデルが下を向いていたということもあるので、注意が必要です。

もし、下向きの y 軸がいやなのであれば、z 軸を中心にして 180 度回転させます。

2D の座標は Point2D クラスで表しますが、3D は Point3D クラスで表します。

Node クラスのたとえば、移動の API には setTranslateX/setTranslateY/setTranslateZ というように 3 つが組になっているので、移動や回転、スケーリングなどは 2D と同じように行うことができます。

そして、もう 1 つ座標に関連して、3D で重要なのがカメラです。

カメラは、どこから空間に配置しているモデルを見るかということを表しています。

カメラには 2 種類あります。

光が広がっていかないカメラと、広がっていくカメラです。前者を直投影、後者を透視投影と呼びます。

直答英は 2D の世界です。奥行きがあっても、同じ大きさの物体は奥行き方向の位置が違っていても、同じ大きさになります。こちらは ParallelCamera クラスで表します。

一方の透視投影は、いわゆる一転透視法で遠いものは 1 点に修練していきます。こちらは PerspectiveCamera クラスで表します。

もちろん、デフォルトは ParallelCamera クラスです。

カメラをセットするには Scene クラスに対して setCamera メソッドで行います。たとえば、立方体を PerspectiveCamera クラスで見るには、次のようになります。

public class PerspectiveCameraDemo extends Application {

    @Override
    public void start(Stage stage) {
        Group root = new Group();
        
        // 辺の長さが20の立方体
        Box box = new Box(20, 20, 20);
        // 立方体を y 軸を中心に 30 度回転させる
        box.setRotationAxis(new Point3D(0.0, 1.0, 0.0));
        box.setRotate(30.0);
        root.getChildren().add(box);
        
        Scene scene = new Scene(root, 400, 400);
        scene.setFill(Color.BLACK);
        
        // 透視投影カメラを設定する
        PerspectiveCamera camera = new PerspectiveCamera(true);
        scene.setCamera(camera);

        // カメラの位置を (0, 0, -100) にする
        camera.setTranslateZ(-100.0);
        
        stage.setScene(scene);
        stage.setTitle("Perspective Camera Demo");
        stage.show();
    }

    public static void main(String... args) {
        launch(args);
    }
}

PerspectiveCamera クラスのコンストラクタの引数に true を設定すると、カメラの位置が原点に配置されます。この時、カメラの向きは z 軸の正の方向になります。

カメラもノードのサブクラスなので、setTranslateX/setTranslateY/setTranslateZ メソッドで任意の位置に移動することができます。ここでは、z 軸方向に -100 移動させているので、カメラの位置は (0, 0, -100) になります。

実行すると以下のようになります。

一点透視になるので、手前が大きく表示されます。

これに対し、ParallelCame クラスで立方体を表示させてみましょう。

public class ParallelCameraDemo extends Application {

    @Override
    public void start(Stage stage) {
        StackPane root = new StackPane();
        
        // 辺の長さが20の立方体
        Box box = new Box(150, 150, 150);
        // 立方体を y 軸を中心に 30 度回転させる
        box.setRotationAxis(new Point3D(0.0, 1.0, 0.0));
        box.setRotate(30.0);
        root.getChildren().add(box);
        
        Scene scene = new Scene(root, 400, 400);
        scene.setFill(Color.BLACK);
        
        // 直投影カメラを設定する
        ParallelCamera camera = new ParallelCamera();
        scene.setCamera(camera);
        
        stage.setScene(scene);
        stage.setTitle("Parallel Camera Demo");
        stage.show();
    }

    public static void main(String... args) {
        launch(args);
    }
}

同じようなサイズになるように、立方体のサイズなどを若干調整しています。

直投影だと全然立方体に見えません。というより、真ん中がくびれているように見えてしまうのが不思議なところです。

長くなってしまったので、続きはまた今度。


つづく

Touch で Animation

今日公開された ITpro の連載 で、タッチのジェスチャについて書きました。

記事の中では、イメージをスワイプで切り替えるビューアーのサンプルを書いたのですが、分量の問題でボツにしたサンプルもあります。そのままにするのももったいなので、ここで公開しておきます。

ちなみに、このエントリーは JavaFX Advent Calendar 向けに書こうと思っていたのですが、意外なことにすべて埋まってしまったので、普通のエントリーです ^ ^;;

みなさま、ゾエトロープ (Zoetrope) ってご存じですか?

アニメーションの元祖のような器具で、円筒の内側に連続した絵を描いておいて、回転させながらスリットからのぞき込むと絵が動いているように見えるものです。日本語だと回転のぞき絵というらしいです。

Wikipedia

Zoetrope は円筒を自分の手で回してアニメーションさせるわけですが、これをタッチというかジェスチャでやってみようかなと。

というわけで、作ってみました。

アニメーションさせるのは、おなじみの手を振る Duke です。Duke の画像は duke.kenai.com からダウンロードしてきました。

さて、できあがりの GUI を下に示します。Duke が並んでいますが、この画面を右側にスワイプさせると Duke がアニメーションするようにします。また、左側にスワイプすると、アニメーションが逆転します。

このアニメーションはスワイプのスピードによって速さが決まり、慣性スクロールで指が離れてもしばらくアニメーションが続きます。

Duke の画像を表示しているのは ImageView クラスです。アニメーションさせるには ImageView オブジェクトで表示する Image オブジェクトを切り替えるということをやっています。

ところで、Image オブジェクトは順番に呼び出して、終わりまでいったら最初からということを何度も繰り返すので、先にリングバッファを作ってしまいました。

リングバッファというのはコレクションの一種で、円環状に要素を取得できるコレクションです。これを作るのは簡単で、単純にインデックスを保持しておいて、最後までいったら 0 に戻すということをやっているだけです。

通常はこれでいいのですが、逆向きにも要素を取得できるメソッドを追加しました。

public class RingBuffer<T> {
    private List<T> buffer;
    private int index = -1;
    
    public RingBuffer(Collection<T> collection) {
        buffer = new ArrayList<>(collection);
    }
    
    // 次の要素を取得する
    // 最後まで到達したら、最初に戻る
    public T next() {
        index++;
        if (index >= buffer.size()) {
            index = 0;
        }

        T element = buffer.get(index);
        
        return element;
    }

    // 前の要素を取得する
    // 最初まで到達したら、最後に戻る
    public T previous() {
        index--;
        if (index < 0) {
            index = buffer.size() - 1;
        }

        T element = buffer.get(index);

        return element;
    }
}

さて、準備はできたので、メインクラスを作っていきます。

ちなみに、Stream と Lambda 式使いまくっているので、Java SE 8 必須ですww

まずは、タッチを考えずにイメージを水平方向に並べることにします。

public class Zoetrope extends Application {
    private List<ImageView> views;
    private RingBuffer<Image> images;
    
    @Override
    public void start(Stage stage) {
        HBox root = new HBox(10.0);
        root.setAlignment(Pos.CENTER);
        
        // ImageView オブジェクトを 10 作成して、リストに格納する
        views = IntStream.range(0, 10)
                         .mapToObj(i -> new ImageView())
                         .collect(Collectors.toList());
        root.getChildren().addAll(views);

        // 9個のファイルを読み込んでリストに格納し、それをリングバッファにする
        images = new RingBuffer(
            IntStream.range(1, 10)
                     .mapToObj(i -> new Image(getClass().getResource("resources/T" + i + ".gif").toString()))
                     .collect(Collectors.toList()));

        // ImageオブジェクトをImageViewオブジェクトにセットする
        views.forEach(view -> view.setImage(images.next()));

        Scene scene = new Scene(root, 800, 200);
        stage.setTitle("Zoetrope");
        stage.setScene(scene);
        stage.show();
    }
    
    public static void main(String... args) {
        launch(args);
    }    
}

Java SE 7 までは、for 文を何回も書かなくてはいけなかったのですが、Stream を使うことでとてもスッキリしました。

これからは、for (int i = 0; i < 10; i++) { ... } という文は、IntStream.range(0, 10).forEach(i -> { ... }); というように書くのが当たり前になるでしょうね。

これで実行すると、上の図のようになります。

では、タッチの処理を追加していきましょう。

ITpro の記事でも解説しましたが、タッチはマウスなどと同じようにイベントとして扱います。タッチは TouchEvent で表せますが、ここではジェスチャのイベントで扱うことにします。

ここで行っているジェスチャはスワイプなのですが、スワイプのイベントはスワイプ中に 1 度しか発生しません。ここでは指がどの程度動いたかによって、イメージを切り替えているので、1 度しか発生しないスワイプイベントは使えません。

そこで、代わりに使うのがスクロールイベント ScrollEvent です。

ScrollEvent はスクロールの開始時、スクロール中、スクロール終了時にイベントが発生します。スクロール中は定期的にイベントが発生します。さらに、スクロール終了した (指が離れた) 後にも、慣性でスクロールイベントが発生します。

また、ScrollEvent は都合のいいことに、前回のイベントからの移動分を取得できるようになっています。この移動分を使えば、イメージを切り替えるかどうかの判断に使用することができます。

ここでは、イメージの切り替えを、スクロールを始めてから、しきい値を超えるまで指を移動させたかで判断するようにします。このため、スクロール開始時に移動量を 0 にリセットし、スクロール中には前回からの移動分を移動量に加えていきます。

その移動量がしきい値を超えたら、イメージを切り替えます。

スクロールの開始は Node#setOnScrollStart メソッド、スクロール中は Node#setOnScroll メソッドで登録します。

イベント処理の登録は initScrollEventHandler メソッドでやることにしました。

    private void initScrollEventHandler(HBox root) {
        // スクロール開始時に移動量をリセットしておく
        root.setOnScrollStarted(e -> diff = 0.0);
        
        // 移動量がしきい値 CHANGE_THRESHOLD を超えたらイメージを切り替える
        root.setOnScroll(e -> {
            // 前回からの移動量の差分を加える
            diff += e.getDeltaX();
            
            if (diff >= CHANGE_THRESHOLD) { // 右方向のスクロール
                diff = 0;
                
                // イメージの切り替え
                views.forEach(view -> view.setImage(images.next()));
            }
        });
    }

前回からの水平方向の差分は ScrollEvent クラスの getDeltaX メソッドで取得できます。

Image オブジェクトは 9 個あり、ImageView オブジェクトは 10 個あるので、そのままリングバッファで取得し続けていくと、1 つずつずれるので、アニメーションしてくれます。

ただこれだと、右方向へのスクロールになってしまうので、左方向も加えましょう。

アニメーションを戻すには前のイメージに切り替えることが必要です。そこで、ImageView オブジェクトの右側のものから戻していきます。

    private void initScrollEventHandler(HBox root) {
        // スクロール開始時に移動量をリセットしておく
        root.setOnScrollStarted(e -> diff = 0.0);
        
        // 移動量がしきい値 CHANGE_THRESHOLD を超えたらイメージを切り替える
        root.setOnScroll(e -> {
            // 前回からの移動量の差分を加える
            diff += e.getDeltaX();
            
            if (diff >= CHANGE_THRESHOLD) { // 右方向のスクロール
                diff = 0;
                
                // イメージの切り替え
                views.forEach(view -> view.setImage(images.next()));
            } else if (diff <= -CHANGE_THRESHOLD) { // 左方向のスクロール
                diff = 0;
                
                // イメージを戻すように切り替える
                IntStream.iterate(views.size() - 1, i -> i-1)
                         .limit(views.size())
                         .mapToObj(i -> views.get(i))
                         .forEach(view -> view.setImage(images.previous()));
            }
        });
    }

IntStream#range が開始と最後だけでなく、差分も記述できれば、もっと簡単に書けるのですが、ないのでしかたありません。iterate メソッドだと無限に IntStream が続くので、limit メソッドで要素数を限定し、その後に ImageView オブジェクトを取得して、イメージをセットしていきます。

これで、左方向にスクロールするとアニメーションの逆回転ができます。とはいっても、この Duke のアニメーションは逆回転が分かりにくい ><

実行して、タッチでスクロールさせれば、下の図のようにアニメーションしますよ。タッチのスピードを変えてあげれば、アニメーションのスピードも変化します。

また、慣性スクロールしている間に、再びスクロールすればシームレスにアニメーションし続けます。

いかがですか? けっこう、いい感じにアニメーションしてますよね。

最後にソースの全体を貼っておきます。


Zoetrope: Animation using touch interface