さぁ!検索しよう!

ポイントが加算される等といったゲーム的な機能はありません。7が揃ったら喜んでください。

Demo

右側のレバーをドラッグ&ドロップで下へ倒すとリールが動き出します。
止める時はリールの下部にある赤いボタンを押していきます。
すべて止め終わると結果が画面中央から浮かび上がります。
もう一度プレイしたければリプレイボタンを押すと初めの状態に戻ります。

動作環境について

Google chromeでのみ動作確認済みです。

HTMLを書く

始めにHTMLでスロットマシーンの構造を定義します。

    <div class="result-message" id="result-message"></div>
    <div class="slot" id="slot">
      <div class="display-wrap" id="display-wrap">
        <div class="display" id="display">
          <div class="items items-left">
            <div class="item item-left"><i class="fa fa-heart color-pink" aria-hidden="true"></i>
            </div>
            <div class="item item-left"><i class="fa fa-bell color-gold" aria-hidden="true"></i>
            </div>
            <div class="item item-left"><i class="seven color-orange">7</i>
            </div>
            <div class="item item-left"><i class="fa fa-tree color-green" aria-hidden="true"></i>
            </div>
            <div class="item item-left"><i class="fa fa-car color-blue" aria-hidden="true"></i>
            </div>
          </div>
          <div class="items items-center">
            <div class="item item-center"><i class="fa fa-heart color-pink" aria-hidden="true"></i>
            </div>
            <div class="item item-center"><i class="fa fa-bell color-gold" aria-hidden="true"></i>
            </div>
            <div class="item item-center"><i class="seven color-orange">7</i>
            </div>
            <div class="item item-center"><i class="fa fa-tree color-green" aria-hidden="true"></i>
            </div>
            <div class="item item-center"><i class="fa fa-car color-blue" aria-hidden="true"></i>
            </div>
          </div>
          <div class="items items-right">
            <div class="item item-right"><i class="fa fa-heart color-pink" aria-hidden="true"></i>
            </div>
            <div class="item item-right"><i class="fa fa-bell color-gold" aria-hidden="true"></i>
            </div>
            <div class="item item-right"><i class="seven color-orange">7</i>
            </div>
            <div class="item item-right"><i class="fa fa-tree color-green" aria-hidden="true"></i>
            </div>
            <div class="item item-right"><i class="fa fa-car color-blue" aria-hidden="true"></i>
            </div>
          </div>
        </div>
        <div class="btns">
          <button class="btn-wrap" id="btn-left"><a class="btn-glass" href="#"></a></button>
          <button class="btn-wrap" id="btn-center"><a class="btn-glass" href="#"></a></button>
          <button class="btn-wrap" id="btn-right"><a class="btn-glass" href="#"></a></button>
        </div>
      </div>
      <div class="lever">
        <div class="lever-axis">
          <div class="lever-stick-wrap lever-stick-wrap-upper-half">
            <div class="lever-stick lever-stick-upper-half"></div>
            <div class="lever-ball lever-ball-top"></div>
          </div>
          <div class="lever-stick-wrap lever-stick-wrap-lower-half">
            <div class="lever-stick lever-stick-lower-half"></div>
            <div class="lever-ball lever-ball-bottom"></div>
          </div>
        </div>
      </div>
    </div>
    <button class="replay-btn btn-square" id="replay-btn">Replay!   </button>

主な要素については以下の通りです。

要素名 役割
.result-message 結果のメッセージ
.display ディスプレイ
.items リール
.btns ストップボタン
.lever スタートレバー
.replay-btn リプレイボタン

スロットマシーンはボディ、スタートレバー、ディスプレイ、リール、ストップボタンの要素から構成されています。

HTMLのポイントはスタートレバーを上半分と下半分に分けている点です。後にJavaScriptでドラッグによる操作を可能とするためこのようなことをしています。

<!-- 上半分 -->
<div class="lever-stick-wrap lever-stick-wrap-upper-half">
    <div class="lever-stick lever-stick-upper-half"></div>
    <div class="lever-ball lever-ball-top"></div>
</div>

<!-- 下半分 -->
<div class="lever-stick-wrap lever-stick-wrap-lower-half">
    <div class="lever-stick lever-stick-lower-half"></div>
    <div class="lever-ball lever-ball-bottom"></div>
</div>

CSS(SCSS)を書く

次にCSSでスロットマシーンの見た目やスタイルを表現します。

@import url('https://fonts.googleapis.com/css?family=Shrikhand');
@import url('https://fonts.googleapis.com/css?family=Concert+One');
@mixin glassCircle($colorLight,$colorDark,$h,$w) {
    background: radial-gradient($colorLight 50%, $colorDark 80%);
    box-shadow: 0 2px 6px rgba(0, 0, 0, 0.5),
        0 2px 10px rgba($colorLight, 0.7),
        inset 0 0 10px rgba(0, 0, 150, 0.6);
    border: 1px solid rgba(0, 0, 0, .5);
    border-radius: 100%;
    position: relative;
    display: block;
    height: $h;
    width: $w;
    &::before {
        background-image: linear-gradient(
            to bottom,
            rgba(250, 250, 255, 1) 0%,
            rgba(250, 250, 255, 0.7) 10%,
            rgba(250, 250, 255, 0) 100%
        );
        border-radius: 6vmin;//6em
        content: '';
        display: block;
        height: 100%;
        position: absolute;
        top: -1.7vmin;//-.5em
        left: 0;
        transform: scale(0.8, 0.6);
        width: 100%;
        z-index: 3;
    }
}
@mixin gradient($start-color, $end-color, $orientation) {
    background: $start-color;
    @if $orientation == vertical {
        // vertical
        background: linear-gradient(to bottom, $start-color 0%, $end-color 100%);
        filter: progid:DXImageTransform.Microsoft.gradient(
                startColorstr='$start-color',
                endColorstr='$end-color',
                GradientType=0
            );
    }
    @else if $orientation == horizontal {
        // horizontal
        background: linear-gradient(to right, $start-color 0%, $end-color 100%);
        filter: progid:DXImageTransform.Microsoft.gradient(
                startColorstr='$start-color',
                endColorstr='$end-color',
                GradientType=1
            );
    }
    @else {
        // radial
        background: radial-gradient(
            ellipse at center,
            $start-color 0%,
            $end-color 100%
        );
        filter: progid:DXImageTransform.Microsoft.gradient(
                startColorstr='$start-color',
                endColorstr='$end-color',
                GradientType=1
            );
    }
}
html {
    box-sizing: border-box;
}
* {
    margin: 0;
    padding: 0;
}
*, *::before, *::after {
    box-sizing: inherit;
}
html, body {
    height: 100%;
}
body {
    background: #ccc; //#ccc
    display: flex;
    align-items: center;
    flex-direction: column;
    justify-content: center;
    line-height: 1.8;
    overflow: hidden;
}
a {
    pointer-events: none;
}
.result-zoom {
    animation: zoom 1s 1;
}
.fadeout {
    opacity: 0;
    visibility: hidden;
}
.result-message {
    font-size: 20vmin;
    opacity: 0;
    visibility: hidden;
    position: absolute;
    z-index: 100;
}
@keyframes zoom {
    0% {
        transform: scale(0.2);
        opacity: 1;
        visibility: visible;
    }
    70% {
        transform: scale(1);
    }
    100% {
        opacity: 0;
        visibility: hidden;
    }
}
.slot {
    background-image: linear-gradient(#fcbdbd 0%, #e24a4a 100%);
    text-shadow: 1px 1px 1px rgba(255, 255, 255, 0.66);
    border-radius: 4em/20em;
    display: flex;
    margin-bottom: 6vmin;
    padding: 12vmin;
}
.display {
    border-radius: 2em/20em;
    display: flex;
    justify-content: space-around;
    height:40vmin;
    overflow: hidden;
    margin-bottom: 4vmin;
    position: relative;
    &::before {
        background: linear-gradient(
            180deg,
            #333 0%,
            transparent 30%,
            transparent 70%,
            #333 100%
        );
        content: '';
        height: 100%;
        position: absolute;
        top: 0;
        left: 0;
        width: 100%;
    }
}
.items {
    background: #fff;
    border-radius: 0.2em/10em;
    display: flex;
    flex-direction: column;
    flex-wrap: nowrap;
    padding:0 4.6vmin;
}
.item {
    display: flex;
    align-items: center;
    justify-content: center;
    flex: 1;
    font-size: 6vmin;
    padding: 0.4vmin 0;
}
.lever {
    background-image: linear-gradient(#ededed 0%, #adadad 100%);
    box-shadow: 0 1px 1px rgba(0, 0, 0, 0.28);
    border-radius: 0.5em;
    height:40vmin;
    margin-left: 3vmin;
    padding: 2.4vmin;
    &-axis {
        background: linear-gradient(
            180deg,
            #333 0%,
            transparent 30%,
            transparent 70%,
            #333 100%
        );
        height:100%;
        width: 100%;
    }
    &-stick-wrap {
        height: 50%;
        position: relative;
        width: 4.8vmin;
    }
    &-stick {
        background: linear-gradient(
            90deg,
            #333 0%,
            transparent 30%,
            transparent 70%,
            #333 100%
        );
        border-radius: 2em;
        height:100%;
        position: absolute;
        left:0;
        width: 4.8vmin;
        z-index:0;
        &-upper-half {
            height:100%;
            bottom:0;
        }
        &-lower-half {
            height:0%;
            top:0;
        }
    }
    &-ball {
        @include glassCircle(#bcffd1,#26ef66,10vmin,10vmin);
        margin-left: -5vmin;
        position: absolute;
        left: 50%;
        z-index:5;
        &-top {
            top: -5vmin;
            &.hide {
                visibility: hidden;
            }
        }
        &-bottom {
            bottom: -5vmin;
            .hide {
                visibility: hidden;
            }
        }
    }
}
.btns {
    background-image: linear-gradient(#ffe3b7 0%, #fcbc55 100%);
    box-shadow: 0 1px 1px rgba(0, 0, 0, 0.28);
    border-radius: 0.5em;
    display: flex;
}
button {
    background: none;
    border: none;
}

.btn-wrap {
    flex: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 4vmin;
}

.btn-glass {
    @include glassCircle(#fc6a6a, #fc1b1b,10vmin,10vmin);
}
.btn-square {
    background-image: linear-gradient(#6795fd 0%, #67ceff 100%);
    box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.29);
    border-bottom: solid 3px #5e7fca;
    border-radius: 4px;
    color: rgba(0, 69, 212, 0.47);
    display: inline-block;
    font-weight: bold;
    font-size:4vmin;
    padding: 4vmin 6vmin;
    text-decoration: none;
    text-shadow: 1px 1px 1px rgba(255, 255, 255, 0.5);
    &:active {
        box-shadow: 0px 0px 1px rgba(0, 0, 0, 0.2);
        border-bottom: none;
        transform: translateY(4px);
    }
}
.hide {
    display: none;
}

.start-btn {
    font-family: 'Concert One', cursive;
}
.seven {
    font-family: 'Shrikhand', cursive;
    font-weight: bold;
}
.color-pink {
    color: #f9205a;
}
.color-gold {
    color: #f9c220;
}
.color-orange {
    color: #f45d33;
}
.color-green {
    color: #317036;
}
.color-blue {
    color: #3e6be8;
}

CSSでは主に以下のことを行なっています。

結果メッセージをふわりと表示

CSS3のアニメーションで結果メッセージがふわりと浮かび上がって消えるようにしています。

CSS

@keyframes zoom {
    0% {
        -webkit-transform: scale(0.2);
        transform: scale(0.2);
        opacity: 1;
        visibility: visible;
    }
    70% {
        -webkit-transform: scale(1);
        transform: scale(1);
    }
    100% {
        opacity: 0;
        visibility: hidden;
    }
}
.result-zoom {
    -webkit-animation: zoom 1s 1;
    animation: zoom 1s 1;
}

上記の.result-zoomクラスがJavaScriptによって結果メッセージ.result-messageに追加されると浮かび上がるように表示されます。

レスポンシブ化

単位にvminを指定することでレスポンシブ化しています。

リアルな見た目を表現

グラデーションを使ってリアルな見た目を表現しています。

JavaScriptを書く

最後にJavaScriptでスロットマシーンに動きを加えます。

JavaScriptはクラスが大部分を占めています。始めに以下の順でクラスを作成していきます。

Btnクラス

ボタンの基となるBtnクラスを作成します。

abstract class Btn {

    //ボタン要素の参照を取得
    protected _elem: HTMLButtonElement;

    constructor(elem) {
        this._elem = document.getElementById(elem);
    }
}

Btnクラスは非常にシンプルでボタン要素の参照を取得しているだけです。

ReplayBtnクラス

リプレイボタンに関するReplayBtnクラスを作成します。

class ReplayBtn extends Btn {

    //スロットマシーンをリセットするメソッド
    private reset;
    constructor(elem, reset) {
        super(elem);
        this.reset = reset;

        //初期状態ではリセットボタンを非表示
        this.hide();

        //リセットボタンが押されるとスロットマシーンをリセットする
        this._elem.addEventListener('click', () => this.reset());
    }

    //リプレイボタンを表示するメソッド
    public show() {
        this._elem.classList.remove("hide");
    }

    //リプレイボタンを非表示にするメソッド
    public hide() {
        this._elem.classList.add("hide");
    }
}

このクラスはBtnクラスを継承し、button要素の参照、resetメソッド、ボタンを表示するshowメソッド、そしてボタンを非表示にするhideメソッドを持ちます。生成されたオブジェクトは始めは非表示となります。
リプレイボタンが押されるとリプレイします。

Displayクラス

スロットマシーンのディスプレイに関するDisplayクラスを作成します。

class Display {

    //#displayの参照を取得
    private elem: HTMLElement;

    //#displayの子要素を取得
    private inner: string;

    constructor() {
        this.elem = document.getElementById('display');
        this.inner = this.elem.innerHTML;
    }

    //ディスプレイ内をリセットするメソッド
    reset() {
        this.elem.innerHTML = this.inner;
    }
}

このクラスは#display要素とその子要素の参照、そしてディスプレイをリセットするresetメソッドを持ちます。

リセットされると3つのリールが元の並びに戻ります。

ResultMsgクラス

結果メッセージに関するResultMsgクラスを作成します。

class ResultMsg {
    private elem: HTMLElement;
    constructor() {
        //.result-message要素の参照を取得
        this.elem = document.getElementById('result-message');

        //初期状態ではメッセージを非表示
        this.hide();
    }

    //結果メッセージの非表示・フェードアウト
    public hide() {
        this.elem.classList.remove("result-zoom");
    }

    //結果メッセージの表示・フェードイン
    public show() {
        this.elem.classList.add("result-zoom");
    }

    //Great!!!をフェードイン表示
    public get isGreat() {
        this.show();
        return this.elem.textContent = "Great!!!";
    }

    //Good!をフェードイン表示
    public get isGood() {
        this.show();
        return this.elem.textContent = "Good!";
    }

    //Regret...をフェードイン表示
    public get isRegret() {
        this.show();
        return this.elem.textContent = "Regret...";
    }

    //メッセージをリセット
    public empty() {
        this.elem.textContent = "";
    }
}

結果メッセージはリールが全て止まると表示されます。

Reelクラス

リール単体に関するReelクラスを作成します。

class Reel {

    //この配列内でリール内の要素を並び替える
    private _items;

    //リール要素の参照を取得
    private elem: Element;

    //cancelAnimationFrameに渡す為に必要
    private requestId: number;

    //リール内をリセットする際に必要
    private elemInner: string;

    constructor(elem) {
        this.elem = document.getElementsByClassName(elem)[0];

        //リール内のアイテムを取得
        this.elemInner = this.elem.innerHTML;
    }

    //リール内のアイテムの並びをリセットするメソッド
    reset() {
        this.elem.innerHTML = this.elemInner;
    }

    //リールを初期化するメソッド
    init() {

        //リール内の要素を取り出して配列化する。([item, item,...])
        this._items = Array.prototype.slice.call(this.elem.children);
    }

    //他のクラスで参照するためにgetterを定義
    get items() {
        return this._items;
    }

    //リールを動かすメソッド
    public move() {

        //moveメソッドを繰り返し実行する
        this.requestId = requestAnimationFrame(this.move.bind(this));

        //リール内から最後のアイコンを削除し、それを取得
        const lastItem = this._items.pop();

        //リール内の先頭に先ほど取得した最後のアイコンを追加
        this._items.unshift(lastItem);

        //リール内を更新
        for(let i = 0; i < this._items.length; i++) {
            this.elem.appendChild(this._items[i]);
        }
    }

    //リールを止めるメソッド
    public stop() {
        //アニメーション停止
        cancelAnimationFrame(this.requestId);
    }
}

Reelsクラス

3つのリールを一つにまとめたReelsクラスを作成します。

class Reels {
    //3つのリールを生成
    private reelLeft: Reel;
    private reelJenter: Reel;
    private reelRight: Reel;

    //リールを一まとめにするための配列
    private _elems: Reel[] = [];
    constructor() {
        const reelLeft = new Reel('items-left');
        const reelCenter = new Reel('items-center');
        const reelRight = new Reel('items-right');
        this._elems = [reelLeft, reelCenter, reelRight];

        //全てのリールを初期化
        this.init();
    }

    //全てのリールを初期化するメソッド
    public init() {
        for(let i = 0; i < this._elems.length; i++) {
            this._elems[i].init();
        }
    }

    //3つのリールをリセットするメソッド
    public reset() {
        for(let i = 0; i < this._elems.length; i++) {
            this._elems[i].reset();
        }
    }

    //他のクラスで参照するためにgetterを定義
    public get elems() {
        return this._elems;
    }

    //3つのリールを動かすメソッド
    public move() {
        for(let i = 0; i < this._elems.length; i++) {
            this._elems[i].move();
        }
    }

    //i番目のリールを止めるメソッド
    public stop(i) {
        this._elems[i].stop();
    }
}

3つのリールを一つにまとめたほうがfor文等でまとめて処理できると考えました。正しい方法かどうかはわかりません。

StopBtnクラス

ストップボタン単体に関するStopBtnクラスを作成します。このクラスはBtnクラスを継承します。

class StopBtn extends Btn {
    private reel: Reel;

    constructor(elem) {
        super(elem);
    }

    //他のクラスで参照するためにgetterを定義
    get elem() {
        return this._elem;
    }

    //ボタンを無効化にするメソッド
    public disabled() {
        this._elem.disabled = true;
    }

    //ボタンを有効化にするメソッド
    public enabled() {
        this._elem.disabled = false;
    }
}

ストップボタンは真上に位置するリールを止める働きを持ちます。

StopBtnsクラス

3つのストップボタンを一つにまとめたStopBtnsクラスを作成します。

class StopBtns {

    //ストップボタンを3つ生成
    private stopBtnLeft: StopBtn;
    private stopBtnCenter: StopBtn;
    private stopBtnRight: StopBtn;

    //ストップボタンを入れる配列
    private _elems: StopBtn[] = [];

    //3つのリール
    private reels;

    //リールが止まった回数
    private stopCount: number = 0;

    //判定するメソッド
    private judge;
    constructor(reels, judge) {
        this.reels = reels;
        this.judge = judge;
        this.stopBtnLeft = new StopBtn('btn-left');
        this.stopBtnCenter = new StopBtn('btn-center');
        this.stopBtnRight = new StopBtn('btn-right');
        this._elems = [this.stopBtnLeft, this.stopBtnCenter, this.stopBtnRight];

        for(let i = 0; i < this._elems.length; i++) {

            //初期状態ではストップボタンを無効化
            this._elems[i].disabled();

            //ストップボタンを押すと
            this._elems[i].elem.addEventListener('click', () => {

                //ストップボタンの真上に位置するリールを止める
                this.reels.stop(i);

                //リールが止まる度にインクリメント
                this.stopCount++;
                ///console.log(StopBtns.stopCount);

                //押されたストップボタンを無効化
                this._elems[i].disabled();

                //全てのストップボタンが押されたら判定
                if(this.stopCount >= 3) {
                    this.stopCount = 0;
                    this.judge();
                }
            }, false);
        }   
    }

    //他のクラスで参照するためにgetterを定義
    public get elems() {
        return this._elems;
    }

    //ボタンを無効化にするメソッド
    public disabled() {
        for(let i = 0; i < this._elems.length; i++) {
            this._elems[i].disabled();
            //this._elems[i].style.filterかrgba
        }
    }
    //ボタンを有効化にするメソッド
    public enabled() {
        for(let i = 0; i < this._elems.length; i++) {
            this._elems[i].enabled();
            //this._elems[i].style.filterかrgba
        }
    }
}

Reelsクラスと同様の考え方からこのようなクラスを作成しました。

StartLeverクラス

スタートレバーとその操作に関するStartLeverクラスを作成します。

class StartLever {
    //レバーのボール部分(以下ボールと呼ぶ)
    private ball: HTMLElement;

    //ボールの親要素からの相対y座標
    private ballY: number;

    //ボールの高さ
    private ballHeight: number;

    //レバー上半分
    private stickUpperHalf: HTMLElement;

    //レバー上半分の親要素からの相対y座標
    private stickUpperHalfY: number;

    //レバー上半分の高さ
    private stickUpperHalfHeight: number;

    //レバー下半分
    private stickLowerHalf: HTMLElement;

    //レバー下半分から追従するボール
    private ballBottom: HTMLElement;

    //ドラッグしているかどうか
    private isDragging: boolean;

    //ボールを押下したときのカーソルのy座標
    private dy: number;

    //ドラッグしているときのカーソルのy座標
    private my: number;

    //dyからmyまでの距離
    private distance: number;

    constructor() {
        this.ball = document.querySelector(".lever-ball-top");
        this.ballY = this.ball.offsetTop;
        this.ballHeight = this.ball.clientHeight;
        this.ballBottom = document.querySelector(".lever-ball-bottom");
        this.hideBall(this.ballBottom);
        this.stickUpperHalf = document.querySelector(".lever-stick-upper-half");
        this.stickUpperHalfY = this.stickUpperHalf.offsetTop;
        this.stickUpperHalfHeight = this.stickUpperHalf.clientHeight;
        this.stickLowerHalf = document.querySelector(".lever-stick-lower-half");
        this.isDragging = false;

        //addEventListenerメソッドでイベントターゲットにイベントが発生したときに実行されるコールバック関数を追加
        this.ball.addEventListener("mousedown", e => this.down(e), false);
        this.ball.addEventListener("touchstart", e => this.down(e), false);
        document.body.addEventListener("mousemove", e => this.move(e), false);
        document.body.addEventListener("touchmove", e => this.move(e), false);
        document.body.addEventListener("mouseup", e => this.up(e), false);
        document.body.addEventListener("mouseleave", e => this.up(e), false);
        document.body.addEventListener("touchend", e => this.up(e), false);
        document.body.addEventListener("touchleave", e => this.up(e), false);
    }

    //ボールを表示するメソッド
    private showBall(ball) {
        ball.classList.remove('hide');
    }

    //ボールを非表示にするメソッド
    private hideBall(ball) {
        ball.classList.add('hide');
    }

    //レバーをリセットするメソッド
    public reset() {
        //この記述がないと2回目以降にレバーをmousedownしただけで倒れてスタートしてしまった。
        this.distance = null;
        this.showBall(this.ball);
        this.ball.style.transform = "";//ボールに追加したスタイルを削除
        this.stickUpperHalf.style.height = "";
        this.stickLowerHalf.style.height = "";
        this.hideBall(this.ballBottom);
        this.isDragging = false;
    }

    //ボールを押下したときに実行されるメソッド
    private down(e) {
        //リールが動いている間はスタートレバーの操作を禁止とする。document.body.addEventListener...の後に書いてしまうと、すでにthis.move(e)が呼ばれているのでこの記述は意味がなくなる。
        if (slotMachine.isPlaying) {
            return;
        }

        //ドラッグ開始
        this.isDragging = true;
        console.log("down!");

        //タッチ・マウスイベントの差異を吸収
        let event;
        if (e.type === "mousedown") {
            event = e;
        } else {
            event = e.changedTouches[0];
        }

        //押下したときのカーソルのy座標
        this.dy = event.clientY;
    }

    //ボールが移動できる範囲を制限するユーティリティメソッド
    private static clamp(number, min, max) {
        return Math.max(min, Math.min(number, max));
    }

    //押下したまま動かしたときに実行されるメソッド
    private move(e) {
        //ドラッグ中であれば
        if (this.isDragging) {
            console.log('move');
            const stickUpperHalfY = this.stickUpperHalf.offsetTop;
            const stickUpperHalfHeight = this.stickUpperHalf.clientHeight;

            //タッチ・マウスイベントの差異を吸収
            let event;
            if (e.type === "mousemove") {
                event = e;
            } else {
                event = e.changedTouches[0];
            }

            //移動中のカーソルの絶対的なy座標
            this.my = event.clientY;

            //ドラッグ時にリール内のアイコンが選択されるのを防ぐ
            e.preventDefault();

            //this.dyからの相対的なカーソルの移動中のy座標
            this.distance = this.my - this.dy;

            //distanceの変化に合わせてボールを縦方向に移動させる
            this.ball.style.transform =
                "translateY(" +
                StartLever.clamp(
                this.distance,
                0,
                (stickUpperHalfY + stickUpperHalfHeight) * 2
            ) +
                "px)";

            //distanceの変化に合わせて上半分のスティックの高さを0に向かって変化させる
            this.stickUpperHalf.style.height = `${StartLever.clamp((stickUpperHalfY + stickUpperHalfHeight) - this.distance, 0, (stickUpperHalfY + stickUpperHalfHeight))}px`;

            //上半分のスティックの高さが0以下になったら
            if(stickUpperHalfHeight <= 0) {
                //distanceの変化に合わせて下半分のスティックの高さを0からスティック半分の高さまで変化させる(上半分の逆)
                this.stickLowerHalf.style.height = `${StartLever.clamp(this.distance - (stickUpperHalfY + stickUpperHalfHeight), 0, (stickUpperHalfY + stickUpperHalfHeight))}px`;
                //0以上に戻ったら
            }else{
                //下半分のスティックを0に戻す
                this.stickLowerHalf.style.height = `${0}px`;
            }
        }
    }

    //ドロップしたときに実行されるメソッド
    private up(e) {
        //ドラッグ中であれば(現時点でドラッグ中ではないがスタート中にイベント(mouseup,mouseleave,touchend,touchleave)が反応してup(e)メソッドが実行されるのを防ぐために判定している)
        if (this.isDragging) {
            console.log('up');
            const stickUpperHalfY = this.stickUpperHalf.offsetTop;
            const stickUpperHalfHeight = this.stickUpperHalf.clientHeight;

            //ドラッグ終了
            this.isDragging = false;

            //スタートレバーを倒しきるのに十分な移動量であれば
            if (this.distance >= (stickUpperHalfY + stickUpperHalfHeight) * 2) {
                //上半分のスティックに追従していたボールを非表示
                this.hideBall(this.ball);

                //下半分のスティックに追従していたボールを表示
                this.showBall(this.ballBottom);

                //倒しきったのでスタートレバーを固定
                this.stickUpperHalf.style.height = `${0}%`;
                this.stickLowerHalf.style.height = `${100}%`;

                //スロットスタート
                slotMachine.isPlaying = true;
            } 

            //十分でなければ
            else {
                //スタートレバーを元の位置に戻す
                this.ball.style.transform = '';
                this.stickUpperHalf.style.height = "";
                this.stickLowerHalf.style.height = "";
            }
        }
    }
}

スタートレバーについてですが、始めはボールとスティックを共に一つ用意してtransformscaleYでスティックの長さの調節してそれに合わせてボールの追従を試みましたが、ウィンドウのリサイズ後に上手くボールが追従せず、大変苦労しました。

そこでようやく辿り着いた方法が、スタートレバーを高さ100%の上半分と高さ0%の下半分に分け、distanceの変化に応じて上半分の高さが0%になったら下半分を高さ0%から100%にしていくことでスタートレバーが倒れたように見せる方法です。

SlotMachineクラス

スロットマシーン全体に関するSlotMachineクラスを作成します。

class SlotMachine {
    //スロットが動いたかどうか
    private _isPlaying: boolean = false;

    //インスタンス化
    private startLever: StartLever;
    private display: Display;
    private resultMsg: ResultMsg;
    private reels: Reels;
    private stopBtns: StopBtns;
    private replayBtn: ReplayBtn;
    constructor() {
        this.display = new Display;
        this.reels = new Reels;
        this.stopBtns = new StopBtns(this.reels, this.judge.bind(this));
        this.startLever = new StartLever;
        this.resultMsg = new ResultMsg;
        ///this.replayBtn = new ReplayBtn('replay-btn', this.reels, this.display, this.resultMsg, this.startLever);
        this.replayBtn = new ReplayBtn('replay-btn', this.reset.bind(this));
    }

    //スロットを動かすメソッド
    run() {
        this.reels.move();
        this.stopBtns.enabled();
    }

    //判定を行うメソッド
    judge() {
        //リプレイボタンを表示
        this.replayBtn.show();

        //真ん中横・左斜め上・右斜め上1行全て7が揃ったら「Great!!!!!」
        const reels = this.reels;
        const result = this.resultMsg;
        //console.log(reels);
        if (
            reels.elems[0].items[2].innerHTML == '<span class="seven color-orange">7</span>' &&
            reels.elems[1].items[2].innerHTML == '<span class="seven color-orange">7</span>' &&
            reels.elems[2].items[2].innerHTML == '<span class="seven color-orange">7</span>' ||
            reels.elems[0].items[1].innerHTML == '<span class="seven color-orange">7</span>' &&
            reels.elems[1].items[2].innerHTML == '<span class="seven color-orange">7</span>' &&
            reels.elems[2].items[3].innerHTML == '<span class="seven color-orange">7</span>' ||
            reels.elems[0].items[3].innerHTML == '<span class="seven color-orange">7</span>' &&
            reels.elems[1].items[2].innerHTML == '<span class="seven color-orange">7</span>' &&
            reels.elems[2].items[1].innerHTML == '<span class="seven color-orange">7</span>'
        ) {
            result.isGreat;
        } 

        //7以外で真ん中横1行or左斜め上or右斜め上が揃ったら
        else if (
            (reels.elems[0].items[2].innerHTML == reels.elems[1].items[2].innerHTML &&
             reels.elems[1].items[2].innerHTML == reels.elems[2].items[2].innerHTML) ||
            (reels.elems[0].items[1].innerHTML == reels.elems[1].items[2].innerHTML &&
             reels.elems[1].items[2].innerHTML == reels.elems[2].items[3].innerHTML) ||
            (reels.elems[0].items[3].innerHTML == reels.elems[1].items[2].innerHTML &&
             reels.elems[1].items[2].innerHTML == reels.elems[2].items[1].innerHTML)
        ) {
            result.isGood;
        }

        //何も揃っていなければ
        else {
            result.isRegret;
        }
    }

    //スロットマシーンをリセットするメソッド
    reset() {
        this.resultMsg.empty();
        this.resultMsg.hide();
        this.startLever.reset();
        this.replayBtn.hide();
        this.isPlaying = false;
        this.reels.reset();
        this.reels.init();
    }

    //他のクラスで参照するためにgetterで定義
    get isPlaying() {
        return this._isPlaying;
    }

    //trueが代入されるとスロットマシーンを動かす
    set isPlaying(flag) {
        this._isPlaying = flag;
        if(flag) {
            this.run();
            console.log('run');
        }else{
            return;
        }
    }
}

このクラスではそれぞれのオブジェクトを集合させて全体を制御し、スロットマシーンを動かすかどうかを判別するisPlayingメソッドを始め、スロットマシーンを動かすrunメソッド、判定を行うjudgeメソッド、スロットマシーンをリセットするresetメソッドを持ちます。

判定とその結果に関しては以下の通りです。

条件 結果
真ん中横一行or左斜めor右斜めが全て7 Great
真ん中横一行or左斜めor右斜め Good
何処も揃わない Regret

スロットマシーンを完成させる

最後にSlotMachineクラスからslotMachineオブジェクトを生成することでスロットマシーンが完成します。

//スロットマシーンオブジェクトを生成
const slotMachine = new SlotMachine;

最後に

以上でHTML+CSS+JavaScriptでスロットマシーンを作る方法を終わります。今後はポイントが加算されたり、その結果をツイートできるといったゲームらしい要素を取り入れる予定です。

参考文献