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

シェルピンスキーの三角形など9種 — 文字が枝を生むキャンバス

Fは前進、+は回転。この数文字を読み直すだけで枝がひとりでに伸びます。再帰エンジン一式をコピペで持ち帰れます。シェルピンスキーの三角形も含む9つの図形はすべて、この文字列書き換え(Lシステム)とタートルグラフィックの組み合わせで一枚のキャンバスに計算して描いたものです。

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

デモ

デモは9つとも同じ構成です。ライブラリ・WebGL・外部CDNを使わず、index.html 1枚と style.css だけで完結するので、コピーしてそのままブラウザで開けば同じ場面が動きます。測定では9項目のうち7項目(01・02・03・04・06・08・09)が24コマ中23コマで世代が切り替わり(23/23)、05カーペットと07植物だけが22コマでの切り替わり(22/23)でした。世代の境目で進行度が1コマ止まる場所があるのは、文字列を世代ごとにしか書き換えていない証拠で正常です。累積面積は1.706%(08風に揺れる木)から16.167%(05カーペット)まで幅があります。05だけが fillRect でマス目を塗りつぶす方式で塗られた面積そのものが数値になり、残り8項目は幅1〜5px程度の lineTo 線が枝や海岸線を1本ずつなぞる方式だからです。背景を塗る絵ではなく、線が世代ごとに育つ絵、というのがこの回の性質です。

文字Fひとつの再帰から曲線・パラメータまで広がったL-systemの順序

9つの並びは、規則の複雑さと図形の知名度の両方に沿っています。01の再帰枝は F 一文字と角括弧2つだけで木になる最小実装、02のピタゴラスの木・03の雪片の海岸線・04のシェルピンスキーの三角形は教科書にも載る古典フラクタルで、規則を1つ増やすごとに図形の性格が変わることを見せます。05のカーペットで初めて「トゥイーンの数値ではなく実測」に踏み込み、06のドラゴン曲線は2文字が互いを書き換え合う応用、07の植物は角度を連続的に揺らして生成文法を体感させます。08は世代番号そのものを振動のパラメータに変え、最後の09は5本の木が3種類の規則を使い分けて表紙の一場面に組み上がります。単純な規則から複合的な場面へ、という順番です。

項目別コード

01再帰枝 — 2又に分かれる最小の木

幹を描き、短くした自分自身を2回呼ぶだけで木が完成します。角度26度・縮小比率0.74で世代0→6まで育て、育ち切った後は枝先がサイン波でわずかに呼吸します。

再帰枝 — 2又に分かれる最小の木
新しいタブで開く

01 · 再帰枝 — 2又に分かれる最小の木 — 計測 累積面積 2.123% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

Math.sinMath.coslineTobeginPath

規則と回転角

axiom は文字 F 一つだけで、規則は F を F[+F][-F]F に置き換えるものです。角括弧は現在の位置・向き・長さをスタックへ push/pop する記号で、これが「幹から2本の枝が分かれる」を文字列だけで表しています。

// Lシステム文字列書き換え — axiom F を規則で n世代分展開する
var RULE = { 'F': 'F[+F][-F]F' };
function expand(axiom, gen) {
  var s = axiom;
  for (var g = 0; g < gen; g++) {
    var out = '';
    for (var i = 0; i < s.length; i++) {
      var ch = s.charAt(i);
      out += RULE[ch] !== undefined ? RULE[ch] : ch;
    }
    s = out;
  }
  return s;
}

呼吸するとどまり

世代0から世代6までの文字列をあらかじめ配列に保存しておき、進行度に合わせて前世代の点を次世代の点へ混ぜて滑らかに伸ばします。育ち切った保持区間では枝先の座標にサイン波の微小なずれを足すことで、成長が止まっても画面が完全な静止画にならないようにしています。

02ピタゴラスの木 — 正方形の上に正方形

正方形の斜辺の上に、その斜辺を一辺とする正方形を2つ載せる再帰を世代0→8まで繰り返すと、樽型の森が育ちます。実測候補にもある古典フラクタルをキャンバスで蘇らせる項目です。

ピタゴラスの木 — 正方形の上に正方形
新しいタブで開く

02 · ピタゴラスの木 — 正方形の上に正方形 — 計測 累積面積 1.792% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

Math.sinMath.coslineTofillRect

2つの正方形が育つ場所

最初の正方形の一辺は舞台の縦幅の0.21倍、そこから世代ごとに1/√2(約0.707倍)ずつ縮みます。斜辺の中点と√2で割った新しい辺の長さから、左右2つの子正方形の基準点を三角関数で求め、それぞれを自分自身の再帰呼び出しに渡します。

if (gen + 1 > genF) return;
// 上に直角三角形(45-45-90) — 斜辺がこの辺。2つの正方形を斜辺に載せる。
// 斜辺の両端は (x, y) と (x+s, y)。45度三角形の頂点は中点の上 s/2。
var mx = x + s / 2, my = y - s / 2;
var NS = s / Math.SQRT2;
// 左の正方形: 斜辺 (x,y)-(mx,my)。辺は NS。
var angL = -Math.PI / 4;
var lx = x, ly = y;
// 左の正方形の左上頂点 = 斜辺の始点から外側へ NS だけ
var lcx = lx + Math.cos(-Math.PI * 3 / 4) * NS;
var lcy = ly + Math.sin(-Math.PI * 3 / 4) * NS;
drawTree(ctx, lcx, lcy, NS, gen + 1, genF, seasonT);
// 右の正方形: 斜辺 (mx,my)-(x+s,y)。
var angR = Math.PI / 4;
var rx = mx, ry = my;
var rcx = rx + Math.cos(Math.PI * 3 / 4) * NS;
var rcy = ry + Math.sin(Math.PI * 3 / 4) * NS;
drawTree(ctx, rcx, rcy, NS, gen + 1, genF, seasonT);

季節が巡る色

正方形の色は幹色から葉色へ、再帰の深さの割合(depthT)で混ぜています。育ち切った後は葉色そのものが緑の夏から琥珀色の秋へ、また春へと周期の中で巡り、8世代まで進むと正方形はおよそ250個(2の8乗の近似、キャンバスの外に出た分はサブピクセルで自然に間引かれます)まで増えます。

03コッホ雪片 — 辺を持ち上げて無限の海岸線

axiom F--F--F(正三角形の3辺)から規則 F→F+F--F+F を世代ごとに適用すると、毎回さらに1辺が細かく折れて雪片の海岸線が伸びます。日本語の実測候補(コッホ雪片とは・次元)まで出る検索軸の代表項目です。

コッホ雪片 — 辺を持ち上げて無限の海岸線
新しいタブで開く

03 · コッホ雪片 — 辺を持ち上げて無限の海岸線 — 計測 累積面積 8.315% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

Math.sinMath.coslineTostrokeStyle

三角形から伸びる海岸線

角度は60度固定で、世代0から世代4まで、前世代の点を次世代の点へ混ぜながら補間します。1本の辺が4本に置き換わる規則なので、世代を1つ進めるごとに辺の総数はおよそ4倍に増え、周の長さそのものが伸び続けます。

var RULE = { 'F': 'F+F--F+F' };
function expand(axiom, gen) {
  var s = axiom;
  for (var g = 0; g < gen; g++) {
    var out = '';
    for (var i = 0; i < s.length; i++) {
      var ch = s.charAt(i);
      out += RULE[ch] !== undefined ? RULE[ch] : ch;
    }
    s = out;
  }
  return s;
}

輪の呼吸と光の点

塗りの不透明度がサイン波でわずかに明滅する「呼吸」の上に、周囲を回る装飾的な光の点を arc で重ねています。構造そのものは直線(lineTo)だけで、曲線関数は種のような強調点にしか使っていません。

04シェルピンスキーの三角形 — 三角形に潜む三角形

タートル文字数文字で三角形の中に三角形が無限に並びます。シェルピンスキーのギャスケットとも呼ばれる図形で、日本語実測で最も厚い候補が出た軸のため、他ロケールへの展開も容易な項目です。

シェルピンスキーの三角形 — 三角形に潜む三角形
新しいタブで開く

04 · シェルピンスキーの三角形 — 三角形に潜む三角形 — 計測 累積面積 5.87% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

Math.sinMath.coslineTobeginPath

互いを書き換える2文字

axiom は A 一文字、規則は A→B-A-B と B→A+B+A の2つで、角度60度のタートル歩行に解釈すると一筆書きの矢じり曲線になります。A と B が互いを呼び合いながら書き換わることで、世代0→6と増えるほどその曲線が三角形の隙間をそのまま埋めていきます。

var RULE = { 'A': 'B-A-B', 'B': 'A+B+A' };
function expand(axiom, gen) {
  var s = axiom;
  for (var g = 0; g < gen; g++) {
    var out = '';
    for (var i = 0; i < s.length; i++) {
      var ch = s.charAt(i);
      out += RULE[ch] !== undefined ? RULE[ch] : ch;
    }
    s = out;
  }
  return s;
}

太さが世代で細る理由

線の太さは世代が進むほど細くします。1本の線が担う三角形が世代ごとに小さくなるため、太いまま描くと隙間が線に埋まって見えてしまうからです。世代番号から太さを逆算する式を使い、最小1.3px・最大4.2pxの範囲に収めています。

05シェルピンスキーのカーペット — マスを抜って掘る穴

9マスごとに中央を空ける規則を繰り返すと、布に穴が開くように正方形が掘れます。面積が減り辺が増えるフラクタル次元を、トゥイーンではなく測定で示す項目です。

シェルピンスキーのカーペット — マスを抜って掘る穴
新しいタブで開く

05 · シェルピンスキーのカーペット — マスを抜って掘る穴 — 計測 累積面積 16.167% · 動いたフレーム 22/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

fillRectgetImageDatarequestAnimationFrame

9マスの穴あけ規則

塗りつぶし1マス(文字1)を3×3の9文字に書き換え、中央だけを穴(文字0)にする規則 1→111101111 を世代0→4まで再帰適用し、結果の文字列を fillRect の格子として描きます。

drawCarpet(ctx, str, gA, ox, oy, side, thisF, breath);

if (genEl) genEl.textContent = String(gA);

// 残った面積% — getImageData で実際に塗られた(背景ではない)ピクセルの割合を数える。
// 値は世代だけに依存するので、毎フレーム数え直さず世代が変わった時だけ再計算して計算量を節約する。
if (areaEl && gA !== lastAreaTick) {
  lastAreaTick = gA;
  try {
    var img = ctx.getImageData(Math.round(ox), Math.round(oy), Math.round(side), Math.round(side));
    var data = img.data, filled = 0, total = data.length / 4;
    for (var p = 0; p < data.length; p += 4) {
      if (data[p] !== C_INK[0] || data[p + 1] !== C_INK[1] || data[p + 2] !== C_INK[2]) filled++;
    }
    areaEl.textContent = Math.round((filled / total) * 100) + '%';
  } catch (e) { /* キャンバス読み取り失敗時も画面はそのまま動作する */ }
}

実測でしか出せない残存率

「残った面積%」の表示は getImageData で読んだ実際のピクセルを数えた値で、アニメーションの進行度をそのまま数値化したものではありません。育ち切った後は 0.72〜1.0 の範囲で明るさがサイン波で呼吸し、累積面積16.167%・強度平均141.3と9項目中もっとも明るいコントラストになります。

06ドラゴン曲線 — 折り紙の規則が育てる紙の恐竜

紐を半分に折る規則を文字に写すと、紙を折って作ったドラゴンが画面に現れます。Lシステムの2文字(X・Y)の相互書き換えで描く代表応用です。

ドラゴン曲線 — 折り紙の規則が育てる紙の恐竜
新しいタブで開く

06 · ドラゴン曲線 — 折り紙の規則が育てる紙の恐竜 — 計測 累積面積 5.192% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

Math.sinMath.coslineTorequestAnimationFrame

紙を半分に折る規則

axiom FX の一行に X→X+YF+、Y→-FX-Y の2つの規則を繰り返し適用すると、紙を半分に折り続けた跡のように重ならずぎっしり折り重なる曲線が育ちます。角度は90度固定です。

var RULE = { 'X': 'X+YF+', 'Y': '-FX-Y' };
function expand(axiom, gen) {
  var s = axiom;
  for (var g = 0; g < gen; g++) {
    var out = '';
    for (var i = 0; i < s.length; i++) {
      var ch = s.charAt(i);
      out += RULE[ch] !== undefined ? RULE[ch] : ch;
    }
    s = out;
  }
  return s;
}

11世代まで伸ばす理由

世代を0→11まで伸ばすのは、折り重なりの密度が最後まで満ちる境目がそのあたりにあるからです。9項目の中でもっとも高い世代数ですが、文字列は世代ごとにしか展開しないので、11世代まで進めても毎フレームの計算量は一定に保たれます。

07パラメータ植物 — F+-[] 文字列が育つ葉

括弧で枝を覚える文字列に世代と角度を付けると観葉植物の葉が広がります。数文字変えるだけで別の植物になる生成文法の実験項目です。

パラメータ植物 — F+-[] 文字列が育つ葉
新しいタブで開く

07 · パラメータ植物 — F+-[] 文字列が育つ葉 — 計測 累積面積 6.449% · 動いたフレーム 22/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

Math.sinMath.coslineToglobalAlpha

角度が揺れる生成文法

axiom X に X→F+[[X]-X]-F[-FX]+X、F→FF の2規則を世代0→5で適用すると葉が広がります。育ち切った後は基準の分岐角25度がサイン波で±6度の帯を揺れ、「同じ文法・異なるパラメータ=異なる植物」をリアルタイムで示します。

var RULE = { 'X': 'F+[[X]-X]-F[-FX]+X', 'F': 'FF' };
function expand(axiom, gen) {
  var s = axiom;
  for (var g = 0; g < gen; g++) {
    var out = '';
    for (var i = 0; i < s.length; i++) {
      var ch = s.charAt(i);
      out += RULE[ch] !== undefined ? RULE[ch] : ch;
    }
    s = out;
  }
  return s;
}
var STRS = [];
for (var g0 = 0; g0 <= MAXGEN; g0++) STRS.push(expand('X', g0));

深さで薄れる葉

STRS 配列は世代0からMAXGENまでの文字列をあらかじめ全部展開して保存したもので、毎フレーム再展開しません。葉は枝の深さに応じて globalAlpha を下げることで、奥の枝ほど薄く見えるようにしています。

08風に揺れる木 — 世代ごとに位相がずれる揺れ

枝の深さごとに風の位相を少しずつずらすと、幹はゆっくり、先端は速く揺れます。世代番号を振動パラメータに使う計算が乗る、生きた項目です。

風に揺れる木 — 世代ごとに位相がずれる揺れ
新しいタブで開く

08 · 風に揺れる木 — 世代ごとに位相がずれる揺れ — 計測 累積面積 1.706% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

Math.sinlineTorequestAnimationFrameglobalAlpha

深さの2乗に比例する振幅

規則 F→F[+F]F-Fはそのままに、タートルが一歩ごとに曲げる角度へ、枝の深さに比例する振幅(最大30度・周期あたり1.6回の振動)のサイン波を足します。枝の長さも深さごとに0.78の深さ乗で縮み、根元はほとんど動かず枝先だけが大きく揺れます。

function render(loop) {
  var ctx = cv.getContext('2d');
  ctx.fillStyle = rgb(C_INK, 1);
  ctx.fillRect(0, 0, W, H);

  var sc = Math.min(W, H) * 0.24;
  var ox = W * 0.5, oy = H * 0.95;
  var windPhase = loop * Math.PI * 2 * WIND_HZ;
  var windGust = 0.55 + 0.45 * Math.sin(loop * Math.PI * 2 * 0.5);   // 突風の強さ自体もゆっくり上下する
  var windAmp = WIND_AMP * windGust;

  if (windEl) windEl.textContent = String(Math.round(windGust * 10));

  drawWind(ctx, STR, 0, 0, 1, ox, oy, sc, windPhase, windAmp, windGust);
}

根元と枝先の位相差

風の揺れ角は windAmp * depthT * Math.sin(windPhase + depth * 0.8) という式で計算します。深さ(depth)が位相そのものをずらす項になっているため、根元(depth=0)と枝先で山と谷の位置がわずかにずれ、風が波として枝を渡っていくように見えます。

09表紙の森 — ブランド行の上に育つフラクタルの森

1つの種から複数の木がそれぞれ別の規則で育ち、文の上に届く場面を表紙に置くと、初訪問で画面が生きて見えます。計算シーン1つがサービスの第一印象になる組立例です。

表紙の森 — ブランド行の上に育つフラクタルの森
新しいタブで開く

09 · 表紙の森 — ブランド行の上に育つフラクタルの森 — 計測 累積面積 11.5% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

Math.sinlineTocreateRadialGradientrequestAnimationFrame

5本の木と3つの規則

木は5本あり、それぞれ F[+F]F[-F]・F[+F][-F]F・FF[+F][-F] の3種類の規則を順に使って世代0→4まで同時に育ちます。木ごとに角度(20+(i%3)5)・大きさ(0.85+(i%2)0.22)・成長開始の遅延(0.06×i)が異なり、同じ種のコピーではなく本当に異なる規則で育っているように見えます。

var RULES = [
  { 'F': 'F[+F]F[-F]' },
  { 'F': 'F[+F][-F]F' },
  { 'F': 'FF[+F][-F]' }
];
function expand(rule, gen) {
  var s = 'F';
  for (var g = 0; g < gen; g++) {
    var out = '';
    for (var i = 0; i < s.length; i++) {
      var ch = s.charAt(i);
      out += rule[ch] !== undefined ? rule[ch] : ch;
    }
    s = out;
  }
  return s;
}

ビネットが呼吸する仕組み

背後の createRadialGradient によるスポットライトは、成長が満ちるほど明るく広がるビネットです。累積面積は11.5%(面積最大9.198%)で、これは9本の線ではなく5本の木が同時に重なって育つためで、9項目の中では05カーペットに次いで高い値です。

expand() をループの中で呼ぶと壊れる

曲線関数を構造線に使うこと

この画面の曲線は文字列書き換えが作った歩行順の結果です。arc や quadraticCurveTo で枝そのものを描いてしまうと、書き換え規則がただの絵に置き換わり、文字が育つ意味が消えます。

毎フレーム文字列を再展開すること

規則によって文字列の長さは世代ごとに急増します。progress の値に合わせて毎フレーム expand() を呼び直すと、世代が進むほど計算が重くなりフレームが落ちます。世代ごとの文字列は最初に全部展開して配列に保存しておく必要があります。

05の残存率をトゥイーンの数値で見せかけること

getImageData で実際に塗られたピクセルを数えなければ、「残った面積%」の表示はただのアニメーション進行度になってしまいます。実測することそのものがこの項目の意味です。

04の2文字規則を1つに統合すること

A→B-A-B、B→A+B+A は互いを書き換え合うからこそ三角形の隙間が埋まります。同じ形に見えるからと1文字の規則にまとめると、まったく別の曲線になります。

08の風の振幅を深さと無関係に固定すること

根元がほとんど動かず枝先だけが大きく揺れてこそ「風」に見えます。振幅を深さに関係なく一定にすると、木全体が同じ幅で揺れるただの硬い振動になってしまいます。

9つの項目のコードはコメントまでそのまま zip に収まっているので、z8hwyvrwを読んで開けば、この落とし穴を踏まずに同じ計算をそのまま自分のキャンバスへ持ち帰れます。

なぜ数文字の規則が木になるのか — 書き換えと自己相似

9項目すべてに共通しているのは、画面上の曲線を1行も直接描いていないということです。あるのは axiom という開始文字列と、文字ごとの置き換え規則だけで、その規則を世代の数だけ繰り返し適用した結果の文字列を、タートル(F=1歩前進してlineTo、+/-=固定角度の回転、[ ]=位置と向きのスタックpush/pop)がなぞった軌跡がそのまま枝や海岸線や曲線になります。世代を1つ増やすと文字列の中に同じ規則がもう一段入れ子で現れるため、拡大しても縮小しても似た形が繰り返される「自己相似」が自然に生まれます。構造を担う線はすべて lineTo による直線で、arc・quadraticCurveTo のような曲線関数は種の点や強調の光点のような装飾にしか使っていません。文字列の長さは規則によって世代ごとに2〜3倍で増えるため、9項目とも世代ごとの文字列をあらかじめ配列に保存しておき、毎フレームは保存済みの点をなぞるだけにして計算の暴走を防いでいます。

この画面はどこで使う — ブランド導入とローディング画面

9項目の中で最初に貼る場所として向いているのは、09 表紙の森のようなヒーローセクションです。ロゴやブランド行の上で木が育つ数秒がそのままローディング時間になり、裏で次のページを読み込む間、訪問者を自然に引き止めます。01 再帰枝のように最小構成の項目はローディングスピナーの置き換えにも使いやすく、規則の文字を数文字変えるだけで枝の姿が変わるので、同じコードを貼っても他のサイトと同じには見えません。

07 パラメータ植物は逆に、操作できる場所に向いています。角度や世代数をスライダーで変えられるようにすれば、訪問者が自分で植物を育てるミニツールになり、園芸系の商品紹介の横やコーディング学習ページの実習例として貼ると、同じコードがまったく違う役割を果たします。

枝の角度と reduced-motion — 3つの変形が変える見え方

名前 値 感じ方
枝の角度 01 角度25度→45度 枝が大きく広がり、針葉樹から広葉樹へ姿を変える
再帰の深さ 07 世代4→8 世代が3つ増えると葉が画面を埋め、計算量は2の累乗で跳ね上がる
縮小比率 08 長さ比率0.72→0.85 枝が縮まないほど森が密になり、揺れが重くゆっくりに感じられる

定数を1行だけ差し替える

どの変形も、コード中の定数ブロック(ANGLE・MAXGEN・0.78のような縮小比率)を1行書き換えるだけで試せます。文字列書き換えの規則そのものは変えていないので、育つ図形の種類は同じまま姿だけが変わります。

reduced-motionで残る完成形

@media (prefers-reduced-motion: reduce) を検知した環境ではループを回さず、固定した一場面だけを描いて止め、ルート要素に .is-still クラスを付けます。8項目はMAXGENまで育ち切った姿を固定しますが01だけは世代が1つ足りない枝を固定するので、静止していても画面に残るのは空のキャンバスではなく育った木や海岸線や曲線の形です。

L システムの木を試す前によくある質問

zipにはReact版も入っていますか?

はい。バニラJavaScript版9種とReact版9種の両方が同じzipに入っています。どちらも同じ規則・同じ測定値で動きます。

シェルピンスキー四面体も作れますか?

このzipに入っているのは平面(2D)の9種だけです。シェルピンスキー四面体は3次元への投影計算が別途必要になるため、今回のキャンバス実装には含まれていません。

世代数をどこまで増やせますか?

規則によって文字列は世代ごとに2〜3倍で伸びるため、際限なく増やすと配列の保存に使うメモリと初回展開の計算時間が急に重くなります。9項目中もっとも世代数が多いのは06ドラゴン曲線の11世代で、それより先はブラウザのタブごとに試すことをおすすめします。

もっと読む

タートルの直線描画は moveTo・lineTo、位置と向きの保存/復元は save・restore、ループは requestAnimationFrame が土台です。仕様は MDN の lineTo と MDN の requestAnimationFrame にまとまっています。文字列書き換えの一般的な定義は Wikipedia の L-system を参照してください。

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

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