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 &lt;angle&gt;, ]?
 396      *                 [focus-distance &lt;percentage&gt;, ]?
 397      *                 [center &lt;point&gt;, ]?
 398      *                 radius [&lt;length&gt; | &lt;percentage&gt;],
 399      *                 [[repeat | reflect],]?
 400      *                 &lt;color-stop&gt;[, &lt;color-stop&gt;]+)
 401      * </pre>
 402      * where
 403      * <pre>
 404      * &lt;point&gt; = [ [ &lt;length&gt; &lt;length&gt; ] | [ &lt;percentage&gt; | &lt;percentage&gt; ] ]
 405      * &lt;color-stop&gt; = [ &lt;color&gt; [ &lt;percentage&gt; | &lt;length&gt;]? ]
 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 }