Как написать текст по кривой Безье?

Я хочу сделать что-то подобное в javafx 2.2 или, по крайней мере, в javafx 8. Я просмотрел Text javadoc и css ссылка без результатов.

enter image description here

Это можно сделать, показывая и svg в WebView. Но мое приложение должно отображать много текста с этим эффектом. WebView - слишком тяжелый компонент для рисования текста с этим эффектом.

Я задал тот же вопрос в технологической сети оракула.

Ответы

Ответ 1

Вот злоупотребление PathTransition, чтобы получить текст, построенный по Кривая Безье.

Программа позволяет перетаскивать контрольные точки для определения кривой, а затем печатать текст вдоль этой кривой. Символы в тексте расположены на одинаковом расстоянии, поэтому он лучше всего работает, если общая длина кривой близка к ширине текста с "нормальным" интервалом и не корректирует такие вещи, как кернинг.

В приведенных ниже образцах показано:

  • Изогнутый текст с эффектом glow.
  • Некоторые кривые тексты без эффекта.
  • Точки управления, используемые для определения кривого пути, текст без эффекта был нанесен вдоль.

Curved Text With GlowCurved TextCurve Manipulator

Решение было быстрым взломом, основанным на ответе на вопрос StackOverflow: CubicCurve JavaFX. Я уверен, что лучшее решение можно найти с большим количеством усилий, времени и навыков.

Поскольку программа основана на переходах, было бы очень легко принять ее, чтобы текст можно анимировать, чтобы следовать кривой, обертывая справа налево при переполнении (например, вы можете видеть в текст marquee или биржевой тикер).

Любой из стандартных эффектов JavaFX, таких как свечение, тени и т.д. и изменения шрифтов, может применяться для получения таких вещей, как теневой эффект от текста в стиле painttshop в вашем вопросе. Эффект свечения - хороший эффект для применения здесь, поскольку он тонко смягчает зубчатые края вокруг повернутых символов.

Также PathTransition, на основе этого решения, может принимать любую произвольную форму в качестве входного для пути, поэтому текст может следовать другим путям, а не только кубическим кривым.

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 и создать пользовательский PathLayout, который похож на FlowPane, но выставляет узлы по пути скорее чем прямая линия. Выделенные узлы формируются текстом node для каждого символа, аналогичным тому, что я сделал в своем ответе. Но даже тогда вы на самом деле не точно отображаете текст, потому что хотите учесть такие вещи, как пропорционально разнесенные буквы, kerning и т.д. Таким образом, для полной точности и точности вам потребуется реализовать свой собственный алгоритм компоновки текста низкого уровня. Если бы это был я, я бы пошел только на это, если бы "достаточно хорошее" решение, предоставленное в этом ответе с использованием PathTransitions, оказалось для вас не достаточно высоким.

Ответ 2

Вы можете использовать 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();
  }
}

Результат:

enter image description here

Это не оптимальное решение, так как 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();
  }
}