1 /* 2 * Copyright (c) 2010, 2017, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26 package javafx.scene.paint; 27 28 import java.util.List; 29 30 import com.sun.javafx.scene.paint.GradientUtils; 31 import com.sun.javafx.tk.Toolkit; 32 import javafx.beans.NamedArg; 33 34 /** 35 * The {@code RadialGradient} class provides a way to fill a shape 36 * with a circular radial color gradient pattern. 37 * The user may specify 2 or more gradient colors, 38 * and this paint will provide an interpolation between each color. 39 * <p> 40 * The user must specify the circle controlling the gradient pattern, 41 * which is defined by a center point and a radius. 42 * The user can also specify a separate focus point within that circle, 43 * which controls the location of the first color of the gradient. 44 * By default the focus is set to be the center of the circle. 45 * <p> 46 * The center and radius are specified 47 * relative to a unit square, unless the <code>proportional</code> 48 * variable is false. By default proportional is true, and the 49 * gradient will be scaled to fill whatever shape it is applied to. 50 * The focus point is always specified relative to the center point 51 * by an angle and a distance relative to the radius. 52 * <p> 53 * This paint will map the first color of the gradient to the focus point, 54 * and the last color to the perimeter of the circle, 55 * interpolating smoothly for any in-between colors specified by the user. 56 * Any line drawn from the focus point to the circumference will 57 * thus span all of the gradient colors. 58 * <p> 59 * The focus distance will be clamped to the range {@code (-1, 1)} 60 * so that the focus point is always strictly inside the circle. 61 * <p> 62 * The application provides an array of {@code Stop}s specifying how to distribute 63 * the colors along the gradient. The {@code Stop#offset} variable must be 64 * the range 0.0 to 1.0 and act like keyframes along the gradient. 65 * They mark where the gradient should be exactly a particular color. 66 * @since JavaFX 2.0 67 */ 68 public final class RadialGradient extends Paint { 69 private double focusAngle; 70 71 /** 72 * Defines the angle in degrees from the center of the gradient 73 * to the focus point to which the first color is mapped. 74 * @return the angle in degrees from the center of the gradient 75 * to the focus point to which the first color is mapped 76 */ 77 public final double getFocusAngle() { 78 return focusAngle; 79 } 80 81 private double focusDistance; 82 83 /** 84 * Defines the distance from the center of the gradient to the 85 * focus point to which the first color is mapped. 86 * A distance of 0.0 will be at the center of the gradient circle. 87 * A distance of 1.0 will be on the circumference of the gradient circle. 88 * @return the distance from the center of the gradient to the 89 * focus point to which the first color is mapped 90 */ 91 public final double getFocusDistance() { 92 return focusDistance; 93 } 94 95 private double centerX; 96 97 /** 98 * Defines the X coordinate of the center point of the circle defining the gradient. 99 * If proportional is true (the default), this value specifies a 100 * point on a unit square that will be scaled to match the size of the 101 * the shape that the gradient fills. 102 * The last color of the gradient is mapped to the perimeter of this circle. 103 * 104 * @return the X coordinate of the center point of the circle defining the 105 * gradient 106 * @defaultValue 0.0 107 */ 108 public final double getCenterX() { 109 return centerX; 110 } 111 112 private double centerY; 113 114 /** 115 * Defines the X coordinate of the center point of the circle defining the gradient. 116 * If proportional is true (the default), this value specifies a 117 * point on a unit square that will be scaled to match the size of the 118 * the shape that the gradient fills. 119 * The last color of the gradient is mapped to the perimeter of this circle. 120 * 121 * @return the X coordinate of the center point of the circle defining the 122 * gradient 123 * @defaultValue 0.0 124 */ 125 public final double getCenterY() { 126 return centerY; 127 } 128 129 private double radius; 130 131 /** 132 * Specifies the radius of the circle defining the extents of the color gradient. 133 * If proportional is true (the default), this value specifies a 134 * size relative to unit square that will be scaled to match the size of the 135 * the shape that the gradient fills. 136 * 137 * @return the radius of the circle defining the extents of the color 138 * gradient 139 * @defaultValue 1.0 140 */ 141 public final double getRadius() { 142 return radius; 143 } 144 145 private boolean proportional; 146 147 /** 148 * Indicates whether the center and radius values are proportional or 149 * absolute. 150 * If this flag is true, the center point and radius are defined 151 * in a coordinate space where coordinates in the range {@code [0..1]} 152 * are scaled to map onto the bounds of the shape that the gradient fills. 153 * If this flag is false, then the center coordinates and the radius are 154 * specified in the local coordinate system of the node. 155 * 156 * @return true if the center and radius values are proportional, otherwise 157 * absolute 158 * @defaultValue true 159 */ 160 public final boolean isProportional() { 161 return proportional; 162 } 163 164 private CycleMethod cycleMethod; 165 166 /** 167 * Defines the cycle method applied 168 * to the {@code RadialGradient}. One of: {@code CycleMethod.NO_CYCLE}, 169 * {@code CycleMethod.REFLECT}, or {@code CycleMethod.REPEAT}. 170 * 171 * @return the cycle method applied to this radial gradient 172 * @defaultValue NO_CYCLE 173 */ 174 public final CycleMethod getCycleMethod() { 175 return cycleMethod; 176 } 177 178 private List<Stop> stops; 179 180 /** 181 * A sequence of 2 or more {@code Stop} values specifying how to distribute 182 * the colors along the gradient. These values must be in the range 183 * 0.0 to 1.0. They act like keyframes along the gradient: they mark where the 184 * gradient should be exactly a particular color. 185 * 186 * <p>Each stop in the sequence must have an offset that is greater than the previous 187 * stop in the sequence.</p> 188 * 189 * <p>The list is unmodifiable and will throw 190 * {@code UnsupportedOperationException} on each modification attempt.</p> 191 * 192 * @return the list of Stop values 193 * @defaultValue empty 194 */ 195 public final List<Stop> getStops() { 196 return stops; 197 } 198 199 /** 200 * {@inheritDoc} 201 * @since JavaFX 8.0 202 */ 203 @Override public final boolean isOpaque() { 204 return opaque; 205 } 206 207 private final boolean opaque; 208 209 /** 210 * A cached reference to the platform paint, no point recomputing twice 211 */ 212 private Object platformPaint; 213 214 /** 215 * The cached hash code, used to improve performance in situations where 216 * we cache gradients, such as in the CSS routines. 217 */ 218 private int hash; 219 220 /** 221 * Creates a new instance of RadialGradient. 222 * @param focusAngle the angle in degrees from the center of the gradient 223 * to the focus point to which the first color is mapped 224 * @param focusDistance the distance from the center of the gradient to the 225 * focus point to which the first color is mapped 226 * @param centerX the X coordinate of the center point of the gradient's circle 227 * @param centerY the Y coordinate of the center point of the gradient's circle 228 * @param radius the radius of the circle defining the extents of the color gradient 229 * @param proportional whether the coordinates and sizes are proportional 230 * to the shape which this gradient fills 231 * @param cycleMethod cycle method applied to the gradient 232 * @param stops the gradient's color specification 233 */ 234 public RadialGradient( 235 @NamedArg("focusAngle") double focusAngle, 236 @NamedArg("focusDistance") double focusDistance, 237 @NamedArg("centerX") double centerX, 238 @NamedArg("centerY") double centerY, 239 @NamedArg(value="radius", defaultValue="1") double radius, 240 @NamedArg(value="proportional", defaultValue="true") boolean proportional, 241 @NamedArg("cycleMethod") CycleMethod cycleMethod, 242 @NamedArg("stops") Stop... stops) { 243 this.focusAngle = focusAngle; 244 this.focusDistance = focusDistance; 245 this.centerX = centerX; 246 this.centerY = centerY; 247 this.radius = radius; 248 this.proportional = proportional; 249 this.cycleMethod = (cycleMethod == null) ? CycleMethod.NO_CYCLE : cycleMethod; 250 this.stops = Stop.normalize(stops); 251 this.opaque = determineOpacity(); 252 } 253 254 /** 255 * Creates a new instance of RadialGradient. 256 * @param focusAngle the angle in degrees from the center of the gradient 257 * to the focus point to which the first color is mapped 258 * @param focusDistance the distance from the center of the gradient to the 259 * focus point to which the first color is mapped 260 * @param centerX the X coordinate of the center point of the gradient's circle 261 * @param centerY the Y coordinate of the center point of the gradient's circle 262 * @param radius the radius of the circle defining the extents of the color gradient 263 * @param proportional whether the coordinates and sizes are proportional 264 * to the shape which this gradient fills 265 * @param cycleMethod cycle method applied to the gradient 266 * @param stops the gradient's color specification 267 */ 268 public RadialGradient( 269 @NamedArg("focusAngle") double focusAngle, 270 @NamedArg("focusDistance") double focusDistance, 271 @NamedArg("centerX") double centerX, 272 @NamedArg("centerY") double centerY, 273 @NamedArg(value="radius", defaultValue="1") double radius, 274 @NamedArg(value="proportional", defaultValue="true") boolean proportional, 275 @NamedArg("cycleMethod") CycleMethod cycleMethod, 276 @NamedArg("stops") List<Stop> stops) { 277 this.focusAngle = focusAngle; 278 this.focusDistance = focusDistance; 279 this.centerX = centerX; 280 this.centerY = centerY; 281 this.radius = radius; 282 this.proportional = proportional; 283 this.cycleMethod = (cycleMethod == null) ? CycleMethod.NO_CYCLE : cycleMethod; 284 this.stops = Stop.normalize(stops); 285 this.opaque = determineOpacity(); 286 } 287 288 /** 289 * Iterate over all the stops. If any one of them has a transparent 290 * color, then we return false. If there are no stops, we return false. 291 * Otherwise, we return true. Note that this is called AFTER Stop.normalize, 292 * which ensures that we always have at least 2 stops. 293 * 294 * @return Whether this gradient is opaque 295 */ 296 private boolean determineOpacity() { 297 final int numStops = this.stops.size(); 298 for (int i = 0; i < numStops; i++) { 299 if (!stops.get(i).getColor().isOpaque()) { 300 return false; 301 } 302 } 303 return true; 304 } 305 306 @Override 307 Object acc_getPlatformPaint() { 308 if (platformPaint == null) { 309 platformPaint = Toolkit.getToolkit().getPaint(this); 310 } 311 return platformPaint; 312 } 313 314 /** 315 * Indicates whether some other object is "equal to" this one. 316 * @param obj the reference object with which to compare. 317 * @return {@code true} if this object is equal to the {@code obj} argument; {@code false} otherwise. 318 */ 319 @Override public boolean equals(Object obj) { 320 if (obj == this) return true; 321 if (obj instanceof RadialGradient) { 322 final RadialGradient other = (RadialGradient) obj; 323 if ((focusAngle != other.focusAngle) || 324 (focusDistance != other.focusDistance) || 325 (centerX != other.centerX) || 326 (centerY != other.centerY) || 327 (radius != other.radius) || 328 (proportional != other.proportional) || 329 (cycleMethod != other.cycleMethod)) return false; 330 if (!stops.equals(other.stops)) return false; 331 return true; 332 } else return false; 333 } 334 335 /** 336 * Returns a hash code for this {@code RadialGradient} object. 337 * @return a hash code for this {@code RadialGradient} object. 338 */ 339 @Override public int hashCode() { 340 // We should be able to just call focusAngle.hashCode(), 341 // see http://javafx-jira.kenai.com/browse/JFXC-4247 342 if (hash == 0) { 343 long bits = 17; 344 bits = 37 * bits + Double.doubleToLongBits(focusAngle); 345 bits = 37 * bits + Double.doubleToLongBits(focusDistance); 346 bits = 37 * bits + Double.doubleToLongBits(centerX); 347 bits = 37 * bits + Double.doubleToLongBits(centerY); 348 bits = 37 * bits + Double.doubleToLongBits(radius); 349 bits = 37 * bits + (proportional ? 1231 : 1237); 350 bits = 37 * bits + cycleMethod.hashCode(); 351 for (Stop stop: stops) { 352 bits = 37 * bits + stop.hashCode(); 353 } 354 hash = (int) (bits ^ (bits >> 32)); 355 } 356 return hash; 357 } 358 359 /** 360 * Returns a string representation of this {@code RadialGradient} object. 361 * @return a string representation of this {@code RadialGradient} object. 362 */ 363 @Override public String toString() { 364 final StringBuilder s = new StringBuilder("radial-gradient(focus-angle ").append(focusAngle) 365 .append("deg, focus-distance ").append(focusDistance * 100) 366 .append("% , center ").append(GradientUtils.lengthToString(centerX, proportional)) 367 .append(" ").append(GradientUtils.lengthToString(centerY, proportional)) 368 .append(", radius ").append(GradientUtils.lengthToString(radius, proportional)) 369 .append(", "); 370 371 switch (cycleMethod) { 372 case REFLECT: 373 s.append("reflect").append(", "); 374 break; 375 case REPEAT: 376 s.append("repeat").append(", "); 377 break; 378 } 379 380 for (Stop stop : stops) { 381 s.append(stop).append(", "); 382 } 383 384 s.delete(s.length() - 2, s.length()); 385 s.append(")"); 386 387 return s.toString(); 388 } 389 390 /** 391 * Creates a radial gradient value from a string representation. 392 * <p>The format of the string representation is based on 393 * JavaFX CSS specification for radial gradient which is 394 * <pre> 395 * radial-gradient([focus-angle <angle>, ]? 396 * [focus-distance <percentage>, ]? 397 * [center <point>, ]? 398 * radius [<length> | <percentage>], 399 * [[repeat | reflect],]? 400 * <color-stop>[, <color-stop>]+) 401 * </pre> 402 * where 403 * <pre> 404 * <point> = [ [ <length> <length> ] | [ <percentage> | <percentage> ] ] 405 * <color-stop> = [ <color> [ <percentage> | <length>]? ] 406 * </pre> 407 * <p>Currently length can be only specified in px, the specification of unit can be omited. 408 * Format of color representation is the one used in {@link Color#web(String color)}. 409 * The radial-gradient keyword can be omited. 410 * For additional information about the format of string representation, see the 411 * <a href="../doc-files/cssref.html">CSS Reference Guide</a>. 412 * </p> 413 * 414 * Examples: 415 * <pre>{@code 416 * RadialGradient g 417 * = RadialGradient.valueOf("radial-gradient(center 100px 100px, radius 200px, red 0%, blue 30%, black 100%)"); 418 * RadialGradient g 419 * = RadialGradient.valueOf("center 100px 100px, radius 200px, red 0%, blue 30%, black 100%"); 420 * RadialGradient g 421 * = RadialGradient.valueOf("radial-gradient(center 50% 50%, radius 50%, cyan, violet 75%, magenta)"); 422 * RadialGradient g 423 * = RadialGradient.valueOf("center 50% 50%, radius 50%, cyan, violet 75%, magenta"); 424 * }</pre> 425 * 426 * @param value the string to convert 427 * @throws NullPointerException if the {@code value} is {@code null} 428 * @throws IllegalArgumentException if the {@code value} cannot be parsed 429 * @return a {@code RadialGradient} object holding the value represented 430 * by the string argument. 431 * @since JavaFX 2.1 432 */ 433 public static RadialGradient valueOf(String value) { 434 if (value == null) { 435 throw new NullPointerException("gradient must be specified"); 436 } 437 438 String start = "radial-gradient("; 439 String end = ")"; 440 if (value.startsWith(start)) { 441 if (!value.endsWith(end)) { 442 throw new IllegalArgumentException("Invalid gradient specification," 443 + " must end with \"" + end + '"'); 444 } 445 value = value.substring(start.length(), value.length() - end.length()); 446 } 447 448 GradientUtils.Parser parser = new GradientUtils.Parser(value); 449 if (parser.getSize() < 2) { 450 throw new IllegalArgumentException("Invalid gradient specification"); 451 } 452 453 double angle = 0, distance = 0; 454 GradientUtils.Point centerX, centerY, radius; 455 456 String[] tokens = parser.splitCurrentToken(); 457 if ("focus-angle".equals(tokens[0])) { 458 GradientUtils.Parser.checkNumberOfArguments(tokens, 1); 459 angle = GradientUtils.Parser.parseAngle(tokens[1]); 460 parser.shift(); 461 } 462 463 tokens = parser.splitCurrentToken(); 464 if ("focus-distance".equals(tokens[0])) { 465 GradientUtils.Parser.checkNumberOfArguments(tokens, 1); 466 distance = GradientUtils.Parser.parsePercentage(tokens[1]); 467 468 parser.shift(); 469 } 470 471 tokens = parser.splitCurrentToken(); 472 if ("center".equals(tokens[0])) { 473 GradientUtils.Parser.checkNumberOfArguments(tokens, 2); 474 centerX = parser.parsePoint(tokens[1]); 475 centerY = parser.parsePoint(tokens[2]); 476 parser.shift(); 477 } else { 478 centerX = GradientUtils.Point.MIN; 479 centerY = GradientUtils.Point.MIN; 480 } 481 482 tokens = parser.splitCurrentToken(); 483 if ("radius".equals(tokens[0])) { 484 GradientUtils.Parser.checkNumberOfArguments(tokens, 1); 485 radius = parser.parsePoint(tokens[1]); 486 parser.shift(); 487 } else { 488 throw new IllegalArgumentException("Invalid gradient specification: " 489 + "radius must be specified"); 490 } 491 492 CycleMethod method = CycleMethod.NO_CYCLE; 493 String currentToken = parser.getCurrentToken(); 494 if ("repeat".equals(currentToken)) { 495 method = CycleMethod.REPEAT; 496 parser.shift(); 497 } else if ("reflect".equals(currentToken)) { 498 method = CycleMethod.REFLECT; 499 parser.shift(); 500 } 501 502 Stop[] stops = parser.parseStops(radius.proportional, radius.value); 503 504 return new RadialGradient(angle, distance, centerX.value, centerY.value, 505 radius.value, radius.proportional, method, stops); 506 } 507 508 }