Wednesday, May 11, 2011

LinearGradientPaint wrapper...

This is just a short post about a little wrapper around the java.awt.LinearGradientPaint class that i created as a helper for the SteelSeries library.
Well my problem was that i would like to create a gradient and after creating it i would like to get a color at a specific fraction. I might be wrong but as far as i know there's no method implemented in the LinearGradientPaint class that could help me here. So i created a little wrapper that wraps the LinearGradientPaint and also contains two additional methods, named getColorAt(float fraction) and getGradient().
The getColorAt(float fraction) method returns the color of the given gradient at the given fraction.
The getGradient() method simply returns the wrapped LinearGradientPaint so that you could set the paint in the Graphics content like: g.setPaint(LGP2.getGradient());


UPDATE:
I implemented the java.awt.Paint interface so that you could use the wrapper like an gradient paint (thx for the comment).


The class contains all constructor methods from the original LinearGradientPaint so that you could create it in the same way as the original.


To give you an idea of what i'm talking about here, a little screenshot might help:




On the screenshot you could see a gradient that i created and i picked the color at the fraction = 0.61.


There might be better solution like this but for me it does the job and if you could use it somewhere...here is the source...
public class LinearGradientPaint2 implements java.awt.Paint
{
    private final java.awt.LinearGradientPaint GRADIENT;
    private float[] fractions;
    private java.awt.Color[] colors;
    
    public LinearGradientPaint2(float startX, float startY, float endX, float endY, float[] fractions, java.awt.Color[] colors)
    {
        GRADIENT = new java.awt.LinearGradientPaint(new java.awt.geom.Point2D.Float(startX, startY), new java.awt.geom.Point2D.Float(endX, endY), fractions, colors, java.awt.MultipleGradientPaint.CycleMethod.NO_CYCLE);
        copyArrays(fractions, colors);
    }
    
    public LinearGradientPaint2(float startX, float startY, float endX, float endY, float[] fractions, java.awt.Color[] colors, java.awt.MultipleGradientPaint.CycleMethod cycleMethod)
    {
        GRADIENT = new java.awt.LinearGradientPaint(new java.awt.geom.Point2D.Float(startX, startY), new java.awt.geom.Point2D.Float(endX, endY), fractions, colors, cycleMethod);
        copyArrays(fractions, colors);
    }

    public LinearGradientPaint2(java.awt.geom.Point2D start, java.awt.geom.Point2D end, float[] fractions, java.awt.Color[] colors)
    {
        GRADIENT = new java.awt.LinearGradientPaint(start, end, fractions, colors, java.awt.MultipleGradientPaint.CycleMethod.NO_CYCLE);
        copyArrays(fractions, colors);
    }
    
    public LinearGradientPaint2(java.awt.geom.Point2D start, java.awt.geom.Point2D end, float[] fractions, java.awt.Color[] colors, java.awt.MultipleGradientPaint.CycleMethod cycleMethod)
    {
        GRADIENT = new java.awt.LinearGradientPaint(start, end, fractions, colors, cycleMethod, java.awt.MultipleGradientPaint.ColorSpaceType.SRGB, new java.awt.geom.AffineTransform());
        copyArrays(fractions, colors);
    }
       
    public LinearGradientPaint2(java.awt.geom.Point2D start, java.awt.geom.Point2D end, float[] fractions, java.awt.Color[] colors, java.awt.MultipleGradientPaint.CycleMethod cycleMethod,  java.awt.MultipleGradientPaint.ColorSpaceType colorSpace, java.awt.geom.AffineTransform gradientTransform)
    {
        GRADIENT = new java.awt.LinearGradientPaint(start, end, fractions, colors, cycleMethod, colorSpace, gradientTransform);
        copyArrays(fractions, colors);
    }
        
    /**
     * Returns the point where the wrapped java.awt.LinearGradientPaint will start
     * @return the point where the wrapped java.awt.LinearGradientPaint will start
     */
    public java.awt.geom.Point2D getStartPoint()
    {
        return GRADIENT.getStartPoint();
    }
    
    /**
     * Returns the point where the wrapped java.awt.LinearGradientPaint will stop
     * @return the point where the wrapped java.awt.LinearGradientPaint will stop
     */
    public java.awt.geom.Point2D getEndPoint()
    {
        return GRADIENT.getEndPoint();
    }
            
    /**
     * Returns the color that is defined by the given fraction in the linear gradient paint
     * @param FRACTION
     * @return the color that is defined by the given fraction in the linear gradient paint
     */
    public java.awt.Color getColorAt(final float FRACTION)
    {
        float fraction = FRACTION > 1 ? 1f : FRACTION;
        fraction = FRACTION < 0 ? 0f : fraction;
        float lowerLimit = 0f;        
        int lowerIndex = 0;
        float upperLimit = 1f;
        int upperIndex = 1;
        int index = 0;
        
        for (float currentFraction : fractions)
        {
            if (Float.compare(currentFraction, fraction) < 0)
            {
                lowerLimit = currentFraction;
                lowerIndex = index;
            }
            if (Float.compare(currentFraction, fraction) == 0)
            {
                return colors[index];
            }
            if (Float.compare(currentFraction, fraction) > 0)
            {
                upperLimit = currentFraction;
                upperIndex = index;
                break;
            }
            index++;
        }
        
        float interpolationFraction = (fraction - lowerLimit) / (upperLimit - lowerLimit);
        
        return interpolateColor(colors[lowerIndex], colors[upperIndex], interpolationFraction);
    }
    
    /**
     * Returns the wrapped java.awt.LinearGradientPaint
     * @return the wrapped java.awt.LinearGradientPaint
     */
    public java.awt.LinearGradientPaint getGradient()
    {
        return GRADIENT;
    }
    
    @Override
    public java.awt.PaintContext createContext(final java.awt.image.ColorModel COLOR_MODEL, final java.awt.Rectangle DEVICE_BOUNDS, java.awt.geom.Rectangle2D USER_BOUNDS, java.awt.geom.AffineTransform X_FORM, java.awt.RenderingHints HINTS)
    {
        return GRADIENT.createContext(COLOR_MODEL, DEVICE_BOUNDS, USER_BOUNDS, X_FORM, HINTS);
    }
 
    @Override
    public int getTransparency()
    {
        return GRADIENT.getTransparency();
    }

    /**
     * Just create a local copy of the fractions and colors array
     * @param fractions
     * @param colors 
     */
    private void copyArrays(final float[] fractions, final java.awt.Color[] colors)
    {
        this.fractions = new float[fractions.length]; 
        System.arraycopy( fractions, 0, this.fractions, 0, fractions.length );
        this.colors = colors.clone();
    }
    
    /**
     * Returns the interpolated color that you get if you multiply the delta between
     * color2 and color1 with the given fraction (for each channel) and interpolation. The fraction should
     * be a value between 0 and 1.
     * @param COLOR1 The first color as integer in the hex format 0xALPHA RED GREEN BLUE, e.g. 0xFF00FF00 for a pure green
     * @param COLOR2 The second color as integer in the hex format 0xALPHA RED GREEN BLUE e.g. 0xFFFF0000 for a pure red
     * @param FRACTION The fraction between those two colors that we would like to get e.g. 0.5f will result in the color 0xFF808000     
     * @return the interpolated color between color1 and color2 calculated by the given fraction and interpolation
     */
    private java.awt.Color interpolateColor(final java.awt.Color COLOR1, final java.awt.Color COLOR2, final float FRACTION)
    {
        assert(Float.compare(FRACTION, 0f) >= 0 && Float.compare(FRACTION, 1f) <= 0);
        
        final float INT_TO_FLOAT_CONST = 1f / 255f;
        
        final float RED1 = COLOR1.getRed() * INT_TO_FLOAT_CONST;
        final float GREEN1 = COLOR1.getGreen() * INT_TO_FLOAT_CONST;
        final float BLUE1 = COLOR1.getBlue() * INT_TO_FLOAT_CONST;
        final float ALPHA1 = COLOR1.getAlpha() * INT_TO_FLOAT_CONST;
        
        final float RED2 = COLOR2.getRed() * INT_TO_FLOAT_CONST;
        final float GREEN2 = COLOR2.getGreen() * INT_TO_FLOAT_CONST;
        final float BLUE2 = COLOR2.getBlue() * INT_TO_FLOAT_CONST;
        final float ALPHA2 = COLOR2.getAlpha() * INT_TO_FLOAT_CONST;
        
        final float DELTA_RED = RED2 - RED1;
        final float DELTA_GREEN = GREEN2 - GREEN1;
        final float DELTA_BLUE = BLUE2 - BLUE1;
        final float DELTA_ALPHA = ALPHA2 - ALPHA1;
        
        float red = RED1 + (DELTA_RED * FRACTION);
        float green = GREEN1 + (DELTA_GREEN * FRACTION);
        float blue = BLUE1 + (DELTA_BLUE * FRACTION);
        float alpha = ALPHA1 + (DELTA_ALPHA * FRACTION);
        
        red = red < 0f ? 0f : red;
        red = red > 1f ? 1f : red;
        green = green < 0f ? 0f : green;
        green = green > 1f ? 1f : green;
        blue = blue < 0f ? 0f : blue;
        blue = blue > 1f ? 1f : blue;
        alpha = alpha < 0f ? 0f : alpha;
        alpha = alpha > 1f ? 1f : alpha;
        
        return new java.awt.Color(red, green, blue, alpha);        
    }
}
     
To give you an idea on where i will use this helper class, let me explain an example.
If i have a linear gauge and the color of the column (which represents the current value of the gauge) should fade through different colors from the minimum to the maximum value it would be nice if i could create the underlying linear gradient once and pick the current color of the column dependent on the current value of the linear gauge.


That's all for today (to be honest it was more a little reminder that i have to implement the feature i described above to the linear gauge).


So keep coding...



5 comments:

  1. Hi there,
    In principle i do agree to what you wrote as you can see in my other gradients. In this case i did not really need another gradient but only a helper class that uses a LinearGradientPaint in the background to store the gradient parameters. You are right in saying that if the API changes my helper class won't work anylonger but this would also affect every usage of LinearGradientPaint right ?
    I just would like to use the LinearGradientPaint as a storage for the gradient parameters, the gradient itself will never be used, only the color from getColorAt() will be used in my code. Thanx for the critics, it's good to know that people really taking a look at the code...
    Cheers, Gerrit

    ReplyDelete
  2. If you create a gradient paint, your gradient should implement the interface Paint...
    And my second remark is: if the implementation of the LinearGradientPaint itself would change (which is, of course, very unlikely), your LinearGradientPaint2 would be broken, because you calculate the color in getColorAt() by yourself, but the gradient for drawing comes from LinearGradientPaint. Therefore for me it seems to be better if you implement Paint itself with all its functionality.

    ReplyDelete
  3. Well in principle i agree to what you wrote (i added the implementation of java.awt.Paint) but the reason why i created this helper class was that i would like to use the LinearGradientPaint object to store the gradient fractions and colors. I needed this to be able to get the color on every given fraction of the gradient to realize for example a continuous colorchange in the linear gauge.
    Thanx for the critics (it's good to know that some people really reading the code ;-) ).
    Cheers,
    Gerrit

    ReplyDelete
  4. Why

    red = red < 0f ? 0f : red;

    instead of

    if ( red < 0f ) red = 0f;

    If the range of the valus is correct you can save the cost of assiging the same value.

    ReplyDelete
  5. Hi Anonymous,
    Yep you're right...

    ReplyDelete