Java etc...

アクセスカウンタ

help RSS JavaFX の Bind で三角形の面積を求める... の続き(4) - マルチタッチに挑戦

<<   作成日時 : 2012/12/30 02:47   >>

ブログ気持玉 0 / トラックバック 0 / コメント 0

If you prefer to read this article in ENGLISH, go to [in English] page.

JavaFX の Bind で三角形の面積を求める... の続き(3) の続き。

とりあえず、ジェスチャイベントに対応させてみた。

三角形を指定するマルチタッチは 3 点固定のみ受け付けるようした。
3点をタッチすると、その位置に円を、円と円とのあいたを結んだ線が描画される。

そして、ズームと回転のジェスチャにも対応。2点のマルチタッチで動く。回転は、三角形の重心を中心にして回転させている。ズームは、頂点の円の大きさと、辺の線の太さを変えている。三角形の大きさは変えていない。

面倒だったのは、ムーズと回転をさせたあと、多くの場合に、MouseClicck のイベントハンドラが動いてしまうこと。そこで isSynthesized() が true のときは、イベント消費だけするようにしたら、改善された。

ちなみに、Windows 8 Pro のリモートデスクトップ環境で動作させた。具体的には、Windwos 8 Pro + Splashtop Streamer, iPad + Splashtop Win8 Metro Testbed。Splashtop は、2012/11/30 の JavaFX 勉強会の LT で @aoetk さんがデモで利用し、紹介されたツール。

回転とズーム、マルチタッチに対応した


おまけに、右下に回転角度と、ズーム率を、それぞれ変えるためのテキストフィールドとスライダも用意した。これは、マルチタッチ環境でなくても、ロジックの正しさを確認する)ために(デバッグしやすいように)仕込んだもの。回転のロジックは、同じメソッド(rotation)を呼び出している。

ソースはこれ ... 長い ....

今は、コメントを書いていないけど、後で追加するかも。

それより、インデントを &nbsp; でやると、文字数オーバーで投稿できない。pre タグも使えないので、全角空白文字で行っているので、Copy&Paste されるかたはご注意を。

import java.util.List;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.NumberBinding;
import javafx.beans.binding.When;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.concurrent.Task;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.geometry.Orientation;
import javafx.geometry.Pos;
import javafx.scene.Group;
import javafx.scene.GroupBuilder;
import javafx.scene.Scene;
import javafx.scene.SceneBuilder;
import javafx.scene.control.Label;
import javafx.scene.control.LabelBuilder;
import javafx.scene.control.Slider;
import javafx.scene.control.SliderBuilder;
import javafx.scene.control.TextField;
import javafx.scene.control.TextFieldBuilder;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.RotateEvent;
import javafx.scene.input.TouchEvent;
import javafx.scene.input.TouchPoint;
import javafx.scene.input.ZoomEvent;
import javafx.scene.layout.HBoxBuilder;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.CircleBuilder;
import javafx.scene.shape.Line;
import javafx.scene.shape.LineBuilder;
import javafx.scene.shape.Rectangle;
import javafx.scene.shape.RectangleBuilder;
import javafx.stage.Stage;
import javafx.util.StringConverter;

public class MultiTouch1 extends Application {
 final double PPI = 96.0;
 final double INCHI = 2.54;
 final double PPC = PPI/INCHI;

 final int WIDTH = 800;
 final int HEIGHT = 600;
 final int MARGIN = 5;
 final int OFFSET = 5;

 final int INNER_WIDTH = WIDTH - MARGIN * 2;
 final int INNER_HEIGHT = HEIGHT - MARGIN * 2;

 final int ORIGIN_X = MARGIN + OFFSET;
 final int ORIGIN_Y = INNER_HEIGHT - OFFSET;

 final double THIN_STROKE = 0.1;
 final double THICK_STROKE = 0.3;

 private int clickcount = 0;

 private Rectangle rectangle;
 private Circle c[] = new Circle[3];
 private Line l[] = new Line[3];
 private Line a_x_l[] = new Line[(int)(INNER_HEIGHT/PPC)];
 private Line a_y_l[] = new Line[(int)(INNER_WIDTH/PPC)];
 private TextField tx[] = new TextField[3];
 private TextField ty[] = new TextField[3];
 private TextField tr;
 private Label area_Label = new Label();
 private Slider slider;
 private Circle gp_c = CircleBuilder.create()
  .radius(0.0).fill(Color.RED).build();

 private DoubleProperty org_x[] =
  new SimpleDoubleProperty[3];

 private DoubleProperty org_y[]
   new SimpleDoubleProperty[3];

 private NumberBinding map_x[] = new NumberBinding[3];
 private NumberBinding map_y[] = new NumberBinding[3];
 private NumberBinding area;
 private NumberBinding gp_x;
 private NumberBinding gp_y;

 private DoubleProperty rotation_angle =
  new SimpleDoubleProperty(0.0);

 private Color color[] = { Color.RED, Color.BLUE, Color.GREEN };

 {
  rectangle = RectangleBuilder.create()
   .layoutX(MARGIN).layoutY(MARGIN)
   .width(INNER_WIDTH).height(INNER_HEIGHT)
   .fill(Color.LIGHTYELLOW)
   .build();

  slider = SliderBuilder.create()
   .min(0.0).max(10.0).value(1.0)
   .showTickMarks(true).showTickLabels(true)
   .majorTickUnit(1.0).blockIncrement(0.5)
   .orientation(Orientation.HORIZONTAL)
   .build();

  tr = TextFieldBuilder.create()
   .prefColumnCount(4)
   .text("0.0")
   .onAction(new EventHandler<ActionEvent>() {
    @Override public void handle(ActionEvent e) {
     double angle = Math.toRadians(
      Double.parseDouble(
       ((TextField)e.getSource()).getText()));
     rotation_angle.set(angle%360.0);
     rotation(angle);
    }
   })
   .build();

  for(int i = 0; i < 3; i++ ) {
   c[i] = CircleBuilder.create()
    .radius(0.0).fill(color[i]).build();

   l[i] = LineBuilder.create()
    .strokeWidth(0.0).stroke(Color.BLACK).build();

   org_x[i] = new SimpleDoubleProperty(ORIGIN_X);
   org_y[i] = new SimpleDoubleProperty(ORIGIN_Y);
   map_x[i] = org_x[i].subtract(ORIGIN_X).divide(PPC);
   map_y[i] = org_y[i].negate().add(ORIGIN_Y).divide(PPC);
   c[i].radiusProperty().bind(
       Bindings.multiply(slider.valueProperty(), 10.0));

   l[i].strokeWidthProperty().bind(
       Bindings.multiply(slider.valueProperty(), 5.0));

   tx[i] = TextFieldBuilder.create()
    .prefColumnCount(2).build();

   ty[i] = TextFieldBuilder.create()
    .prefColumnCount(2)build();
  }    

  for (int i = 0; i < a_x_l.length; i++) {
   double delta_y = (INNER_HEIGHT - 5) - (i+1) * (PPC);
   a_x_l[i] = new Line(
       MARGIN, delta_y, MARGIN + INNER_WIDTH, delta_y);
   a_x_l[i].setStrokeWidth(
          (i+1)%5==0 ? THICK_STROKE : THIN_STROKE);
  }

  for (int i = 0; i < a_y_l.length; i++) {
   double delta_x = (MARGIN + 5) + (i+1) * (PPC);
   a_y_l[i] = new Line(
      delta_x, MARGIN, delta_x, MARGIN + INNER_HEIGHT);
   a_y_l[i].setStrokeWidth(
          (i+1)%5==0 ? THICK_STROKE : THIN_STROKE);
  }

  gp_x = org_x[0].add(org_x[1]).add(org_x[2]).divide(3.0);
  gp_y = org_y[0].add(org_y[1]).add(org_y[2]).divide(3.0);

  gp_c.centerXProperty().bind(gp_x);
  gp_c.centerYProperty().bind(gp_y);
  gp_c.radiusProperty().bind(
        Bindings.min(slider.valueProperty(), 2.0));
 }

 StringConverter<Number> sc_x =
  new StringConverter<Number>() {
   @Override public Number fromString(String from) {
    double map_x = Double.parseDouble(from);
    double org_x = (MARGIN + 5) + (map_x*PPC);
    return new Double(org_x);
   }

   @Override public String toString(Number org_x) {
    double map_x = (org_x.doubleValue() - (MARGIN + 5))/PPC;
    return String.format("%2.1f", map_x);
   }
 };

 StringConverter<Number> sc_y =
  new StringConverter<Number>() {
   @Override public Number fromString(String from) {
    double map_y = Double.parseDouble(from);
    double org_y = (INNER_HEIGHT - 5) - (map_y*PPC);
    return new Double(org_y);
   }

   @Override public String toString(Number org_y) {
    double map_y = ((INNER_HEIGHT - 5) - org_y.doubleValue())/PPC;
    return String.format("%2.1f", map_y);
   }
 };

 StringConverter<Number> sc_angle =
  new StringConverter<Number>() {
   @Override public Number fromString(String from) {
    double angle = Double.parseDouble(from);
    double r_angle = Math.toRadians(angle%360.0);
    return new Double(r_angle);
   }

   @Override public String toString(Number r_angle) {
    double angle = Math.toDegrees(r_angle.doubleValue());
    return String.format("%2.1f", (angle%360.0));
   }
 };

 public MultiTouch1() {
  initialize();
    
  for(int i = 0; i < 3; i++) {
   int j = (i == 2) ? 0 : i+1;

   l[i].startXProperty().bind(c[i].centerXProperty());
   l[i].startYProperty().bind(c[i].centerYProperty());
   l[i].endXProperty().bind(c[j].centerXProperty());
   l[i].endYProperty().bind(c[j].centerYProperty());

   Bindings.bindBidirectional(
            c[i].centerXProperty(), org_x[i]);
   Bindings.bindBidirectional(
            c[i].centerYProperty(), org_y[i]);
   Bindings.bindBidirectional(
        tx[i].textProperty(), c[i].centerXProperty(), sc_x);
   Bindings.bindBidirectional(
        ty[i].textProperty(), c[i].centerYProperty(), sc_y);
   Bindings.bindBidirectional(
        tr.textProperty(), rotation_angle, sc_angle);
  }

  NumberBinding tmp_area =
   map_x[0].multiply(map_y[1])
    .add(map_x[1].multiply(map_y[2]))
    .add(map_x[2].multiply(map_y[0]))
    .subtract(map_x[0].multiply(map_y[2]))
    .subtract(map_x[1].multiply(map_y[0]))
    .subtract(map_x[2].multiply(map_y[1]))
    .divide(2.0);

  area = new When(tmp_area.lessThan(0))
       .then(tmp_area.negate())
       .otherwise(tmp_area);
    
  area_Label.textProperty()
   .bind(new SimpleStringProperty("Area = ")
    .concat(area.asString("%2.1f")));
 }

 private void initialize() {
  clickcount = 0;
 }

 @Override public void start(Stage stage) {
  final Group root;    
  Scene scene = SceneBuilder.create()
   .width(WIDTH).height(HEIGHT)
   .fill(Color.WHITE)
   .root(root = GroupBuilder.create()
   .children(rectangle)
   .build()
  ).build();
   
  root.getChildren().addAll(a_x_l);
  root.getChildren().addAll(a_y_l);
    
  root.getChildren().addAll(
   LineBuilder.create()
    .startX(MARGIN).startY(ORIGIN_Y)
    .endX(MARGIN + INNER_WIDTH).endY(ORIGIN_Y)
    .build(),
   LineBuilder.create()
    .startX(ORIGIN_X).startY(MARGIN)
    .endX(ORIGIN_X).endY(MARGIN + INNER_HEIGHT)
    .build()
  );

  root.getChildren().addAll(
   HBoxBuilder.create()
    .spacing(3).padding(new Insets(10, 10, 10, 10))
    .layoutX(10)
    .alignment(Pos.BOTTOM_CENTER)
    .children(
     LabelBuilder.create().text("A(").build(),
     tx[0],
     LabelBuilder.create().text(",").build(),
     ty[0],
     LabelBuilder.create().text("),").build(),
     LabelBuilder.create().text("B(").build(),
     tx[1],
     LabelBuilder.create().text(",").build(),
     ty[1],
     LabelBuilder.create().text("),").build(),
     LabelBuilder.create().text("C(").build(),
     tx[2],
     LabelBuilder.create().text(",").build(),
     ty[2],
     LabelBuilder.create().text(") ⇒ ").build(),
     area_Label
    )
    .build(),
   HBoxBuilder.create()
    .layoutX(INNER_WIDTH - 270).layoutY(INNER_HEIGHT - 30)
    .children(
     LabelBuilder.create().text("Rotate").build(),
     tr,
     LabelBuilder.create().text("Zoome").build(),
     slider
    )
    .build()
   );

  rectangle.setOnMouseClicked(new EventHandler<MouseEvent>() {
   @Override public void handle(MouseEvent e) {
    if(e.isSynthesized()) { e.consume(); return; }
    if (clickcount > 2) { initialize(); }

    final int i = clickcount;
    final double x = e.getSceneX();
    final double y = e.getSceneY();

    Task<Void> task = new Task<Void>() {
     @Override public Void call() {
      Platform.runLater(new Runnable() {
       @Override public void run() {
        if(i == 0) {
         root.getChildren()
           .removeAll(l[0], l[1], l[2], c[0], c[1], c[2], gp_c);
        }
        org_x[i].set(x);
        org_y[i].set(y);
        switch(i) {
         case 0:
          root.getChildren().add(c[0]);
          break;
         case 1:
          root.getChildren().remove(c[0]);
          root.getChildren().addAll(l[0], c[0], c[1]);
          break;
         case 2:
          root.getChildren().removeAll(c[0], c[1], l[0]);
          root.getChildren()
            .addAll(l[0], l[1], l[2], c[0], c[1], c[2], gp_c);
          break;
         default :
        }
       }
      });
      return null;
     }
    };
    new Thread(task).start();
    clickcount++;
    e.consume();
   }
  });

  rectangle.setOnTouchPressed(new EventHandler<TouchEvent>() {
   @Override public void handle(TouchEvent e) {
    int tc = e.getTouchCount();
    if(tc != 3) { e.consume(); return; }
    final List<TouchPoint> tps = e.getTouchPoints();
    Platform.runLater(new Runnable() {
     @Override public void run() {
      root.getChildren().removeAll(
          l[0], l[1], l[2], c[0], c[1], c[2], gp_c);
      int i = 0;
      for(TouchPoint p : tps) {
       org_x[i].set(p.getSceneX());
       org_y[i].set(p.getSceneY());
       i++;
      }
      root.getChildren().addAll(
          l[0], l[1], l[2], c[0], c[1], c[2], gp_c);
     }
    });
    clickcount += tc;
    e.consume();
   }
  });

  rectangle.setOnZoom(new EventHandler<ZoomEvent> (){
   @Override public void handle(ZoomEvent e) {
    double scale = e.getZoomFactor()
             * slider.valueProperty().get();
    slider.valueProperty().set(scale);
    e.consume();
   }
  });

  rectangle.setOnRotate(new EventHandler<RotateEvent>() {
   @Override public void handle(RotateEvent e) {
    double angle = e.getAngle()/20.0;
    rotation_angle.set(e.getTotalAngle()%360.0);
    rotation(angle);
    e.consume();
   }
  });

  stage.setTitle("Triangle Area");
  stage.setScene(scene);
  stage.show();
 }

 private void rotation(double angle) {
  double o_x[] = new double[3];
  double o_y[] = new double[3];
  double r_x[] = new double[3];
  double r_y[] = new double[3];
  double g_x = gp_x.doubleValue();
  double g_y = gp_y.doubleValue();
  for(int i = 0; i < 3; i++) {
   o_x[i] = org_x[i].get();
   o_y[i] = org_y[i].get();
   r_x[i] = (o_x[i]-g_x)*Math.cos(angle
        - (o_y[i]-g_y)*Math.sin(angle) + g_x;
   r_y[i] = (o_x[i]-g_x)*Math.sin(angle)
        + (o_y[i]-g_y)*Math.cos(angle) + g_y;
  }
  for(int i = 0; i < 3; i++) {
   org_x[i].set(r_x[i]);
   org_y[i].set(r_y[i]);
  }
 }

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


一連の JavaFX のお試しネタは、これで、ひとまずお終い。

なお、このソースを実行すると、ウィンドウのサイズは変更できるけど、それに合わせて座標系の範囲が拡大するということまではしてません。

テーマ

注目テーマ 一覧


月別リンク

ブログ気持玉

クリックして気持ちを伝えよう!
ログインしてクリックすれば、自分のブログへのリンクが付きます。
→ログインへ

トラックバック(0件)

タイトル (本文) ブログ名/日時

トラックバック用URL help


自分のブログにトラックバック記事作成(会員用) help

タイトル
本 文

コメント(0件)

内 容 ニックネーム/日時

コメントする help

ニックネーム
本 文
JavaFX の Bind で三角形の面積を求める... の続き(4) - マルチタッチに挑戦  Java etc.../BIGLOBEウェブリブログ