Spec-Zone .ru
спецификации, руководства, описания, API
001/*
002 * Copyright (c) 2010, 2013, Oracle and/or its affiliates. All rights reserved.
003 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
004 *
005 * This code is free software; you can redistribute it and/or modify it
006 * under the terms of the GNU General Public License version 2 only, as
007 * published by the Free Software Foundation.  Oracle designates this
008 * particular file as subject to the "Classpath" exception as provided
009 * by Oracle in the LICENSE file that accompanied this code.
010 *
011 * This code is distributed in the hope that it will be useful, but WITHOUT
012 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
013 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
014 * version 2 for more details (a copy is included in the LICENSE file that
015 * accompanied this code).
016 *
017 * You should have received a copy of the GNU General Public License version
018 * 2 along with this work; if not, write to the Free Software Foundation,
019 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
020 *
021 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
022 * or visit www.oracle.com if you need additional information or have any
023 * questions.
024 */
025
026package javafx.scene.paint;
027
028import java.util.List;
029
030import com.sun.javafx.beans.annotations.Default;
031import com.sun.javafx.collections.annotations.ReturnsUnmodifiableCollection;
032import com.sun.javafx.scene.paint.GradientUtils;
033import com.sun.javafx.tk.Toolkit;
034
035/**
036 * The {@code RadialGradient} class provides a way to fill a shape
037 * with a circular radial color gradient pattern.
038 * The user may specify 2 or more gradient colors,
039 * and this paint will provide an interpolation between each color.
040 * <p/>
041 * The user must specify the circle controlling the gradient pattern,
042 * which is defined by a center point and a radius.
043 * The user can also specify a separate focus point within that circle,
044 * which controls the location of the first color of the gradient.
045 * By default the focus is set to be the center of the circle.
046 * <p/>
047 * The center and radius are specified
048 * relative to a unit square, unless the <code>proportional</code>
049 * variable is false.  By default proportional is true, and the
050 * gradient will be scaled to fill whatever shape it is applied to.
051 * The focus point is always specified relative to the center point
052 * by an angle and a distance relative to the radius.
053 * <p/>
054 * This paint will map the first color of the gradient to the focus point,
055 * and the last color to the perimeter of the circle,
056 * interpolating smoothly for any in-between colors specified by the user.
057 * Any line drawn from the focus point to the circumference will
058 * thus span all of the gradient colors.
059 * <p/>
060 * The focus distance will be clamped to the range {@code (-1, 1)}
061 * so that the focus point is always strictly inside the circle.
062 * <p/>
063 * The application provides an array of {@code Stop}s specifying how to distribute
064 * the colors along the gradient. The {@code Stop#offset} variable must be
065 * the range 0.0 to 1.0 and act like keyframes along the gradient.
066 * They mark where the gradient should be exactly a particular color.
067 */
068public final class RadialGradient extends Paint {
069    private double focusAngle;
070
071    /**
072     * Defines the angle in degrees from the center of the gradient
073     * to the focus point to which the first color is mapped.
074     */
075    public final double getFocusAngle() {
076        return focusAngle;
077    }
078
079    private double focusDistance;
080
081    /**
082     * Defines the distance from the center of the gradient to the
083     * focus point to which the first color is mapped.
084     * A distance of 0.0 will be at the center of the gradient circle.
085     * A distance of 1.0 will be on the circumference of the gradient circle.
086     */
087    public final double getFocusDistance() {
088        return focusDistance;
089    }
090
091    private double centerX;
092
093    /**
094     * Defines the X coordinate of the center point of the circle defining the gradient.
095     * If proportional is true (the default), this value specifies a
096     * point on a unit square that will be scaled to match the size of the
097     * the shape that the gradient fills.
098     * The last color of the gradient is mapped to the perimeter of this circle.
099     *
100     * @defaultValue 0.0
101     */
102    public final double getCenterX() {
103        return centerX;
104    }
105
106    private double centerY;
107
108    /**
109     * Defines the X coordinate of the center point of the circle defining the gradient.
110     * If proportional is true (the default), this value specifies a
111     * point on a unit square that will be scaled to match the size of the
112     * the shape that the gradient fills.
113     * The last color of the gradient is mapped to the perimeter of this circle.
114     *
115     * @defaultValue 0.0
116     */
117    public final double getCenterY() {
118        return centerY;
119    }
120
121    private double radius;
122
123    /**
124     * Specifies the radius of the circle defining the extents of the color gradient.
125     * If proportional is true (the default), this value specifies a
126     * size relative to  unit square that will be scaled to match the size of the
127     * the shape that the gradient fills.
128     *
129     * @defaultValue 1.0
130     */
131    public final double getRadius() {
132        return radius;
133    }
134
135    private boolean proportional;
136
137    /**
138     * Indicates whether the center and radius values are proportional or
139     * absolute.
140     * If this flag is true, the center point and radius are defined
141     * in a coordinate space where coordinates in the range {@code [0..1]}
142     * are scaled to map onto the bounds of the shape that the gradient fills.
143     * If this flag is false, then the center coordinates and the radius are
144     * specified in the local coordinate system of the node.
145     *
146     * @defaultValue true
147     */
148    public final boolean isProportional() {
149        return proportional;
150    }
151
152    private CycleMethod cycleMethod;
153
154    /**
155     * Defines which of the follwing cycle method is applied
156     * to the {@code LinearGradient}: {@code CycleMethod.NO_CYCLE},
157     * {@code CycleMethod.REFLECT}, or {@code CycleMethod.REPEAT}.
158     *
159     * @defaultValue NO_CYCLE
160     */
161    public final CycleMethod getCycleMethod() {
162        return cycleMethod;
163    }
164
165    private List<Stop> stops;
166
167    /**
168     * A sequence of 2 or more {@code Stop} values specifying how to distribute
169     * the colors along the gradient. These values must be in the range
170     * 0.0 to 1.0. They act like keyframes along the gradient: they mark where the
171     * gradient should be exactly a particular color.
172     *
173     * <p>Each stop in the sequence must have an offset that is greater than the previous
174     * stop in the sequence.</p>
175     *
176     * <p>The list is unmodifiable and will throw
177     * {@code UnsupportedOperationException} on each modification attempt.</p>
178     *
179     * @defaultValue empty
180     */
181    @ReturnsUnmodifiableCollection
182    public final List<Stop> getStops() {
183        return stops;
184    }
185
186    /**
187     * @inheritDoc
188     */
189    @Override public final boolean isOpaque() {
190        return opaque;
191    }
192
193    private final boolean opaque;
194
195    /**
196     * A cached reference to the platform paint, no point recomputing twice
197     */
198    private Object platformPaint;
199
200    /**
201     * The cached hash code, used to improve performance in situations where
202     * we cache gradients, such as in the CSS routines.
203     */
204    private int hash;
205
206    /**
207     * Creates a new instance of RadialGradient.
208     * @param focusAngle the angle in degrees from the center of the gradient
209     * to the focus point to which the first color is mapped
210     * @param focusDistance the distance from the center of the gradient to the
211     * focus point to which the first color is mapped
212     * @param centerX the X coordinate of the center point of the gradient's circle 
213     * @param centerY the Y coordinate of the center point of the gradient's circle 
214     * @param radius the radius of the circle defining the extents of the color gradient
215     * @param proportional whether the coordinates and sizes are proportional
216     * to the shape which this gradient fills
217     * @param cycleMethod cycle method applied to the gradient
218     * @param stops the gradient's color specification
219     */
220    public RadialGradient(
221            double focusAngle,
222            double focusDistance,
223            double centerX,
224            double centerY,
225            @Default("1") double radius,
226            @Default("true") boolean proportional,
227            CycleMethod cycleMethod,
228            Stop... stops) {
229        this.focusAngle = focusAngle;
230        this.focusDistance = focusDistance;
231        this.centerX = centerX;
232        this.centerY = centerY;
233        this.radius = radius;
234        this.proportional = proportional;
235        this.cycleMethod = (cycleMethod == null) ? CycleMethod.NO_CYCLE : cycleMethod;
236        this.stops = Stop.normalize(stops);
237        this.opaque = determineOpacity();
238    }
239
240    /**
241     * Creates a new instance of RadialGradient.
242     * @param focusAngle the angle in degrees from the center of the gradient
243     * to the focus point to which the first color is mapped
244     * @param focusDistance the distance from the center of the gradient to the
245     * focus point to which the first color is mapped
246     * @param centerX the X coordinate of the center point of the gradient's circle 
247     * @param centerY the Y coordinate of the center point of the gradient's circle 
248     * @param radius the radius of the circle defining the extents of the color gradient
249     * @param proportional whether the coordinates and sizes are proportional
250     * to the shape which this gradient fills
251     * @param cycleMethod cycle method applied to the gradient
252     * @param stops the gradient's color specification
253     */
254    public RadialGradient(
255            double focusAngle,
256            double focusDistance,
257            double centerX,
258            double centerY,
259            @Default("1") double radius,
260            @Default("true") boolean proportional,
261            CycleMethod cycleMethod,
262            List<Stop> stops) {
263        this.focusAngle = focusAngle;
264        this.focusDistance = focusDistance;
265        this.centerX = centerX;
266        this.centerY = centerY;
267        this.radius = radius;
268        this.proportional = proportional;
269        this.cycleMethod = (cycleMethod == null) ? CycleMethod.NO_CYCLE : cycleMethod;
270        this.stops = Stop.normalize(stops);
271        this.opaque = determineOpacity();
272    }
273
274    /**
275     * Iterate over all the stops. If any one of them has a transparent
276     * color, then we return false. If there are no stops, we return false.
277     * Otherwise, we return true. Note that this is called AFTER Stop.normalize,
278     * which ensures that we always have at least 2 stops.
279     *
280     * @return Whether this gradient is opaque
281     */
282    private boolean determineOpacity() {
283        final int numStops = this.stops.size();
284        for (int i = 0; i < numStops; i++) {
285            if (!stops.get(i).getColor().isOpaque()) {
286                return false;
287            }
288        }
289        return true;
290    }
291
292    @Override
293    Object acc_getPlatformPaint() {
294        if (platformPaint == null) {
295            platformPaint = Toolkit.getToolkit().getPaint(this);
296        }
297        return platformPaint;
298    }
299
300    /**
301     * Indicates whether some other object is "equal to" this one.
302     * @param obj the reference object with which to compare.
303     * @return {@code true} if this object is equal to the {@code obj} argument; {@code false} otherwise.
304     */
305    @Override public boolean equals(Object obj) {
306        if (obj == this) return true;
307        if (obj instanceof RadialGradient) {
308            final RadialGradient other = (RadialGradient) obj;
309            if ((focusAngle != other.focusAngle) ||
310                (focusDistance != other.focusDistance) ||
311                (centerX != other.centerX) ||
312                (centerY != other.centerY) ||
313                (radius != other.radius) ||
314                (proportional != other.proportional) ||
315                (cycleMethod != other.cycleMethod)) return false;
316            if (!stops.equals(other.stops)) return false;
317            return true;
318        } else return false;
319    }
320
321    /**
322     * Returns a hash code for this {@code RadialGradient} object.
323     * @return a hash code for this {@code RadialGradient} object.
324     */ 
325    @Override public int hashCode() {
326        // We should be able to just call focusAngle.hashCode(),
327        // see http://javafx-jira.kenai.com/browse/JFXC-4247
328        if (hash == 0) {
329            long bits = 17;
330            bits = 37 * bits + Double.doubleToLongBits(focusAngle);
331            bits = 37 * bits + Double.doubleToLongBits(focusDistance);
332            bits = 37 * bits + Double.doubleToLongBits(centerX);
333            bits = 37 * bits + Double.doubleToLongBits(centerY);
334            bits = 37 * bits + Double.doubleToLongBits(radius);
335            bits = 37 * bits + (proportional ? 1231 : 1237);
336            bits = 37 * bits + cycleMethod.hashCode();
337            for (Stop stop: stops) {
338                bits = 37 * bits + stop.hashCode();
339            }
340            hash = (int) (bits ^ (bits >> 32));
341        }
342        return hash;
343    }
344
345    /**
346     * Returns a string representation of this {@code RadialGradient} object.
347     * @return a string representation of this {@code RadialGradient} object.
348     */ 
349    @Override public String toString() {
350        final StringBuilder s = new StringBuilder("radial-gradient(focus-angle ").append(focusAngle)
351                .append("deg, focus-distance ").append(focusDistance * 100)
352                .append("% , center ").append(GradientUtils.lengthToString(centerX, proportional))
353                .append(" ").append(GradientUtils.lengthToString(centerY, proportional))
354                .append(", radius ").append(GradientUtils.lengthToString(radius, proportional))
355                .append(", ");                
356
357        switch (cycleMethod) {
358            case REFLECT:
359                s.append("reflect").append(", ");
360                break;
361            case REPEAT:
362                s.append("repeat").append(", ");
363                break;
364        }
365
366        for (Stop stop : stops) {
367            s.append(stop).append(", ");
368        }
369
370        s.delete(s.length() - 2, s.length());
371        s.append(")");
372
373        return s.toString();
374    }
375
376    /**
377     * Creates a radial gradient value from a string representation. 
378     * <p>The format of the string representation is based on 
379     * JavaFX CSS specification for radial gradient which is 
380     * <pre>
381     * radial-gradient([focus-angle &lt;angle&gt;, ]? 
382     *                 [focus-distance &lt;percentage&gt;, ]? 
383     *                 [center &lt;point&gt;, ]? 
384     *                 radius [&lt;length&gt; | &lt;percentage&gt;], 
385     *                 [[repeat | reflect],]? 
386     *                 &lt;color-stop&gt;[, &lt;color-stop&gt;]+) 
387     * </pre>
388     * where
389     * <pre>
390     * &lt;point&gt; = [ [ &lt;length&gt; &lt;length&gt; ] | [ &lt;percentage&gt; | &lt;percentage&gt; ] ]
391     * &lt;color-stop&gt; = [ &lt;color&gt; [ &lt;percentage&gt; | &lt;length&gt;]? ]
392     * </pre>
393     * </p>
394     * <p>Currently length can be only specified in px, the specification of unit can be omited.
395     * Format of color representation is the one used in {@link Color#web(String color)}.
396     * The radial-gradient keyword can be omited.
397     * For additional information about the format of string representation, see the 
398     * <a href="../doc-files/cssref.html">CSS Reference Guide</a>.
399     * </p>
400     * 
401     * Examples:
402     * <pre><code>
403     * RadialGradient g
404     *      = RadialGradient.valueOf("radial-gradient(center 100px 100px, radius 200px, red  0%, blue 30%, black 100%)");
405     * RadialGradient g
406     *      = RadialGradient.valueOf("center 100px 100px, radius 200px, red  0%, blue 30%, black 100%");
407     * RadialGradient g
408     *      = RadialGradient.valueOf("radial-gradient(center 50% 50%, radius 50%,  cyan, violet 75%, magenta)");
409     * RadialGradient g
410     *      = RadialGradient.valueOf("center 50% 50%, radius 50%,  cyan, violet 75%, magenta");
411     * </code></pre>
412     *
413     * @param value the string to convert
414     * @throws NullPointerException if the {@code value} is {@code null}
415     * @throws IllegalArgumentException if the {@code value} cannot be parsed
416     * @return a {@code RadialGradient} object holding the value represented 
417     * by the string argument.
418     */
419    public static RadialGradient valueOf(String value) {
420        if (value == null) {
421            throw new NullPointerException("gradient must be specified");
422        }
423
424        String start = "radial-gradient(";
425        String end = ")";
426        if (value.startsWith(start)) {
427            if (!value.endsWith(end)) {
428                throw new IllegalArgumentException("Invalid gradient specification,"
429                        + " must end with \"" + end + '"');
430            }
431            value = value.substring(start.length(), value.length() - end.length());
432        }        
433
434        GradientUtils.Parser parser = new GradientUtils.Parser(value);
435        if (parser.getSize() < 2) {
436            throw new IllegalArgumentException("Invalid gradient specification");
437        }
438        
439        double angle = 0, distance = 0;
440        GradientUtils.Point centerX, centerY, radius;
441
442        String[] tokens = parser.splitCurrentToken();
443        if ("focus-angle".equals(tokens[0])) {
444            GradientUtils.Parser.checkNumberOfArguments(tokens, 1);
445            angle = GradientUtils.Parser.parseAngle(tokens[1]);
446            parser.shift();
447        }
448        
449        tokens = parser.splitCurrentToken();
450        if ("focus-distance".equals(tokens[0])) {
451            GradientUtils.Parser.checkNumberOfArguments(tokens, 1);
452            distance = GradientUtils.Parser.parsePercentage(tokens[1]);
453
454            parser.shift();
455        }
456
457        tokens = parser.splitCurrentToken();
458        if ("center".equals(tokens[0])) {
459            GradientUtils.Parser.checkNumberOfArguments(tokens, 2);
460            centerX = parser.parsePoint(tokens[1]);
461            centerY = parser.parsePoint(tokens[2]);
462            parser.shift();
463        } else {
464            centerX = GradientUtils.Point.MIN;
465            centerY = GradientUtils.Point.MIN;
466        }
467
468        tokens = parser.splitCurrentToken();
469        if ("radius".equals(tokens[0])) {
470            GradientUtils.Parser.checkNumberOfArguments(tokens, 1);
471            radius = parser.parsePoint(tokens[1]);
472            parser.shift();
473        } else {
474            throw new IllegalArgumentException("Invalid gradient specification: "
475                    + "radius must be specified");
476        }
477        
478        CycleMethod method = CycleMethod.NO_CYCLE;
479        String currentToken = parser.getCurrentToken();
480        if ("repeat".equals(currentToken)) {
481            method = CycleMethod.REPEAT;
482            parser.shift();
483        } else if ("reflect".equals(currentToken)) {
484            method = CycleMethod.REFLECT;
485            parser.shift();
486        }
487        
488        Stop[] stops = parser.parseStops(radius.proportional, radius.value);
489
490        return new RadialGradient(angle, distance, centerX.value, centerY.value,
491                                  radius.value, radius.proportional, method, stops);
492    }
493    
494}