Core Java Tech Tips
   
Tech Tips archive

Tech Tips
2006 年 9月 23日

2006 年 9 月号の Core Java Technologies Tech Tips をお届けします。Core Java Technology Tech Tips は、Java 2 Platform, Standard Edition (J2SE) のものなど Java のコアテクノロジと API を最大限活用するために役立つヒントを紹介します。

この号には次の記事が掲載されています。

» Java 2D のソフトクリッピング
» Java 2D の光と影

これらのヒントは、Java 2 Platform, Standard Edition Development Kit 5.0 (JDK 5.0) を使用して作成されました。JDK 5.0 は、http://java.sun.com/j2se/1.5.0/download.jsp でダウンロードできます。

この号の Tech Tip は、Sun Microsystems の Java 2D ソフトウェアエンジニア、Chris Campbell が執筆しました。

ほかの Java プラットフォームのテクノロジや製品を重点的に取り上げた Tech Tips を購読するには、このニュースレターの最後にある「Subscribe/Unsubscribe note」を参照してください。
 

注:今月の Tech Tips のコードサンプルを実行するために、Scott Violet の Interactive Graphics Editor (IGE) を使用できます。Interactive Graphics Editor アプリケーションにコードをカット&ペーストし、変更が描画にどのような影響を与えるかを確認してください。

Java 2D のソフトクリッピング

Java 2D に慣れている方であれば、任意の形状を使用して描画をクリップできることをおそらくすでにご存知でしょう。たとえば、円形のクリップを設定して、シーンを通常どおり描画すると、アプリケーション上でだれかが懐中電灯を照らしているような印象を与えられます。Java 2D のクリッピングを復習するには、『Java Tutorial』を参照してください。

複雑な形状を使用してクリップすると、一般的に、クリップした領域の縁に「ジャギー」が見られます。ジャギーとは、描画したイメージの見苦しいギザギザの縁です。「ハードクリッピング」と呼ぶことにするこの効果を図示するために、次の例を試してください。

// Clear the background to black
g.setColor(Color.BLACK);
g.fillRect(0, 0, width, height);

// Set the clip shape
g.clip(new java.awt.geom.Ellipse2D.Float(width/4, height/4, width/2, height/2));

// Fill the area with a gradient; this will be clipped by our ellipse, but
// note the ugly jaggies
g.setPaint(new GradientPaint(0, 0, Color.RED, 0, height, Color.YELLOW));
g.fillRect(0, 0, width, height);
    

この結果、次のようなイメージが描画されます。

フレーム 1

図 1:クリッピングによって生じるなめらかさのないギザギザの縁

このようななめらかさのない縁をアンチエイリアス処理して、クリッピングによって生じるジャギーを取り除けたら嬉しくありませんか。残念ながら、Sun の Java 2D の現在の実装は、「ソフトクリッピング」をサポートしていません。上記のコードを Mac で試したところ、驚くべきことにジャギーができませんでした。どうなっているのでしょうか。Apple の Java 2D 実装は、Quartz を内部で使用しており、Quartz は、デフォルトでソフトクリッピングを実行しているようだということが判明しました。Apple は、Java SE 6 では、Quartz レンダラではなく Sun のソフトウェアレンダラをデフォルトで使用する予定なので、この Tech Tip は Mac にも適用できるはずです。

RenderingHint オブジェクトがこの動作を制御してくれるのではと期待されるかもしれませんが、残念ながら、そうではありません。過去、2、3 人の開発者がソフトクリッピングを求めましたが、Sun の実装でサポートの追加を保証するほど一般的ではないようです。

中間イメージ (このテーマに関する Chet Haases の記事を参照) および SrcAtop と呼ばれる、あまり知られていない AlphaComposite ルールを使用して、ソフトクリッピング効果を実現するかなり簡単な方法を発見できたのは幸運でした。この例では SrcIn も同じように有効ですが、SrcAtop には、単に置き換えるのではなく、ソースとターゲットをブレンド処理するという追加利点があります。次のコードの抜粋を試してください。

import java.awt.image.*;

// Clear the background to black
g.setColor(Color.BLACK);
g.fillRect(0, 0, width, height);

// Create a translucent intermediate image in which we can perform
// the soft clipping
GraphicsConfiguration gc = g.getDeviceConfiguration();
BufferedImage img = gc.createCompatibleImage(width, height, Transparency.TRANSLUCENT);
Graphics2D g2 = img.createGraphics();

// Clear the image so all pixels have zero alpha
g2.setComposite(AlphaComposite.Clear);
g2.fillRect(0, 0, width, height);

// Render our clip shape into the image.  Note that we enable
// antialiasing to achieve the soft clipping effect.  Try
// commenting out the line that enables antialiasing, and
// you will see that you end up with the usual hard clipping.
g2.setComposite(AlphaComposite.Src);
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2.setColor(Color.WHITE);
g2.fillOval(width/4, height/4, width/2, height/2);

// Here's the trick... We use SrcAtop, which effectively uses the
// alpha value as a coverage value for each pixel stored in the
// destination.  For the areas outside our clip shape, the destination
// alpha will be zero, so nothing is rendered in those areas.  For
// the areas inside our clip shape, the destination alpha will be fully
// opaque, so the full color is rendered.  At the edges, the original
// antialiasing is carried over to give us the desired soft clipping
// effect.
g2.setComposite(AlphaComposite.SrcAtop);
g2.setPaint(new GradientPaint(0, 0, Color.RED, 0, height, Color.YELLOW));
g2.fillRect(0, 0, width, height);
g2.dispose();

// Copy our intermediate image to the screen
g.drawImage(img, 0, 0, null);
    

図 2 の結果イメージを、先に示したジャギーなものと比較してください。

アンチエイリアス処理したソフトクリップ
図 2:アンチエイリアス処理によるソフトクリッピング効果の実現

こちらの方がきれいですよね。この例は不自然であるため、実世界にどのように適用できるかわかりづらいかもしれません。しかし、次のヒントでは、任意の形状のライティング効果を作成するときにこの手法を適用する方法を紹介します。

Java 2D の光と影

最初のヒントでは、ソフトクリッピング効果を実現する手法を実際に示しました。今度は、この効果を十分利用しましょう。このヒントでは、効果なしでは平坦な形状に 3 次元の見た目を与えるために、ライティング効果を追加する方法を紹介します。

文章を理解してもらうには、画像化するのがよいでしょう。退屈で平坦な左の形状を少し魅力を増したつやつやした右の形状に変化させる方法を紹介します。

平坦なイメージいきいきしたイメージ
図 3:色を使用したいきいきした効果のシミュレーション

適切な色を使用して、この手法で、形状全体を照らす色の付いた光の輝きをシミュレートして、微妙な輝きを生むことができます。この効果はどのように実現するのでしょうか。次のコードをチェックしてください。PaintBorderGlow() メソッドの上にあるコメントは、中心となるアプローチをもう少し詳細に説明しています。

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

private static final Color clrHi = new Color(255, 229, 63);
private static final Color clrLo = new Color(255, 105, 0);

private static final Color clrGlowInnerHi = new Color(253, 239, 175, 148);
private static final Color clrGlowInnerLo = new Color(255, 209, 0);
private static final Color clrGlowOuterHi = new Color(253, 239, 175, 124);
private static final Color clrGlowOuterLo = new Color(255, 179, 0);

private Shape createClipShape() {
    float border = 20.0f;

    float x1 = border;
    float y1 = border;
    float x2 = width - border;
    float y2 = height - border;

    float adj = 3.0f; // helps round out the sharp corners
    float arc = 8.0f;
    float dcx = 0.18f * width;
    float cx1 = x1-dcx;
    float cy1 = 0.40f * height;
    float cx2 = x1+dcx;
    float cy2 = 0.50f * height;

    GeneralPath gp = new GeneralPath();
    gp.moveTo(x1-adj, y1+adj);
    gp.quadTo(x1, y1, x1+adj, y1);
    gp.lineTo(x2-arc, y1);
    gp.quadTo(x2, y1, x2, y1+arc);
    gp.lineTo(x2, y2-arc);
    gp.quadTo(x2, y2, x2-arc, y2);
    gp.lineTo(x1+adj, y2);
    gp.quadTo(x1, y2, x1, y2-adj);
    gp.curveTo(cx2, cy2, cx1, cy1, x1-adj, y1+adj);
    gp.closePath();
    return gp;
}

private BufferedImage createClipImage(Shape s) {
    // Create a translucent intermediate image in which we can perform
    // the soft clipping
    GraphicsConfiguration gc = g.getDeviceConfiguration();
    BufferedImage img = gc.createCompatibleImage(width, height, Transparency.TRANSLUCENT);
    Graphics2D g2 = img.createGraphics();

    // Clear the image so all pixels have zero alpha
    g2.setComposite(AlphaComposite.Clear);
    g2.fillRect(0, 0, width, height);

    // Render our clip shape into the image.  Note that we enable
    // antialiasing to achieve the soft clipping effect.  Try
    // commenting out the line that enables antialiasing, and
    // you will see that you end up with the usual hard clipping.
    g2.setComposite(AlphaComposite.Src);
    g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
    g2.setColor(Color.WHITE);
    g2.fill(s);
    g2.dispose();

    return img;
}

private static Color getMixedColor(Color c1, float pct1, Color c2, float pct2) {
    float[] clr1 = c1.getComponents(null);
    float[] clr2 = c2.getComponents(null);
    for (int i = 0; i < clr1.length; i++) {
        clr1[i] = (clr1[i] * pct1) + (clr2[i] * pct2);
    }
    return new Color(clr1[0], clr1[1], clr1[2], clr1[3]);
}

// Here's the trick... To render the glow, we start with a thick pen
// of the "inner" color and stroke the desired shape.  Then we repeat
// with increasingly thinner pens, moving closer to the "outer" color
// and increasing the opacity of the color so that it appears to
// fade towards the interior of the shape.  We rely on the "clip shape"

// having been rendered into our destination image already so that
// the SRC_ATOP rule will take care of clipping out the part of the
// stroke that lies outside our shape.
private void paintBorderGlow(Graphics2D g2, int glowWidth) {
    int gw = glowWidth*2;
    for (int i=gw; i >= 2; i-=2) {
        float pct = (float)(gw - i) / (gw - 1);

        Color mixHi = getMixedColor(clrGlowInnerHi, pct,
                                    clrGlowOuterHi, 1.0f - pct);
        Color mixLo = getMixedColor(clrGlowInnerLo, pct,
                                    clrGlowOuterLo, 1.0f - pct);
        g2.setPaint(new GradientPaint(0.0f, height*0.25f,  mixHi,
                                      0.0f, height, mixLo));
        //g2.setColor(Color.WHITE);

        // See my "Java 2D Trickery: Soft Clipping" entry for more
        // on why we use SRC_ATOP here
        g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, pct));
        g2.setStroke(new BasicStroke(i));
        g2.draw(clipShape);
    }
}

Shape clipShape = createClipShape();
//Shape clipShape = new Ellipse2D.Float(width/4, height/4, width/2, height/2);

// Clear the background to white
g.setColor(Color.WHITE);
g.fillRect(0, 0, width, height);

// Set the clip shape
BufferedImage clipImage = createClipImage(clipShape);
Graphics2D g2 = clipImage.createGraphics();

// Fill the shape with a gradient
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2.setComposite(AlphaComposite.SrcAtop);
g2.setPaint(new GradientPaint(0, 0, clrHi, 0, height, clrLo));
g2.fill(clipShape);

// Apply the border glow effect
paintBorderGlow(g2, 8);

g2.dispose();
g.drawImage(clipImage, 0, 0, null);

上記の例では、「代替」コード行を 2、3 行コメントにして残してあります。これらの行は自由にコメント解除して、それらの行が描画にどのような影響を与えるのかを確認してください。

ボーナス: 鋭い読者は、上記の paintBorderGlow() メソッドで使用されているのと同じ手法を使用して、形状の周りに影を落とせることに気付いたかもしれません。これはどのような仕組みかに興味が湧くことでしょう。でも時間切れです。形状の上に境界を描画する代わりに (クリップにより、ストロークは形状の内部にしか接触しない)、あらかじめ形状の周りに変化するグレーの境界を描画できます。つまり、影のストロークは形状の外側に表示されます。影のストロークの内側部分は、事実上、形状が重ねられます。

同じ形状に影の境界を追加するために、上記の例に挿入できるコードをもう少し示します。

private void paintBorderShadow(Graphics2D g2, int shadowWidth) {
    g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                        RenderingHints.VALUE_ANTIALIAS_ON);
    int sw = shadowWidth*2;
    for (int i=sw; i >= 2; i-=2) {
        float pct = (float)(sw - i) / (sw - 1);
        g2.setColor(getMixedColor(Color.LIGHT_GRAY, pct,
                                  Color.WHITE, 1.0f-pct));
        g2.setStroke(new BasicStroke(i));
        g2.draw(clipShape);
    }
}

// Apply the border shadow before we paint the rest of the shape
paintBorderShadow(g, 6);
    

結果イメージは、次のようになります。

影
図 4:Java 2D を使用した影の追加

これは、影の追加を実演するための、簡単な試みでしかありません。筆者が怠惰でなければ、より明るいグレーと直線ではない斜面を使用して、より現実的な効果を実現したでしょう。また、これは、Java 2D を使用して影を追加する多くの方法の 1 つでしかありません。Romain Guy が、2 つのブログエントリ、『Non Rectangular Shadows』と『Fast or Good Drop Shadows』で別の影の実装について説明しています。SwingLabs のスタッフは、SwingX プロジェクトで DropShadowBorder を使用しており、DropShadowPanel も現在準備中です。

関連情報

Java 2D および Java Platform の詳細情報は、次のソースから入手できます。