GODRICH

css 背景 パターン ジェネレーター 自作9種

css 背景 パターン ジェネレーターは、画像ファイルを配る代わりに数字ひとつから模様をその場で計算する仕組みです。ここで紹介する9つはシードひとつで絵が決まり、同じ数字を入れ直せば同じ絵が必ず戻ってきます。

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

9つを並べた基準は人気順ではなく、乱数をどこに使うかです。はじめの3つは乱数を画面全体にばらまきます — 格子ごとに高さを乱数で決めて等高線で切り(01)、散らした点ごとに領域を分け(02)、位置ごとに角度を決めて線を流します(03)。真ん中の3つは乱数を一度だけ使い、あとは規則が複製します — ひと筆を12回まわすマンダラ(04)、タイル1枚を4つの角度で置くだけのトルシェ(05)、位置に応じて傾けた確率でマスを塗る六角格子(06)。最後の3つは乱数を再帰に流し込みます — 切ってまた切る四角(07)、分かれてまた分かれる木(08)、そしてシードが数字ではなく文字列である名前の背景(09)。背景そのものを動かしたいなら CSS 背景 アニメーション 9選 が、同じ SVG で数値を描く話は css グラフ をSVGで描く9種 にあります。

01ノイズ等高線マップ

粗い格子の頂点にだけ乱数を置き、あいだを smoothstep でつなぐと、でこぼこした高さの地図になります。マーチングスクエアで「同じ高さ」を結ぶ線を6層取り出すやり方は、地形図の描き方そのものです。プレビューでは累積面積 11.93%・強度 94.5・移動フレーム 16/23 を記録しました。紹介ページ冒頭の背景に使います。

stroke-dasharraystroke-dashoffsetanimation-fill-mode
// mulberry32 — 32ビット整数のシードひとつで、いつも同じ乱数列を返す。再現できる生成の根っこ
function mulberry32(a) {
  return function () {
    a = a + 0x6D2B79F5 | 0;
    var t = Math.imul(a ^ a >>> 15, 1 | a);
    t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
    return ((t ^ t >>> 14) >>> 0) / 4294967296;
  };
}
@keyframes ncScan {
  0%   { stroke-dashoffset: 1; animation-timing-function: $easing; }
  38%  { stroke-dashoffset: 0; animation-timing-function: linear; }
  50%  { stroke-dashoffset: 0; animation-timing-function: $ease-out; }
  86%  { stroke-dashoffset: -1; }
  100% { stroke-dashoffset: -1; }
}

02ボロノイセルのタイル

点を26個ばらまき、それぞれに「自分にいちばん近い領域」を割り当てると、割れたガラスのような面になります。専用の幾何ライブラリを読み込む必要はありません。外側の四角をほかの点との垂直二等分線で順に切っていけば、残ったものがその点のセルです。累積面積 19.07%・強度 72.5・移動フレーム 23/23 で、料金表セクションの背景に向きます。

animation-delaytransform-boxtransform-origin
// 2点の垂直二等分線で多角形を切り、a 側の半平面だけ残す (サザーランド・ホッジマン法)
function clip(poly, a, b) {
  var mx = (a[0] + b[0]) / 2, my = (a[1] + b[1]) / 2;
  var dx = b[0] - a[0], dy = b[1] - a[1], out = [], i;
  function side(p) { return (p[0] - mx) * dx + (p[1] - my) * dy; }
  for (i = 0; i < poly.length; i++) {
    var p = poly[i], q = poly[(i + 1) % poly.length], sp = side(p), sq = side(q);
    if (sp <= 0) { out.push(p); }
    if ((sp < 0 && sq > 0) || (sp > 0 && sq < 0)) {
      var t = sp / (sp - sq);
      out.push([p[0] + (q[0] - p[0]) * t, p[1] + (q[1] - p[1]) * t]);
    }
  }
  return out;
}

03フローフィールドの線画

位置ごとに角度をひとつずつ決めておいた面(方向の場)の上で、短い歩幅を30回つなぐと長い曲線になります。54本の線の開始点はただの乱数ではなく揺らした格子から取っているので、片側に固まらず画面全体に散ります。累積面積 5.71%・強度 90.8・移動フレーム 23/23 で、音楽や展示ページの全面背景に使います。

stroke-linecapstroke-dashoffsetcubic-bezier
// ある点から始めて、その位置の角度へ一歩ずつ移りながら座標をつないでいく
function walk(angleAt, x, y) {
  var d = 'M' + x.toFixed(1) + ' ' + y.toFixed(1), i;
  for (i = 0; i < STEPS; i++) {
    var a = angleAt(x, y);
    x += Math.cos(a) * STEP;
    y += Math.sin(a) * STEP;
    if (x < -8 || x > W + 8 || y < -8 || y > H + 8) { break; }
    d += 'L' + x.toFixed(1) + ' ' + y.toFixed(1);
  }
  return d;
}

04回転対称のマンダラ

乱数を使う場所は層ごとに「中心から外へ伸びるひと筆」だけで、その層の残り11本はそれを30度ずつまわした複製です。対称は、乱数をほとんど使わずに模様を成立させるいちばん安い手です。3層・図形37個で累積面積 11.88%・強度 76.2・移動フレーム 23/23 を記録しました。瞑想アプリの起動画面や招待状の装飾リングに使います。

transform-originrotatestroke-linejoin
  for (ring = 0; ring < RINGS; ring++) {
    var d = stroke(rand, 62 - ring * 17), inner = '';
    for (arm = 0; arm < ARMS; arm++) {
      // ひと筆を30度ずつまわして重ねる — 回転対称は乱数を一度だけ使って模様にする、いちばん安い手
      inner += '<path transform="translate(' + CX + ' ' + CY + ') rotate(' + (arm * 360 / ARMS) + ')" d="' + d + '"/>';
    }
    parts.push('<g class="md__ring md__ring--' + (ring % 2 ? 'b' : 'a') + '" style="--i:' + ring + '">' + inner + '</g>');
  }

05トルシェタイルの迷路

タイルは1枚しかありません。隣り合う辺の中点を結ぶ四分円を2つ描き、0・90・180・270度のいずれかで置くだけで、マスの境目を越えてつながる曲線が勝手にできます。60マスで累積面積 26.12%・強度 150.0・移動フレーム 22/23。404ページの背景やカード格子の後ろの模様に使います。

rotatetransform-originsteps
  for (row = 0; row < ROWS; row++) {
    for (col = 0; col < COLS; col++) {
      var turn = Math.floor(rand() * 4) * 90;
      var tone = rand() < 0.28 ? 'b' : 'a';
      // 外側の g は位置を決めるだけ(静的)。アニメーションが付く内側の g に CSS transform を書くと
      // transform 属性が上書きされ、タイルが自分の場所を失うため
      parts.push('<g transform="translate(' + col * SIZE + ' ' + row * SIZE + ')">' +
        '<g class="tr__tile tr__tile--' + tone + '" style="--i:' + (col * ROWS + row) + '">' +
        '<path transform="rotate(' + turn + ' 12 12)" d="' + TILE + '"/></g></g>');
    }
  }

06六角格子の塗り分け

マスの色を純粋な乱数だけで決めると、塩をまいたように散らばって模様に見えません。左から右へ傾けた確率に乱数を混ぜると、位置が結果に残り、色が流れているように見えます。六角形77個で累積面積 25.93%・強度 84.4・移動フレーム 23/23。ダッシュボードの空状態や統計ページのヘッダー帯に使います。

polygonscaleanimation-delay
// 平たい六角形の6頂点 — 0度から60度ずつ
function hexPoints(cx, cy) {
  var pts = [], k;
  for (k = 0; k < 6; k++) {
    var a = k * Math.PI / 3;
    pts.push((cx + R * Math.cos(a)).toFixed(1) + ',' + (cy + R * Math.sin(a)).toFixed(1));
  }
  return pts.join(' ');
}

07再帰分割の四角

大きな四角を一度切り、出てきた断片を同じ規則でまた切ります。関数が自分自身を呼びながら深さをひとつずつ減らす、それだけです。調整すべきは偏りで、長いほうを切る側へ確率を傾けないと細長い断片ばかりになります。断片27個で累積面積 14.92%・強度 77.2・移動フレーム 23/23。ポートフォリオの表紙に使います。

scaletransform-originanimation-fill-mode
// 自分自身を2回呼びながら深さをひとつずつ減らす。これ以上切れない大きさになったらその断片を結果に入れる
function split(x, y, w, h, depth, rand, out) {
  if (depth === 0 || (w < MIN * 2 && h < MIN * 2)) { out.push([x, y, w, h]); return; }
  var cutDown = w > h ? rand() < 0.82 : rand() < 0.18;   // 長いほうを切る側へ傾ける
  var t = 0.32 + rand() * 0.36;
  if (cutDown) {
    split(x, y, w * t, h, depth - 1, rand, out);
    split(x + w * t, y, w * (1 - t), h, depth - 1, rand, out);
  } else {
    split(x, y, w, h * t, depth - 1, rand, out);
    split(x, y + h * t, w, h * (1 - t), depth - 1, rand, out);
  }
}

08枝分かれする木

07 と同じ再帰ですが、切り込む代わりに伸びていきます。枝を1本描いてその先から自分自身を2回呼び、分かれるたびに長さを 0.70〜0.82 倍、太さを 0.72 倍に減らします。枝63本で累積面積 3.97%・強度 125.2・移動フレーム 16/23。環境ページの挿絵やローディング画面で育つ絵に使います。

scaletransform-originstroke-linecap
function branch(x, y, angle, len, width, depth, rand) {
  var x2 = x + Math.cos(angle) * len, y2 = y + Math.sin(angle) * len;
  var gen = DEPTH - depth;
  var inner = '<line class="bt__line bt__line--' + (gen > 3 ? 'b' : 'a') + '" x1="' + x.toFixed(1) +
    '" y1="' + y.toFixed(1) + '" x2="' + x2.toFixed(1) + '" y2="' + y2.toFixed(1) +
    '" stroke-width="' + width.toFixed(2) + '"/>';
  if (depth > 0) {
    var spread = 0.3 + rand() * 0.34;
    inner += branch(x2, y2, angle - spread, len * (0.7 + rand() * 0.12), width * 0.72, depth - 1, rand);
    inner += branch(x2, y2, angle + spread, len * (0.7 + rand() * 0.12), width * 0.72, depth - 1, rand);
  }
  // --ox/--oy はその枝の根もとの座標。CSS がこの点を transform-origin として受け取り、そこから育たせる
  return '<g class="bt__seg" style="--i:' + gen + ';--ox:' + x.toFixed(1) + 'px;--oy:' + y.toFixed(1) +
    'px">' + inner + '</g>';
}

09名前から作る背景

ここではシードが数字ではなく文字列です。名前を FNV-1a で32ビット整数ひとつに畳んで乱数のシードに渡すと、同じ名前はいつも同じ模様で戻ってきます。左半分だけを生成して右半分は鏡写しにしているので、ただの乱数格子が顔のように読めます。図形60個で累積面積 11.03%・強度 72.5・移動フレーム 23/23。プロフィールヘッダーの背景に使います。

charCodeAtMath.imulanimation-delay
// FNV-1a: 文字を1つずつ混ぜて32ビット整数ひとつに畳む。同じ文字列はいつも同じ数値になる
function hash(text) {
  var h = 2166136261, i;
  for (i = 0; i < text.length; i++) {
    h = Math.imul(h ^ text.charCodeAt(i), 16777619);
  }
  return h >>> 0;
}

どこで壊れるか — 再帰は画面の上でも入れ子にする

08 を最初に作ったとき、63本の枝をすべて兄弟として並べました。座標は完成した木を基準に計算してあり、枝はそれぞれ自分の根もとから育つので合っているはずでした。ところが、折りたたまれる区間の代表画像では、幹が消えた場所に小枝だけが宙に浮いていました。世代ごとに 70ms の時差を付けたので、幹(遅延 0ms)が先に畳まれ、外側の小枝(遅延 350ms)はまだ立っていたからです。自動再生のループを自分の目で見るまでゲートはすべて通過していて、そのときの値は累積面積 1.83%・強度 74.5・移動フレーム 18/23 でした(run/293/_수리전실측.json)。直し方はひとつ、子の枝を親の <g>中に入れることでした。そうすれば親の拡大縮小が子まで一緒に効くので、どの瞬間で止めても木は木のままです。

同じ根から来る落とし穴がもうひとつあります — 要素数です。生成するコードは格子の数字を上げるだけで図形がいくらでも増えてしまい、その全部に animation-delay が付くと、320px の非力な端末ではフレームより先にスクロールが崩れます。そこで9つとも上限を決め、シードを10回めくって実際にいくつ出るかを数えました(run/293/_요소수.json)。

項目 既定シードの図形数 シード10回中の最大 増える軸
01 等高線 6 6 等高線の層数(6)。標本格子 32×20 は層の中だけを細かくする
02 ボロノイ 26 26 点の数(26)
03 フローフィールド 54 54 開始点の格子 9×6
04 マンダラ 37 37 3層 × 12筆 + 中心の円 1
05 トルシェ 60 60 タイル格子 10×6
06 六角格子 77 77 ハチの巣 11×7
07 再帰分割 27 32 深さ5 — 乱数なのでシードごとに変わる
08 木 63 63 深さ5 (2⁶−1)
09 名前の背景 60 60 12×8 格子のうち埋まったマス

表のなかで値が動くのは 07 だけで、それも32個で止まります。模様をもっと密にしたいときは図形の数を増やさず、ひとつの図形をより細かく描くほうを選んでください。01 は図形が6個しかないのに、9つのなかでいちばん線が詰まっています。9つのファイルと React 版が入った zip のパスワードは 69g487dg で、マーチングスクエアの表からボロノイのクリッピングまで、手を入れずにそのまま動きます。

アクセシビリティ (reduced-motion)

9つとも SVG に role="img"aria-label を付け、ラベルの中には今描かれているシードの値が入ります。スクリーンリーダーの利用者が「再生成」を押したとき、何が変わったのかを値で分かるようにするためです。色は情報を運びません — 色の段階は装飾で、模様の意味は形と位置にあります。文字のコントラストは実測しました。インクのステージ上のシードラベルが 11.16:1、オレンジのステージ(#ff4d1f)の上が 5.5:1、ペーパーとイエローのステージの上が 7.27:1、ボタン内の文字はすべて 17.11:1 で、いずれも AA 基準の 4.5:1 を超えています(run/293/_대비실측.json)。prefers-reduced-motion: reduce では9つともアニメーションを止めますが、完成した模様はそのまま残します。動きを減らしてほしいという設定は、背景を消してほしいという意味ではないからです。reduce ブロックの中の宣言には !important を付けてあります。ここでは reduce ブロックが .nc.is-demo .nc__line同じセレクタを後ろで宣言し直しているので付けなくても勝ちますが、あとから誰かが詳細度の高い規則を上に重ねても動きが復活しないよう、守りを固めておく側を選びました。

FAQ

同じシードなのにブラウザによって絵が変わることはありますか

ありません。mulberry32 は32ビット整数の演算(Math.imul>>>)だけを使い、浮動小数点の誤差を積み上げないので、どのエンジンでも同じ数列になります。座標の計算に入る Math.cosMath.sin は実装ごとに末尾の桁が食い違うことがありますが、toFixed(1) で小数第1位に丸めるため、出力される文字列は同じです。

背景画像のファイルを使うのと比べて何がよいのですか

管理するファイルが1つ減ります。1,000人に別々の模様を配るために画像を1,000枚作る必要がなくなり、09 のようにユーザー名を渡せばその場で出てきます。生成された SVG は「SVGをコピー」ボタンでそのまま持ち出せるので、気に入ったシードをひとつ選んで静的ファイルとして固定するのも自由です。

毎フレーム描き直さず CSS だけで動かすのはなぜですか

requestAnimationFrame で描くと、タブが裏に回ったり画面が忙しかったりするときに、フレームがまとめて飛びます。生成は一度だけにして動きを @keyframes に任せると、ブラウザの合成段階まで下りるので、スクロール中も途切れず、prefers-reduced-motion で止めるのも1行です。stroke-dashoffset がアニメーション可能なプロパティであることは MDN の stroke-dasharray リファレンス に書かれています。

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

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