javafx.scene.paint.RadialGradient Maven / Gradle / Ivy
/*
* Copyright (c) 2010, 2024, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package javafx.scene.paint;
import java.util.List;
import java.util.Objects;
import com.sun.javafx.scene.paint.GradientUtils;
import com.sun.javafx.tk.Toolkit;
import com.sun.javafx.util.InterpolationUtils;
import javafx.beans.NamedArg;
/**
* The {@code RadialGradient} class provides a way to fill a shape
* with a circular radial color gradient pattern.
* The user may specify 2 or more gradient colors,
* and this paint will provide an interpolation between each color.
*
* The user must specify the circle controlling the gradient pattern,
* which is defined by a center point and a radius.
* The user can also specify a separate focus point within that circle,
* which controls the location of the first color of the gradient.
* By default the focus is set to be the center of the circle.
*
* The center and radius are specified
* relative to a unit square, unless the proportional
* variable is false. By default proportional is true, and the
* gradient will be scaled to fill whatever shape it is applied to.
* The focus point is always specified relative to the center point
* by an angle and a distance relative to the radius.
*
* This paint will map the first color of the gradient to the focus point,
* and the last color to the perimeter of the circle,
* interpolating smoothly for any in-between colors specified by the user.
* Any line drawn from the focus point to the circumference will
* thus span all of the gradient colors.
*
* The focus distance will be clamped to the range {@code (-1, 1)}
* so that the focus point is always strictly inside the circle.
*
* The application provides an array of {@code Stop}s specifying how to distribute
* the colors along the gradient. The {@code Stop#offset} variable must be
* the range 0.0 to 1.0 and act like keyframes along the gradient.
* They mark where the gradient should be exactly a particular color.
* @since JavaFX 2.0
*/
public final class RadialGradient extends Paint {
private double focusAngle;
/**
* Defines the angle in degrees from the center of the gradient
* to the focus point to which the first color is mapped.
*
* @return the angle in degrees from the center of the gradient to the focus point
* to which the first color is mapped
* @interpolationType linear
*/
public final double getFocusAngle() {
return focusAngle;
}
private double focusDistance;
/**
* Defines the distance from the center of the gradient to the
* focus point to which the first color is mapped.
* A distance of 0.0 will be at the center of the gradient circle.
* A distance of 1.0 will be on the circumference of the gradient circle.
*
* @return the distance from the center of the gradient to the focus point to which
* the first color is mapped
* @interpolationType linear
*/
public final double getFocusDistance() {
return focusDistance;
}
private double centerX;
/**
* Defines the X coordinate of the center point of the circle defining the gradient.
* If proportional is true (the default), this value specifies a
* point on a unit square that will be scaled to match the size of
* the shape that the gradient fills.
* The last color of the gradient is mapped to the perimeter of this circle.
*
* @return the X coordinate of the center point of the circle defining the gradient
* @defaultValue 0.0
* @interpolationType linear
* if both values are absolute or both values are {@link #isProportional() proportional},
* discrete otherwise
*/
public final double getCenterX() {
return centerX;
}
private double centerY;
/**
* Defines the X coordinate of the center point of the circle defining the gradient.
* If proportional is true (the default), this value specifies a
* point on a unit square that will be scaled to match the size of the
* the shape that the gradient fills.
* The last color of the gradient is mapped to the perimeter of this circle.
*
* @return the X coordinate of the center point of the circle defining the gradient
* @defaultValue 0.0
* @interpolationType linear
* if both values are absolute or both values are {@link #isProportional() proportional},
* discrete otherwise
*/
public final double getCenterY() {
return centerY;
}
private double radius;
/**
* Specifies the radius of the circle defining the extents of the color gradient.
* If proportional is true (the default), this value specifies a
* size relative to unit square that will be scaled to match the size of the
* the shape that the gradient fills.
*
* @return the radius of the circle defining the extents of the color gradient
* @defaultValue 1.0
* @interpolationType linear
* if both values are absolute or both values are {@link #isProportional() proportional},
* discrete otherwise
*/
public final double getRadius() {
return radius;
}
private boolean proportional;
/**
* Indicates whether the center and radius values are proportional or
* absolute.
* If this flag is true, the center point and radius are defined
* in a coordinate space where coordinates in the range {@code [0..1]}
* are scaled to map onto the bounds of the shape that the gradient fills.
* If this flag is false, then the center coordinates and the radius are
* specified in the local coordinate system of the node.
*
* @return true if the center and radius values are proportional, otherwise absolute
* @defaultValue true
* @interpolationType discrete
*/
public final boolean isProportional() {
return proportional;
}
private CycleMethod cycleMethod;
/**
* Defines the cycle method applied
* to the {@code RadialGradient}. One of: {@code CycleMethod.NO_CYCLE},
* {@code CycleMethod.REFLECT}, or {@code CycleMethod.REPEAT}.
*
* @return the cycle method applied to this radial gradient
* @defaultValue NO_CYCLE
* @interpolationType discrete
*/
public final CycleMethod getCycleMethod() {
return cycleMethod;
}
private List stops;
/**
* A sequence of 2 or more {@code Stop} values specifying how to distribute
* the colors along the gradient. These values must be in the range
* 0.0 to 1.0. They act like keyframes along the gradient: they mark where the
* gradient should be exactly a particular color.
*
* Each stop in the sequence must have an offset that is greater than the previous
* stop in the sequence.
*
* The list is unmodifiable and will throw
* {@code UnsupportedOperationException} on each modification attempt.
*
* @return the list of Stop values
* @defaultValue empty
* @interpolationType Stop list interpolation produces smooth transitions of gradient stops by allowing
* the insertion of new stops along the gradient. At most, the intermediate stop list
* has the combined number of gradient stops of both the start list and the target list.
*/
public final List getStops() {
return stops;
}
/**
* {@inheritDoc}
* @since JavaFX 8.0
*/
@Override public final boolean isOpaque() {
return opaque;
}
private final boolean opaque;
/**
* A cached reference to the platform paint, no point recomputing twice
*/
private Object platformPaint;
/**
* The cached hash code, used to improve performance in situations where
* we cache gradients, such as in the CSS routines.
*/
private int hash;
/**
* Creates a new instance of RadialGradient.
* @param focusAngle the angle in degrees from the center of the gradient
* to the focus point to which the first color is mapped
* @param focusDistance the distance from the center of the gradient to the
* focus point to which the first color is mapped
* @param centerX the X coordinate of the center point of the gradient's circle
* @param centerY the Y coordinate of the center point of the gradient's circle
* @param radius the radius of the circle defining the extents of the color gradient
* @param proportional whether the coordinates and sizes are proportional
* to the shape which this gradient fills
* @param cycleMethod cycle method applied to the gradient
* @param stops the gradient's color specification
*/
public RadialGradient(
@NamedArg("focusAngle") double focusAngle,
@NamedArg("focusDistance") double focusDistance,
@NamedArg("centerX") double centerX,
@NamedArg("centerY") double centerY,
@NamedArg(value="radius", defaultValue="1") double radius,
@NamedArg(value="proportional", defaultValue="true") boolean proportional,
@NamedArg("cycleMethod") CycleMethod cycleMethod,
@NamedArg("stops") Stop... stops) {
this.focusAngle = focusAngle;
this.focusDistance = focusDistance;
this.centerX = centerX;
this.centerY = centerY;
this.radius = radius;
this.proportional = proportional;
this.cycleMethod = (cycleMethod == null) ? CycleMethod.NO_CYCLE : cycleMethod;
this.stops = Stop.normalize(stops);
this.opaque = determineOpacity();
}
/**
* Creates a new instance of RadialGradient.
* @param focusAngle the angle in degrees from the center of the gradient
* to the focus point to which the first color is mapped
* @param focusDistance the distance from the center of the gradient to the
* focus point to which the first color is mapped
* @param centerX the X coordinate of the center point of the gradient's circle
* @param centerY the Y coordinate of the center point of the gradient's circle
* @param radius the radius of the circle defining the extents of the color gradient
* @param proportional whether the coordinates and sizes are proportional
* to the shape which this gradient fills
* @param cycleMethod cycle method applied to the gradient
* @param stops the gradient's color specification
*/
public RadialGradient(
@NamedArg("focusAngle") double focusAngle,
@NamedArg("focusDistance") double focusDistance,
@NamedArg("centerX") double centerX,
@NamedArg("centerY") double centerY,
@NamedArg(value="radius", defaultValue="1") double radius,
@NamedArg(value="proportional", defaultValue="true") boolean proportional,
@NamedArg("cycleMethod") CycleMethod cycleMethod,
@NamedArg("stops") List stops) {
this.focusAngle = focusAngle;
this.focusDistance = focusDistance;
this.centerX = centerX;
this.centerY = centerY;
this.radius = radius;
this.proportional = proportional;
this.cycleMethod = (cycleMethod == null) ? CycleMethod.NO_CYCLE : cycleMethod;
this.stops = Stop.normalize(stops);
this.opaque = determineOpacity();
}
/**
* Private constructor accepting a stop list that is already normalized.
* This constructor is only called from the {@link #interpolate} method.
*/
private RadialGradient(
double focusAngle, double focusDistance, double centerX, double centerY, double radius,
boolean proportional, CycleMethod cycleMethod, List stops, int ignored) {
this.focusAngle = focusAngle;
this.focusDistance = focusDistance;
this.centerX = centerX;
this.centerY = centerY;
this.radius = radius;
this.proportional = proportional;
this.cycleMethod = cycleMethod;
this.stops = stops;
this.opaque = determineOpacity();
}
/**
* Iterate over all the stops. If any one of them has a transparent
* color, then we return false. If there are no stops, we return false.
* Otherwise, we return true. Note that this is called AFTER Stop.normalize,
* which ensures that we always have at least 2 stops.
*
* @return Whether this gradient is opaque
*/
private boolean determineOpacity() {
final int numStops = this.stops.size();
for (int i = 0; i < numStops; i++) {
if (!stops.get(i).getColor().isOpaque()) {
return false;
}
}
return true;
}
@Override
Object acc_getPlatformPaint() {
if (platformPaint == null) {
platformPaint = Toolkit.getToolkit().getPaint(this);
}
return platformPaint;
}
/**
* Returns an intermediate value between the value of this {@code RadialGradient} and the specified
* {@code endValue} using the linear interpolation factor {@code t}, ranging from 0 (inclusive)
* to 1 (inclusive).
*
* @param endValue the target value
* @param t the interpolation factor
* @throws NullPointerException if {@code endValue} is {@code null}
* @return the intermediate value
* @since 24
*/
public RadialGradient interpolate(RadialGradient endValue, double t) {
Objects.requireNonNull(endValue, "endValue cannot be null");
// We don't check equals(endValue) here to prevent unnecessary equality checks,
// and only check for equality with 'this' or 'endValue' after interpolation.
if (t <= 0.0) {
return this;
}
if (t >= 1.0) {
return endValue;
}
double newCenterX, newCenterY, newRadius;
boolean newProportional;
if (this.proportional == endValue.proportional) {
newCenterX = InterpolationUtils.interpolate(this.centerX, endValue.centerX, t);
newCenterY = InterpolationUtils.interpolate(this.centerY, endValue.centerY, t);
newRadius = InterpolationUtils.interpolate(this.radius, endValue.radius, t);
newProportional = this.proportional;
} else if (t < 0.5) {
newCenterX = this.centerX;
newCenterY = this.centerY;
newRadius = this.radius;
newProportional = this.proportional;
} else {
newCenterX = endValue.centerX;
newCenterY = endValue.centerY;
newRadius = endValue.radius;
newProportional = endValue.proportional;
}
double newFocusAngle = InterpolationUtils.interpolate(this.focusAngle, endValue.focusAngle, t);
double newFocusDistance = InterpolationUtils.interpolate(this.focusDistance, endValue.focusDistance, t);
CycleMethod newCycleMethod = InterpolationUtils.interpolateDiscrete(this.cycleMethod, endValue.cycleMethod, t);
// Optimization: if both lists are equal, we don't compute a new intermediate list.
List newStops = this.stops.equals(endValue.stops) ?
null : Stop.interpolateLists(this.stops, endValue.stops, t);
if (isSame(newFocusAngle, newFocusDistance, newCenterX, newCenterY, newRadius, newProportional,
newCycleMethod, Objects.requireNonNullElse(newStops, this.stops))) {
return this;
}
if (endValue.isSame(newFocusAngle, newFocusDistance, newCenterX, newCenterY, newRadius, newProportional,
newCycleMethod, Objects.requireNonNullElse(newStops, endValue.stops))) {
return endValue;
}
return new RadialGradient(newFocusAngle, newFocusDistance, newCenterX, newCenterY, newRadius, newProportional,
newCycleMethod, Objects.requireNonNullElse(newStops, this.stops), 0);
}
private boolean isSame(double focusAngle, double focusDistance, double centerX, double centerY,
double radius, boolean proportional, CycleMethod cycleMethod, List stops) {
return this.focusAngle == focusAngle
&& this.focusDistance == focusDistance
&& this.centerX == centerX
&& this.centerY == centerY
&& this.radius == radius
&& this.proportional == proportional
&& this.cycleMethod == cycleMethod
&& this.stops == stops;
}
/**
* {@inheritDoc}
*
* @throws NullPointerException {@inheritDoc}
* @since 24
*/
@Override
public Paint interpolate(Paint endValue, double t) {
return InterpolationUtils.interpolatePaint(this, endValue, t);
}
/**
* Indicates whether some other object is "equal to" this one.
* @param obj the reference object with which to compare.
* @return {@code true} if this object is equal to the {@code obj} argument; {@code false} otherwise.
*/
@Override public boolean equals(Object obj) {
if (obj == this) return true;
if (obj instanceof RadialGradient) {
final RadialGradient other = (RadialGradient) obj;
if ((focusAngle != other.focusAngle) ||
(focusDistance != other.focusDistance) ||
(centerX != other.centerX) ||
(centerY != other.centerY) ||
(radius != other.radius) ||
(proportional != other.proportional) ||
(cycleMethod != other.cycleMethod)) return false;
if (!stops.equals(other.stops)) return false;
return true;
} else return false;
}
/**
* Returns a hash code for this {@code RadialGradient} object.
* @return a hash code for this {@code RadialGradient} object.
*/
@Override public int hashCode() {
// We should be able to just call focusAngle.hashCode(),
// see http://javafx-jira.kenai.com/browse/JFXC-4247
if (hash == 0) {
long bits = 17;
bits = 37 * bits + Double.doubleToLongBits(focusAngle);
bits = 37 * bits + Double.doubleToLongBits(focusDistance);
bits = 37 * bits + Double.doubleToLongBits(centerX);
bits = 37 * bits + Double.doubleToLongBits(centerY);
bits = 37 * bits + Double.doubleToLongBits(radius);
bits = 37 * bits + (proportional ? 1231 : 1237);
bits = 37 * bits + cycleMethod.hashCode();
for (Stop stop: stops) {
bits = 37 * bits + stop.hashCode();
}
hash = (int) (bits ^ (bits >> 32));
}
return hash;
}
/**
* Returns a string representation of this {@code RadialGradient} object.
* @return a string representation of this {@code RadialGradient} object.
*/
@Override public String toString() {
final StringBuilder s = new StringBuilder("radial-gradient(focus-angle ").append(focusAngle)
.append("deg, focus-distance ").append(focusDistance * 100)
.append("% , center ").append(GradientUtils.lengthToString(centerX, proportional))
.append(" ").append(GradientUtils.lengthToString(centerY, proportional))
.append(", radius ").append(GradientUtils.lengthToString(radius, proportional))
.append(", ");
switch (cycleMethod) {
case REFLECT:
s.append("reflect").append(", ");
break;
case REPEAT:
s.append("repeat").append(", ");
break;
}
for (Stop stop : stops) {
s.append(stop).append(", ");
}
s.delete(s.length() - 2, s.length());
s.append(")");
return s.toString();
}
/**
* Creates a radial gradient value from a string representation.
* The format of the string representation is based on
* JavaFX CSS specification for radial gradient which is
*
* radial-gradient([focus-angle <angle>, ]?
* [focus-distance <percentage>, ]?
* [center <point>, ]?
* radius [<length> | <percentage>],
* [[repeat | reflect],]?
* <color-stop>[, <color-stop>]+)
*
* where
*
* <point> = [ [ <length> <length> ] | [ <percentage> | <percentage> ] ]
* <color-stop> = [ <color> [ <percentage> | <length>]? ]
*
* Currently length can be only specified in px, the specification of unit can be omited.
* Format of color representation is the one used in {@link Color#web(String color)}.
* The radial-gradient keyword can be omited.
* For additional information about the format of string representation, see the
* CSS Reference Guide.
*
*
* Examples:
* {@code
* RadialGradient g
* = RadialGradient.valueOf("radial-gradient(center 100px 100px, radius 200px, red 0%, blue 30%, black 100%)");
* RadialGradient g
* = RadialGradient.valueOf("center 100px 100px, radius 200px, red 0%, blue 30%, black 100%");
* RadialGradient g
* = RadialGradient.valueOf("radial-gradient(center 50% 50%, radius 50%, cyan, violet 75%, magenta)");
* RadialGradient g
* = RadialGradient.valueOf("center 50% 50%, radius 50%, cyan, violet 75%, magenta");
* }
*
* @param value the string to convert
* @throws NullPointerException if the {@code value} is {@code null}
* @throws IllegalArgumentException if the {@code value} cannot be parsed
* @return a {@code RadialGradient} object holding the value represented
* by the string argument.
* @since JavaFX 2.1
*/
public static RadialGradient valueOf(String value) {
if (value == null) {
throw new NullPointerException("gradient must be specified");
}
String start = "radial-gradient(";
String end = ")";
if (value.startsWith(start)) {
if (!value.endsWith(end)) {
throw new IllegalArgumentException("Invalid gradient specification,"
+ " must end with \"" + end + '"');
}
value = value.substring(start.length(), value.length() - end.length());
}
GradientUtils.Parser parser = new GradientUtils.Parser(value);
if (parser.getSize() < 2) {
throw new IllegalArgumentException("Invalid gradient specification");
}
double angle = 0, distance = 0;
GradientUtils.Point centerX, centerY, radius;
String[] tokens = parser.splitCurrentToken();
if ("focus-angle".equals(tokens[0])) {
GradientUtils.Parser.checkNumberOfArguments(tokens, 1);
angle = GradientUtils.Parser.parseAngle(tokens[1]);
parser.shift();
}
tokens = parser.splitCurrentToken();
if ("focus-distance".equals(tokens[0])) {
GradientUtils.Parser.checkNumberOfArguments(tokens, 1);
distance = GradientUtils.Parser.parsePercentage(tokens[1]);
parser.shift();
}
tokens = parser.splitCurrentToken();
if ("center".equals(tokens[0])) {
GradientUtils.Parser.checkNumberOfArguments(tokens, 2);
centerX = parser.parsePoint(tokens[1]);
centerY = parser.parsePoint(tokens[2]);
parser.shift();
} else {
centerX = GradientUtils.Point.MIN;
centerY = GradientUtils.Point.MIN;
}
tokens = parser.splitCurrentToken();
if ("radius".equals(tokens[0])) {
GradientUtils.Parser.checkNumberOfArguments(tokens, 1);
radius = parser.parsePoint(tokens[1]);
parser.shift();
} else {
throw new IllegalArgumentException("Invalid gradient specification: "
+ "radius must be specified");
}
CycleMethod method = CycleMethod.NO_CYCLE;
String currentToken = parser.getCurrentToken();
if ("repeat".equals(currentToken)) {
method = CycleMethod.REPEAT;
parser.shift();
} else if ("reflect".equals(currentToken)) {
method = CycleMethod.REFLECT;
parser.shift();
}
Stop[] stops = parser.parseStops(radius.proportional, radius.value);
return new RadialGradient(angle, distance, centerX.value, centerY.value,
radius.value, radius.proportional, method, stops);
}
}