❄️ ウェブ上に雪を降らせる4つの方法とその比較 ―― ライブラリ, CSS, Canvas, WebGL
クリスマスイブ、いかがお過ごしですか?
えっ、予定がない? そうですか・・・ いや、私もです。
東京では相変わらず雪が降る気配もありません。
それでは、せっかくなので、ウェブ上だけでも、雪を降らせてみましょう。
雪の降らせ方
「ウェブ上に雪を降らせる」とは、 「画面上に多数の白い粒を描き、ランダムな軌跡で動かすこと」 と言い換えられます。
しかし、たったそれだけの実装でも、アプローチの仕方は複数あります。
ここでは、私がぱっと思いついた代表的な4つの手法を比較してみようと思います。
Snowstorm.js ― 懐かしき平成の冬
かつての "インターネット" では、とにかく画面上に何か演出をしたがる人がたくさんいました。
かつての個人サイトなどでは、当時の流行として、(なぜか)カーソルが "+" 型になっているサイトとか、突然文脈なく音楽が流れるサイトとか、ありましたね。
2000年代初頭にリリースされ、多くのWebサイトに(やはり大抵の場合は文脈なく)雪を降らせたライブラリがあります。その名も Snowstorm.js 。
<script src="https://schillmania.com/projects/snowstorm/snowstorm.js"></script>

これだけで雪が降ります。
いま見ると少し古臭い。けれど、あの頃のWebを思い出すといった感じでしょうか。
"Storm" と言っているだけあり、ホワイトクリスマスというか雪嵐ですね。風速が強い。
公式ドキュメント に細かいオプションが多く提供されていることがわかり、上の動画にあるような雪嵐(デフォルト値)から、深々と降る優しい雪まで、幅広くカバーしています。
HTML/CSSで降らせる雪 ― 最小限のコード
2011年に提案された CSS animation が今では当たり前に普及しているため、javascript を使わずともCSSだけで雪が降らせられるようになりました。
-# Ruby のテンプレートエンジンである haml を使って、divタグを250個生成します
- 250.times do
- left = rand(100) # 画面上の0%~100%の位置に
- size = rand(10) # 1px ~ 10px のサイズで
- delay = rand(5) # 3 / 0秒 ~ 2秒の遅延をつけて
- duration = rand(5) + 5 # 5秒 ~ 10秒かけて
- translateX = rand(75) + 25 # 75px ~ 100pxを
- direction = rand(2) === 1 ? 1 : -1 # 1(右)または-1(左)に降っていく
.snow{:style => "--left: #{left}%; --size: #{size}px; --delay: #{delay}s; --duration: #{duration}s; --translateX: #{translateX * direction}px;"}
.snow {
position: fixed;
top: -10px;
/* hamlから渡された値をCSSの変数を使って設定 */
left: var(--left);
width: var(--size);
height: var(--size);
...
animation: fall linear infinite;
animation-delay: var(--delay);
animation-duration: var(--duration);
}
@keyframes fall {
to {
transform: translateX(var(--translateX)) translateY(100vh);
}
}
ほんの数行で、白い粒がふわりと落ちていきます。ただし、風や重力、粒の揺らぎまではさすがに再現しづらいです。
この程度だと、どうしても軌跡も直線的になってしまいます。
また、 haml のコードとしては少なく見えますが、実際にコンパイルされるコードには250個の <div> タグがHTML上にレンダリングされています。

これは、あまりかっこよくないですね。
Canvasで降らせる雪 ― 風のあるコード
Canvasを使うと、雪の粒ひとつひとつに「性格」をわかりやすく持たせられます。
// 以下、実装の一部を抜粋
// 250個の要素を持つ配列に対して、座標・大きさ・降っていく速度・左右への揺れ具合を定義
this.flakes = Array.from({length: 250}, () => ({
x: Math.random() * this.canvas.width,
y: Math.random() * this.canvas.height,
radius: Math.random() * 3 + 1,
dimension: Math.random() + 1,
direction: Math.random() < 0.5 ? -1 : 1,
}));
// HTMLCanvasElementに定義したcontextに対して `arc()` メソッドで粒を描いていく
this.flakes.forEach(f => {
this.ctx.beginPath();
this.ctx.arc(f.x, f.y, f.radius, 0, Math.PI * 2);
this.ctx.fill();
});
// 以下のコードを `setInterval()` して連続的に実行させる
this.flakes.forEach(f => {
// Y軸を増やし、雪を降らせていく
f.y += Math.pow(f.dimension, 2);
// 画面の一番下まできたら一番上に戻してあげる
if (f.y > this.canvas.height) f.y = 0;
// ランダムに与えられた direction 値をもとにX軸を移動させて左右への流れを表現
switch (f.direction) {
case 1: {
f.x += Math.sin(f.y * 0.01);
if (f.x > this.canvas.width) f.x = 0;
break;
}
case -1: {
f.x -= Math.sin(f.y * 0.01);
if (f.x < 0) f.x = this.canvas.width;
break;
}
}
});
Canvasの雪は、まるで 風を感じるような動き を見せます。
HTML/CSSのみでは直線的な移動だったのに対し、コードで「揺らぎ」を作ることで、 生きた雪になった と言えるでしょう!
WebGLで降らせる雪 ― 世界を包む雪
Three.js を使えば、雪はもはや “演出” ではなく “環境” になります。
描かれた雪が立体的に絡み合い、画面の中に "空気" が生まれます。
// 以下、実装の一部を抜粋
// 同じくHTMLCanvasElementに定義したcontextに対して
// こちらでは `createRadialGradient()` メソッドを使い、
// 返したgradientに対してaddColorStopで雪っぽいオブジェクトを作ってみます
// (ここまではThree.jsは関係ないですが)
const gradient = context.createRadialGradient(
canvas.width / 2, canvas.height / 2, 0,
canvas.width / 2, canvas.height / 2, canvas.width / 2
);
// 0 -> 0.75 -> 1 と外側になるにつれてだんだん透明度をあげていき
// 雪っぽい丸いオブジェクトを作る
gradient.addColorStop(0, 'rgba(255,255,255,1)');
gradient.addColorStop(0.75, 'rgba(255,255,255,0.75)');
gradient.addColorStop(1, 'rgba(255,255,255,0.0)');
context.fillStyle = gradient;
context.fillRect(0, 0, canvas.width, canvas.height);
// Three.jsの `Float32BufferAttribute` クラスを使って、
// xyzそれぞれの座標を立体的に定義した2500個のオブジェクトを geometry に渡します
const vertices = [];
for (let i = 0; i < 2500; i++) {
vertices.push(
(Math.random() - 0.5) * 1000 * 2, // x
Math.random() * 1000 * 2, // y
(Math.random() - 0.5) * 1000 * 2 // z
);
}
this.geometry.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3));
const material = new THREE.PointsMaterial({
color: 0xffffff,
size: Math.random() * 5 + 15,
// 上のコードの出力をここに map しています
map: this.generateSnowTexture(),
// `THREE.AdditiveBlending` は加算合成となり、雪の粒が単なる点ではなく、
// 柔らかく光を反射しているように見えるのではないかと思います
blending: THREE.AdditiveBlending,
// ちらつきの防止
depthWrite: false,
});
this.snow = new THREE.Points(this.geometry, material);
// あとは 定義した xyz 座標をもとにオブジェクトの移動を描画していきます
const positions = this.geometry.attributes.position.array;
const vertexCount = positions.length / 3;
for (let i = 0; i < vertexCount; i++) {
const xIndex = i * 3;
const yIndex = i * 3 + 1;
const zIndex = i * 3 + 2;
// y座標の値を減らして、雪を降らせる
positions[yIndex] -= Math.random() * 2;
// 同じくランダムに左右にも降らせたいので、x座標を移動
const direction = i % 2 === 0 ? 1 : -1;
positions[xIndex] -= Math.random() * direction;
if (positions[yIndex] < -1000) {
positions[yIndex] = 1000;
positions[xIndex] = (Math.random() - 0.5) * 1000 * 2;
positions[zIndex] = (Math.random() - 0.5) * 1000 * 2;
}
}
// 最後におまけで、 `THREE.Points()` した結果である `this.snow` に対して
// `rotation.y` を微妙に移動させる
// これにより、カメラ自体が移動しているような動きになり、 **歩きながら降っている雪を眺めるような立体的な動き** になります
this.snow.rotation.y += 0.001;
これまでの雪が平面的で、ただ「降る」だけのものだったのに対して、WebGLでは 空間そのもの を作りだすことができます。
雪の粒を2,500個も描いていますがレンダリングはスムーズで、パフォーマンスもあまり低下しませんでした(とはいえ、今時この程度でパフォーマンスを気にする必要もないかもしれませんが)、より低スペックなデバイスでのバッテリー消費なども考慮すると、「重さ」に負があるかもしれません。
もっと工夫すれば、風向き・奥行きに応じた速度差(手前の雪は速く降るなど)といった物理的な複雑さの表現も可能で、ポテンシャルが高いと言えるでしょう。
まとめるとこんな感じでしょうか。
| 手法 | 難易度 | 表現力 | 軽さ | 滑らかさ | 代表的な使い所 |
|---|---|---|---|---|---|
| Snowstorm.js | ★☆☆ | ★☆☆ | ★★☆ | ★☆☆ | 懐かし系・デモ用途 |
| CSS Animation | ★☆☆ | ★★☆ | ★★★★ | ★★☆ | 軽い演出・LPなど |
| Canvas 2D | ★★☆ | ★★★ | ★★☆ | ★★★★ | やわらかな粒の動き |
| WebGL (Three.js) | ★★★ | ★★★★ | ★☆☆ | ★★★★ | 没入感のある演出 |
今回はアドベントカレンダーのテーマとして「雪を降らせる」というテーマを選んでみました。
要件に対してさまざまなアプローチがある一方で、おそらくどの技術を選んでも、遊び心と、命を吹き込むという点では楽しみを感じられると思います。
今回掲載したコードの全体像はそれぞれの Codepen のリンクで参照いただけますが、拙速で作ったところもあり、リファクタリングやさらなる演出の向上はいくらでも余地があると思いますので、ご自由にパラメータを調整したりして使ってみてください。
他の季節、たとえば梅雨の雨、春の桜、秋の落ち葉、などに展開してもいいかもしれませんね。
ご覧いただきありがとうございました。
Discussion