Onyx reloaded

May 22nd, 2010

Exactly a year ago I’ve introduced project Onyx – animation blueprints for Swing using the Trident animation library. Onyx is a small application that loads album information for the specific artist from the Internet and displays it as a scrollable gallery. Here is how Onyx looked a year ago:

And here is how Onyx looks now:

Along with running on Java 6 (with Java 7 weekly binaries for Mac nowhere in sight), Onyx also has received a slight visual facelift for the main window. First, let’s see the code for disposing a window by fading it out:

public static void fadeOutAndDispose(final Window window,
      int fadeOutDuration) {
   Timeline dispose = new Timeline(window);

   try {
      Class clazz = Class.forName("com.sun.awt.AWTUtilities");
      final Method opacityMethod = clazz.getDeclaredMethod(
            "setWindowOpacity", Window.class, float.class);

      dispose.addPropertyToInterpolate(Timeline. property(
            "opacity").from(1.0f).to(0.0f).setWith(
            new PropertySetter() {
               @Override
               public void set(Object obj, String fieldName,
                     Float value) {
                  try {
                     opacityMethod.invoke(null, obj, value);
                  } catch (Exception exc) {
                     exc.printStackTrace();
                  }
               }
            }));
      dispose.addCallback(new UIThreadTimelineCallbackAdapter() {
         @Override
         public void onTimelineStateChanged(TimelineState oldState,
               TimelineState newState, float durationFraction,
               float timelinePosition) {
            if (newState == TimelineState.DONE) {
               window.dispose();
            }
         }
      });
      dispose.setDuration(fadeOutDuration);
      dispose.play();
   } catch (Exception exc) {
      exc.printStackTrace();
   }
}

With Java 7, you can simply do this:

dispose.addPropertyToInterpolate("opacity", 1.0f, 0.0f);

and let Trident call Window.setOpacity method at every timeline pulse. In Java 6 (starting with 6u10), the AWTUtilities.setWindowOpacity needs to be called on every pulse explicitly.

And here is the code for painting the main content window. First, we store the current clip and enable anti-aliasing:

@Override
protected void paintComponent(Graphics g) {
   Graphics2D g2d = (Graphics2D) g.create();
   Shape clip = g2d.getClip();

   g2d.setStroke(new BasicStroke(1.0f));
   g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
         RenderingHints.VALUE_ANTIALIAS_ON);

Next, we compute the outer and inner contours of the panel (the inner contour will be painted with slightly brighter colors as shown in the screenshot above):

   int radius = 16;

   Shape contour = new RoundRectangle2D.Double(0, 0, getWidth() - 1,
         getHeight() - 1, radius, radius);
   Shape innerContour = new RoundRectangle2D.Double(1, 1, getWidth() - 3,
         getHeight() - 3, radius - 1, radius - 1);

When the main window is first shown and disposed, a special timeline is played to make the window appear and disappear smoothly. The timeline interpolates the alpha field between 0.0 and 0.9. The main panel has two parts – the bright blue header and the black content. While the content is painted based on the current value of alpha, the header uses less translucency:

   g2d.setComposite(AlphaComposite.SrcOver.derive(1.0f - (float) Math.pow(
         1.0f - alpha, 3.0)));

To paint the header, we first update the current clip to make sure that the painting is only done in the header section. Next, we fill the contour, draw the inner contour and draw the outer contour – all based on the specific colors required by the target design:

   // top part
   g2d.clipRect(0, 0, getWidth(), TITLE_HEIGHT);
   g2d.setPaint(new LinearGradientPaint(0, 0, 0, TITLE_HEIGHT,
         new float[] { 0.0f, 0.49999f, 0.5f, 1.0f }, new Color[] {
               new Color(119, 152, 251), new Color(80, 127, 250),
               new Color(48, 109, 250), new Color(10, 97, 250) }));
   g2d.fill(contour);
   g2d.setPaint(new GradientPaint(0, 0, new Color(151, 179, 253), 0,
         TITLE_HEIGHT, new Color(19, 92, 233)));
   g2d.draw(innerContour);
   g2d.setColor(new Color(11, 61, 200));
   g2d.draw(contour);

Now we restore the clip:

   g2d.setClip(clip);

If we have an artist name to display, we position it in the middle of the title area (using font ascent for proper baseline alignment) and paint the string twice to create a lighting effect:

   if (this.searchString != null) {
      g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
            RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
      g2d.setFont(UIManager.getFont("Label.font").deriveFont(14.0f)
            .deriveFont(Font.BOLD));
      int fa = g2d.getFontMetrics().getAscent();
      int x = (getWidth() - g2d.getFontMetrics().stringWidth(
            this.searchString)) / 2;
      int y = (TITLE_HEIGHT + fa) / 2;
      g2d.setColor(new Color(31, 60, 114));
      g2d.drawString(this.searchString, x, y + 1);
      g2d.setColor(new Color(255, 255, 255));
      g2d.drawString(this.searchString, x, y);
   }

The main content area is painted in the same way as the header. We update the clip, fill the contour, draw the inner contour and draw the outer contour:

   // bottom part
   g2d.setComposite(AlphaComposite.SrcOver.derive(this.alpha));
   g2d.clipRect(0, TITLE_HEIGHT, getWidth(), getHeight() - TITLE_HEIGHT
         + 1);

   g2d.setColor(new Color(0, 0, 0));
   g2d.fill(contour);
   g2d.setPaint(new GradientPaint(0, TITLE_HEIGHT, new Color(57, 56, 57),
         0, getHeight() - TITLE_HEIGHT, new Color(50, 48, 50)));
   g2d.draw(innerContour);
   g2d.setPaint(new GradientPaint(0, TITLE_HEIGHT, new Color(13, 11, 15),
         0, getHeight() - TITLE_HEIGHT, new Color(15, 8, 13)));
   g2d.draw(contour);

Finally, we draw a single line separator between the title and main content areas:

   // separator
   g2d.setClip(clip);
   g2d.setColor(new Color(12, 11, 12));
   g2d.drawLine(1, TITLE_HEIGHT, getWidth() - 2, TITLE_HEIGHT);

   g2d.dispose();
}

To see the latest Onyx in action, click on the button below to launch the WebStart demo. Use the mouse wheel to scroll between the albums. Click a specific album to show a larger cover and the track list.

If you cannot run the demo, here is a short video of Onyx in action:

Design, uninterrupted #28

May 21st, 2010

Today’s post highlights the design of BlackRabb.it. A single page design with a strong grid places the main focus on highlighting the portfolio with embedded thumbnails. The overall dark colors are balanced with background noise texture, glowing text effects on section headers, and rollover highlight effects. Move the mouse over the themed client logos to see translucent glowing popups, and click the “+” button between the header and main sections for a colorful illustration. Press Alt+G or Cmd+G to view the grid lines that govern the placement of all the elements in the main section. If you open the site in a WebKit browser (such as Safari), the rollover effects use fading / moving animations, and the expanded illustration has a few moving elements as well.

Adding some polish

May 20th, 2010

A few screenshots of Seesmic running on Droid: