001/* ============ 002 * FXGraphics2D 003 * ============ 004 * 005 * (C)opyright 2014-2021, by Object Refinery Limited. 006 * 007 * http://www.jfree.org/fxgraphics2d/index.html 008 * 009 * The FXGraphics2D class has been developed by Object Refinery Limited for 010 * use in Orson Charts (http://www.object-refinery.com/orsoncharts) and 011 * JFreeChart (http://www.jfree.org/jfreechart). It may be useful for other 012 * code that uses the Graphics2D API provided by Java2D. 013 * 014 * Redistribution and use in source and binary forms, with or without 015 * modification, are permitted provided that the following conditions are met: 016 * - Redistributions of source code must retain the above copyright 017 * notice, this list of conditions and the following disclaimer. 018 * - Redistributions in binary form must reproduce the above copyright 019 * notice, this list of conditions and the following disclaimer in the 020 * documentation and/or other materials provided with the distribution. 021 * - Neither the name of the Object Refinery Limited nor the 022 * names of its contributors may be used to endorse or promote products 023 * derived from this software without specific prior written permission. 024 * 025 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 026 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 027 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 028 * ARE DISCLAIMED. IN NO EVENT SHALL OBJECT REFINERY LIMITED BE LIABLE FOR ANY 029 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 030 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 031 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 032 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 033 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 034 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 035 * 036 */ 037 038package org.jfree.fx; 039 040import java.awt.AlphaComposite; 041import java.awt.BasicStroke; 042import java.awt.Color; 043import java.awt.Composite; 044import java.awt.Font; 045import java.awt.FontMetrics; 046import java.awt.GradientPaint; 047import java.awt.Graphics; 048import java.awt.Graphics2D; 049import java.awt.GraphicsConfiguration; 050import java.awt.Image; 051import java.awt.LinearGradientPaint; 052import java.awt.MultipleGradientPaint; 053import java.awt.Paint; 054import java.awt.RadialGradientPaint; 055import java.awt.Rectangle; 056import java.awt.RenderingHints; 057import java.awt.Shape; 058import java.awt.Stroke; 059import java.awt.font.FontRenderContext; 060import java.awt.font.GlyphVector; 061import java.awt.font.TextLayout; 062import java.awt.geom.AffineTransform; 063import java.awt.geom.Arc2D; 064import java.awt.geom.Area; 065import java.awt.geom.Ellipse2D; 066import java.awt.geom.GeneralPath; 067import java.awt.geom.Line2D; 068import java.awt.geom.NoninvertibleTransformException; 069import java.awt.geom.Path2D; 070import java.awt.geom.PathIterator; 071import java.awt.geom.Point2D; 072import java.awt.geom.Rectangle2D; 073import java.awt.geom.RoundRectangle2D; 074import java.awt.image.BufferedImage; 075import java.awt.image.BufferedImageOp; 076import java.awt.image.ColorModel; 077import java.awt.image.ImageObserver; 078import java.awt.image.RenderedImage; 079import java.awt.image.WritableRaster; 080import java.awt.image.renderable.RenderableImage; 081import java.text.AttributedCharacterIterator; 082import java.util.Arrays; 083import java.util.Hashtable; 084import java.util.Map; 085import java.util.Set; 086import javafx.embed.swing.SwingFXUtils; 087 088import javafx.scene.canvas.Canvas; 089import javafx.scene.canvas.GraphicsContext; 090import javafx.scene.effect.BlendMode; 091import javafx.scene.paint.CycleMethod; 092import javafx.scene.paint.LinearGradient; 093import javafx.scene.paint.RadialGradient; 094import javafx.scene.paint.Stop; 095import javafx.scene.shape.ArcType; 096import javafx.scene.shape.FillRule; 097import javafx.scene.shape.StrokeLineCap; 098import javafx.scene.shape.StrokeLineJoin; 099import javafx.scene.text.FontPosture; 100import javafx.scene.text.FontWeight; 101 102/** 103 * A {@link Graphics2D} implementation that writes to a JavaFX {@link Canvas}. 104 * This is intended for general purpose usage, but has been created for use in 105 * Orson Charts (<a href="http://www.object-refinery.com/orsoncharts/">http://www.object-refinery.com/orsoncharts/</a>) and 106 * JFreeChart (<a href="http://www.jfree.org/jfreechart/">http://www.jfree.org/jfreechart/</a>). 107 */ 108public class FXGraphics2D extends Graphics2D { 109 110 /** The graphics context for the JavaFX canvas. */ 111 private final GraphicsContext gc; 112 113 /** Rendering hints. */ 114 private final RenderingHints hints; 115 116 private Shape clip; 117 118 /** Stores the AWT Paint object for get/setPaint(). */ 119 private Paint paint = Color.BLACK; 120 121 /** Stores the AWT Color object for get/setColor(). */ 122 private Color color = Color.BLACK; 123 124 private Composite composite = AlphaComposite.getInstance( 125 AlphaComposite.SRC_OVER, 1.0f); 126 127 private Stroke stroke = new BasicStroke(1.0f); 128 129 /** 130 * The width of the stroke to use when the user supplies a 131 * BasicStroke with a width of 0.0 (in this case the Java specification 132 * says "If width is set to 0.0f, the stroke is rendered as the thinnest 133 * possible line for the target device and the antialias hint setting.") 134 */ 135 private double zeroStrokeWidth; 136 137 private Font font = new Font("SansSerif", Font.PLAIN, 12); 138 139 private final FontRenderContext fontRenderContext = new FontRenderContext( 140 null, false, true); 141 142 private AffineTransform transform = new AffineTransform(); 143 144 /** The background color, used in the {@code clearRect()} method. */ 145 private Color background = Color.BLACK; 146 147 /** A flag that is set when the JavaFX graphics state has been saved. */ 148 private boolean stateSaved = false; 149 150 private Stroke savedStroke; 151 private Paint savedPaint; 152 private Color savedColor; 153 private Font savedFont; 154 private AffineTransform savedTransform; 155 156 /** 157 * An instance that is lazily instantiated in drawLine and then 158 * subsequently reused to avoid creating a lot of garbage. 159 */ 160 private Line2D line; 161 162 /** 163 * An instance that is lazily instantiated in fillRect and then 164 * subsequently reused to avoid creating a lot of garbage. 165 */ 166 Rectangle2D rect; 167 168 /** 169 * An instance that is lazily instantiated in draw/fillRoundRect and then 170 * subsequently reused to avoid creating a lot of garbage. 171 */ 172 private RoundRectangle2D roundRect; 173 174 /** 175 * An instance that is lazily instantiated in draw/fillOval and then 176 * subsequently reused to avoid creating a lot of garbage. 177 */ 178 private Ellipse2D oval; 179 180 /** 181 * An instance that is lazily instantiated in draw/fillArc and then 182 * subsequently reused to avoid creating a lot of garbage. 183 */ 184 private Arc2D arc; 185 186 /** A hidden image used for font metrics. */ 187 private BufferedImage fmImage; 188 189 /** 190 * A Graphics2D instance for the hidden image that is used for font 191 * metrics. Used in the getFontMetrics(Font f) method. 192 */ 193 private Graphics2D fmImageG2; 194 195 /** The FXFontMetrics. */ 196 private FXFontMetrics fxFontMetrics; 197 198 /** 199 * The device configuration (this is lazily instantiated in the 200 * getDeviceConfiguration() method). 201 */ 202 private GraphicsConfiguration deviceConfiguration; 203 204 /** 205 * Throws an {@code IllegalArgumentException} if {@code arg} is 206 * {@code null}. 207 * 208 * @param arg the argument to check. 209 * @param name the name of the argument (to display in the exception 210 * message). 211 */ 212 private static void nullNotPermitted(Object arg, String name) { 213 if (arg == null) { 214 throw new IllegalArgumentException("Null '" + name + "' argument."); 215 } 216 } 217 218 /** 219 * Creates a new instance that will render to the specified JavaFX 220 * {@code GraphicsContext}. 221 * 222 * @param gc the graphics context ({@code null} not permitted). 223 */ 224 public FXGraphics2D(GraphicsContext gc) { 225 nullNotPermitted(gc, "gc"); 226 this.gc = gc; 227 this.zeroStrokeWidth = 0.5; 228 this.hints = new RenderingHints(RenderingHints.KEY_ANTIALIASING, 229 RenderingHints.VALUE_ANTIALIAS_DEFAULT); 230 this.hints.put(FXHints.KEY_USE_FX_FONT_METRICS, true); 231 } 232 233 /** 234 * Returns the width to use for the stroke when the AWT stroke 235 * specified has a zero width (the default value is {@code 0.5}). 236 * <p>In the Java specification for {@code BasicStroke} it states "If width 237 * is set to 0.0f, the stroke is rendered as the thinnest possible 238 * line for the target device and the antialias hint setting." We don't 239 * have a means to implement that accurately since we must specify a fixed 240 * width to the JavaFX canvas - this attribute is the width that is 241 * used.</p> 242 * 243 * @return The width. 244 */ 245 public double getZeroStrokeWidth() { 246 return this.zeroStrokeWidth; 247 } 248 249 /** 250 * Sets the width to use for the stroke when setting a new AWT stroke that 251 * has a width of {@code 0.0}. 252 * 253 * @param width the new width (must be 0.0 or greater). 254 */ 255 public void setZeroStrokeWidth(double width) { 256 if (width < 0.0) { 257 throw new IllegalArgumentException("Width cannot be negative."); 258 } 259 this.zeroStrokeWidth = width; 260 } 261 262 /** 263 * Returns the device configuration associated with this 264 * {@code Graphics2D}. 265 * 266 * @return The device configuration (never {@code null}). 267 */ 268 @Override 269 public GraphicsConfiguration getDeviceConfiguration() { 270 if (this.deviceConfiguration == null) { 271 int width = (int) this.gc.getCanvas().getWidth(); 272 int height = (int) this.gc.getCanvas().getHeight(); 273 this.deviceConfiguration = new FXGraphicsConfiguration(width, 274 height); 275 } 276 return this.deviceConfiguration; 277 } 278 279 /** 280 * Creates a new graphics object that is a copy of this graphics object. 281 * 282 * @return A new graphics object. 283 */ 284 @Override 285 public Graphics create() { 286 FXGraphics2D copy = new FXGraphics2D(this.gc); 287 copy.setRenderingHints(getRenderingHints()); 288 copy.setClip(getClip()); 289 copy.setPaint(getPaint()); 290 copy.setColor(getColor()); 291 copy.setComposite(getComposite()); 292 copy.setStroke(getStroke()); 293 copy.setFont(getFont()); 294 copy.setTransform(getTransform()); 295 copy.setBackground(getBackground()); 296 return copy; 297 } 298 299 /** 300 * Returns the paint used to draw or fill shapes (or text). The default 301 * value is {@link Color#BLACK}. This attribute is updated by both the 302 * {@link #setPaint(java.awt.Paint)} and {@link #setColor(java.awt.Color)} 303 * methods. 304 * 305 * @return The paint (never {@code null}). 306 * 307 * @see #setPaint(java.awt.Paint) 308 */ 309 @Override 310 public Paint getPaint() { 311 return this.paint; 312 } 313 314 /** 315 * Sets the paint used to draw or fill shapes (or text). If 316 * {@code paint} is an instance of {@code Color}, this method will 317 * also update the current color attribute (see {@link #getColor()}). If 318 * you pass {@code null} to this method, it does nothing (in 319 * accordance with the JDK specification). 320 * <br><br> 321 * Note that this implementation will map {@link Color}, 322 * {@link GradientPaint}, {@link LinearGradientPaint} and 323 * {@link RadialGradientPaint} to JavaFX equivalents, other paint 324 * implementations are not handled. 325 * 326 * @param paint the paint ({@code null} is permitted but ignored). 327 * 328 * @see #getPaint() 329 */ 330 @Override 331 public void setPaint(Paint paint) { 332 if (paint == null) { 333 return; 334 } 335 if (paintsAreEqual(paint, this.paint)) { 336 return; 337 } 338 applyPaint(paint); 339 } 340 341 private CycleMethod toJavaFXCycleMethod(MultipleGradientPaint.CycleMethod method) { 342 switch (method) { 343 case NO_CYCLE: 344 return CycleMethod.NO_CYCLE; 345 case REFLECT: 346 return CycleMethod.REFLECT; 347 case REPEAT: 348 return CycleMethod.REPEAT; 349 default: 350 throw new IllegalStateException("Unknown cycle method " + method); 351 } 352 } 353 354 private void applyPaint(Paint paint) { 355 this.paint = paint; 356 if (paint instanceof Color) { 357 setColor((Color) paint); 358 } else if (paint instanceof GradientPaint) { 359 GradientPaint gp = (GradientPaint) paint; 360 Stop[] stops = new Stop[] { new Stop(0, 361 awtColorToJavaFX(gp.getColor1())), 362 new Stop(1, awtColorToJavaFX(gp.getColor2())) }; 363 Point2D p1 = gp.getPoint1(); 364 Point2D p2 = gp.getPoint2(); 365 CycleMethod cm = gp.isCyclic() ? CycleMethod.REFLECT : CycleMethod.NO_CYCLE; 366 LinearGradient lg = new LinearGradient(p1.getX(), p1.getY(), 367 p2.getX(), p2.getY(), false, cm, stops); 368 this.gc.setStroke(lg); 369 this.gc.setFill(lg); 370 } else if (paint instanceof MultipleGradientPaint) { 371 MultipleGradientPaint mgp = (MultipleGradientPaint) paint; 372 Color[] colors = mgp.getColors(); 373 float[] fractions = mgp.getFractions(); 374 Stop[] stops = new Stop[colors.length]; 375 for (int i = 0; i < colors.length; i++) { 376 stops[i] = new Stop(fractions[i], awtColorToJavaFX(colors[i])); 377 } 378 379 if (paint instanceof RadialGradientPaint) { 380 RadialGradientPaint rgp = (RadialGradientPaint) paint; 381 Point2D center = rgp.getCenterPoint(); 382 Point2D focus = rgp.getFocusPoint(); 383 double focusDistance = focus.distance(center); 384 double focusAngle = 0.0; 385 if (!focus.equals(center)) { 386 focusAngle = Math.atan2(focus.getY() - center.getY(), 387 focus.getX() - center.getX()); 388 } 389 double radius = rgp.getRadius(); 390 RadialGradient rg = new RadialGradient( 391 Math.toDegrees(focusAngle), focusDistance / radius, 392 center.getX(), center.getY(), radius, false, 393 toJavaFXCycleMethod(rgp.getCycleMethod()), stops); 394 this.gc.setStroke(rg); 395 this.gc.setFill(rg); 396 } else if (paint instanceof LinearGradientPaint) { 397 LinearGradientPaint lgp = (LinearGradientPaint) paint; 398 Point2D start = lgp.getStartPoint(); 399 Point2D end = lgp.getEndPoint(); 400 LinearGradient lg = new LinearGradient(start.getX(), 401 start.getY(), end.getX(), end.getY(), false, 402 toJavaFXCycleMethod(lgp.getCycleMethod()), stops); 403 this.gc.setStroke(lg); 404 this.gc.setFill(lg); 405 } 406 } else { 407 // this is a paint we don't recognise 408 } 409 } 410 411 /** 412 * Returns the foreground color. This method exists for backwards 413 * compatibility in AWT, you should use the {@link #getPaint()} method. 414 * This attribute is updated by the {@link #setColor(java.awt.Color)} 415 * method, and also by the {@link #setPaint(java.awt.Paint)} method if 416 * a {@code Color} instance is passed to the method. 417 * 418 * @return The foreground color (never {@code null}). 419 * 420 * @see #getPaint() 421 */ 422 @Override 423 public Color getColor() { 424 return this.color; 425 } 426 427 /** 428 * Sets the foreground color. This method exists for backwards 429 * compatibility in AWT, you should use the 430 * {@link #setPaint(java.awt.Paint)} method. 431 * 432 * @param c the color ({@code null} permitted but ignored). 433 * 434 * @see #setPaint(java.awt.Paint) 435 */ 436 @Override 437 public void setColor(Color c) { 438 if (c == null || c.equals(this.color)) { 439 return; 440 } 441 applyColor(c); 442 } 443 444 private void applyColor(Color c) { 445 this.color = c; 446 this.paint = c; 447 javafx.scene.paint.Color fxcolor = awtColorToJavaFX(c); 448 this.gc.setFill(fxcolor); 449 this.gc.setStroke(fxcolor); 450 } 451 452 /** 453 * Returns a JavaFX color that is equivalent to the specified AWT color. 454 * 455 * @param c the color ({@code null} not permitted). 456 * 457 * @return A JavaFX color. 458 */ 459 private javafx.scene.paint.Color awtColorToJavaFX(Color c) { 460 return javafx.scene.paint.Color.rgb(c.getRed(), c.getGreen(), 461 c.getBlue(), c.getAlpha() / 255.0); 462 } 463 464 /** 465 * Returns the background color (the default value is {@link Color#BLACK}). 466 * This attribute is used by the {@link #clearRect(int, int, int, int)} 467 * method. 468 * 469 * @return The background color (possibly {@code null}). 470 * 471 * @see #setBackground(java.awt.Color) 472 */ 473 @Override 474 public Color getBackground() { 475 return this.background; 476 } 477 478 /** 479 * Sets the background color. This attribute is used by the 480 * {@link #clearRect(int, int, int, int)} method. The reference 481 * implementation allows {@code null} for the background color so 482 * we allow that too (but for that case, the {@link #clearRect(int, int, int, int)} 483 * method will do nothing). 484 * 485 * @param color the color ({@code null} permitted). 486 * 487 * @see #getBackground() 488 */ 489 @Override 490 public void setBackground(Color color) { 491 this.background = color; 492 } 493 494 /** 495 * Returns the current composite. 496 * 497 * @return The current composite (never {@code null}). 498 * 499 * @see #setComposite(java.awt.Composite) 500 */ 501 @Override 502 public Composite getComposite() { 503 return this.composite; 504 } 505 506 /** 507 * Sets the composite. There is limited handling for 508 * {@code AlphaComposite}, other composites will have no effect on the 509 * output. 510 * 511 * @param comp the composite ({@code null} not permitted). 512 * 513 * @see #getComposite() 514 */ 515 @Override 516 public void setComposite(Composite comp) { 517 nullNotPermitted(comp, "comp"); 518 this.composite = comp; 519 if (comp instanceof AlphaComposite) { 520 AlphaComposite ac = (AlphaComposite) comp; 521 this.gc.setGlobalAlpha(ac.getAlpha()); 522 this.gc.setGlobalBlendMode(blendMode(ac.getRule())); 523 } 524 } 525 526 /** 527 * Returns a JavaFX BlendMode that is the closest match for the Java2D 528 * alpha composite rule. 529 * 530 * @param rule the rule. 531 * 532 * @return The blend mode. 533 */ 534 private BlendMode blendMode(int rule) { 535 switch (rule) { 536 case AlphaComposite.SRC_ATOP: 537 return BlendMode.SRC_ATOP; 538 case AlphaComposite.CLEAR: 539 case AlphaComposite.DST: 540 case AlphaComposite.DST_ATOP: 541 case AlphaComposite.DST_IN: 542 case AlphaComposite.DST_OUT: 543 case AlphaComposite.DST_OVER: 544 case AlphaComposite.SRC: 545 case AlphaComposite.SRC_IN: 546 case AlphaComposite.SRC_OUT: 547 case AlphaComposite.SRC_OVER: 548 case AlphaComposite.XOR: 549 return BlendMode.SRC_OVER; 550 default: 551 return BlendMode.SRC_OVER; 552 } 553 } 554 555 /** 556 * Returns the current stroke (this attribute is used when drawing shapes). 557 * 558 * @return The current stroke (never {@code null}). 559 * 560 * @see #setStroke(java.awt.Stroke) 561 */ 562 @Override 563 public Stroke getStroke() { 564 return this.stroke; 565 } 566 567 /** 568 * Sets the stroke that will be used to draw shapes. 569 * 570 * @param s the stroke ({@code null} not permitted). 571 * 572 * @see #getStroke() 573 */ 574 @Override 575 public void setStroke(Stroke s) { 576 nullNotPermitted(s, "s"); 577 if (s == this.stroke) { // quick test, full equals test later 578 return; 579 } 580 if (stroke instanceof BasicStroke) { 581 BasicStroke bs = (BasicStroke) s; 582 if (bs.equals(this.stroke)) { 583 return; // no change 584 } 585 } 586 this.stroke = s; 587 applyStroke(s); 588 } 589 590 private void applyStroke(Stroke s) { 591 if (s instanceof BasicStroke) { 592 applyBasicStroke((BasicStroke) s); 593 } 594 } 595 596 private void applyBasicStroke(BasicStroke bs) { 597 double lineWidth = bs.getLineWidth(); 598 if (lineWidth == 0.0) { 599 lineWidth = this.zeroStrokeWidth; 600 } 601 this.gc.setLineWidth(lineWidth); 602 this.gc.setLineCap(awtToJavaFXLineCap(bs.getEndCap())); 603 this.gc.setLineJoin(awtToJavaFXLineJoin(bs.getLineJoin())); 604 this.gc.setMiterLimit(bs.getMiterLimit()); 605 this.gc.setLineDashes(floatToDoubleArray(bs.getDashArray())); 606 this.gc.setLineDashOffset(bs.getDashPhase()); 607 } 608 609 /** 610 * Maps a line cap code from AWT to the corresponding JavaFX StrokeLineCap 611 * enum value. 612 * 613 * @param c the line cap code. 614 * 615 * @return A JavaFX line cap value. 616 */ 617 private StrokeLineCap awtToJavaFXLineCap(int c) { 618 if (c == BasicStroke.CAP_BUTT) { 619 return StrokeLineCap.BUTT; 620 } else if (c == BasicStroke.CAP_ROUND) { 621 return StrokeLineCap.ROUND; 622 } else if (c == BasicStroke.CAP_SQUARE) { 623 return StrokeLineCap.SQUARE; 624 } else { 625 throw new IllegalArgumentException("Unrecognised cap code: " + c); 626 } 627 } 628 629 /** 630 * Maps a line join code from AWT to the corresponding JavaFX 631 * StrokeLineJoin enum value. 632 * 633 * @param j the line join code. 634 * 635 * @return A JavaFX line join value. 636 */ 637 private StrokeLineJoin awtToJavaFXLineJoin(int j) { 638 if (j == BasicStroke.JOIN_BEVEL) { 639 return StrokeLineJoin.BEVEL; 640 } else if (j == BasicStroke.JOIN_MITER) { 641 return StrokeLineJoin.MITER; 642 } else if (j == BasicStroke.JOIN_ROUND) { 643 return StrokeLineJoin.ROUND; 644 } else { 645 throw new IllegalArgumentException("Unrecognised join code: " + j); 646 } 647 } 648 649 private double[] floatToDoubleArray(float[] f) { 650 if (f == null) { 651 return null; 652 } 653 double[] d = new double[f.length]; 654 for (int i = 0; i < f.length; i++) { 655 d[i] = (double) f[i]; 656 } 657 return d; 658 } 659 660 /** 661 * Returns the current value for the specified hint. 662 * 663 * @param hintKey the hint key ({@code null} permitted, but the 664 * result will be {@code null} also in that case). 665 * 666 * @return The current value for the specified hint 667 * (possibly {@code null}). 668 * 669 * @see #setRenderingHint(java.awt.RenderingHints.Key, java.lang.Object) 670 */ 671 @Override 672 public Object getRenderingHint(RenderingHints.Key hintKey) { 673 return this.hints.get(hintKey); 674 } 675 676 /** 677 * Sets the value for a hint. See the {@link FXHints} class for 678 * information about the hints that can be used with this implementation. 679 * 680 * @param hintKey the hint key ({@code null} not permitted). 681 * @param hintValue the hint value. 682 * 683 * @see #getRenderingHint(java.awt.RenderingHints.Key) 684 */ 685 @Override 686 public void setRenderingHint(RenderingHints.Key hintKey, Object hintValue) { 687 this.hints.put(hintKey, hintValue); 688 } 689 690 /** 691 * Returns a copy of the rendering hints. Modifying the returned copy 692 * will have no impact on the state of this {@code Graphics2D} 693 * instance. 694 * 695 * @return The rendering hints (never {@code null}). 696 * 697 * @see #setRenderingHints(java.util.Map) 698 */ 699 @Override 700 public RenderingHints getRenderingHints() { 701 return (RenderingHints) this.hints.clone(); 702 } 703 704 /** 705 * Sets the rendering hints to the specified collection. 706 * 707 * @param hints the new set of hints ({@code null} not permitted). 708 * 709 * @see #getRenderingHints() 710 */ 711 @Override 712 public void setRenderingHints(Map<?, ?> hints) { 713 this.hints.clear(); 714 this.hints.putAll(hints); 715 } 716 717 /** 718 * Adds all the supplied rendering hints. 719 * 720 * @param hints the hints ({@code null} not permitted). 721 */ 722 @Override 723 public void addRenderingHints(Map<?, ?> hints) { 724 this.hints.putAll(hints); 725 } 726 727 /** 728 * Draws the specified shape with the current {@code paint} and 729 * {@code stroke}. There is direct handling for {@code Line2D}, 730 * {@code Rectangle2D}, {@code Ellipse2D}, {@code Arc2D} and 731 * {@code Path2D}. All other shapes are mapped to a path outline and then 732 * drawn. 733 * 734 * @param s the shape ({@code null} not permitted). 735 * 736 * @see #fill(java.awt.Shape) 737 */ 738 @Override 739 public void draw(Shape s) { 740 // if the current stroke is not a BasicStroke then it is handled as 741 // a special case 742 if (!(this.stroke instanceof BasicStroke)) { 743 fill(this.stroke.createStrokedShape(s)); 744 return; 745 } 746 if (s instanceof Line2D) { 747 Line2D l = (Line2D) s; 748 Object hint = getRenderingHint(RenderingHints.KEY_STROKE_CONTROL); 749 if (hint != RenderingHints.VALUE_STROKE_PURE) { 750 double x1 = Math.rint(l.getX1()) - 0.5; 751 double y1 = Math.rint(l.getY1()) - 0.5; 752 double x2 = Math.rint(l.getX2()) - 0.5; 753 double y2 = Math.rint(l.getY2()) - 0.5; 754 l = line(x1, y1, x2, y2); 755 } 756 this.gc.strokeLine(l.getX1(), l.getY1(), l.getX2(), l.getY2()); 757 } else if (s instanceof Rectangle2D) { 758 Rectangle2D r = (Rectangle2D) s; 759 if (s instanceof Rectangle) { 760 r = new Rectangle2D.Double(r.getX(), r.getY(), r.getWidth(), 761 r.getHeight()); 762 } 763 Object hint = getRenderingHint(RenderingHints.KEY_STROKE_CONTROL); 764 if (hint != RenderingHints.VALUE_STROKE_PURE) { 765 double x = Math.rint(r.getX()) - 0.5; 766 double y = Math.rint(r.getY()) - 0.5; 767 double w = Math.floor(r.getWidth()); 768 double h = Math.floor(r.getHeight()); 769 r = rect(x, y, w, h); 770 } 771 this.gc.strokeRect(r.getX(), r.getY(), r.getWidth(), r.getHeight()); 772 } else if (s instanceof RoundRectangle2D) { 773 RoundRectangle2D rr = (RoundRectangle2D) s; 774 this.gc.strokeRoundRect(rr.getX(), rr.getY(), rr.getWidth(), 775 rr.getHeight(), rr.getArcWidth(), rr.getArcHeight()); 776 } else if (s instanceof Ellipse2D) { 777 Ellipse2D e = (Ellipse2D) s; 778 this.gc.strokeOval(e.getX(), e.getY(), e.getWidth(), e.getHeight()); 779 } else if (s instanceof Arc2D) { 780 Arc2D a = (Arc2D) s; 781 this.gc.strokeArc(a.getX(), a.getY(), a.getWidth(), a.getHeight(), 782 a.getAngleStart(), a.getAngleExtent(), 783 intToArcType(a.getArcType())); 784 } else { 785 shapeToPath(s); 786 this.gc.stroke(); 787 } 788 } 789 790 private final double[] coords = new double[6]; 791 792 /** 793 * Maps a shape to a path in the graphics context. 794 * 795 * @param s the shape ({@code null} not permitted). 796 */ 797 private void shapeToPath(Shape s) { 798 if (s instanceof Path2D) { 799 Path2D path = (Path2D) s; 800 if (path.getWindingRule() == Path2D.WIND_EVEN_ODD) { 801 this.gc.setFillRule(FillRule.EVEN_ODD); 802 } else { 803 this.gc.setFillRule(FillRule.NON_ZERO); 804 } 805 } 806 this.gc.beginPath(); 807 PathIterator iterator = s.getPathIterator(null); 808 while (!iterator.isDone()) { 809 int segType = iterator.currentSegment(coords); 810 switch (segType) { 811 case PathIterator.SEG_MOVETO: 812 this.gc.moveTo(coords[0], coords[1]); 813 break; 814 case PathIterator.SEG_LINETO: 815 this.gc.lineTo(coords[0], coords[1]); 816 break; 817 case PathIterator.SEG_QUADTO: 818 this.gc.quadraticCurveTo(coords[0], coords[1], coords[2], 819 coords[3]); 820 break; 821 case PathIterator.SEG_CUBICTO: 822 this.gc.bezierCurveTo(coords[0], coords[1], coords[2], 823 coords[3], coords[4], coords[5]); 824 break; 825 case PathIterator.SEG_CLOSE: 826 this.gc.closePath(); 827 break; 828 default: 829 throw new RuntimeException("Unrecognised segment type " 830 + segType); 831 } 832 iterator.next(); 833 } 834 } 835 836 private ArcType intToArcType(int t) { 837 if (t == Arc2D.CHORD) { 838 return ArcType.CHORD; 839 } else if (t == Arc2D.OPEN) { 840 return ArcType.OPEN; 841 } else if (t == Arc2D.PIE) { 842 return ArcType.ROUND; 843 } 844 throw new IllegalArgumentException("Unrecognised t: " + t); 845 } 846 847 /** 848 * Fills the specified shape with the current {@code paint}. There is 849 * direct handling for {@code RoundRectangle2D}, 850 * {@code Rectangle2D}, {@code Ellipse2D} and {@code Arc2D}. 851 * All other shapes are mapped to a path outline and then filled. 852 * 853 * @param s the shape ({@code null} not permitted). 854 * 855 * @see #draw(java.awt.Shape) 856 */ 857 @Override 858 public void fill(Shape s) { 859 if (s instanceof Rectangle2D) { 860 Rectangle2D r = (Rectangle2D) s; 861 this.gc.fillRect(r.getX(), r.getY(), r.getWidth(), r.getHeight()); 862 } else if (s instanceof RoundRectangle2D) { 863 RoundRectangle2D rr = (RoundRectangle2D) s; 864 this.gc.fillRoundRect(rr.getX(), rr.getY(), rr.getWidth(), 865 rr.getHeight(), rr.getArcWidth(), rr.getArcHeight()); 866 } else if (s instanceof Ellipse2D) { 867 Ellipse2D e = (Ellipse2D) s; 868 this.gc.fillOval(e.getX(), e.getY(), e.getWidth(), e.getHeight()); 869 } else if (s instanceof Arc2D) { 870 Arc2D a = (Arc2D) s; 871 this.gc.fillArc(a.getX(), a.getY(), a.getWidth(), a.getHeight(), 872 a.getAngleStart(), a.getAngleExtent(), 873 intToArcType(a.getArcType())); 874 } else { 875 shapeToPath(s); 876 this.gc.fill(); 877 } 878 } 879 880 /** 881 * Returns the current font used for drawing text. 882 * 883 * @return The current font (never {@code null}). 884 * 885 * @see #setFont(java.awt.Font) 886 */ 887 @Override 888 public Font getFont() { 889 return this.font; 890 } 891 892 /** 893 * Sets the font to be used for drawing text. 894 * 895 * @param font the font ({@code null} is permitted but ignored). 896 * 897 * @see #getFont() 898 */ 899 @Override 900 public void setFont(Font font) { 901 if (font == null || this.font.equals(font)) { 902 return; 903 } 904 applyFont(font); 905 } 906 907 private void applyFont(Font font) { 908 this.font = font; 909 FontWeight weight = font.isBold() ? FontWeight.BOLD : FontWeight.NORMAL; 910 FontPosture posture = font.isItalic() 911 ? FontPosture.ITALIC : FontPosture.REGULAR; 912 javafx.scene.text.Font jfxfont = javafx.scene.text.Font.font( 913 font.getFamily(), weight, posture, font.getSize()); 914 this.gc.setFont(jfxfont); 915 } 916 917 /** 918 * Returns the font metrics for the specified font. The font metrics 919 * returned are from Java2D (via an internal {@code BufferedImage}) which 920 * does not always match exactly the font metrics used by JavaFX. 921 * 922 * @param f the font. 923 * 924 * @return The font metrics. 925 */ 926 @Override 927 public FontMetrics getFontMetrics(Font f) { 928 if (getRenderingHint(FXHints.KEY_USE_FX_FONT_METRICS) == Boolean.TRUE) { 929 if (this.fxFontMetrics == null 930 || !f.equals(this.fxFontMetrics.getFont())) { 931 this.fxFontMetrics = new FXFontMetrics(this.font, this); 932 } 933 return this.fxFontMetrics; 934 } 935 936 // be lazy about creating the underlying objects... 937 if (this.fmImage == null) { 938 this.fmImage = new BufferedImage(10, 10, 939 BufferedImage.TYPE_INT_RGB); 940 this.fmImageG2 = this.fmImage.createGraphics(); 941 this.fmImageG2.setRenderingHint( 942 RenderingHints.KEY_FRACTIONALMETRICS, 943 RenderingHints.VALUE_FRACTIONALMETRICS_ON); 944 } 945 return this.fmImageG2.getFontMetrics(f); 946 } 947 948 /** 949 * Returns the font render context. The implementation here returns the 950 * {@code FontRenderContext} for an image that is maintained 951 * internally (as for {@link #getFontMetrics}). 952 * 953 * @return The font render context. 954 */ 955 @Override 956 public FontRenderContext getFontRenderContext() { 957 return this.fontRenderContext; 958 } 959 960 /** 961 * Draws a string at {@code (x, y)}. The start of the text at the 962 * baseline level will be aligned with the {@code (x, y)} point. 963 * 964 * @param str the string ({@code null} not permitted). 965 * @param x the x-coordinate. 966 * @param y the y-coordinate. 967 * 968 * @see #drawString(java.lang.String, float, float) 969 */ 970 @Override 971 public void drawString(String str, int x, int y) { 972 drawString(str, (float) x, (float) y); 973 } 974 975 /** 976 * Draws a string at {@code (x, y)}. The start of the text at the 977 * baseline level will be aligned with the {@code (x, y)} point. 978 * 979 * @param str the string ({@code null} not permitted). 980 * @param x the x-coordinate. 981 * @param y the y-coordinate. 982 */ 983 @Override 984 public void drawString(String str, float x, float y) { 985 if (str == null) { 986 throw new NullPointerException("Null 'str' argument."); 987 } 988 this.gc.fillText(str, x, y); 989 } 990 991 /** 992 * Draws a string of attributed characters at {@code (x, y)}. The 993 * call is delegated to 994 * {@link #drawString(AttributedCharacterIterator, float, float)}. 995 * 996 * @param iterator an iterator for the characters. 997 * @param x the x-coordinate. 998 * @param y the x-coordinate. 999 */ 1000 @Override 1001 public void drawString(AttributedCharacterIterator iterator, int x, int y) { 1002 drawString(iterator, (float) x, (float) y); 1003 } 1004 1005 /** 1006 * Draws a string of attributed characters at {@code (x, y)}. 1007 * 1008 * @param iterator an iterator over the characters ({@code null} not 1009 * permitted). 1010 * @param x the x-coordinate. 1011 * @param y the y-coordinate. 1012 */ 1013 @Override 1014 public void drawString(AttributedCharacterIterator iterator, float x, 1015 float y) { 1016 Set<AttributedCharacterIterator.Attribute> 1017 s = iterator.getAllAttributeKeys(); 1018 if (!s.isEmpty()) { 1019 TextLayout layout = new TextLayout(iterator, 1020 getFontRenderContext()); 1021 layout.draw(this, x, y); 1022 } else { 1023 StringBuilder strb = new StringBuilder(); 1024 iterator.first(); 1025 for (int i = iterator.getBeginIndex(); i < iterator.getEndIndex(); 1026 i++) { 1027 strb.append(iterator.current()); 1028 iterator.next(); 1029 } 1030 drawString(strb.toString(), x, y); 1031 } 1032 } 1033 1034 /** 1035 * Draws the specified glyph vector at the location {@code (x, y)}. 1036 * 1037 * @param g the glyph vector ({@code null} not permitted). 1038 * @param x the x-coordinate. 1039 * @param y the y-coordinate. 1040 */ 1041 @Override 1042 public void drawGlyphVector(GlyphVector g, float x, float y) { 1043 fill(g.getOutline(x, y)); 1044 } 1045 1046 /** 1047 * Applies the translation {@code (tx, ty)}. This call is delegated 1048 * to {@link #translate(double, double)}. 1049 * 1050 * @param tx the x-translation. 1051 * @param ty the y-translation. 1052 * 1053 * @see #translate(double, double) 1054 */ 1055 @Override 1056 public void translate(int tx, int ty) { 1057 translate((double) tx, (double) ty); 1058 } 1059 1060 /** 1061 * Applies the translation {@code (tx, ty)}. 1062 * 1063 * @param tx the x-translation. 1064 * @param ty the y-translation. 1065 */ 1066 @Override 1067 public void translate(double tx, double ty) { 1068 this.transform.translate(tx, ty); 1069 this.gc.translate(tx, ty); 1070 } 1071 1072 /** 1073 * Applies a rotation (anti-clockwise) about {@code (0, 0)}. 1074 * 1075 * @param theta the rotation angle (in radians). 1076 */ 1077 @Override 1078 public void rotate(double theta) { 1079 this.transform.rotate(theta); 1080 this.gc.rotate(Math.toDegrees(theta)); 1081 } 1082 1083 /** 1084 * Applies a rotation (anti-clockwise) about {@code (x, y)}. 1085 * 1086 * @param theta the rotation angle (in radians). 1087 * @param x the x-coordinate. 1088 * @param y the y-coordinate. 1089 */ 1090 @Override 1091 public void rotate(double theta, double x, double y) { 1092 translate(x, y); 1093 rotate(theta); 1094 translate(-x, -y); 1095 } 1096 1097 /** 1098 * Applies a scale transformation. 1099 * 1100 * @param sx the x-scaling factor. 1101 * @param sy the y-scaling factor. 1102 */ 1103 @Override 1104 public void scale(double sx, double sy) { 1105 this.transform.scale(sx, sy); 1106 this.gc.scale(sx, sy); 1107 } 1108 1109 /** 1110 * Applies a shear transformation. This is equivalent to the following 1111 * call to the {@code transform} method: 1112 * <br><br> 1113 * <ul><li> 1114 * {@code transform(AffineTransform.getShearInstance(shx, shy));} 1115 * </ul> 1116 * 1117 * @param shx the x-shear factor. 1118 * @param shy the y-shear factor. 1119 */ 1120 @Override 1121 public void shear(double shx, double shy) { 1122 transform(AffineTransform.getShearInstance(shx, shy)); 1123 } 1124 1125 /** 1126 * Applies this transform to the existing transform by concatenating it. 1127 * 1128 * @param t the transform ({@code null} not permitted). 1129 */ 1130 @Override 1131 public void transform(AffineTransform t) { 1132 AffineTransform tx = getTransform(); 1133 tx.concatenate(t); 1134 setTransform(tx); 1135 } 1136 1137 /** 1138 * Returns a copy of the current transform. 1139 * 1140 * @return A copy of the current transform (never {@code null}). 1141 * 1142 * @see #setTransform(java.awt.geom.AffineTransform) 1143 */ 1144 @Override 1145 public AffineTransform getTransform() { 1146 return (AffineTransform) this.transform.clone(); 1147 } 1148 1149 /** 1150 * Sets the transform. 1151 * 1152 * @param t the new transform ({@code null} permitted, resets to the 1153 * identity transform). 1154 * 1155 * @see #getTransform() 1156 */ 1157 @Override 1158 public void setTransform(AffineTransform t) { 1159 if (t == null) { 1160 this.transform = new AffineTransform(); 1161 t = this.transform; 1162 } else { 1163 this.transform = new AffineTransform(t); 1164 } 1165 this.gc.setTransform(t.getScaleX(), t.getShearY(), t.getShearX(), 1166 t.getScaleY(), t.getTranslateX(), t.getTranslateY()); 1167 } 1168 1169 /** 1170 * Returns {@code true} if the rectangle (in device space) intersects 1171 * with the shape (the interior, if {@code onStroke} is {@code false}, 1172 * otherwise the stroked outline of the shape). 1173 * 1174 * @param rect a rectangle (in device space). 1175 * @param s the shape. 1176 * @param onStroke test the stroked outline only? 1177 * 1178 * @return A boolean. 1179 */ 1180 @Override 1181 public boolean hit(Rectangle rect, Shape s, boolean onStroke) { 1182 Shape ts; 1183 if (onStroke) { 1184 ts = this.transform.createTransformedShape( 1185 this.stroke.createStrokedShape(s)); 1186 } else { 1187 ts = this.transform.createTransformedShape(s); 1188 } 1189 if (!rect.getBounds2D().intersects(ts.getBounds2D())) { 1190 return false; 1191 } 1192 Area a1 = new Area(rect); 1193 Area a2 = new Area(ts); 1194 a1.intersect(a2); 1195 return !a1.isEmpty(); 1196 } 1197 1198 /** 1199 * Not implemented - the method does nothing. 1200 */ 1201 @Override 1202 public void setPaintMode() { 1203 // not implemented 1204 } 1205 1206 /** 1207 * Not implemented - the method does nothing. 1208 */ 1209 @Override 1210 public void setXORMode(Color c1) { 1211 // not implemented 1212 } 1213 1214 /** 1215 * Returns the bounds of the user clipping region. 1216 * 1217 * @return The clip bounds (possibly {@code null}). 1218 * 1219 * @see #getClip() 1220 */ 1221 @Override 1222 public Rectangle getClipBounds() { 1223 if (this.clip == null) { 1224 return null; 1225 } 1226 return getClip().getBounds(); 1227 } 1228 1229 /** 1230 * Returns the user clipping region. The initial default value is 1231 * {@code null}. 1232 * 1233 * @return The user clipping region (possibly {@code null}). 1234 * 1235 * @see #setClip(java.awt.Shape) 1236 */ 1237 @Override 1238 public Shape getClip() { 1239 if (this.clip == null) { 1240 return null; 1241 } 1242 AffineTransform inv; 1243 try { 1244 inv = this.transform.createInverse(); 1245 return inv.createTransformedShape(this.clip); 1246 } catch (NoninvertibleTransformException ex) { 1247 return null; 1248 } 1249 } 1250 1251 /** 1252 * Sets the user clipping region. 1253 * 1254 * @param shape the new user clipping region ({@code null} permitted). 1255 * 1256 * @see #getClip() 1257 */ 1258 @Override 1259 public void setClip(Shape shape) { 1260 if (this.stateSaved) { 1261 this.gc.restore(); // get back original clip 1262 reapplyAttributes(); // but keep other attributes 1263 this.stateSaved = false; 1264 } 1265 // null is handled fine here... 1266 this.clip = this.transform.createTransformedShape(shape); 1267 if (clip != null) { 1268 this.gc.save(); 1269 rememberSavedAttributes(); 1270 shapeToPath(shape); 1271 this.gc.clip(); 1272 } 1273 } 1274 1275 /** 1276 * Remember the Graphics2D attributes in force at the point of pushing 1277 * the JavaFX context. 1278 */ 1279 private void rememberSavedAttributes() { 1280 this.stateSaved = true; 1281 this.savedColor = this.color; 1282 this.savedFont = this.font; 1283 this.savedPaint = this.paint; 1284 this.savedStroke = this.stroke; 1285 this.savedTransform = new AffineTransform(this.transform); 1286 } 1287 1288 private void reapplyAttributes() { 1289 if (!paintsAreEqual(this.paint, this.savedPaint)) { 1290 applyPaint(this.paint); 1291 } 1292 if (!this.color.equals(this.savedColor)) { 1293 applyColor(this.color); 1294 } 1295 if (!this.stroke.equals(this.savedStroke)) { 1296 applyStroke(this.stroke); 1297 } 1298 if (!this.font.equals(this.savedFont)) { 1299 applyFont(this.font); 1300 } 1301 if (!this.transform.equals(this.savedTransform)) { 1302 setTransform(this.transform); 1303 } 1304 this.savedColor = null; 1305 this.savedFont = null; 1306 this.savedPaint = null; 1307 this.savedStroke = null; 1308 this.savedTransform = null; 1309 } 1310 1311 /** 1312 * Clips to the intersection of the current clipping region and the 1313 * specified shape. 1314 * 1315 * According to the Oracle API specification, this method will accept a 1316 * {@code null} argument, but there is an open bug report (since 2004) 1317 * that suggests this is wrong: 1318 * <p> 1319 * <a href="http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6206189"> 1320 * http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6206189</a> 1321 * 1322 * In this implementation, a {@code null} argument is not permitted. 1323 * 1324 * @param s the clip shape ({@code null} not permitted). 1325 */ 1326 @Override 1327 public void clip(Shape s) { 1328 if (s instanceof Line2D) { 1329 s = s.getBounds2D(); 1330 } 1331 if (this.clip == null) { 1332 setClip(s); 1333 return; 1334 } 1335 Shape ts = this.transform.createTransformedShape(s); 1336 Shape clipNew; 1337 if (!ts.intersects(this.clip.getBounds2D())) { 1338 clipNew = new Rectangle2D.Double(); 1339 } else { 1340 Area a1 = new Area(ts); 1341 Area a2 = new Area(this.clip); 1342 a1.intersect(a2); 1343 clipNew = new Path2D.Double(a1); 1344 } 1345 this.clip = clipNew; 1346 if (!this.stateSaved) { 1347 this.gc.save(); 1348 rememberSavedAttributes(); 1349 } 1350 shapeToPath(this.clip); 1351 this.gc.clip(); 1352 } 1353 1354 /** 1355 * Clips to the intersection of the current clipping region and the 1356 * specified rectangle. 1357 * 1358 * @param x the x-coordinate. 1359 * @param y the y-coordinate. 1360 * @param width the width. 1361 * @param height the height. 1362 */ 1363 @Override 1364 public void clipRect(int x, int y, int width, int height) { 1365 clip(rect(x, y, width, height)); 1366 } 1367 1368 /** 1369 * Sets the user clipping region to the specified rectangle. 1370 * 1371 * @param x the x-coordinate. 1372 * @param y the y-coordinate. 1373 * @param width the width. 1374 * @param height the height. 1375 * 1376 * @see #getClip() 1377 */ 1378 @Override 1379 public void setClip(int x, int y, int width, int height) { 1380 setClip(rect(x, y, width, height)); 1381 } 1382 1383 /** 1384 * Draws a line from {@code (x1, y1)} to {@code (x2, y2)} using 1385 * the current {@code paint} and {@code stroke}. 1386 * 1387 * @param x1 the x-coordinate of the start point. 1388 * @param y1 the y-coordinate of the start point. 1389 * @param x2 the x-coordinate of the end point. 1390 * @param y2 the x-coordinate of the end point. 1391 */ 1392 @Override 1393 public void drawLine(int x1, int y1, int x2, int y2) { 1394 draw(line(x1, y1, x2, y2)); 1395 } 1396 1397 /** 1398 * Fills the specified rectangle with the current {@code paint}. 1399 * 1400 * @param x the x-coordinate. 1401 * @param y the y-coordinate. 1402 * @param width the rectangle width. 1403 * @param height the rectangle height. 1404 */ 1405 @Override 1406 public void fillRect(int x, int y, int width, int height) { 1407 fill(rect(x, y, width, height)); 1408 } 1409 1410 /** 1411 * Clears the specified rectangle by filling it with the current 1412 * background color. If the background color is {@code null}, this 1413 * method will do nothing. 1414 * 1415 * @param x the x-coordinate. 1416 * @param y the y-coordinate. 1417 * @param width the width. 1418 * @param height the height. 1419 * 1420 * @see #getBackground() 1421 */ 1422 @Override 1423 public void clearRect(int x, int y, int width, int height) { 1424 if (getBackground() == null) { 1425 return; // we can't do anything 1426 } 1427 Paint saved = getPaint(); 1428 setPaint(getBackground()); 1429 fillRect(x, y, width, height); 1430 setPaint(saved); 1431 } 1432 1433 /** 1434 * Draws a rectangle with rounded corners using the current 1435 * {@code paint} and {@code stroke}. 1436 * 1437 * @param x the x-coordinate. 1438 * @param y the y-coordinate. 1439 * @param width the width. 1440 * @param height the height. 1441 * @param arcWidth the arc-width. 1442 * @param arcHeight the arc-height. 1443 * 1444 * @see #fillRoundRect(int, int, int, int, int, int) 1445 */ 1446 @Override 1447 public void drawRoundRect(int x, int y, int width, int height, 1448 int arcWidth, int arcHeight) { 1449 draw(roundRect(x, y, width, height, arcWidth, arcHeight)); 1450 } 1451 1452 /** 1453 * Fills a rectangle with rounded corners using the current {@code paint}. 1454 * 1455 * @param x the x-coordinate. 1456 * @param y the y-coordinate. 1457 * @param width the width. 1458 * @param height the height. 1459 * @param arcWidth the arc-width. 1460 * @param arcHeight the arc-height. 1461 * 1462 * @see #drawRoundRect(int, int, int, int, int, int) 1463 */ 1464 @Override 1465 public void fillRoundRect(int x, int y, int width, int height, 1466 int arcWidth, int arcHeight) { 1467 fill(roundRect(x, y, width, height, arcWidth, arcHeight)); 1468 } 1469 1470 /** 1471 * Draws an oval framed by the rectangle {@code (x, y, width, height)} 1472 * using the current {@code paint} and {@code stroke}. 1473 * 1474 * @param x the x-coordinate. 1475 * @param y the y-coordinate. 1476 * @param width the width. 1477 * @param height the height. 1478 * 1479 * @see #fillOval(int, int, int, int) 1480 */ 1481 @Override 1482 public void drawOval(int x, int y, int width, int height) { 1483 draw(oval(x, y, width, height)); 1484 } 1485 1486 /** 1487 * Fills an oval framed by the rectangle {@code (x, y, width, height)}. 1488 * 1489 * @param x the x-coordinate. 1490 * @param y the y-coordinate. 1491 * @param width the width. 1492 * @param height the height. 1493 * 1494 * @see #drawOval(int, int, int, int) 1495 */ 1496 @Override 1497 public void fillOval(int x, int y, int width, int height) { 1498 fill(oval(x, y, width, height)); 1499 } 1500 1501 /** 1502 * Draws an arc contained within the rectangle 1503 * {@code (x, y, width, height)}, starting at {@code startAngle} 1504 * and continuing through {@code arcAngle} degrees using 1505 * the current {@code paint} and {@code stroke}. 1506 * 1507 * @param x the x-coordinate. 1508 * @param y the y-coordinate. 1509 * @param width the width. 1510 * @param height the height. 1511 * @param startAngle the start angle in degrees, 0 = 3 o'clock. 1512 * @param arcAngle the angle (anticlockwise) in degrees. 1513 * 1514 * @see #fillArc(int, int, int, int, int, int) 1515 */ 1516 @Override 1517 public void drawArc(int x, int y, int width, int height, int startAngle, 1518 int arcAngle) { 1519 draw(arc(x, y, width, height, startAngle, arcAngle)); 1520 } 1521 1522 /** 1523 * Fills an arc contained within the rectangle 1524 * {@code (x, y, width, height)}, starting at {@code startAngle} 1525 * and continuing through {@code arcAngle} degrees, using 1526 * the current {@code paint}. 1527 * 1528 * @param x the x-coordinate. 1529 * @param y the y-coordinate. 1530 * @param width the width. 1531 * @param height the height. 1532 * @param startAngle the start angle in degrees, 0 = 3 o'clock. 1533 * @param arcAngle the angle (anticlockwise) in degrees. 1534 * 1535 * @see #drawArc(int, int, int, int, int, int) 1536 */ 1537 @Override 1538 public void fillArc(int x, int y, int width, int height, int startAngle, 1539 int arcAngle) { 1540 fill(arc(x, y, width, height, startAngle, arcAngle)); 1541 } 1542 1543 /** 1544 * Draws the specified multi-segment line using the current 1545 * {@code paint} and {@code stroke}. 1546 * 1547 * @param xPoints the x-points. 1548 * @param yPoints the y-points. 1549 * @param nPoints the number of points to use for the polyline. 1550 */ 1551 @Override 1552 public void drawPolyline(int[] xPoints, int[] yPoints, int nPoints) { 1553 GeneralPath p = createPolygon(xPoints, yPoints, nPoints, false); 1554 draw(p); 1555 } 1556 1557 /** 1558 * Draws the specified polygon using the current {@code paint} and 1559 * {@code stroke}. 1560 * 1561 * @param xPoints the x-points. 1562 * @param yPoints the y-points. 1563 * @param nPoints the number of points to use for the polygon. 1564 * 1565 * @see #fillPolygon(int[], int[], int) */ 1566 @Override 1567 public void drawPolygon(int[] xPoints, int[] yPoints, int nPoints) { 1568 GeneralPath p = createPolygon(xPoints, yPoints, nPoints, true); 1569 draw(p); 1570 } 1571 1572 /** 1573 * Fills the specified polygon using the current {@code paint}. 1574 * 1575 * @param xPoints the x-points. 1576 * @param yPoints the y-points. 1577 * @param nPoints the number of points to use for the polygon. 1578 * 1579 * @see #drawPolygon(int[], int[], int) 1580 */ 1581 @Override 1582 public void fillPolygon(int[] xPoints, int[] yPoints, int nPoints) { 1583 GeneralPath p = createPolygon(xPoints, yPoints, nPoints, true); 1584 fill(p); 1585 } 1586 1587 /** 1588 * Creates a polygon from the specified {@code x} and 1589 * {@code y} coordinate arrays. 1590 * 1591 * @param xPoints the x-points. 1592 * @param yPoints the y-points. 1593 * @param nPoints the number of points to use for the polyline. 1594 * @param close closed? 1595 * 1596 * @return A polygon. 1597 */ 1598 public GeneralPath createPolygon(int[] xPoints, int[] yPoints, 1599 int nPoints, boolean close) { 1600 GeneralPath p = new GeneralPath(); 1601 p.moveTo(xPoints[0], yPoints[0]); 1602 for (int i = 1; i < nPoints; i++) { 1603 p.lineTo(xPoints[i], yPoints[i]); 1604 } 1605 if (close) { 1606 p.closePath(); 1607 } 1608 return p; 1609 } 1610 1611 /** 1612 * Draws an image at the location {@code (x, y)}. Note that the 1613 * {@code observer} is ignored. 1614 * 1615 * @param img the image ({@code null} permitted...method will do nothing). 1616 * @param x the x-coordinate. 1617 * @param y the y-coordinate. 1618 * @param observer ignored. 1619 * 1620 * @return {@code true} if there is no more drawing to be done. 1621 */ 1622 @Override 1623 public boolean drawImage(Image img, int x, int y, ImageObserver observer) { 1624 if (img == null) { 1625 return true; 1626 } 1627 int w = img.getWidth(observer); 1628 if (w < 0) { 1629 return false; 1630 } 1631 int h = img.getHeight(observer); 1632 if (h < 0) { 1633 return false; 1634 } 1635 return drawImage(img, x, y, w, h, observer); 1636 } 1637 1638 /** 1639 * Draws the image into the rectangle defined by {@code (x, y, w, h)}. 1640 * Note that the {@code observer} is ignored (it is not useful in this 1641 * context). 1642 * 1643 * @param img the image ({@code null} permitted...draws nothing). 1644 * @param x the x-coordinate. 1645 * @param y the y-coordinate. 1646 * @param w the width. 1647 * @param h the height. 1648 * @param observer ignored. 1649 * 1650 * @return {@code true} if there is no more drawing to be done. 1651 */ 1652 @Override 1653 public boolean drawImage(final Image img, int x, int y, 1654 int w, int h, ImageObserver observer) { 1655 final BufferedImage buffered; 1656 if (img instanceof BufferedImage) { 1657 buffered = (BufferedImage) img; 1658 } else { 1659 buffered = new BufferedImage(w, h, 1660 BufferedImage.TYPE_INT_ARGB); 1661 final Graphics2D g2 = buffered.createGraphics(); 1662 g2.drawImage(img, 0, 0, w, h, null); 1663 g2.dispose(); 1664 } 1665 javafx.scene.image.WritableImage fxImage = SwingFXUtils.toFXImage( 1666 buffered, null); 1667 this.gc.drawImage(fxImage, x, y, w, h); 1668 return true; 1669 } 1670 1671 /** 1672 * Draws an image at the location {@code (x, y)}. Note that the 1673 * {@code observer} is ignored. 1674 * 1675 * @param img the image ({@code null} permitted...draws nothing). 1676 * @param x the x-coordinate. 1677 * @param y the y-coordinate. 1678 * @param bgcolor the background color ({@code null} permitted). 1679 * @param observer ignored. 1680 * 1681 * @return {@code true} if there is no more drawing to be done. 1682 */ 1683 @Override 1684 public boolean drawImage(Image img, int x, int y, Color bgcolor, 1685 ImageObserver observer) { 1686 if (img == null) { 1687 return true; 1688 } 1689 int w = img.getWidth(null); 1690 if (w < 0) { 1691 return false; 1692 } 1693 int h = img.getHeight(null); 1694 if (h < 0) { 1695 return false; 1696 } 1697 return drawImage(img, x, y, w, h, bgcolor, observer); 1698 } 1699 1700 /** 1701 * Draws an image to the rectangle {@code (x, y, w, h)} (scaling it if 1702 * required), first filling the background with the specified color. Note 1703 * that the {@code observer} is ignored. 1704 * 1705 * @param img the image. 1706 * @param x the x-coordinate. 1707 * @param y the y-coordinate. 1708 * @param w the width. 1709 * @param h the height. 1710 * @param bgcolor the background color ({@code null} permitted). 1711 * @param observer ignored. 1712 * 1713 * @return {@code true} if the image is drawn. 1714 */ 1715 @Override 1716 public boolean drawImage(Image img, int x, int y, int w, int h, 1717 Color bgcolor, ImageObserver observer) { 1718 Paint saved = getPaint(); 1719 setPaint(bgcolor); 1720 fillRect(x, y, w, h); 1721 setPaint(saved); 1722 return drawImage(img, x, y, w, h, observer); 1723 } 1724 1725 /** 1726 * Draws part of an image (defined by the source rectangle 1727 * {@code (sx1, sy1, sx2, sy2)}) into the destination rectangle 1728 * {@code (dx1, dy1, dx2, dy2)}. Note that the {@code observer} 1729 * is ignored in this implementation. 1730 * 1731 * @param img the image. 1732 * @param dx1 the x-coordinate for the top left of the destination. 1733 * @param dy1 the y-coordinate for the top left of the destination. 1734 * @param dx2 the x-coordinate for the bottom right of the destination. 1735 * @param dy2 the y-coordinate for the bottom right of the destination. 1736 * @param sx1 the x-coordinate for the top left of the source. 1737 * @param sy1 the y-coordinate for the top left of the source. 1738 * @param sx2 the x-coordinate for the bottom right of the source. 1739 * @param sy2 the y-coordinate for the bottom right of the source. 1740 * 1741 * @return {@code true} if the image is drawn. 1742 */ 1743 @Override 1744 public boolean drawImage(Image img, int dx1, int dy1, int dx2, int dy2, 1745 int sx1, int sy1, int sx2, int sy2, ImageObserver observer) { 1746 int w = dx2 - dx1; 1747 int h = dy2 - dy1; 1748 BufferedImage img2 = new BufferedImage(w, h, 1749 BufferedImage.TYPE_INT_ARGB); 1750 Graphics2D g2 = img2.createGraphics(); 1751 g2.drawImage(img, 0, 0, w, h, sx1, sy1, sx2, sy2, null); 1752 return drawImage(img2, dx1, dy1, null); 1753 } 1754 1755 /** 1756 * Draws part of an image (defined by the source rectangle 1757 * {@code (sx1, sy1, sx2, sy2)}) into the destination rectangle 1758 * {@code (dx1, dy1, dx2, dy2)}. The destination rectangle is first 1759 * cleared by filling it with the specified {@code bgcolor}. Note that 1760 * the {@code observer} is ignored. 1761 * 1762 * @param img the image. 1763 * @param dx1 the x-coordinate for the top left of the destination. 1764 * @param dy1 the y-coordinate for the top left of the destination. 1765 * @param dx2 the x-coordinate for the bottom right of the destination. 1766 * @param dy2 the y-coordinate for the bottom right of the destination. 1767 * @param sx1 the x-coordinate for the top left of the source. 1768 * @param sy1 the y-coordinate for the top left of the source. 1769 * @param sx2 the x-coordinate for the bottom right of the source. 1770 * @param sy2 the y-coordinate for the bottom right of the source. 1771 * @param bgcolor the background color ({@code null} permitted). 1772 * @param observer ignored. 1773 * 1774 * @return {@code true} if the image is drawn. 1775 */ 1776 @Override 1777 public boolean drawImage(Image img, int dx1, int dy1, int dx2, int dy2, 1778 int sx1, int sy1, int sx2, int sy2, Color bgcolor, 1779 ImageObserver observer) { 1780 Paint saved = getPaint(); 1781 setPaint(bgcolor); 1782 fillRect(dx1, dy1, dx2 - dx1, dy2 - dy1); 1783 setPaint(saved); 1784 return drawImage(img, dx1, dy1, dx2, dy2, sx1, sy1, sx2, sy2, observer); 1785 } 1786 1787 /** 1788 * Draws the rendered image. When {@code img} is {@code null} this method 1789 * does nothing. 1790 * 1791 * @param img the image ({@code null} permitted). 1792 * @param xform the transform. 1793 */ 1794 @Override 1795 public void drawRenderedImage(RenderedImage img, AffineTransform xform) { 1796 if (img == null) { // to match the behaviour specified in the JDK 1797 return; 1798 } 1799 BufferedImage bi = convertRenderedImage(img); 1800 drawImage(bi, xform, null); 1801 } 1802 1803 /** 1804 * Converts a rendered image to a {@code BufferedImage}. This utility 1805 * method has come from a forum post by Jim Moore at: 1806 * <p> 1807 * <a href="http://www.jguru.com/faq/view.jsp?EID=114602"> 1808 * http://www.jguru.com/faq/view.jsp?EID=114602</a> 1809 * 1810 * @param img the rendered image. 1811 * 1812 * @return A buffered image. 1813 */ 1814 private static BufferedImage convertRenderedImage(RenderedImage img) { 1815 if (img instanceof BufferedImage) { 1816 return (BufferedImage) img; 1817 } 1818 ColorModel cm = img.getColorModel(); 1819 int width = img.getWidth(); 1820 int height = img.getHeight(); 1821 WritableRaster raster = cm.createCompatibleWritableRaster(width, height); 1822 boolean isAlphaPremultiplied = cm.isAlphaPremultiplied(); 1823 Hashtable properties = new Hashtable(); 1824 String[] keys = img.getPropertyNames(); 1825 if (keys != null) { 1826 for (int i = 0; i < keys.length; i++) { 1827 properties.put(keys[i], img.getProperty(keys[i])); 1828 } 1829 } 1830 BufferedImage result = new BufferedImage(cm, raster, 1831 isAlphaPremultiplied, properties); 1832 img.copyData(raster); 1833 return result; 1834 } 1835 1836 /** 1837 * Draws the renderable image. 1838 * 1839 * @param img the renderable image. 1840 * @param xform the transform. 1841 */ 1842 @Override 1843 public void drawRenderableImage(RenderableImage img, 1844 AffineTransform xform) { 1845 RenderedImage ri = img.createDefaultRendering(); 1846 drawRenderedImage(ri, xform); 1847 } 1848 1849 /** 1850 * Draws an image with the specified transform. Note that the 1851 * {@code observer} is ignored in this implementation. 1852 * 1853 * @param img the image. 1854 * @param xform the transform ({@code null} permitted). 1855 * @param obs the image observer (ignored). 1856 * 1857 * @return {@code true} if the image is drawn. 1858 */ 1859 @Override 1860 public boolean drawImage(Image img, AffineTransform xform, 1861 ImageObserver obs) { 1862 AffineTransform savedTransform = getTransform(); 1863 if (xform != null) { 1864 transform(xform); 1865 } 1866 boolean result = drawImage(img, 0, 0, obs); 1867 if (xform != null) { 1868 setTransform(savedTransform); 1869 } 1870 return result; 1871 } 1872 1873 /** 1874 * Draws the image resulting from applying the {@code BufferedImageOp} 1875 * to the specified image at the location {@code (x, y)}. 1876 * 1877 * @param img the image. 1878 * @param op the operation ({@code null} permitted). 1879 * @param x the x-coordinate. 1880 * @param y the y-coordinate. 1881 */ 1882 @Override 1883 public void drawImage(BufferedImage img, BufferedImageOp op, int x, int y) { 1884 BufferedImage imageToDraw = img; 1885 if (op != null) { 1886 imageToDraw = op.filter(img, null); 1887 } 1888 drawImage(imageToDraw, new AffineTransform(1f, 0f, 0f, 1f, x, y), null); 1889 } 1890 1891 /** 1892 * Not yet implemented. 1893 * 1894 * @param x the x-coordinate. 1895 * @param y the y-coordinate. 1896 * @param width the width of the area. 1897 * @param height the height of the area. 1898 * @param dx the delta x. 1899 * @param dy the delta y. 1900 */ 1901 @Override 1902 public void copyArea(int x, int y, int width, int height, int dx, int dy) { 1903 // FIXME: implement this, low priority 1904 } 1905 1906 /** 1907 * This method does nothing. 1908 */ 1909 @Override 1910 public void dispose() { 1911 // nothing to do 1912 } 1913 1914 /** 1915 * Returns a recyclable {@link Line2D} object. 1916 * 1917 * @param x1 the x-coordinate. 1918 * @param y2 the y-coordinate. 1919 * @param x2 the width. 1920 * @param y2 the height. 1921 * 1922 * @return A line (never {@code null}). 1923 */ 1924 private Line2D line(double x1, double y1, double x2, double y2) { 1925 if (this.line == null) { 1926 this.line = new Line2D.Double(x1, y1, x2, y2); 1927 } else { 1928 this.line.setLine(x1, y1, x2, y2); 1929 } 1930 return this.line; 1931 } 1932 1933 /** 1934 * Sets the attributes of the reusable {@link Rectangle2D} object that is 1935 * used by the {@link FXGraphics2D#drawRect(int, int, int, int)} and 1936 * {@link FXGraphics2D#fillRect(int, int, int, int)} methods. 1937 * 1938 * @param x the x-coordinate. 1939 * @param y the y-coordinate. 1940 * @param width the width. 1941 * @param height the height. 1942 * 1943 * @return A rectangle (never {@code null}). 1944 */ 1945 private Rectangle2D rect(double x, double y, double width, double height) { 1946 if (this.rect == null) { 1947 this.rect = new Rectangle2D.Double(x, y, width, height); 1948 } else { 1949 this.rect.setRect(x, y, width, height); 1950 } 1951 return this.rect; 1952 } 1953 1954 /** 1955 * Sets the attributes of the reusable {@link RoundRectangle2D} object that 1956 * is used by the {@link #drawRoundRect(int, int, int, int, int, int)} and 1957 * {@link #fillRoundRect(int, int, int, int, int, int)} methods. 1958 * 1959 * @param x the x-coordinate. 1960 * @param y the y-coordinate. 1961 * @param width the width. 1962 * @param height the height. 1963 * @param arcWidth the arc width. 1964 * @param arcHeight the arc height. 1965 * 1966 * @return A round rectangle (never {@code null}). 1967 */ 1968 private RoundRectangle2D roundRect(int x, int y, int width, int height, 1969 int arcWidth, int arcHeight) { 1970 if (this.roundRect == null) { 1971 this.roundRect = new RoundRectangle2D.Double(x, y, width, height, 1972 arcWidth, arcHeight); 1973 } else { 1974 this.roundRect.setRoundRect(x, y, width, height, 1975 arcWidth, arcHeight); 1976 } 1977 return this.roundRect; 1978 } 1979 1980 /** 1981 * Sets the attributes of the reusable {@link Arc2D} object that is used by 1982 * {@link #drawArc(int, int, int, int, int, int)} and 1983 * {@link #fillArc(int, int, int, int, int, int)} methods. 1984 * 1985 * @param x the x-coordinate. 1986 * @param y the y-coordinate. 1987 * @param width the width. 1988 * @param height the height. 1989 * @param startAngle the start angle in degrees, 0 = 3 o'clock. 1990 * @param arcAngle the angle (anticlockwise) in degrees. 1991 * 1992 * @return An arc (never {@code null}). 1993 */ 1994 private Arc2D arc(int x, int y, int width, int height, int startAngle, 1995 int arcAngle) { 1996 if (this.arc == null) { 1997 this.arc = new Arc2D.Double(x, y, width, height, startAngle, 1998 arcAngle, Arc2D.OPEN); 1999 } else { 2000 this.arc.setArc(x, y, width, height, startAngle, arcAngle, 2001 Arc2D.OPEN); 2002 } 2003 return this.arc; 2004 } 2005 2006 /** 2007 * Returns an {@link Ellipse2D} object that may be reused (so this instance 2008 * should be used for short term operations only). See the 2009 * {@link #drawOval(int, int, int, int)} and 2010 * {@link #fillOval(int, int, int, int)} methods for usage. 2011 * 2012 * @param x the x-coordinate. 2013 * @param y the y-coordinate. 2014 * @param width the width. 2015 * @param height the height. 2016 * 2017 * @return An oval shape (never {@code null}). 2018 */ 2019 private Ellipse2D oval(int x, int y, int width, int height) { 2020 if (this.oval == null) { 2021 this.oval = new Ellipse2D.Double(x, y, width, height); 2022 } else { 2023 this.oval.setFrame(x, y, width, height); 2024 } 2025 return this.oval; 2026 } 2027 2028 /** 2029 * Returns {@code true} if the two {@code Paint} objects are equal 2030 * OR both {@code null}. This method handles 2031 * {@code GradientPaint}, {@code LinearGradientPaint} 2032 * and {@code RadialGradientPaint} as special cases, since those classes do 2033 * not override the {@code equals()} method. 2034 * 2035 * @param p1 paint 1 ({@code null} permitted). 2036 * @param p2 paint 2 ({@code null} permitted). 2037 * 2038 * @return A boolean. 2039 */ 2040 private static boolean paintsAreEqual(Paint p1, Paint p2) { 2041 if (p1 == p2) { 2042 return true; 2043 } 2044 2045 // handle cases where either or both arguments are null 2046 if (p1 == null) { 2047 return (p2 == null); 2048 } 2049 if (p2 == null) { 2050 return false; 2051 } 2052 2053 // handle cases... 2054 if (p1 instanceof Color && p2 instanceof Color) { 2055 return p1.equals(p2); 2056 } 2057 if (p1 instanceof GradientPaint && p2 instanceof GradientPaint) { 2058 GradientPaint gp1 = (GradientPaint) p1; 2059 GradientPaint gp2 = (GradientPaint) p2; 2060 return gp1.getColor1().equals(gp2.getColor1()) 2061 && gp1.getColor2().equals(gp2.getColor2()) 2062 && gp1.getPoint1().equals(gp2.getPoint1()) 2063 && gp1.getPoint2().equals(gp2.getPoint2()) 2064 && gp1.isCyclic() == gp2.isCyclic() 2065 && gp1.getTransparency() == gp1.getTransparency(); 2066 } 2067 if (p1 instanceof LinearGradientPaint 2068 && p2 instanceof LinearGradientPaint) { 2069 LinearGradientPaint lgp1 = (LinearGradientPaint) p1; 2070 LinearGradientPaint lgp2 = (LinearGradientPaint) p2; 2071 return lgp1.getStartPoint().equals(lgp2.getStartPoint()) 2072 && lgp1.getEndPoint().equals(lgp2.getEndPoint()) 2073 && Arrays.equals(lgp1.getFractions(), lgp2.getFractions()) 2074 && Arrays.equals(lgp1.getColors(), lgp2.getColors()) 2075 && lgp1.getCycleMethod() == lgp2.getCycleMethod() 2076 && lgp1.getColorSpace() == lgp2.getColorSpace() 2077 && lgp1.getTransform().equals(lgp2.getTransform()); 2078 } 2079 if (p1 instanceof RadialGradientPaint 2080 && p2 instanceof RadialGradientPaint) { 2081 RadialGradientPaint rgp1 = (RadialGradientPaint) p1; 2082 RadialGradientPaint rgp2 = (RadialGradientPaint) p2; 2083 return rgp1.getCenterPoint().equals(rgp2.getCenterPoint()) 2084 && rgp1.getRadius() == rgp2.getRadius() 2085 && rgp1.getFocusPoint().equals(rgp2.getFocusPoint()) 2086 && Arrays.equals(rgp1.getFractions(), rgp2.getFractions()) 2087 && Arrays.equals(rgp1.getColors(), rgp2.getColors()) 2088 && rgp1.getCycleMethod() == rgp2.getCycleMethod() 2089 && rgp1.getColorSpace() == rgp2.getColorSpace() 2090 && rgp1.getTransform().equals(rgp2.getTransform()); 2091 } 2092 return p1.equals(p2); 2093 } 2094 2095}