/* Javaで書いた、画像ビュワーです。 http://funini.com/kei/ivy/ */ import java.awt.*; import java.awt.event.*; import java.io.*; import java.net.URL; import java.net.URLConnection; import javax.imageio.ImageIO; import javax.swing.JApplet; import javax.swing.JPanel; import javax.swing.Timer; import java.util.ArrayList; import java.util.regex.*; /** 各種定数の宣言 */ class Env { public static int W, H, BTN_W; public static int T_MAX; public static int REFRESH; public static double WH_RATIO; public static String URL, BACKGROUND_URL; } /******************************************************************************* * Ivylet: アプレットのクラス。今回のメイン、IvyPanel が載っている。 ******************************************************************************/ public class Ivylet extends JApplet { public void init() { loadParameter(); String[] imgPaths = extractPaths(Env.URL); IvyPanel tp = new IvyPanel(imgPaths); setContentPane(tp); new Timer(Env.REFRESH, tp).start(); } /** 各種パラメータのデフォルト値を設定 */ void loadParameter() { Env.W = getIntParameter("width", 800); Env.H = getIntParameter("height", 600); Env.BTN_W = getIntParameter("button_width", 30); Env.T_MAX = getIntParameter("t_max", 20); Env.WH_RATIO = getDoubleParameter("wh_ratio", 1.333333); Env.REFRESH = getIntParameter("refresh", 15); Env.URL = getParameter("url", "http://funini.com/kei/ivy/narita/"); Env.BACKGROUND_URL = getParameter("background_url", "http://funini.com/kei/ivy/imgs/back.jpg"); } /** タグから String のパラメータを取得。失敗時はデフォルト値を返す */ String getParameter(String key, String defval) { String s = getParameter(key); return (s == null) ? defval : s; } /** タグから int のパラメータを取得。失敗時はデフォルト値を返す */ int getIntParameter(String key, int defval) { try { String s = getParameter(key); return (s == null) ? defval : Integer.parseInt(s); } catch (Exception e){ return defval; } } /** タグから、double のパラメータを取得。失敗時はデフォルト値を返す */ double getDoubleParameter(String key, double defval) { try { String s = getParameter(key); return (s == null) ? defval : Double.parseDouble(s); } catch (Exception e) { return defval; } } /** 指定されたパスから、画像の 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: メインのパネル。画像を描画したり、クリック時の動作を記述したクラス。 ******************************************************************************/ class IvyPanel extends JPanel implements ActionListener, MouseListener { Button button; // ボタンです。クリックすると表示モードが変わります Shadow shadow; // 全体に被せる影です。モードが変わるときに描画されます。 Background back; // 背景です。 IconList iconList; // 各画像の重ね順です。 boolean listmode; // 表示モードです。「一覧表示(true)」と「重ね表示(false)」があります public IvyPanel(String[] urls) { /** コンストラクタ 引数は画像が入っているパス */ setBackground(new Color(0x33, 0x33, 0x33)); // 背景色設定 setPreferredSize(new Dimension(Env.W, Env.H)); // サイズ設定 button = new Button(0, Env.H - Env.BTN_W, 210, Env.BTN_W); // ボタンを配置 shadow = new Shadow(Env.W, Env.H); // 影作成 back = new Background(Env.BACKGROUND_URL, Env.W, Env.H); iconList = new IconList(urls); listmode = false; addMouseListener(this); // マウスのイベントを扱えるようにする } public synchronized void mousePressed(MouseEvent e) { /** マウスがクリックされたときのイベント */ Point p = e.getPoint(); if(button.hit(p)) { // ボタンがクリックされた場合 listmode = !listmode; // 一覧表示 <=> 重ね表示を切り替え initView(); // 初期表示 return; } // ボタン以外がクリックされた時 for(int i = iconList.size() - 1; i >= 0; i--) { // クリックは表示と逆順で調べる Icon icon = iconList.get(i); // i 番目のアイコンについて if(icon.hit(p)){ // クリックされている場合 if(listmode) iconList.listTop(icon); // icon をトップに表示 (一覧表示) else iconList.piledTop(icon); // icon をトップに表示 (重ね表示) return; } } // ボタンもアイコンもクリックされていない場合、初期表示に戻る initView(); } void initView() { /** 初期表示を行う */ if(listmode) iconList.list(); // 一覧表示 else iconList.pile(); // 重ね表示 shadow.animationStart(); // 影を表示 } /** タイマイベントが発生したら、時間を進める */ public void actionPerformed(ActionEvent ae) { addTime(); } /** 時間を進める (何度も呼ばれると困るから、synchronized にしました) */ synchronized void addTime() { for(int i = 0; i < iconList.size(); i++) // 全てのアイコンについて iconList.get(i).addTime(); // 時間を進める shadow.addTime(); // 影も時間を進める repaint(); // 再描画 } /** 描画を行う */ public void paintComponent(Graphics g) { super.paintComponent(g); // ウィンドウを表示 Graphics2D g2 = (Graphics2D) g; // アンチエイリアスの設定など g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); back.draw(g2); // 背景を表示 button.draw(g2); // ボタンを表示 for(int i = 0; i < iconList.size(); i++) iconList.get(i).draw(g2); // 各アイコンを表示 shadow.draw(g2); // 影を表示 } /** MouseListener を実装するために必要なメソッド群 */ public void mouseReleased(MouseEvent e) {} public void mouseEntered(MouseEvent e) {} public void mouseExited(MouseEvent e) {} public void mouseClicked(MouseEvent e) {} } /******************************************************************************* * IconList: アイコンのリスト、重ね順、位置リストなどを保持するクラス ******************************************************************************/ class IconList { int nIcons; // アイコンの数 Icon[] icons; // アイコン (画像1枚につき、一つのアイコンです) int[] orders; ListPosition listPos; // 一覧表示した時の各画像の位置 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);// 一覧表示での位置を計算 listPos = new ListPosition(nIcons); // 重ね表示での位置を計算 pile(); // 初期表示(重ね表示)に移行 } /** 一覧表示にアイコンを移動 */ public void list() { for(int i = 0; i < nIcons; i++) { orders[i] = nIcons - i - 1; // 順番に重ねる icons[i].setDestination(listPos.getPos(i)); // アイコンの目的地をセットする } } /** 重ね表示にアイコンを移動 */ 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 listTop(Icon ic) { int index = searchIcon(ic); if(index != orders[nIcons - 1]) icons[orders[nIcons - 1]].setDestination(listPos.getPos(orders[nIcons - 1])); icons[index].setDestination(listPos.getZoomedPos(index)); for(int i = searchOrder(index) + 1; i < nIcons; i++) orders[i - 1] = orders[i]; orders[nIcons - 1] = index; } /** 重ね表示において、指定されたアイコンをトップに移動 */ 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, PiledPosition, ListPosition: * アイコンの位置を持つクラス。PiledPositionが重ね表示、ListPositionが一覧表示。 ******************************************************************************/ 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); } } } 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 }; } } /** 一覧表示の時の位置情報を持つクラス */ class ListPosition extends IconPosition { int[] ZX, ZY; // ズームした後の位置 int zw, zh; // ズームした後の最大幅、高さ public ListPosition(int n) { super(n); System.err.println("n=" + n); // 幅と高さを決める int availableH = Env.H - Env.BTN_W; double r = Math.sqrt(Env.W * availableH * Env.WH_RATIO / n); int nw = (int) Math.round(Env.W / r); // 横に並ぶ画像数 int nh = (int) Math.ceil((double) n / nw); setWH((int)(0.93 * Env.W / nw),(int)(0.93 * Env.H / nh)); // 一覧表示の X 座標、Y 座標を決める int marginX = (Env.W - (nw * w)) / (nw + 1); int marginY = (availableH - (nh * h)) / (nh + 1); int index = 0; outer:for(int cntY = marginY; cntY < availableH; cntY += marginY + h){ for(int cntX = marginX; cntX < Env.W; cntX += marginX + w){ X[index] = cntX; Y[index] = cntY; if(++index == n) break outer; } } // ズーム時の位置を計算する calcZ(n); } /** ズーム後の X 座標、Y 座標を決める。 * もし画面からはみ出るときは、適宜調整する */ void calcZ(int n){ ZX = new int[n]; ZY = new int[n]; zw = Env.W / 2; // ズーム後の幅 zh = (int) (zw / Env.WH_RATIO); for(int index = 0; index < n; index++){ ZX[index] = limitTopBottom(0, X[index] - (zw - w) / 2, Env.W - zw); ZY[index] = limitTopBottom(0, Y[index] - (zh - h) / 2, Env.H - zh); } } /** v が v_min 未満なら v_mim、v_max 以上なら v_max、それ以外は v を返す */ int limitTopBottom(int vMin, int v, int vMax) { return (vMin >= v) ? vMin : (vMax <= v) ? vMax : v; } /** ズームされた後の位置を返す */ public int[] getZoomedPos(int i) { return new int[] { ZX[i], ZY[i], zw, zh }; } } /******************************************************************************* * Rect, Icon, MyBtn, Background, Shadow: * 様々な長方形のGUI部品。位置・大きさ・アニメーション用の変数と draw() を備える ******************************************************************************/ /** ベースになる、長方形クラス */ 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); } } /** アイコンクラス。1枚の写真の情報を持ちます */ 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); } } /** 左下のボタン。ボタンクリック時の動作は、IvyPanel.mousePressed() にある */ class Button extends Rect { Font font; public Button(int x, int y, int w, int h) { super(x, y, w, h); font = new Font("Century Gothic", Font.PLAIN, 24); col = new Color(0f, 0f, 0f, 0.5f); } /** ボタンを描画する */ public void draw(Graphics2D g) { g.setColor(col); g.fillRect(P[0], P[1], P[2], P[3]); g.setColor(Color.white); g.setFont(font); g.drawString("Mode Change", P[0] + 10, P[1] + P[3] - 8); } } /** 画面全体を暗くする影 */ class Shadow extends Rect { public Shadow(int w, int h) { super(0, 0, w, h); } public void draw(Graphics2D g) { /** 影を全体に描画する */ if(t == Env.T_MAX) return; // 影が終了していれば return // 透明度を計算する float alpha = (float)(0.5 - 2 * Math.pow(state - 0.5, 2)); g.setColor(new Color(0.6f, 0.4f, 0.2f, alpha)); // 全体を塗りつぶす g.fillRect(0, 0, Env.W, Env.H); } } /** 背景 (指定された画像を並べて表示) */ class Background extends Rect { /** コンストラクタ。引数: 画像のURL(またはパス)、幅、高さ*/ public Background(String url, int w, int h) { super(0, 0, w, h); loadImage(url); } /** img にロードされた画像を、指定された幅・高さを満たすよう敷き詰める */ public void draw(Graphics2D g) { int n = (int) Math.ceil((double) P[2] / imgW); int m = (int) Math.ceil((double) P[3] / imgH); for(int i = 0; i < n; i++) for(int j = 0; j < m; j++) g.drawImage(img, i * imgW, j * imgH, null); } } 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; } }