2013-06-25 42 views
15

我期待在javafx 2.2或至少在javafx 8中做類似的事情。我瀏覽了Text javadoccss reference,結果沒有。如何沿貝塞爾曲線書寫文字?

enter image description here

有可能通過在WebView顯示和SVG做到這個效果。但是我的應用程序必須顯示大量帶有這種效果的文本。 WebView是用於繪製具有此效果的文本的過重組件。

我在oracle technology network上問了同樣的問題。

+0

我不知道這是否對你有幫助。我必須在unity3d中沿着貝塞爾曲線移動圖形,並使用svg tracepaths(因爲我們有svgs開始)。這就是我們用來建立我們的圖書館; http://www.w3.org/TR/SVG/paths.html – MichelleJS

+0

@MichelleJS謝謝,但SVG在javafx中並不真正支持。 – gontard

+0

也許這可能會幫助你:https://forums.oracle.com/thread/1712335 – Sebastian

回答

23

這裏是PathTransition的濫用讓文本沿着Bézier Curve繪製。

該程序可讓您拖動控制點以定義曲線,然後沿該曲線繪製文本。文本中的字符等距離排列,因此如果曲線的總長度與「正常」間距非常接近文本寬度,並且不會對字距調整等進行調整,則效果最佳。

下圖顯示了樣本:

  1. glow效果弧形文本。
  2. 一些曲線文本沒有應用效果。
  3. 用於定義曲線路徑的控制操縱點不帶任何效果的文字被繪製在一起。

Curved Text With Glow Curved Text Curve Manipulator

的解決方案是一個快速劈基礎上,答案StackOverflow的問題:CubicCurve JavaFX。我相信可以通過更多的努力,時間和技巧找到更好的解決方案。

因爲程序基於轉換,所以採用它會非常容易,因此文本可以按照曲線動畫化,在溢出時從右向左包裝(例如,您可能會在marquee text或股票代碼中看到)。

任何標準的JavaFX效果,例如發光,陰影等等和字體的改變都可以用來從你的問題中得到來自paintshop pro文本的陰影效果。發光效果在這裏適用是一個很好的效果,因爲它巧妙地柔化旋轉角色周圍的鋸齒狀邊緣。

此解決方案所基於的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框架中嗎?

你可以繼承窗格並創建一個自定義PathLayout類似於一個FlowPane,但勾畫出沿着一條路徑,而不是一條直線節點出來。要佈局的節點由每個字符的Text節點組成,類似於我在答案中所做的。但即使如此,由於您要考慮像比例間隔字母,kerning等東西,因此您並不是真正精確地呈現文本。因此,爲了獲得完整的保真度和準確性,您需要實現自己的低級別文本佈局算法。如果是我,我只會去那個努力,如果在這個答案中使用PathTransitions提供的「足夠好」的解決方案證明對你來說質量不夠高。

+0

好的答案謝謝!你認爲可以通過調整代碼來實現javafx.scene.effect.Effect嗎?或者可能有更好的方法將其正確地集成到JFX框架中? – gontard

+0

@gontard我編輯了我的答案以解決您的其他問題。 – jewelsea

+0

感謝您使用這些精度和完整的答案。我想我會創建一個基於AnimationPathHelper或PathIterator的自定義窗格。再次感謝您抽出時間。 – gontard

7

您可以使用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(); 
    } 
} 
+0

感謝您的貢獻。我應該提到使用WebView是不可接受的,因爲我的應用程序必須顯示大量帶有這種效果的文本。 WebView是用於繪製具有此效果的文本的過重組件。 – gontard

+0

@gontard查看我的編輯 – Sebastian