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?

Markdown AIのサーバーAI機能を使ってWebサイトを作ってみよう by MarkdownAI Advent Calendar 2024の12月20日分の企画に滑り込みます。

Markdown AIで架空の映画をテーマにしたWebサイトを作ってみました。

内容はAIGeminiImageFxをもりもりに使っています。

  • Google Apps Scriptでホストしたタイピングゲーム
  • Looker Studioによるダッシュボード

上記も使ってみました。

Markdown AIとは

その名の通りMarkdownを書いて、手軽にWebサイトを構築できるツールです。
HTMLCSSなどの知識がなくても、Markdownで書いたテキストをWebサイトとして公開できます。
サンプルアプリが充実していますので、ざっとどんなものが作れるのかは調べられます。

アレンジの詳細は下記を参考にしてみてください。

項目 説明
作成できるWebページ数 最大500ページ
1ページあたりの入力可能文字数 半角英語で約100万文字
挿入可能な画像枚数 500枚
画像の最大サイズ 7.5MB
拡張Markdown テーブル、チェックボックス ※GitHubで使える拡張機能やiframe にも対応しているようです

作成できるWebページを最大500ページまで、各ページには、半角英語で約100万文字まで入力可能です。

Q:HTML形式で記述すれば反映されますか? A:反映されます。

Q&A – Markdown編 –

上記がありだとなんでもできそうです。ということでLet's challenge!!
アレンジマシマシでWebサイトを作ってみましょう!

Markdown AIで作ってみたWebサイト

Markdown AIを使って、映画 「OTTER FORCE: The Last Water Guardian」 の架空の広告サイトを作成してみました。

image.png

画像はImageFxをもりもりに使っています。
下記の通り架空の情報です。

登場するキャラクター、出来事、団体は、すべてOTTER FORCEの世界観内での創作です。 実在の人物や出来事、団体との類似は完全に偶然であり、意図したものではありません。

MarkdownだけではなくHTMLをふんだんに使ったサイトです。
今回のテーマはServer AIの活用のため、チャットボットを入れてみました。

数回のクリックでServer AI(チャットボット)を挿入できます。

下記を入れると
image.png

ページの情報に基づいた回答が返ってきます。

"もちろん!OTTER FORCE: The Last Water Guardianの特長は、冒険と友情、そして環境保護のメッセージが詰まった素晴らしいストーリーです。映画は2157年の未来を舞台に、主人公のリップルが古代のライトセーバーを発見し、邪悪なハイドラテックと戦いながら地球の水のバランスを取り戻す旅を描いています。また、映画のビジュアルは美しい水中シーンや未来的なテクノロジーで彩られ、観客を魅了します。リップルの成長や仲間たちとの絆も感動的で、観る人に勇気を与えてくれます。もっとリップルの冒険や仲間たちについて知りたいですか?"

返答例

内容については、ぜひサイトで試してみてください。

Server AIの使い方

  1. Markdown AIのロボット🤖のマークをクリックします
    image.png
  2. Create Modelの画面が出るため、モデルの名前AIモデルの設定、Promptを指定、必要に応じてKnowledgeを指定
    image.png
    1. 選択可能なAIモデル
      1. gpt-4o-mini
      2. gpt-3.5-turbo
      3. gpt-4
      4. claude
      5. gemini
      6. dallL-e-3
    2. Knowledge
      1. 作成したMarkdownのページ、もといfilelinkを指定

私はgpt-4o-miniを指定しています。
Knowledgeは表示されるWebページのMarkdownです。

Server AIの詳細は下記のようにまとめられます。

項目 機能
最大応答 1回の返信で出力可能な最大文字数。2048トークン(英語で約4000〜5000文字)まで対応
プロンプト入力制限 約100万文字
複数モデルの生成 リスト未指定時は「モデル名」入力で作成可
モデル作成上限 最大500個
Webサイトへの埋め込み方法 サイト作成画面の「挿入」→「スクリプト」から AI モデルを選択し、テキストフォームと回答ボタンを配置
Knowledge機能 file、link
Prompt機能 AIへの指示設定機能、システムプロンプトの立ち位置
Playground機能 AIの動作確認・応答テスト用の検証

前置きが長くなりましたが作成したページの内容に移ります。

Google Apps Script Typing Shooting Game

Typing Shooting Gameを作成して参照しています。
当初Markdown AIでホストするつもりでしたが、Google Apps Scriptでいいのでは🧐となり、書き換えました。

image.png

Google Apps Scriptで作成しています。

一回でも外したら終了の残念な仕様です。

Google Apps Scriptの機能を利用して、Google スプレッドシートにゲーム🎮の結果を記録しています。

image.png

その内容がiframeLooker Studioに表示されます。

image.png

コスパが非常にいいプランですね。
Google Apps Scriptは、スプレッドシートにバインドされたスクリプトです。
がばがば条件ですが、サクッと設定できます。

#Google Apps Script
dogetでHTMLを表示しています。Google Apps Scriptの利点はsaveScoreで発揮されます。

コード.gs
function doGet() {
  return HtmlService.createHtmlOutputFromFile('index.html')
    .setTitle('Shooting Game')
    .setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
}

function saveScore(username, score) {
  try {
    const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Scores');

    const timestamp = new Date();
    const rowData = [timestamp, username, score];

    sheet.appendRow(rowData);

    return { success: true, message: 'Score submitted successfully!' };
  } catch (error) {
    Logger.log('Error saving score: ' + error.toString());
    return { success: false, message: 'Failed to submit score. Please try again.' };
  }
}

Claude様様の内容です。

index.html
<!DOCTYPE html>
<html>

<head>
  <style>
    body {
      background: #1a1a1a;
      color: #fff;
      font-family: Arial, sans-serif;
      margin: 0;
      display: flex;
      flex-direction: column;
      align-items: center;
      min-height: 100vh;
      padding: 20px;
    }

    #game-container {
      position: relative;
      width: 800px;
      margin: 20px auto;
    }

    canvas {
      border: 2px solid #33ccff;
      background: #000033;
    }

    #word-input {
      width: 300px;
      padding: 10px;
      font-size: 18px;
      margin: 10px 0;
      background: #000033;
      color: #33ccff;
      border: 2px solid #33ccff;
      border-radius: 5px;
      text-align: center;
    }

    #current-word {
      font-size: 24px;
      color: #33ccff;
      margin: 10px 0;
      min-height: 36px;
    }

    #score {
      font-size: 20px;
      color: #33ccff;
      margin: 10px 0;
    }

    #game-over {
      display: none;
      position: absolute;
      top: 50%;
      left: 50%;
      transform: translate(-50%, -50%);
      background: rgba(0, 0, 0, 0.8);
      padding: 20px;
      border-radius: 10px;
      text-align: center;
    }

    button {
      padding: 10px 20px;
      font-size: 18px;
      background: #33ccff;
      color: #000033;
      border: none;
      border-radius: 5px;
      cursor: pointer;
      margin: 10px;
    }

    button:hover {
      background: #66ddff;
    }

    /* 新規追加のスタイル */
    #username-input {
      width: 200px;
      padding: 10px;
      font-size: 16px;
      margin: 10px 0;
      background: #000033;
      color: #33ccff;
      border: 2px solid #33ccff;
      border-radius: 5px;
    }

    #submit-score {
      background: #33ff33;
    }

    #submit-score:hover {
      background: #66ff66;
    }

    .score-message {
      color: #33ff33;
      margin: 10px 0;
      font-size: 16px;
    }
  </style>
</head>

<body>
  <div id="game-container">
    <div id="score">Score: 0</div>
    <canvas id="gameCanvas" width="800" height="600"></canvas>
    <div id="current-word"></div>
    <input type="text" id="word-input" placeholder="Type the word..." autocomplete="off">
    <div id="game-over">
      <h2>Game Over!</h2>
      <p id="final-score"></p>
      <input type="text" id="username-input" placeholder="Enter your name" maxlength="20">
      <button id="submit-score">Submit Score</button>
      <p id="submit-message" class="score-message"></p>
      <button onclick="startGame()">Play Again</button>
    </div>
  </div>

  <script>
    const canvas = document.getElementById('gameCanvas');
        const ctx = canvas.getContext('2d');
        const wordInput = document.getElementById('word-input');
        const currentWordDisplay = document.getElementById('current-word');
        const scoreDisplay = document.getElementById('score');
        const gameOverScreen = document.getElementById('game-over');
        const usernameInput = document.getElementById('username-input');
        const submitScoreButton = document.getElementById('submit-score');
        const submitMessage = document.getElementById('submit-message');

        const words = [
            'splash', 'ripple', 'water', 'crystal', 'force', 'otter',
            'aqua', 'wave', 'flow', 'swim', 'dive', 'fish',
            'river', 'ocean', 'stream', 'pool', 'lake', 'sea'
        ];

        const OTTER_SIZE = 50;
        const ENEMY_SIZE = 40;
        const LASER_SPEED = 7;
        const BASE_ENEMY_SPEED = 0.5;
        const ENEMY_SPAWN_RATE = 0.01;
        const DIFFICULTY_INCREASE_RATE = 0.0001;
        const MAX_ENEMIES = 4;
        let currentEnemySpeed = BASE_ENEMY_SPEED;

        let gameLoop;
        let score = 0;
        let currentWord = '';
        let currentEnemy = null;
        let enemies = [];
        let lasers = [];
        let otterX = canvas.width / 2;
        let gameActive = false;

        class Enemy {
            constructor(word) {
                this.x = Math.random() * (canvas.width - ENEMY_SIZE);
                this.y = 0;
                this.word = word;
                this.targeted = false;
                this.active = false;
            }

            draw() {
                ctx.fillStyle = this.targeted ? '#ff3333' : 
                              this.active ? '#33ff33' : '#33ccff';
                ctx.fillRect(this.x, this.y, ENEMY_SIZE, ENEMY_SIZE);
                
                ctx.fillStyle = this.active ? '#ffff00' : '#ffffff';
                ctx.font = '16px Arial';
                ctx.textAlign = 'center';
                ctx.fillText(this.word, this.x + ENEMY_SIZE/2, this.y + ENEMY_SIZE + 20);
            }

            update() {
                this.y += currentEnemySpeed;
                return this.y > canvas.height;
            }
        }

        class Laser {
            constructor(startX, startY, targetX, targetY) {
                this.x = startX;
                this.y = startY;
                const angle = Math.atan2(targetY - startY, targetX - startX);
                this.dx = Math.cos(angle) * LASER_SPEED;
                this.dy = Math.sin(angle) * LASER_SPEED;
                this.target = null;
            }

            draw() {
                ctx.strokeStyle = '#33ff33';
                ctx.lineWidth = 2;
                ctx.beginPath();
                ctx.moveTo(this.x, this.y);
                ctx.lineTo(this.x - this.dx * 2, this.y - this.dy * 2);
                ctx.stroke();
            }

            update() {
                this.x += this.dx;
                this.y += this.dy;
                return this.x < 0 || this.x > canvas.width || 
                       this.y < 0 || this.y > canvas.height;
            }
        }

        function drawOtter(x) {
            ctx.fillStyle = '#996633';
            ctx.fillRect(x - OTTER_SIZE/2, canvas.height - OTTER_SIZE, OTTER_SIZE, OTTER_SIZE);
        }

        function spawnEnemy() {
            if (Math.random() < ENEMY_SPAWN_RATE && enemies.length < MAX_ENEMIES) {
                const word = words[Math.floor(Math.random() * words.length)];
                const newEnemy = new Enemy(word);
                while (enemies.some(enemy => 
                    Math.abs(enemy.x - newEnemy.x) < ENEMY_SIZE * 1.5)) {
                    newEnemy.x = Math.random() * (canvas.width - ENEMY_SIZE);
                }
                enemies.push(newEnemy);
            }
        }

        function checkCollisions() {
            for (let laser of lasers) {
                currentEnemySpeed += DIFFICULTY_INCREASE_RATE;
            
                if (laser.target) {
                    const enemy = enemies.find(e => e === laser.target);
                    if (enemy) {
                        const dx = laser.x - (enemy.x + ENEMY_SIZE/2);
                        const dy = laser.y - (enemy.y + ENEMY_SIZE/2);
                        const distance = Math.sqrt(dx * dx + dy * dy);
                        
                        if (distance < ENEMY_SIZE/2) {
                            enemies = enemies.filter(e => e !== enemy);
                            lasers = lasers.filter(l => l !== laser);
                            score += 100;
                            scoreDisplay.textContent = `Score: ${score}`;
                            currentWord = '';
                            currentWordDisplay.textContent = '';
                            wordInput.value = '';
                            return;
                        }
                    }
                }
            }
        }

        async function submitScore(username, score) {
            try {
                const result = await google.script.run
                    .withSuccessHandler(function(response) {
                        if (response.success) {
                            submitMessage.textContent = response.message;
                            submitMessage.style.color = '#33ff33';
                            submitScoreButton.disabled = true;
                        } else {
                            throw new Error(response.message);
                        }
                    })
                    .withFailureHandler(function(error) {
                        submitMessage.textContent = 'Failed to submit score. Please try again.';
                        submitMessage.style.color = '#ff3333';
                    })
                    .saveScore(username, score);
            } catch (error) {
                submitMessage.textContent = 'Failed to submit score. Please try again.';
                submitMessage.style.color = '#ff3333';
            }
        }

        function gameOver() {
            gameActive = false;
            clearInterval(gameLoop);
            document.getElementById('final-score').textContent = `Final Score: ${score}`;
            gameOverScreen.style.display = 'block';
            usernameInput.value = '';
            submitMessage.textContent = '';
            submitScoreButton.disabled = false;
        }

        function update() {
            ctx.clearRect(0, 0, canvas.width, canvas.height);
            
            spawnEnemy();
            drawOtter(otterX);

            enemies = enemies.filter(enemy => {
                enemy.draw();
                if (enemy.update()) {
                    gameOver();
                    return false;
                }
                return true;
            });

            lasers = lasers.filter(laser => {
                laser.draw();
                return !laser.update();
            });

            checkCollisions();
        }

        function startGame() {
            gameActive = true;
            score = 0;
            enemies = [];
            lasers = [];
            currentWord = '';
            currentWordDisplay.textContent = '';
            wordInput.value = '';
            scoreDisplay.textContent = `Score: ${score}`;
            gameOverScreen.style.display = 'none';
            wordInput.focus();

            if (gameLoop) clearInterval(gameLoop);
            gameLoop = setInterval(update, 1000/60);
        }

        wordInput.addEventListener('input', (e) => {
            if (!gameActive) return;

            const typed = e.target.value.toLowerCase();
            
            if (!currentWord) {
                const availableEnemies = enemies.filter(e => !e.targeted);
                if (availableEnemies.length > 0) {
                    for (let enemy of availableEnemies) {
                        if (enemy.word.toLowerCase().startsWith(typed)) {
                            currentWord = enemy.word.toLowerCase();
                            enemy.targeted = true;
                            currentWordDisplay.textContent = currentWord;
                            currentWordDisplay.style.color = '#33ff33';
                            break;
                        }
                    }
                }
            }

            if (currentWord && typed === currentWord) {
                const targetEnemy = enemies.find(e => 
                    e.word.toLowerCase() === currentWord && e.targeted);
                
                if (targetEnemy) {
                    const laser = new Laser(
                        otterX,
                        canvas.height - OTTER_SIZE,
                        targetEnemy.x + ENEMY_SIZE/2,
                        targetEnemy.y + ENEMY_SIZE/2
                    );
                    laser.target = targetEnemy;
                    lasers.push(laser);
                }

                currentWord = '';
                currentWordDisplay.textContent = '';
                e.target.value = '';
            } else if (currentWord && !currentWord.startsWith(typed)) {
                const targetEnemy = enemies.find(e => 
                    e.word.toLowerCase() === currentWord && e.targeted);
                if (targetEnemy) {
                    targetEnemy.targeted = false;
                }
                currentWord = '';
                currentWordDisplay.textContent = '';
                currentWordDisplay.style.color = '#33ccff';
            }
        });

        // スコア送信イベントリスナーを追加
        submitScoreButton.addEventListener('click', () => {
            const username = usernameInput.value.trim();
            if (username) {
                submitScore(username, score);
            } else {
                submitMessage.textContent = 'Please enter your name';
                submitMessage.style.color = '#ff3333';
            }
        });

        startGame();
  </script>
</body>

</html>

おわりに

知識不足の自分でもシームレスな連携ができる時代に感激しています。
一方で2025年はどうなるんだろうと焦る今です。

上手く技術と付き合わないとですね!

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?