NoiseConf.java
package swingtree.style;
import com.google.errorprone.annotations.Immutable;
import org.jspecify.annotations.Nullable;
import org.slf4j.Logger;
import swingtree.UI;
import swingtree.api.NoiseFunction;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import java.awt.Color;
import java.util.Arrays;
import java.util.Objects;
/**
* A noise gradient configuration which is used to define a noise gradient style
* for a component based on a {@link NoiseFunction} which is a function
* that takes a coordinate and returns a value between 0 and 1. <br>
* The noise gradient is then defined by a list of colors which will transition from one
* to the next in the order they are specified. <br>
* The noise gradient can also be offset, scaled, rotated and clipped to a specific area of the component,
* and positioned at a specific boundary of the component.
*/
@Immutable
@SuppressWarnings("Immutable")
public final class NoiseConf implements Simplifiable<NoiseConf>
{
private static final Logger log = org.slf4j.LoggerFactory.getLogger(NoiseConf.class);
static final UI.Layer DEFAULT_LAYER = UI.Layer.BACKGROUND;
private static final NoiseConf _NONE = new NoiseConf(
UI.NoiseType.STOCHASTIC,
new Color[0],
Offset.none(),
Scale.none(),
UI.ComponentArea.BODY,
UI.ComponentBoundary.EXTERIOR_TO_BORDER,
0f,
new float[0]
);
/**
* Use the returned instance as a representation of the absence of a noise gradient.
*
* @return A noise gradient without any colors, effectively
* representing the absence of a noise gradient.
*/
public static NoiseConf none() { return _NONE; }
static NoiseConf of(
NoiseFunction function,
Color[] colors,
Offset offset,
Scale scale,
UI.ComponentArea area,
UI.ComponentBoundary boundary,
float rotation,
float[] fractions
) {
// The rotation may be any number
// which always has to be normalized to a value between -180 and 180
rotation = ( (((rotation+180f) % 360f + 360f) % 360f) - 180f );
NoiseConf none = none();
if ( function .equals( none._function ) &&
Arrays.equals(colors, none._colors) &&
offset .equals( none._offset ) &&
scale .equals( none._scale ) &&
area .equals( none._area ) &&
boundary .equals( none._boundary ) &&
rotation == none._rotation &&
Arrays.equals(fractions, none._fractions)
)
return none;
return new NoiseConf(
function,
colors,
offset,
scale,
area,
boundary,
rotation,
fractions
);
}
private final NoiseFunction _function;
private final Color[] _colors;
private final Offset _offset;
private final Scale _scale;
private final UI.ComponentArea _area;
private final UI.ComponentBoundary _boundary;
private final float _rotation;
private final float[] _fractions;
private NoiseConf(
NoiseFunction function,
Color[] colors,
Offset offset,
Scale scale,
UI.ComponentArea area,
UI.ComponentBoundary boundary,
float rotation,
float[] fractions
) {
_function = Objects.requireNonNull(function);
_colors = Objects.requireNonNull(colors);
_offset = Objects.requireNonNull(offset);
_scale = Objects.requireNonNull(scale);
_area = Objects.requireNonNull(area);
_boundary = Objects.requireNonNull(boundary);
_rotation = rotation;
_fractions = Objects.requireNonNull(fractions);
}
NoiseFunction function() { return _function; }
Color[] colors() { return _colors; }
Offset offset() { return _offset; }
Scale scale() { return _scale; }
UI.ComponentArea area() { return _area; }
UI.ComponentBoundary boundary() { return _boundary; }
float rotation() { return _rotation; }
float[] fractions() { return _fractions; }
boolean isOpaque() {
if ( _colors.length == 0 )
return false;
boolean foundTransparentColor = false;
for ( Color c : _colors ) {
if ( c.getAlpha() < 255 ) {
foundTransparentColor = true;
break;
}
}
return !foundTransparentColor;
}
NoiseConf _scale( double scale ) {
if ( scale == 1 )
return this;
if ( this.equals(_NONE) )
return _NONE;
return of(
_function,
_colors,
_offset,
_scale.scale(scale),
_area,
_boundary,
_rotation,
_fractions
);
}
/**
* Accepts the {@link NoiseFunction}, which takes a coordinate and returns a value
* between 0 and 1. <br>
* The noise function is used to define the noise gradient.
* <p>
* <b>Take a look at {@link UI.NoiseType} for a rich set of predefined noise functions.</b>
*
* @param function The noise function mapping the translated, scaled and rotated virtual space
* to a gradient value of a pixel in the color space / view space of the screen.
* @return A new noise gradient style with the specified noise function.
*/
public NoiseConf function( NoiseFunction function ) {
return of(function, _colors, _offset, _scale, _area, _boundary, _rotation, _fractions);
}
/**
* Define a list of colors which will, as part of the noise gradient, transition from one
* to the next in the order they are specified.
* <p>
* Note that you need to specify at least two colors for a noise gradient to be visible.
*
* @param colors The colors in the noise gradient.
* @return A new noise gradient style with the specified colors.
* @throws NullPointerException if any of the colors is {@code null}.
*/
public NoiseConf colors( Color... colors ) {
Objects.requireNonNull(colors);
for ( Color color : colors )
Objects.requireNonNull(color, "Use UI.Color.UNDEFINED instead of null to represent the absence of a color.");
return of(_function, colors, _offset, _scale, _area, _boundary, _rotation, _fractions);
}
/**
* Define a list of {@link String} based colors which will, as part of the noise gradient, transition from one
* to the next in the order they are specified.
* <p>
* Note that you need to specify at least two colors for a noise gradient to be visible.
*
* @param colors The colors in the noise gradient in {@link String} format.
* @return A new noise gradient style with the specified colors.
* @throws NullPointerException if any of the colors is {@code null}.
*/
public NoiseConf colors( String... colors ) {
Objects.requireNonNull(colors);
try {
Color[] actualColors = new Color[colors.length];
for ( int i = 0; i < colors.length; i++ )
actualColors[i] = UI.color(colors[i]);
return of(_function, actualColors, _offset, _scale, _area, _boundary, _rotation, _fractions);
} catch ( Exception e ) {
log.error("Failed to parse color strings: " + Arrays.toString(colors), e);
return this; // We want to avoid side effects other than a wrong color
}
}
/**
* Define the offset of the noise gradient which is the start position of the noise gradient
* on the x and y-axis. <br>
* Note that the offset is relative to the component that the noise gradient is applied to.
* <p>
* @param x The noise gradient start offset on the x-axis.
* @param y The noise gradient start offset on the y-axis.
* @return A new noise gradient style with the specified offset.
*/
public NoiseConf offset(double x, double y ) {
return of(_function, _colors, Offset.of(x,y), _scale, _area, _boundary, _rotation, _fractions);
}
/**
* Define the scale of the noise gradient in terms of its size / granularity.
* It scales the input space of the noise function.
*
* @param scale The noise gradient size.
* @return A new noise gradient style with the specified size.
*/
public NoiseConf scale( double scale ) {
return of(_function, _colors, _offset, Scale.of(scale, scale), _area, _boundary, _rotation, _fractions);
}
/**
* Define the x and y scale of the noise gradient in terms of its size / granularity.
* It scales the input space of the noise function.
*
* @param x The noise gradient size on the x-axis.
* @param y The noise gradient size on the y-axis.
* @return A new noise gradient style with the specified size.
*/
public NoiseConf scale( double x, double y ) {
return of(_function, _colors, _offset, Scale.of(x,y), _area, _boundary, _rotation, _fractions);
}
/**
* Define the area of the component to which the noise gradient is clipped to.
* Which means that the noise gradient will only be visible within the
* specified area of the component.
*
* @param area The area of the component to which the noise gradient is clipped to.
* @return A new noise gradient style with the specified area.
*/
public NoiseConf clipTo( UI.ComponentArea area ) {
return of(_function, _colors, _offset, _scale, area, _boundary, _rotation, _fractions);
}
/**
* Define the boundary at which the noise gradient should start in terms of its base position.
* So if the boundary is set to {@link UI.ComponentBoundary#EXTERIOR_TO_BORDER}
* then the noise gradient position will be determined by the margin of the component. <br>
* Here a complete list of the available boundaries:
* <ul>
* <li>{@link UI.ComponentBoundary#OUTER_TO_EXTERIOR} -
* The outermost boundary of the entire component, including any margin that might be applied.
* Using this boundary will cause the noise gradient to be positioned somewhere at
* the outer most edge of the component.
* </li>
* <li>{@link UI.ComponentBoundary#EXTERIOR_TO_BORDER} -
* The boundary located after the margin but before the border.
* This tightly wraps the entire {@link UI.ComponentArea#BODY}.
* Using this boundary will cause the noise gradient to be positioned somewhere at
* the outer most edge of the component's body, which is between the margin and the border.
* </li>
* <li>{@link UI.ComponentBoundary#BORDER_TO_INTERIOR} -
* The boundary located after the border but before the padding.
* It represents the edge of the component's interior.
* Using this boundary will cause the noise gradient to be positioned somewhere at
* the outer most edge of the component's interior, which is between the border and the padding area.
* </li>
* <li>{@link UI.ComponentBoundary#INTERIOR_TO_CONTENT} -
* The boundary located after the padding.
* It represents the innermost boundary of the component, where the actual content of the component begins,
* like for example the contents of a {@link JPanel} or {@link JScrollPane}.
* Using this boundary will cause the noise gradient to be positioned somewhere after the padding area
* and before the content area, which is where all of the child components are located.
* </li>
* </ul>
* <p>
* You can think of this property as a convenient way to define the base position of the noise gradient.
* So if you want to do the positioning yourself, then you may configure this property to
* {@link UI.ComponentBoundary#OUTER_TO_EXTERIOR}, which will cause the noise gradient to be positioned
* at the outermost edge of the component, and then use the {@link #offset(double, double)} method
* to define the exact position of the noise gradient.
*
* @param boundary The boundary at which the noise gradient should start in terms of its offset.
* @return A new noise gradient style with the specified boundary.
*/
public NoiseConf boundary( UI.ComponentBoundary boundary ) {
return of(_function, _colors, _offset, _scale, _area, boundary, _rotation, _fractions);
}
/**
* Define the rotation of the noise gradient in degrees.
* This will rotate the input space of the noise function.
*
* @param rotation The rotation of the noise gradient in degrees.
*/
public NoiseConf rotation( float rotation ) {
return of(_function, _colors, _offset, _scale, _area, _boundary, rotation, _fractions);
}
/**
* Define the fractions of the noise gradient which is an array of values between 0 and 1
* that defines the relative position of each color in the noise gradient.
* <p>
* Note that the number of fractions must match the number of colors in the noise gradient.
* If the number of fractions is less than the number of colors, then the remaining
* colors will be evenly distributed between the last two fractions.
*
* @param fractions The fractions of the noise gradient.
*/
public NoiseConf fractions( double... fractions ) {
float[] actualFractions = new float[fractions.length];
for ( int i = 0; i < fractions.length; i++ )
actualFractions[i] = (float) fractions[i];
return of(_function, _colors, _offset, _scale, _area, _boundary, _rotation, actualFractions);
}
@Override
public String toString() {
if ( this.equals(_NONE) )
return getClass().getSimpleName() + "[NONE]";
return getClass().getSimpleName() + "[" +
"function=" + _function + ", " +
"colors=" + Arrays.toString(_colors) + ", " +
"offset=" + _offset + ", " +
"scale=" + _scale + ", " +
"area=" + _area + ", " +
"boundary=" + _boundary + ", " +
"rotation=" + _rotation + ", " +
"fractions=" + Arrays.toString(_fractions) +
"]";
}
@Override
public boolean equals( @Nullable Object o ) {
if ( this == o ) return true;
if ( !(o instanceof NoiseConf) ) return false;
NoiseConf that = (NoiseConf) o;
return Objects.equals(_function, that._function) &&
Arrays.equals(_colors, that._colors) &&
Objects.equals(_offset, that._offset) &&
Objects.equals(_scale, that._scale) &&
_area == that._area &&
_boundary == that._boundary &&
_rotation == that._rotation &&
Arrays.equals(_fractions, that._fractions);
}
@Override
public int hashCode() {
return Objects.hash(
_function,
Arrays.hashCode(_colors),
_offset,
_scale,
_area,
_boundary,
_rotation,
Arrays.hashCode(_fractions)
);
}
@Override
public NoiseConf simplified() {
if ( this.equals(_NONE) )
return _NONE;
if ( _colors.length == 0 )
return _NONE;
if ( Arrays.stream(_colors).allMatch( color -> color.getAlpha() == 0 || StyleUtil.isUndefinedColor(color) ) )
return _NONE;
int numberOfRealColors = Arrays.stream(_colors).mapToInt( color -> StyleUtil.isUndefinedColor(color) ? 0 : 1 ).sum();
if ( numberOfRealColors == 0 )
return _NONE;
if ( numberOfRealColors != _colors.length ) {
Color[] realColors = new Color[numberOfRealColors];
int index = 0;
for ( Color color : _colors )
if ( !StyleUtil.isUndefinedColor(color) )
realColors[index++] = color;
return of( _function, realColors, _offset, _scale, _area, _boundary, _rotation, _fractions );
}
return this;
}
}