オンラインでご相談ください!

ご相談内容が完全に固まっていない場合でも
遠慮なくミーティングの予約をどうぞ

TOP デザイン・技術 【JavaScript】スロットの作り方

【JavaScript】スロットの作り方

【JavaScript】スロットの作り方

こんにちは、筒井です。

今回の記事では、jsで作る簡単なスロットの作り方を紹介します。

DOMとスタイリング

先に完成形はこちらです。

 

まずは、DOMを用意してスタイリングします。

<section class="section">
 <div class="slot">
  <div class="wheel js-slot_wheel">
   <div class="wheelInner js-slot_wheel_inner">
    <div class="wheelPic"></div>
   </div>
  </div>

  <div class="btns">
   <div class="btn js-slot_startbtn"><span>START</span></div>
   <div class="btn js-slot_stopbtn"><span>STOP</span></div>
  </div>
 </div>
</section>

jsで取得する要素にはjs- 〜というクラスをふっておきました。
今回のスロットは、.wheelInnerをtransformでy方向に動かすことで実現します。

.section {
 width: 100vw;
 height: 100vh;
 display: flex;
 justify-content: center;
 align-items: center;
 flex-direction: column;
}

.wheel {
 width: 40px;
 height: 120px;
 border: 1px solid #000;
 margin: 0 auto;
 overflow: hidden;
}

.wheelPic {
 background-image: url("../resource/img/pic_slot01.png");
 background-position: 50% 50%;
 background-repeat: no-repeat;
 background-size: cover;
 width: 40px;
 height: 400px;
}

.btns {
 display: flex;
 margin-top: 50px;
}

.btn {
 background-color: #000;
 border-radius: 50%;
 width: 60px;
 height: 60px;
 cursor: pointer;

 display: flex;
 justify-content: center;
 align-items: center;

 span {
  color: #fff;
  font-weight: 700;
 }

 &:nth-child(2) {
  margin-left: 30px;
 }
}

スロット画像について、少し注意です。

今回、こちらの画像を使用していますが、
無限ループさせるため、最後の3つは最初の3つと同じ柄を用意してください

7つ目の柄まで見えた時点で、1周目が終わりということになります。

用意するファイル

まずは以下の2ファイルを用意します。

・Controller.js
・Renderer.js

Controller.jsには、ロジック部分、Renderer.jsには、演出(描画)部分を書いていきます。
何度も使い回せるようにクラスで書いていきます。

ロジック部分

書くべき内容としては、

・startボタンを押したら、スロットゲーム開始
・stopボタンを押したら、スロットが止まる

だけです。

export default class Controller {
 constructor() {
  this.startbtn = document.querySelector(".js-slot_startbtn");
  this.stopbtn = document.querySelector(".js-slot_stopbtn");

  this.setup();
  this.setEvents();
 }

 setup() {
  this.renderer = new Renderer();
 }

 setEvents() {
  this.startbtn.addEventListener("click", (e) => {
   this.renderer.start();
  });

  this.stopbtn.addEventListener("click", (e) => {
   this.renderer.stop();
  });
 }
}

描画部分担当のRendererも、Controllerでインスタンス化しておきます。

描画部分

描画部分は、少しややこしいので順番に説明していきます。

1.とにかくy方向に動かす

細かいことは置いておいて、まずは、.js-slot_wheel_innerをrequestAnimationFrameでy方向に動かすことを考えます。

y += speedのようにして、yにspeedを足してやることで動かします。

今回は上向きに動かそうと思うので、最後に、yに-1をかけてやります。

export default class Controller extends Base {
 constructor() {
  super();

   this.wheel = document.querySelector(".js-slot_wheel_inner"); 
   this.isUEv = true;

  this.speed = 1;
  this.y = 0;

  this.setEvents();
 }

 update() {
  this.y += this.speed; // yの値を増やしていく
  const y = this.y * -1; // 上方向に動かす
  this.wheel.style.transform = `translate3d(0px, ${y}px, 0px)`;
 }

 start() {}

 stop() {}

 setEvents() {
  super.setEvents();
 }
}

 

2.startボタンを押したときに動かす

今のままだと、ページが読み込まれるとすぐに動いてしまうので、
スタートボタンを押すと動くようにします。

easingをかけて動き始めてほしいので、gsapを使います。

また、gsapで扱いやすいように、speedの値もthis.speed = { value: 0 };のようにオブジェクトにします。

export default class Controller extends Base {
 constructor() {
  super();

  this.wheel = document.querySelector(".js-slot_wheel_inner"); 
  this.isUEv = true;

  this.speed = { value: 0 }; // オブジェクトに変更
  this.y = 0;

  this.setEvents();
 }

 update() {
  this.y += this.speed.value; // this.speed.valueに変更
  const y = this.y * -1;

  this.wheel.style.transform = `translate3d(0px, ${y}px, 0px)`;
 }

 start() {
  this.tl = gsap.timeline();

  this.tl.to(this.speed, 5, {
   value: 10,
   ease: "expo.out",
  });
 }

 ...
}

 

3.ループさせる

次にループさせることを考えます。

どこで一周するかというと、再び赤い四角がスロット枠の一番上に到達した時です。

赤い四角は2つありますが、
2つ目の赤い四角がスロット枠の一番上に到達した瞬間、
一気にtransformで初期の位置に戻します。

図の方がわかりやすいと思うので載せておきます。

初期位置にはどのタイミングで戻すのかというと、
一つあたりの 図形の高さが40pxで、それが7つ進んだ時、つまり40 * 7 =280px進んだ時です。

具体的には、this.wheelが

0px
-1px
..
-200px

-280px
———————————
0px(初期位置に戻す)
-1px
-2px
..
-280px
———————————
0px(初期位置に戻す)

こんな感じで、ずっとループすればスロットの回転が実現しそうです。
このようなループは余り(%)を使うと便利です。

export default class Controller extends Base {
 constructor() {
  super();

  this.wheel = document.querySelector(".js-slot_wheel_inner"); 
  this.isUEv = true;

  this.speed = { value: 0 };
  this.y = 0;
  this.picHight = 40; // 柄一つ分の高さ
  this.len = 7; // 7個動くと一周

  this.setEvents();
 }

 update() {
  this.y += this.speed.value;
  const y = -1 * (this.y % (this.picHight * this.len)); // -280を超えると0に戻るようにする

  this.wheel.style.transform = `translate3d(0px, ${y}px, 0px)`;
 }

 ...
}

4.スロットをキリのいいところで止める

とりあえずスロットを止めるとなると、stop()はこんな感じだと思います。

...

stop() {
 if (this.tl) this.tl.kill();

 this.tl = gsap.timeline();

 this.tl
  .set(this.speed, {
   value: 0,
  });
}

...

これを見ると、キリの良いところで止める必要があることがわかります。

キリがいいところとは、具体的に、this.wheelがy方向に、以下の数値分進んだ時です。
スロットの柄の区切り目のところですね。

0px
-40px
-80px
-120px
-160px
-200px
-280px

図形一つ分の高さが、-40pxなので、yが-40の倍数だとピタッととまるはずです。

コードはこうなります。

export default class Controller extends Base {
 constructor() {
  super();

  this.wheel = document.querySelector(".js-slot_wheel_inner"); 
  this.isUEv = true;

  this.speed = { value: 0 };
  this.num = { value: 0 }; // this.yをthis.numに変更
  this.picHight = 40;
  this.len = 7;

  this.setEvents();
 }

 update() {
  this.num.value += this.speed.value;
  const y = -1 * ((this.num.value * this.picHight) % (this.picHight * this.len)); // ループするように, キリよく止まるように

  this.wheel.style.transform = `translate3d(0px, ${y}px, 0px)`;
 }

 start() {
   this.tl = gsap.timeline();

   this.tl.to(this.speed, 5, {
    value: 0.4, // 値を下げる
    ease: "expo.out",
   });
  }

 stop() { 
  if (this.tl) this.tl.kill(); 
   this.tl = gsap.timeline(); 

   this.tl
       .set(this.speed, { value: 0, }) 
       .to(this.num, 4, { 
         value: 0, 
         ease: "power3.out", 
       }); 
   }
...
}

this.num.value * this.picHightが、さっきのthis.yに相当します。
これで、this.num.value * this.picHightは、必ずthis.picHight(-40)の倍数になります。

this.num.value * this.picHightの値はとても大きいので、
this.speed.valueの値を下げて回転スピードを調整しています。

 

5.スロットを逆回転させずに止める

現状、一つ問題点があります。

見ての通り、スロットが逆回転してしまいます。

ちなみに、this.num.valueを1にすると

...

stop() {
 if (this.tl) this.tl.kill();

 this.tl = gsap.timeline();

 this.tl
  .set(this.speed, {
   value: 0,
  })
  .to(this.num, 4, {
   value: 1,
   ease: "power3.out",
  });
}

...

柄が、初期値から一つ分進んだところで止まることがわかります。(逆回転ですが)

同様に、this.num.valueを7にすると、7番目の図形のところで止まります。
ここの値をいじれば、どこで止まるかコントロールできそうです。

そして、逆回転をしないようにどうすれば良いかを考えます。

例えば、初期位置でスロットを止めるためには、さまざまな値が考えられます。

this.num.value * this.picHightが
0  *  40px = 0px
7  *  40px = 280px
14 * 40px = 560px
21 * 40px = 840px

など。

この差は、何回転したかに関わります。
0px (1回転目)
280px(2回転目 – 図形が7個進んだところ)
560px(3回転目 – 図形が14個進んだところ)
840px(4回転目 – 図形が21個進んだところ)

1回転目の途中で、ストップボタンを押して、初期位置の図形が出て欲しい時は、
this.num.valueを7にすれば良いです。

2回転目の途中でストップボタンを押した場合、this.num.valueを14

3回転目の途中でストップボタンを押した場合、this.num.valueを21

にすれば良いです。

それを求めるためには、stopボタンを押した時点で、現在何周目かを算出します。
以下で、計算可能です。

const lap = Math.ceil((this.num.value * this.picHight) / 280);

何周目であるかに加えて、図形で言えば、何個進んだかの情報が必要になります。

lap * 7

でそれを算出できます。

7というのは、1周あたりに含まれる図形の数です。
this.lenという変数に格納しておき、全体のコードは以下の通りになります。

export default class Controller extends Base {
 constructor() {
  super();

 this.wheel = document.querySelector(".js-slot_wheel_inner"); 
  this.isUEv = true;

  this.speed = { value: 0 };
  this.num = { value: 0 };
  this.picHight = 40;
  this.len = 7;

  this.setEvents();
 }

 update() {
  this.num.value += this.speed.value;
  const y = -1 * ((this.num.value * this.picHight) % (this.picHight * this.len)); // ループするように, キリよく止まるように

  this.wheel.style.transform = `translate3d(0px, ${y}px, 0px)`;
 }

 ...
 
 stop() {
  if (this.tl) this.tl.kill();

  // 現在何周目か
  const lap = Math.ceil((this.num.value * this.picHight) / 280);

  this.tl = gsap.timeline();

  this.tl
  .set(this.speed, {
   value: 0,
  })
  .to(this.num, 4, {
   value: lap * this.len,
   ease: "power3.out",
  });
  }
  
  ...
}

 

逆回転せずにキリよく止まるようになりました!
ただ、回転がたりてない感じがすごいので、lapに+1をして、1周多く回って止まるようにします。

const lap = Math.ceil((this.num.value * this.picHight) / 280) + 1;

 

6.スロットをランダムなところで止める

最後にランダムな位置で止まるようにします。

現状、this.num.valueがthis.lenの倍数になってるので、
いつも初期位置で止まるようになってしまってます。

value: lap * this.len + 1 => 2番目の図形で止まる
value: lap * this.len + 2 => 3番目の図形で止まる
value: lap * this.len + 3 => 4番目の図形で止まる

です。

なので、1 〜 7のランダムな数値を足してあげます。

minからmaxの範囲でランダムな整数を作るには、以下の関数を使います。

const randomInt = (min, max) => {
  return Math.floor(Math.random() * (max + 1 - min) + min);
};

コード全文はこうなります。

const randomInt = (min, max) => {
  return Math.floor(Math.random() * (max + 1 - min) + min);
};

export default class Controller extends Base {
 constructor() {
  super();

 this.wheel = document.querySelector(".js-slot_wheel_inner"); 
  this.isUEv = true;

  this.speed = { value: 0 };
  this.num = { value: 0 };
  this.picHight = 40;
  this.len = 7;

  this.setEvents();
 }

 update() {
  this.num.value += this.speed.value;
  const y = -1 * ((this.num.value * this.picHight) % 280);

  this.wheel.style.transform = `translate3d(0px, ${y}px, 0px)`;
 }

 start() {
  this.tl = gsap.timeline();

  this.tl.to(this.speed, 5, {
   value: 0.4,
   ease: "expo.out",
  });
 }

 stop() {
  if (this.tl) this.tl.kill();

  // 現在何周目か + 1回転足す
  const lap = Math.ceil((this.num.value * this.picHight) / (this.picHight * this.len)) + 1; 

  this.tl = gsap.timeline();

  this.tl
  .set(this.speed, {
   value: 0,
  })
  .to(this.num, 4, {
   value: lap * this.len + randomInt(0, this.len),
   ease: "power3.out",
  });
 }

 setEvents() {
  super.setEvents();
 }
}

 

ランダムなところで止まるようになりました!

今回はinnerを動かしましたが、各要素を個別に動かして、スロットを作ることも可能です。
その場合、各要素を歪めたり、色を変えたりなど、より柔軟に演出を入れることができます。

最後までお読みいただきありがとうございました。

 

DOWNLOAD

未公開実績も多数!
BALANCeの【デザイン・技術】サービス説明資料には、『Javascript』『アニメーション』の実績が多数掲載されています。
また、関連した会社資料も無料ダウンロードできますので、ぜひご覧ください!

CONTACT

お見積もりやご提案に関しては、費用は発生いたしません。お気軽に、お問い合わせください。

RECRUIT

BALANCeでは共に働く仲間を随時お待ちしております。
印象に残るプロダクトを一緒に作りませんか?

採用情報はこちら

タグから選ぶ