ORIGINAL DRAFT

Some visual components are more fun than others, teaching us valuable techniques while remaining interesting and, in some ways, even entertaining. JGradient is precisely that kind of ‘colorful’ widget. It gives us a chance to dig under the 2D Graphics API and implement our own Paint class, which allows us to fill arbitrary shapes with a CompoundGradient. The component provides a visual editing environment for complex gradient patterns, an interface for moving, adding and deleting gradient boundaries, selecting colors and transparency for each, providing an easy way for end users to work with gradient patterns.

The architecture for this component includes a number of independent elements which work together to provide the desired functionality. We’ll list the more important classes and talk briefly about the others, though space limitations prevent us from publishing all the classes in the magazine. You can find them online at www.java-pro.com. Figure 1 shows the relationship between all the JGradient classes. There is only one interface, GradientModel, which is implemented by the CompoundGradient class.

Figure 1: JGradient Class Relationships.

Figure 1: JGradient Class Relationships.

Two classes inherit from SelectionPanel, which simply provides some basic ChangeListener functionality common to both ColorSpectrum and TransparencyGradient. These two classes allow the user to select color and transparency for a given position along the gradient. ColorSpectrum provides a color spectrum view that lets you select an RGB value, while the TransparencySpectrum class shows a white-to-black view that lets you select a value between 0 and 255, indicating a transparency level. In each case, we provide mouse click/drag functionality and allow keyboard movement when the object has the focus. You can see these two classes at work in Figure 2.

Figure 2: JGradient Screen Shot.

Figure 2: JGradient Screen Shot.

The main focus of our component is constructing and editing compound gradients. A gradient is an interpolation between two colors. Interpolation is a fancy word for a (usually linear) change between two values. To change colors in a linear, incremental series of steps, we merely need to take the distance between the pixels and calculate the appropriate red, green, blue and alpha color components for each step. To move between 100 and 200 in 50 steps, for example, we would start at 100 and increase the value by two at each pixel. The Java 2D API does all this for you under the hood if you use the GradientPaint class.

We’re interested in developing compound gradients, which involve several colors and respective interpolations. Furthermore, we want to take advantage of the 2D API’s ability to fill any 2D Shape, at any angle or transformation, with our compound gradient, as easily as using the GradientPaint class. To do this, we’ll develop our own Paint-compatible class and a basic model that lets us handle compound gradients effectively. The JGradient widget will gives us a visible window on these interpolations, allowing us to edit sophisticated gradient patterns.

Modeling Compound Gradients

If you look at the Java 2 GradientPaint class, you’ll see that you can specify a gradient with two colors to be interpolated, and two points to draw between. The GradientPaint class also lets you determine whether drawing will be cyclic, repeating the pattern in alternating directions or not, filling from the end points out with the color at each respective point. The GradientPaint class uses a GradientPaintContext to draw the gradient across 256 pixels and then maps this onto the Shape object by applying appropriate transformations. We want to provide exactly the same functionality in our compound gradients.

To keep things simple, we’ll assume that a compound gradient is defined within a 256 pixel area. We’ll need to store a list of Colors with a relative position within a 0 to 255 range. Listing 1 shows the GradientAnchor class. An anchor is a point along our 256 pixel continuum that represents a given color. We provide some accessors to allow values to be set and retrieved, along with an implementation of the Comparable interface, from the Java Collections API, so that we can sort a list of these elements. This is especially important for managing the painting order when new elements are inserted.

Listing 2 shows the GradientModel interface. A compound gradient is effectively a list of GradientAnchor elements. We provide accessors for setting and retrieving anchors and values inside these elements without needing to understand the underlying implementation. The GradientModel also manages selection, which might have been handled with a selection model, but that would have made our implementation overly complex. Colors and offsets can be set or retrieved based on a current selection, allowing us to edit the GradientModel dynamically. We also implement the ability to add and remove ChangeListeners to that views can stay synchronized as values are changed.

The CompoundGradient class in Listing 3 implements the GradientModel interface. The internal list storage is handled by a Vector class and most of the methods are fairly straight forward. We fire change events when certain values are set, including anchor color and position, selection, deletions and sorting. The only time we need to sort is when a relative position changes. Insertion automatically places a new GradientAnchor object into the right relative position. Deleting doesn’t require sorting, though we do have to be sensitive to the current selection if the last element is removed.

Handling Paint Brushes

The Java 2D API uses a Paint interface to define how color patterns can be painted in Graphics2D operations. The Paint interface extends Transparency, so two methods need to be implemented. The Transparency interface method getTransparency returns a value indicating whether the drawing will be OPAQUE (with no transparency), TRANSLUCENT (partly or completely transparent) or a BITMASK (completely transparent or completely opaque). The Paint interface method createContext returns a class that implements the PaintContext interface and expects to be told about the ColorModel, device and user bounding rectangles, an AffineTransform to be applied and a set of RenderingHints.

Listing 4 shows the CompoundGradientPaint class, which implements the Paint interface. The constructors let us specify the GradientModel, start and end points to draw between and an optional cyclic argument. The getTransparency implementation checks the alpha value for each of the GradientAnchor elements, setting the response to TRANSLUCENT if any of them have an alpha setting less that 255. The createContext method ignores the color model, bounding regions and rendering hints, using only the transform to determine where the points should be placed. We create a CompoundGradientPaintContext object with the model, transformed end points and the cyclic flag, fulfilling our Paint interface contract.

The CompoundGradientPaintContext, in Listing 5, class is more complicated and largely pilfered from the GradientPaintContext source code. Unfortunately, Sun’s engineers decided to make it impossible to extend the GradientPaint or GradientPaintContext classes, so copying the original source was the only way to extend the behavior appropriately. The getRaster and supporting cycleFillRaster and clipFillRaster methods are unchanged. Our extended implementation changes the constructor and introduces a supporting method called interpolator.

The constructor makes sure the points are handled in the proper order, determines the distance and line length between them and creates a 256 (non-cyclic) or 512 (cyclic) element integer (color) array to represent the interpolation colors. We construct these by calling the interpolate method for each GradientAnchor pair in the GradientModel. The resulting array covers 256 or 512 color positions in the compound gradient, which is applied by the getRaster method. If we are handling the cyclic variant, all we need to do is place the same values from the first 256 positions into the upper 256 positions in reverse order.

Putting it Together

Figure 3 shows the layout arrangement for the JGradient component. Both the GradientPreview and the GradientDisplay panels use the CompoundGradientPaint class to draw the GradientModel. The GradientBar provides control over the GradientAnchor positions in the GradientModel. The ColorSpectrum and TransparencyGradient were mentioned earlier and won’t be covered in detail. The source code is fairly self-explanatory and available online at www.java-pro.com.

Figure 3: JGradient Layout.

Figure 3: JGradient Layout.

Listing 6 shows the GradientPreview class. Drawing the GradientModel is a simple matter of getting a Graphics2D context (by casting the Graphics object, which actually subclasses Graphics2D), creating a CompoundGradientPaint instance and using it to fill the rectangular drawing area of the component. The GradientPreview class assumes a fixed (preferred) width of 256 pixels, plus the border edges. The GradientDisplay class does something similar, but it draws the gradient at an angle and shows the bounding rectangle where the compound gradient is painted (along the diagonal) so you can see where the cycling takes effect.

The last class we need to look at is the JGradient class in Listing 7. This class ties all the elements together and provides an interface for use in your applications. The constructor expects a GradientModel and a boolean flag indicating whether you want to see the DisplayPanel on the right. Figure 4 shows the JGradient control at work with the preview flag turned on. On slower machines, all this work may be a detriment and it’ll clearly be more effective to leave this flag off. Conversely, on a fast machine, the ability to see a cyclic display in action enhances the user’s ability to predict results.

Figure 4: JGradient with the DisplayPanel visible.

Figure 4: JGradient with the DisplayPanel visible.

The JGradient class sets up all the nested panels in the constructor. We expose setModel and getModel methods to provide access to the GradientModel. The only other relevant method is the stageChanged handler, which watches for model changes and repaints updates to the gradient, spectrum and transparency panels. The JGradientTest class (available online) provides a mechanism for trying out the JGradient panel in a JFrame.

It’s worth noting that my original impulse was to use the JColorChooser to handle color selection, or an earlier JColor widget I’d written, but JColorChooser fell short when it came to handling transparency and the JColor widget seemed overly complex for the job. The ColorSpectrum class provides a good two dimensional view of available colors that should suit most needs.

With the JGradient widget, you can provide control over complex gradient patterns in your application. This will, of course, have greater appeal to developers working on imaging applications, but can work well with other customization features you may want to provide for your users. Have fun with it.

Listing 1

import java.awt.*;

public class GradientAnchor implements Comparable
{
  protected Color color;
  protected int offset;

  public GradientAnchor() {}

  public GradientAnchor(Color color, int offset)
  {
    this.color = color;
    this.offset = offset;
  }
  
  public void setColor(Color color)
  {
    this.color = color;
  }
  
  public Color getColor()
  {
    return color;
  }
  
  public int getOffset()
  {
    return offset;
  }

  public void setOffset(int offset)
  {
    this.offset = offset;
  }

  public int compareTo(Object obj)
  {
    if (obj instanceof GradientAnchor)
    {
      GradientAnchor other = (GradientAnchor)obj;
      if (offset < other.offset) return -1;
      if (offset > other.offset) return 1;
    }
    return 0;
  }
  
  public boolean equals(Object obj)
  {
    return compareTo(obj) == 0;
  }

  public String toString()
  {
    return "GradientAnchor(" +
      color.toString() + "," + offset + ")";
  }
}

Listing 2

import java.awt.Color;
import javax.swing.event.*;

public interface GradientModel
{
  public int getSize();
  public void addAnchor(GradientAnchor anchor);
  public GradientAnchor getAnchor(int index);
  public Color getColor(int index);
  public void setColor(int index, Color color);
  public int getOffset(int index);
  public void setOffset(int index, int offset);
  public int getSelected();
  public void setSelected(int selected);
  public void deleteSelected();
  public void insert(GradientAnchor anchor);
  public void addChangeListener(ChangeListener listener);
  public void removeChangeListener(ChangeListener listener);
  public void fireChangeEvent();
}

Listing 3

import java.util.*;
import java.awt.Color;
import javax.swing.event.*;

public class CompoundGradient
  implements GradientModel
{
  protected Vector listeners = new Vector();
  protected List anchorList;
  protected int selected;

  public CompoundGradient()
  {
    anchorList = new Vector();
  }

  public int getSize()
  {
    return anchorList.size();
  }
  
  public void addAnchor(GradientAnchor anchor)
  {
    anchorList.add(anchor);
    setSelected(anchorList.size() - 1);
  }

  public GradientAnchor getAnchor(int index)
  {
    return (GradientAnchor)anchorList.get(index);
  }

  public Color getColor(int index)
  {
    return getAnchor(index).getColor();
  }
  
  public void setColor(int index, Color color)
  {
    getAnchor(index).setColor(color);
    fireChangeEvent();
  }
  
  public int getOffset(int index)
  {
    return getAnchor(index).getOffset();
  }
  
  public void setOffset(int index, int offset)
  {
    getAnchor(index).setOffset(offset);
    sort();
  }
  
  public int getSelected()
  {
    return selected;
  }
  
  public void setSelected(int selected)
  {
    this.selected = selected;
    fireChangeEvent();
  }

  public void deleteSelected()
  {
    anchorList.remove(selected);
    if (selected == anchorList.size())
      setSelected(selected--);
    else fireChangeEvent();
  }

  public void insert(GradientAnchor anchor)
  {
    for (int i = 0; i < anchorList.size(); i++)
    {
      if (getAnchor(i).getOffset() > anchor.getOffset())
      {
        anchorList.add(i, anchor);
        setSelected(i);
        return;
      }
    }
  }

  public void addChangeListener(ChangeListener listener)
  {
    listeners.addElement(listener);
  }
  
  public void removeChangeListener(ChangeListener listener)
  {
    listeners.removeElement(listener);
  }
  
  public void fireChangeEvent()
  {
    Vector list = (Vector)listeners.clone();
    ChangeEvent event = new ChangeEvent(this);
    ChangeListener listener;
    for (int i = 0; i < list.size(); i++)
    {
      listener = (ChangeListener)list.elementAt(i);
      listener.stateChanged(event);
    }
  }

  private void sort()
  {
    GradientAnchor selection = getAnchor(selected);
    Collections.sort(anchorList);
    for (int i = 0; i < anchorList.size(); i++)
    {
      if (anchorList.get(i) == selection)
      {
        selected = i;
        break;
      }
    }
    fireChangeEvent();
  }

  public String toString()
  {
    StringBuffer buffer = new StringBuffer("[");
    for (int i = 0; i < anchorList.size(); i++)
    {
      if (i > 0) buffer.append(",");
      buffer.append(getAnchor(i).toString());
    }
    buffer.append("]");
    return buffer.toString();
  }
}

Listing 4

import java.awt.*;
import java.awt.geom.*;
import java.awt.image.*;

public class CompoundGradientPaint implements Paint
{
  protected boolean cyclic;
  protected GradientModel model;
  protected Point2D source, target;

  public CompoundGradientPaint(GradientModel model,
    Point2D source, Point2D target)
  {
    this(model, source, target, false);
  }

  public CompoundGradientPaint(GradientModel model,
    Point2D source, Point2D target, boolean cyclic)
  {
    this.model = model;
    this.source = source;
    this.target = target;
    this.cyclic = cyclic;
  }

  public int getTransparency()
  {
    int total = 0;
    for (int i = 0; i < model.getSize(); i++)
    {
      total &= model.getColor(i).getAlpha();
    }
    return total == 0xff ? OPAQUE : TRANSLUCENT;
  }
  
  public PaintContext createContext(ColorModel cm,
    Rectangle deviceBounds, Rectangle2D userBounds,
    AffineTransform transform, RenderingHints hints)
  {
    return new CompoundGradientPaintContext(model,
      transform.transform(source, null),
      transform.transform(target, null), cyclic);
  }
}

Listing 5

import java.awt.*;
import java.awt.geom.*;
import java.awt.color.*;
import java.awt.image.*;
import sun.awt.image.IntegerComponentRaster;

public class CompoundGradientPaintContext
  implements PaintContext
{
  protected double x1, y1, dx, dy;
  protected boolean cyclic;
  protected int interpolation[];
  protected Raster saved;
  protected ColorModel colorModel;

  public CompoundGradientPaintContext(
    GradientModel model, Point2D p1, Point2D p2, boolean cyclic)
  {
    double y1, y2;
    double x1 = p1.getX();
    double x2 = p2.getX();
    if (x1 > x2)
    {
      y1 = x1;
      x1 = x2;
      x2 = y1;
      y1 = p2.getY();
      y2 = p1.getY();
    }
    else
    {
      y1 = p1.getY();
      y2 = p2.getY();
    }
    double dx = x2 - x1;
    double dy = y2 - y1;
    double lenSq = dx * dx + dy * dy;
    this.x1 = x1;
    this.y1 = y1;
    if (lenSq >= Double.MIN_VALUE)
    {
      dx = dx / lenSq;
      dy = dy / lenSq;
      if (cyclic)
      {
        dx = dx % 1.0;
        dy = dy % 1.0;
      }
    }
    this.dx = dx;
    this.dy = dy;
    this.cyclic = cyclic;
    colorModel = ColorModel.getRGBdefault();
    GradientAnchor source, target;
    interpolation = new int[cyclic ? 513 : 257];
    for (int i = 0; i < model.getSize() - 1; i++)
    {
      source = model.getAnchor(i);
      target = model.getAnchor(i + 1);
      interpolate(source, target);
    }
  }
  
  protected void interpolate(GradientAnchor source, GradientAnchor target)
  {
    int from = source.getOffset();
    int to = target.getOffset();
    int units = to - from;
    int rgb1 = source.getColor().getRGB();
    int rgb2 = target.getColor().getRGB();
    int a1 = (rgb1 >> 24) & 0xff;
    int r1 = (rgb1 >> 16) & 0xff;
    int g1 = (rgb1 >> 8) & 0xff;
    int b1 = (rgb1) & 0xff;
    int da = ((rgb2 >> 24) & 0xff) - a1;
    int dr = ((rgb2 >> 16) & 0xff) - r1;
    int dg = ((rgb2 >> 8) & 0xff) - g1;
    int db = ((rgb2) & 0xff) - b1;
    for (int i = 0; i <= units; i++)
    {
      float rel = i / (float)units;
      int rgb =
        (((int) (a1 + da * rel)) << 24) |
        (((int) (r1 + dr * rel)) << 16) |
        (((int) (g1 + dg * rel)) <<  8) |
        (((int) (b1 + db * rel)));
      interpolation[from + i] = rgb;
      if (cyclic)
        interpolation[512 - (from + i)] = rgb;
    }
  }

  public void dispose()
  {
    saved = null;
  }

  public ColorModel getColorModel()
  {
    return colorModel;
  }

  public Raster getRaster(int x, int y, int w, int h)
  {
    double rowrel = (x - x1) * dx + (y - y1) * dy;
    Raster raster = saved;
    if (raster == null || raster.getWidth() < w || raster.getHeight() < h)
    {
      raster = getColorModel().createCompatibleWritableRaster(w, h);
      saved = raster;
    }
    IntegerComponentRaster irast = (IntegerComponentRaster)raster;
    int off = irast.getDataOffset(0);
    int adjust = irast.getScanlineStride() - w;
    int[] pixels = irast.getDataStorage();
    if (cyclic)
    {
      cycleFillRaster(pixels, off, adjust, w, h, rowrel, dx, dy);
    }
    else
    {
      clipFillRaster(pixels, off, adjust, w, h, rowrel, dx, dy);
    }
    return raster;
  }

  public void cycleFillRaster(int[] pixels, int off, int adjust,
    int w, int h, double rowrel, double dx, double dy)
  {
    rowrel = rowrel % 2.0;
    int irowrel = ((int) (rowrel * (1 << 30))) << 1;
    int idx = (int) (-dx * (1 << 31));
    int idy = (int) (-dy * (1 << 31));
    while (--h >= 0)
    {
      int icolrel = irowrel;
      for (int j = w; j > 0; j--)
      {
        pixels[off++] = interpolation[icolrel >>> 23];
        icolrel += idx;
      }
      off += adjust;
      irowrel += idy;
    }
  }

  public void clipFillRaster(int[] pixels, int off, int adjust,
    int w, int h, double rowrel, double dx, double dy)
  {
    while (--h >= 0)
    {
      double colrel = rowrel;
      int j = w;
      if (colrel <= 0.0)
      {
        int rgb = interpolation[0];
        do
        {
          pixels[off++] = rgb;
          colrel += dx;
        }
        while (--j > 0 && colrel <= 0.0);
      }
      while (colrel < 1.0 && --j >= 0)
      {
        pixels[off++] = interpolation[(int) (colrel * 256)];
        colrel += dx;
      }
      if (j > 0)
      {
        int rgb = interpolation[255];
        do
        {
          pixels[off++] = rgb;
        }
        while (--j > 0);
      }
      off += adjust;
      rowrel += dy;
    }
  }
}

Listing 6

import java.awt.*;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.border.*;

public class GradientPreview extends JPanel
{
  protected static final Dimension
    preferredSize = new Dimension(256, 24);
    
  protected GradientModel model;

  public GradientPreview(GradientModel model)
  {
    this.model = model;
  }
  
  public void paintComponent(Graphics g)
  {
    g.setColor(getBackground());
    g.fillRect(0, 0, getSize().width, getSize().height);
    drawModel(g, model);
  }

  protected void drawModel(Graphics gc, GradientModel model)
  {
    Graphics2D g = (Graphics2D)gc;
    CompoundGradientPaint gradient =
      new CompoundGradientPaint(model, new Point(0,0), new Point(255,0));
    g.setPaint(gradient);
    g.fillRect(0, 0, getSize().width, getSize().height);
  }

  public Dimension getPreferredSize()
  {
    return preferredSize;
  }
}

Listing 7

import java.awt.*;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.border.*;

public class JGradient extends JPanel
  implements ChangeListener
{
  protected GradientModel model;
  protected ColorSpectrum spectrum;
  protected TransparencyGradient transparency;
  
  public JGradient(GradientModel model, boolean preview)
  {
    this.model = model;
    model.addChangeListener(this);
    setLayout(new BorderLayout());
    setBorder(new EmptyBorder(4, 4, 4, 4));
    
    JPanel gradientPanel = new JPanel(new FlowLayout());
    gradientPanel.add(new GradientPanel(model));
    gradientPanel.setBorder(new TitledBorder("Gradient"));
    
    JPanel colorPanel = new JPanel(new BorderLayout());
    colorPanel.add(BorderLayout.CENTER,
      spectrum = new ColorSpectrum());
    colorPanel.setBorder(new CompoundBorder(
      new TitledBorder("Spectrum"),
      new EmptyBorder(0, 4, 4, 4)));
    spectrum.addChangeListener(this);
    
    JPanel alphaPanel = new JPanel(new BorderLayout());
    alphaPanel.add(BorderLayout.CENTER,
      transparency = new TransparencyGradient());
    alphaPanel.setBorder(new CompoundBorder(
      new TitledBorder("Transparency"),
      new EmptyBorder(0, 4, 4, 4)));
    transparency.addChangeListener(this);

    JPanel mainPanel = new JPanel(new BorderLayout());
    mainPanel.add(BorderLayout.NORTH, gradientPanel);
    mainPanel.add(BorderLayout.CENTER, colorPanel);
    mainPanel.add(BorderLayout.SOUTH, alphaPanel);
    
    add(BorderLayout.CENTER, mainPanel);
    
    JPanel testPanel = new JPanel(new BorderLayout());
    testPanel.add(BorderLayout.EAST, new GradientDisplay(model));
    testPanel.setBorder(new CompoundBorder(
      new TitledBorder("Preview"),
      new EmptyBorder(0, 4, 4, 4)));
    
    if (preview) add(BorderLayout.EAST, testPanel);
    model.fireChangeEvent();
  }

  public void setModel(GradientModel model)
  {
    this.model = model;
    model.fireChangeEvent();
  }
  
  public GradientModel getModel()
  {
    return model;
  }

  public void stateChanged(ChangeEvent event)
  {
    Object source = event.getSource();
    if (source == spectrum || source == transparency)
    {
      Color color = new Color(
        spectrum.getRed(),
        spectrum.getGreen(),
        spectrum.getBlue(),
        transparency.getAlpha());
      model.setColor(model.getSelected(), color);
    }
    else
    {
      Color color = model.getColor(model.getSelected());
      spectrum.setColor(color);
      transparency.setAlpha(color);
    }
    repaint();
  }
}

Listing 8

import java.util.*;
import javax.swing.*;
import javax.swing.event.*;

public class SelectionPanel extends JPanel
{
  protected Vector listeners = new Vector();

  public void addChangeListener(ChangeListener listener)
  {
    listeners.addElement(listener);
  }
  
  public void removeChangeListener(ChangeListener listener)
  {
    listeners.removeElement(listener);
  }
  
  protected void fireChangeEvent()
  {
    Vector list = (Vector)listeners.clone();
    ChangeEvent event = new ChangeEvent(this);
    ChangeListener listener;
    for (int i = 0; i < list.size(); i++)
    {
      listener = (ChangeListener)list.elementAt(i);
      listener.stateChanged(event);
    }
  }
}

Listing 9

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;

public class GradientBar extends JPanel
  implements SwingConstants, MouseListener,
    MouseMotionListener, KeyListener
{
  protected static final Dimension
    preferredSize = new Dimension(262, 16);

  protected GradientModel model;
  protected int position;

  public GradientBar(GradientModel model, int position)
  {
    this.model = model;
    this.position = position;
    addMouseListener(this);
    addMouseMotionListener(this);
    setOpaque(true);
  }
  
  public Dimension getPreferredSize()
  {
    return preferredSize;
  }
  
  public void paintComponent(Graphics g)
  {
    int w = getSize().width;
    int h = getSize().width;
    g.setColor(getBackground());
    g.fillRect(0, 0, w, h);
    for (int i = 0; i < model.getSize(); i++)
    {
      drawAnchor(g, i);
    }
  }
  
  public void drawAnchor(Graphics g, int index)
  {
    int offset = model.getOffset(index) + 3;
    Color color = model.getColor(index);
    int h = getSize().height;
    g.setColor(color);
    g.fillRect(offset - 2, 5, 5, 6);
    
    g.setColor(getForeground());
    g.drawRect(offset - 3, 4, 6, 7);
    
    if (position == SOUTH)
    {
      g.drawLine(offset, 0, offset, 0);
      g.drawLine(offset - 1, 1, offset + 1, 1);
      g.drawLine(offset - 2, 2, offset + 2, 2);
      g.drawLine(offset - 3, 3, offset + 3, 3);
    }
    else
    {
      g.drawLine(offset - 3, 12, offset + 3, 12);
      g.drawLine(offset - 2, 13, offset + 2, 13);
      g.drawLine(offset - 1, 14, offset + 1, 14);
      g.drawLine(offset, 15, offset, 15);
    }
    
    if (index == model.getSelected())
    {
      if (getParent().hasFocus()) g.setColor(Color.white);
      else g.setColor(getForeground());
    }
    else 
      g.setColor(getBackground());

    if (position == SOUTH)
    {
      g.drawLine(offset, 1, offset, 1);
      g.drawLine(offset - 1, 2, offset + 1, 2);
      g.drawLine(offset - 2, 3, offset + 2, 3);
    }
    else
    {
      g.drawLine(offset - 2, 12, offset + 2, 12);
      g.drawLine(offset - 1, 13, offset + 1, 13);
      g.drawLine(offset, 14, offset, 14);
    }
  }

  public void mousePressed(MouseEvent event)
  {
    getParent().requestFocus();
    int x = event.getX() - 3;
    for (int i = 0 ; i < model.getSize(); i++)
    {
      int offset = model.getOffset(i);
      if (x >= offset - 3 && x <= offset + 3)
      {
        model.setSelected(i);
        return;
      }
    }
    // If we got here, we did not find a match
    model.insert(new GradientAnchor(Color.white, x));
  }
  
  public void mouseReleased(MouseEvent event) {}
  public void mouseClicked(MouseEvent event) {}
  public void mouseEntered(MouseEvent event) {}

  public void mouseExited(MouseEvent event)
  {
    setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
  }
  
  public void mouseMoved(MouseEvent event)
  {
    int x = event.getX() - 3;
    for (int i = 0 ; i < model.getSize(); i++)
    {
      int offset = model.getOffset(i);
      if (x >= offset - 3 && x <= offset + 3)
      {
        setCursor(Cursor.getPredefinedCursor(Cursor.E_RESIZE_CURSOR));
        return;
      }
    }
    setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
  }

  public void mouseDragged(MouseEvent event)
  {
    int x = event.getX() - 3;
    if (x < 0) x = 0;
    if (x > 255) x = 255;
    int index = model.getSelected();
    if (index > -1)
      model.setOffset(index, x);
  }

  public void keyPressed(KeyEvent event)
  {
    int size = model.getSize() - 1;
    int selected = model.getSelected();
    int offset = model.getOffset(selected);
    int code = event.getKeyCode();
    if (code == KeyEvent.VK_DELETE)
      model.deleteSelected();
    if (code == KeyEvent.VK_UP)
    {
      selected++;
      if (selected > size) selected = 0;
      model.setSelected(selected);
    }
    if (code == KeyEvent.VK_DOWN)
    {
      selected--;
      if (selected < 0) selected = size;
      model.setSelected(selected);
    }
    if (code == KeyEvent.VK_LEFT)
    {
      offset--;
      if (offset < 0) offset = 0;
      model.setOffset(selected, offset);
    }
    if (code == KeyEvent.VK_RIGHT)
    {
      offset++;
      if (offset > 255) offset = 255;
      model.setOffset(selected, offset);
    }
    repaint();
  }
  
  public void keyReleased(KeyEvent event) {}
  public void keyTyped(KeyEvent event) {}
}

Listing 10

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.border.*;

public class GradientPanel extends JPanel
  implements SwingConstants, FocusListener
{
  public GradientPanel(GradientModel model)
  {
    setLayout(new BorderLayout());
    setBorder(new EmptyBorder(0, 4, 4, 4));
    GradientBar north = new GradientBar(model, NORTH);
    GradientBar south = new GradientBar(model, SOUTH);
    
    GradientPreview preview = new GradientPreview(model);
    JPanel previewPanel = new JPanel(new BorderLayout());
    previewPanel.setBorder(new CompoundBorder(
      new LineBorder(getBackground(), 1),
      new BevelBorder(BevelBorder.LOWERED)));
    previewPanel.add(BorderLayout.CENTER, preview);
      
    add(BorderLayout.CENTER, previewPanel);
    add(BorderLayout.SOUTH, south);
    addKeyListener(south);
    addFocusListener(this);
  }

  public boolean isFocusTraversable()
  {
    return true;
  }

  public void focusGained(FocusEvent event)
  {
    repaint();
  }
  
  public void focusLost(FocusEvent event)
  {
    repaint();
  }
}

Listing 11

import java.awt.*;
import javax.swing.*;

public class GradientDisplay extends JPanel
{
  protected GradientModel model;
  
  public GradientDisplay(GradientModel model)
  {
    this.model = model;
  }

  public void paintComponent(Graphics gc)
  {
    Graphics2D g = (Graphics2D)gc;
    CompoundGradientPaint gradient =
      new CompoundGradientPaint(model, new Point(100, 100),
        new Point(200, 200), true);
    g.setPaint(gradient);
    g.fillRect(0, 0, getSize().width, getSize().height);
    
    g.setColor(Color.black);
    g.drawRect(100, 100, 100, 100);
  }
  
  public Dimension getPreferredSize()
  {
    return new Dimension(300, 300);
  }
}

Listing 12

import java.awt.*;
import java.awt.geom.*;
import java.awt.event.*;
import javax.swing.*;

public class TransparencyGradient extends SelectionPanel
  implements MouseListener, MouseMotionListener,
		FocusListener, KeyListener
{
  public static final Dimension preferredSize =
    new Dimension(200, 16);
    
  protected int alpha;
  protected int x;
  protected boolean hasFocus;

  public TransparencyGradient()
  {
    addMouseListener(this);
    addFocusListener(this);
    addKeyListener(this);
  }
  
  public void paintComponent(Graphics gc)
  {
    Graphics2D g = (Graphics2D)gc;
    int w = getSize().width;
    int h = getSize().height;
    g.setPaint(new GradientPaint(
      new Point(0, 0), Color.white,
      new Point(w, 0), Color.black));
    g.fillRect(0, 0, w, h);
    drawCursor(g);
  }

  public void drawCursor(Graphics g)
  {
    int h = getSize().height;
    g.setColor(Color.black);
    g.drawLine(x - 1, 0, x - 1, h);
    g.drawLine(x + 1, 0, x + 1, h);
    g.setColor(hasFocus ? Color.blue : Color.white);
    g.drawLine(x, 0, x, h);
  }
  
  public int getAlpha()
  {
    return alpha;
  }
  
  public void setAlpha(Color color)
  {
    alpha = (color.getRGB() >>> 24) & 0xff;
    float factor = (255f - (float)alpha) / 255f; 
    x = (int)(factor * getSize().width);
  }
  
  public Dimension getPreferredSize()
  {
    return preferredSize;
  }

  public void mouseClicked(MouseEvent event) {}
  public void mouseReleased(MouseEvent event)
	{
		removeMouseMotionListener(this);
	}
  
	public void mouseEntered(MouseEvent event) {}
  public void mouseExited(MouseEvent event) {}

  public void mousePressed(MouseEvent event)
  {
    mouseDragged(event);
		addMouseMotionListener(this);
  }

  public void mouseMoved(MouseEvent event) {}
  public void mouseDragged(MouseEvent event)
	{
    int w = getSize().width;
    x = event.getX();
    alpha = 255 - (int)(((float)x / (float)w) * 255f);
    fireChangeEvent();
	}
	
  public boolean isFocusTraversable()
  {
    return true;
  }
  
  public void focusGained(FocusEvent event)
  {
    hasFocus = true;
    repaint();
  }
  
  public void focusLost(FocusEvent event)
  {
    hasFocus = false;
    repaint();
  }

  public void keyPressed(KeyEvent event)
  {
    int w = getSize().width - 1;
    int code = event.getKeyCode();
    if (code == KeyEvent.VK_RIGHT) x++;
    if (code == KeyEvent.VK_LEFT) x--;
    if (code == KeyEvent.VK_HOME) x = 0;
    if (code == KeyEvent.VK_END) x = w;
    if (x < 0) x = 0;
    if (x > w) x = w;
    alpha = 255 - (int)(((float)x / (float)w) * 255f);
    repaint();
  }
  
  public void keyReleased(KeyEvent event) {}
  public void keyTyped(KeyEvent event) {}
}

Listing 13

import java.awt.*;
import java.awt.event.*;
import java.awt.image.*;
import javax.swing.*;

public class ColorSpectrum extends SelectionPanel
  implements MouseListener, MouseMotionListener,
		FocusListener, KeyListener
{
  public static final Dimension preferredSize =
    new Dimension(200, 100);
  protected BufferedImage image;
  protected int rgb = 0;
  protected int x, y;
  protected Dimension size;
  protected boolean hasFocus;

  public ColorSpectrum()
  {
    addMouseListener(this);
    addFocusListener(this);
    addKeyListener(this);
  }
  
  public void paintComponent(Graphics gc)
  {
    int w = getSize().width;
    int h = getSize().height;
    if (image == null || w != size.width || h != size.height)
    {
      size = new Dimension(w, h);
      image = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
      Graphics g = image.getGraphics();
      int m = w / 2;
      for(int x = 0; x <= m; x++)
      {
        for(int y = 0; y < h; y++)
        {
          float hue = (float)y / (float)h;
          float saturation = (float)x / (float)m;
          float brightness = (float)x / (float)m;
          g.setColor(Color.getHSBColor(hue, saturation, 1f));
          g.drawLine(x, y, x, y);
          g.setColor(Color.getHSBColor(hue, 1f, brightness));
          g.drawLine(w - x, y, w - x, y);
        }
      }
    }
    gc.drawImage(image, 0, 0, this);
    drawCursor(gc);
  }
  
  public void drawCursor(Graphics g)
  {
    int h = getSize().height;
    int e = 5;
    g.setColor(Color.black);
    g.drawLine(x - 1, y - e, x - 1, y + e);
    g.drawLine(x + 1, y - e, x + 1, y + e);
    g.drawLine(x - e, y - 1, x + e, y - 1);
    g.drawLine(x - e, y + 1, x + e, y + 1);
    g.setColor(hasFocus ? Color.blue : Color.white);
    g.drawLine(x, y - e, x, y + e);
    g.drawLine(x - e, y, x + e, y);
  }
  
  private int makeColor(int r, int g, int b)
  {
    return (0xff << 24) | (r << 16) | (g << 8) | b;
  }
  
  public int getRed()
  {
    return (rgb >> 16) & 0xff;
  }

  public int getGreen()
  {
    return (rgb >> 8) & 0xff;
  }

  public int getBlue()
  {
    return (rgb) & 0xff;
  }

  public void setColor(Color color)
  {
    rgb = color.getRGB() & 0xffffff;
    float[] hsba = new float[4];
    hsba = Color.RGBtoHSB(
      color.getRed(), color.getGreen(), color.getBlue(),
      hsba);
    float hue = hsba[0];
    float saturation = hsba[1];
    float brightness = hsba[2];
    int w = getSize().width;
    int h = getSize().height;
    int m = w / 2;
    y = (int)(h * hue);
    x = (int)(brightness == 1f ? m * saturation : w - m * brightness);
  }
  
  public Dimension getPreferredSize()
  {
    return preferredSize;
  }

  public void mouseClicked(MouseEvent event) {}
  public void mouseReleased(MouseEvent event)
	{
		removeMouseMotionListener(this);
	}
  
	public void mouseEntered(MouseEvent event) {}
  public void mouseExited(MouseEvent event) {}

  public void mousePressed(MouseEvent event)
  {
    mouseDragged(event);
		addMouseMotionListener(this);
  }

  public void mouseMoved(MouseEvent event) {}
  public void mouseDragged(MouseEvent event)
	{
    x = event.getX();
    y = event.getY();
    rgb = image.getRGB(x, y);
    fireChangeEvent();
  }

  public boolean isFocusTraversable()
  {
    return true;
  }
  
  public void focusGained(FocusEvent event)
  {
    hasFocus = true;
    repaint();
  }
  
  public void focusLost(FocusEvent event)
  {
    hasFocus = false;
    repaint();
  }

  public void keyPressed(KeyEvent event)
  {
    int w = getSize().width - 1;
    int h = getSize().height - 1;
    int code = event.getKeyCode();
    if (code == KeyEvent.VK_RIGHT) x++;
    if (code == KeyEvent.VK_LEFT) x--;
    if (code == KeyEvent.VK_UP) y--;
    if (code == KeyEvent.VK_DOWN) y++;
    if (code == KeyEvent.VK_HOME) x = 0;
    if (code == KeyEvent.VK_END) x = w;
    if (code == KeyEvent.VK_PAGE_UP) y = 0;
    if (code == KeyEvent.VK_PAGE_DOWN) y = h;
    if (x < 0) x = 0;
    if (x > w) x = w;
    if (y < 0) y = 0;
    if (y > h) y = h;
    rgb = image.getRGB(x, y);
    repaint();
  }
  
  public void keyReleased(KeyEvent event) {}
  public void keyTyped(KeyEvent event) {}
}

Listing 14

import java.awt.*;
import javax.swing.*;

public class JGradientTest
{
  public static void main(String[] args)
  {
    PLAF.setNativeLookAndFeel(true);
  
    GradientModel model = new CompoundGradient();
    model.addAnchor(new GradientAnchor(Color.red, 0));
    model.addAnchor(new GradientAnchor(Color.yellow, 50));
    model.addAnchor(new GradientAnchor(Color.green, 100));
    model.addAnchor(new GradientAnchor(Color.orange, 150));
    model.addAnchor(new GradientAnchor(Color.blue, 200));
    model.addAnchor(new GradientAnchor(Color.red, 255));

    JFrame frame = new JFrame("JGradient Test");
    frame.getContentPane().setLayout(new BorderLayout());
    frame.getContentPane().add(BorderLayout.CENTER,
      new JGradient(model, true));
    frame.pack();
    frame.setVisible(true);
    model.fireChangeEvent();
  }
}