001 /*
002 * $Id: BusyPainter.java 3288 2009-03-10 14:36:28Z kschaefe $
003 *
004 * Copyright 2006 Sun Microsystems, Inc., 4150 Network Circle,
005 * Santa Clara, California 95054, U.S.A. All rights reserved.
006 *
007 * This library is free software; you can redistribute it and/or
008 * modify it under the terms of the GNU Lesser General Public
009 * License as published by the Free Software Foundation; either
010 * version 2.1 of the License, or (at your option) any later version.
011 *
012 * This library is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015 * Lesser General Public License for more details.
016 *
017 * You should have received a copy of the GNU Lesser General Public
018 * License along with this library; if not, write to the Free Software
019 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
020 */
021
022 package org.jdesktop.swingx.painter;
023
024 import java.awt.Color;
025 import java.awt.Graphics2D;
026 import java.awt.Rectangle;
027 import java.awt.Shape;
028 import java.awt.geom.Ellipse2D;
029 import java.awt.geom.PathIterator;
030 import java.awt.geom.Point2D;
031 import java.awt.geom.RoundRectangle2D;
032 import java.awt.geom.Point2D.Float;
033 import java.util.ArrayList;
034 import java.util.List;
035 import java.util.NoSuchElementException;
036
037 import org.jdesktop.swingx.JXBusyLabel.Direction;
038 import org.jdesktop.swingx.color.ColorUtil;
039
040 /**
041 * A specific painter that paints an "infinite progress" like animation. For
042 * more details see {@link org.jdesktop.swingx.JXBusyLabel}
043 */
044 public class BusyPainter extends AbstractPainter<Object> {
045
046 private int frame = -1;
047
048 private int points = 8;
049
050 private Color baseColor = new Color(200, 200, 200);
051
052 private Color highlightColor = Color.BLACK;
053
054 private int trailLength = 4;
055
056 private Shape pointShape;
057
058 private Shape trajectory;
059
060 private Direction direction = Direction.RIGHT;
061
062 private boolean paintCentered;
063
064 /**
065 * Creates new busy painter initialized to the shape of circle and bounds size 26x26 points.
066 */
067 public BusyPainter() {
068 this(26);
069 }
070
071 /**
072 * Creates new painter initialized to the shape of circle and bounds of square of specified height.
073 * @param height Painter height.
074 */
075 public BusyPainter(int height) {
076 this(getScaledDefaultPoint(height),
077 getScaledDefaultTrajectory(height));
078 }
079
080 protected static Shape getScaledDefaultTrajectory(int height) {
081 return new Ellipse2D.Float(((height * 8) / 26) / 2, ((height * 8) / 26) / 2, height
082 - ((height * 8) / 26), height - ((height * 8) / 26));
083 }
084
085 protected static Shape getScaledDefaultPoint(int height) {
086 return new RoundRectangle2D.Float(0, 0, (height * 8) / 26, 4,
087 4, 4);
088 }
089
090 /**
091 * Initializes painter to the specified trajectory and and point shape. Bounds are dynamically calculated to so the specified trajectory fits in.
092 * @param point Point shape.
093 * @param trajectory Trajectory shape.
094 */
095 public BusyPainter(Shape point, Shape trajectory) {
096 init(point, trajectory, Color.LIGHT_GRAY, Color.BLACK);
097 }
098
099 /**
100 * Initializes painter to provided shapes and default colors.
101 * @param point Point shape.
102 * @param trajectory Trajectory shape.
103 */
104 protected void init(Shape point, Shape trajectory, Color baseColor, Color highlightColor) {
105 this.baseColor = baseColor;
106 this.highlightColor = highlightColor;
107 this.pointShape = point;
108 this.trajectory = trajectory;
109 }
110
111 /**
112 * @inheritDoc
113 */
114 @Override
115 protected void doPaint(Graphics2D g, Object t, int width, int height) {
116 Rectangle r = getTrajectory().getBounds();
117 int tw = width - r.width - 2*r.x;
118 int th = height - r.height - 2*r.y;
119 if (isPaintCentered()) {
120 ((Graphics2D) g).translate(tw/2, th/2);
121 }
122
123 PathIterator pi = trajectory.getPathIterator(null);
124 float[] coords = new float[6];
125 Float cp = new Point2D.Float();
126 Point2D.Float sp = new Point2D.Float();
127 int ret;
128 float totalDist = 0;
129 List<float[]> segStack = new ArrayList<float[]>();
130 do {
131 try {
132 ret = pi.currentSegment(coords);
133 } catch (NoSuchElementException e) {
134 // invalid object definition - one of the bounds is zero or less
135 return;
136 }
137 if (ret == PathIterator.SEG_LINETO || (ret == PathIterator.SEG_CLOSE && (sp.x != cp.x || sp.y != cp.y))) {
138 //close by line
139 float c = calcLine(coords, cp);
140 totalDist += c;
141 // move the point to the end (just so it is same for all of them
142 segStack.add(new float[] { c, 0, 0, 0, 0, coords[0], coords[1], ret });
143 cp.x = coords[0];
144 cp.y = coords[1];
145 }
146 if (ret == PathIterator.SEG_MOVETO) {
147 sp.x = cp.x = coords[0];
148 sp.y = cp.y = coords[1];
149
150 }
151 if (ret == PathIterator.SEG_CUBICTO) {
152 float c = calcCube(coords, cp);
153 totalDist += c;
154 segStack.add(new float[] { c, coords[0], coords[1], coords[2],
155 coords[3], coords[4], coords[5], ret });
156 cp.x = coords[4];
157 cp.y = coords[5];
158 }
159 if (ret == PathIterator.SEG_QUADTO) {
160 float c = calcLengthOfQuad(coords, cp);
161 totalDist += c;
162 segStack.add(new float[] { c, coords[0], coords[1], 0 ,0 , coords[2],
163 coords[3], ret });
164 cp.x = coords[2];
165 cp.y = coords[3];
166 }
167 // got a starting point, center point on it.
168 pi.next();
169 } while (!pi.isDone());
170 float nxtP = totalDist / getPoints();
171 List<Point2D.Float> pList = new ArrayList<Point2D.Float>();
172 pList.add(new Float(sp.x, sp.y));
173 int sgIdx = 0;
174 float[] sgmt = segStack.get(sgIdx);
175 float len = sgmt[0];
176 float travDist = nxtP;
177 Float center = new Float(sp.x, sp.y);
178 for (int i = 1; i < getPoints(); i++) {
179 while (len < nxtP) {
180 sgIdx++;
181 // Be carefull when messing around with points.
182 sp.x = sgmt[5];
183 sp.y = sgmt[6];
184 sgmt = segStack.get(sgIdx);
185 travDist = nxtP - len;
186 len += sgmt[0];
187 }
188 len -= nxtP;
189 Float p = calcPoint(travDist, sp, sgmt, width, height);
190 pList.add(p);
191 center.x += p.x;
192 center.y += p.y;
193 travDist += nxtP;
194 }
195 // calculate center
196 center.x = ((float) width) / 2;
197 center.y = ((float) height) / 2;
198
199 // draw the stuff
200 int i = 0;
201 g.translate(center.x, center.y);
202 for (Point2D.Float p : pList) {
203 drawAt(g, i++, p, center);
204 }
205 g.translate(-center.x, -center.y);
206
207 if (isPaintCentered()) {
208 ((Graphics2D) g).translate(-tw/2, -th/2);
209 }
210 }
211
212 /**
213 * Gets value of centering hint. If true, shape will be positioned in the center of painted area.
214 * @return Whether shape will be centered over painting area or not.
215 */
216 public boolean isPaintCentered() {
217 return this.paintCentered;
218 }
219
220 private void drawAt(Graphics2D g, int i, Point2D.Float p, Float c) {
221 g.setColor(calcFrameColor(i));
222 paintRotatedCenteredShapeAtPoint(p, c, g);
223 }
224
225 private void paintRotatedCenteredShapeAtPoint(Float p, Float c, Graphics2D g) {
226 Shape s = getPointShape();
227 double hh = s.getBounds().getHeight() / 2;
228 double wh = s.getBounds().getWidth() / 2;
229 double t, x, y;
230 double a = c.y - p.y;
231 double b = p.x - c.x;
232 double sa = Math.signum(a);
233 double sb = Math.signum(b);
234 sa = sa == 0 ? 1 : sa;
235 sb = sb == 0 ? 1 : sb;
236 a = Math.abs(a);
237 b = Math.abs(b);
238 t = Math.atan(a / b);
239 t = sa > 0 ? sb > 0 ? -t : -Math.PI + t : sb > 0 ? t : Math.PI - t;
240 x = Math.sqrt(a * a + b * b) - wh;
241 y = -hh;
242 g.rotate(t);
243 g.translate(x, y);
244 g.fill(s);
245 g.translate(-x, -y);
246 g.rotate(-t);
247
248 }
249
250 private Point2D.Float calcPoint(float dist2go, Point2D.Float startPoint,
251 float[] sgmt, int w, int h) {
252 Float f = new Point2D.Float();
253 if (sgmt[7] == PathIterator.SEG_LINETO) {
254 // linear
255 float a = sgmt[5] - startPoint.x;
256 float b = sgmt[6] - startPoint.y;
257 float pathLen = sgmt[0];
258 f.x = startPoint.x + a * dist2go / pathLen;
259 f.y = startPoint.y + b * dist2go / pathLen;
260 } else if (sgmt[7] == PathIterator.SEG_QUADTO) {
261 // quadratic curve
262 Float ctrl = new Point2D.Float(sgmt[1]/w, sgmt[2]/h);
263 Float end = new Point2D.Float(sgmt[5]/w, sgmt[6]/h);
264 Float start = new Float(startPoint.x/w, startPoint.y/h);
265
266 // trans coords from abs to rel
267 f = getXY(dist2go / sgmt[0], start, ctrl, end);
268 f.x *= w;
269 f.y *= h;
270
271 } else if (sgmt[7] == PathIterator.SEG_CUBICTO) {
272 // bezier curve
273 float x = Math.abs(startPoint.x - sgmt[5]);
274 float y = Math.abs(startPoint.y - sgmt[6]);
275
276 // trans coords from abs to rel
277 float c1rx = Math.abs(startPoint.x - sgmt[1]) / x;
278 float c1ry = Math.abs(startPoint.y - sgmt[2]) / y;
279 float c2rx = Math.abs(startPoint.x - sgmt[3]) / x;
280 float c2ry = Math.abs(startPoint.y - sgmt[4]) / y;
281 f = getXY(dist2go / sgmt[0], c1rx, c1ry, c2rx, c2ry);
282
283 float a = startPoint.x - sgmt[5];
284 float b = startPoint.y - sgmt[6];
285
286 f.x = startPoint.x - f.x * a;
287 f.y = startPoint.y - f.y * b;
288 }
289 return f;
290 }
291
292
293 /**
294 * Calculates length of the linear segment.
295 * @param coords Segment coordinates.
296 * @param cp Start point.
297 * @return Length of the segment.
298 */
299 private float calcLine(float[] coords, Float cp) {
300 float a = cp.x - coords[0];
301 float b = cp.y - coords[1];
302 float c = (float) Math.sqrt(a * a + b * b);
303 return c;
304 }
305
306 /**
307 * Claclulates length of the cubic segment.
308 * @param coords Segment coordinates.
309 * @param cp Start point.
310 * @return Length of the segment.
311 */
312 private float calcCube(float[] coords, Float cp) {
313 float x = Math.abs(cp.x - coords[4]);
314 float y = Math.abs(cp.y - coords[5]);
315
316 // trans coords from abs to rel
317 float c1rx = Math.abs(cp.x - coords[0]) / x;
318 float c1ry = Math.abs(cp.y - coords[1]) / y;
319 float c2rx = Math.abs(cp.x - coords[2]) / x;
320 float c2ry = Math.abs(cp.y - coords[3]) / y;
321 float prevLength = 0, prevX = 0, prevY = 0;
322 for (float t = 0.01f; t <= 1.0f; t += .01f) {
323 Point2D.Float xy = getXY(t, c1rx, c1ry, c2rx, c2ry);
324 prevLength += (float) Math.sqrt((xy.x - prevX) * (xy.x - prevX)
325 + (xy.y - prevY) * (xy.y - prevY));
326 prevX = xy.x;
327 prevY = xy.y;
328 }
329 // prev len is a fraction num of the real path length
330 float z = ((Math.abs(x) + Math.abs(y)) / 2) * prevLength;
331 return z;
332 }
333
334 /**
335 * Calculates length of the quadratic segment
336 * @param coords Segment coordinates
337 * @param cp Start point.
338 * @return Length of the segment.
339 */
340 private float calcLengthOfQuad(float[] coords, Point2D.Float cp) {
341 Float ctrl = new Point2D.Float(coords[0], coords[1]);
342 Float end = new Point2D.Float(coords[2], coords[3]);
343 // get abs values
344 // ctrl1
345 float c1ax = Math.abs(cp.x - ctrl.x) ;
346 float c1ay = Math.abs(cp.y - ctrl.y) ;
347 // end1
348 float e1ax = Math.abs(cp.x - end.x) ;
349 float e1ay = Math.abs(cp.y - end.y) ;
350 // get max value on each axis
351 float maxX = Math.max(c1ax, e1ax);
352 float maxY = Math.max(c1ay, e1ay);
353
354 // trans coords from abs to rel
355 // ctrl1
356 ctrl.x = c1ax / maxX;
357 ctrl.y = c1ay / maxY;
358 // end1
359 end.x = e1ax / maxX;
360 end.y = e1ay / maxY;
361
362 // claculate length
363 float prevLength = 0, prevX = 0, prevY = 0;
364 for (float t = 0.01f; t <= 1.0f; t += .01f) {
365 Point2D.Float xy = getXY(t, new Float(0,0), ctrl, end);
366 prevLength += (float) Math.sqrt((xy.x - prevX) * (xy.x - prevX)
367 + (xy.y - prevY) * (xy.y - prevY));
368 prevX = xy.x;
369 prevY = xy.y;
370 }
371 // prev len is a fraction num of the real path length
372 float a = Math.abs(coords[2] - cp.x);
373 float b = Math.abs(coords[3] - cp.y);
374 float dist = (float) Math.sqrt(a*a+b*b);
375 return prevLength * dist;
376 }
377
378 /**
379 * Calculates the XY point for a given t value.
380 *
381 * The general spline equation is: x = b0*x0 + b1*x1 + b2*x2 + b3*x3 y =
382 * b0*y0 + b1*y1 + b2*y2 + b3*y3 where: b0 = (1-t)^3 b1 = 3 * t * (1-t)^2 b2 =
383 * 3 * t^2 * (1-t) b3 = t^3 We know that (x0,y0) == (0,0) and (x1,y1) ==
384 * (1,1) for our splines, so this simplifies to: x = b1*x1 + b2*x2 + b3 y =
385 * b1*x1 + b2*x2 + b3
386 *
387 * @author chet
388 *
389 * @param t parametric value for spline calculation
390 */
391 private Point2D.Float getXY(float t, float x1, float y1, float x2, float y2) {
392 Point2D.Float xy;
393 float invT = (1 - t);
394 float b1 = 3 * t * (invT * invT);
395 float b2 = 3 * (t * t) * invT;
396 float b3 = t * t * t;
397 xy = new Point2D.Float((b1 * x1) + (b2 * x2) + b3, (b1 * y1)
398 + (b2 * y2) + b3);
399 return xy;
400 }
401
402 /**
403 * Calculates relative position of the point on the quad curve in time t<0,1>.
404 * @param t distance on the curve
405 * @param ctrl Control point in rel coords
406 * @param end End point in rel coords
407 * @return Solution of the quad equation for time T in non complex space in rel coords.
408 */
409 public static Point2D.Float getXY(float t, Point2D.Float begin, Point2D.Float ctrl, Point2D.Float end) {
410 /*
411 * P1 = (x1, y1) - start point of curve
412 * P2 = (x2, y2) - end point of curve
413 * Pc = (xc, yc) - control point
414 *
415 * Pq(t) = P1*(1 - t)^2 + 2*Pc*t*(1 - t) + P2*t^2 =
416 * = (P1 - 2*Pc + P2)*t^2 + 2*(Pc - P1)*t + P1
417 * t = [0:1]
418 * // thx Jim ...
419 *
420 * b0 = (1 -t)^2, b1 = 2*t*(1-t), b2 = t^2
421 */
422 Point2D.Float xy;
423 float invT = (1 - t);
424 float b0 = invT * invT;
425 float b1 = 2 * t * invT ;
426 float b2 = t * t;
427 xy = new Point2D.Float(b0 * begin.x + (b1 * ctrl.x) + b2* end.x, b0 * begin.y + (b1 * ctrl.y) + b2* end.y);
428
429 return xy;
430 }
431
432 /**
433 * Selects appropriate color for given frame based on the frame position and gradient difference.
434 * @param i Frame.
435 * @return Frame color.
436 */
437 private Color calcFrameColor(final int i) {
438 if (frame == -1) {
439 return getBaseColor();
440 }
441
442 for (int t = 0; t < getTrailLength(); t++) {
443 if (direction == Direction.RIGHT
444 && i == (frame - t + getPoints()) % getPoints()) {
445 float terp = 1 - ((float) (getTrailLength() - t))
446 / (float) getTrailLength();
447 return ColorUtil.interpolate(getBaseColor(),
448 getHighlightColor(), terp);
449 } else if (direction == Direction.LEFT
450 && i == (frame + t) % getPoints()) {
451 float terp = ((float) (t)) / (float) getTrailLength();
452 return ColorUtil.interpolate(getBaseColor(),
453 getHighlightColor(), terp);
454 }
455 }
456 return getBaseColor();
457 }
458
459 /**
460 * Gets current frame.
461 * @return Current frame.
462 */
463 public int getFrame() {
464 return frame;
465 }
466
467 /**Sets current frame.
468 * @param frame Current frame.
469 */
470 public void setFrame(int frame) {
471 int old = getFrame();
472 this.frame = frame;
473 firePropertyChange("frame", old, getFrame());
474 }
475
476 /**
477 * Gets base color.
478 * @return Base color.
479 */
480 public Color getBaseColor() {
481 return baseColor;
482 }
483
484 /**
485 * Sets new base color. Bound property.
486 * @param baseColor Base color.
487 */
488 public void setBaseColor(Color baseColor) {
489 Color old = getBaseColor();
490 this.baseColor = baseColor;
491 firePropertyChange("baseColor", old, getBaseColor());
492 }
493
494 /**
495 * Gets highlight color.
496 * @return Current highlight color.
497 */
498 public Color getHighlightColor() {
499 return highlightColor;
500 }
501
502 /**
503 * Sets new highlight color. Bound property.
504 * @param highlightColor New highlight color.
505 */
506 public void setHighlightColor(Color highlightColor) {
507 Color old = getHighlightColor();
508 this.highlightColor = highlightColor;
509 firePropertyChange("highlightColor", old, getHighlightColor());
510 }
511
512 /**
513 * Gets total amount of distinct points in spinner.
514 * @return Total amount of points.
515 */
516 public int getPoints() {
517 return points;
518 }
519
520 /**
521 * Sets total amount of points in spinner. Bound property.
522 * @param points Total amount of points.
523 */
524 public void setPoints(int points) {
525 int old = getPoints();
526 this.points = points;
527 firePropertyChange("points", old, getPoints());
528 }
529
530 /**
531 * Gets length of trail in number of points.
532 * @return Trail lenght.
533 */
534 public int getTrailLength() {
535 return trailLength;
536 }
537
538 /**
539 * Sets length of the trail in points. Bound property.
540 * @param trailLength Trail length in points.
541 */
542 public void setTrailLength(int trailLength) {
543 int old = getTrailLength();
544 this.trailLength = trailLength;
545 firePropertyChange("trailLength", old, getTrailLength());
546 }
547
548 /**
549 * Gets shape of current point.
550 * @return Shape of the point.
551 */
552 public final Shape getPointShape() {
553 return pointShape;
554 }
555
556 /**
557 * Sets new point shape. Bound property.
558 * @param pointShape new Shape.
559 */
560 public final void setPointShape(Shape pointShape) {
561 Shape old = getPointShape();
562 this.pointShape = pointShape;
563 if (getPointShape() != old && getPointShape() != null
564 && !getPointShape().equals(old)) {
565 firePropertyChange("pointShape", old, getPointShape());
566 }
567 }
568
569 /**
570 * Gets current trajectory.
571 * @return Current spinner trajectory .
572 */
573 public final Shape getTrajectory() {
574 return trajectory;
575 }
576
577 /**
578 * Sets new trajectory. Expected trajectory have to be closed shape. Bound property.
579 * @param trajectory New trajectory.
580 */
581 public final void setTrajectory(Shape trajectory) {
582 Shape old = getTrajectory();
583 this.trajectory = trajectory;
584 if (getTrajectory() != old && getTrajectory() != null
585 && !getTrajectory().equals(old)) {
586 firePropertyChange("trajectory", old, getTrajectory());
587 }
588 }
589
590 /**
591 * Sets new spinning direction.
592 * @param dir Spinning direction.
593 */
594 public void setDirection(Direction dir) {
595 Direction old = getDirection();
596 this.direction = dir;
597 if (getDirection() != old && getDirection() != null
598 && !getDirection().equals(old)) {
599 firePropertyChange("direction", old, getDirection());
600 }
601 }
602
603 /**
604 * Gets current direction of spinning.
605 * @return Current spinning direction.
606 */
607 public Direction getDirection() {
608 return this.direction;
609 }
610
611 protected Shape provideShape(Graphics2D g, Object comp, int width, int height) {
612 return new Rectangle(0,0,width,height);
613 }
614
615 /**
616 * Centers shape in the area covered by the painter.
617 * @param paintCentered Centering hint.
618 */
619 public void setPaintCentered(boolean paintCentered) {
620 boolean old = isPaintCentered();
621 this.paintCentered = paintCentered;
622 firePropertyChange("paintCentered", old, isPaintCentered());
623 }
624
625 }