/* * Copyright (c) 2012, Gerrit Grunwald * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright notice, this * list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * The names of its contributors may not be used to endorse or promote * products derived from this software without specific prior written * permission. * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF * THE POSSIBILITY OF SUCH DAMAGE. */ package org.mars_sim.msp.ui.steelseries.tools; import java.awt.Color; import java.awt.Paint; import java.awt.PaintContext; import java.awt.Rectangle; import java.awt.RenderingHints; import java.awt.Transparency; import java.awt.geom.AffineTransform; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.awt.image.ColorModel; import java.awt.image.Raster; import java.awt.image.WritableRaster; import java.util.HashMap; import java.util.List; /** * A paint class that creates conical gradients around a given center point * It could be used in the same way as LinearGradientPaint and RadialGradientPaint * and follows the same syntax. * You could use floats from 0.0 to 1.0 for the fractions which is standard but it's * also possible to use angles from 0.0 to 360 degrees which is most of the times * much easier to handle. * Gradients always start at the top with a clockwise direction and you could * rotate the gradient around the center by given offset. * The offset could also be defined from -0.5 to +0.5 or -180 to +180 degrees. * If you would like to use degrees instead of values from 0 to 1 you have to use * the full constructor and set the USE_DEGREES variable to true * @version 1.0 * @author hansolo */ public final class ConicalGradientPaint implements Paint { private final Point2D CENTER; private final float[] FRACTION_ANGLES; private final float[] RED_STEP_LOOKUP; private final float[] GREEN_STEP_LOOKUP; private final float[] BLUE_STEP_LOOKUP; private final float[] ALPHA_STEP_LOOKUP; private final Color[] COLORS; private static final float INT_TO_FLOAT_CONST = 1f / 255f; /** * Standard constructor which takes the FRACTIONS in values from 0.0f to 1.0f * @param CENTER * @param GIVEN_FRACTIONS * @param GIVEN_COLORS * @throws IllegalArgumentException */ public ConicalGradientPaint(final Point2D CENTER, final float[] GIVEN_FRACTIONS, final Color[] GIVEN_COLORS) throws IllegalArgumentException { this(false, CENTER, 0.0f, GIVEN_FRACTIONS, GIVEN_COLORS); } /** * Enhanced constructor which takes the FRACTIONS in degress from 0.0f to 360.0f and * also an GIVEN_OFFSET in degrees around the rotation CENTER * @param USE_DEGREES * @param CENTER * @param GIVEN_OFFSET * @param GIVEN_FRACTIONS * @param GIVEN_COLORS * @throws IllegalArgumentException */ public ConicalGradientPaint(final boolean USE_DEGREES, final Point2D CENTER, final float GIVEN_OFFSET, final float[] GIVEN_FRACTIONS, final Color[] GIVEN_COLORS) throws IllegalArgumentException { // Check that fractions and colors are of the same size if (GIVEN_FRACTIONS.length != GIVEN_COLORS.length) { throw new IllegalArgumentException("Fractions and colors must be equal in size"); } final java.util.List<Float> fractionList = new java.util.ArrayList<Float>(GIVEN_FRACTIONS.length); final float OFFSET; if (USE_DEGREES) { final float DEG_FRACTION = 1f / 360f; if (Float.compare((GIVEN_OFFSET * DEG_FRACTION), -0.5f) == 0) { OFFSET = -0.5f; } else if (Float.compare((GIVEN_OFFSET * DEG_FRACTION), 0.5f) == 0) { OFFSET = 0.5f; } else { OFFSET = (GIVEN_OFFSET * DEG_FRACTION); } for (final float fraction : GIVEN_FRACTIONS) { fractionList.add((fraction * DEG_FRACTION)); } } else { // Now it seems to work with rotation of 0.5f, below is the old code to correct the problem // if (GIVEN_OFFSET == -0.5) // { // // This is needed because of problems in the creation of the Raster // // with a angle offset of exactly -0.5 // OFFSET = -0.49999f; // } // else if (GIVEN_OFFSET == 0.5) // { // // This is needed because of problems in the creation of the Raster // // with a angle offset of exactly +0.5 // OFFSET = 0.499999f; // } // else { OFFSET = GIVEN_OFFSET; } for (final float fraction : GIVEN_FRACTIONS) { fractionList.add(fraction); } } // Check for valid offset if (OFFSET > 0.5f || OFFSET < -0.5f) { throw new IllegalArgumentException("Offset has to be in the range of -0.5 to 0.5"); } // Adjust fractions and colors array in the case where startvalue != 0.0f and/or endvalue != 1.0f final java.util.List<Color> colorList = new java.util.ArrayList<Color>(GIVEN_COLORS.length); colorList.addAll(java.util.Arrays.asList(GIVEN_COLORS)); // Assure that fractions start with 0.0f if (fractionList.get(0) != 0.0f) { fractionList.add(0, 0.0f); final Color TMP_COLOR = colorList.get(0); colorList.add(0, TMP_COLOR); } // Assure that fractions end with 1.0f if (fractionList.get(fractionList.size() - 1) != 1.0f) { fractionList.add(1.0f); colorList.add(GIVEN_COLORS[0]); } // Recalculate the fractions and colors with the given offset final java.util.Map<Float, Color> fractionColors = recalculate(fractionList, colorList, OFFSET); // Clear the original FRACTION_LIST and COLOR_LIST fractionList.clear(); colorList.clear(); // Sort the hashmap by fraction and add the values to the FRACION_LIST and COLOR_LIST final java.util.SortedSet<Float> sortedFractions = new java.util.TreeSet<Float>(fractionColors.keySet()); for (final Float CURRENT_FRACTION : sortedFractions) { fractionList.add(CURRENT_FRACTION); colorList.add(fractionColors.get(CURRENT_FRACTION)); } // Set the values this.CENTER = CENTER; COLORS = colorList.toArray(new Color[colorList.size()]); // Prepare lookup table for the angles of each fraction final int MAX_FRACTIONS = fractionList.size(); this.FRACTION_ANGLES = new float[MAX_FRACTIONS]; for (int i = 0; i < MAX_FRACTIONS; i++) { FRACTION_ANGLES[i] = fractionList.get(i) * 360f; } // Prepare lookup tables for the color stepsize of each color RED_STEP_LOOKUP = new float[COLORS.length]; GREEN_STEP_LOOKUP = new float[COLORS.length]; BLUE_STEP_LOOKUP = new float[COLORS.length]; ALPHA_STEP_LOOKUP = new float[COLORS.length]; for (int i = 0; i < (COLORS.length - 1); i++) { RED_STEP_LOOKUP[i] = ((COLORS[i + 1].getRed() - COLORS[i].getRed()) * INT_TO_FLOAT_CONST) / (FRACTION_ANGLES[i + 1] - FRACTION_ANGLES[i]); GREEN_STEP_LOOKUP[i] = ((COLORS[i + 1].getGreen() - COLORS[i].getGreen()) * INT_TO_FLOAT_CONST) / (FRACTION_ANGLES[i + 1] - FRACTION_ANGLES[i]); BLUE_STEP_LOOKUP[i] = ((COLORS[i + 1].getBlue() - COLORS[i].getBlue()) * INT_TO_FLOAT_CONST) / (FRACTION_ANGLES[i + 1] - FRACTION_ANGLES[i]); ALPHA_STEP_LOOKUP[i] = ((COLORS[i + 1].getAlpha() - COLORS[i].getAlpha()) * INT_TO_FLOAT_CONST) / (FRACTION_ANGLES[i + 1] - FRACTION_ANGLES[i]); } } /** * Recalculates the fractions in the FRACTION_LIST and their associated colors in the COLOR_LIST with a given OFFSET. * Because the conical gradients always starts with 0 at the top and clockwise direction * you could rotate the defined conical gradient from -180 to 180 degrees which equals values from -0.5 to +0.5 * @param fractionList * @param colorList * @param OFFSET * @return Hashmap that contains the recalculated fractions and colors after a given rotation */ private java.util.HashMap<Float, Color> recalculate(final List<Float> fractionList, final List<Color> colorList, final float OFFSET) { // Recalculate the fractions and colors with the given offset final int MAX_FRACTIONS = fractionList.size(); final HashMap<Float, Color> fractionColors = new HashMap<Float, Color>(MAX_FRACTIONS); for (int i = 0; i < MAX_FRACTIONS; i++) { // Add offset to fraction final float TMP_FRACTION = fractionList.get(i) + OFFSET; // Color related to current fraction final Color TMP_COLOR = colorList.get(i); // Check each fraction for limits (0...1) if (TMP_FRACTION <= 0) { fractionColors.put(1.0f + TMP_FRACTION + 0.0001f, TMP_COLOR); final float NEXT_FRACTION; final Color NEXT_COLOR; if (i < MAX_FRACTIONS - 1) { NEXT_FRACTION = fractionList.get(i + 1) + OFFSET; NEXT_COLOR = colorList.get(i + 1); } else { NEXT_FRACTION = 1 - fractionList.get(0) + OFFSET; NEXT_COLOR = colorList.get(0); } if (NEXT_FRACTION > 0) { final Color NEW_FRACTION_COLOR = getColorFromFraction(TMP_COLOR, NEXT_COLOR, (int) ((NEXT_FRACTION - TMP_FRACTION) * 10000), (int) ((-TMP_FRACTION) * 10000)); fractionColors.put(0.0f, NEW_FRACTION_COLOR); fractionColors.put(1.0f, NEW_FRACTION_COLOR); } } else if (TMP_FRACTION >= 1) { fractionColors.put(TMP_FRACTION - 1.0f - 0.0001f, TMP_COLOR); final float PREVIOUS_FRACTION; final Color PREVIOUS_COLOR; if (i > 0) { PREVIOUS_FRACTION = fractionList.get(i - 1) + OFFSET; PREVIOUS_COLOR = colorList.get(i - 1); } else { PREVIOUS_FRACTION = fractionList.get(MAX_FRACTIONS - 1) + OFFSET; PREVIOUS_COLOR = colorList.get(MAX_FRACTIONS - 1); } if (PREVIOUS_FRACTION < 1) { final Color NEW_FRACTION_COLOR = getColorFromFraction(TMP_COLOR, PREVIOUS_COLOR, (int) ((TMP_FRACTION - PREVIOUS_FRACTION) * 10000), (int) (TMP_FRACTION - 1.0f) * 10000); fractionColors.put(1.0f, NEW_FRACTION_COLOR); fractionColors.put(0.0f, NEW_FRACTION_COLOR); } } else { fractionColors.put(TMP_FRACTION, TMP_COLOR); } } // Clear the original FRACTION_LIST and COLOR_LIST fractionList.clear(); colorList.clear(); return fractionColors; } /** * With the START_COLOR at the beginning and the DESTINATION_COLOR at the end of the given RANGE the method will calculate * and return the color that equals the given VALUE. * e.g. a START_COLOR of BLACK (R:0, G:0, B:0, A:255) and a DESTINATION_COLOR of WHITE(R:255, G:255, B:255, A:255) * with a given RANGE of 100 and a given VALUE of 50 will return the color that is exactly in the middle of the * gradient between black and white which is gray(R:128, G:128, B:128, A:255) * So this method is really useful to calculate colors in gradients between two given colors. * @param START_COLOR * @param DESTINATION_COLOR * @param RANGE * @param VALUE * @return Color calculated from a range of values by given value */ public static Color getColorFromFraction(final Color START_COLOR, final Color DESTINATION_COLOR, final int RANGE, final int VALUE) { final float SOURCE_RED = START_COLOR.getRed() * INT_TO_FLOAT_CONST; final float SOURCE_GREEN = START_COLOR.getGreen() * INT_TO_FLOAT_CONST; final float SOURCE_BLUE = START_COLOR.getBlue() * INT_TO_FLOAT_CONST; final float SOURCE_ALPHA = START_COLOR.getAlpha() * INT_TO_FLOAT_CONST; final float DESTINATION_RED = DESTINATION_COLOR.getRed() * INT_TO_FLOAT_CONST; final float DESTINATION_GREEN = DESTINATION_COLOR.getGreen() * INT_TO_FLOAT_CONST; final float DESTINATION_BLUE = DESTINATION_COLOR.getBlue() * INT_TO_FLOAT_CONST; final float DESTINATION_ALPHA = DESTINATION_COLOR.getAlpha() * INT_TO_FLOAT_CONST; final float RED_DELTA = DESTINATION_RED - SOURCE_RED; final float GREEN_DELTA = DESTINATION_GREEN - SOURCE_GREEN; final float BLUE_DELTA = DESTINATION_BLUE - SOURCE_BLUE; final float ALPHA_DELTA = DESTINATION_ALPHA - SOURCE_ALPHA; final float RED_FRACTION = RED_DELTA / RANGE; final float GREEN_FRACTION = GREEN_DELTA / RANGE; final float BLUE_FRACTION = BLUE_DELTA / RANGE; final float ALPHA_FRACTION = ALPHA_DELTA / RANGE; float red = SOURCE_RED + RED_FRACTION * VALUE; float green = SOURCE_GREEN + GREEN_FRACTION * VALUE; float blue = SOURCE_BLUE + BLUE_FRACTION * VALUE; float alpha = SOURCE_ALPHA + ALPHA_FRACTION * VALUE; red = red < 0f ? 0f : (red > 1f ? 1f : red); green = green < 0f ? 0f : (green > 1f ? 1f : green); blue = blue < 0f ? 0f : (blue > 1f ? 1f : blue); alpha = alpha < 0f ? 0f : (alpha > 1f ? 1f : alpha); return new Color(red, green, blue, alpha); } @Override public java.awt.PaintContext createContext(final ColorModel COLOR_MODEL, final Rectangle DEVICE_BOUNDS, final Rectangle2D USER_BOUNDS, final AffineTransform TRANSFORM, final RenderingHints HINTS) { final Point2D TRANSFORMED_CENTER = TRANSFORM.transform(CENTER, null); return new ConicalGradientPaintContext(TRANSFORMED_CENTER); } @Override public int getTransparency() { return Transparency.TRANSLUCENT; } private final class ConicalGradientPaintContext implements PaintContext { final private Point2D CENTER; public ConicalGradientPaintContext(final Point2D CENTER) { this.CENTER = new Point2D.Double(CENTER.getX(), CENTER.getY()); } @Override public void dispose() { } @Override public java.awt.image.ColorModel getColorModel() { return ColorModel.getRGBdefault(); } @Override public Raster getRaster(final int X, final int Y, final int TILE_WIDTH, final int TILE_HEIGHT) { final double ROTATION_CENTER_X = -X + CENTER.getX(); final double ROTATION_CENTER_Y = -Y + CENTER.getY(); final int MAX = FRACTION_ANGLES.length - 1; // Create raster for given colormodel final WritableRaster RASTER = getColorModel().createCompatibleWritableRaster(TILE_WIDTH, TILE_HEIGHT); // Create data array with place for red, green, blue and alpha values final int[] data = new int[(TILE_WIDTH * TILE_HEIGHT * 4)]; double dx; double dy; double distance; double angle; double currentRed = 0; double currentGreen = 0; double currentBlue = 0; double currentAlpha = 0; for (int tileY = 0; tileY < TILE_HEIGHT; tileY++) { for (int tileX = 0; tileX < TILE_WIDTH; tileX++) { // Calculate the distance between the current position and the rotation angle dx = tileX - ROTATION_CENTER_X; dy = tileY - ROTATION_CENTER_Y; distance = Math.sqrt(dx * dx + dy * dy); // Avoid division by zero if (distance == 0) { distance = 1; } // 0 degree on top angle = Math.abs(Math.toDegrees(Math.acos(dx / distance))); if (dx >= 0 && dy <= 0) { angle = 90.0 - angle; } else if (dx >= 0 && dy >= 0) { angle += 90.0; } else if (dx <= 0 && dy >= 0) { angle += 90.0; } else if (dx <= 0 && dy <= 0) { angle = 450.0 - angle; } // Check for each angle in fractionAngles array for (int i = 0; i < MAX; i++) { if ((angle >= FRACTION_ANGLES[i])) { currentRed = COLORS[i].getRed() * INT_TO_FLOAT_CONST + (angle - FRACTION_ANGLES[i]) * RED_STEP_LOOKUP[i]; currentGreen = COLORS[i].getGreen() * INT_TO_FLOAT_CONST + (angle - FRACTION_ANGLES[i]) * GREEN_STEP_LOOKUP[i]; currentBlue = COLORS[i].getBlue() * INT_TO_FLOAT_CONST + (angle - FRACTION_ANGLES[i]) * BLUE_STEP_LOOKUP[i]; currentAlpha = COLORS[i].getAlpha() * INT_TO_FLOAT_CONST + (angle - FRACTION_ANGLES[i]) * ALPHA_STEP_LOOKUP[i]; } } // Fill data array with calculated color values final int BASE = (tileY * TILE_WIDTH + tileX) * 4; data[BASE] = (int) (currentRed * 255); data[BASE + 1] = (int) (currentGreen * 255); data[BASE + 2] = (int) (currentBlue * 255); data[BASE + 3] = (int) (currentAlpha * 255); } } // Fill the raster with the data RASTER.setPixels(0, 0, TILE_WIDTH, TILE_HEIGHT, data); return RASTER; } } }