javafx - Move node along path without PathTransition -
problem
i want move object along path. pathtransition works in terms of duration, need use movement along path in animationtimer.
question
does know way move node along given path via animationtimer?
or if has better idea of smoothing rotation of nodes @ sharp edges along hard waypoints, suffice well.
code
i need moving object along sharp path, rotation should have smooth turns. code below draws path along waypoints (black color).
i thought means of doing shorten path segments (red color) , instead of hard lineto make cubiccurveto (yellow color).
the pathtransition conveniently move node along path correct rotation @ edges, unfortunately works on duration basis.
import java.util.arraylist; import java.util.list; import javafx.animation.pathtransition; import javafx.animation.pathtransition.orientationtype; import javafx.animation.transition; import javafx.application.application; import javafx.geometry.point2d; import javafx.scene.scene; import javafx.scene.snapshotparameters; import javafx.scene.image.imageview; import javafx.scene.image.writableimage; import javafx.scene.layout.pane; import javafx.scene.paint.color; import javafx.scene.shape.cubiccurveto; import javafx.scene.shape.lineto; import javafx.scene.shape.moveto; import javafx.scene.shape.path; import javafx.scene.shape.polygon; import javafx.scene.shape.stroketype; import javafx.stage.stage; import javafx.util.duration; /** * cut given path. * black = original * red = cut off * yellow = smoothed using bezier curve */ public class main extends application { /** * pixels cut off start , end of paths in order shorten them , make path smoother. */ private double smoothness = 30; @override public void start(stage primarystage) { pane root = new pane(); scene scene = new scene(root,1600,900); primarystage.setscene(scene); primarystage.show(); // waypoints path list<point2d> waypoints = getwaypoints(); // draw path sharp edges // -------------------------------------------- path sharppath = createsharppath( waypoints); sharppath.setstroke(color.black); sharppath.setstrokewidth(8); sharppath.setstroketype(stroketype.centered); root.getchildren().add( sharppath); // draw path shortened edges // -------------------------------------------- path shortenedpath = createshortenedpath(waypoints, smoothness); shortenedpath.setstroke(color.red); shortenedpath.setstrokewidth(5); shortenedpath.setstroketype(stroketype.centered); root.getchildren().add( shortenedpath); // draw path smooth edges // -------------------------------------------- path smoothpath = createsmoothpath(waypoints, smoothness); smoothpath.setstroke(color.yellow); smoothpath.setstrokewidth(2); smoothpath.setstroketype(stroketype.centered); root.getchildren().add( smoothpath); // move arrow on path // -------------------------------------------- imageview arrow = createarrow(30,30); root.getchildren().add( arrow); pathtransition pt = new pathtransition( duration.millis(10000), smoothpath); pt.setnode(arrow); pt.setautoreverse(true); pt.setcyclecount( transition.indefinite); pt.setorientation(orientationtype.orthogonal_to_tangent); pt.play(); } /** * create path waypoints * @param waypoints * @return */ private path createsharppath( list<point2d> waypoints) { path path = new path(); for( point2d point: waypoints) { if( path.getelements().isempty()) { path.getelements().add(new moveto( point.getx(), point.gety())); } else { path.getelements().add(new lineto( point.getx(), point.gety())); } } return path; } /** * create path waypoints, shorten path , create line segment between segments * @param smoothness pixels cut of start , end. * @return */ private path createshortenedpath( list<point2d> waypoints, double smoothness) { path path = new path(); // waypoints path point2d prev = null; double x; double y; for( int i=0; < waypoints.size(); i++) { point2d curr = waypoints.get( i); if( == 0) { path.getelements().add(new moveto( curr.getx(), curr.gety())); x = curr.getx(); y = curr.gety(); } else { // shorten previous path double distancex = curr.getx() - prev.getx(); double distancey = curr.gety() - prev.gety(); double rad = math.atan2(distancey, distancex); double distance = math.sqrt( distancex * distancex + distancey * distancey); // cut off paths except last 1 if( != waypoints.size() - 1) { distance -= smoothness; } x = prev.getx() + distance * math.cos(rad); y = prev.gety() + distance * math.sin(rad); path.getelements().add(new lineto( x, y)); // shorten current path if( + 1 < waypoints.size()) { point2d next = waypoints.get( i+1); distancex = next.getx() - curr.getx(); distancey = next.gety() - curr.gety(); distance = smoothness; rad = math.atan2(distancey, distancex); x = curr.getx() + distance * math.cos(rad); y = curr.gety() + distance * math.sin(rad); path.getelements().add(new lineto( x, y)); } } prev = curr; } return path; } /** * create path waypoints, shorten path , create smoothing cubic curve segment between segments * @param smoothness pixels cut of start , end. * @return */ private path createsmoothpath( list<point2d> waypoints, double smoothness) { path smoothpath = new path(); smoothpath.setstroke(color.yellow); smoothpath.setstrokewidth(2); smoothpath.setstroketype(stroketype.centered); // waypoints path point2d ctrl1; point2d ctrl2; point2d prev = null; double x; double y; for( int i=0; < waypoints.size(); i++) { point2d curr = waypoints.get( i); if( == 0) { smoothpath.getelements().add(new moveto( curr.getx(), curr.gety())); x = curr.getx(); y = curr.gety(); } else { // shorten previous path double distancex = curr.getx() - prev.getx(); double distancey = curr.gety() - prev.gety(); double rad = math.atan2(distancey, distancex); double distance = math.sqrt( distancex * distancex + distancey * distancey); // cut off paths except last 1 if( != waypoints.size() - 1) { distance -= smoothness; } // system.out.println( "segment " + + ", angle: " + math.todegrees( rad) + ", distance: " + distance); x = prev.getx() + distance * math.cos(rad); y = prev.gety() + distance * math.sin(rad); smoothpath.getelements().add(new lineto( x, y)); // shorten current path , add smoothing segment if( + 1 < waypoints.size()) { point2d next = waypoints.get( i+1); distancex = next.getx() - curr.getx(); distancey = next.gety() - curr.gety(); distance = smoothness; rad = math.atan2(distancey, distancex); x = curr.getx() + distance * math.cos(rad); y = curr.gety() + distance * math.sin(rad); ctrl1 = curr; ctrl2 = curr; smoothpath.getelements().add(new cubiccurveto(ctrl1.getx(), ctrl1.gety(), ctrl2.getx(), ctrl2.gety(), x, y)); } } prev = curr; } return smoothpath; } /** * waypoints path * @return */ public list<point2d> getwaypoints() { list<point2d> path = new arraylist<>(); // rectangle // path.add(new point2d( 100, 100)); // path.add(new point2d( 400, 100)); // path.add(new point2d( 400, 400)); // path.add(new point2d( 100, 400)); // path.add(new point2d( 100, 100)); // rectangle peak on right path.add(new point2d( 100, 100)); path.add(new point2d( 400, 100)); path.add(new point2d( 450, 250)); path.add(new point2d( 400, 400)); path.add(new point2d( 100, 400)); path.add(new point2d( 100, 100)); return path; } /** * create arrow imageview * @param width * @param height * @return */ private imageview createarrow( double width, double height) { writableimage wi; polygon arrow = new polygon( 0, 0, width, height / 2, 0, height); // left/right lines of arrow snapshotparameters parameters = new snapshotparameters(); parameters.setfill(color.transparent); wi = new writableimage( (int) width, (int) height); arrow.snapshot(parameters, wi); return new imageview( wi); } public static void main(string[] args) { launch(args); } }
thanks lot help!
pathtransition
has public interpolate
method called in fraction between 0 (start) , 1 (end), sadly it's not intended user, , can called while path transition running.
if have @ how interpolate
works, uses internal class called segment
, based on linear segments within path.
so first step converting original path linear one:
import java.util.arraylist; import java.util.list; import java.util.stream.intstream; import javafx.geometry.point2d; import javafx.scene.shape.closepath; import javafx.scene.shape.cubiccurveto; import javafx.scene.shape.lineto; import javafx.scene.shape.moveto; import javafx.scene.shape.path; import javafx.scene.shape.pathelement; import javafx.scene.shape.quadcurveto; /** * * @author jpereda */ public class linearpath { private final path originalpath; public linearpath(path path){ this.originalpath=path; } public path generatelinepath(){ /* generate list of points interpolating original path */ originalpath.getelements().foreach(this::getpoints); /* create path moveto,lineto */ path path = new path(new moveto(list.get(0).getx(),list.get(0).gety())); list.stream().skip(1).foreach(p->path.getelements().add(new lineto(p.getx(),p.gety()))); path.getelements().add(new closepath()); return path; } private point2d p0; private list<point2d> list; private final int points_curve=5; private void getpoints(pathelement elem){ if(elem instanceof moveto){ list=new arraylist<>(); p0=new point2d(((moveto)elem).getx(),((moveto)elem).gety()); list.add(p0); } else if(elem instanceof lineto){ list.add(new point2d(((lineto)elem).getx(),((lineto)elem).gety())); } else if(elem instanceof cubiccurveto){ point2d ini = (list.size()>0?list.get(list.size()-1):p0); intstream.rangeclosed(1, points_curve).foreach(i->list.add(evalcubicbezier((cubiccurveto)elem, ini, ((double)i)/points_curve))); } else if(elem instanceof quadcurveto){ point2d ini = (list.size()>0?list.get(list.size()-1):p0); intstream.rangeclosed(1, points_curve).foreach(i->list.add(evalquadbezier((quadcurveto)elem, ini, ((double)i)/points_curve))); } else if(elem instanceof closepath){ list.add(p0); } } private point2d evalcubicbezier(cubiccurveto c, point2d ini, double t){ point2d p=new point2d(math.pow(1-t,3)*ini.getx()+ 3*t*math.pow(1-t,2)*c.getcontrolx1()+ 3*(1-t)*t*t*c.getcontrolx2()+ math.pow(t, 3)*c.getx(), math.pow(1-t,3)*ini.gety()+ 3*t*math.pow(1-t, 2)*c.getcontroly1()+ 3*(1-t)*t*t*c.getcontroly2()+ math.pow(t, 3)*c.gety()); return p; } private point2d evalquadbezier(quadcurveto c, point2d ini, double t){ point2d p=new point2d(math.pow(1-t,2)*ini.getx()+ 2*(1-t)*t*c.getcontrolx()+ math.pow(t, 2)*c.getx(), math.pow(1-t,2)*ini.gety()+ 2*(1-t)*t*c.getcontroly()+ math.pow(t, 2)*c.gety()); return p; } }
now, based on pathtransition.segment
class, , removing private or deprecated api, i've come class public interpolator
method:
import java.util.arraylist; import javafx.geometry.bounds; import javafx.scene.node; import javafx.scene.shape.closepath; import javafx.scene.shape.lineto; import javafx.scene.shape.moveto; import javafx.scene.shape.path; /** * based on javafx.animation.pathtransition * * @author jpereda */ public class pathinterpolator { private final path originalpath; private final node node; private double totallength = 0; private static final int smooth_zone = 10; private final arraylist<segment> segments = new arraylist<>(); private segment movetoseg = segment.getzerosegment(); private segment lastseg = segment.getzerosegment(); public pathinterpolator(path path, node node){ this.originalpath=path; this.node=node; calculatesegments(); } private void calculatesegments() { segments.clear(); path linepath = new linearpath(originalpath).generatelinepath(); linepath.getelements().foreach(elem->{ segment newseg = null; if(elem instanceof moveto){ movetoseg = segment.newmoveto(((moveto)elem).getx(),((moveto)elem).gety(), lastseg.accumlength); newseg = movetoseg; } else if(elem instanceof lineto){ newseg = segment.newlineto(lastseg, ((lineto)elem).getx(),((lineto)elem).gety()); } else if(elem instanceof closepath){ newseg = segment.newclosepath(lastseg, movetoseg); if (newseg == null) { lastseg.converttoclosepath(movetoseg); } } if (newseg != null) { segments.add(newseg); lastseg = newseg; } }); totallength = lastseg.accumlength; } public void interpolate(double frac) { double part = totallength * math.min(1, math.max(0, frac)); int segidx = findsegment(0, segments.size() - 1, part); segment seg = segments.get(segidx); double lengthbefore = seg.accumlength - seg.length; double partlength = part - lengthbefore; double ratio = partlength / seg.length; segment prevseg = seg.prevseg; double x = prevseg.tox + (seg.tox - prevseg.tox) * ratio; double y = prevseg.toy + (seg.toy - prevseg.toy) * ratio; double rotateangle = seg.rotateangle; // provide smooth rotation on segment bounds double z = math.min(smooth_zone, seg.length / 2); if (partlength < z && !prevseg.ismoveto) { //interpolate rotation previous segment rotateangle = interpolate( prevseg.rotateangle, seg.rotateangle, partlength / z / 2 + 0.5f); } else { double dist = seg.length - partlength; segment nextseg = seg.nextseg; if (dist < z && nextseg != null) { //interpolate rotation next segment if (!nextseg.ismoveto) { rotateangle = interpolate( seg.rotateangle, nextseg.rotateangle, (z - dist) / z / 2); } } } node.settranslatex(x - getpivotx()); node.settranslatey(y - getpivoty()); node.setrotate(rotateangle); } private double getpivotx() { final bounds bounds = node.getlayoutbounds(); return bounds.getminx() + bounds.getwidth()/2; } private double getpivoty() { final bounds bounds = node.getlayoutbounds(); return bounds.getminy() + bounds.getheight()/2; } /** * returns index of first segment having accumulated length * path beginning, greater {@code length} */ private int findsegment(int begin, int end, double length) { // check search termination if (begin == end) { // find last non-moveto segment given length return segments.get(begin).ismoveto && begin > 0 ? findsegment(begin - 1, begin - 1, length) : begin; } // otherwise continue binary search int middle = begin + (end - begin) / 2; return segments.get(middle).accumlength > length ? findsegment(begin, middle, length) : findsegment(middle + 1, end, length); } /** interpolates angle according rate, * correct 0->360 , 360->0 transitions */ private static double interpolate(double fromangle, double toangle, double ratio) { double delta = toangle - fromangle; if (math.abs(delta) > 180) { toangle += delta > 0 ? -360 : 360; } return normalize(fromangle + ratio * (toangle - fromangle)); } /** converts angle range 0-360 */ private static double normalize(double angle) { while (angle > 360) { angle -= 360; } while (angle < 0) { angle += 360; } return angle; } private static class segment { private static final segment zerosegment = new segment(true, 0, 0, 0, 0, 0); boolean ismoveto; double length; // total length path's beginning end of segment double accumlength; // end point of segment double tox; double toy; // segment's rotation angle in degrees double rotateangle; segment prevseg; segment nextseg; private segment(boolean ismoveto, double tox, double toy, double length, double lengthbefore, double rotateangle) { this.ismoveto = ismoveto; this.tox = tox; this.toy = toy; this.length = length; this.accumlength = lengthbefore + length; this.rotateangle = rotateangle; } public static segment getzerosegment() { return zerosegment; } public static segment newmoveto(double tox, double toy, double accumlength) { return new segment(true, tox, toy, 0, accumlength, 0); } public static segment newlineto(segment fromseg, double tox, double toy) { double deltax = tox - fromseg.tox; double deltay = toy - fromseg.toy; double length = math.sqrt((deltax * deltax) + (deltay * deltay)); if ((length >= 1) || fromseg.ismoveto) { // filtering out flattening noise double sign = math.signum(deltay == 0 ? deltax : deltay); double angle = (sign * math.acos(deltax / length)); angle = normalize(angle / math.pi * 180); segment newseg = new segment(false, tox, toy, length, fromseg.accumlength, angle); fromseg.nextseg = newseg; newseg.prevseg = fromseg; return newseg; } return null; } public static segment newclosepath(segment fromseg, segment movetoseg) { segment newseg = newlineto(fromseg, movetoseg.tox, movetoseg.toy); if (newseg != null) { newseg.converttoclosepath(movetoseg); } return newseg; } public void converttoclosepath(segment movetoseg) { segment firstlinetoseg = movetoseg.nextseg; nextseg = firstlinetoseg; firstlinetoseg.prevseg = this; } } }
basically, once have linear path, every line generates segment
. list of these segments can call interpolate
method calculate position , rotation of node @ fraction between 0 , 1.
and can create animationtimer
in application:
@override public void start(stage primarystage) { ... // move arrow on path // -------------------------------------------- imageview arrow = createarrow(30,30); root.getchildren().add( arrow); pathinterpolator interpolator=new pathinterpolator(smoothpath, arrow); animationtimer timer = new animationtimer() { @override public void handle(long now) { double millis=(now/1_000_000)%10000; interpolator.interpolate(millis/10000); } }; timer.start(); }
Comments
Post a Comment