Subclassing java.awt.Paint

2007-04-02

In the good old days you could walk into your local electronic consumer goods stores and walk up to any one of a variety of intriguing and odd 8-bit microcomputers and proclaim one’s fame by typing something like:

10 PRINT "DRJ IS SKILL"
20 GOTO 10

These days you have to do it with procedural textures and Java subclasses. Like this:

/drj is skill/ in 70 point Optima Bold printed with a kind of fractally looking cloudy red and black ink

This is less of a normal blog post and more an article on how to subclass java.awt.Paint. If you’ve ever wondered why or how you might do that, read on. Technically you can’t subclass it because java.awt.Paint is an Interface, not a Class. Meh.) The essential ability of a class that implements java.awt.Paint is that it should be able to colour in a rectangle of pixels on demand. The essential coolness of Paint is that you can be quite clever about how you choose the colour of each pixel.

Assumes knowledge of Java SE 1.4.2 and similar. Might get hairy (if you think inner classes are hairy), and yes I had look up how to write the constructor when you extend somebody else’s inner class. I’ve abbreviated this article in parts (from a longer version) so you might need to dip into a good Java reference to keep up.

The preformatted code sections below may lose the extreme right hand end of some of the longer lines. Sorry about that. If you know a good solution, then post a comment. Thanks.

Java SE provides as standard 3 classes that implement the java.awt.Paint interface. They are: java.awt.Color, java.awt.GradientPaint, and java.awt.TexturePaint.

Color is the most boring, but probably most used. When you use a Color object as paint, for example in a call to the setPaint method of java.awt.Graphics2D, the shape being drawn gets coloured in a single colour throughout. GradientPaint and TexturePaint hint at some of the cleverer things you can do with Paint. GradientPaint gives you a paint that fades from one colour to another, TexturePaint gives you a paint that consists of lots of repeated copies of an image (usually quite a small one).

The crucial thing about GradientPaint and TexturePaint is that instead of being a single block of colour, they have some algorithm for determining the colour of any given pixel on the screen. In GradientPaint this algorithm starts by computing the distance along a line (specified when an instance is created); in the case of TexturePaint the algorithm imagines the image is repeated and works out which part of the image the requested pixel corresponds to. The core of all the examples I show is an algorithm that determines a colour for any given screen pixel.

Implementing Paint

At first glance it looks like java.awt.Paint should be easy to implement, there’s only one method in the interface:

public PaintContext createContext(ColorModel cm,
                                  Rectangle deviceBounds,
                                  Rectangle2D userBounds,
                                  AffineTransform xform,
                                  RenderingHints hints)

Sadly the story doesn’t end here: note that this method returns a java.awt.PaintContext instance which you probably haven’t heard of; it also takes a good number of arguments, not all of which are obvious. The good news is that much of the apparent complexity is only needed by certain optimised implementations of java.awt.Paint. You can get away with ignoring a lot of the arguments, especially initially, when you shouldn’t be concerned with optimising the implementation.

Before I delve into the details of the createContext method and the java.awt.PaintContext class I’ll explain what the PaintContext class is for. The Java 2D API works by rendering shapes (letters, rectangles, Bézier curves, and so on) onto the device (usually the screen, but not always). The same java.awt.Paint instance may be used to render different shapes on the screen, some rotated text over here, some rectangles over there, and or even rendering on different devices (a printer and a screen for example). The PaintContext object captures this rendering context. So whilst you might use the same Paint instance to render onto both a screen and a printer two different PaintContext instances will be used. This is especially useful for optimised implementations, for example the TexturePaint class. This class computes for each pixel step in X and Y on the screen how that corresponds to steps across the image. This information is the same for any render of a single shape, but changes when the current User Space to Device Space change. Because the Java 2D API uses a PaintContext the implementation of TexturePaint can make many time saving calculations and store the results in the PaintContext instance that it returns. The examples that I show won’t be optimised so they will have relatively simple implementations of PaintContext.

Ultimately it’s the PaintContext object returned by this method that is responsible for colouring in pixels. It does that by returning a Raster object on demand. A Raster object is essentially a 2D array of pixels, and we’ll come across it in more detail later.

java.awt.PaintContext is an interface, generally the implementor of java.awt.Paint also provides an implementation of PaintContext. An instance (generally a fresh one) is returned from the createContext method. In the examples I give, the arguments to the createContext method are generally ignored. This is allowed because the arguments are generally hints. If you want nitty gritty details then I recommend you read Sun’s documentation and also the source code to their TexturePaint class.

Time for Tea

Checkerboard swatch

Time for a cup of tea and some code. This is code for a pretty simply CheckPaint class which implements paint that is an alternating checkerboard pattern of two colours (that are specified when it’s instantiated).

The example swatch has its paint constructed thus:

new CheckPaint(Color.magenta, Color.white)

I’ll give a complete listing here, but the interesting bits are the CheckPaint constructor and the getRaster method:

// $Header: //depot/prj/javashader/master/code/CheckPaint.java#2 $
//
// CheckPaint
//
// A demonstration of a user-defined java.awt.Paint (sub-) class.
//
// This Paint implements a simple alternating checkerboard pattern.

import java.awt.Color;
import java.awt.Paint;
import java.awt.PaintContext;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;
import java.awt.image.ColorModel;
import java.awt.image.Raster;
import java.awt.image.WritableRaster;


class CheckPaint implements Paint {
  // Colors a and b stored in RGB component form and scaled by 0xff.
  private float[] colorA;
  private float[] colorB;

  public CheckPaint(Color aArg, Color bArg) {
    colorA = aArg.getRGBComponents(null);
    colorB = bArg.getRGBComponents(null);

    for (int b=0; b<3; ++b) {
      colorA[b] *= 0xff;
      colorB[b] *= 0xff;
    }
    // Set alpha so that colours are opaque.  For simplicity's sake.
    colorA[3] = 0xff;
    colorB[3] = 0xff;
  }

  public PaintContext createContext(ColorModel cm,
      Rectangle deviceBounds,
      Rectangle2D userBounds,
      AffineTransform xform,
      RenderingHints hints) {
    return new Context();
  }

  public int getTransparency() {
    return OPAQUE;
  }

  class Context implements PaintContext {

    public Context() { }

    public void dispose() {}

    public ColorModel getColorModel() {
      return ColorModel.getRGBdefault();
    }

    // getRaster makes use of the enclosing CheckPaint instance
    public Raster getRaster(int xOffset, int yOffset, int w, int h) {
      WritableRaster raster =
          getColorModel().createCompatibleWritableRaster(w, h);

      // Row major traversal, x coordinate changes fastest.
      for (int j=0; j<h; ++j) {
        for (int i=0; i<w; ++i) {
          int x = i+xOffset;
          int y = j+yOffset;
          int s = (x+y) % 2;
          if (0 == s) {
            raster.setPixel(i, j, colorA);
          } else {
            raster.setPixel(i, j, colorB);
          }
        }
      }
      return raster;
    }
  }
}

The CheckPaint constructor is what you’ll call when you want to use this paint. It takes two colours. These colours are converted to a convenient format and squirreled away in the instance in the colorA and colorB fields. They’re used later by the getRaster method.

If you look carefully you’ll notice that the getRaster method is a method on the Context class which is an inner class of CheckPaint. The createContext method of CheckPaint creates an instance of the (inner) Context class and the getRaster method of the Context class serves up coloured pixels. The getRaster method needs to get hold of the colour parameters that were specified when the CheckPaint instance was created, so it’s convenient to make it an inner class.

There are two issues that are quite complicated and that I ultimately finesse. They are: what ColorModel should I use, and what Raster should I use. Optimised paint implementations will select both of these so that they match the ColorModel hint passed to createContext; that way the transfer of pixels from the Raster to the device will be very efficient. The source code to the TexturePaint class (provided by Sun) makes good reading for those interested in how to optimise this sort of thing. I make a massive simplification by using a very simple default format returned by java.awt.image.ColorModel.getRGBdefault(); it simply packs each of Alpha, Red, Green, and Blue into a 32-bit int using 8 bits for each. That ColorModel is used to create the Raster using the convenient createCompatibleWritableRaster method.

The essence of my CheckPaint class is the inner loop of getRaster method. The inner loop computes, for each pixel in the raster, the value s such that s alternates between 0 and 1 in a checkerboard pattern. s is then used to select one of the two colours specified when the CheckPaint was instantiated. Each pixel in the raster is assigned a colour in this way. Bonus question: what is the effect of ignoring the AffineTransform parameter passed to the createContext method?

Noise

NoisePaint, BrownianPaint, and CamoPaint show some of the more interesting possibilities. Code for these is at the end. These paints use a more sophisticated calculation method to determine the colour of each pixel. The techniques are borrowed from the procedural texture generation community. In computer graphics a texture usually just means an image that is used to paint the pixels of a surface. Sometimes the textures are generated by computer programs, other times by artists.

Perlin noise swatch, kinda blobby.

new NoisePaint(Color.black, Color.white, AffineTransform.getScaleInstance(0.25, 0.25))

NoisePaint implements a 2D Perlin noise function. The underlying computation I have shamelessly borrowed from Ken Perlin‘s reference implementation which, handily, is in Java. The basic idea behind it is that you get a surface which is random and irregular. The noise function generates a value (in this case between -1 and 1) for each (x,y,z) triple. In NoisePaint this value is used to interpolate a colour from the two colors specified when NoisePaint was instantiated.

It’s important to note that the noise function, ImprovedNoise.noise, does not change for a given position. That is, if you call it with the same x, y, and z coordinates you’ll get the same answer, thus it isn’t truly random (it’s not even pseudo-random really). This is important when the function is used for painting texture onto the screen; you wouldn’t want the texture to change each time your applet’s paint method was called.

NoisePaint can look quite interesting and nice, but Perlin noise has a more important role in procedural texture generation which is that it forms the basis of more interesting and flexible generated textures. Note that Perlin noise has a characteristic scale (or frequency), you can see this by using the AffineTransform constructor to zoom in, it looks blurry. That’s because even though Perlin noise is random it has blobs that tend to be a certain size.

A common use of Perlin noise is in making Fractional Brownian Noise, this is a texture that has features at a wide range of sizes. The basic idea is to take a few copies of some Perlin noise, scale them by different amounts and add the functions together (this is actually the computational approximation to a mathemetical ideal where an infinite number of noise functions are added together). That’s what BrownianPaint does. You can see, from the constructors, that many more parameters are involved.

Brownian noise swatch, kinda cloudy.

new BrownianPaint(Color.black, Color.red, 1, 2.1753974, 5)

The extra constructor parameters h and lacunarity control the way the texture is generated and affect its appearance (you can think of h as controlling roughness and lacunarity is how gappy the texture appears). The octaves parameter controls how many copies of Perlin noise are used which determines to what level of detail the computation is carried out. More octaves gives a finer level of detail but also take more computation time.

CamoPaint is a simple variation on BrownianPaint that gives strikingly different results. In BrownianPaint the underlying (fractional Brownian) noise function generates a real value centred around 0 and this is used to select a colour in between the two colours specified when BrownianPaint was instantiated (similar to NoisePaint). In CamoPaint I use exactly the same noise function, but use the value to select one of 3 colours depending on whether the value is bigger than, smaller than, or in between two specified threshold values. It’s called CamoPaint because the shader in BRL-CAD whence I borrowed the algorithm is called camo. With a little stretch of the imagination (and the texture!) it does look a little bit like camoflage paint:


Color a = new Color(0.38f, 0.29f, 0.16f);
Color b = new Color(0.1f, 0.3f, 0.04f);
Color c = new Color(0.15f, 0.15f, 0.15f);
AffineTransform xform = new AffineTransform();
xform.scale(0.1, 0.03);
xform.rotate(-0.6);
new CamoPaint(a, b, c, -0.25, 0.25, 1, 2.1753974, 5, xform)

The common thread binding all these different implementations of java.awt.Paint is the core of their getRaster methods. If you can think of a function that will generate a colour when given an (x,y) coordinate pair, then you can make a Paint out of it.

Appendix

An appendix? In a blog? Sorry about that. Repurposed content and all that.

NoisePaint:

// $Header: //depot/prj/javashader/master/code/NoisePaint.java#2 $
//
// NoisePaint
//
// A demonstration of a user-defined java.awt.Paint (sub-) class.
// This Paint implements a 2 dimensional Perlin noise texture, based on
// Perlin's reference implmentation (see ImprovedNoise.java,
// http://mrl.nyu.edu/~perlin/noise/, and
// http://mrl.nyu.edu/~perlin/paper445.pdf ).

import java.awt.Color;
import java.awt.Paint;
import java.awt.PaintContext;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;
import java.awt.image.ColorModel;
import java.awt.image.Raster;
import java.awt.image.WritableRaster;


class NoisePaint implements Paint {
  static final AffineTransform defaultXForm =
      AffineTransform.getScaleInstance(0.25, 0.25);

  // Colors a and b stored in component form.
  private float[] colorA;
  private float[] colorB;
  private AffineTransform xform;

  public NoisePaint(Color aArg, Color bArg) {
    colorA = aArg.getComponents(null);
    colorB = bArg.getComponents(null);
    xform = defaultXForm;
  }

  public NoisePaint(Color aArg, Color bArg, AffineTransform xformArg) {
    colorA = aArg.getComponents(null);
    colorB = bArg.getComponents(null);
    xform = xformArg;
  }

  public PaintContext createContext(ColorModel cm,
      Rectangle deviceBounds,
      Rectangle2D userBounds,
      AffineTransform xform,
      RenderingHints hints) {
    return new Context(cm, xform);
  }

  public int getTransparency() {
    return OPAQUE;
  }

  class Context implements PaintContext {

    public Context(ColorModel cm_, AffineTransform xform_) { }

    public void dispose() {}

    public ColorModel getColorModel() {
      return ColorModel.getRGBdefault();
    }

    // getRaster makes heavy use of the enclosing NoisePaint instance
    public Raster getRaster(int xOffset, int yOffset, int w, int h) {
      WritableRaster raster =
          getColorModel().createCompatibleWritableRaster(w, h);
      float [] color = new float[4];

      // Row major traversal, x coordinate changes fastest.
      for (int j=0; j<h; ++j) {
        for (int i=0; i<w; ++i) {
          // 2D point stored in array format for convenience of using
          // AffineTransform.transform(float[], _)
          float [] p = { i+xOffset, j+yOffset };

          xform.transform(p, 0, p, 0, 1);

          float t = (float)ImprovedNoise.noise(p[0], p[1], 2.718);
          // ImprovedNoise.noise returns a result in the range [-1,1],
          // we want a lerp factor in the range [0,1].
          t = (t+1)/2;

          for (int b=0; b<4; ++b) {
            color[b] = lerp(t, colorA[b] ,colorB[b]);
            // The multiplication assumes the default RGB model, 8 bits
            // per band (channel).
            color[b] *= 0xff;
          }
          raster.setPixel(i, j, color);
        }
      }
      return raster;
    }

    float lerp(float t, float a, float b) {
      return a + t*(b-a);
    }
  }
}

BrownianPaint:

// $Header: //depot/prj/javashader/master/code/BrownianPaint.java#5 $
//
// BrownianPaint
//
// A demonstration of a user-defined java.awt.Paint (sub-) class.
// This Paint implements a 2 dimensional fractional brownian motion texture.
// The algorithm is taken from the BRL-CAD sources (files libbn/noise.c
// and liboptical/sh_camo.c).
// See www.brl-cad.org

import java.awt.Color;
import java.awt.Paint;
import java.awt.PaintContext;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;
import java.awt.image.ColorModel;
import java.awt.image.Raster;
import java.awt.image.WritableRaster;


class BrownianPaint implements Paint {
  static AffineTransform defaultXForm =
      AffineTransform.getScaleInstance(0.05, 0.05);
  private double h;
  private double lacunarity;
  private double octaves;
  // Colors a and b stored in component form.
  float[] colorA;
  float[] colorB;
  private AffineTransform xform;
  // Spectral weights, computed once for each instance and used many
  // times by getRaster.
  private float[] weight;
  private AffineTransform lacunarScale;

  public BrownianPaint(Color aArg, Color bArg,
      double hArg, double lacunarityArg, double octavesArg) {
    h = hArg;
    lacunarity = lacunarityArg;
    octaves = octavesArg;
    colorA = aArg.getComponents(null);
    colorB = bArg.getComponents(null);
    xform = defaultXForm;
    weight = SpectralWeight(h, lacunarity, octaves);
    lacunarScale = AffineTransform.getScaleInstance(lacunarity,
        lacunarity);
  }

  public BrownianPaint(Color aArg, Color bArg,
      double hArg, double lacunarityArg, double octavesArg,
      AffineTransform xformArg) {
    this(aArg, bArg, hArg, lacunarityArg, octavesArg);
    if (xformArg != null) {
      xform = xformArg;
    }
  }

  static float[] SpectralWeight(double h, double lacunarity, double octaves) {
    float[] weight = new float[(int)Math.ceil(octaves)];
    double frequency = 1.0;

    for(int i=0; i<octaves; ++i) {
      weight[i] = (float)Math.pow(frequency, -h);
      frequency *= lacunarity;
    }
    return weight;
  }

  public PaintContext createContext(ColorModel cm,
      Rectangle deviceBounds,
      Rectangle2D userBounds,
      AffineTransform xform,
      RenderingHints hints) {
    return new Context(cm, xform);
  }

  public int getTransparency() {
    return OPAQUE;
  }

  public class Context implements PaintContext {

    public Context(ColorModel cm_, AffineTransform xform_) { }

    public void dispose() {}

    public ColorModel getColorModel() {
      return ColorModel.getRGBdefault();
    }

    // getRaster makes heavy use of the enclosing BrownianPaint instance
    public Raster getRaster(int xOffset, int yOffset, int w, int h) {
      WritableRaster raster =
          getColorModel().createCompatibleWritableRaster(w, h);

      // Some temporary arrays, allocated outside the loops.
      float[] color = new float[4];
      // 2D point stored in array format for convenience of using
      // AffineTransform.transform(float[], _)
      float[] p = new float[2];

      // Row major traversal, x coordinate changes fastest.
      for (int j=0; j<h; ++j) {
        for (int i=0; i<w; ++i) {
          p[0] = i+xOffset;
          p[1] = j+yOffset;

          xform.transform(p, 0, p, 0, 1);

          // The inner loop accumulates a Perlin noise value for each
          // "octave".  Octave is a slight misnomer since each one
          // encompasses a range equal to the lacunarity which isn't
          // necessarily 2.
          float t = 0;
          double z = 2.718;     // Essentially arbitrary.

          int o; // Declared outside the loop, so that we can use it later.
          for (o=0; o<octaves; ++o) {
            t += (float)ImprovedNoise.noise(p[0], p[1], z) * weight[o];
            lacunarScale.transform(p, 0, p, 0, 1);
          }
          double remainder = octaves - (int)octaves;
          if (remainder != 0) {
            t += (float)ImprovedNoise.noise(p[0], p[1], z) * weight[o] *
                remainder;
          }
          colorise(t, color);
          raster.setPixel(i, j, color);
        }
      }
      return raster;
    }

    void colorise(float t, float[] color) {
      // t is centred on 0.0 with (conceptually) infinite range.
      // Scale and clamp to [0,1].
      t = (t+1)/2;
      if (t > 1.0) {
        t = 1.0f;
      }
      if (t < 0.0) {
        t = 0.0f;
      }

      for (int b=0; b<4; ++b) {
        color[b] = lerp(t, colorA[b], colorB[b]);
        // The multiplication assumes the default RGB model, 8 bits
        // per band (channel).
        color[b] *= 0xff;
      }
      return;
    }

    float lerp(float t, float a, float b) {
      return a + t*(b-a);
    }
  }
}

CamoPaint:

// $Header: //depot/prj/javashader/master/code/CamoPaint.java#5 $
//
// CamoPaint
//
// Algorithm taken from BRL-CAD, www.brl-cad.org

import java.awt.Color;
import java.awt.Paint;
import java.awt.PaintContext;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;
import java.awt.image.ColorModel;
import java.awt.image.Raster;
import java.awt.image.WritableRaster;


class CamoPaint extends BrownianPaint {
  float[][] stripe;     // Array of colors stored in component form
  double[] threshold;

  /** Helper method used by constructors. */
  protected void init3(Color a, Color b, Color c,
      double threshold1, double threshold2) {
    init(new Color[] {a, b, c}, new double[] { threshold1, threshold2});
  }

  /** Helper method used by constructors. */
  protected void init(Color[] color, double[] threshold) {
    if (color.length != threshold.length + 1) {
      throw new IllegalArgumentException();
    }
    stripe = new float[color.length][];
    for (int i=0; i<color.length; ++i) {
      stripe[i] = color[i].getComponents(null);
    }
    this.threshold = new double[color.length];
    System.arraycopy(threshold, 0, this.threshold, 0, threshold.length);
    this.threshold[threshold.length] = Double.POSITIVE_INFINITY;
    return;
  }

  public CamoPaint(Color aArg, Color bArg, Color cArg,
      double threshold1, double threshold2,
      double hArg, double lacunarityArg, double octavesArg) {
    super(aArg, bArg, hArg, lacunarityArg, octavesArg);
    init3(aArg, bArg, cArg, threshold1, threshold2);
  }

  public CamoPaint(Color aArg, Color bArg, Color cArg,
      double threshold1, double threshold2,
      double hArg, double lacunarityArg, double octavesArg,
      AffineTransform xformArg) {
    super(aArg, bArg, hArg, lacunarityArg, octavesArg, xformArg);
    init3(aArg, bArg, cArg, threshold1, threshold2);
  }

  /** color and threshold arrays are copied, not referenced. */
  public CamoPaint(Color[] color, double[] threshold,
      double hArg, double lacunarityArg, double octavesArg,
      AffineTransform xformArg) {
    super(color[0], color[1], hArg, lacunarityArg, octavesArg, xformArg);
    init(color, threshold);
  }

  public PaintContext createContext(ColorModel cm,
      Rectangle deviceBounds,
      Rectangle2D userBounds,
      AffineTransform xform,
      RenderingHints hints) {
    return new CamoContext(cm, xform);
  }

  class CamoContext extends BrownianPaint.Context {
    public CamoContext(ColorModel cm_, AffineTransform xform_) {
      // Warning: Hairy because this class extends an inner class of an
      // unrelated class.
      CamoPaint.this.super(cm_, xform_);
    }

    void colorise(float t, float[] color) {
      for (int i=0; i<threshold.length; ++i) {
        if (t < threshold[i]) {
          System.arraycopy(stripe[i], 0, color, 0, 4);
          for (int b=0; b<4; ++b) {
            color[b] *= 0xff;
          }
          return;
        }
      }
    }
  }
}
About these ads

One Response to “Subclassing java.awt.Paint”

  1. Patrick Roche Says:

    These classes are perfect for a java application I am trying to write that imitates Rothko’s style of painting. Can you please email me an example of the classes in a test application. I cannot figure out how to use them.


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

%d bloggers like this: