JavaScriptで雪を降らせてみよう！

# はじめに
この記事は東京高専プロコンゼミ Advent Calendar①11日目の記事です。
https://adventar.org/calendars/5509

# 本題
最近，急に寒くなってきましたね．秋は高専プロコンと定期試験でバタバタしていたので，気づいたらもう冬になっていてびっくりです．時間が過ぎるのは早いですね...😢

ところで，冬といえば皆さんは何を連想しますか？色々あると思いますが，僕は雪ですね！
あたり一面を雪でおおわれた白銀の世界や，ふわりと舞い降りる雪はいつ見てもいいものです．

というわけで(?)，今回はJavaScript(以下js)を使ってwebの世界にも雪を降らせて行こうと思います！

# jsを書く

## 準備
とりあえずベースとなるhtml,jsファイルを用意します

```
|-- snow
|-- |-- index.html
|-- |-- snow.js
```

```html:index.html
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
</head>
<body>
    <script src="./snow.js" type="text/javascript"></script>
</body>
</html>
```

```javascript:snow.js
function initSnow(){
    const snow_div = document.createElement("div");
    snow_div.id = "snow_div";
    document.body.appendChild(snow_div);
}

window.onload = initSnow;
```
<p class="codepen" data-height="265" data-theme-id="light" data-default-tab="html,result" data-user="fazerog02" data-slug-hash="WNGodqB" data-preview="true" style="height: 265px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="WNGodqB">
  <span>See the Pen <a href="https://codepen.io/fazerog02/pen/WNGodqB">
  WNGodqB</a> by fazerog02 (<a href="https://codepen.io/fazerog02">@fazerog02</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

現時点では，読み込むとbodyタグの中に`snow_div`というidを持ったdiv要素が配置されるだけのページです．
これに色々手を加えて雪を降らせて行きましょう！

## 雪を生成する

とりあえず何個か雪を作ってみましょう！

```html:index.html
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
</head>
<body style="background-color: #000000">
    <script src="./snow.js" type="text/javascript"></script>
</body>
</html>
```

```javascript:snow.js
const SNOW_NUM = 10;
const snow_list = [];

function getRandom(min, max){
  min = Math.ceil(min);
  max = Math.floor(max);
  return Math.floor(Math.random() * (max - min + 1) + min); 
}

function createSnow(){
    const snow = document.createElement("div");
    
    // 位置の設定(下左右の10px端を除く))
    snow.style.position = "absolute";
    snow.style.top = String(getRandom(0, document.documentElement.clientHeight - 10)) + "px";
    snow.style.left = String(getRandom(10, document.documentElement.clientWidth - 10)) + "px";

    // サイズの設定(今回は直径5px)
    snow.style.width = "5px";
    snow.style.height = "5px";
    
    // 円にする
    snow.style.borderRadius = "50%";

    // 背景を白に
    snow.style.backgroundColor = "#ffffff";

    return snow;
}

function initSnow(){
    const snow_div = document.createElement("div");
    snow_div.id = "snow_div";
    document.body.appendChild(snow_div);

    // 雪を10個生成
    for(let i = 0; i < SNOW_NUM; i++){
        let snow = createSnow();
        snow_div.appendChild(snow);
        snow_list.push(snow);
    }
}

window.onload = initSnow;
```
<p class="codepen" data-height="265" data-theme-id="light" data-default-tab="js,result" data-user="fazerog02" data-slug-hash="MWjbQYa" data-preview="true" style="height: 265px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="MWjbQYa">
  <span>See the Pen <a href="https://codepen.io/fazerog02/pen/MWjbQYa">
  MWjbQYa</a> by fazerog02 (<a href="https://codepen.io/fazerog02">@fazerog02</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

雪として直径5pxの白い円を10個生成するようにjsを変更しました．また，bodyの背景色が白だと雪が見えないので黒に変更しました．

## 雪を落下させる

次は雪を落下させていきます！

```javascript:snow.js
const SNOW_NUM = 10;
const snow_list = [];

function getRandom(min, max){
  min = Math.ceil(min);
  max = Math.floor(max);
  return Math.floor(Math.random() * (max - min + 1) + min); 
}

// "100px"のような文字列から数字部分だけを抜き出す
function getPxNum(px_str){
    var i;
    for(i = 0; i < px_str.length; i++){
        if(isNaN(px_str[i])) break;
    }
    const num_str = px_str.substring(0, i);
    
    return Number(num_str);
}

function moveSnow(){
    for(let i = 0; i < SNOW_NUM; i++){
        // 雪が画面外に出る前に一番上に戻す
        let top_num = getPxNum(snow_list[i].style.top) + 1;
        if(top_num >= document.documentElement.clientHeight - 10) top_num = 0;

        // 各雪を1px下にずらす
        snow_list[i].style.top = String(top_num) + "px";
    }
}

function createSnow(){
    const snow = document.createElement("div");
    
    // 位置の設定(ランダム(下左右の10px端を除く))
    snow.style.position = "absolute";
    snow.style.top = String(getRandom(0, document.documentElement.clientHeight - 10)) + "px";
    snow.style.left = String(getRandom(10, document.documentElement.clientWidth - 10)) + "px";

    // サイズの設定(今回は直径5px)
    snow.style.width = "5px";
    snow.style.height = "5px";
    
    // 円にする
    snow.style.borderRadius = "50%";

    // 背景を白に
    snow.style.backgroundColor = "#ffffff";

    return snow;
}

function initSnow(){
    const snow_div = document.createElement("div");
    snow_div.id = "snow_div";
    document.body.appendChild(snow_div);

    // 雪を10個生成
    for(let i = 0; i < SNOW_NUM; i++){
        let snow = createSnow();
        snow_div.appendChild(snow);
        snow_list.push(snow);
    }

    // moveSnowを10msごとに定期実行
    setInterval(moveSnow, 10);
}

window.onload = initSnow;
```
<p class="codepen" data-height="265" data-theme-id="light" data-default-tab="js,result" data-user="fazerog02" data-slug-hash="QWKGQwe" data-preview="true" style="height: 265px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="QWKGQwe">
  <span>See the Pen <a href="https://codepen.io/fazerog02/pen/QWKGQwe">
  QWKGQwe</a> by fazerog02 (<a href="https://codepen.io/fazerog02">@fazerog02</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

雪が落下し，画面外に出るとまた上に戻るというプログラムができました．
`setInterval`を使用して雪を1pxずつ落とす関数を10msごとに定期実行することで実現しています．10msというのは適当に決めた値なので，変更しても大丈夫ですが，大きくしすぎると雪の動きがカクカクするようになるので気を付けてください．

## 雪の大きさをランダムに変更する

さて雪を落下させたのはいいんですが，このままでは不自然で雪っぽくありませんよね．
これを解消するために，いろいろなランダム要素を加えていきます．

まずは，雪の大きさから始めます！

```javascript:snow.js
function createSnow(){
    const snow = document.createElement("div");
    
    // 位置の設定(下左右の10px端を除く))
    snow.style.position = "absolute";
    snow.style.top = String(getRandom(0, document.documentElement.clientHeight - 10)) + "px";
    snow.style.left = String(getRandom(10, document.documentElement.clientWidth - 10)) + "px";

    // サイズの設定(ランダムに2～7px)
    const random_size = String(getRandom(2, 7)) + "px";
    snow.style.width = random_size;
    snow.style.height = random_size;
    
    // 円にする
    snow.style.borderRadius = "50%";

    // 背景を白に
    snow.style.backgroundColor = "#ffffff";

    return snow;
}
```
<p class="codepen" data-height="265" data-theme-id="light" data-default-tab="js,result" data-user="fazerog02" data-slug-hash="ZEpBrbW" data-preview="true" style="height: 265px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="ZEpBrbW">
  <span>See the Pen <a href="https://codepen.io/fazerog02/pen/ZEpBrbW">
  ZEpBrbW</a> by fazerog02 (<a href="https://codepen.io/fazerog02">@fazerog02</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

2~7pxぐらいが雪としてちょうどいいサイズだったので，ランダムに2~7pxの雪を生成するようにしました．

## 雪の落下速度をランダムに変える

全ての雪が同じ速度で落ちると，横並びになってしまって不自然極まりないので，速度にランダム要素を追加します．

```javascript:snow.js
const SNOW_NUM = 10;
const snow_list = [];

class Snow{
    constructor(snow, speed){
        this.snow = snow;
        this.speed = speed;
    }
}

function getRandom(min, max){
    min = Math.ceil(min);
    max = Math.floor(max);
    return Math.floor(Math.random() * (max - min + 1) + min); 
}

function getRandomSnowSpeed(){
    return Math.random()*2 + 1;
}

// "100px"のような文字列から数字部分だけを抜き出す
function getPxNum(px_str){
    var i;
    for(i = 0; i < px_str.length; i++){
        if(isNaN(px_str[i]) && px_str[i] != ".") break;
    }
    const num_str = px_str.substring(0, i);
    
    return Number(num_str);
}

function moveSnow(){
    for(let i = 0; i < SNOW_NUM; i++){
        // 雪が画面外に出る前に一番上に戻す
        let top_num = getPxNum(snow_list[i].snow.style.top) + snow_list[i].speed;
        if(top_num >= document.documentElement.clientHeight - 10){
            // 雪を初期化
            top_num = 0;
            const random_size = String(getRandom(2, 7)) + "px";
            snow_list[i].snow.style.width = random_size;
            snow_list[i].snow.style.height = random_size;
            snow_list[i].speed = getRandomSnowSpeed();
        }

        // 各雪を1px下にずらす
        snow_list[i].snow.style.top = String(top_num) + "px";
    }
}

function createSnow(){
    const snow = document.createElement("div");
    
    // 位置の設定(下左右の10px端を除く))
    snow.style.position = "absolute";
    snow.style.top = String(getRandom(0, document.documentElement.clientHeight - 10)) + "px";
    snow.style.left = String(getRandom(10, document.documentElement.clientWidth - 10)) + "px";

    // サイズの設定(ランダムに2～7px)
    const random_size = String(getRandom(2, 7)) + "px";
    snow.style.width = random_size;
    snow.style.height = random_size;
    
    // 円にする
    snow.style.borderRadius = "50%";

    // 背景を白に
    snow.style.backgroundColor = "#ffffff";

    return snow;
}

function initSnow(){
    const snow_div = document.createElement("div");
    snow_div.id = "snow_div";
    document.body.appendChild(snow_div);

    // 雪を10個生成
    for(let i = 0; i < SNOW_NUM; i++){
        let snow = createSnow();
        snow_div.appendChild(snow);
        
        snow_list.push(new Snow(snow, getRandomSnowSpeed()));
    }

    // moveSnowを10msごとに定期実行
    setInterval(moveSnow, 10);
}

window.onload = initSnow;
```
<p class="codepen" data-height="265" data-theme-id="light" data-default-tab="js,result" data-user="fazerog02" data-slug-hash="QWKGQEW" data-preview="true" style="height: 265px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="QWKGQEW">
  <span>See the Pen <a href="https://codepen.io/fazerog02/pen/QWKGQEW">
  QWKGQEW</a> by fazerog02 (<a href="https://codepen.io/fazerog02">@fazerog02</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

雪に速さの情報を持たせるために，新しく`Snow`クラスを作ってそれで雪を管理することにしました．
雪の速さは`Math.random()*2 + 1`となっており，`Math.random()`は0以上1未満の少数を返すので，大体1~3の間になっています．この値は職人芸で調整したものなので，皆さんも自分でいじって調節してみてください．
今回はこれで完成です．最初の方と比べるとだいぶ雪っぽいのではないでしょうか！他にも横方向に動きをつけたり，工夫できる点はあると思うので自分好みにカスタマイズしてみてください！！

## おまけ
背景を雪景色の写真にしたらそれっぽくなった

<p class="codepen" data-height="265" data-theme-id="light" data-default-tab="js,result" data-user="fazerog02" data-slug-hash="gOwLvVO" data-preview="true" style="height: 265px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="gOwLvVO">
  <span>See the Pen <a href="https://codepen.io/fazerog02/pen/gOwLvVO">
  gOwLvVO</a> by fazerog02 (<a href="https://codepen.io/fazerog02">@fazerog02</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

# おわりに
実はこれを書いているのはテスト週間真っただ中で，あまり記事に時間をとられるわけにはいかないので微妙な内容になってしまいました...
本当は自分がやっているValorantというFPSゲームについて熱くオタク語り🤓したかったので，また機会があったらどこかでします．

ここまで拙い文章&コードでしたが，お付き合いいただきありがとうございました！またどこかでお会いしましょう！！