GODRICH
#cssアニメーション#ボタン#ローディング#トランジション#スクロール#ダッシュボード#実用ui#デザイントレンド

マンデルブロ集合 9種 — 無限ズーム・ジュリア・バーニングシップを複素平面の1ピクセルずつ計算する

z = z² + c を1ピクセルごとに反復して発散回数を数えると、カージオイドのマンデルブロ集合が現れ、ズームを重ねても新しいミニ・マンデルブロが無限に現れます。ライブラリなしのコピペで無限ズームを持ち帰れます。

自動再生 · タイルを押すとその項目へ · すべて1つのzip

デモ

九つの盤はそれぞれ index.html と style.css の2ファイルで自立し、開いた瞬間から動きます。24フレームの録画区間で九つの盤すべてが23フレーム動き続け、どのフレームも直前と同じ絵になりませんでした。03でポインタを動かすと右のジュリア集合がリアルタイムに変わります。

項目 焦点 累積面積(%) フレーム最大(%) 強度平均
01 エスケープタイム基本 カージオイドと電球が1枚に現れる基本画面 32.008 30.799 24.9
02 無限ディープズーム 渦目標点へ2秒でx25潜る自動ダイブ 34.847 31.349 73.4
03 ジュリア集合リンク c を選ぶと隣の盤の形が変わる接続 12.885 7.357 73.3
04 オービットトラップ彩色 軌道がかすめた形でもう一つの模様を重ねる 32.008 28.188 28.5
05 ブッダブロ 発散する軌道だけ積み上げて浮かぶ形 45.133 41.835 34.4
06 バーニングシップ 絶対値1つで燃える船に変わる式の変更 45.881 44.984 35.9
07 ポインタズーム・パン ホイールとドラッグで潜る探検機 41.972 36.078 87.3
08 スムーズ反復数 階段の帯をグラデーションへ流す後処理 31.206 28.339 25.7
09 フラクタルヒーローシーン ディープズームに文字と星を重ねた表紙 27.91 5.805 48.7

累積面積が最も広いのは06バーニングシップの45.881%で、05ブッダブロの45.133%が続きます。03ジュリアの12.885%が九つで最も小さいのは欠陥ではなく、画面を2つのキャンバスに左右に分け、それぞれ内部解像度150pxで同時計算する構造の結果です。この項目の価値は2つの集合が1画面でリアルタイムにつながる様子を見せることにあり、数値を膨らませようとキャンバスを1つ消してはいけません。強度平均は07ポインタズーム・パンの87.3が最も明るく、02の73.4と03の73.3が続きます。

脱出時間からランディング画面まで、マンデルブロ九層を重ねた順序

並び順の基準は派手さではなく、式 z = z² + c のどこを眺めるかです。01は発散回数を色にする基本の眺め方、02はビューポートを毎フレーム狭めて同じ式を深く読み、03は固定する側を入れ替えてジュリア集合とつなぎます。04は反復の回数ではなく軌道の通り道を記録し、05は発散する点の軌道だけを残して絵を反転させ、06は式そのものに絶対値を1つ足します。07で初めて手で操作し、08は同じ計算に後処理を1枚足して品質を上げ、09がその計算画面をサービスの表紙に組み立てて締めくくります。式を変えずに眺め方だけ変える反応拡散の縞とも、レイで奥行きを読むレイマーチングの距離関数とも違うのは、この9種がすべて同じたった1行の反復から出ている点です。

項目別コード

項目ごとに、挙動を決める定数を示し、動いている関数をそのまま引用します。切り詰めた断片ではなく動いているコードそのものなので、この画面から自分のページへ貼って動かせます。コメントだけこの記事の言葉に置き換えてあり、変数名と関数名はzipの中身と同じ英語のままです。

01エスケープタイム基本 — 1ピクセルごとに z = z² + c を反復して色を決める

各ピクセルを複素数 c として二乗と加算を繰り返し、発散までの回数で色を塗ると、カージオイドと電球がひとりでに描かれます。発散の判定は |z|² = zr² + zi² > 4 と平方根なしで済ませ、最大反復96に達しても発散しない点だけ集合の内部色で塗ります。累積面積32.008%・強度平均24.9は、パレット循環(2秒周期)だけで毎フレーム画面を塗り替える基本盤の値です。

エスケープタイム基本 — 1ピクセルごとに z = z² + c を反復して色を決める
新しいタブで開く

01 · エスケープタイム基本 — 1ピクセルごとに z = z² + c を反復して色を決める — 計測 累積面積 32.008% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

CanvasRenderingContext2DcreateImageDatarequestAnimationFrame

ピクセルループの核心 — zr² − zi² + cr、2·zr·zi + ci

複素数の二乗 z² = (zr + zi·i)² を実数2つに展開すると実部が zr² − zi²、虚部が 2·zr·zi になり、それぞれに c を足すだけです。内部キャンバス240pxで計算してCSSで画面寸法に引き伸ばすのが性能の鍵で、フル解像度で回すとフレームが死にます。

  function render(phase) {
    var iw = ov.width, ih = ov.height;
    var img = octx.createImageData(iw, ih);
    var px = img.data;
    var dx = SPAN / iw, dy = SPAN / iw;      // dy を dx と同じに — 正方ピクセル(縦横比補正)
    var oy0 = CY - (ih * dy) / 2;
    var k = 0;
    for (var j = 0; j < ih; j++) {
      var ci = oy0 + j * dy;
      for (var i = 0; i < iw; i++) {
        var cr = CX - SPAN / 2 + i * dx;
        // z = 0 から z = z² + c を反復 — 発散した時点の n が色インデックス
        var zr = 0, zi = 0, n = 0;
        while (n < MAXIT && zr * zr + zi * zi <= 4) {
          var t = zr * zr - zi * zi + cr;
          zi = 2 * zr * zi + ci;
          zr = t;
          n++;
        }
        px[k++] = r; px[k++] = g; px[k++] = b; px[k++] = 255;
      }
    }
    octx.putImageData(img, 0, 0);
    ctx.drawImage(ov, 0, 0, W, H);
  }

階段の帯を流す5色パレット

反復回数 n は整数なので、n をそのままパレットインデックスに使うと色帯が階段になります。01は5色のパステルパレットの上で n に時間の phase を足して循環させ、帯の中でも色が滑らかに流れます。この階段を完全に消すのが08の役目です。

02無限ディープズーム — 目標点へ潜り続ける自動ダイブ

倍率 k を毎フレーム掛けて2秒で合計x25まで上がり、カージオイド先端の渦目標点 (−0.743643887037, 0.13182590421) へ潜ります。要諦は最大反復を倍率に比例して 96 → 500 へ一緒に上げることで、これがないと深く入るほど境界の輪郭が切れます。累積面積34.847%・強度平均73.4は九つの中で2番目に明るい潜行です。

無限ディープズーム — 目標点へ潜り続ける自動ダイブ
新しいタブで開く

02 · 無限ディープズーム — 目標点へ潜り続ける自動ダイブ — 計測 累積面積 34.847% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

createImageDatarequestAnimationFrameMath.log

指数潜水と倍率→進行度の逆変換

scale = 2.9 / k、k = 25^t の指数で潜り、進行度 t は Math.log(k)/Math.log(ZOOM2S) でいつでも戻せます。この逆変換で倍率ラベル(x1 → x25)を検算し、ループの補正にも使います。

  function stateAt(t) {
    var k = Math.pow(ZOOM2S, t);
    var progress = Math.log(k) / Math.log(ZOOM2S);   // 倍率→進行度の逆変換(=t、検算用)
    var cx = -0.75 + (TX + 0.75) * t;
    var cy = (TY) * t;
    var maxit = Math.round(MAXIT0 + (MAXIT1 - MAXIT0) * progress);
    return { cx: cx, cy: cy, scale: 2.9 / k, maxit: maxit, k: k };
  }

  function setZoomLabel(k) {
    // 倍率ラベル — x25 のように短く。1,000 を超えたら k 単位で
    zoomEl.textContent = k >= 1000 ? 'x' + Math.round(k / 100) / 10 + 'k' : 'x' + Math.round(k);
  }

フル解像度で回すとフレームが死ぬ

ピクセルごとに最大96〜500回の複素反復が走る計算なので、02は内部解像度260px(面積でいえば480×300の3分の1弱)で計算して引き伸ばします。ズームを重ねても新しいミニ・マンデルブロが現れ続ける マンデルブロ集合 拡大 の体験は、この引き伸ばしと反復の伸びの組み合わせで初めて60fpsに乗ります。

03ジュリア集合リンク — ポインタがcを選ぶと別キャンバスが変わる

左のキャンバスはマンデルブロ(幅2.9)、右はジュリア集合で、ポインタがマンデルブロ上で選んだ座標がそのまま右のジュリアの固定 c になります。同じ式 z = z² + c で固定する側が変わるだけで、雲・兎・枝の形がリアルタイムに変わります。自動巡航は集合の縁を2秒周期で往復し、縁の近くでジュリアが最も劇的に変わります。

ジュリア集合リンク — ポインタがcを選ぶと別キャンバスが変わる
新しいタブで開く

03 · ジュリア集合リンク — ポインタがcを選ぶと別キャンバスが変わる — 計測 累積面積 12.885% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

CanvasRenderingContext2DcreateImageDatapointermove

マンデルブロはcを、ジュリアはzをピクセルにする

renderM は各ピクセルを c として z = 0 から始め、renderJ は各ピクセルを z の初期値として c を固定します。反復の中身は同じ1行なのがこの対の面白さです。

  function renderJ(cr, ci) {
    var iw = ovJ.width, ih = ovJ.height;
    var img = octxJ.createImageData(iw, ih);
    var px = img.data;
    var span = 3.0;                     // ジュリアのビューポート幅 — z 平面 ±1.5
    var dx = span / iw, dy = span / iw;
    var oy0 = -(ih * dy) / 2;
    var k = 0;
    for (var j = 0; j < ih; j++) {
      var zi0 = oy0 + j * dy;
      for (var i = 0; i < iw; i++) {
        var zr0 = -span / 2 + i * dx;
        var zr = zr0, zi = zi0, n = 0;
        while (n < MAXIT_J && zr * zr + zi * zi <= 4) {
          var t = zr * zr - zi * zi + cr;
          zi = 2 * zr * zi + ci;
          zr = t; n++;
        }
        var c = colorOf(n, MAXIT_J);
        px[k++] = c[0]; px[k++] = c[1]; px[k++] = c[2]; px[k++] = 255;
      }
    }
    octxJ.putImageData(img, 0, 0);
    ctxJ.drawImage(ovJ, 0, 0, WJ, HJ);
  }

2つの盤を同時に回す解像度の割り切り

九つの中で累積面積12.885%と最も小さいのは、2つのキャンバスをそれぞれ150pxで同時計算するため、1フレームで実際に色が変わるピクセル数が半分以下に減るからです。フレーム最大7.357%も同じ構造の結果で、濃さは落ちていません。

04オービットトラップ彩色 — 反復軌道が触れる形で色を塗る

発散回数の代わりに、各ピクセルの z の軌道が実軸・虚軸・半径0.62の円にどれだけ近くをかすめたかの最小値を記録して色にすると、帯・円環・対称模様が集合の上にもう一つのフラクタルのように重なります。3種のトラップを2秒周期で循環し、かすめた場所をトラップ強調色で光らせます。累積面積32.008%・強度平均28.5です。

オービットトラップ彩色 — 反復軌道が触れる形で色を塗る
新しいタブで開く

04 · オービットトラップ彩色 — 反復軌道が触れる形で色を塗る — 計測 累積面積 32.008% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

createImageDataMath.hypotrequestAnimationFrame

3種のトラップ — 軸2つと円1つ

trapDist は軌道の各点で実軸からの距離・虚軸からの距離・円からの距離のいずれかを取り、それまでの最小値と比べます。反復の中でこの1行を足すだけで同じ集合からまったく別の絵が生まれます。

  function trapDist(zr, zi, kind, best) {
    var d;
    if (kind === 0) d = Math.abs(zi);                    // 実軸への近さ
    else if (kind === 1) d = Math.abs(zr);               // 虚軸への近さ
    else d = Math.abs(Math.hypot(zr, zi) - 0.62);        // 円への近さ — 半径0.62の円環
    return d < best ? d : best;
  }

かすめた距離を明るさに変える

記録した最小距離 best が小さいほど近くを通ったということなので、glow = 1 / (1 + best·26) の飽和曲線で明るさに変換し、トラップ強調色と元のパレットをその比で混ぜます。遠くを通ったピクセルは元の発散回数の色に戻るので、2つの彩色が1画面に共存します。

05ブッダブロ — 発散する軌道を積み重ねて浮かぶ幽霊の形

01は発散回数を色に使いましたが、ここでは発散する点を選び、その軌道が通ったすべてのピクセルに1ずつ足します。発散しない点(集合内部)の軌道は捨てる、この1行の違いでカージオイドが消え、左右対称の瞑想する形が浮かびます。毎ステップ260個の軌道を — 毎秒30ステップ — シード固定乱数で撒き、ガンマ0.94の減衰で古い軌道が静まり、2秒ループごとにリセットして再び浮かびます。

ブッダブロ — 発散する軌道を積み重ねて浮かぶ幽霊の形
新しいタブで開く

05 · ブッダブロ — 発散する軌道を積み重ねて浮かぶ幽霊の形 — 計測 累積面積 45.133% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

createImageDataglobalAlphaMath.random

発散する点だけもう一度歩かせる

まず軌道保存なしで発散判定だけ行い、発散する点だけもう一度 z = 0 から歩いて通過ピクセルに1を足します。Math.random の代わりにシード固定の mulberry32 を使い、同じ時刻のフレームはいつも同じ絵にします。

  function rnd() {
    // mulberry32 — シード固定。Math.random では描くたびに別の絵になる
    rndState |= 0; rndState = (rndState + 0x6D2B79F5) | 0;
    var t = Math.imul(rndState ^ (rndState >>> 15), 1 | rndState);
    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
  }

  function traceOrbit() {
    var cr = RX0 + rnd() * (RX1 - RX0);
    var ci = RY0 + rnd() * (RY1 - RY0);
    var zr = 0, zi = 0, n = 0;
    // まず発散判定(軌道保存なし) — 発散する点だけもう一度歩いて痕を残す
    while (n < MAXIT && zr * zr + zi * zi <= 4) {
      var t = zr * zr - zi * zi + cr;
      zi = 2 * zr * zi + ci;
      zr = t; n++;
    }
    if (n >= MAXIT) return;                    // 集合内部 — 軌道を残さない(ブッダブロの条件)
    zr = 0; zi = 0;                            // 改めて出発し、痕を残す
  }

対数スケールと減衰で形を整える

訪問回数は30で飽和する対数スケールで明るさに変え、フレームごとの減衰0.94で古い軌道が静まります。累積面積45.133%は九つで2番目に広く、軌道を積み上げるほど形がくっきり浮かぶ性質がそのまま数値に現れています。

06バーニングシップ変種 — 絶対値1つで燃える船が現れる

二乗の前に実部・虚部へ絶対値を1つずつかけた z ← |z|² + c だけでマンデルブロが消え、炎を吹く船が現れます。絶対値が軌道を下半平面へ折り込み、その折り込みが左右対称の炎模様を生みます。画面は船の本体 (−1.755, −0.04 · 幅1.35) を映し、±4.5%の息づく微細ズームを重ねて毎フレーム生かしておきます。

バーニングシップ変種 — 絶対値1つで燃える船が現れる
新しいタブで開く

06 · バーニングシップ変種 — 絶対値1つで燃える船が現れる — 計測 累積面積 45.881% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

createImageDataMath.absrequestAnimationFrame

z ← |z|² + c と3乗への切り替え

POWER 定数1つで2乗(|z|²+c)と3乗(|z|³+c、ド・モアブルの3倍角)を切り替えられます。3乗では対称が2回から3回に変わり、まったく別の生き物が現れます。

        while (n < MAXIT && zr * zr + zi * zi <= 4) {
          // ── バーニングシップの1行 — 二乗の前の絶対値 ──
          var ar = Math.abs(zr), ai = Math.abs(zi);
          if (POWER === 2) {
            var t = ar * ar - ai * ai + cr;
            zi = 2 * ar * ai + ci;
          } else {
            // 3乗変形 — (|z|)³: ド・モアブルの3倍角
            var t = ar * ar * ar - 3 * ar * ai * ai + cr;
            zi = 3 * ar * ar * ai - ai * ai * ai + ci;
          }
          zr = t;
          n++;
        }

炎のランプと息づくズーム

色は画面の下(炎の先端)ほど明るくなるランプを重ね、毎フレーム ±4.5% の span の呼吸で全体を動かし続けます。累積面積45.881%・フレーム最大44.984%は九つで最も広く、炎がほぼ全面を動かす盤の性格をそのまま示しています。

07ポインタズーム・パン — クリックした点へ吸い込まれる探検機

ホイールはポインタの下の点を画面中央へ吸い込むズーム、ドラッグはパンです。最大反復は倍率の対数に比例して 96 → 400 まで上がります。プレビューは人の手の代わりに2秒4幕(電球へズームイン → 上へパン → さらにズーム → ズームアウトで復帰)の決定論のシナリオを再生します。強度平均87.3は九つで最も明るい数値です。

ポインタズーム・パン — クリックした点へ吸い込まれる探検機
新しいタブで開く

07 · ポインタズーム・パン — クリックした点へ吸い込まれる探検機 — 計測 累積面積 41.972% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

pointerdownpointermovewheel

ポインタの下の点を固定するcenter補正

ズーム後に (px_, py_) の複素平面上の点が同じ場所に残るよう、center を p + (center − p)/factor で動かします。これがないとホイールのたびに画面中央が基準になって、指した点が逃げていきます。

  function zoomAt(px_, py_, factor) {
    // (px_, py_) 画面座標の複素平面の点がズーム後もその場に残るよう center を補正
    var p = toPlane(px_, py_);
    view.scale /= factor;
    view.cx = p.x + (view.cx - p.x) / factor;
    view.cy = p.y + (view.cy - p.y) / factor;
  }
  function panBy(dxPx, dyPx) {
    var dx = view.scale / W;
    view.cx -= dxPx * dx;
    view.cy += dyPx * dx;
  }

  function maxIter() {
    var k = startScale / view.scale;
    return Math.round(MAXIT0 + (MAXIT1 - MAXIT0) * Math.min(1, Math.log(k) / Math.log(150)));
  }

倍率の対数に比例する反復数

深く入るほど境界の細かな構造を生かすには反復がより要るので、07は倍率 k に対して Math.log(k)/Math.log(150) の比率で 96 → 400 を補間します。02・09も同じ考えで倍率に比例して上げています。

08スムーズ反復数 — 帯を消してグラデーションで流れる彩色

脱出直後の大きさの二重対数を引いて ν = n + 1 − log₂(log|z|) の小数反復数を作ると、整数 n の階段帯が連続グラデーションへ流れます。画面は左に整数オリジナル・右にスムーズ処理版を並べ、パレットを64反復周期で循環させて違いを直接比べます。1つの絞り(smooth真偽値)で同じレンダラーが両場面を出す構造です。

スムーズ反復数 — 帯を消してグラデーションで流れる彩色
新しいタブで開く

08 · スムーズ反復数 — 帯を消してグラデーションで流れる彩色 — 計測 累積面積 31.206% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

createImageDataMath.logcreateLinearGradient

二重対数で小数の反復数を作る

脱出した瞬間の |z| は2よりわずかに大きいだけなので、その対数の対数を取ると整数 n の間を埋める小数が得られます。この1行の後処理だけで同じ計算の結果が帯からグラデーションに変わります。

        var c;
        if (n >= MAXIT) {
          c = C_SET;
        } else {
          var nu;
          if (smooth) {
            // ── スムーズ式 — 脱出直後の |z| の二重対数で補間 ──
            var mod = Math.sqrt(zz);
            nu = n + 1 - Math.log(Math.log(mod)) / Math.LN2;
          } else {
            nu = n;                       // 原本 — 階段の原因
          }
          c = palAt((nu / PERIOD) * 4 + phase);
        }
        px[k++] = c[0]; px[k++] = c[1]; px[k++] = c[2]; px[k++] = 255;

左右で比べる同じレンダラー

render は smooth の真偽値を引数に取る1つの関数で、左盤は false・右盤は true で呼び出します。累積面積31.206%・強度平均25.7と数値は地味ですが、フラクタル ズーム の映像で帯が気になったら最初に足す後処理です。

09フラクタルヒーローシーン — 文字・星明かりと重ねる表紙の場面

02のディープズームをゆっくり(2秒でx3)背景に敷き、その上に createRadialGradient のビネット・タイポ(INFINITE DIVE)・脈動する星印を重ねた表紙の場面です。ビネットの縁のアルファは0.42+0.20の呼吸で2秒周期で息づき、ズームは潜り続けるので毎フレーム全面が動きます。累積面積27.91%・強度平均48.7の組立例です。

フラクタルヒーローシーン — 文字・星明かりと重ねる表紙の場面
新しいタブで開く

09 · フラクタルヒーローシーン — 文字・星明かりと重ねる表紙の場面 — 計測 累積面積 27.91% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

createRadialGradientcreateImageDatarequestAnimationFrame

息づくビネットを毎フレーム重ねる

背景のズームとは独立に、ビネットの深さを2秒の呼吸で変えながら全画面に重ねます。静的なオーバーレイではなく毎フレーム描き直すからこそ、表紙がずっと生きているように見えます。

  function draw(t) {
    var k = Math.pow(ZOOM2S, t);
    var cx = -0.75 + (TX + 0.75) * t;
    var cy = TY * t;
    var maxit = Math.round(MAXIT0 + (MAXIT1 - MAXIT0) * t);
    var ctx = cv.getContext('2d');
    ctx.drawImage(renderFractal(cx, cy, 2.9 / k, maxit), 0, 0, W, H);
    // 放射ビネット — 中央は透明、縁へいくほどインク。呼吸(t のサイン)に合わせて深さが変わる
    var breath = 0.5 + 0.5 * Math.sin(t * Math.PI * 2);      // 0〜1 — 2秒で一呼吸
    var vg = ctx.createRadialGradient(W / 2, H / 2, Math.min(W, H) * 0.18, W / 2, H / 2, Math.max(W, H) * 0.72);
    vg.addColorStop(0, 'rgba(' + C_INK[0] + ',' + C_INK[1] + ',' + C_INK[2] + ',0)');
    vg.addColorStop(1, 'rgba(' + C_INK[0] + ',' + C_INK[1] + ',' + C_INK[2] + ',' + (0.42 + breath * 0.20).toFixed(3) + ')');
    ctx.fillStyle = vg;
    ctx.fillRect(0, 0, W, H);
  }

フレーム最大5.805%が低い理由

フレーム最大5.805%は九つで最も小さいですが、遅い背景ズーム(x3/2秒)の上でビネットだけが息づく場面の性格上妥当な値です。星印の脈動も2秒周期で、数字を膨らませるためにズームを速くすると表紙としての落ち着きが失われます。

最大反復を固定したままだと深いズームで壊れる場所

フル解像度でピクセルループを回すとフレームが死ぬ

ピクセルごとに最大96〜500回の複素反復が走る計算なので、480×300のフル解像度で回すと1フレームに数千万回の反復が積み重なり、動きが止まります。九つの盤すべてが内部240px(03・08は各盤150px)で計算して putImageData で送り、CSSで引き伸ばしています。

発散判定に毎反復 Math.sqrt を呼ぶ

|z| > 2 の判定は |z|² > 4 の比較で平方根なしに同じ結果が出ます。ループ内の Math.sqrt は数百万回の無駄になるので、九つの盤はすべて二乗のまま比較します。

ディープズームで最大反復を固定したままにする

倍率が大きくなるほど境界の細かな構造を生かすには反復がより要ります。02が96 → 500、07が96 → 400、09が90 → 260と倍率に比例して上げるのはこのためで、固定したままだと深く入るほど輪郭が切れて、細かい粒子がざらついたように濁って見えます。

ブッダブロに Math.random を使う

レンダーごとに違う絵になり決定論が壊れます。05はシード固定の mulberry32(rndState = 20260916)で同じ時刻 = 同じ蓄積状態にします。また発散しない点の軌道まで積むと絵がどろりとして形が浮かびません。発散する点だけ残す1行がこの絵の正体です。

zipを展開すると九つの項目の index.html と style.css が入ったフォルダーごと出てきます。展開に使うパスワードはm5rczsswです。本文に引用した関数がコメント込みでそのまま入っているので、行単位で突き合わせて確かめられます。

この9枚を実際どこに置くか — ダイブ背景とポインタ探検機

ランディングページの表紙をフラクタルで埋めたいなら、まず09 フラクタルヒーローシーンをそのまま乗せるのが一番早い方法です。文字と星明かりを重ねる構図がすでにできているので、追加のCSSなしで表紙が完成します。ポートフォリオや実験室ページのように訪問者に触ってほしい場所には、受け身の02 無限ディープズームの代わりに07 ポインタズーム・パンを置くと、クリックした座標へ吸い込まれる操作がスクロール誘導も兼ねます。

数学やアルゴリズムの教育コンテンツなら03 ジュリア集合リンクが向いています。ポインタが選んだcの値が隣のキャンバスのジュリア集合をその場で変えるので、説明文なしでもパラメータと結果の関係が画面に表れます。3枚ともファイル1つで完結しているので、フレームワークなしでiframeや静的セクションにそのまま組み込めます。

定数を一つ変える3つの変種とreduced-motionのアクセシビリティ

名前 変更する値 変わる感触
最大反復回数 02 MAXIT1 500 → 2,000 深く潜るほど切れていた輪郭がつながり、ズームが目に見えて遅くなる
パレット周期 08 PERIOD 64 → 16 色が密に循環し、カージオイド内側の構造がはるかにくっきりする
式の変更 06 POWER 2 → 3 対称が2回から3回に変わり、まったく別の生き物が現れる

定数ブロックの1行を書き換えるだけ

3つとも書き換えるのはソース先頭の変数ブロックの1行だけです。最大反復は深さの解像度、パレット周期は読み取りやすさ、式の変更は世界そのものという、それぞれ別の性格のつまみです。

動きを減らす設定で残る1枚

九つの盤すべてに prefers-reduced-motion: reduce の分岐が組み込まれており、ループを回さず phase を固定した1場面を描いて .is-still クラスを付けます。静止していてもカージオイドや燃える船が見えるので、動きを止めた訪問者にも各盤の結論の場面が届きます。

触れる2枚と完結する7枚

03と07だけがポインター入力を受け付け、ほかの7つは手を触れなくても完結します。03のポインタ移動と07のホイール・ドラッグはいずれも画面の下のメタ行で状態を読め、07のホイールズームは preventDefault でページのスクロールを飲み込まないように分けてあります。

なぜズームしても新しい形が出るのか — エスケープタイムと自己相似

ズームをやめても同じ形が続く絵は拡大アニメーションで、 フラクタル ズーム と呼べるかどうかは計算にあります。エスケープタイム法ではビューポートの幅 SPAN を縮めるたびに、見えている領域の全ピクセルについて z = z² + c を初めから計算し直すので、縁の一部にミニ・マンデルブロがあればその内部構造が新しいピクセル計算として現れます。CSS transform の scale で画像を引き伸ばすだけでは新しいピクセルが生まれないので、どんなに拡大しても元の絵の一部しか見えません。式を1行も変えずに眺める幅だけを変えて別の生き物が出てくるのは、境界の構造がどの尺度でも似た形を繰り返す自己相似のためで、02の目標点も07の潜り先もその構造が詰まった縁の近くを選んでいます。

マンデルブロズームを組み込む前によくある質問

マンデルブロ集合 式 の計算をそのままコピーして自分のページに置けますか?

置けます。zipを展開すると九つの項目のフォルダーが出るので、好きな盤1つの index.html と style.css の2ファイルをフォルダーごとアップロードすればそのまま動きます。ライブラリも WebGL も CDN も使っていないので、読み込みの設定は何も要りません。色は style.css の--mb-系トークンで、場面の大きさは同じファイルの盤の高さで調整します。

なぜどの盤もキャンバスの解像度を下げて計算するのですか?

ピクセルごとに最大96〜500回の複素反復が走るので、表示サイズのまま計算すると1フレームの計算量が数千万回に膨らみ、動きが止まります。内部キャンバス(150〜260px)で計算して putImageData で送り、CSSで画面寸法に引き伸ばすと、フラクタルの輪郭はそのままにフレームが保てます。

ズームはどこまで続けても新しい形が出ますか?

計算上は倍率をいくらでも上げられますが、通常の倍精度の数では深く入るほど座標の刻みが足りなくなって、ピクセルが同じ値に丸められて画面が固まります。02の2秒でx25程度なら余裕があり、それより深く潜りたい場合は反復数を上げると同時に、目標点の座標をより多くの桁で指定する必要があります。

もっと読む

解凍パスワードを入力してください

パスワードはこの記事の本文の中にあります。読み進めると出てきます。