Jak pisać tekst wzdłuż krzywej Beziera?

Szukam czegoś takiego w javafx 2.2 lub przynajmniej w javafx 8. Przeglądałem Tekst javadoci CSS reference bez wyników.

Tutaj wpisz opis obrazka

Możliwe jest wykonanie tego efektu poprzez wyświetlenie i svg w widoku WebView . Ale moja aplikacja musi wyświetlać dużo tekstu z tym efektem. WebView jest zbyt ciężkim komponentem do rysowania tekstu z takim efektem.

Zadałem to samo pytanie na wyrocznia sieć technologiczna .

Author: Cœur, 2013-06-25

2 answers

Tutaj jest nadużycie PathTransition aby uzyskać tekst wykreślony wzdłuż krzywej Béziera.

Program umożliwia przeciąganie punktów kontrolnych w celu zdefiniowania krzywej, a następnie rysowanie tekstu wzdłuż tej krzywej. Znaki w tekście są rozmieszczone równomiernie, więc najlepiej sprawdza się, jeśli całkowita długość krzywej jest zbliżona do szerokości tekstu z "normalnymi" odstępami i nie wprowadza korekt dla takich rzeczy, jak kerning.

Poniższe próbki pokazują:

  1. zakrzywione tekst z efektem blasku .
  2. jakiś zakrzywiony tekst bez efektu.
  3. punkty kontroli użyte do zdefiniowania zakrzywionej ścieżki, wzdłuż której wykreślono tekst bez efektu.

Zakrzywiony Tekst Z BlaskiemZakrzywiony TekstManipulator Krzywej

Rozwiązaniem był szybki hack oparty na odpowiedzi na pytanie StackOverflow: Cubiccurve JavaFX . Jestem pewien, że lepsze rozwiązanie można znaleźć przy większym wysiłku, czasie i umiejętnościach.

Ponieważ program oparty jest na przejścia, bardzo łatwo byłoby go zastosować, aby tekst mógł być animowany zgodnie z krzywą, owijając od prawej do lewej na przepełnieniu(jak można zobaczyć w tekst markizy lub znacznik akcji).

Każdy ze standardowych efektów JavaFX, takich jak świecenie, cienie itp.i zmiany czcionek, może być zastosowany, aby uzyskać efekt cienia z tekstu paintshop pro w twoim pytaniu. Efekt blasku jest przyjemny do zastosowania tutaj, ponieważ subtelnie zmiękcza postrzępione krawędzie wokół obrócone znaki.

Również PathTransition, na którym opiera się to rozwiązanie, może przyjmować dowolny dowolny kształt jako Wejście dla ścieżki, więc tekst może podążać za innymi rodzajami ścieżek, a nie tylko krzywiznami sześciennymi.

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());
            }
        }
    }
}

Innym możliwym rozwiązaniem byłoby zmierzenie każdego znaku tekstu i wykonanie matematyki w celu interpolacji położenia i obrotu tekstu bez użycia PathTransition. Ale PathTransition już tam było i działało dobrze dla mnie (może pomiary odległości krzywej dla tekstu zaliczki i tak mogą mnie wyzwać).

odpowiedzi na dodatkowe pytania

Czy uważasz, że możliwe jest zaimplementowanie javafx.miejsce.efekt.Efekt poprzez dostosowanie kodu?

Nie. Zaimplementowanie efektu wymagałoby wykonania matematyki do wyświetlania tekstu wzdłuż krzywej Beziera, czego moja odpowiedź nie dostarcza(ponieważ tylko przyjmuje do tego istniejącą ścieżkę).

Poza tym w JavaFX 2.2 nie ma publicznego API dla wdrożenie własnego efektu niestandardowego.

Istnieje istniejący efekt DisplacementMap , który może być użyty do uzyskania czegoś podobnego. Jednak uważam, że użycie efektu DisplacementMap (i być może jakiegokolwiek efektu do dostosowania układu tekstu) prawdopodobnie zniekształciłoby tekst.

IMO pisanie tekstu wzdłuż krzywej Beziera jest bardziej związane z układem niż z efektem - najlepiej jest dostosować układ i obrót znaków, zamiast używać efektu do ich przenoszenia w pobliżu.

A może jest lepszy sposób na poprawną integrację w frameworku JFX ?

Można podklasować panel i utworzyć niestandardowy PathLayout, który jest podobny do strumienia przepływu , ale rozkłada węzły wzdłuż ścieżki, a nie linii prostej. Węzły, które mają być ułożone, są tworzone przez węzeł tekstowy dla każdego znaku, podobny do tego, co zrobiłem w mojej odpowiedzi. Ale nawet wtedy, nie bardzo dokładnie renderujesz tekst, ponieważ chcesz wziąć w konta takie jak litery przestrzenne, kerning itp. Tak więc, aby uzyskać całkowitą wierność i dokładność, trzeba zaimplementować własny algorytm układu tekstu niskiego poziomu. Gdybym to był ja, poszedłbym do tego wysiłku tylko wtedy, gdyby" wystarczająco dobre " rozwiązanie podane w tej odpowiedzi przy użyciu PathTransitions okazało się nie być wystarczająco wysokiej jakości dla Ciebie.

 26
Author: jewelsea,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 12:24:25

Możesz użyć WebView i trochę html, aby wyświetlić svg. Oto przykład:

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();
  }
}

Wynik:

Tutaj wpisz opis obrazka

Nie jest to optymalne rozwiązanie, ponieważ JavaFX WebView zachowuje się trochę drażliwie, kiedy powinien zachowywać się jak etykieta z mojego doświadczenia, ale jest to coś na początek.

EDIT

Ponieważ nie chcesz używać widoku sieci Web bezpośrednio, możesz użyć pojedynczej instancji widoku sieci Web do renderowania sceny za pomocą html, a następnie zrobić jej migawkę stwórz obraz. Zobacz ten przykład:

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();
  }
}
 8
Author: zhujik,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-06-28 14:21:17