[an error occurred while processing this directive] [an error occurred while processing this directive]

(4) アニメーションでふわっと場所が変わる

前回はマウスでクリックすることで、奥の方に重なった写真を手前に移動できるようにします。
今回は、場所が変わるときにふわっとなめらかに変わるようにします。 前回 から変わったのは、以下の点です。 なお、新しいクラスは増えていません。

Env: 色々な定数を入れるクラス

定数をまとめたクラスです。時間関係の変数が少し増えました。
T_MAX は、一回のアニメーションが終わるまでのステップ数です。 REFRESH は、アニメーションのフレーム数(1秒間に何回呼び出されるか)です。1秒間に15回ってことですが、ほとんどの環境ではそれより少ない回数になってしまうと思います。
class Env {
    public static int    W, H;
    public static int    T_MAX;
    public static int    REFRESH;
    public static double WH_RATIO;
    public static String URL;

    /** 各種パラメータのデフォルト値を設定 */
    static {
        W = 800;
        H = 600;
        T_MAX = 20;
        REFRESH = 15;
        WH_RATIO = 1.333333;
        URL = "http://funini.com/kei/ivy/narita/";
    }
}

Ivylet: メインのアプレットのクラス

アプレットのメインのクラスです。タイマが追加されました。
new Timer()の中にリフレッシュ回数 (15回) とパネルを入れてあるので、 1/15秒に一回、IvyPanel の actionPerformed が呼び出されます。
public class Ivylet extends JApplet {
    public void init() {
        String[] imgPaths = extractPaths(Env.URL);
        IvyPanel tp = new IvyPanel(imgPaths);
        setContentPane(tp);
        new Timer(Env.REFRESH, tp).start();
    }

 
    /** 指定されたパスから、画像の URL 一覧を取得 */
    public String[] extractPaths(String urlStr) {
        // urlBase には、URLの / 以前の部分が入る ( http://funini.com/kei/ など)
        String urlBase = urlStr.substring(0, urlStr.lastIndexOf("/") + 1);
        ArrayList files = new ArrayList();
        try {
            BufferedReader br = new BufferedReader(new InputStreamReader(
                    new URL(urlStr).openConnection().getInputStream()));
            extractFiles(files, urlBase, br);
        } catch (IOException ie) {
            ie.printStackTrace();
        }
        return (String[]) files.toArray(new String[0]);
    }

    /** br から全体を読み込み、画像の URL 一覧を取得 */
    void extractFiles(ArrayList files, String urlBase, BufferedReader br) throws IOException{
        Pattern pattern = Pattern.compile("^(.+\\.(jpg|JPG))<.*");
        for(;;) {
            String line = br.readLine();
            if(line == null) break;
            String[] targets = line.split(">");
            for(int i = 0; i < targets.length; i++) {
                Matcher m = pattern.matcher(targets[i]);
                if(m.matches()) files.add(urlBase + m.group(1));
            }
        }
    }
}

IvyPanel: メインのパネル

タイマで actionPerformed() を呼び出せるようにするため、 ActionListener を実装します。 actionPerformed()の中では、もっと色々なactionが起こる (例えば GUI 部品のクリック)場合は 分岐が必要ですが、今回はタイマのイベントしか起こらないので、直接addTime()を呼んでいます。 IvyPanel.addTime() は、さらに全てのアイコンについて時間を進めるように、各アイコンの Icon.addTime()を呼びます。
class IvyPanel extends JPanel implements ActionListener, MouseListener {
    IconList    iconList; // 各画像の重ね順です。

    public IvyPanel(String[] urls) {
        /** コンストラクタ 引数は画像が入っているパス */
        setBackground(new Color(0x33, 0x33, 0x33)); // 背景色設定
        setPreferredSize(new Dimension(Env.W, Env.H)); // サイズ設定

        iconList = new IconList(urls);
        addMouseListener(this); // マウスのイベントを扱えるようにする
    }

    public synchronized void mousePressed(MouseEvent e) {
        /** マウスがクリックされたときのイベント */
        Point p = e.getPoint();

        // ボタン以外がクリックされた時
        for(int i = iconList.size() - 1; i >= 0; i--) { // クリックは表示と逆順で調べる
            Icon icon = iconList.get(i); // i 番目のアイコンについて
            if(icon.hit(p)){ // クリックされている場合
                iconList.piledTop(icon); // icon をトップに表示 (重ね表示)
                return;
            }
        }
        // ボタンもアイコンもクリックされていない場合、初期表示に戻る
        initView();
    }

    void initView() {
        /** 初期表示を行う */
        iconList.pile(); // 重ね表示
    }

    /** タイマイベントが発生したら、時間を進める */
    public void actionPerformed(ActionEvent ae) 
    {   addTime(); }

    /** 時間を進める (何度も呼ばれると困るから、synchronized にしました) */
    synchronized void addTime() {
        for(int i = 0; i < iconList.size(); i++) // 全てのアイコンについて
            iconList.get(i).addTime(); // 時間を進める
        repaint(); // 再描画
    }

    /** 描画を行う */
    public void paintComponent(Graphics g) {
        super.paintComponent(g); // ウィンドウを表示
        Graphics2D g2 = (Graphics2D) g; // アンチエイリアスの設定など
        g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                RenderingHints.VALUE_ANTIALIAS_ON);
        for(int i = 0; i < iconList.size(); i++)
            iconList.get(i).draw(g2); // 各アイコンを表示
    }

    /** MouseListener を実装するために必要なメソッド群 */
    public void mouseReleased(MouseEvent e) {}
    public void mouseEntered(MouseEvent e) {}
    public void mouseExited(MouseEvent e) {}
    public void mouseClicked(MouseEvent e) {}
}

IconList: アイコンのリスト、重ね順、位置リストなどを保持するクラス

クリック時に場所を変えるのが、いままではいきなり場所を変えていた setPosition() のを、setDestination() にして、 「とりあえず目的地をセットして、そこまでなめらかにつなぐ」ようにします。setDestination() の詳細は Icon クラスに書いてあります。
class IconList {
    int           nIcons;       // アイコンの数
    Icon[]        icons;   // アイコン (画像1枚につき、一つのアイコンです)
    int[]         orders;
    PiledPosition piledPos; // 重ねた表示の時の各画像の位置

    public IconList(String[] imgPaths) {
        /** 引数: ロードされた画像リスト */
        nIcons = imgPaths.length;
        icons = new Icon[nIcons];
        for(int i = 0; i < nIcons; i++)
            icons[i] = new Icon(imgPaths[i]);
        orders = new int[nIcons];
        piledPos = new PiledPosition(nIcons);// 一覧表示での位置を計算
        pile();                               // 初期表示(重ね表示)に移行
    }

    /** 重ね表示にアイコンを移動 */
    public void pile() {
        for(int i = 0; i < nIcons; i++) {
            // 重ね順を設定。orders は描画順なので、一番新しい画像を最後にする
            orders[i] = nIcons - i - 1; 
            icons[i].setDestination(piledPos.getPos(i));
        }
    }

    /** アイコンのインデックス(何番目のアイコンか)を返す */
    int searchIcon(Icon ic) {
        for(int i = 0; i < nIcons; i++)
            if(icons[i] == ic) return i;
        return -1; // 見つからなかった場合
    }

    /** 指定されたインデックスが何番目に重なっているかを返す */
    int searchOrder(int index) {
        for(int i = 0; i < nIcons; i++)
            if(orders[i] == index) return i;
        return -1; // 見つからなかった場合
    }

    /** 重ね表示において、指定されたアイコンをトップに移動 */
    public void piledTop(Icon ic) {
        int index = searchIcon(ic);
        // 指定されたアイコンより手前のものは、画面外に移動   
        for(int i = 0; i < index; i++)
            icons[i].setDestination(piledPos.getOuterPos()); 
        // 指定されたアイコン以後のものを手前から順番に並べる   
        for(int i = index; i < nIcons; i++)
            icons[i].setDestination(piledPos.getPos(i - index));
    }

    /** アイコンの数を返すメソッド */
    public int size() { return nIcons; }

    /** 重ね順が(下から) i 番目のアイコンを返すメソッド */
    public Icon get(int i){ return icons[orders[i]]; }
}

IconPosition: アイコンの位置を持つクラス

前回から変化なしです。
abstract class IconPosition {
    /** 各アイコンの配置を示すクラス */
    int[] X, Y; // アイコンの X, Y, W, H
    int   w, h;

    public IconPosition(int n) {
        /** 各アイコンの座標、大きさ */
        X = new int[n];
        Y = new int[n];
    }

    /** 指定されたインデックスのアイコン位置を返す */
    public int[] getPos(int i) 
    {   return new int[] { X[i], Y[i], w, h }; }

    /** 指定された幅/高さ 最大値を超えないように、幅・高さを設定する */
    public void setWH(int wMax, int hMax) {
        w = wMax; // アイコンの(最大)幅
        h = (int) ((double) w / Env.WH_RATIO); // アイコンの(最大)高さ
        if(h > hMax) { // 幅を基準にしたら、高さがhMaxをはみ出てしまった場合
            h = hMax;
            w = (int) (h * Env.WH_RATIO);
        }
    }
}

PiledPosition: 重ね表示 の時の位置情報を持つクラス

変化なしです。
class PiledPosition extends IconPosition {
    /** 重ね表示 の時の位置情報を持つクラス */
    int zw, zh;
    int outerX, outerY;

    /** コンストラクタ (引数: 画像の数) */
    public PiledPosition(int n) {
        super(n);
        setWH(Env.W / 2, Env.H / 2);

        X[0] = Env.W / 2 - Env.W / 10; // 一番手前のアイコンのX座標
        Y[0] = Env.H / 2 - Env.H / 10; // 一番手前のアイコンのY座標
        for(int i = 1; i < n; i++) {
            X[i] = X[i - 1] - (int) ((10 - i) * (10 - i) * 0.9);
            Y[i] = Y[i - 1] - (int) ((10 - i) * (10 - i) * 0.4 + 20);
        }
        outerX = Env.W;
        outerY = Env.H;
    }

    /** フォーカスアウトして、画面からはみ出た位置を返す */
    public int[] getOuterPos() 
    {   return new int[] { outerX, outerY, w, h }; }
}

Rect: パネルに置く色んな部品の基本クラス

時間経過に関する変数がいろいろ加わりました。ここの変更はけっこうポイントです。
まず、変数 t は、0から Env.T_MAX まで1ずつ増えていきます。例えば後に出てくる Icon クラスでは、t が 0 の時には初期位置で、 t が T_MAX の時には目的地に着くようにしています。t は一定の間隔で1ずつ増えていきますが、動きは「ふわっ」とさせるために、始めたくさん動いて、だんだん t が T_MAX に近づくにつれて動きをゆっくりにしています。これを実現するために、state という変数を作りました。これは0 から 1 の間で動いて、初めはたくさん動き、1に近づくとゆっくり動きます。以下の式が、t と state の換算式です。
    state = 1 - Math.pow(((double) t / Env.T_MAX - 1), 2);
では、クラスの中身を見てみます。
abstract class Rect {
    int[]   P;     // X,Y座標, 幅、高さ
    int     t;     // 経過時間 (0から Env.E_MAX まで)
    double  state; // 経過時間を0.0 - 1.0の割合にしたもの。
                   //ふわっとした感じはこの state の更新がポイント
    Image   img;
    int     imgW, imgH; // 画像の幅、高さ
    Color   col;

    public Rect(int x, int y, int w, int h) {
        P = new int[] { x, y, w, h };
        animationEnd();
    }

    /** 指定された座標 p が、この長方形の内部かどうかを返す */
    public boolean hit(Point p) {
        return (P[0] <= p.x && p.x <= P[0] + P[2] 
              && P[1] <= p.y && p.y <= P[1] + P[3]);
    }

    /** アニメーションを開始状態にする */
    public void animationStart() { t = 0; state = 0.0; }

    /** アニメーションを終了状態にする */
    public void animationEnd() { t = Env.T_MAX; state = 1.0; }

    /** 時間を進める */
    public void addTime() {
        if(t >= Env.T_MAX) return;
        t++;
        state = 1 - Math.pow(((double) t / Env.T_MAX - 1), 2);
    }

    /** 画像をロード。path がhttp:// で始まる場合、HTTPで取得する */
    public void loadImage(String path){ 
        img = ImageLoader.load(path); 
        imgW = img.getWidth(null);
        imgH = img.getHeight(null);
    }
}

Icon: アイコンクラス

アイコンの現在位置 (P) のほかに、初期位置、終了位置を表す変数が増えました。
また、setDestination() で今の位置を初期位置に、終了位置をセットします。 さらにaddTime では、state の値に応じて初期位置と終了位置の間に現在位置を設定します。 例えば、state = 0.5 だと、現在位置は初期位置と終了位置のちょうど真ん中になります。
class Icon extends Rect {
    int[] dest, orig; // 移動先と移動元のx, y, w, hを持つ配列
    public Icon(String imgPath) {
        super(0, 0, 0, 0);
        loadImage(imgPath);
        col = new Color(0.85f, 0.85f, 0.75f, 0.3f);
        dest = new int[] { 0, 0, 0, 0 };
        orig = new int[] { 0, 0, 0, 0 };
    }

    /** アニメーションの目的地を設定 */
    public void setDestination(int[] A) {
        for(int i = 0; i < 4; i++) {
            orig[i] = P[i];
            dest[i] = A[i];
        }
        animationStart(); // 時間変数 t,state を初期化
    }

    /** 時間を進め、state に応じて、このオブジェクトを移動させる。 
     * 移動元は orig, 移動先は dest。*/
    public void addTime() {
        super.addTime();
        for(int i = 0; i < 4; i++)
            P[i] = (int) (orig[i] * (1 - state) + dest[i] * state);
    }

    /** アイコンを描画する */
    void draw(Graphics2D g) {
        // まずは、画像が半分以上画面外にはみ出ている場合は描画しない
        if(P[0] + P[2] / 2 < 0 || P[1] + P[3] / 2 < 0 
                || Env.W <= P[0] || Env.H <= P[1]) return;
        g.setColor(col);
        // アイコンの周りの、少し透明な枠を描画する
        g.fillRect(P[0], P[1], P[2], P[3]);
        // 少し余白をあけて、アイコンを描画する
        int marginX = P[2] / 30;
        int marginY = P[3] / 30;
        g.drawImage(img, P[0] + marginX, P[1] + marginY, P[0] + P[2] - marginX,
                P[1] + P[3] - marginY, 0, 0, imgW, imgH, null);
    }
}

ImageLoader: 画像を読み込むためのクラス

前回から変更無しです。
class ImageLoader {
    /** 画像をロード。path がhttp:// で始まる場合、HTTPで取得する */
    public static Image load(String path) {
        try {
            if(path.startsWith("http://")){
                URLConnection urlc = new URL(path).openConnection();
                return ImageIO.read(urlc.getInputStream());
            }
            return ImageIO.read(new File(path));
        } catch (IOException ie) { ie.printStackTrace();}
        return null;
    }
}
[an error occurred while processing this directive]