fisheye view の計算式とプログラム

fisheye view とは、なんかインターフェイスの世界では常識っぽい、フォーカスとなる点を中心に座標をぐにょーんと引き延ばす方法です。日本語が不自由ですみません。要するにこういう変換です。

皇居あたりを中心に線路地図をぐにょーんと引き延ばしています。これを実装しようと思って計算式やサンプルプログラムを探したのですが、意外に情報が少なくて手間取りました。なので記録を残しておきます。

種類

参考文献 *1 を眺めたところ、cartesian fisheye と polar fisheye の二種類があるようです。左が cartesian で右が polar です。でもこの例だとほとんど区別が付かないですね。よく見ると端っこの方のつぶれ方が違います。

cartesian fisheye view

フォーカスの座標を (0, \quad 0) 、引き延ばしたい点の座標を (x, \quad y) 、壁の位置を (x_{\mathit{max}}, \quad y_{\mathit{max}}) とするとき (0 \, \le \, x \le \, x_{max} になる) 、引き延ばし後の座標は以下になります。

\Bigl( \frac{(d+1)xx_{\mathit{max}}}{dx+x_{\mathit{max}}}, \quad \frac{(d+1)yy_{\mathit{max}}}{dy+y_{\mathit{max}}} \Bigr)

d は引き延ばし度のパラメータです。0 だと変化せず、大きいほどいっぱい引き延ばされます。x と y が独立に決まるあたりが cartesian (デカルト座標) です。フォーカスが原点以外であることを考慮してプログラムを書いたらこんな感じ。

# 画面の大きさ
$width, $height = [300, 300]

# フォーカス
$x_focus, $y_focus = [100, 100]

# 引き延ばし度 (0 だと変化なし、大きいほどいっぱい引き延ばす)
$factor = 8

# (x, y) を cartesian fisheye view でずらす
def cartesian_fisheye(x, y)
  x2 = cartesian_fisheye_aux(x, $x_focus, 0, $width )
  y2 = cartesian_fisheye_aux(y, $y_focus, 0, $height)
  [x2, y2]
end

# 補助関数
def cartesian_fisheye_aux(x, x_focus, x_low, x_high)
  x_max = x > x_focus ? x_high - x_focus : x_low - x_focus
  x = (x - x_focus).abs
  ($factor + 1) * x_max * x / ($factor * x + x_max.abs) + x_focus
end

ローカル関数を定義できない Ruby が死ぬほど歯がゆい瞬間! Proc 使うのはなぜか嫌。

polar fisheye view

今度は座標を全部極座標で扱います。フォーカスの座標を (0, \quad 0) 、引き延ばしたい点の座標を (r, \quad \theta) 、その方向に突き進んで壁に当たる位置を (r_{\mathit{max}}, \quad \theta) とするとき (0 \, \le \, r \le \, r_{max} になる) 、引き延ばし後の座標は以下になります。

\Bigl( \frac{(d+1)rr_{\mathit{max}}}{dr+r_{\mathit{max}}}, \quad \theta \Bigr)

\theta が保存されるのが特徴です。以下、フォーカスが原点以外であることを考慮したプログラム。

# (x, y) を polar fisheye view でずらす
def polar_fisheye(x, y)
  r, t = cartesian_to_polar(x, y)
  r_max = [
    (x > $x_focus ? ($width  - $x_focus) : $x_focus) / Math.cos(t).abs,
    (y > $y_focus ? ($height - $y_focus) : $y_focus) / Math.sin(t).abs
  ].min
  r2 = ($factor + 1) * r_max * r / ($factor * r + r_max)
  polar_to_cartesian(r2, t)
end

# デカルト座標から極座標へ変換する
def cartesian_to_polar(x, y, x_polar = $x_focus, y_polar = $y_focus)
  x -= x_polar; y -= y_polar
  [Math.hypot(y, x), Math.atan2(y, x)]
end

# 極座標からデカルト座標へ変換する
def polar_to_cartesian(r, t, x_polar = $x_focus, y_polar = $y_focus)
  [r * Math.cos(t) + x_polar, r * Math.sin(t) + y_polar]
end

おまけ: 式のグラフ化

どちらの fisheye も以下の関数に基づいて変換してます。

G(x)=\frac{(d+1)xx_{\mathit{max}}}{dx+x_{\mathit{max}}}

これをグラフ化するとこんな感じです (d = 8 の場合) 。

さらにおまけ: アニメ GIF の作り方

Gifsicle というのを使ってみました。Debian のパッケージもありました。

gifsicle -m -d 20 -UO --colors 255 \
         --background '#ffffff' \
         --loopcount -o output.gif \
         input*.gif

input*.gif が合体して output.gif にします。オプションの意味は以下。

  • -m: merge
  • -d 20: 更新間隔を 0.2 秒に設定
  • -UO: 最適化をオフにしたあとオンにする。おまじない
  • --colors 255: 255 色に減色 (背景色のために 1 色分余裕を持たせる)
  • --background: 背景色指定
  • --loopcount: 無限ループ再生 (ループ回数も指定できるらしい)
  • -o: 出力ファイル指定

*1:Sarkar, M. and Brown, M. Graphical Fisheye Views of Graphs. ACM CHI Conference on Human Factors in Computing Systems. p. 83-91. 1992.