FXEyes

id:aoe-tk さんが xeyes を JavaFX で作られていましたが、それにインスパイアされたので、私も作ってみました。

AOEの日記: JavaFXでxeyes作ってみました

私の方は以前、JavaFX 1.x で作ったもの をベースにしてあります。

JavaFX 1.x だと JavaFX Script で bind が簡単に使えたので楽だったのですが、JavaFX 2.0 の bind が使いにくくなっているのがつらいですね。自分的には、座標計算の部分が冗長なので、もうちょっと見直してみたいと思ってます。

また、ウィンドウをドラッグして移動するのと、終了のためのポップアップメニューを出す部分ができてません。ここらへんも、JavaFX 1.x と異なっているので、意外にやりにくいです。特にドラッグは全然違います。困ったww

また、終了する時に、JavaFX 側は落ちるのですが、Swing の EDT が落ちないという不具合があります。これもなるべく早く修正しないと。

とりあえず、現状のコードを張っておきます。ある程度できたら GitHub にでもあげようと思ってます。

ちなみに、 id:aoe-tk さんが指摘しているように、現在の方式だと Mac だと動かないかもしれません。

まずは本体部分の FXEyes クラス。

package net.javainthebox.fxeyes;

import java.awt.MouseInfo;
import java.awt.PointerInfo;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import javax.swing.SwingUtilities;
import javax.swing.Timer;

public class FXEyes extends Application {
    private Eyes eyes;

    @Override
    public void start(Stage stage) throws Exception {
        stage.initStyle(StageStyle.TRANSPARENT);
        
        Group root = new Group();
        Scene scene = new Scene(root, 200, 200);
        scene.setFill(null);
        
        eyes = new Eyes(scene.getWidth(), scene.getHeight());
        eyes.locationXProperty().bind(stage.xProperty());
        eyes.locationYProperty().bind(stage.yProperty());
        
        root.getChildren().add(eyes);

        stage.setScene(scene);
        stage.show();
        
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                Timer timer = new Timer(50, new ActionListener() {
                    @Override
                    public void actionPerformed(ActionEvent e) {
                        PointerInfo info = MouseInfo.getPointerInfo();
                        eyes.setMouseLocation(info.getLocation().getX(), info.getLocation().getY());
                    }
                });
                timer.start();
            }
        });
    }

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

次に目の部分の Eyes クラスです。

package net.javainthebox.fxeyes;

import javafx.beans.binding.DoubleBinding;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.geometry.Point2D;
import javafx.scene.Parent;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Ellipse;

public class Eyes extends Parent {
    
    private DoubleProperty locationX = new SimpleDoubleProperty();
    private DoubleProperty locationY = new SimpleDoubleProperty();
    private double width;
    private double height;
    private double strokeWidth;
    private double blackEyeRadius;
    private DoubleProperty mouseX = new SimpleDoubleProperty();
    private DoubleProperty mouseY = new SimpleDoubleProperty();
    
    private DoubleBinding leftX = new DoubleBinding() {
        {
            super.bind(locationX, locationY, mouseX, mouseY);
        }

        @Override
        protected double computeValue() {
            double x = mouseX.get() - locationX.get();
            double y = -mouseY.get() + locationY.get() + height;
            double cx = width / 4;
            double cy = height / 2;

            return calcurateEyePosition(x, y, cx, cy).getX();
        }
    };
    
    private DoubleBinding leftY = new DoubleBinding() {
        {
            super.bind(locationX, locationY, mouseX, mouseY);
        }

        @Override
        protected double computeValue() {
            double x = mouseX.get() - locationX.get();
            double y = -mouseY.get() + locationY.get() + height;
            double cx = width / 4;
            double cy = height / 2;

            return calcurateEyePosition(x, y, cx, cy).getY();
        }
    };
    
    private DoubleBinding rightX = new DoubleBinding() {
        {
            super.bind(locationX, locationY, mouseX, mouseY);
        }

        @Override
        protected double computeValue() {
            double x = mouseX.get() - locationX.get();
            double y = -mouseY.get() + locationY.get() + height;
            double cx = width / 4 * 3;
            double cy = height / 2;

            return calcurateEyePosition(x, y, cx, cy).getX();
        }
    };
    private DoubleBinding rightY = new DoubleBinding() {

        {
            super.bind(locationX, locationY, mouseX, mouseY);
        }

        @Override
        protected double computeValue() {
            double x = mouseX.get() - locationX.get();
            double y = -mouseY.get() + locationY.get() + height;
            double cx = width / 4 * 3;
            double cy = height / 2;

            return calcurateEyePosition(x, y, cx, cy).getY();
        }
    };

    public Eyes(double width, double height) {
        this.width = width;
        this.height = height;

        if (width < height) {
            strokeWidth = (width > 100) ? 10 : width / 10;
        } else {
            strokeWidth = (height > 100) ? 10 : height / 10;
        }
        strokeWidth = (strokeWidth < 1) ? 1 : strokeWidth;
        blackEyeRadius = strokeWidth;

        createEyes();
    }

    public void setMouseLocation(double x, double y) {
        this.mouseX.set(x);
        this.mouseY.set(y);
    }

    public DoubleProperty locationXProperty() {
        return locationX;
    }

    public DoubleProperty locationYProperty() {
        return locationY;
    }

    private Point2D calcurateEyePosition(double x, double y, double cx, double cy) {
        // 角度は arctan で求めます
        double theta = Math.atan2(y - cy, x - cx);
        double hr = width / 4 - strokeWidth - blackEyeRadius;

        double eyeX;
        double eyeY;

        // マウスカーソルの位置が目の内部だったら、カーソルの位置に目玉を描画
        if (Math.abs(x - cx) > Math.abs(hr * Math.cos(theta))) {
            eyeX = cx + hr * Math.cos(theta);
        } else {
            eyeX = x;
        }

        double vr = height / 2 - strokeWidth - blackEyeRadius;
        if (Math.abs(y - cy) > Math.abs(vr * Math.sin(theta))) {
            eyeY = height - cy - vr * Math.sin(theta);
        } else {
            eyeY = height - y;
        }

        return new Point2D(eyeX, eyeY);
    }

    private void createEyes() {
        // Left Eye
        Ellipse left = new Ellipse();
        left.setCenterX(width / 4);
        left.setCenterY(height / 2);
        left.setRadiusX(width / 4 - strokeWidth / 2);
        left.setRadiusY(height / 2 - strokeWidth / 2);
        left.setStroke(Color.BLACK);
        left.setStrokeWidth(strokeWidth);
        left.setFill(Color.WHITE);
        getChildren().add(left);

        // Left Black Eye
        Circle leftBlackEye = new Circle();
        leftBlackEye.centerXProperty().bind(leftX);
        leftBlackEye.centerYProperty().bind(leftY);
        leftBlackEye.setRadius(blackEyeRadius);
        leftBlackEye.setFill(Color.BLACK);
        getChildren().add(leftBlackEye);

        // Right Eye
        Ellipse right = new Ellipse();
        right.setCenterX(width / 4 * 3);
        right.setCenterY(height / 2);
        right.setRadiusX(width / 4 - strokeWidth / 2);
        right.setRadiusY(height / 2 - strokeWidth / 2);
        right.setStroke(Color.BLACK);
        right.setStrokeWidth(strokeWidth);
        right.setFill(Color.WHITE);
        getChildren().add(right);

        // Left Black Eye
        Circle rightBlackEye = new Circle();
        rightBlackEye.centerXProperty().bind(rightX);
        rightBlackEye.centerYProperty().bind(rightY);
        rightBlackEye.setRadius(blackEyeRadius);
        rightBlackEye.setFill(Color.BLACK);
        getChildren().add(rightBlackEye);
    }
}

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

f:id:skrb:20111227080604j:image