🌨️

【CSS】雪を降らせるエフェクト

2022/12/14に公開約3,000字

Twitterで偶然見つけた雪を降らせるエフェクト。
https://twitter.com/JoshWComeau/status/1602401245385396250
私もアドベントカレンダーの初日に同じようなものを作ったことを記事に書いたので、これはどのように実装しているのかを見てみました。
ちなみに私が作ったものはこちら↓
https://zenn.dev/k41531/articles/ea787d5d091ef8

公式サイトに行ってみると、誰でもコピペするだけで使えるようになっています。

スクリプトの中身もminifyされているだけで、コードも比較的短かったので簡単に読むことができました。ちなみに、minifyされたコードを元に戻すためにdirtyMarkupというサイトを利用したました。
https://www.10bestdesign.com/dirtymarkup/

コードが短いとはいえ、そのまま貼り付けてしまうのは憚られたので、読んだコードを元に自分でも似たようなコードを書いてみます。

プログラムの概要

コードを見た方が早いかと思いますが、どのように作っているのかを言語化します。

HTML文字列の作成

  1. JavaScript を使用して、同じクラスを持つ多数のiタグ文字列を作成します。

CSSの作成

  1. 作成したiタグのクラスに対するCSS文字列を作成します。
  2. 乱数を使って、位置や透明度、大きさなどの値を決めます。
  3. CSSの:nth-child()を使って、作成したタグの数の分だけ、位置などを定義します。
  4. position:fixedに設定します。

埋め込み

  1. createElementdiv要素を作り、独自のidを設定する。
  2. 作成したdiv要素の中にHTML文字列とCSS文字列をinnerHTMLを使って入れます。
  3. 完成

作ってみる

参考にしたスクリプトでは、JavaScriptで全てを文字列として作成し埋め込んでいましたが、それでは分かりにくいので、HTML、CSS、JSに分けて出来るだけ分かりやすくなるように書き換えてみました。
HTML

<div id="snow-scene">
</div>

CSS

body {
  background-color: black;
  color:white;
}
#snow-scene {
  position: fixed;
  left: 0;
  top: 0;
  bottom: 0;
  width: 100vw;
  height: 100vh;
  overflow: hidden;
  z-index: 9999999;
  pointer-events: none;
}
.snow {
  position: absolute;
  width: 10px;
  height: 10px;
  background: white;
  border-radius: 50%;
}
// JSで変更するパラメータ
.snow{
  opacity: 0.5;
  transform: translate(6.21vw,-10px) scale(0.51);
  animation: fall-x 18s -21s linear infinite;
}
@keyframes fall-x {
  30% {
    transform: translate(11.40vw,30.00vh) scale(0.51);
  }
  100% {
    transform: translate(8.80vw, 105vh) scale(0.51);
  }
}

snowクラスを二つに分けているのは、2つ目をサンプルとして残しているからです。また、keyframesはJavaScript側で値を操作できなかったので、fall-xもサンプルとして残しているだけで使っています。
keyframesは、JavaScript側で作成して埋め込んでいます。(ここが汚い)


const rnd = (a, b, c) => Math.floor(Math.random() * (b - a + 1)) + a;
function SnowFall() {
  const snow_scene = document.getElementById("snow-scene");
  let keyframes = ""
  for(i = 1; i < 200; i++) {
    let snow = document.createElement('i');
    snow.classList.add('snow');
    
    const X = (rnd(0, 1000000) * 0.0001),
		      O = rnd(-100000, 100000) * 0.0001,
		      T = (rnd(3, 8) * 10).toFixed(2),
		      S = (rnd(0, 10000) * 0.0001).toFixed(2);
    snow.style.animationName = `fall-${i}`;
    snow.style.opacity = (rnd(1, 10000) * 0.0001).toFixed(2);
    snow.style.transform = `translate(${X.toFixed(2)}vw, -10px) scale(${S})`;
    snow.style.animation = `fall-${i} ${rnd(10, 30)}s -${rnd(0, 30)}s linear infinite`
    keyframes += `@keyframes fall-${i}{
      ${T}%{
        transform: translate(${(X + O).toFixed(2)}vw,${T}vh);
      }
      100%{
        transform:translate(${(X + (O / 2)).toFixed(2)}vw,105vh);
      }
    }`;
    snow_scene.appendChild(snow);
  }
  snow_scene.innerHTML += `<style>${keyframes}</style>`
}
SnowFall();

感想

自分が作ったものを他の人と比べるのはとても勉強になりました。
私が作った雪はアニメーションの切れ目がわかってしまうという欠点がありますが、個人的には自分が書いたSVGを使ったコードの方が比較的短めで好みなので、これを組み合わせてに少し改良してみようかと思います。また、このようなエフェクトを誰でも使えるようにいくつか整備してみるのも面白いかもしれません。

Discussion

ログインするとコメントできます