2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Gemini】【プロンプト有】Canvasと「@ktrk2002の利き手クイズ」を作ったレポート

Posted at

はじめに

GeminiのCanvasを使って、「@ktrk2002の利き手クイズ」を作りました ↓
ぜひ回答してみてください♪
(「時と場合によらないか?」な問いもありますが、ご了承ください...)

Canvasと一緒にゲームを作る

@ktrk2002の利き手クイズ」を作る

個人的な話ですが、利き手が動作によって変わります。
完全な両利きではなく、「ボールを投げるのは右」「文字を書くのは左」などバラバラです。

自分では把握していない利き手もありそう+ちょうど整理してみたいので、クイズにします

さっそくGeminiに相談する

ツール -> Canvas を選んで相談してみます

image.png

以下の内容でお願いしてみました

ゲームを作ろうとしています。ゲームの概要と手順を伝えるので作成可能か教えてください

- 私は利き手が物事によって変わります
- 完全な両利きではなく、ボールを投げるのは右、文字を書くのは左などバラバラです
- この様々な事象をデータベースに、「〇〇は左?右?」の二択クイズを作りたいです


手順
- 「左」「右」の二択クイズを作成してください
- 私が様々な事象を記録して、スプレッドシート(.xlsx)で渡します
- スプレッドシートを読み込んで、データベースとして問題を作成してください

Geminiのアウトプット

スプレッドシート(.xlsx)ファイルを直接読み込む機能は、Webブラウザのセキュリティ制限とこの環境の制約により実装できません。

スプレッドシートの読み込みはできないようでしたが、お願いしてからすぐに二択クイズを作成してくれました!

出力されたコード
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>@ktrk2002の利き手クイズ</title>
    <!-- Tailwind CSS CDN -->
    <script src="https://cdn.tailwindcss.com"></script>
    <style>
        /* カスタムスタイル */
        body {
            font-family: 'Inter', sans-serif;
            background-color: #f7f7f7;
            display: flex;
            justify-content: center;
            align-items: center;
            min-height: 100vh;
        }
        .quiz-container {
            max-width: 480px;
            width: 95%;
            background: white;
            padding: 1.5rem;
            border-radius: 1.5rem;
            box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
        }
        .quiz-button {
            transition: all 0.2s;
            transform: translateY(0);
        }
        .quiz-button:hover {
            box-shadow: 0 4px 10px rgba(66, 153, 225, 0.4);
            transform: translateY(-2px);
        }
        .quiz-button:active {
            transform: translateY(0);
        }
        /* ロード画面のスタイル (完全に削除しました) */

        /* メインコンテンツは常に表示状態とします */
        #main-content {
            /* display: none を削除 */
            display: block; 
        }
    </style>
</head>
<body>

    <!-- ローディングオーバーレイを完全に削除しました -->
    
    <div class="quiz-container" id="main-content">
        <h1 class="text-3xl font-extrabold text-center text-gray-800 mb-6">@ktrk2002の利き手クイズ</h1>
        
        <div id="quiz-area">
            <div class="text-lg font-medium text-gray-600 mb-2">
                <!-- 初期値をプレースホルダーにして、JSで即座に書き換えます -->
                <span id="question-counter">問題数...</span>
            </div>
            
            <div class="bg-indigo-100 p-6 rounded-xl mb-8">
                <p id="question-text" class="text-2xl font-bold text-indigo-800 text-center">クイズを読み込み中...</p>
            </div>
            
            <div class="flex space-x-4">
                <button id="left-button" data-hand="左" class="quiz-button flex-1 bg-blue-500 hover:bg-blue-600 text-white font-bold py-4 px-4 rounded-xl text-xl shadow-md">
                    左
                </button>
                <button id="right-button" data-hand="右" class="quiz-button flex-1 bg-red-500 hover:bg-red-600 text-white font-bold py-4 px-4 rounded-xl text-xl shadow-md">
                    右
                </button>
            </div>

            <!-- メッセージ表示エリア (正誤判定用) -->
            <div id="message-box" class="mt-6 h-8 text-center text-xl font-semibold"></div>
        </div>

        <div id="result-area" class="hidden text-center">
            <h2 class="text-4xl font-extrabold text-green-600 mb-4">クイズ終了!</h2>
            <p class="text-xl text-gray-700 mb-6">あなたのスコア:</p>
            <p id="final-score" class="text-6xl font-black text-indigo-700">0 / 0</p>
            
            <button id="restart-button" class="mt-8 bg-green-500 hover:bg-green-600 text-white font-bold py-3 px-8 rounded-xl text-lg shadow-lg">
                もう一度挑戦
            </button>
        </div>
    </div>

    <!-- 外部依存のない純粋なJavaScriptとして再構成 -->
    <script>
        // =================================================================
        // 1. DOM要素と定数
        // =================================================================
        
        let isQuizStarted = false; 
        
        // 出題数を10問に設定
        const QUESTION_LIMIT = 10; 

        // =================================================================
        // 2. クイズデータ (スプレッドシートの代わり)
        // =================================================================

        const handednessData = [
            { activity: 'ボールを投げる', answer: '右' },
            { activity: '文字を書く', answer: '左' },
            { activity: 'ビンの蓋をあける', answer: '右' },
            { activity: '歯磨き', answer: '左' },
            { activity: '習字', answer: '左' },
            { activity: '箸を使う', answer: '左' },
            { activity: 'ライブで手をつきあげる', answer: '右' },
            { activity: 'ドアノブを回す', answer: '右' },
            { activity: 'トラックパッドを触る', answer: '左' },
            { activity: 'ナイフとフォークで食べる時、ナイフを持つ手', answer: '右' },
            { activity: 'パン屋さんのトング', answer: '左' },
            { activity: '頬杖をつく', answer: '右' },
            { activity: '餃子のひだを作る', answer: '右' },
            { activity: 'じゃがりこの蓋をめくる', answer: '右' },
            { activity: '紙を破る', answer: '左' },
            { activity: '財布から千円札を出す', answer: '右' },
            { activity: 'カッター', answer: '左' },
            { activity: '傘をさす', answer: '左' },
        ];

        // クイズの状態変数
        let currentQuestionIndex = 0;
        let score = 0;
        let shuffledData = [];

        // DOM要素
        const questionCounterEl = document.getElementById('question-counter');
        const questionTextEl = document.getElementById('question-text');
        const leftButton = document.getElementById('left-button');
        const rightButton = document.getElementById('right-button');
        const quizArea = document.getElementById('quiz-area');
        const resultArea = document.getElementById('result-area');
        const finalScoreEl = document.getElementById('final-score');
        const restartButton = document.getElementById('restart-button');
        const messageBoxEl = document.getElementById('message-box');

        // =================================================================
        // 3. クイズロジック
        // =================================================================

        /**
         * 配列をシャッフルする(Fisher-Yatesアルゴリズム)
         * @param {Array} array シャッフルする配列
         * @returns {Array} シャッフルされた配列
         */
        function shuffleArray(array) {
            for (let i = array.length - 1; i > 0; i--) {
                const j = Math.floor(Math.random() * (i + 1));
                [array[i], array[j]] = [array[j], array[i]];
            }
            return array;
        }

        /**
         * クイズを初期化し、最初の質問を表示します。
         */
        function startQuiz() {
            if (isQuizStarted) return; // 二重起動防止
            isQuizStarted = true;

            currentQuestionIndex = 0;
            score = 0;
            
            // データが空でないかチェック
            if (handednessData.length === 0) {
                 questionTextEl.textContent = 'エラー: クイズデータがありません。 handednessDataにデータを追加してください。';
                 return;
            }
            
            // 1. 全データをシャッフル
            const allShuffled = shuffleArray([...handednessData]);
            
            // 2. 最初のQUESTION_LIMIT問だけを選択して出題データとする
            shuffledData = allShuffled.slice(0, QUESTION_LIMIT);
            
            // 質問数が0になった場合の安全チェック
            if (shuffledData.length === 0) {
                questionTextEl.textContent = 'エラー: 出題できるクイズがありません。';
                return;
            }
            
            quizArea.classList.remove('hidden');
            resultArea.classList.add('hidden');
            messageBoxEl.textContent = '';
            
            loadQuestion();
        }

        /**
         * 現在の質問を画面に表示します。
         */
        function loadQuestion() {
            if (currentQuestionIndex >= shuffledData.length) {
                // 全ての質問が終了
                endQuiz();
                return;
            }

            const currentQuestion = shuffledData[currentQuestionIndex];
            
            questionCounterEl.textContent = `問 ${currentQuestionIndex + 1} / ${shuffledData.length}`; // ここは10問中の何問目になる
            questionTextEl.textContent = `${currentQuestion.activity} は左?右?`;
            
            // ボタンを有効化
            leftButton.disabled = false;
            rightButton.disabled = false;
            leftButton.classList.remove('opacity-50', 'cursor-not-allowed');
            rightButton.classList.remove('opacity-50', 'cursor-not-allowed');

            // メッセージをクリア
            messageBoxEl.textContent = '';
        }

        /**
         * ユーザーの選択を処理します。
         * @param {string} selectedHand ユーザーが選択した利き手 ('左' or '右')
         * @param {HTMLElement} clickedButton クリックされたボタン要素
         */
        function handleAnswer(selectedHand, clickedButton) {
            const currentQuestion = shuffledData[currentQuestionIndex];
            const correctAnswer = currentQuestion.answer;
            
            // ボタンを無効化して連打を防ぐ
            leftButton.disabled = true;
            rightButton.disabled = true;
            leftButton.classList.add('opacity-50', 'cursor-not-allowed');
            rightButton.classList.add('opacity-50', 'cursor-not-allowed');

            let isCorrect = selectedHand === correctAnswer;

            // メッセージボックスで結果を表示
            if (isCorrect) {
                score++;
                messageBoxEl.textContent = '⭕ 正解!';
                messageBoxEl.classList.remove('text-red-500');
                messageBoxEl.classList.add('text-green-600');
                clickedButton.classList.remove('bg-blue-500', 'bg-red-500');
                clickedButton.classList.add('bg-green-500', 'hover:bg-green-600');
            } else {
                messageBoxEl.textContent = `❌ 不正解!正解は「${correctAnswer}」`;
                messageBoxEl.classList.remove('text-green-600');
                messageBoxEl.classList.add('text-red-500');
                clickedButton.classList.add('bg-gray-400'); // 不正解のボタンを灰色に
            }

            // 1.5秒後に次の質問へ
            setTimeout(() => {
                // 正誤判定後のボタンの色を元に戻す
                leftButton.classList.remove('bg-green-500', 'bg-gray-400');
                rightButton.classList.remove('bg-green-500', 'bg-gray-400');
                // クリックされたボタンの色を元に戻す(間違っていた場合の色を解除)
                if (clickedButton.id === 'left-button') {
                    clickedButton.classList.remove('bg-gray-400');
                    clickedButton.classList.add('bg-blue-500');
                } else {
                    clickedButton.classList.remove('bg-gray-400');
                    clickedButton.classList.add('bg-red-500');
                }

                currentQuestionIndex++;
                loadQuestion();
            }, 1500);
        }

        /**
         * クイズを終了し、結果を表示します。
         */
        function endQuiz() {
            const totalQuestions = shuffledData.length;
            
            finalScoreEl.textContent = `${score} / ${totalQuestions}`;
            
            quizArea.classList.add('hidden');
            resultArea.classList.remove('hidden');
            messageBoxEl.textContent = '';

            // スコア保存機能は現在無効化されています。
            console.log(`[Score] 最終スコア: ${score}/${totalQuestions}`);
        }

        // =================================================================
        // 4. イベントリスナーとアプリ起動
        // =================================================================

        leftButton.addEventListener('click', () => handleAnswer('左', leftButton));
        rightButton.addEventListener('click', () => handleAnswer('右', rightButton));
        restartButton.addEventListener('click', () => {
            isQuizStarted = false; // クイズ状態をリセット
            startQuiz();
        });

        // 起動ロジック: DOMContentLoadedを待つ必要はなく、スクリプト実行後すぐに起動します
        window.addEventListener('load', startQuiz); 

    </script>
</body>
</html>

アウトプットの特徴

  • 一つのファイルで、HTML,CSS,JavaScriptを書いている
    • コメントで処理のセクションを分けている

Canvasの環境では、動作するために一つのファイルにまとめる必要があるようです

  • Tailwind CSSを使用しつつ、ユーティリティクラスでは表現しにくいホバーエフェクトや特定コンテナのレイアウトを、<style>タグ内のカスタムCSSで補完している

主な機能

Gemini機能を追加

「Gemini機能を追加」を押すと、Gemini APIを使った機能を追加してくれます。

image.png

選択して質問

「選択して質問」を押すと、相談したい箇所をプレビューで直接選択して質問できます

image.png

アウトプットの問題

コードを見ると、ロジックも含めて書いてくれていて、二択クイズとして機能していそうでした
しかし、プレビューを見ても「準備中...」でフリーズしていました

image.png

フリーズの原因

Canvasの環境特性と外部依存(ライブラリ読み込みやUI切替)が組み合わさった「描画ブロッキング」が原因だったようです。

  • Firebase等の外部ライブラリ初期化処理が、UIスレッドをブロック
    • この処理自体を遅らせても、import文によるモジュール読み込み自体がレンダリングをブロック
  • import文の削除後も、残っていたローディング画面の非表示処理が、Canvas環境で描画負荷となりフリーズ

:pencil: Firebase
・Googleが提供するモバイルおよびWeb開発プラットフォーム
・ユーザー認証、データベースの保存、通知の送信などを簡単に実装できる

再発しないためにプロンプトに入れると良い内容

いくつか原因が重なったフリーズの再発を防ぐために、以下をプロンプトに含めると良さそうです。
※ 開発の要件によっては、適切ではないかもです。

目的 プロンプト入れると良さそうな内容
起動の高速化 アプリケーションの起動時、ローディング画面やUIの表示切り替えを一切行わず、メインコンテンツを直ちに表示させてください。
外部依存の管理 Firebaseや外部APIの初期化が必要な場合、window.onload後に非同期処理で実行するなど、UIの描画をブロックしないように注意してください。
CSSの最適化 HTML要素の表示/非表示の切り替え(display: noneやvisibility: hidden)を多用せず、可能な限り最初からメインUIが表示される構造を優先してください。
開発環境の考慮 Canvas(開発環境)におけるUIスレッドのブロックを厳格に避け、描画性能を最優先したコード構造にしてください。

試しに、最初のプロンプトに追記して、新規チャットでお願いしてみると、フリーズせず機能しました!

おまけ - Canvasでスライドが作れるようになった!

プレゼンテーションを作成する機能が追加されたみたいです!

  • 対話しながらドキュメントをまとめる
  • 参考資料をアップロードして、スライド生成
  • そのまま Google スライドにエクスポートできる!

これもまた試してみたいですね...!

まとめ

Canvasは業務で使うなら、実装のたたきとして使ったり、機能系のプロトタイプをサクッと作るのに良さそうでした!
Gemini以外のAIもそうですが、日々できることが増えていくので、試していかないと...!

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?