地図/幾何/対称性

円で裏返す

円を一つ決めて、その内と外をひっくり返す操作。中心に近い点は遠くへ、遠い点は中心の近くへ飛ぶ。距離を r²/d² 倍する円反転。

円反転は、中心 O・半径 r の円について、点 P を半直線 OP 上の点 P' に写す写像。OP·OP' = r² を保つので、距離 d の点は r²/d の距離へ移る。円周上(d = r)の点は動かず、内と外が入れ替わる。角度を保つ等角写像の一つで、円と直線をまとめて「円または直線」に写す。直線は半径無限大の円とみなせる。複素数で書くと z → r²/(z̄ − ō) + ō にあたり、メビウス変換(z → (az+b)/(cz+d))の一族に含まれる。

写像そのものは数行。中心 (ox,oy) からの差ベクトルを、長さの2乗 d2 で割って を掛け直す。向きはそのまま、中心からの距離だけが r²/d に化ける。中心の上に乗った点(d2 が0)で割れないので、小さな値を足してかわす。

// 点 (px,py) を中心 (ox,oy)・半径² r2 の円で反転
const invert = (px, py, ox, oy, r2) => {
  const dx = px - ox
  const dy = py - oy
  const d2 = dx * dx + dy * dy + 0.0001
  const k = r2 / d2
  return [ox + dx * k, oy + dy * k]
}

格子の各交点をこの invert に通して点を打つ。中心に近い格子点は遠くへ弾かれ、遠い格子点は中心の近くに寄る。等間隔だった格子が、円の内側でぎゅっと詰まり、外側で散る。半径を時間で伸び縮みさせると、詰まる範囲が呼吸する。

点でなく線にすると、直線が円弧に化ける。1本の直線を80個のサンプル点に刻み、各点を反転して折れ線で繋ぐ。反転で点が画面のはるか外へ飛ぶと折れ線が暴れるので、範囲外に出たサンプルは描かずに on を倒して線を切る。

// 1本の縦線(x=fixed)を、サンプル点ごとに反転して折れ線で描く
c.beginPath()
let on = false
for (let s = 0; s <= 80; s++) {
  const y = (s / 80) * h
  const inv = invert(fixed, y, ox, oy, r2) // x を固定、y を 0..h に振る
  if (inv[0] < -w || inv[0] > w * 2 || inv[1] < -h || inv[1] > h * 2) {
    on = false
    continue
  }
  if (!on) {
    c.moveTo(inv[0], inv[1])
    on = true
  } else {
    c.lineTo(inv[0], inv[1])
  }
}
c.stroke()

縦線と横線の両方を反転して敷き、反転中心をゆっくり動かす。pass で縦横を切り替え、固定する座標を入れ替える。縦線は px0..w に張り、横線は py0..h に張って、面の全幅と全高をそれぞれ埋める。中心が動くたびに格子全体がうねり、直線が中心を通る円弧へ流れ込む。中心を通らない直線は円弧に写り、中心を通る直線は直線のまま残る。

鏡映が直線で折るのに対し、反転は円で折る。角度を保つので、直交した格子は反転後も直交したまま曲がる。

角度を保つ写像をまとめて等角写像。正則関数(複素数の滑らかな関数)は局所的には回転+拡大なので、小さな図形の形を崩さずに角度だけ残して曲げる。円反転は、その中のメビウス変換という一族にあたる。