Showing posts with label Swing. Show all posts
Showing posts with label Swing. Show all posts

Friday, November 16, 2012

Hanoi Tower

The backend algorithm is just a few lines big..., it's all about rendering.
This browser does not support Applets. It could be a matter of authorization. Or install Java or use another browser...
The core code is pretty cool:
 public class BackendAlgorithm
 {
   public static void move(int n, String from, String to, String using)
   {
     if (n == 0)
       return;
     move(n-1, from, using, to);
     HanoiContext.getInstance().fireMoveRequired(from, to); // Broadcast the move
     move(n-1, using, to, from);
   }
 }
Again, it's mostly about rendering... 99% of the code of the applet above is for the graphical display!
The simplest user interface would look like this:
 package hanoitower;
 
 public class BackendAlgorithm
 {
   public static void move(int n, String from, String to, String using)
   {
     if (n == 0)
       return;
     move(n-1, from, using, to);
     System.out.println("Moving from " + from + " to " + to);
     move(n-1, using, to, from); 
   }
  
   public static void main(String[] args) throws Exception
   {
     move(Integer.parseInt(args[0]), "A", "C", "B"); // Moving A to C using B
   }
 }
Run it this way:
 Prompt> java hanoitower.BackendAlgorithm 4
 Moving from A to B
 Moving from A to C
 Moving from B to C
 Moving from A to B
 Moving from C to A
 Moving from C to B
 Moving from A to B
 Moving from A to C
 Moving from B to C
 Moving from B to A
 Moving from C to A
 Moving from B to C
 Moving from A to B
 Moving from A to C
 Moving from B to C
Boom!

Wednesday, October 24, 2012

Smooth moves with Swing

It's about data rendering in Swing. How to make a smooth rendering of real time data...
The secret seems to be in SwingUtilities.invokeAndWait

This browser does not support Applets.
Use the button to start or stop the applet

Above are two JPanels, displaying, on the left a speed, on the right a heading.
Both are fed by real time data (simulated in this case).
Obviously - and we will not show this code, unless anyone insists - the position of the display (namely the position of the hand) is done when the repaint() is invoked, and its implementation is done by overriding the paintComponent(Graphics) method.
The smoothing is done in a setValue(double) method.
When this method is invoked, the difference between the previous and the next values is splitted in small intervals, between which a repaint() is invoked.
All the difficulty - the problem - is that when a repaint() is invoked, maybe the previous one is not completed yet... This would result in a jerky display, far from being smooth, at the opposite of the expected result.
A solution is to tell Swing to wait, which happens to be shamefully simple... Here is the code for the speed display:
  public void setSpeed(final double d)
  {
    this.speed = d;
    double from = this.prevSpeed; 
    double to = d;
    
    int sign = (from>to)?-1:1;
    prevSpeed = d;
    // Smooth rotation
    for (double s=from; (sign==1 && s<=to) || (sign==-1 && s>=to); s+=(0.1*sign)) // 0.1 is the damping factor
    {
      final double _s = s;
      try
      {
        SwingUtilities.invokeAndWait(new Runnable()
          {
            public void run()
            {
              speed = _s;
              repaint();
            }
          });
      }
      catch (Exception ex)
      {
        ex.printStackTrace();
      }
    }
  }
The paintComponent(Graphics) method uses the speed member to position the hand on the display.
Notice the SwingUtilities.invokeAndWait method that takes a Runnable as a parameter. This Runnable is the one invoking the repaint() method. This way, Swing will do nothing until the repaint is completed, and it is not even necessary to worry about synchronizing anything...

See also SwingUtilities.invokeLater. It allows Swing to perform demanding tasks asynchronously.

Friday, July 20, 2012

Smoothing a color chart in Java

The goal here is to smooth a colored drawing, to avoid the tile effect.
Before
After
Although they look similar, the path for the two images above is pretty different.
The regular path to draw on a javax.swing.JPanel is to override the method
  public void paintComponent(Graphics gr)
We have defined an array of colors, for the example
  private final static int[][] COLOR_VALUES =
  {
   {255, 255, 255 },
   {208, 244, 250 },
   {161, 233, 246 },
   {115, 222, 241 },
   {68, 211, 237 },
   {21, 200, 232 },
   {21, 207, 223 },
   {20, 214, 214 },
  ...
   {59, 2, 64 },
   {30, 1, 32 },
   {0, 0, 0 }
  };

The tile panel

The method used to the tile (left) approach would look like this:
  private static JPanel colorPane = new JPanel()
    {
      public void paintComponent(Graphics gr)
      {
        ((Graphics2D)gr).setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
                                          RenderingHints.VALUE_TEXT_ANTIALIAS_ON);      
        ((Graphics2D)gr).setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                                          RenderingHints.VALUE_ANTIALIAS_ON);      
        int w = this.getWidth();
        int h = this.getHeight();
        
        paintColors(gr, w, h);

        gr.setColor(Color.black);
        gr.setFont(gr.getFont().deriveFont(40f));
        
        String str = "Some text on top";
        int strWidth  = gr.getFontMetrics(gr.getFont()).stringWidth(str);        
        gr.drawString(str, (w / 2) - (strWidth / 2), (h / 2) + (40 / 2));
      }
      
      private void paintColors(Graphics gr, int w, int h)
      {
        // Draw 7x7
        int wStep = (int)((float)w / 7f);
        int hStep = (int)((float)h / 7f);
        for (int hor=0; hor<7; hor++)
        {
          for (int vert=0; vert<7; vert++)
          {
            Color c = new Color(COLOR_VALUES[(hor * 7) + vert][0],
                                COLOR_VALUES[(hor * 7) + vert][1],
                                COLOR_VALUES[(hor * 7) + vert][2]);
            gr.setColor(c);
            gr.fillRect(hor * wStep, vert * hStep, wStep, hStep);
          }
        }        
      }
    };

The smoothed panel

The other approach is a bit less straightforward.
The way to proceed is to
  • Generate an Image
  • Apply a blur filter on it
  • Display the image
The blur filter could look like this:
  public static BufferedImage blur(BufferedImage bimg, int matrixDim)
  {
    float blurMatrix[] = new float[matrixDim * matrixDim];
    for (int i=0; i< blurMatrix.length; i++)
      blurMatrix[i] = 1f / (float)(matrixDim * matrixDim);

    Kernel kernel = new Kernel(matrixDim, matrixDim, blurMatrix);
    BufferedImageOp blurFilter = new ConvolveOp(kernel, 
                                                ConvolveOp.EDGE_NO_OP,
                                                null);
    bimg = blurFilter.filter(bimg, null);
    return bimg;
  }
And it can be used as follow in the paintComponent method:
      public void paintComponent(Graphics gr)
      {
        ((Graphics2D)gr).setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
                                          RenderingHints.VALUE_TEXT_ANTIALIAS_ON);      
        ((Graphics2D)gr).setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                                          RenderingHints.VALUE_ANTIALIAS_ON);      
        int w = this.getWidth();
        int h = this.getHeight();
        
        // Create the image
        BufferedImage bufferedImage = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);       
        // Create a graphics contents on the buffered image
        Graphics2D g2d = bufferedImage.createGraphics();

        this.paintColors((Graphics)g2d, w, h);
        // Graphics context no longer needed so dispose it
        g2d.dispose();
        // Blur it
        bufferedImage = blur(bufferedImage, w / 7);
        // Display it
        gr.drawImage(bufferedImage, 0, 0, null);

        gr.setColor(Color.black);
        gr.setFont(gr.getFont().deriveFont(40f));
        
        String str = "Some text on top";
        int strWidth  = gr.getFontMetrics(gr.getFont()).stringWidth(str);        
        gr.drawString(str, (w / 2) - (strWidth / 2), (h / 2) + (40 / 2));
      }
And that's it!
It's fully compatible with transparency,
  g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.5f));

But beware, depending on the size of the Kernel matrix, the blur operation can be pretty demanding.

Monday, May 21, 2012

From ugly to glossy

This is about showing how the exact same data can be displayed with the same original idea, yellow text on a red circle, from Java and Swing.

OK, that's the first shot. The circle is itchy, it's anything but attractive. Let's add some anti-alias to the picture:

The edge of the circle looks better, the way the font is rendered too. But a nicer font should help too

The flat background is not looking good. Let's add a gradient to it, going from red to dark red, from the top to one third of the diameter.

That's better, but quite not there yet... We would need some glossy effect. That one is interesting, and surprisingly simple. The dark red background is flat (no gradient), the glossy effect comes from the light gray oval drawn on top, that one has a gradient, from light gray to dark red, going vertically, from the top of the circle, to one third of the diameter.

A shadow in the background would bring some bevel to the picture. It is a RadialGradientPaint.

Now that looks better!
The java code is not specially complicated, and it can certainly be reused.
Here is an example of some code using this technique.
And by the way, forget about dark red... Black is much classier.

All the code demonstrated in this article is available here.