package src.com.fxexperience.tools.splineeditor; import javafx.beans.property.DoubleProperty; import javafx.beans.property.SimpleDoubleProperty; import javafx.collections.FXCollections; import javafx.event.EventHandler; import javafx.scene.Cursor; import javafx.scene.chart.NumberAxis; import javafx.scene.chart.XYChart; import javafx.scene.input.MouseEvent; import javafx.scene.paint.Color; import javafx.scene.shape.Circle; import javafx.scene.shape.CircleBuilder; import javafx.scene.shape.CubicCurveTo; import javafx.scene.shape.Line; import javafx.scene.shape.LineBuilder; import javafx.scene.shape.LineTo; import javafx.scene.shape.MoveTo; import javafx.scene.shape.Path; import javafx.scene.shape.PathBuilder; /** * @author Jasper Potts */ public class SplineEditorControl extends XYChart<Number, Number> { private DoubleProperty controlPoint1x = new SimpleDoubleProperty(0.8); public double getControlPoint1x() { return controlPoint1x.get(); } public DoubleProperty controlPoint1xProperty() { return controlPoint1x; } private DoubleProperty controlPoint1y = new SimpleDoubleProperty(0.2); public double getControlPoint1y() { return controlPoint1y.get(); } public DoubleProperty controlPoint1yProperty() { return controlPoint1y; } private DoubleProperty controlPoint2x = new SimpleDoubleProperty(0.2); public double getControlPoint2x() { return controlPoint2x.get(); } public DoubleProperty controlPoint2xProperty() { return controlPoint2x; } private DoubleProperty controlPoint2y = new SimpleDoubleProperty(0.8); public double getControlPoint2y() { return controlPoint2y.get(); } public DoubleProperty controlPoint2yProperty() { return controlPoint2y; } private Circle controlPoint1Circle = CircleBuilder.create() .fill(Color.WHITE).stroke(Color.RED).strokeWidth(3).radius(6) .cursor(Cursor.HAND).build(); private Circle controlPoint2Circle = CircleBuilder.create() .fill(Color.WHITE).stroke(Color.RED).strokeWidth(3).radius(6) .cursor(Cursor.HAND).build(); private Line cp1Line = LineBuilder.create().stroke(Color.RED) .strokeWidth(2).build(); private Line cp2Line = LineBuilder.create().stroke(Color.RED) .strokeWidth(2).build(); private Path dottedLinesPath = PathBuilder.create().stroke(Color.WHITE) .strokeWidth(1).strokeDashArray(1d, 8d).build(); private Path splinePath = PathBuilder.create().stroke(Color.WHITE) .strokeWidth(2).build(); @SuppressWarnings("unchecked") public SplineEditorControl() { super(new NumberAxis(0, 1, 0.1), new NumberAxis(0, 1, 0.1)); setAnimated(false); getXAxis().setAutoRanging(false); getXAxis().setAutoRanging(false); setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE); setAlternativeRowFillVisible(false); getPlotChildren().addAll(splinePath, dottedLinesPath, cp1Line, cp2Line, controlPoint1Circle, controlPoint2Circle); setData(FXCollections.observableArrayList(new Series<Number, Number>())); controlPoint1Circle.setOnMouseDragged(new EventHandler<MouseEvent>() { @Override public void handle(MouseEvent event) { final double x = event.getX() + controlPoint1Circle.getLayoutX(); final double y = event.getY() + controlPoint1Circle.getLayoutY(); final double dataX = getXAxis().getValueForDisplay(x) .doubleValue(); final double dataY = getYAxis().getValueForDisplay(y) .doubleValue(); controlPoint1x.set(clamp(dataX)); controlPoint1y.set(clamp(dataY)); requestChartLayout(); } }); controlPoint2Circle.setOnMouseDragged(new EventHandler<MouseEvent>() { @Override public void handle(MouseEvent event) { final double x = event.getX() + controlPoint2Circle.getLayoutX(); final double y = event.getY() + controlPoint2Circle.getLayoutY(); final double dataX = getXAxis().getValueForDisplay(x) .doubleValue(); final double dataY = getYAxis().getValueForDisplay(y) .doubleValue(); controlPoint2x.set(clamp(dataX)); controlPoint2y.set(clamp(dataY)); requestChartLayout(); } }); } @Override protected void layoutPlotChildren() { double cp1x = getXAxis().getDisplayPosition(controlPoint1x.get()); double cp1y = getYAxis().getDisplayPosition(controlPoint1y.get()); double cp2x = getXAxis().getDisplayPosition(controlPoint2x.get()); double cp2y = getYAxis().getDisplayPosition(controlPoint2y.get()); double minx = getXAxis().getZeroPosition(); double miny = getYAxis().getZeroPosition(); double maxx = getXAxis().getDisplayPosition(1); double maxy = getYAxis().getDisplayPosition(1); controlPoint1Circle.setLayoutX(cp1x); controlPoint1Circle.setLayoutY(cp1y); controlPoint2Circle.setLayoutX(cp2x); controlPoint2Circle.setLayoutY(cp2y); cp1Line.setStartX(minx); cp1Line.setStartY(miny); cp1Line.setEndX(cp1x); cp1Line.setEndY(cp1y); cp2Line.setStartX(maxx); cp2Line.setStartY(maxy); cp2Line.setEndX(cp2x); cp2Line.setEndY(cp2y); dottedLinesPath.getElements().setAll( new MoveTo(minx - 0.5, cp1y - 0.5), new LineTo(cp1x - 0.5, cp1y - 0.5), new LineTo(cp1x - 0.5, miny - 0.5), new MoveTo(minx - 0.5, cp2y - 0.5), new LineTo(cp2x - 0.5, cp2y - 0.5), new LineTo(cp2x - 0.5, miny - 0.5)); splinePath.getElements().setAll(new MoveTo(minx, miny), new CubicCurveTo(cp1x, cp1y, cp2x, cp2y, maxx, maxy)); } private static double clamp(double value) { return value < 0 ? 0 : value > 1 ? 1 : value; } @Override protected void dataItemAdded(Series<Number, Number> series, int i, Data<Number, Number> data) { } @Override protected void dataItemRemoved(Data<Number, Number> data, Series<Number, Number> series) { } @Override protected void dataItemChanged(Data<Number, Number> data) { } @Override protected void seriesAdded(Series<Number, Number> series, int i) { } @Override protected void seriesRemoved(Series<Number, Number> series) { } }