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);     } } 

enter image description here

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

Popular posts from this blog

angularjs - ADAL JS Angular- WebAPI add a new role claim to the token -

php - CakePHP HttpSockets send array of paramms -

node.js - Using Node without global install -