Sooner or Later

このEntryは JavaFX Advent Calendar の16 日目です。

昨日は @khasunuma さんの JavaFX から Payara Micro を呼び出す際の注意点 でした。

明日は @nodamushi さんです。

そして、この Entry は Java Puzzlers Advent Calendar 2016 の 16 日目でもあります。

昨日は @khasunuma さんの Puzzle : String concatination でした。

そして、明日も @khasunuma さんです。

さて、JavaFX で Puzzle を書くわけですが、GUI は出てきません。

GUI の機能ではないのですが、JavaFX で重要な機能の 1 つにバインドがあります。みなさん、バインド使ってますか?

バインドは JavaFX のプロパティ間で値を自動的に同期させるための機構です。

たとえば、以下のコードではどうでしょう。

    IntegerProperty x = new SimpleIntegerProperty(0);
    IntegerProperty y = new SimpleIntegerProperty();
    y.bind(x);

    x.set(10);
    System.out.println(y.get());

JavaFX ではプロパティを なんちゃらProperty というクラスで表します。この なんちゃらProperty クラスを使うと、バインドももれなく着いてくるというわけです。

で、y は x にバインドしているので、x が変更されると y も勝手に値が更新されます。

なので、このコードを実行すると 10 が出力されます。

バインドは自動で同期してくれるだけではなく、バインドが遅延処理されることも特徴です。

上のコードだと、x が変更されたら、y が変更されるのではありません。y を使用する時に、x が変更されているかどうかをチェックします。この遅延処理によってムダな同期処理を減らすことができるのです。

さて、ここからが Puzzle です。

次のコードを実行したらどうなるでしょう。

import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;

public class SoonerOrLater {
    public static void main(String... args) {
        IntegerProperty x = new SimpleIntegerProperty(0);
        IntegerProperty y = new SimpleIntegerProperty();
        y.bind(x);
        
        y.addListener((observable, previous, present) -> {
            System.out.println("Change");
        });
        y.addListener(observable -> {
            System.out.println("Change");
        });

        x.set(10);
        x.set(20);

        System.out.println(y.get());
    }
}

他の Puzzlers Advent の人はちゃんと 4 択にしているんですけど、Java Puzzlers の本はそうでないんですよね。 4 択にするのは、あくまでもプレゼンテーションのためです。

なので、ここでは選択肢は出しません。実行したら、どうなるでしょうか?

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

答えは、次のようになります。

Change
Change
Change
Change
20

たぶん大方の人は、こうなると想像していたと思います。パズルにもなにもなっていないように感じられるかもしれません。

この問題は JavaFX をある程度知っている人が陥るパズルになっているのです。

試しに、次のコードではどうなるでしょう。

import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;

public class SoonerOrLater {
    public static void main(String... args) {
        IntegerProperty x = new SimpleIntegerProperty(0);
        IntegerProperty y = new SimpleIntegerProperty();
        y.bind(x);
        
//        y.addListener((observable, previous, present) -> {
//            System.out.println("Change");
//        });
        y.addListener(observable -> {
            System.out.println("Change");
        });

        x.set(10);
        x.set(20);

        System.out.println(y.get());
    }
}

先ほどのパズルのうち、一方の addListener メソッドをコメントアウトしただけです。

実行すると、次のようになります。

Change
20

Change が出力されるのは、2 回だと思いませんでしたか?

ところが、Change が出力されるのは、1 回です。

ラムダ式を使っているので、分かりにくくなっているのですが、x に登録しているリスナはそれぞれインタフェースが異なります。

引数が 3 つのラムダ式が ChangeListener インタフェース、1 つラムダ式が InvalidationListener インタフェースです。

ChangeListener インタフェースは、Java Beans の PropertyChangeListener インタフェースと同様に、プロパティの値が変更されたら、コールされます。

一方の InvalidationListener インタフェースはプロパティが invalid になった時にコールされます。これはどういうことかというと、遅延処理に関係しているわけです。

プロパティの使用時にプロパティの値が valid かどうかを調べて、invalid だったらコールされるのです。

上のコードでは y が使用されるのは、最後の出力の時だけなので、その時に valid かどうかチェックされます。

x は 2 回変更されていますが、valid かどうかのチェックはその後に行うので、結果的に 1 度しかコールされません。

ところが、ChangeListener インタフェースは異なります。バインドされているターゲットの値が変更されてもコールされます。つまり、その都度、値が変化しているかどうかチェックするわけです。

問題は、2 種類のリスナを登録するとどうなるかということです。

バインドのターゲットの値が変更されたら、ChangeListener インタフェースのメソッドがコールされます。この時に、古い値と新しい値を引数に渡します。このプロパティの値を取得するということは、プロパティを使用するということになります。

つまり、valid かどうかのチェックが行われてしまうのです。

ChangeListener インタフェースのメソッドは 2 回コールされるので、InvalidationListener インタフェースのメソッドも 2 回コールされてしまいます。

そして、最後の y.get() 時にも valid かどうかのチェックは行われるのですが、x.set(20) の後に変更はないので、値は valid です。このため、InvalidationListener インタフェースのメソッドはコールされないのです。

このパズルの教訓です。

  • バインドしているプロパティの値をチェックしたいからといって、安易に ChangeListener インタフェースを登録するのはやめよう

Puzzle のネーミング

Joshua Bloch のパズルのネーミングって、とってもウィットに富んでいます。中には日本ではよく分からない習慣や文化に基づいているものもあるので、全部理解できるわけではないのですが...

Java Puzzlers の翻訳をされた柴田さんもパズルのネーミングの意味を調べるのが大変だったらしいですし。

でも、こういうウィットやユーモアを含ませたネーミングというのは、とても重要だと思うわけです。

みんな、パズルを作る時にこういうところまで気にすると、もっとパズルがおもしろくなるのになぁと個人的には思っています。

今回の Sooner or Later ですが、日本語にすると「遅かれ早かれ」ということです。

遅いか早いかはよく分からないけど、最終的にはリスナのメソッドがコールされるよということです。もちろん、遅延処理が絡んでくるからこういうネーミングにしたわけですが、どうでしょうか?

Scene Builder 小ネタ 3つ

このEntryはJavaFX Advent Calendarの11日目です。

昨日は @Yucchi_jp さんの HitInfoを少しだけ… でした。

明日は @boochnich さんです。

すいません、小ネタです。

Scene Builder をビルドする

みなさん、Scene Builder使ってますか? 便利ですよね。

でも、Oracleバイナリパッケージを配布しなくなってしまったのが... めんどくさいんですかねぇ。まぁ、Gluonがバイナリパッケージを配布してくれているからいいのですが。

とはいえ、せっかくのOSSなのですから、ビルドしてみたいと思いませんか。

JavaFX、というかOpenJFXをビルドするにはいろいろと準備が必要なのでめんどうなんですけど、Scene Builderだけであれば簡単です。

さっそくやってみましょう。

なお、今回はJava SE 8用のScene Builderをビルドします。

まずは、OpenJFXをクローンします。残念ながら、Scene Builderだけをクローンすることはできないのです。ちなみに、OpenJDKが使っているのはMercurialです。

hg clone http://hg.openjdk.java.net/openjfx/8u-dev/rt

クローンが完了しましたか。そうしたら、rt/apps/scenebuilderを見てみましょう。SceneBuilderAppディレクトリがScene BUilder本体、SceneBuilderKitディレクトリがIDEに組み込むなどの用途で使用するライブラリになってます。

OpenJFXのビルドにはgradleを使うのですが... なんとSceneBuilderAppのディレクトリを見てみるとnbprojectというディレクトリがあります。

これが何を意味するかというと、NetBeans用のプロジェクトになっているということです。

さっそくNetBeansで読み込んでみましょう。

SceneBuilderApp単独ではビルドできないので、SceneBuilderKitも一緒に読み込みます。

後は、SceneBuilderAppプロジェクトでF6すれば、ビルドして実行します。

JavaFXのアプリケーションは実行するとJARファイルも作ってくれます。distディレクトリにSceneBuilderApp.jarファイルができているはずです。後は-jarオプションで実行すればOK。

java -jar SceneBuilderApp.jar

とはいえ、dist/libにあるSceneBuilderKit.jarも一緒に使ってます。なので、他の場所にコピーする時はSceneBuilderKit.jarも忘れずに。

ルートコンテナを変更する

たとえば、NetBeans で FXML を生成すると、勝手に AnchorPane をルートコンテナとする FXML を生成しますよね。「空のFXML」を生成するといっても、ルートコンテナだけは勝手に設定してしまいます。

でも、AnchorPane を使いたいわけじゃないんだよ、他のコンテナを使いたいんだよ、ということも多いのでは。

そんな時、どうするかというと、IDEテキストエディタで FXML を編集すればいいのですが... そんな時でも、Scene Builder を使いたいわけです。

Scene Builder には Wrap in という機能があります。これを使えば、ルートコンテナを変更することもできるのです。

Wrap in は任意のノードをコンテナで包み込む機能です。

たとえば、AnchorPane に Button が貼られているとします。この Button を、AnchorPane の直下ではなく、間にコンテナ、たとえば FlowPane を挟みたいような場合に使用します。

これをやるには、Wrap in したいノードを選択します。選択するのは Hierarchy の処でも、中央のエディタ部分でもどちらでも OK。

そして、右クリックでポップアップメニューを表示させ、[Wrap in] を選択します。すると、下の図のようにコンテナの一覧がサブメニューに表示されるので、使用したいコンテナを選択します。

ノードを直接右クリックしなくても、メニューバーの [Arrange] - [Wrap in] でもおなじことができます。

Wrap in すると、ノードとコンテナの間に、指定したコンテナが挟まります。

この機能、どんなノードに対してもできるので、コンテナに対してもできます。つまり、ルートコンテナでも OK ということです。

ルートコンテナに対して Wrap in をしても、ちゃんと名前空間の定義やコントローラクラスへのリンクが保ったままにしてくれます。

ただし、スタイルシートは引き継がないので、注意が必要です。

たとえば、次のような FXML があったとしましょう。

<AnchorPane id="AnchorPane" prefHeight="400.0" prefWidth="600.0" 
            xmlns:fx="http://javafx.com/fxml/1" fx:controller="ViewController">
    <stylesheets>
        <URL value="@view.css"/>
    </stylesheets>
</AnchorPane>

これを BorderPane で Wrap in すると、次のようになります。

<BorderPane xmlns="http://javafx.com/javafx/8.0.60" 
            xmlns:fx="http://javafx.com/fxml/1" 
            fx:controller="ViewController">
   <center>
      <AnchorPane id="AnchorPane" prefHeight="400.0" prefWidth="600.0">
          <stylesheets>
              <URL value="@view.css" />
          </stylesheets>
      </AnchorPane>
   </center>
</BorderPane>

CSS の view.css がルートコンテナに引き継がれていません。また、サイズは設定されていないのは、しかたないですね。

コントローラクラスの雛形を作る

「コントローラクラスの雛形を作るのにいい方法ないですか」と FB で質問されたのですが、意外にみなさん Scene Builder の機能を知らないのですね ^ ^;;

コントローラクラスの雛形を作るのは簡単。

メニューバーの [View] - [Show Sample Controller Skelton] を選択すれば OK です。

たとえば、某連載用に作った次の FXML でコントローラクラスを作ってみましょう。

<VBox alignment="CENTER" prefHeight="100.0" prefWidth="400.0" spacing="12.0" 
      xmlns="http://javafx.com/javafx/8.0.60" 
      xmlns:fx="http://javafx.com/fxml/1" 
      fx:controller="DictionaryController">
   <children>
      <HBox alignment="CENTER" spacing="20.0">
         <children>
            <TextField fx:id="keyField" prefColumnCount="20" promptText="Key" />
            <Button mnemonicParsing="false" onAction="#search" text="Search" />
         </children></HBox>
      <Label fx:id="valueLabel" />
   </children>
</VBox>

この FXML は fx:id で結びつけられた要素が 2 つ (keyField, valueLabel)、イベント処理が 1 つ (searchメソッド) あります。

では、[View] - {Show Sample Controller Skelton] を選択してみると...

というダイアログが表示されます。ファイルには落とせないので、左下の[コピー]を選択すれば、クリップボードに保存してくれるので、後はペーストするだけ。

でも、このスケルトン、間違っているんですよね ^ ^;;

何が違うかというと、ActionEvent の import 文が抜けてます。まぁ、import 文は IDE で補完してくれるから、問題ないといえば、ないのですが。

ちなみに、右下の [Full] を選択すると、コードが次のようになります。

import java.net.URL;
import java.util.ResourceBundle;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;

public class DictionaryController {

    @FXML
    private ResourceBundle resources;

    @FXML
    private URL location;

    @FXML
    private TextField keyField;

    @FXML
    private Label valueLabel;

    @FXML
    void search(ActionEvent event) {

    }

    @FXML
    void initialize() {
        assert keyField != null : "fx:id=\"keyField\" was not injected: check your FXML file 'dictionaryView.fxml'.";
        assert valueLabel != null : "fx:id=\"valueLabel\" was not injected: check your FXML file 'dictionaryView.fxml'.";

    }
}

これだと、ちゃんと ActionEvent の import 文も含まれています。

ちなみに、initializeメソッドが Initializable インタフェースの initialize メソッドではないところに注意が必要ですね。

ところで、このダイアログ、違和感ないですか?

今の Scene Builder は L8N されていないのですが、なぜかこの [コピー] ボタンだけ日本語になっているのです。なんでなんでしょうね?

Displacement Map

ということで、JavaFX Advent Calendar も最後になりました。

昨日は id:kikutaro777 さんの Salesforce Driver by JavaFX でした。

さて、今年は JavaFX Advent Calendar を 6 回も書くことになってしまって、完全にネタ切れです。しかも、12 月だけで、3 回も講演をしに旅行に行っているので、いろいろと試す時間もなくて、つらいです ><

今日の内容も自分的には極められていないので、悔しいのですが、時間が迫っているので、しかたないのです。

で、今日の話題は Displacement Map です。

クラス的には javafx.scene.effect.DisplacementMap クラスです。

Displacement Map というのは、元々は 3D CG の言葉です。たとえば、惑星の表面のようにデコボコがあるような場合、従来はバンプマッピングという手法を使って、見せかけのデコボコを描いてました。

でも、見せかけだけなので、近くに寄ってきたりすると分かっちゃうんですよね。デコボコがないのが。そこで、使われるようになったのが Displacement Map です。

Displacement Map を使うと本当にポリゴンの表面を変異させてデコボコを作れます。GPU でシェーダーが使えるようになってから普及した手法ですね。

さて、問題は JavaFX の Displacement Map です。

DisplacementMap クラスはパッケージから分かるように Effect の一種です。使いこなせればけっこう強力なエフェクトですが、自分もまだ完全に使いこなしているわけではないのです...

まぁ、とりあえず分かっているところまでをやってみましょう。

Displacement Map はある座標を与えると、それを新しい座標に変換して描画します。2 次元で考えた場合、元の座標が (x, y) で新しい座標が (x', y') だったとします。この場合、座標の変換は次の式で与えられます。

  x' = x - (offsetX + scaleX * map(x, y)[0] * width)
  y' = y - (offsetY + scaleY * map(x, y)[1] * height) 

ここで、map(x, y) は座標を入力にした時に、変位を返す関数です。map(x, y)[0] が x 座標、map(x, y)[1] が y 座標だと考えてください。

offsetX, offsetY はその名の通りオフセットです。scaleX, scaleY は map 関数の倍率です。width と height は対象としているノードの幅と高さです。幅と高さをかけているということは、変位は相対値になります。

そして、map 関数を実現しているのが、FloatMap クラスです。

たとえば、イメージを x 軸方向に 10%、y 軸方向に 10% だけ平行移動をするには次のように記述します。

        FloatMap floatMap = new FloatMap(width, height);

        for (int x = 0; x < width; x++) {
            for (int y = 0; y < height; y++) {
                floatMap.setSamples(x, y, -0.1f, -0.1f);
            }
        }

        DisplacementMap displacementMap = new DisplacementMap(floatMap);

        ImageView view = new ImageView(new Image(getClass().getResource("cat.jpg").toString()));
        view.setEffect(displacementMap);

DisplacementMap クラスのコンストラクタには FloatMap オブジェクトを指定しています。FloatMap オブジェクトだけを指定した場合、offset は 0、scale は 1 になります。

コンストラクタオーバーロードには offset や scale を指定できるものがありますが、ここでは FloatMap クラスの使い方をメインにするため、offset を 0、scale を 1 に限定します。

で、FloatMap クラスはコンストラクタで幅と高さを指定します。そして、setSample メソッドでサンプルを設定します。つまり、map(x, y) の結果を指定していることになります。

第 1 引数、第 2 引数が座標、第 3 引数、第 4 引数がその時の変位となります。FlotMap なので、変位を示す型は float です。

setSample メソッドなんて使わずに、map 関数をラムダ式で指定できるようになると使うのも簡単になると思うのですが、残念ながら対応しておらず。自分で作ってしまえと思ったのですが、FloatMap クラスと DisplacementMap クラスのソースを読み切れてないので、こちらもできてません ><

それはそれとして、ここでは 10% ずつ動かすので、-0.1f を指定しています。

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

元の画像がこちらです。

これに対し、上記の DisplacementMap を施したものがこちら。

ここで、わかりやすさのために、もともとの画像の位置に赤枠を描画しています。右下にずれていることが分かりますよね。

たとえば、x 軸方向だけ 20% だけ拡大するならば、こんな感じ。

        FloatMap floatMap = new FloatMap(width, height);

        for (int x = 0; x < width; x++) {
            for (int y = 0; y < height; y++) {
                floatMap.setSamples(x, y, -(float)x/width * 0.2f, 0.0f);
            }
        }

        DisplacementMap displacementMap = new DisplacementMap(floatMap);

        ImageView view = new ImageView(new Image(getClass().getResource("cat.jpg").toString()));
        view.setEffect(displacementMap);

で、実行してみたのがこちら。ずいぶん、丸顔になってしまいましたw


レンズ効果

簡単な使い方が分かったので、ちょっとした応用をしてみましょう。

一時期、イヌの鼻デカ写真が流行ったのを覚えているでしょうか。広角レンズや魚眼レンズで撮ったイヌの写真で、中央に鼻を持ってくると、鼻が強調して写るのです。

で、それをやってみたいわけです。

変位で考えると、中央は移動させず、中央の周りは変位を大きくして拡大、端っこになると縮小させれば、できるはずです。

しかも、中央の右側と左側、上と下で変位の符号を変えなくてはなりません。これを簡単にやるには... だいたいこういうのは三角関数というのが定番なんですよね。

中央で変位の符号を変えるには、sin で考えると、中央を 180 度 (ラジアンだとπ) にしてやれば OK です。x 方向で考えた時に、x = 0 と x = width の時に変位を 0 にするには、width で 1 周期、つまり 2π にすればいいわけです。

で、できた FloatMap オブジェクトはこんなのです。

        FloatMap floatMap = new FloatMap(width, height);

        for (int x = 0; x < width; x++) {
            float u = (float) Math.sin(Math.PI * x / width * 2.0) * 0.05f;
            for (int y = 0; y < height; y++) {
                float v = (float) Math.sin(Math.PI * y / height * 2.0) * 0.05f;
                floatMap.setSamples(x, y, u, v);
            }
        }

x 軸方向の変位が u、y 軸方向の変位が v です。最後に 0.05 を掛けているのが、最大の拡大率というわけです。つまり、最大 5% の拡大になります。

で、やってみました。

確かに中央部は拡大されているものの、ちょっと違う ><

まぁ、理由は分かっているんですけどね。本来はノードの中心からの極座標で変位を決めなくてはいけないのですが、手抜きしたのでこうなってしまっているわけです。

もうちょっと時間がある時に、再チャレンジしてみたいと思います。


Lens Effect

黒地に黄色のクローラー

この記事は、JavaFX Advent Calendar 2015 の 17 日目の記事です。

昨日は id:c9katayama さんの PDFBoxとFXGraphics2Dを使って大きなPDFをレンダリングする でした。

明日は @yumix_h さんです

タイトルの黒地に黄色のクローラーといえば、あれですよ、あれ。

明日の公開のあの映画。そう Star Wars です。こんなやつです。

http://img.lum.dolimg.com/v1/images/episode-3-crawl_4d843f60.jpeg

過去に、プレゼンのはじめに Star Wars のパロディのムービーを作ったことがある櫻庭ですから、この波に乗らないわけにはいきません。

ということで、JavaFX であのクローラーを作ってみようというわけです。

3D でやってみる

あのクローラーは文字列が奥のほうに移動しているので、まずは単純に 3D の API を使って、z 軸方向に移動させてみます。

でも、単に文字列を移動させても、台形の形にはならないので、文字列を x 軸を回転軸にして回転させます。ようするに、文字列を寝かせるわけですね。そして、z 軸方向に移動させるわけです。

まず、3D にするにはカメラを設定しなくてはいけません。

通常、2D の GUI では平行透視法のカメラが設定されています。このため、奥にあるノードでも小さく表示されることはありません。でも、3D の場合は、奥にあるノードは小さく表示しなくてはなりません。そのためには、それようのカメラ、つまり透視法を使用したカメラを設定します。

これは簡単で、Scene に PerspectiveCamera を設定します。

        Scene scene = new Scene(root);
        scene.setCamera(new PerspectiveCamera());

これで、奥の方のノードは小さく表示されるようになりました。

さて、次は文字列を寝かせる処理です。これはノードの setRotate メソッドで行います。しかし、単に setRotate メソッドをコールすると、z 軸を中心に回転してしまいます。これを x 軸を中心にさせるには setRoateAxis メソッドを使用します。

        Text text = new Text(crawl);

        text.setRotationAxis(new Point3D(1.0, 0.0, 0.0));
        text.setRotate(-80.0);

2 行目で、x 軸を表す Point3D オブジェクトを生成して、setRotationAxis メソッドに指定しています。そして、3 行目で後ろに 80 度倒す処理をします。JavaFX では角度はラジアンではなくてディグリーを使用することに注意してください。

準備ができたので、z 軸方向に移動するアニメーションを作成してみましょう。z 軸方向に移動する場合でも、移動は移動なので、TransTranslateTransition を使用します。

        TranslateTransition trans = new TranslateTransition(Duration.millis(20_000), text);
        trans.setToZ(1_000);
        trans.setInterpolator(Interpolator.LINEAR);
        trans.setCycleCount(Animation.INDEFINITE);
        trans.play();

プログラムの全体はブログの最後の方にまとめて載せました。

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

確かに、奥に移動する感じはできましたけど、なんかちょっと違う...

奥の方に移動すると、x 軸方向に回転させた角度が変化してしまうのが気になります。3D なので、当たり前といえば当たり前なのですが。

また、いくら z 軸上で奥に移動させても、見えなくなるほどにはなりません。そのため、さいごの方の挙動が止まっているように見えてしまいます。

単純に 3D で移動させるよりも、もうちょっといい方法があるような気がします。もうちょっと模索してみましょう。

3D と 2D のアニメーションを組み合わせる

次に思いついたのが、3D の表現と 2D のアニメーションを組み合わせる方法です。

文字列を後ろに寝かせるのは 3D で行うとして、奥に移動するアニメーションは疑似的に 2D で表せないかということです。具体的には y 軸方向の上に移動するアニメーションと、縮小するアニメーションを組み合わすということです。

y 軸方向に移動させるアニメーションは先ほどと同じように TranslateTransition を使用します。また、拡大縮小には ScaleTransition を使用します。

まずは、移動のアニメーションです。

        TranslateTransition trans = new TranslateTransition(Duration.millis(20_000), text);
        trans.setFromY(1_000);
        trans.setToY(-100);
        trans.setInterpolator(Interpolator.EASE_OUT);
        trans.setCycleCount(Animation.INDEFINITE);
        trans.play();

単に y 軸上を動かしているだけです。

そして、拡大縮小です。はじめは拡大しておいて、徐々に縮小します。ただし、文字列を寝かしているため、z 軸方向にも大きさがあります。そのため、z 軸方向も拡大縮小します。

        ScaleTransition scale = new ScaleTransition(Duration.millis(20_000), text);
        scale.setFromX(5.0);
        scale.setFromY(5.0);
        scale.setFromZ(5.0);
        scale.setToX(0.01);
        scale.setToY(0.01);
        scale.setToZ(0.01);
        scale.setInterpolator(Interpolator.LINEAR);
        scale.setCycleCount(Animation.INDEFINITE);
        scale.play();

なお、複数のアニメーションを同時にやるためには ParallelTransition を使うこともありますが、そんなにきっちり合わす必要がなければ、単に複数のアニメーションを play してあげれば大丈夫です。

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

先ほどよりはいい感じになっていると思うのですが、どうでしょう。

しかし、これもアニメーションで移動させていると、文字列の寝ている角度が変わってしまうのが気になります。まぁ、こちらも、当たり前といったら当たり前ですが。

そこで、移動と同時に角度を変化させるアニメーションも一緒に行ってみます。

角度を変化させるアニメーションは RotateTransition です。

        RotateTransition rotate = new RotateTransition(Duration.millis(20_000), text);
        rotate.setToAngle(-80.0);
        rotate.setInterpolator(Interpolator.EASE_OUT);
        rotate.setCycleCount(Animation.INDEFINITE);
        rotate.play();

角度を変化させているといっても、ほんのちょっとです。それでも、見た目はかなり変化します。

ここでは回転の中心を設定していませんが、それははじめに文字列に対して設定しているからです。

これで同時に行うアニメーションは 3 つになりましたが、効果はどうでしょう。さっそく実行してみましょう。

かなりよくなったと思いませんか。これはこれでいいかもしれませんが、他の方法も模索してみましょう。

すべて 2D のアニメーションで行う

さいごに、3D はあきらめて、すべて 2D で行ってみましょう。もちろん、Star Wars の Episode IV の頃には CG はなかったのですから、2D のアニメーションでやっているはずです。

ということは、2D のアニメーションで実現するということは、原点回帰になるのかな?

もちろん、2D なので、Scene にカメラを設定させる必要もありません。

しかし、問題になるのは文字列を寝かせることができるかどうかです。実をいうと、これはエフェクトを使うことで、比較的簡単にできます。

どんなエフェクトかというと PerspectiveTransform です。先ほどまで使用していた PerspectiveCamera と同じく、透視法が使えるエフェクトというわけです。

PerspectiveTransform はノードを囲む四隅を任意の点に写像することができるエフェクトです。AffineTransform が実現するアフィン変換だと平行は平行に変換されますが、PerspectvieTransform では平行は維持されません。つまり長方形を台形などに変換することができるのです。

変換には四隅の座標を指定します。

        PerspectiveTransform perspective = new PerspectiveTransform();
        perspective.setUlx(300.0);
        perspective.setUly(200.0);

        perspective.setUrx(500.0);
        perspective.setUry(200.0);

        perspective.setLlx(0.0);
        perspective.setLly(500.0);

        perspective.setLrx(800.0);
        perspective.setLry(500.0);

        text.setEffect(perspective);

それぞれ 2 行ずつで、上左、上右、下左、下右の座標を指定しています。ここでは、ルートのコンテナの座標を台形にするような変形になります。

そして、後は先ほどと同じように y 軸上を移動するアニメーションと拡大縮小を組み合わせます。ただし、拡大縮小はノードの真ん中を中心に拡大縮小してしまうため、上記の変換と合わせると変な位置に変換されてしまいます。

そこで、Scale を使用して、拡大縮小の中心を変化させています。Scale を使ってアニメーションをするため、Transition ではなく Timeline を使用しています。

        TranslateTransition trans = new TranslateTransition(Duration.millis(20_000), text);
        trans.setFromY(400);
        trans.setToY(0);
        trans.setInterpolator(Interpolator.EASE_OUT);
        trans.setCycleCount(Animation.INDEFINITE);
        trans.play();

        Scale scale = Transform.scale(4.0, 4.0, 400.0, 200.0);
        text.getTransforms().add(scale);
        Timeline timeline = new Timeline(
                new KeyFrame(Duration.millis(19_000),
                        new KeyValue(scale.xProperty(), 0.3), 
                        new KeyValue(scale.yProperty(), 0.3)),
                new KeyFrame(Duration.millis(20_000),
                        new KeyValue(scale.xProperty(), 0.001), 
                        new KeyValue(scale.yProperty(), 0.001))
        );
        timeline.setCycleCount(Animation.INDEFINITE);
        timeline.play();

拡大縮小のアニメーションを 2 段階にしているのは、よりそれっぽさを出すためです。

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

PerspectiveTransform と Scale を組み合わせているせいか、アニメーション開始時の拡大している状態では少しノードの表示が荒れてしまっていますが、動きは一番それらしいのではないでしょうか。

ということで、3 種類の方法で Star Warsクローラーを実現してみました!

これで、時代の波に乗れたかなww

プログラム

3D でやってみる


Crawler Demo 1

3D と 2D のアニメーションを組み合わせる


Crawler Demo 2

すべて 2D のアニメーションで行う


Crawler Demo 3

JavaFX と Project Jigsaw

この記事は、JavaFX Advent Calendar 2015 の 12 日目の記事です。

昨日は @y_q1m さんの JavaFX アプリケーションに隠し Control を搭載する でした。

明日は @tomo_taka01 さんです

ごめんなさい。今日も小ネタです。

JavaOne に行ってからというもの、Project Jigsaw が面白くなっていろいろやってます。

ところで、JavaFXJava SE 9 ではあまり新機能が盛り込まれないようなのです。Feature Complete がちょっと伸びたので、もう少し盛り込むようですが...

で、なんで新機能が少ないかというと、Project Jigsaw への対応が大変だったからということらしいのです。JavaFX 9 については、 id:aoe-tk さんの JavaFX9に追加される機能が増えるかもしれません を見ていただくとして、「じゃあ JavaFX のモジュールはどうなったの?」というところをちょっと調べてみました。

Java SE 9 の java コマンドには -listmods というオプションが追加になって、標準のモジュールの一覧を表示することができます。まずは、これを試してみましょう。

C:\>java -listmods 2>&1 | grep javafx -
javafx.base@9.0
javafx.controls@9.0
javafx.deploy@9.0
javafx.fxml@9.0
javafx.graphics@9.0
javafx.media@9.0
javafx.swing@9.0
javafx.web@9.0

8 個のモジュールが JavaFX に関連するようです。

基本となるのが javafx.base で、コントロールは javafx.controls というようになっているのはわかるのですが、javafx.graphics などがよく分かりません。javafx.base が基本というのは分かりますけど、基本というのはどのぐらいの範囲、つまりどういうクラスが含まれているのかなどもよく分かりません。

これを調べるために、簡単なクラスで試してみました。

public class Test extends Application {
    @Override
    public void start(Stage stage) throws Exception {
        stage.show();
    }
    
    public static void main(String... args) {
        launch(args);
    }
}

単にステージを表示するだけのクラスです。これが含まれる JAR ファイルが test.jar だとします。で、この JAR ファイル (or クラス) がどのようなモジュールを使用しているかを調べるには、jdeps コマンドを使用します。

jdeps -s test.jar
test.jar -> java.base
test.jar -> javafx.graphics

オプションの -s はサマリだけを出力させるオプションです。

ステージを表示するだけで、他には何もやっていないのだから javafx.base に依存しているだろうと思ったら、javafx.graphics に依存していました。もちろん、javafx.graphics は javafx.base に依存しているでしょうから、間接的には依存はしているのですが...

-s オプションを外すと、もうちょっと詳しく見ることができます。

jdeps test.jar
test.jar -> java.base
test.jar -> javafx.graphics
   <unnamed> (test.jar)
      -> java.lang
      -> javafx.application
      -> javafx.stage

やはり、Application クラスや Stage クラスは javafx.graphics モジュールに含まれるようです。

ちょっとサンプルを改造してみましょう。

public class Test extends Application {
    @Override
    public void start(Stage stage) throws Exception {
        Label label = new Label("Label");
        
        StackPane root = new StackPane(label);
        Scene scene = new Scene(root);
        stage.setScene(scene);
        stage.show();
    }
    
    public static void main(String... args) {
        launch(args);
    }
}

こうすると、依存しているモジュールには javafx.control が出てくるはず。

jdeps -s test.jar
test.jar -> java.base
test.jar -> javafx.controls
test.jar -> javafx.graphics

やはり、javafx.controls に依存していました。

しかし、javafx.base や javafx.graphics はよく分からないままです。こういう時は、モジュールを直接調べてみましょう。

一般的にモジュールには JAR ファイルを使用するのですが、標準のモジュールは JAR ファイルではなく、JMOD ファイルになっています。モジュールが JAR ファイルの時は jar コマンドで調べられるのですが、JMOD ファイルの時は jmod コマンドを使用します。どちらもオプションとして -p (--print-module-descriptor) を使用すれば、モジュールの情報を表示してくれます。

さっそくやってみましょう。JMOD ファイルは JDK のディレクトリの jmods ディレクトリにあります。

C:\Program Files\Java\jdk1.9.0\jmods>jmod javafx.base.jmod -p

Name:
  javafx.base@9.0
Requires:
  jdk.jfr
  java.base [ MANDATED ]
  java.desktop
Exports:
  com.sun.javafx to [javafx.controls, javafx.graphics, javafx.swing]
  com.sun.javafx.beans to [javafx.controls, javafx.graphics, javafx.fxml]
  com.sun.javafx.collections to [javafx.controls, javafx.graphics, javafx.swing, javafx.media]
  com.sun.javafx.binding to [javafx.controls, javafx.graphics]
  com.sun.javafx.event to [javafx.controls, javafx.graphics]
  com.sun.javafx.logging to [javafx.graphics]
  com.sun.javafx.property to [javafx.controls]
  com.sun.javafx.runtime to [javafx.graphics]
  javafx.beans
  javafx.beans.binding
  javafx.beans.property
  javafx.beans.property.adapter
  javafx.beans.value
  javafx.collections
  javafx.collections.transformation
  javafx.event
  javafx.util
  javafx.util.converter
Conceals:
  com.sun.javafx.property.adapter
Hashes:
  Algorithm: SHA-256
  java.desktop: AE4yDbzkIjSeteMHRe9ZMpEkjE/u4PMGSL7WCwpEyOw=
  jdk.jfr: 6hhBHpCTp8ZkLutorg8GIFCp+D9MrL++mIsTh6grzyE=
  java.base: Jjip1psj45/C01FcGzXZc83cwqFHAZVVL3wSXJLiMpc=

外に公開しているパッケージは Exports の項に書いてあります。実をいうと、Exports の部分は順番がぐちゃぐちゃだったので、ソートしなおしてあります。

これを見ると、javafx.base は、意外にもグラフィックに関する部分はまったく含んでおらず、プロパティやバインドに関するクラスだけを含んでいるようです。

ただし、Requires の項に、java.desktop が含まれていることに注意が必要です。java.desktop は AWT や Swing が含まれているモジュールです。これに依存しているので、まったくグラフィックに関連がないというわけではないことが分かります。

次に、javafx.graphics です。

C:\Program Files\Java\jdk1.9.0\jmods>jmod javafx.graphics.jmod -p

Name:
  javafx.graphics@9.0
Requires:
  java.base [ MANDATED ]
  java.desktop
  java.xml
  javafx.base [ PUBLIC ]
Exports:
  com.sun.glass.ui to [javafx.web, javafx.media]
  com.sun.glass.utils to [javafx.web, javafx.media]
  com.sun.javafx.application to [javafx.web, javafx.controls, javafx.deploy, javafx.swing]
  com.sun.javafx.cursor to [javafx.deploy, javafx.swing]
  com.sun.javafx.css to [javafx.controls, javafx.deploy]
  com.sun.javafx.css.converters to [javafx.deploy]
  com.sun.javafx.css.parser to [javafx.deploy]
  com.sun.javafx.embed to [javafx.deploy, javafx.swing]
  com.sun.javafx.font to [javafx.web]
  com.sun.javafx.geom to [javafx.web, javafx.controls, javafx.swing, javafx.media]
  com.sun.javafx.geom.transform to [javafx.web, javafx.controls, javafx.swing, javafx.media]
  com.sun.javafx.iio to [javafx.web]
  com.sun.javafx.jmx to [javafx.web, javafx.swing, javafx.media]
  com.sun.javafx.menu to [javafx.controls, javafx.deploy]
  com.sun.javafx.perf to [javafx.deploy]
  com.sun.javafx.scene to [javafx.web, javafx.controls, javafx.deploy, javafx.swing, javafx.media]
  com.sun.javafx.scene.input to [javafx.web, javafx.controls, javafx.swing]
  com.sun.javafx.scene.text to [javafx.web, javafx.controls]
  com.sun.javafx.scene.traversal to [javafx.web, javafx.controls]
  com.sun.javafx.sg.prism to [javafx.web, javafx.swing, javafx.media]
  com.sun.javafx.stage to [javafx.controls, javafx.deploy, javafx.swing]
  com.sun.javafx.text to [javafx.web, javafx.deploy]
  com.sun.javafx.tk to [javafx.web, javafx.controls, javafx.deploy, javafx.swing, javafx.media]
  com.sun.javafx.util to [javafx.web, javafx.controls, javafx.fxml]
  com.sun.prism to [javafx.web, javafx.media]
  com.sun.prism.image to [javafx.web]
  com.sun.prism.paint to [javafx.web]
  com.sun.scenario.effect to [javafx.web]
  com.sun.scenario.effect.impl to [javafx.web]
  com.sun.scenario.effect.impl.prism to [javafx.web]
  javafx.animation
  javafx.application
  javafx.concurrent
  javafx.css
  javafx.css.converter
  javafx.geometry
  javafx.print
  javafx.scene
  javafx.scene.canvas
  javafx.scene.effect
  javafx.scene.image
  javafx.scene.input
  javafx.scene.layout
  javafx.scene.paint
  javafx.scene.shape
  javafx.scene.text
  javafx.scene.transform
  javafx.stage
Conceals:
   <<省略>>
Hashes:
  Algorithm: SHA-256
  javafx.base: vKYsvlQtkuWc5pwKzHKlUfIp9lXa4SAjnU1TniODA74=
  java.xml: 1FV2E7DCyzOAroiUpxIyJMSg+AzeDI5qm1SmeFyqv4U=
  java.desktop: AE4yDbzkIjSeteMHRe9ZMpEkjE/u4PMGSL7WCwpEyOw=
  java.base: Jjip1psj45/C01FcGzXZc83cwqFHAZVVL3wSXJLiMpc=

こちらも、ソートしなおしてあります。

Applicationや、Scene、Stage も javafx.graphics に含まれていることが分かります。また、イベント関連、CSS 関連なども含まれています。

ちょっと意外だったのが、Animation や Shape も含まれていることです。こういうクラスたちは、外だしされるかと思ってましたけど、基本のところに含まれるんですね。

私がいつも使っているクラスたちは、ほぼ javafx.graphics で済んでしまうということも分かりました。あんまり、コントロール使わないんですよね ^ ^;;; もちろん、仕事では使ってますけど。

ということで、今日はここまで。

Interpolator で補間

この記事は、JavaFX Advent Calendar 2015 の 9 日目の記事です。

昨日も私の JCConf で JavaFX について発表してきた でした。

明日は @kokuzawa さんです

今日は小ネタ。

アニメーションで重要な概念に Interpolator があります。Interpolate が「補間する」という意味なので、Interpolator は「補間をするもの」ぐらいですかね。

コンピュータでアニメーションをする時は、ある時点での状態から、次の時点での状態を指定します。JavaFX で Timeline を使うのであれば、ある時点の状態を示すのが KeyFrame になります。

通常は複数の KeyFrame でアニメーションを行うわけですが、その KeyFrame 間の状態をどのようにつなげていくかということが補間です。

たとえば、移動する場合であれば、ある地点から他の地点へ移動する時に、等速運動で移動するのか、それとも重力落下のように等加速度運動で移動するのかなどさまざまな方法があります。等速運動で移動するのであれば、ある時点から次の時点までに 4 回の画面の書き換えがあれば、1 回の書きかえごとに 1/4 ずつ移動すればいいことになります。

実際には、何回の書き換えが行われるのかは GPU の性能などによって異なるため、補間曲線を定義しておきます。

通常、補間曲線は x 軸が時間、y 軸が状態の変化量を表すのですが、汎用的に使用できるように通常は開始点を原点、終了点を (1, 1) の点とします。

たとえば、等速運動だとこんな補間曲線になります。

この補間曲線があれば、どのように補間すればいいかすぐに分かるわけです。そして、これを保持しているのが Interpolator というわけです。

ところで、自然界で等速運動はあまり見かけることはありません。動作のしはじめはゆっくりなど、動作によって非線形に状態の変化が起こります。

よく使われるのが、はじめゆっくり、さいごゆっくり、はじめとさいごの両方ゆっくりという 3 種類です。たとえば、車が発進する時はアクセルを踏んでゆっくり加速しはじめ、途中は等速運動、さいごはブレーキを踏んでゆっくり止まります。これは、はじめとさいごの両方ゆっくりに当てはまります。

で、この 3 種類は一般的に Ease In、Ease Out、Ease Both (Ease In Out ということもあります) と呼ばれます。Interpolator クラスにもこの 3 種類はそれぞれ EASE_IN、EASE_OUT、EASE_BOTH と定数定義されています。

そのほかに、等速運動を表す LINEAR と、セルアニメーションのように離散的に状態が変化する DISCRETE の、合計 5 種類が定数として定義されています。

それぞれの定数の補間曲線で描いてみましょう。

EASE_IN

EASE_OUT

EASE_BOTH

DISCRETE

この 5 種類がどのように補間されているのか理解するには、実際にアニメーションにしてみるのが手っとり早いです。

5 種類並べて同時に移動させた結果がこちら。プログラムはさいごに載せました。

ちなみに、Timeline では LINEAR がデフォルト、Transition では EASE_BOTH がデフォルトになっています。

ここまでが前振りで、ここからが本題です。

この Interpolator は自分で任意の補間曲線を設定することができます。設定するには SPLINE メソッドか、TANGENT メソッドを使用します。ここでは、SPLINE メソッドを使用します。

SPLINE メソッドは開始点 (0, 0) と終了点 (1, 1) を通るスプライン曲線を補間曲線として設定するメソッドです。引数は 3 次スプライン曲線なので、引数は 2 つのコントロールポイントの座標になります。それを図示したのが、下図です。

たとえば、EASE_BOTH とは逆にロケットスタートして中だるみ、さいごにまたダッシュするような Inerpolator であれば、こんな感じで設定できます。

    Interpolator interpolator = Interpolator.SPLINE(0.2, 0.8, 0.8, 0.2);

で、なんで補間のことを話題にしているかというと、補間関数を下の図のようにして、予備動作を実現できないかと考えたわけです。

予備動作というのは英語だと anticipation といって、何らかの動作を行う前にする動作のことです。これはアニメーションのバイブルともいえる Disney Animation: The Illusion of Life に書かれている 12 の原則の 1 つなのです。下のリンクは英語版の方ですが、日本語版もあります。

The Illusion of Life (Disney Editions Deluxe)

The Illusion of Life (Disney Editions Deluxe)

下の図のように走り出すドナルドがちょっと後ろに体重をかけて、これからロケットスタートをするぞという動作です。アニメではよくある表現ですよね。

http://www.viz.tamu.edu/faculty/parke/ends489f00/section6/donald.gif
The Twelve Principles of Animationより引用

で、上の図のようにはじめマイナス側に補間する補間関数であれば、この予備動作ができるのではと思ったわけです。

で、やってみました。

    Interpolator interpolator = Interpolator.SPLINE(0.2, -1.0, 0.6, 0.6);

そうしたら、怒られちゃいました...

Caused by: java.lang.IllegalArgumentException: Control point coordinates must all be in range [0,1]
	at com.sun.scenario.animation.SplineInterpolator.<init>(SplineInterpolator.java:98)
	at javafx.animation.Interpolator.SPLINE(Interpolator.java:199)
	at InterpolatorDemo.start(InterpolatorDemo.java:26)

どうやら、コントロールポイントの座標は (0, 0) と (1, 1) の四角の中だけのようです。楽して予備動作をしようと思ったのに、残念。

もちろん、複数の KeyFrame、もしくは複数の Transition を SequentialTransition でつなげたアニメーションを使えば予備動作できるんですけどね。

簡単にできないかなぁと思ったわけです。まぁ、そんなに楽してはできないというのが、今日の教訓でした。

長くなってしまったわりには、内容がないエントリーだ ><


Interpolator Demonstration

JCConf で JavaFX について発表してきた

この記事は、JavaFX Advent Calendar 2015 の 8 日目の記事です。

昨日は id:aoe-tk さんの JavaFX9に追加される機能が増えるかもしれません でした。

台湾の JUG である TWJUG が主催している JCConf で CfP をしていたので、軽い気持ちで応募したら受かっちゃいました。JCConf は、JJUG でいうところの CCC のようなカンファレンスです。

日本でも話したことがないことを話すつもりはないので、Swing から JavaFXマイグレーションの話をすることにしました。

資料はもともと英語で作っているので、そのままでいいやと思っていたのですが... 見直してみたら、やっぱりここも直したい、あそこも直したいで半分ぐらい手を入れてしまいました。

そのせいで、喋りの準備が全然できなかったというのは、言い訳にしかすぎないんですけどね。

いちおう、原稿は作って、そのキーワードを抜き出したカンペを作ってスマホですぐに見られるようにしていたのですが、やっぱりそれを見る余裕はありませんでした ><

で、資料はこちら。

Swing がメインテナンスモードだし、もうこれから新しい機能は入らないから、Swing から JavaFX に移行することを考えようという内容です。

マイグレーションの例を 2 つ、ライブコーディングしようとしていたのですが、実際には 1 つ目の簡単な例だけでかなり時間を食ってしまって、2 つ目はライブではできませんでした。

そのせいもあって、後半はグダグダ。英語のプレゼンはもっと入念に練習しないとダメですね。

喋っている時は気がつかなかったのですが、Q&A の時に JRuby の Charles Nutter さんが聞いているのに気がつきました!!!

話している時に気がつかなくてよかった!気がついていたら、緊張してなおさらグダグダになってしまうところでした。

講演の後、ちょっとだけ話をして、おもしろかったといってもらえたので、よかったです。

ちなみに、台湾での JavaFX の認知度は低かったようです。というか、やっぱりサーバーサイドの人がほとんどで、Swing を使ったこともないという人ばかりでした。セッションの後、TWJUG の @kojilin さんとも喋ったのですが、台湾ではデスクトップアプリを Java で作るということはあまりないようですね。

まぁ、日本でもそんなにメジャーというわけではないですが、こういうセッションをやるとかなりの割合の人が Swing を使っていたりするのですが...

今日は JavaFX の技術的な話はぜんぜんないですけど、まぁこういうエントリーがあってもいいでしょう ^ ^;;

明日の Advent Calendar は空きなので、誰か埋めて!!