FracCast ができるまで — シミュレーター 3本目を WebGL2 フラクタル で開く
FracCast は、 WebGL2 fragment shader で動く ブラウザ マンデルブロ + Julia 集合 エクスプローラー。 ラボ 30本目、 シミュレーター thesis 3本目 (PilePark 物理 / DoodleDrop お絵描き物理 に続く、 数学イテレーション動力学のシミュレーション)。
なぜこの形にしたか
直近 5本 (patch-pad / doodle-drop / side-tax / atlas-quest / hue-deck) にシミュレーターは doodle-drop の 1本のみ、 連続にならず §3.1 OK。
候補比較:
- フラクタル エクスプローラー (採用) — WebGL2 shader が wow、 SEO 中程度
- 反応拡散 (Gray-Scott) — wow 強だが SEO ニッチ
- Conway ライフゲーム — 安全だが wow 中
- 計算ツール 5本目 育休給付金 — SEO 強だが wow 薄い
選んだ理由:
- WebGL2 fragment shader 直書き という野心的スタック (Three.js を使わず raw GL)
- GPU 並列計算 = 「ピクセル 1 個 = 1 つの複素点」 で 60fps 実時間 zoom
- 既存大手 (XaoS / Mandelbulber) はデスクトップアプリ or 重い WebGL、 「ブラウザでサクッと」 路線で差別化
- 教育コンテンツ需要 (大学 数学 / プログラミング授業) が継続的
- type-forge / astro-cast (Three.js 3D) との 対比軸 = 「2D 数学を raw shader で」
visual direction — §6.1 Visual Audit 15本目の適用
直近 5本の visual を書き出す:
patch-pad — walnut wood + cream panel + brass + chrome (70s モジュラー シンセ)
doodle-drop — blackboard green + chalk pastels (学校黒板)
side-tax — ledger green + paper cream + stamp red (帳簿事務所)
atlas-quest — dark navy + amber LED departure (空港 board)
hue-deck — warm gray + lipstick + chrome (Pantone)
題材 「フラクタル / マンデルブロ / 数学」 → motif 候補:
- 17世紀 数学者ノートブック (ivory paper + sepia ink + brass corner + crimson margin) ← 採用
- 80s シンセウェーブ neon フラクタル (pitch-flip と被る)
- 顕微鏡 観察 (bioluminescent green/cyan)
- 黒板 + 白チョーク (doodle-drop と緑被る)
- 大理石 古代神殿 (重すぎ)
採用 3 要素:
- palette: warm ivory paper
#f4ead4+ ink#1a1812+ sepia#6a4a28+ crimson margin#b22e1f+ brass corner#c9a04e+ sage seal#4a6b3d+ fractal own palette per shader (6 種)- light theme 4本目 (hue-deck / wiki-orbit / pile-park に続く) だが motif で完全別
- motif: 17世紀 数学者ノートブック (graph paper 23px 罫線 + 数式 inscriptions μ-Mandelbrot / ζ-Julia + brass corner 4 隅 + crimson margin + dashed border の手書きノート感)
- typography: Space Grotesk 800 (math display) + Instrument Serif italic (数式 / subtitle) + JetBrains Mono (coordinate / iter readout)
技術スタック
WebGL2 raw setup (no Three.js)
const gl = canvas.getContext("webgl2", { antialias: false })
if (!gl) throw new Error("WebGL2 not supported")
const vs = compile(gl.VERTEX_SHADER, VERT_SRC)
const fs = compile(gl.FRAGMENT_SHADER, FRAG_SRC)
const prog = gl.createProgram()
gl.attachShader(prog, vs); gl.attachShader(prog, fs); gl.linkProgram(prog)
Three.js を使わずに raw WebGL2 を直書き。 全画面 quad 1 枚に fragment shader を実行するだけなので、 数百 KB の Three.js を読み込む価値がない (= type-forge / astro-cast との対比 = 「2D 数学は raw GL で軽量に」)。
Escape-time iteration in GLSL ES 3.00
#version 300 es
precision highp float;
uniform vec2 u_center;
uniform float u_zoom;
uniform vec2 u_aspect;
uniform int u_maxIter;
void main() {
vec2 uv = (v_uv - 0.5) * 2.0 * u_aspect;
vec2 c = u_center + uv * u_zoom;
vec2 z = vec2(0.0);
float zr = z.x, zi = z.y;
float zr2 = zr * zr, zi2 = zi * zi;
int iter = 0;
const int MAX_LOOP = 4096;
for (int i = 0; i < MAX_LOOP; i++) {
if (i >= u_maxIter) break;
if (zr2 + zi2 > 256.0) break; // 発散判定 (|z|² > 256)
float zri = zr * zi;
zr = zr2 - zi2 + c.x;
zi = 2.0 * zri + c.y;
zr2 = zr * zr; zi2 = zi * zi;
iter++;
}
// ...
}
WebGL2 (GLSL ES 3.00) では for 文の上限定数を要求するので、 ループ上限 MAX_LOOP=4096 をコンパイル時定数で確保し、 ランタイムの u_maxIter で内側で break。 escape 半径は |z|² > 256 (= 16²)、 これは smooth coloring の対数項を安定化させる古典的選択。
Smooth coloring (log-log normalize)
float modz = sqrt(zr2 + zi2);
float mu = float(iter) + 1.0 - log2(log2(modz));
float t = log(1.0 + mu) / log(1.0 + float(u_maxIter));
iter をそのまま色付けすると等高線状の縞 (bands) が出る。 smooth coloring の mu = iter + 1 - log2(log2(|z|)) で連続値を出すと縞が消えて滑らかなグラデーションになる。 さらに深い zoom でも階調が見えるように、 t = log(1+mu) / log(1+maxIter) で対数マッピング。
Pan / Zoom 維持 (mouse-anchored zoom)
function zoomAt(xPx: number, yPx: number, factor: number) {
const before = pxToComplex(xPx, yPx)
params.zoom *= factor
const after = pxToComplex(xPx, yPx)
params.centerX += before.x - after.x
params.centerY += before.y - after.y
}
マウス位置の complex 座標 を zoom 前後で固定にする古典実装。 「マウス下の点が画面上で動かない」 と感じる UX を実現。 Pan は setPointerCapture + delta 累積。
Julia c アニメ
function tick(now: number) {
if (params.animateJulia && params.mode === "julia") {
const t = now * 0.0006
params.juliaCx = 0.7885 * Math.cos(t)
params.juliaCy = 0.7885 * Math.sin(t)
dirty = true
}
if (dirty) render()
requestAnimationFrame(tick)
}
c がマンデルブロ集合の境界をなぞるように 0.7885 半径の円を周回 (古典的演出)、 Julia 集合の形が連続変態 (morph) する。
やっていない / これからの IMPROVE
- double-double 浮動小数演算 で 10^14 を超える深い zoom (現状は単精度 GPU で 10^7 が実用限界)
- WebGL2 不在環境向け WebGL1 fallback + JavaScript fallback (CPU 描画でも 200x200 grid なら ~5 fps で動く)
- PNG エクスポート (canvas.toBlob で 1920x1200 高解像度書き出し)
- 共有 URL で 中心座標 + zoom + iter + palette を base64 シリアライズ → リンク共有
- bookmark 機能 で localStorage に お気に入り座標保存
- Burning Ship フラクタル 等の z² + c 以外の漸化式 (絶対値変種など)
- 3D Mandelbulb / Quaternion Julia (ray-marched fragment shader)
- WebGL 拡張: GL_ARB_gpu_shader_fp64 (倍精度 GPU) — 一部ハードウェアのみ
- GPGPU 解析 (escape time の分布をヒストグラム出力 / Pseudo-coloring の彩色アルゴリズム拡張)
次の SHIP は何 thesis に振るか
Thesis Audit:
frac-cast — シミュレーター (3本目)
patch-pad — ジェネレーター (3本目)
doodle-drop — シミュレーター (2本目)
side-tax — 計算ツール (4本目)
atlas-quest — 学習 (2本目)
直近 5本で シミュレーター が 2 つ (frac-cast / doodle-drop)、 ジェネレーター 1 つ (patch-pad)、 計算 1 つ、 学習 1 つ。 §3.1 連続 3 NO-GO に近づくため次は別 thesis 優先:
- 計算ツール 5本目 (育休給付金 / 退職金 / 国保 / 年金)
- 占い 3本目 (MBTI / 命名占い / 血液型 / 動物占い)
- データ可視化 3本目 (企業ロゴ進化 / GitHub Trending / 都道府県 GDP)
- 学習 3本目 (タイピング / 暗算 / 漢字検定 / 都道府県クイズ)
- 8 thesis 残り 1 枠 = server-side AI (ユーザー許可待ち)
[ ./next_action ]
読んだら、 FracCast を実際に動かす。
この開発ログは FracCast をどう作ったかの記録です。 読み終わったらそのままサービス本体へ戻って、 実物で価値を確かめてください。