ひとりでのアプリ開発 - fineの備忘録 -

ひとりでアプリ開発をするなかで起こったことや学んだことを書き溜めていきます

Web開発 - ラバーダック・デバッグをするためのウェブアプリを作ってみた

初めに

 ラバーダッグ・デバッグをブラウザ上でするためのアプリを作成したので、記事にまとめました。

ラバーダッグ・デバッグ(Rubber duck debugging)とは

 独り言でコードを説明する過程で解決策を思いつくというものです。プログラマーがラバー・ダック(アヒルちゃん)を持ち歩きアヒルちゃんに向かってコードを1行ずつ説明することによりデバッグを行うという話が由来です。アンドリュー・ハントとデビッド・トーマスの共著による "The Pragmatic Programmer" という本で紹介されたもので、無生物を用いることにより、プログラマーは、他人を煩わせることなく目的を達成できます。

完成図


簡単な機能の紹介

  • ユーザーの入力を受け付け、その入力を画面上部に表示する
  • ユーザーの入力を表示しきれなくなったら、スクロールできるようにする
  • ユーザーが入力したら、あひるちゃんがうなずき、「クワッ」と鳴く

構成

ファイル 説明
index.html HTML
styles.css CSS
script.js JavaScript
furo-duchy.png あひるちゃんの画像(いらすとや)
MonomaniacOne-Regular.ttf フォント

コード全文

HTML
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="styles.css">
    <title>Rubber Duck Debugging</title>
</head>
<header>
    <h1>Rubber Duck Debugging</h1>
    <p>ラバーダック・デバッグをウェブ上で体験するためにアプリ。</p>
</header>
<body>
    <div class="upper-half">
        <div class="scroll-container">
            <div class="input-text-list" id="inputTexts"></div>
        </div>
    </div>

    <div class="lower-half">
        <div class="rubber-duck-container">
            <div class="rubber-duck" id="rubberDuck">
                <div class="speech-bubble" id="speechBubble"></div>
            </div>
        </div>
        <div class="user-input-container">
            <input type="text" id="userInput" placeholder="何か困ったことを入力...">
            <button onclick="handleUserInput()">ユーザー入力を処理</button>
        </div>
    </div>
    <script src="script.js"></script>
</body>
</html>
CSS
@font-face {
    font-family: 'Monomaniac One';
    src: url('MonomaniacOne-Regular.ttf') format('truetype');
}

header {
    text-align: center;
    padding: 10px;
    background-color: #ffeb3b;
    color: #FFF;
    font-family: 'Monomaniac One', sans-serif;
}

header h1 {
    font-size: 48px;
    margin: 0;
    text-shadow:
        3px 3px 0 #000,
        -1px -1px 0 #000,
        1px -1px 0 #000,
        -1px 1px 0 #000,
        1px 1px 0 #000;
}

header p {
    font-size: 24px;
    margin: 0;
    text-shadow:
        1.5px 1.5px 0 #000,
        -0.5px -0.5px 0 #000,
        0.5px -0.5px 0 #000,
        -0.5px 0.5px 0 #000,
        0.5px 0.5px 0 #000;
}

body {
    display: flex;
    flex-direction: column;
    height: 100vh;
    margin: 0;
    overflow-y: scroll;
}

.upper-half {
    text-align: center;
    background-color: #b3d7f7;
    flex-shrink: 1;
}

.scroll-container {
    overflow-y: scroll;
    background-color: #ffffff;
    margin: 10px;
    border-radius: 5px;
    height: calc(38vh);
}

.input-text-list {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    padding: 5px;
}

.input-text-item {
    background-color: #f0f0f0;
    padding: 5px;
    margin: 5px 0;
    border-radius: 5px;
}

.rubber-duck-container {
    order: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    position: relative;
}

.rubber-duck {
    width: 400px;
    height: 325px;
    background-image: url('furo_ducky.png');
    /* rubber-duckの画像 */
    background-size: cover;
    transition: margin-right 0.3s ease-in-out;
}

.speech-bubble {
    font-family: 'Monomaniac One';
    font-size: xx-large;
    background-color: #fff;
    padding: 10px;
    border-radius: 8px;
    box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
    display: none;
    position: absolute;
    top: 50%;
    left: 100%;
    transform: translate(-50%, -50%);
}

.lower-half {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: flex-end;
    flex-shrink: 1;
}

.user-input-container {
    order: 2;
    display: flex;
    flex-direction: column;
    align-items: center;
    padding: 10px;
    background-color: #f0f0f0;
    border-radius: 5px;
}

#userInput {
    margin-top: 5px;
    padding: 10px;
    width: 100%;
    max-width: 400px;
    box-sizing: border-box;
    border: 1px solid #ccc;
    border-radius: 3px;
}

#userInput::placeholder {
    color: #999;
}

button {
    margin-top: 5px;
    padding: 10px;
    width: 100%;
    max-width: 400px;
    box-sizing: border-box;
    background-color: #4caf50;
    color: #fff;
    border: none;
    border-radius: 3px;
    cursor: pointer;
}

button:hover {
    background-color: #45a049;
}

/* Media query for screens larger than 600px */
@media screen and (min-width: 601px) {
    #userInput {
        width: 600px;
    }
}

/* うなずくアニメーションを適用するクラス */
.nodding {
    animation: nodAnimation 0.5s ease-in-out;
    transform-origin: center bottom;
}

/* うなずくアニメーションを定義 */
@keyframes nodAnimation {

    0%,
    100% {
        transform: rotate(0deg);
    }

    50% {
        transform: rotate(-20deg);
    }
}

.input-text-list {
    display: block;
    flex-wrap: column;
    align-items: flex-start;
    /* 要素が横に溢れたら改行 */
    margin-top: 10px;
}

.input-text-item {
    background-color: #f0f0f0;
    padding: 5px;
    margin: 5px 0;
    border-radius: 5px;
}
JavaScript
document.addEventListener('DOMContentLoaded', function () {
    const inputTexts = document.getElementById('inputTexts');
    const rubberDuck = document.getElementById('rubberDuck');
    const speechBubble = document.getElementById('speechBubble');
    const userInput = document.getElementById('userInput');

    const inputTextList = [];

    // ユーザーが入力したときに呼ばれる関数
    window.handleUserInput = function () {
        const inputText = userInput.value.trim();

        if (inputText !== '') {
            nodRubberDuck();
            showSpeechBubble();
            showInputText();
        }
    };

    // ラバーダックがうなずく関数
    function nodRubberDuck() {
        // うなずくアニメーションを適用
        rubberDuck.classList.add('nodding');

        // アニメーションを解除して通常の姿勢に戻す
        setTimeout(function () {
            rubberDuck.classList.remove('nodding');
        }, 2000);
    }

    // 吹き出しを表示する関数
    function showSpeechBubble() {
        speechBubble.textContent = "クワッ";
        speechBubble.style.display = 'block';

        // 3秒後に吹き出しを非表示にする
        setTimeout(function () {
            speechBubble.style.display = 'none';
        }, 3000);
    }

    function showInputText() {
        const inputText = userInput.value.trim();
    
        if (inputText !== '') {
            inputTextList.push(inputText);
            const inputTextItem = document.createElement('div');
            inputTextItem.className = 'input-text-item';
            inputTextItem.textContent = inputText;
            inputTexts.appendChild(inputTextItem);
            userInput.value = ''; // 入力欄をクリア

            inputTexts.scrollTop = inputTexts.scrollHeight;
        }
    }
});