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