001 package fang;
002
003 import java.awt.Shape;
004 import java.awt.geom.*;
005 import java.util.ArrayList;
006
007 import static java.awt.geom.FlatteningPathIterator.*;
008
009
010 /**
011 * This tracker enables a path to be created from any shape or Sprite.
012 *
013 * @author Jam Jenkins
014 *
015 */
016 public class OutlineTransformer extends Transformer
017 {
018 /** the distance around the outline */
019 private double perimeter;
020
021 /**
022 * whether to keep moving around the outline once the shape has been traversed
023 */
024 private boolean looping = false;
025
026 /** the shape to use the outline of */
027 private Shape shape;
028
029 /** how fast to move and which direction */
030 private double speed;
031
032 /**
033 * where the tracker currently is in the outline of the shape
034 */
035 private Point2D.Double currentPoint;
036
037 /** how far to move the next time step */
038 private Point2D.Double delta;
039
040 /** the points along the outline */
041 private double[][] allPoints;
042
043 /** the type of each point, SEG_MOVETO or SEG_LINETO */
044 private int[] pointType;
045
046 /** which is the current point in the array */
047 private int pointIndex;
048
049 /**
050 * creates the tracker given a Sprite to move around and a speed to travel. If
051 * you want to make the sprite travel along the actual Sprite instead of just
052 * in the shape of the Sprite, you also need to set the original location of
053 * the Sprite moving to the current location of the OutlineTracker using the
054 * method getCurrentPoint().
055 *
056 * @param outline
057 * the sprite on which to traverse the outline
058 * @param speed
059 * the portion of the screen to move per second
060 */
061 public OutlineTransformer(Sprite outline, double speed)
062 {
063 this(outline.getShape(), speed);
064 }
065
066 /**
067 * creates the tracker given a Shape to move around and a speed to travel. If
068 * you want to make the sprite travel along the actual Sprite instead of just
069 * in the shape of the Sprite, you also need to set the original location of
070 * the Sprite moving to the current location of the OutlineTracker using the
071 * method getCurrentPoint().
072 *
073 * @param outline
074 * the shape on which to traverse the outline
075 * @param speed
076 * the portion of the screen to move per second
077 */
078 public OutlineTransformer(Shape outline, double speed)
079 {
080 shape = outline;
081 currentPoint = new Point2D.Double();
082 delta = new Point2D.Double();
083 initializeReversablePath();
084 setSpeed(speed);
085 }
086
087 /**
088 * creates the tracker given a path to move on and a speed to travel. If you
089 * want to make the sprite travel along the actual path instead of just in the
090 * shape of the path, you also need to set the original location of the Sprite
091 * moving to the current location of the OutlineTracker using the method
092 * getCurrentPoint().
093 *
094 * @param speed
095 * the portion of the screen to move per second
096 * @param vertices
097 * the location of the vertices along the path
098 */
099 public OutlineTransformer(double speed, double... vertices)
100 {
101 currentPoint = new Point2D.Double();
102 delta = new Point2D.Double();
103 setShape(vertices);
104 setSpeed(speed);
105 }
106
107 /**
108 * gets the current location along the outline. This method is useful to call
109 * if you want the Sprite that is moving to stay on the outline of the
110 * original Shape/Sprite.
111 *
112 * @return the location along the outline
113 */
114 public Location2D getCurrentPoint()
115 {
116 return new Location2D(currentPoint.x, currentPoint.y);
117 }
118
119 /**
120 * makes a path around the Shape/Sprite such that the points can be traversed
121 * both forward and backward. This is done in two primary ways. First, every
122 * SEG_CLOSE is replaced by a SEG_LINETO since closing would be different when
123 * traversing in the opposite direction. Secondly, each SEG_MOVETO is replaced
124 * by a SEG_MOVETO and SEG_LINETO so that lines to close shapes actually draw
125 * the final closing line and not just move toward it.
126 */
127 private void initializeReversablePath()
128 {
129 pointIndex = 0;
130 perimeter = 0;
131 Point2D.Double startPoint = new Point2D.Double();
132 double[] curve = new double[6];
133 PathIterator path;
134 path = shape.getPathIterator(new AffineTransform(), 0.001);
135 path.currentSegment(curve);
136 currentPoint.x = curve[0];
137 currentPoint.y = curve[1];
138 startPoint.x = curve[0];
139 startPoint.y = curve[1];
140
141 ArrayList<Integer> pointsType = new ArrayList<Integer>();
142 ArrayList < double[] > points = new ArrayList < double[] > ();
143 pointsType.add(SEG_MOVETO);
144 points.add(new double[] { curve[0], curve[1] });
145 pointsType.add(SEG_LINETO);
146 points.add(new double[] { curve[0], curve[1] });
147 path.next();
148 Point2D.Double last = new Point2D.Double(curve[0], curve[1]);
149 boolean fixReverse = false;
150 while (!path.isDone())
151 {
152 int curveType = path.currentSegment(curve);
153 if (curveType == SEG_CLOSE)
154 {
155 curve[0] = startPoint.x;
156 curve[1] = startPoint.y;
157 if (!fixReverse)
158 {
159 curveType = SEG_LINETO;
160 fixReverse = true;
161 }
162 else
163 {
164 fixReverse = false;
165 curveType = SEG_MOVETO;
166 }
167 }
168 else if (curveType == SEG_MOVETO)
169 {
170 startPoint.x = curve[0];
171 startPoint.y = curve[1];
172 if (!fixReverse)
173 {
174 fixReverse = true;
175 }
176 else
177 {
178 fixReverse = false;
179 curveType = SEG_LINETO;
180 }
181 }
182 if (curveType == SEG_LINETO)
183 {
184 perimeter += last.distance(curve[0], curve[1]);
185 }
186 last.x = curve[0];
187 last.y = curve[1];
188 pointsType.add(curveType);
189 points.add(new double[] { curve[0], curve[1] });
190 if (!fixReverse)
191 {
192 path.next();
193 }
194 }
195 allPoints = new double[pointsType.size()][2];
196 pointType = new int[pointsType.size()];
197 for (int i = 0; i < pointType.length; i++)
198 {
199 pointType[i] = pointsType.get(i);
200 allPoints[i] = points.get(i);
201 }
202 }
203
204 /**
205 * sets whether the tracker should stop moving once it goes all the way around
206 * the shape or if it should move continuously
207 *
208 * @param looping
209 * true indicates move continuously and false means stop when the
210 * shape have bee traversed. Calling skipDistance can cause less than
211 * the entire shape to be traversed if looping is false.
212 */
213 public void setLooping(boolean looping)
214 {
215 this.looping = looping;
216 }
217
218 /**
219 * tells whether the tracker should stop moving once it goes all the way
220 * around the shape or if it should move continuously
221 *
222 * @return true if the tracker moves continously or false if it stops once the
223 * shape has been traversed
224 */
225 public boolean isLooping()
226 {
227 return looping;
228 }
229
230 /**
231 * gets the distance around the entire outline of the shape
232 *
233 * @return the perimeter of the shape
234 */
235 public double getPathDistance()
236 {
237 return perimeter;
238 }
239
240 /**
241 * skips past a part of the outline. If the amount skipped past moves past the
242 * end of the outline and looping is false, the tracker will continue moving
243 * around the shape again until it reaches the end. Calling this method does
244 * not move the tracker, it just changes where it starts and alters the
245 * currrentPoint.
246 *
247 * @param distance
248 * the amount to skip. This can be used in conjunction with
249 * getPathDistance.
250 */
251 public void skipDistance(double distance)
252 {
253 double originalX = delta.x;
254 double originalY = delta.y;
255 advanceDistance(distance);
256 delta.x = originalX;
257 delta.y = originalY;
258 }
259
260 /**
261 * moves along the outline a given distance. Unlike shipDistance, this has the
262 * effect of moving the tracker and it changes currentPoint.
263 *
264 * @param distance
265 * the amount to move around the outline
266 */
267 public void advanceDistance(double distance)
268 {
269 while (distance > 0)
270 {
271 distance -= moveAlongCurrentLine(distance);
272 if (distance > 0)
273 {
274 if (!advancePointIndex())
275 {
276 distance = 0;
277 }
278 }
279 }
280 }
281
282 /**
283 * moves up to a given distance along the current line. If the line length is
284 * less than the distance to travel, then the whole line is traversed. If the
285 * line length is more than the distance to travel then the line is only
286 * partially traversed.
287 *
288 * @param distance
289 * the maximum amount to travel along the current line
290 * @return the actual amount traveled along the current line
291 */
292 private double moveAlongCurrentLine(double distance)
293 {
294 double originalDistance = distance;
295 if (pointType[pointIndex] == SEG_MOVETO)
296 {
297 delta.x += allPoints[pointIndex][0] - currentPoint.x;
298 delta.y += allPoints[pointIndex][1] - currentPoint.y;
299 currentPoint.x = allPoints[pointIndex][0];
300 currentPoint.y = allPoints[pointIndex][1];
301 }
302 else
303 {
304 double lineDistance = currentPoint.distance(allPoints[pointIndex][0],
305 allPoints[pointIndex][1]);
306 if (lineDistance < distance)
307 {
308 delta.x += allPoints[pointIndex][0] - currentPoint.x;
309 delta.y += allPoints[pointIndex][1] - currentPoint.y;
310 currentPoint.x = allPoints[pointIndex][0];
311 currentPoint.y = allPoints[pointIndex][1];
312 distance -= lineDistance;
313 }
314 else
315 {
316 double part = distance / lineDistance;
317 delta.x += part * (allPoints[pointIndex][0] - currentPoint.x);
318 delta.y += part * (allPoints[pointIndex][1] - currentPoint.y);
319 currentPoint.x += part * (allPoints[pointIndex][0] - currentPoint.x);
320 currentPoint.y += part * (allPoints[pointIndex][1] - currentPoint.y);
321 distance = 0;
322 }
323 }
324 return originalDistance - distance;
325 }
326
327 /**
328 * moves the current pointIndex forward or backward depending on the speed and
329 * whether it is looping or not.
330 *
331 * @return true if there is a next point, false otherwise
332 */
333 private boolean advancePointIndex()
334 {
335 boolean didAdvance = true;
336 if (speed > 0)
337 {
338 if (pointIndex < allPoints.length - 1)
339 {
340 pointIndex++;
341 }
342 else if (looping)
343 {
344 pointIndex = 0;
345 }
346 else
347 {
348 didAdvance = false;
349 }
350 }
351 else
352 {
353 if (pointIndex > 0)
354 {
355 pointIndex--;
356 }
357 else if (looping)
358 {
359 pointIndex = allPoints.length - 1;
360 }
361 else
362 {
363 didAdvance = false;
364 }
365 }
366 return didAdvance;
367 }
368
369 /**
370 * sets the shape to traverse
371 *
372 * @param sprite
373 * the sprite to trace the outline of
374 */
375 public void setShape(Sprite sprite)
376 {
377 setShape(sprite.getShape());
378 }
379
380 /**
381 * sets the shape to traverse
382 *
383 * @param shape
384 * the shape to trace the outline of
385 */
386 public void setShape(Shape shape)
387 {
388 this.shape = shape;
389 initializeReversablePath();
390 }
391
392 public void setShape(double... vertices)
393 {
394 GeneralPath path = new GeneralPath();
395 path.moveTo((float) vertices[0], (float) vertices[1]);
396 for (int i = 2; i < vertices.length - 1; i += 2)
397 {
398 path.lineTo((float) vertices[i], (float) vertices[i + 1]);
399 }
400 shape = path;
401 initializeReversablePath();
402 }
403
404 /**
405 * resets the tracker to the beginning of tracing the current shape/path
406 */
407 public void reset()
408 {
409 if (speed < 0)
410 {
411 pointIndex = allPoints.length - 1;
412 }
413 else
414 {
415 pointIndex = 0;
416 }
417 }
418
419 public boolean isMoving()
420 {
421 boolean moving;
422 // no speed meand not moving
423 if (speed == 0)
424 {
425 moving = false;
426 }
427 // speed and looping means moving
428 else if (isLooping())
429 {
430 moving = true;
431 }
432 // speed and not at the first or last line means moving
433 else if ((pointIndex > 0) || (pointIndex < allPoints.length - 1))
434 {
435 moving = true;
436 }
437 // moving forward from the beginning or backward from
438 // the end means moving
439 else if (((pointIndex == 0) && (speed > 0))
440 || ((pointIndex == allPoints.length - 1) && (speed < 0)))
441 {
442 moving = true;
443 }
444 // only option here is on the last line
445 // if there is any distance left to travel
446 // we are still moving
447 else
448 {
449 double lineDistance = currentPoint.distance(allPoints[pointIndex][0],
450 allPoints[pointIndex][1]);
451 moving = lineDistance > 1e-4;
452 }
453 return moving;
454 }
455
456 /**
457 * sets the speed of the tracker
458 *
459 * @param speed
460 * the speed of the tracker in screens per second
461 */
462 public void setSpeed(double speed)
463 {
464 if ((speed < 0) && (pointIndex == 0))
465 {
466 pointIndex = allPoints.length - 1;
467 }
468 if ((speed > 0) && (pointIndex == allPoints.length - 1))
469 {
470 pointIndex = 0;
471 }
472 this.speed = speed;
473 }
474
475 /**
476 * sets the speed of the tracker
477 *
478 * @return the speed of the tracker in screens per second
479 */
480 public double getSpeed()
481 {
482 return speed;
483 }
484
485 @Override
486 public Location2D getTranslation()
487 {
488 return new Location2D(delta.x, delta.y);
489 }
490
491 public void advance(double elapsedSeconds)
492 {
493 delta.x = 0;
494 delta.y = 0;
495 double distanceLeft = elapsedSeconds * Math.abs(speed);
496 advanceDistance(distanceLeft);
497 }
498 }