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

ボロノイ図 書き方 9選 — Canvas生成

ボロノイ図は線を引く絵ではない — すべての点が最も近いシード座標値を見つけて塗られた結果だ。その座標値がひとつ動くだけで、1番はセルの色を塗り直し、5番は境界線を押しのけ、9番は場面全体を再計算する。この記事は「ボロノイ図 書き方」を探して来た方に向けて、9枚のキャンバスをソースコードごと見せます。

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

以下の9枚は、どれも一つの最近傍計算が9通りの見た目をまとっているだけです。1番から3番まではシードを動かさず、同じ境界の隙間に起きることだけを変えています(平らなモザイク、インクで滲むひび、掃引するガラスパネル)。4番はセルの形そのものを、その双対にあたる三角メッシュへ置き換えます。5番はポインターの下で動くシードを一つ足し、6番は塗りを剥がして境界だけを残し、7番は毎フレームの再計算をあらかじめ済ませた緩和シーケンスに置き換え、8番は測る前にサンプル空間そのものをノイズで歪め、9番はそれら三つの計算を一つのヒーロー場面に重ねます。

ランディングヒーローやローディング画面で、実際にどう使うか

1番のモザイクと6番のネットワーク型セルは、どちらもランディングページの見出し裏で静かに座っている背景装飾として使えます。平坦なグラデーションを置く代わりにこの二つを敷くと、見出しの後ろに視線を邪魔しない程度の動きが生まれます。2番の乾いた大地の質感と7番の緩和シーケンスは、ブランドイントロやローディング完了の切り替え演出に向いています——コンテンツが表示される瞬間に、模様がちょうど整っていくように見えるからです。3番のステンドグラスパネルと8番の迷彩ワープは、プレミアムなカード背景やアウトドア・ファッション系の商品パネルとして機能します。質感そのものが主役になり、文字を乗せる器で終わらない点が強みです。

4番の三角メッシュはデータ可視化の背景に合います。塗りつぶされたセルが「領域」を連想させるのに対し、メッシュの線は「構造」を連想させるからです。5番と9番はポインターと直接やり取りする唯一の二つで、カーソルの動きに応じて配置そのものが変わるヒーロー演出は、repelSeeds()やapplyPointerRepel()の計算コストを払ってでも作る価値があります。訪れた人自身のマウス操作が、そのまま画面の一部になるからです。

9枚のボロノイセルを塗っているのは、結局どんな計算なのか

以下の9枚はすべて、表示サイズそのものではなく160×100の非表示キャンバスの上で計算しています。nearestTwo()が、あるピクセルに対してすべてのシードとの距離の二乗を総当たりで調べ、最も近いシード(ND)と次点(ND2)だけを残します。その小さなバッファを画面サイズへ拡大する際はimageSmoothingEnabledをfalseにしているため、ぼかされず輪郭がくっきりしたまま引き伸ばされます。ここには時計の値もクリック数も現実のデータセットも入っていません——各項目は409011から409091まで、項目ごとに固定した整数で自前の乱数生成器を種付けしているため、同じモザイクも同じひび割れも同じ三角メッシュも、リロードのたびに同一の形で描かれます。これら9枚が書き込むピクセルバッファについては、MDNのCanvas APIの概要が、renderField()が描画に使っている2Dコンテキストをまとめています。

01シード個数値の分だけ色が満ちるモザイクセル

各シード座標値が最も近い領域を多角形セルとして分け、各セルに固有の色を満たして背景モザイクを作ります。この塗りはnearestTwo()が担っています——各ピクセルとすべてのシードとの距離を二乗のまま比較して、最も近いシード(ND)と次点(ND2)を求め、その差edge = ND2 - NDが1.5を下回った箇所だけ隣のセルの色へ向けて暗く落とします。

シード個数値の分だけ色が満ちるモザイクセル
新しいタブで開く

01 · シード個数値の分だけ色が満ちるモザイクセル — 計測 累積面積 60.149% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

VORONOI_SEED_COUNTdriftSeedsmosaicPalette
  function renderField(t) {
    driftSeeds(t);
    var idx = 0;
    for (var y = 0; y < CALC_H; y += 1) {
      for (var x = 0; x < CALC_W; x += 1) {
        nearestTwo(x + 0.5, y + 0.5, seeds);
        var rgb = paletteRgb[seeds[NEI].c];
        var edge = ND2 - ND;
        var dark = edge < 1.5 ? 0.5 + edge / 3 : 1;
        offBuf[idx] = rgb[0] * dark;
        offBuf[idx + 1] = rgb[1] * dark;
        offBuf[idx + 2] = rgb[2] * dark;
        offBuf[idx + 3] = 255;
        idx += 4;
      }
    }
  }

収録したフレームでは累積面積60.1%・強度平均84.0、動いたフレームは23/23でした。

02境界線太さ値が乾いた大地のように割れるセル

セル境界線の太さ値と明るさ値を組み合わせ、乾いた土が割れたような質感を作ります。同じnearestTwo()の差を使いますが、edgeがCRACK_WIDTH_VALUEを下回ると、正弦波crackPulsePhaseの値でインク色へ混ぜ込み、ひびが呼吸するように濃淡を変えます。

境界線太さ値が乾いた大地のように割れるセル
新しいタブで開く

02 · 境界線太さ値が乾いた大地のように割れるセル — 計測 累積面積 73.787% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

CRACK_WIDTH_VALUEcrackPulsePhasedriftSeeds
  function renderField(t) {
    driftSeeds(t);
    var crackPulsePhase = 0.5 + 0.5 * Math.sin(t * PULSE_FREQ);
    var idx = 0;
    for (var y = 0; y < CALC_H; y += 1) {
      for (var x = 0; x < CALC_W; x += 1) {
        nearestTwo(x + 0.5, y + 0.5, seeds);
        var rgb = paletteRgb[seeds[NEI].c];
        var band = 0.85 + 0.15 * Math.sin(ND * 0.4 + NEI);
        var edge = ND2 - ND;
        var r, g, b;
        if (edge < CRACK_WIDTH_VALUE) {
          var m = 0.15 + 0.55 * crackPulsePhase;
          r = rgb[0] + (INK_RGB[0] - rgb[0]) * m;
          g = rgb[1] + (INK_RGB[1] - rgb[1]) * m;
          b = rgb[2] + (INK_RGB[2] - rgb[2]) * m;
        } else {
          r = rgb[0] * band; g = rgb[1] * band; b = rgb[2] * band;
        }
        offBuf[idx] = r; offBuf[idx + 1] = g; offBuf[idx + 2] = b; offBuf[idx + 3] = 255;
        idx += 4;
      }
    }
  }

測定パスは累積面積73.8%・強度平均47.9、動いたフレームは23/23でした。

03セルごとに光透過値が異なるステンドグラスパネル

各セルの色値と枠太さ値をガラスパネルのように配置し、光が通るステンドグラスの雰囲気を作ります。このシードは動かないため、nearestTwo()は一度だけ実行されてcellIdxとedgeValという二つの配列に結果をキャッシュします。以降の毎フレームは、その結果を使ってピクセルをスイープ角へ射影しガウス状の光の帯を足すだけなので、毎フレームの計算コストは光の帯一つ分に抑えられています。

セルごとに光透過値が異なるステンドグラスパネル
新しいタブで開く

03 · セルごとに光透過値が異なるステンドグラスパネル — 計測 累積面積 72.633% · 動いたフレーム 20/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

GLASS_BORDER_PXlightSweepAnglestainedPalette
  (function precompute() {
    var idx = 0;
    for (var y = 0; y < CALC_H; y += 1) {
      for (var x = 0; x < CALC_W; x += 1) {
        nearestTwo(x + 0.5, y + 0.5, seeds);
        cellIdx[idx] = NEI;
        edgeVal[idx] = ND2 - ND;
        idx += 1;
      }
    }
  })();
  var lightSweepAngle = 0.62;
  var cosA = Math.cos(lightSweepAngle), sinA = Math.sin(lightSweepAngle);
  var projSpan = Math.abs(CALC_W * cosA) + Math.abs(CALC_H * sinA);

測定パスは累積面積72.6%・強度平均42.4で、動いたフレームは20/23でした——9枚の中で最も少ないのは、セルの配置自体は変わらず光の帯だけが横切るためです。

04頂点の揺れ値でモーフィングするドロネー三角メッシュ

同じシード座標値から作った三角メッシュの頂点の揺れ値が時間とともに変わり、面がゆっくりと波打ちます。項目4は最近傍探索の代わりに、毎フレームDelaunay三角分割をゼロから組み直します——circumcircle()が3点を通る外接円を解き、delaunayTriangulate()は次に揺れて入ってきた点がその外接円の内側に入る三角形をすべて壊して作り直します。

頂点の揺れ値でモーフィングするドロネー三角メッシュ
新しいタブで開く

04 · 頂点の揺れ値でモーフィングするドロネー三角メッシュ — 計測 累積面積 66.146% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

VERTEX_JITTER_PXdelaunayTriangulatecircumcircle
  function circumcircle(a, b, c) {
    var ax = a[0], ay = a[1], bx = b[0], by = b[1], cx = c[0], cy = c[1];
    var d = 2 * (ax * (by - cy) + bx * (cy - ay) + cx * (ay - by));
    if (Math.abs(d) < 1e-6) return null;
    var ux = ((ax * ax + ay * ay) * (by - cy) + (bx * bx + by * by) * (cy - ay) + (cx * cx + cy * cy) * (ay - by)) / d;
    var uy = ((ax * ax + ay * ay) * (cx - bx) + (bx * bx + by * by) * (ax - cx) + (cx * cx + cy * cy) * (bx - ax)) / d;
    var r2 = (ux - ax) * (ux - ax) + (uy - ay) * (uy - ay);
    return { x: ux, y: uy, r2: r2 };
  }

測定パスは累積面積66.1%・強度平均75.1、動いたフレームは23/23でした——9枚の中で唯一、ピクセルバッファではなく三角形のパスをctx.fill()で55%の不透明度で塗り、そのうえからctx.stroke()で辺を描いています。

05ポインターが一つのシードとなり境界を押すセル

ポインター位置値を一時的なシードとして加え、カーソルが近づくほど周囲のセル境界値がその分押しのけられます。repelSeeds()は最近傍探索より先に走ります。実ポインター(操作していないときは仮想的に周回する点)との距離を各シードごとに測り、SEED_REPEL_RADIUS_PXの内側にあるシードだけをpush²の減衰で押し出してから、図形を描き始めます。

ポインターが一つのシードとなり境界を押すセル
新しいタブで開く

05 · ポインターが一つのシードとなり境界を押すセル — 計測 累積面積 54.09% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

SEED_REPEL_RADIUS_PXrepelSeedsvirtualPointerOrbit
  function repelSeeds(pointerCalc) {
    var radiusCalc = SEED_REPEL_RADIUS_PX * (CALC_W / cw);
    seeds.forEach(function (s) {
      var dx = s.bx - pointerCalc.x, dy = s.by - pointerCalc.y;
      var dist = Math.sqrt(dx * dx + dy * dy) || 0.0001;
      if (dist < radiusCalc) {
        var push = (radiusCalc - dist) / radiusCalc;
        var k = push * push * 0.95;
        s.x = s.bx + (dx / dist) * k * radiusCalc;
        s.y = s.by + (dy / dist) * k * radiusCalc;
      } else {
        s.x = s.bx; s.y = s.by;
      }
    });
  }

測定パスは9枚の中で最も軽く、累積面積54.1%・強度平均103.4——ここが9枚のうちいちばん高い強度で、動いたフレームは23/23でした。

06境界線だけが残り光るネットワークセル

セルの塗り値を消して境界線の明るさ値だけを残すことで、図が技術系ネットワーク図のように光ります。項目1と同じedge = ND2 - NDという差を使いますが、セルを塗りつぶす代わりに背景色からセルの色へ直接ブレンドするため、実際に見えるのは境界の帯だけになります。

境界線だけが残り光るネットワークセル
新しいタブで開く

06 · 境界線だけが残り光るネットワークセル — 計測 累積面積 63.17% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

EDGE_GLOW_WIDTHdriftSeedsglowPalette
  function renderField(t) {
    driftSeeds(t);
    var idx = 0;
    for (var y = 0; y < CALC_H; y += 1) {
      for (var x = 0; x < CALC_W; x += 1) {
        nearestTwo(x + 0.5, y + 0.5, seeds);
        var edge = ND2 - ND;
        var r, g, b;
        if (edge < EDGE_GLOW_WIDTH) {
          var glowT = 1 - edge / EDGE_GLOW_WIDTH;
          var rgb = paletteRgb[seeds[NEI].c];
          r = BG_RGB[0] + (rgb[0] - BG_RGB[0]) * glowT;
          g = BG_RGB[1] + (rgb[1] - BG_RGB[1]) * glowT;
          b = BG_RGB[2] + (rgb[2] - BG_RGB[2]) * glowT;
        } else {
          r = BG_RGB[0]; g = BG_RGB[1]; b = BG_RGB[2];
        }
        offBuf[idx] = r; offBuf[idx + 1] = g; offBuf[idx + 2] = b; offBuf[idx + 3] = 255;
        idx += 4;
      }
    }
  }

測定パスは累積面積63.2%・強度平均84.7、動いたフレームは23/23でした。

07反復回数値が増えるほど均等に整うセル

緩和の反復回数値が増えるたびにシード座標値が各セルの中心へ寄り、セルの大きさが次第に均等に整います。computeCentroids()は各ピクセルの座標を、そのピクセルを取ったシードごとに合計してから平均し、新しいシード位置を作ります。この処理をLLOYD_RELAXATION_STEPS回あらかじめ繰り返して並びを作っておき、renderField()は再生時間の進みに合わせてその並びの間を補間します。

反復回数値が増えるほど均等に整うセル
新しいタブで開く

07 · 反復回数値が増えるほど均等に整うセル — 計測 累積面積 36.754% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

LLOYD_RELAXATION_STEPScomputeCentroidsrelaxationStates
  function computeCentroids(positions) {
    var sumX = new Array(positions.length).fill(0);
    var sumY = new Array(positions.length).fill(0);
    var cnt = new Array(positions.length).fill(0);
    for (var y = 0; y < CALC_H; y += 1) {
      for (var x = 0; x < CALC_W; x += 1) {
        nearestTwo(x + 0.5, y + 0.5, positions);
        sumX[NEI] += x + 0.5; sumY[NEI] += y + 0.5; cnt[NEI] += 1;
      }
    }
    return positions.map(function (p, i) {
      if (!cnt[i]) return { x: p.x, y: p.y, c: p.c };
      return { x: sumX[i] / cnt[i], y: sumY[i] / cnt[i], c: p.c };
    });
  }

測定パスは9枚で最も穏やかで、累積面積36.8%・強度平均57.1、動いたフレームは23/23でした——ほぼ緩和し終えた並びは、保存された状態の間で動きが小さくなるためです。

08セルサイズのばらつき値で作る迷彩柄

シード密度値を領域ごとに変え、セルサイズのばらつき値を大きくすることで、結果が動物の迷彩柄のように不規則になります。最近傍探索が走る前に、warpCoord()が9×6のランダム格子をバイリニア補間するvalueNoise2D()でサンプル座標そのものを歪めるため、本来同じセルに入るはずの2ピクセルが別のセルへ振り分けられることがあります。

セルサイズのばらつき値で作る迷彩柄
新しいタブで開く

08 · セルサイズのばらつき値で作る迷彩柄 — 計測 累積面積 27.205% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

NOISE_WARP_PXvalueNoise2DwarpCoord
  function valueNoise2D(gx, gy, arr) {
    var x0 = Math.floor(gx), y0 = Math.floor(gy);
    var x1 = Math.min(NOISE_GRID_W - 1, x0 + 1), y1 = Math.min(NOISE_GRID_H - 1, y0 + 1);
    var tx = gx - x0, ty = gy - y0;
    var v00 = arr[y0 * NOISE_GRID_W + x0], v10 = arr[y0 * NOISE_GRID_W + x1];
    var v01 = arr[y1 * NOISE_GRID_W + x0], v11 = arr[y1 * NOISE_GRID_W + x1];
    var top = v00 + (v10 - v00) * tx, bot = v01 + (v11 - v01) * tx;
    return top + (bot - top) * ty;
  }
  function warpCoord(x, y, breathe) {
    var gx = x / CALC_W * (NOISE_GRID_W - 1), gy = y / CALC_H * (NOISE_GRID_H - 1);
    var n1 = valueNoise2D(gx, gy, noiseGridX), n2 = valueNoise2D(gx, gy, noiseGridY);
    return { x: x + (n1 - 0.5) * 2 * NOISE_WARP_PX * breathe, y: y + (n2 - 0.5) * 2 * NOISE_WARP_PX * breathe };
  }

測定パスは9枚で最も静かで、累積面積27.2%・強度平均37.7、動いたフレームは23/23でした——ノイズのワープはセル境界を小さく揺らすだけで、領域全体の大きさは変えないためです。

09複数の値が一つの画面で共に反応するヒーローシーン

シード座標値・ポインター反発値・緩和反復値を一つの場面に重ね、背景全体が生きて動くボロノイヒーローを作ります。renderField()はここまでの計算を一つのフレームに重ねます。relaxNudge()が各シードを生の位置と二重に緩和した位置の間でブレンドし、そのあとapplyPointerRepel()がカーソル付近に残っているシードだけを押しのけてから、最後にnearestTwo()が走ります。

複数の値が一つの画面で共に反応するヒーローシーン
新しいタブで開く

09 · 複数の値が一つの画面で共に反応するヒーローシーン — 計測 累積面積 72.469% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

HERO_SEED_COUNTapplyPointerRepelrelaxNudge
  function applyPointerRepel(pointerCalc) {
    var radiusCalc = HERO_REPEL_RADIUS_PX * (CALC_W / cw);
    seeds.forEach(function (s) {
      var dx = s.x - pointerCalc.x, dy = s.y - pointerCalc.y;
      var dist = Math.sqrt(dx * dx + dy * dy) || 0.0001;
      if (dist < radiusCalc) {
        var push = (radiusCalc - dist) / radiusCalc;
        var k = push * push * 0.85;
        s.x += (dx / dist) * k * radiusCalc;
        s.y += (dy / dist) * k * radiusCalc;
      }
    });
  }

測定パスは累積面積72.5%・強度平均89.4——5番に次いで9枚中2番目に高い強度で、動いたフレームは23/23でした。緩和と反発を同じピクセルループへ重ねた分だけ負荷も上がっています。

境界線を動かす3つの値は、実際に何を担っているか

VORONOI_SEED_COUNT(1番、既定24)は、ほかの何よりも先に多角形がいくつ存在するかを決めます——これは動きではなく図の構造そのものを変える、唯一の値です。SEED_REPEL_RADIUS_PX(5番、既定120px、CALC_W / cw比で160×100の計算空間へ換算)は、ポインターの影響がカーソルからどれだけ離れた場所まで届くかを決めます。LLOYD_RELAXATION_STEPS(7番、既定6)は、再生が始まる前に重心平均化を何回済ませておくかを決めます——これが、1番の生モザイクより7番の緩和済みセルのほうが大きさが揃って見える理由です。

モーション低減設定・非力な端末・タッチ入力でどこが壊れるか

9枚すべてが最初のフレームより先にprefers-reduced-motionを確認しますが、そこで止まる静止状態は項目ごとにそろっていません——ここが本当の落とし穴です。多くの項目はループの半分の位置で止まりますが、2番はパルスの4分の1の位置で止まるため多くの人が動画の途中で見るより淡いひび割れになり、7番は緩和シーケンスの97%まで進んだ位置で止まるためほぼ最も整ったセル配置になり、5番はポインター自体を画面外の座標へ追い出すため、モーション低減を求めた人は反発をまったく見ず、ただ散らばったシードだけを見ることになります。ある項目のモーション低減分岐を別の項目へそのままコピーすると、間違った静止フレームを再現してしまいます。

nearestTwo()の走査だけでも、160×100の計算キャンバスにある16,000ピクセルすべてに対して最大32個のシードを毎フレーム比較します——9枚のうち最も重いのは、毎フレームゼロから作り直す4番のドロネー分割と、緩和と反発を重ねる9番で、非力な端末ではこの負荷がまず滑らかなループではなくフレーム落ちとして現れます。タッチ入力には専用の分岐がありません——pointermoveとpointerleaveはマウスと同じように指にも発火しますが、画面から指が離れる瞬間はpointerleaveと同じ扱いになるため、5番と9番はタッチ端末ではほとんど反発ではなく、待機中の仮想周回ポインターだけを見せることになります。実際のクリック位置も、実際の決済金額も、実際の利用者記録もここには一切登場しません——そのギャップが特定のレイアウトで重要になる場合、zipの中のy8nwxyuaが現実の画面に必要なものです。

9枚のボロノイキャンバスは、フレームごとに実際どれだけ動いたか

項目 累積面積 強度 動いたフレーム
01 シード個数値の分だけ色が満ちるモザイクセル 60.1% 84.0 23/23
02 境界線太さ値が乾いた大地のように割れるセル 73.8% 47.9 23/23
03 セルごとに光透過値が異なるステンドグラスパネル 72.6% 42.4 20/23
04 頂点の揺れ値でモーフィングするドロネー三角メッシュ 66.1% 75.1 23/23
05 ポインターが一つのシードとなり境界を押すセル 54.1% 103.4 23/23
06 境界線だけが残り光るネットワークセル 63.2% 84.7 23/23
07 反復回数値が増えるほど均等に整うセル 36.8% 57.1 23/23
08 セルサイズのばらつき値で作る迷彩柄 27.2% 37.7 23/23
09 複数の値が一つの画面で共に反応するヒーローシーン 72.5% 89.4 23/23

押す力を二乗で効かせる5番が、9枚の中でいちばん高い103.4という強度を記録しています——累積面積は塗りつぶし系より小さくても、少数のシードを強く押す動きは、画面全体をゆるく塗り直す動きに勝ります。

シード値をひとつだけ動かす3つのバリエーション

バリエーション 値 感触
シード個数値を調整する 01 VORONOI_SEED_COUNT 24 → 60 値を短くすると画面は少数の大きく疎なセルに分かれて単純に見え、値を伸ばすと多数の小さく密なセルに分かれて複雑なモザイクのように見えるはずです。
ポインター反発半径値を調整する 05 SEED_REPEL_RADIUS_PX 120 → 240 値を短くするとカーソルのすぐ周りのセルだけが狭く押しのけられ、値を伸ばすと画面の広い範囲のセルまで一緒に押しのけられるはずです。
ロイド緩和の反復回数値を調整する 07 LLOYD_RELAXATION_STEPS 6 → 20 値を短くするとセルの大きさはばらついたまま粗く見え、値を伸ばすとセルの大きさが次第に均等に整いなめらかに見えるはずです。

アクセシビリティ: prefers-reduced-motionが9つの静止状態から実際に消すもの

prefers-reduced-motion: reduceが設定されると、playingがfalseになり、9枚すべてが最初の静止フレームを描いたあとrequestAnimationFrameの呼び出しをやめます——ルート要素からis-demoクラスも外れ、付随スタイルシートにあるホバー用カーソルスタイルも消えます。モーションを減らしてほしいと伝えた人は、ドリフトもひび割れの脈動も光のスイープも、9番の緩和ブレンドも一切見ることなく、その項目のコードが割り当てた固定のelapsed値の上にそのまま着地します。だからこそ静止フレームは項目ごとに違って見え、9枚すべてが同じ一時停止の見た目にそろうわけではありません。

ボロノイ図 アルゴリズムは、最近傍探索としてここにどう現れているか

パレットと揺れを取り除けば、これは結局こういう仕組みです——平面上のどの点についても、あらかじめ決めたシードの集合のうち最も近い一つを見つけ、その点をどのシードが勝ったかで塗ります。nearestTwo()は、三角分割を使わないこのセットのすべての項目が呼び出す唯一の関数で、外部の数学ライブラリを使わない総当たりの実装です——シードについての入れ子ループを、ピクセルについての入れ子ループの中に置き、それまでに見た最小と二番目に小さい距離の二乗だけを残します。9枚のそれぞれは、しきい値・半径・反復回数・ループが走る前に加えるワープのいずれか一つの値だけを、その同じ中心ループから曲げて作った小さなボロノイ パターンの実例です。一言でまとめるなら、色を塗る前に距離を測る、それだけです。

ほかにも似た仕組みの実例を探すなら

CSS 背景パターンジェネレーター 自作9種は、この9枚が属するシード付きキャンバス背景という、より広い系列全体をカバーしています——平坦な生成パターンで足りるなら、セル全体を計算するこの仕組みまでは要らないケースの比較に向いています。ローディングアニメーションjs 9パターン — 物理演算で作るは、最近傍のルールの代わりに実際のバネと重力の計算を使っていて、ここの5番が使う押す力の二乗による反発と見比べる価値があります。ポインターや値がキャンバスをゆがめる、二つの異なるやり方が見えてきます。ライフゲーム9種 — キャンバスで世代を計算は構造がいちばん近い兄弟編です——どちらの系列も、小さなルールから毎フレーム全ピクセルのグリッドを計算し直します。ただしライフゲームは各セルを近傍の状態から決めるのに対し、上の9枚はどれもピクセルをシードまでの純粋な距離だけから決めます。これら9枚が実際に書き込んでいるピクセルバッファの方式については、ここではcreateImageDataとputImageDataの組が使われていて、MDNのgetImageDataのリファレンスは、その組の読み取り側にあたる、書き込んだキャンバスを読み戻すための姉妹メソッドを説明しています。

ボロノイ図を自分のプロジェクトで動かす前によくある質問に答えます

この9枚は本当にボロノイ図を計算しているのですか、それとも見た目をそれらしく似せているだけですか

計算しています——nearestTwo()は、160×100の計算キャンバスにあるすべてのピクセルについて、すべてのシードとの距離の二乗を本当に比較し、どのシードが最も近いかだけでピクセルの色を決めています。グラデーションもborder-radiusの小細工も、あらかじめ焼き込んだ画像も、セルの形を偽装するためには使っていません。

9枚のうちどれかを、そのままsvg版として使えますか

そのままでは使えません。ここではどの項目もImageDataバッファへ直接ピクセルを描いていて、ベクターのpathデータを持っていないからです。シードの配置や最近傍のルール、VORONOI_SEED_COUNTやSEED_REPEL_RADIUS_PXといった値そのものはSVG版へも応用できますが、ピクセルを塗る部分のコードは多角形の座標計算として書き直す必要があります。

なぜ4番だけ、ほかの8枚のようなセルではなく三角形に見えるのですか

4番は、ほかの8枚と同じシード座標から作ったドロネー三角分割——ボロノイ図の双対グラフ——を描いているからです。ほかの項目が「このピクセルに最も近いシードはどれか」を問うのに対し、4番は「外接円の内側にほかのシードを含まない三角形はどの3点で作れるか」を問うという、同じ入力点に対してまったく別の計算をしています。

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

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