18

私はjavafx 2.2または少なくともjavafx 8でこのようなことをしようとしています.結果なしでテキストjavadoccssリファレンスを閲覧しました.

ここに画像の説明を入力

WebViewで表示および svg することで、この効果を実現できます。しかし、私のアプリケーションは、この効果で多くのテキストを表示する必要があります。WebView は、この効果でテキストを描画するには重すぎるコンポーネントです。

オラクルテクノロジーネットワークで同じ質問をしました。

4

2 に答える 2

26

これは、ベジエ曲線に沿ってテキストをプロットするためのPathTransitionの悪用です。

このプログラムでは、制御点をドラッグして曲線を定義し、その曲線に沿ってテキストをプロットできます。テキスト内の文字は等間隔で配置されているため、曲線の全長が「通常の」間隔でテキスト幅にかなり近く一致し、カーニングなどの調整を行わない場合に最適に機能します。

以下のサンプルは以下を示しています。

  1. グロー効果のある湾曲したテキスト。
  2. 効果が適用されていない一部の湾曲したテキスト。
  3. 効果のないテキストがプロットされる曲線パスを定義するために使用されるコントロール操作ポイント。

グロー付きの湾曲したテキスト 湾曲したテキスト カーブ マニピュレータ

解決策は、StackOverflow の質問への回答に基づく簡単なハックでした: CubicCurve JavaFX。より多くの努力、時間、スキルがあれば、より良い解決策が見つかると確信しています。

このプログラムはトランジションに基づいているため、テキストが曲線に沿ってアニメーション化され、オーバーフロー時に右から左に折り返されるようにするのは非常に簡単です (マーキー テキストや株式ティッカーで見られるように)。

グロー、シャドウなどの標準的な JavaFX 効果やフォントの変更を適用して、質問のペイントショップ プロ テキストからシャドウ効果などを取得できます。グロー効果は、回転した文字の周りのギザギザのエッジを微妙に柔らかくするので、ここで適用するのに適した効果です。

また、このソリューションが基づいている PathTransition は、パスの入力として任意の形状を取ることができるため、テキストは 3 次曲線だけでなく、他の種類のパスをたどることができます。

import javafx.animation.*;
import javafx.application.Application;
import javafx.beans.property.DoubleProperty;
import javafx.collections.*;
import javafx.event.*;
import javafx.scene.*;
import javafx.scene.control.ToggleButton;
import javafx.scene.effect.Glow;
import javafx.scene.input.MouseEvent;
import javafx.scene.paint.Color;
import javafx.scene.shape.*;
import javafx.scene.text.Text;
import javafx.stage.Stage;
import javafx.util.Duration;

/**
 * Example of drawing text along a cubic curve.
 * Drag the anchors around to change the curve.
 */
public class BezierTextPlotter extends Application {
    private static final String CURVED_TEXT = "Bézier Curve";

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

    @Override
    public void start(final Stage stage) throws Exception {
        final CubicCurve curve = createStartingCurve();

        Line controlLine1 = new BoundLine(curve.controlX1Property(), curve.controlY1Property(), curve.startXProperty(), curve.startYProperty());
        Line controlLine2 = new BoundLine(curve.controlX2Property(), curve.controlY2Property(), curve.endXProperty(), curve.endYProperty());

        Anchor start = new Anchor(Color.PALEGREEN, curve.startXProperty(), curve.startYProperty());
        Anchor control1 = new Anchor(Color.GOLD, curve.controlX1Property(), curve.controlY1Property());
        Anchor control2 = new Anchor(Color.GOLDENROD, curve.controlX2Property(), curve.controlY2Property());
        Anchor end = new Anchor(Color.TOMATO, curve.endXProperty(), curve.endYProperty());

        final Text text = new Text(CURVED_TEXT);
        text.setStyle("-fx-font-size: 40px");
        text.setEffect(new Glow());
        final ObservableList<Text> parts = FXCollections.observableArrayList();
        final ObservableList<PathTransition> transitions = FXCollections.observableArrayList();
        for (char character : text.textProperty().get().toCharArray()) {
            Text part = new Text(character + "");
            part.setEffect(text.getEffect());
            part.setStyle(text.getStyle());
            parts.add(part);
            part.setVisible(false);

            transitions.add(createPathTransition(curve, part));
        }

        final ObservableList<Node> controls = FXCollections.observableArrayList();
        controls.setAll(controlLine1, controlLine2, curve, start, control1, control2, end);

        final ToggleButton plot = new ToggleButton("Plot Text");
        plot.setOnAction(new PlotHandler(plot, parts, transitions, controls));

        Group content = new Group(controlLine1, controlLine2, curve, start, control1, control2, end, plot);
        content.getChildren().addAll(parts);

        stage.setTitle("Cubic Curve Manipulation Sample");
        stage.setScene(new Scene(content, 400, 400, Color.ALICEBLUE));
        stage.show();
    }

    private PathTransition createPathTransition(CubicCurve curve, Text text) {
        final PathTransition transition = new PathTransition(Duration.seconds(10), curve, text);

        transition.setAutoReverse(false);
        transition.setCycleCount(PathTransition.INDEFINITE);
        transition.setOrientation(PathTransition.OrientationType.ORTHOGONAL_TO_TANGENT);
        transition.setInterpolator(Interpolator.LINEAR);

        return transition;
    }

    private CubicCurve createStartingCurve() {
        CubicCurve curve = new CubicCurve();
        curve.setStartX(50);
        curve.setStartY(200);
        curve.setControlX1(150);
        curve.setControlY1(300);
        curve.setControlX2(250);
        curve.setControlY2(50);
        curve.setEndX(350);
        curve.setEndY(150);
        curve.setStroke(Color.FORESTGREEN);
        curve.setStrokeWidth(4);
        curve.setStrokeLineCap(StrokeLineCap.ROUND);
        curve.setFill(Color.CORNSILK.deriveColor(0, 1.2, 1, 0.6));
        return curve;
    }

    class BoundLine extends Line {
        BoundLine(DoubleProperty startX, DoubleProperty startY, DoubleProperty endX, DoubleProperty endY) {
            startXProperty().bind(startX);
            startYProperty().bind(startY);
            endXProperty().bind(endX);
            endYProperty().bind(endY);
            setStrokeWidth(2);
            setStroke(Color.GRAY.deriveColor(0, 1, 1, 0.5));
            setStrokeLineCap(StrokeLineCap.BUTT);
            getStrokeDashArray().setAll(10.0, 5.0);
        }
    }

    // a draggable anchor displayed around a point.
    class Anchor extends Circle {
        Anchor(Color color, DoubleProperty x, DoubleProperty y) {
            super(x.get(), y.get(), 10);
            setFill(color.deriveColor(1, 1, 1, 0.5));
            setStroke(color);
            setStrokeWidth(2);
            setStrokeType(StrokeType.OUTSIDE);

            x.bind(centerXProperty());
            y.bind(centerYProperty());
            enableDrag();
        }

        // make a node movable by dragging it around with the mouse.
        private void enableDrag() {
            final Delta dragDelta = new Delta();
            setOnMousePressed(new EventHandler<MouseEvent>() {
                @Override
                public void handle(MouseEvent mouseEvent) {
                    // record a delta distance for the drag and drop operation.
                    dragDelta.x = getCenterX() - mouseEvent.getX();
                    dragDelta.y = getCenterY() - mouseEvent.getY();
                    getScene().setCursor(Cursor.MOVE);
                }
            });
            setOnMouseReleased(new EventHandler<MouseEvent>() {
                @Override
                public void handle(MouseEvent mouseEvent) {
                    getScene().setCursor(Cursor.HAND);
                }
            });
            setOnMouseDragged(new EventHandler<MouseEvent>() {
                @Override
                public void handle(MouseEvent mouseEvent) {
                    double newX = mouseEvent.getX() + dragDelta.x;
                    if (newX > 0 && newX < getScene().getWidth()) {
                        setCenterX(newX);
                    }
                    double newY = mouseEvent.getY() + dragDelta.y;
                    if (newY > 0 && newY < getScene().getHeight()) {
                        setCenterY(newY);
                    }
                }
            });
            setOnMouseEntered(new EventHandler<MouseEvent>() {
                @Override
                public void handle(MouseEvent mouseEvent) {
                    if (!mouseEvent.isPrimaryButtonDown()) {
                        getScene().setCursor(Cursor.HAND);
                    }
                }
            });
            setOnMouseExited(new EventHandler<MouseEvent>() {
                @Override
                public void handle(MouseEvent mouseEvent) {
                    if (!mouseEvent.isPrimaryButtonDown()) {
                        getScene().setCursor(Cursor.DEFAULT);
                    }
                }
            });
        }

        // records relative x and y co-ordinates.
        private class Delta {
            double x, y;
        }
    }

    // plots text along a path defined by provided bezier control points.
    private static class PlotHandler implements EventHandler<ActionEvent> {
        private final ToggleButton plot;
        private final ObservableList<Text> parts;
        private final ObservableList<PathTransition> transitions;
        private final ObservableList<Node> controls;

        public PlotHandler(ToggleButton plot, ObservableList<Text> parts, ObservableList<PathTransition> transitions, ObservableList<Node> controls) {
            this.plot = plot;
            this.parts = parts;
            this.transitions = transitions;
            this.controls = controls;
        }

        @Override
        public void handle(ActionEvent actionEvent) {
            if (plot.isSelected()) {
                for (int i = 0; i < parts.size(); i++) {
                    parts.get(i).setVisible(true);
                    final Transition transition = transitions.get(i);
                    transition.stop();
                    transition.jumpTo(Duration.seconds(10).multiply((i + 0.5) * 1.0 / parts.size()));
                    // just play a single animation frame to display the curved text, then stop
                    AnimationTimer timer = new AnimationTimer() {
                        int frameCounter = 0;

                        @Override
                        public void handle(long l) {
                            frameCounter++;
                            if (frameCounter == 1) {
                                transition.stop();
                                stop();
                            }
                        }
                    };
                    timer.start();
                    transition.play();
                }
                plot.setText("Show Controls");
            } else {
                plot.setText("Plot Text");
            }

            for (Node control : controls) {
                control.setVisible(!plot.isSelected());
            }

            for (Node part : parts) {
                part.setVisible(plot.isSelected());
            }
        }
    }
}

別の可能な解決策は、各テキスト文字を測定し、数学を実行して、PathTransition を使用せずにテキストの位置と回転を補間することです。しかし、PathTransition は既にそこにあり、私にとってはうまく機能していました (おそらく、テキスト アドバンスの曲線距離の測定は、とにかく私に挑戦するかもしれません)。

追加の質問への回答

コードを変更して javafx.scene.effect.Effect を実装することは可能だと思いますか?

いいえ。効果を実装するには、ベジェ曲線に沿ってテキストを表示するための数学を実行する必要がありますが、これは私の答えでは提供されません (これを行うために既存の PathTransition を採用するだけなので)。

さらに、JavaFX 2.2 には、独自のカスタム効果を実装するためのパブリック API はありません。

おそらく似たようなものを得るために使用できる既存のDisplacementMap効果があります。ただし、DisplacementMap 効果 (およびおそらくテキスト レイアウトを調整するための効果) を使用すると、テキストが歪む可能性が高いと思います。

IMO、ベジエ曲線に沿ってテキストを書くことは、効果に関連するよりもレイアウトに関連しています-効果を使用して文字を移動するのではなく、文字のレイアウトと回転を調整するのが最善です。

それとも、JFX フレームワークに適切に統合するためのより良い方法があるのでしょうか?

Pane をサブクラス化し、 FlowPaneに似たカスタム PathLayout を作成できますが、直線ではなくパスに沿ってノードをレイアウトします。レイアウトするノードは、回答で行ったのと同様に、各文字の Text ノードによって形成されます。しかし、それでも、プロポーショナルな間隔の文字、カーニングなどを考慮したいため、テキストを実際には正確にレンダリングしていません。したがって、完全な忠実度と正確さを得るには、独自の低レベルのテキスト レイアウト アルゴリズムを実装する必要があります。もしそれが私だったら、PathTransitions を使用してこの回答で提供された「十分に良い」ソリューションがあなたにとって十分な品質ではないことが判明した場合にのみ、その努力に行きます。

于 2013-06-28T22:43:22.903 に答える
8

WebView といくつかの html を使用して svg を表示できます。次に例を示します。

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.StackPane;
import javafx.scene.web.WebView;
import javafx.stage.Stage;

public class CurvedText extends Application {

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

  @Override
  public void start(Stage primaryStage) throws Exception {
    StackPane root = new StackPane();
    WebView view = new WebView();
    view.getEngine().loadContent("<!DOCTYPE html>\n" +
            "<html xmlns=\"http://www.w3.org/1999/xhtml\">\n" +
            "  <body>\n" +
            "<embed width=\"100\" height=\"100\" type=\"image/svg+xml\" src=\"path.svg\">\n" +
            "  <svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\">" +
            "<defs>\n" +
            "  <path id=\"textPath\" d=\"M10 50 C10 0 90 0 90 50\"/>\n" +
            "</defs>\n"+
            "<text fill=\"red\">\n" +
            "  <textPath xlink:href=\"#textPath\">Text on a Path</textPath>\n" +
            "</text>" +
            "</svg>\n" +
            "</embed>" +
            "  </body>\n" +
            "</html>");
    root.getChildren().add(view);
    Scene scene = new Scene(root, 500, 500);
    primaryStage.setScene(scene);
    primaryStage.show();
  }
}

結果:

ここに画像の説明を入力

私の経験では、JavaFX WebView はラベルのように振る舞うべきときに少し厄介な振る舞いをするため、これは最適な解決策ではありませんが、それは最初にすべきことです。

編集

WebView を直接使用したくないため、WebView の単一のインスタンスを使用して html でシーンをレンダリングし、そのスナップショットを取得して ImageView を生成できます。次の例を参照してください。

import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.concurrent.Worker;
import javafx.scene.Scene;
import javafx.scene.image.ImageView;
import javafx.scene.image.WritableImage;
import javafx.scene.layout.HBox;
import javafx.scene.web.WebView;
import javafx.stage.Stage;

public class CurvedText extends Application {

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

  @Override
  public void start(Stage primaryStage) throws Exception {
    final HBox root = new HBox();
    final WebView view = new WebView();
    view.getEngine().loadContent("<!DOCTYPE html>\n" +
            "<html xmlns=\"http://www.w3.org/1999/xhtml\">\n" +
            "  <body>\n" +
            "<embed width=\"100\" height=\"100\" type=\"image/svg+xml\" src=\"path.svg\">\n" +
            "  <svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\">" +
            "<defs>\n" +
            "  <path id=\"textPath\" d=\"M10 50 C10 0 90 0 90 50\"/>\n" +
            "</defs>\n"+
            "<text fill=\"red\">\n" +
            "  <textPath xlink:href=\"#textPath\">Text on a Path</textPath>\n" +
            "</text>" +
            "</svg>\n" +
            "</embed>" +
            "  </body>\n" +
            "</html>");
    root.getChildren().add(view);
    view.getEngine().getLoadWorker().stateProperty().addListener(new ChangeListener<Worker.State>() {
      @Override
      public void changed(ObservableValue<? extends Worker.State> arg0, Worker.State oldState, Worker.State newState) {
        if (newState == Worker.State.SUCCEEDED) {
          // workaround for https://javafx-jira.kenai.com/browse/RT-23265
          AnimationTimer waitForViewToBeRendered = new AnimationTimer(){
            private int frames = 0;
            @Override
            public void handle(long now) {
              if (frames++ > 3){
                WritableImage snapshot = view.snapshot(null, null);
                ImageView imageView = new ImageView(snapshot);
                root.getChildren().add(imageView);
                this.stop();
              }
            }
          };
          waitForViewToBeRendered.start();
        }
      }
    });
    Scene scene = new Scene(root, 500, 500);
    primaryStage.setScene(scene);
    primaryStage.show();
  }
}
于 2013-06-28T13:26:32.280 に答える