如何沿贝塞尔曲线写文字?

我希望在javafx 2.2中或至少在javafx 8中执行类似的操作。我浏览了没有结果的Text javadoc和css引用 。

在此处输入图像描述

可以通过在WebView中显示和svg来实现此效果。 但我的应用程序必须显示大量具有此效果的文本。 WebView是一个太重的组件,用于绘制具有此效果的文本。

我在oracle技术网络上问了同样的问题。

这是滥用PathTransition以沿Bézier曲线绘制文本。

该程序允许您拖动控制点以定义曲线,然后沿该曲线绘制文本。 文本中的字符间隔等距离,因此如果曲线的总长度非常接近文本宽度与“正常”间距匹配,则效果最佳,并且不会对字距调整等内容进行调整。

以下示例显示:

  1. 带有发光效果的弯曲文本。
  2. 一些没有应用效果的弯曲文本。
  3. 用于定义弯曲路径的控制操作点是无效的文本。

弯曲的文本与辉光弯曲的文字曲线操纵器

解决方案是基于StackOverflow问题的答案快速入侵: CubicCurve JavaFX 。 我相信可以通过更多的努力,时间和技巧找到更好的解决方案。

因为程序是基于过渡的,所以很容易采用它,以便文本可以动画跟随曲线,在溢出时从右到左包裹(就像你可能在选框文本或股票代码中看到的那样)。

任何标准的JavaFX效果(如发光,阴影等)和字体更改都可以应用于从问题中的painthop专业文本中获取阴影效果等内容。 发光效果是一种很好的效果,因为它可以巧妙地柔化旋转角色周围的锯齿状边缘。

此解决方案所基于的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 parts = FXCollections.observableArrayList(); final ObservableList 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 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() { @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() { @Override public void handle(MouseEvent mouseEvent) { getScene().setCursor(Cursor.HAND); } }); setOnMouseDragged(new EventHandler() { @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() { @Override public void handle(MouseEvent mouseEvent) { if (!mouseEvent.isPrimaryButtonDown()) { getScene().setCursor(Cursor.HAND); } } }); setOnMouseExited(new EventHandler() { @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 { private final ToggleButton plot; private final ObservableList parts; private final ObservableList transitions; private final ObservableList controls; public PlotHandler(ToggleButton plot, ObservableList parts, ObservableList transitions, ObservableList 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,沿Bezier曲线写文本与效果相关的布局更多 - 最好调整字符的布局和旋转,而不是使用效果来移动它们。

或者可能有更好的方法在JFX框架中正确集成它?

您可以子类化Pane并创建一个类似于FlowPane的自定义PathLayout,但是沿路径而不是直线布置节点。 要布局的节点由每个字符的Text节点组成,类似于我在答案中所做的。 但即使这样,你也不能真正准确地渲染文本,因为你想要考虑比例间隔的字母, 字距等等。因此,为了保证完全保真度和准确性,你需要实现自己的低级文本布局算法。 如果是我的话,如果使用PathTransitions在这个答案中提供的“足够好”的解决方案对你来说不够高质量,那么我只会付出努力。

您可以使用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("\n" + "\n" + " \n" + "\n" + " " + "\n" + " \n" + "\n"+ "\n" + " Text on a Path\n" + "" + "\n" + "" + " \n" + ""); 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("\n" + "\n" + " \n" + "\n" + " " + "\n" + " \n" + "\n"+ "\n" + " Text on a Path\n" + "" + "\n" + "" + " \n" + ""); root.getChildren().add(view); view.getEngine().getLoadWorker().stateProperty().addListener(new ChangeListener() { @Override public void changed(ObservableValue 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(); } }