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:
- testo curvo con un effetto glow.
- Un testo curvo senza effetto applicato.
- I punti di manipolazione del controllo utilizzati per definire il percorso curvo del testo senza effetto sono stati tracciati lungo.
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.
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
@MichelleJS ringrazia, ma SVG non è realmente supportato in javafx. – gontard
forse questo può aiutarti: https://forums.oracle.com/thread/1712335 – Sebastian