2013-06-25 12 views
15

Sto cercando di fare qualcosa di simile in javafx 2.2 o almeno in javafx 8. Ho sfogliato il Text javadoc e css reference senza risultati.Come scrivere il testo lungo una curva di Bezier?

enter image description here

E 'possibile fare questo effetto visualizzando e SVG in un WebView. Ma la mia applicazione deve mostrare molto testo con questo effetto. La WebView è un componente troppo pesante per disegnare un testo con questo effetto.

Ho chiesto la stessa domanda sullo oracle technology network.

+0

Non ho idea se questo sarà utile per voi. Ho dovuto spostare la grafica lungo una curva di Bézier in unity3d e abbiamo usato i tracciamenti di svg (perché avevamo svgs per iniziare). Questo è quello che abbiamo usato per costruire la nostra biblioteca; http://www.w3.org/TR/SVG/paths.html – MichelleJS

+0

@MichelleJS ringrazia, ma SVG non è realmente supportato in javafx. – gontard

+0

forse questo può aiutarti: https://forums.oracle.com/thread/1712335 – Sebastian

risposta

23

Ecco un abuso del PathTransition per ottenere il testo tracciato lungo un Bézier Curve.

Il programma consente di trascinare i punti di controllo intorno per definire una curva, quindi tracciare il testo lungo tale curva. I caratteri nel testo sono equidistanti, quindi funziona meglio se la lunghezza totale della curva corrisponde piuttosto alla larghezza del testo con spaziatura "normale" e non apporta modifiche per cose come il kerning.

I campioni che seguono mostrano:

  1. testo curvo con un effetto glow.
  2. Un testo curvo senza effetto applicato.
  3. I punti di manipolazione del controllo utilizzati per definire il percorso curvo del testo senza effetto sono stati tracciati lungo.

Curved Text With Glow Curved Text Curve Manipulator

La soluzione era un trucco veloce in base alla risposta alla domanda StackOverflow: CubicCurve JavaFX. Sono sicuro che una soluzione migliore potrebbe essere trovata con più sforzo, tempo e abilità.

Poiché il programma si basa sulle transizioni, sarebbe molto semplice adottarlo in modo che il testo possa essere animato per seguire la curva, avvolgendo da destra a sinistra in overflow (come si potrebbe vedere in marquee text o in un titolo di scorta).

È possibile applicare qualsiasi effetto JavaFX standard come bagliore, ombre, ecc. E modifiche ai font per ottenere effetti come l'effetto ombreggiato dal testo di paintshop pro nella domanda. Un effetto bagliore è un bell'effetto da applicare qui in quanto attenua sottilmente i bordi frastagliati attorno ai caratteri ruotati.

Anche PathTransition su cui si basa questa soluzione può assumere qualsiasi forma arbitraria come input per il percorso, in modo che il testo possa seguire altri tipi di percorsi, non solo curve cubiche.

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

Un'altra possibile soluzione sarebbe quella di misurare ogni carattere di testo e fare la matematica per interpolare la posizione del testo e la rotazione senza utilizzare un PathTransition. Ma PathTransition era già lì e ha funzionato bene per me (forse le misurazioni della distanza della curva per gli avanzamenti del testo potrebbero sfidarmi comunque).

risposte alle domande aggiuntive

Pensi che è possibile implementare un javafx.scene.effect.Effect adattando il codice?

No. L'implementazione di un effetto richiederebbe eseguire la matematica per la visualizzazione del testo lungo la curva di Bezier, che la mia risposta non fornisce (come è appena adotta il PathTransition esistente per fare questo).

Inoltre, non è presente alcuna API pubblica in JavaFX 2.2 per l'implementazione del proprio effetto personalizzato.

C'è un effetto DisplacementMap esistente che potrebbe essere utilizzato per ottenere qualcosa di simile. Tuttavia, ritengo che usare l'effetto DisplacementMap (e forse qualsiasi effetto per regolare il layout del testo) possa distorcere il testo.

IMO, la scrittura di testo lungo una curva di Bézier è più legata al layout di quella relativa all'effetto: è preferibile regolare il layout e la rotazione dei caratteri piuttosto che utilizzare un effetto per spostarli.

Oppure potrebbe esserci un modo migliore per integrarlo correttamente nel framework JFX?

Si potrebbe sottoclasse Pane e creare un PathLayout personalizzato che è simile a un FlowPane, ma delinea nodi lungo un percorso piuttosto che una linea retta. I nodi da tracciare sono formati da un nodo di testo per ogni carattere, simile a quello che ho fatto nella mia risposta. Ma anche in questo caso, non stai riproducendo il testo in modo accurato perché vuoi prendere in considerazione cose come le lettere proporzionalmente distanziate, kerning ecc. Quindi, per una fedeltà e una precisione totali, è necessario implementare il proprio algoritmo di layout di testo di basso livello. Se fossi in me, andrei solo a questo sforzo se la soluzione "abbastanza buona" fornita in questa risposta usando PathTransitions risultasse non sufficientemente alta per te.

+0

Bella risposta, grazie! Pensi che sia possibile implementare un javafx.scene.effect.Effect adattando il tuo codice? O potrebbe esserci un modo migliore per integrarlo correttamente nel framework JFX? – gontard

+0

@gontard Ho modificato la mia risposta per rispondere alle vostre domande aggiuntive. – jewelsea

+0

Grazie per queste precisioni e la tua risposta molto completa. Penso che creerò un pannello personalizzato, basato su AnimationPathHelper o PathIterator. Grazie ancora per aver dedicato del tempo. – gontard

7

È possibile utilizzare la WebView e alcuni html per visualizzare un svg. Ecco un esempio:

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

Risultato:

enter image description here

Questa non è una soluzione ottimale in quanto la WebView JavaFX si comporta un po 'permaloso quando dovrebbe comportarsi come un'etichetta nella mia esperienza, ma è qualcosa per cominciare.

EDIT

Dal momento che non si desidera utilizzare direttamente WebView, è possibile utilizzare una singola istanza di un WebView di rendere la scena con html e poi prendere una fotografia istantanea di esso per produrre un ImageView. Vedere questo esempio:

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

Grazie per il tuo contributo.Avrei dovuto dire che l'utilizzo di una WebView non è accettabile in quanto la mia applicazione dovrà visualizzare molto testo con questo effetto. La WebView è un componente troppo pesante per disegnare un testo con questo effetto. – gontard

+0

@gontard vedi la mia modifica – Sebastian

Problemi correlati