🅰️

Googleの新しいTUI Antigravity CLIを試してみた

に公開

Antigravityの軽量なターミナルユーザーインターフェースであるAntigravity CLI (AGY CLI) を試してみたので、セットアップの流れや、従来のGemini CLIとのコード生成比較をまとめてみました。

1. セットアップ

まずはドキュメント(Google Antigravity Documentation)を見ながらインストールをします。
Macの場合、以下のcurlコマンドでインストールができます。

curl -fsSL https://antigravity.google/cli/install.sh | bash

しかし、Antigravityのダウンロードサイトを見ると、CLIを使う前にAntigravityかAntigravity IDE側で認証を通しておく必要があるようです。

ここで少し混乱したのが、ツール群のバージョンと名前の関係です。

  • Antigravity 2.0: 複数のローカルエージェントを並行して管理するための司令塔。会話をプロジェクトごとにまとめ、複数のワークスペースを横断して操作し、予約メッセージ機能でルーチンワークを自動化できます。
  • Antigravity IDE: 多機能なエージェント型IDE。エージェントマネージャーやアーティファクト機能を備え、コードベースを深く理解します。

v1.0からのアップデートだと思ってv2.0をインストールすると、Antigravity IDEではなく、エージェントの管理デスクトップアプリがインストールされてしまいます。
今回は、せっかくなのでAntigravity IDEをインストールして認証を通します。

agyコマンドを実行すると、AGY CLIが起動します。

起動するとテーマ設定などが走ります。
今回は自分のGoogleアカウントで動かしてみようと思うので、Google OAuthを選択します。
リンクが表示されるので,それをクリックして認証を通します。
あとは,テーマ設定とデータ提供の利用規約に答えると完了です。

設定ファイルは~/.gemini/antigravity-cli/settings.jsonに作られます。
AGY CLI起動中に/configまたは/settingsと打つと設定画面が開くので、そこから設定できます。
自分はこんな感じの設定にしています。

{
  "colorScheme": "tokyo night",
  "editor": "vim",
  "enableTerminalSandbox": true,
  "model": "Gemini 3.5 Flash (High)",
  "notifications": true,
  "permissions": {
    "allow": [
      "command(git)",
      "command(npm test)"
    ],
    "deny": [
      "command(rm -rf)"
    ]
  },
  "runningLightSpeed": "fast",
  "statusLine": {
    "type": "",
    "command": "",
    "enabled": true
  },
  "trustedWorkspaces": [
  ]
}

Gemini CLIとの互換性を保つために起動すると、移行オプションが表示されます。
表示されない方は agy plugin import gemini コマンドを実行してください。


2. 覚えておくと便利な操作 & スラッシュコマンド

クイックTipsとスラッシュコマンドを日本語訳付きで表にまとめました。

クイックTips

アクション/機能 ヒント/コマンド
ファイルパスの自動補完 @ を入力するとパスの候補が表示されます
プロンプトの消去 esc を2回押すとプロンプトボックスが空になります(ストリーミング中でない場合)
ターミナルコマンドの実行 プロンプトの先頭に ! を付けると、ターミナルコマンドを直接実行できます
ヘルプの表示 ? を入力すると、ヘルプやスラッシュコマンドの一覧が表示されます
ツール呼び出しの出力削減 /config で verbosity(冗長性)を low に設定すると、大量のツール呼び出しによる出力を最小限に抑えられます
権限の管理 /config または /permissions から権限を制御できます
会話のやり直し /rewind または /undo を使うと、会話履歴を巻き戻せます
会話の分岐 /fork を使うと、別のワークスペースを立ち上げて、過去の時点から会話を枝分かれさせることができます
会話のクリア /clear を使うと、プロンプトが消去され、新しい会話セッションを開始できます
会話の再開 /resume を使うと、過去の会話ログの一覧を表示し、再開できます
自動保存と再開 CLIを閉じると、そのセッションを再開するために必要なコマンドが自動的に表示されます

スラッシュコマンド一覧

AGY CLI では、プロンプトボックスに直接入力するさまざまなスラッシュコマンドを使用して、会話の管理、設定の変更、エージェント機能の確認を行うことができます。

コマンド カテゴリ 用途
/resume (エイリアス /switch) 会話 会話ピッカーを開き、セッションの再開や切り替えを行います。
/rewind (エイリアス /undo) 会話 会話履歴を以前のチェックポイントまで巻き戻します。
/rename <name> 会話 アクティブな会話スレッドの名前を変更し、管理しやすくします。
/permissions 設定 エージェントの自律レベル(request-review, always-proceed, strict)を選択します。
/model 設定 デフォルトの推論モデルを選択します(設定はセッションをまたいで保持されます)。
/keybindings 設定 インタラクティブなキーボードショートカットエディタを開きます。
/statusline 設定 CLI ステータスバーに表示されるリアルタイムインジケーターをカスタマイズします。
/tasks ツール&監視 実行中のバックグラウンドタスクの監視、ログの表示、または終了を行います。
/skills ツール&監視 ローカルおよびグローバルにカプセル化されたエージェントのワークフローを閲覧します。
/mcp ツール&監視 Model Context Protocol(MCP)サーバーを設定・管理するためのパネルを開きます。
/open <path> ユーティリティ 指定したパスのファイルを、設定済みの外部エディタですぐに開きます。
/usage ユーティリティ ターミナル内でインラインのインタラクティブヘルプマニュアルを開きます。
/logout アカウント Google セッションからログアウトし、キャッシュされた資格情報を消去します。

ここの一覧にはないのですが,/fastコマンドを使うとエージェントがタスクを直接実行するため、より高速にタスクを実行できます。
/artifactsを実行するとエージェントの実装計画を管理できます。ここらへんは、Antigravity v1.0と同じで軌道修正がしやすそうでいいですね。
長いタスクを実行している際は/taskを使用して確認したり、/btwで質問を投げたりできるようです。

/model の設定画面を見ると、以下のようなモデルの選択肢が並んでいました。

  • Gemini 3.5 Flash (High)
  • Gemini 3.5 Flash (Medium)
  • Gemini 3.1 Pro (High)
  • Gemini 3.1 Pro (Low)
  • Claude Sonnet 4.6 (Thinking)
  • Claude Opus 4.6 (Thinking)
  • GPT-OSS 120B (Medium)

デフォルトはGemini 3.5 Flash (High)になっていました。
今までモデル名にpreviewがついていたのですが、つかなくなったのですね。
Gemini以外のモデルの顔ぶれはAntigravity v1.0と同じですね。


3. 実力検証:Gemini-CLI vs Antigravity CLI

今回は移行なので選択の余地はないのですが、せっかく両方使えるので比較をしてみます。
お題は「Three.jsを使って、1つのHTMLファイルだけで完結する美しい3D花火アニメーションを作ってください。」のプロンプト一個でPlanモード使わずにindex.htmlを生成してもらいます。
使用するモデルはGemini CLIはgemini-3.1-pro-preview、AGY CLIはGemini 3.1 Pro (High) を選択。
せっかくなので/fastコマンドを使ってエージェントがタスクを直接実行するようにしてみます。
なお私自身Three.jsのエキスパートではないので、コメントは参考までに。

Gemini CLIの結果

生成結果
Gemini CLIの生成画面

生成したコード

コード
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Beautiful 3D Fireworks</title>
    <style>
        body {
            margin: 0;
            padding: 0;
            overflow: hidden;
            background-color: #000;
            font-family: "Helvetica Neue", Arial, sans-serif;
        }
        #info {
            position: absolute;
            top: 20px;
            width: 100%;
            text-align: center;
            color: rgba(255, 255, 255, 0.7);
            pointer-events: none;
            font-size: 14px;
            letter-spacing: 2px;
            text-shadow: 0 0 10px rgba(0,0,0,0.8);
            z-index: 10;
        }
        canvas {
            display: block;
        }
    </style>
</head>
<body>
    <div id="info">DRAG TO ROTATE &nbsp;|&nbsp; SCROLL TO ZOOM</div>

    <!-- Import maps polyfill -->
    <script async src="https://unpkg.com/es-module-shims@1.8.0/dist/es-module-shims.js"></script>

    <script type="importmap">
        {
            "imports": {
                "three": "https://unpkg.com/three@0.160.0/build/three.module.js",
                "three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/"
            }
        }
    </script>

    <script type="module">
        import * as THREE from 'three';
        import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

        // 1. シーン、カメラ、レンダラーの初期設定
        const scene = new THREE.Scene();
        // 遠くが暗くなるようにフォグを設定
        scene.fog = new THREE.FogExp2(0x000000, 0.003);

        const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000);
        camera.position.set(0, 20, 100);

        const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: false });
        renderer.setSize(window.innerWidth, window.innerHeight);
        renderer.setPixelRatio(window.devicePixelRatio);
        // 色を鮮やかにするためのトーンマッピング
        renderer.toneMapping = THREE.ACESFilmicToneMapping;
        renderer.toneMappingExposure = 1.2;
        document.body.appendChild(renderer.domElement);

        // 2. カメラコントロール
        const controls = new OrbitControls(camera, renderer.domElement);
        controls.enableDamping = true;
        controls.dampingFactor = 0.05;
        controls.autoRotate = true;       // ゆっくり自動回転
        controls.autoRotateSpeed = 0.5;
        controls.maxPolarAngle = Math.PI / 2 + 0.1; // 地面の下に行き過ぎないように

        // 3. 背景(星空)の作成
        const createStarfield = () => {
            const starsGeometry = new THREE.BufferGeometry();
            const starsCount = 4000;
            const starPositions = new Float32Array(starsCount * 3);
            const starColors = new Float32Array(starsCount * 3);

            for(let i=0; i<starsCount; i++) {
                starPositions[i*3] = (Math.random() - 0.5) * 600;
                starPositions[i*3+1] = (Math.random() - 0.5) * 600;
                starPositions[i*3+2] = (Math.random() - 0.5) * 600;

                const color = new THREE.Color();
                // 青みや白っぽさをランダムに
                color.setHSL(Math.random() * 0.2 + 0.5, 0.8, Math.random() * 0.5 + 0.5);
                starColors[i*3] = color.r;
                starColors[i*3+1] = color.g;
                starColors[i*3+2] = color.b;
            }
            starsGeometry.setAttribute('position', new THREE.BufferAttribute(starPositions, 3));
            starsGeometry.setAttribute('color', new THREE.BufferAttribute(starColors, 3));

            const starsMaterial = new THREE.PointsMaterial({
                size: 0.8,
                vertexColors: true,
                transparent: true,
                opacity: 0.8,
                sizeAttenuation: true
            });
            return new THREE.Points(starsGeometry, starsMaterial);
        };
        const starField = createStarfield();
        scene.add(starField);

        // 4. 薄暗い地面を追加
        const gridHelper = new THREE.GridHelper(300, 30, 0x111122, 0x050511);
        gridHelper.position.y = -10;
        scene.add(gridHelper);

        // 5. パーティクルを美しく見せるためのテクスチャ生成
        const createParticleTexture = () => {
            const canvas = document.createElement('canvas');
            canvas.width = 32;
            canvas.height = 32;
            const context = canvas.getContext('2d');
            const gradient = context.createRadialGradient(16, 16, 0, 16, 16, 16);
            gradient.addColorStop(0, 'rgba(255,255,255,1)');
            gradient.addColorStop(0.2, 'rgba(255,255,255,0.8)');
            gradient.addColorStop(0.4, 'rgba(255,255,255,0.2)');
            gradient.addColorStop(1, 'rgba(0,0,0,0)');
            context.fillStyle = gradient;
            context.fillRect(0, 0, 32, 32);
            return new THREE.CanvasTexture(canvas);
        };
        const particleTexture = createParticleTexture();

        // 6. 花火のクラス定義
        const fireworks = [];

        class Firework {
            constructor() {
                this.phase = 'launch'; // launch -> explode
                this.rocketColor = new THREE.Color().setHSL(Math.random(), 1, 0.6);

                this.rocketMaterial = new THREE.PointsMaterial({
                    size: 3.0,
                    color: this.rocketColor,
                    transparent: true,
                    blending: THREE.AdditiveBlending, // 光るような合成
                    depthWrite: false,
                    map: particleTexture
                });

                // 打ち上げの初期位置
                this.startX = (Math.random() - 0.5) * 120;
                this.startY = -10;
                this.startZ = (Math.random() - 0.5) * 120;

                // 爆発する高さ
                this.targetY = Math.random() * 50 + 30;

                const rocketGeo = new THREE.BufferGeometry();
                rocketGeo.setAttribute('position', new THREE.BufferAttribute(new Float32Array([this.startX, this.startY, this.startZ]), 3));
                this.rocket = new THREE.Points(rocketGeo, this.rocketMaterial);
                scene.add(this.rocket);

                // 打ち上げの速度(上に上がる)
                this.rocketVelocity = {
                    x: (Math.random() - 0.5) * 0.2,
                    y: Math.random() * 0.4 + 1.2,
                    z: (Math.random() - 0.5) * 0.2
                };
            }

            update() {
                if (this.phase === 'launch') {
                    const pos = this.rocket.geometry.attributes.position.array;

                    pos[0] += this.rocketVelocity.x;
                    pos[1] += this.rocketVelocity.y;
                    pos[2] += this.rocketVelocity.z;

                    // 重力で少しずつ上昇速度を落とす
                    this.rocketVelocity.y -= 0.012;

                    this.rocket.geometry.attributes.position.needsUpdate = true;

                    // 頂点に達したら爆発
                    if (this.rocketVelocity.y <= 0 || pos[1] >= this.targetY) {
                        this.explode(pos[0], pos[1], pos[2]);
                        scene.remove(this.rocket);
                        this.rocket.geometry.dispose();
                        this.rocketMaterial.dispose();
                        this.phase = 'explode';
                    }
                    return true;
                } else if (this.phase === 'explode') {
                    const positions = this.explosionGeometry.attributes.position.array;

                    for (let i = 0; i < this.velocities.length; i++) {
                        // 重力
                        this.velocities[i].y -= this.gravity;
                        // 空気抵抗
                        this.velocities[i].x *= this.drag;
                        this.velocities[i].y *= this.drag;
                        this.velocities[i].z *= this.drag;

                        positions[i * 3] += this.velocities[i].x;
                        positions[i * 3 + 1] += this.velocities[i].y;
                        positions[i * 3 + 2] += this.velocities[i].z;
                    }

                    this.explosionGeometry.attributes.position.needsUpdate = true;
                    this.life -= this.decay;

                    // 透明度をフェードアウト
                    this.explosionMaterial.opacity = Math.max(0, this.life);

                    // 寿命が残り少ない時にチカチカさせる
                    if (Math.random() > 0.6 && this.life < 0.7) {
                        this.explosionMaterial.opacity *= (0.5 + Math.random() * 0.5);
                    }

                    return this.life > 0;
                }
            }

            explode(x, y, z) {
                this.explosionGeometry = new THREE.BufferGeometry();

                const hue = Math.random();
                const isMultiColor = Math.random() > 0.5;

                this.explosionMaterial = new THREE.PointsMaterial({
                    size: 2.0,
                    transparent: true,
                    opacity: 1,
                    blending: THREE.AdditiveBlending,
                    depthWrite: false,
                    vertexColors: true,
                    map: particleTexture
                });

                // 花火のバリエーション
                const type = Math.random();
                let particleCount = 400 + Math.random() * 400;
                let explosionSpeed = Math.random() * 1.5 + 0.8;

                if (type > 0.8) { // 巨大な大玉
                    particleCount = 1000;
                    explosionSpeed = 2.5;
                } else if (type < 0.2) { // 小玉
                    particleCount = 200;
                    explosionSpeed = 0.5;
                }

                const positions = new Float32Array(particleCount * 3);
                const colors = new Float32Array(particleCount * 3);
                this.velocities = [];

                for (let i = 0; i < particleCount; i++) {
                    positions[i * 3] = x;
                    positions[i * 3 + 1] = y;
                    positions[i * 3 + 2] = z;

                    // 球状のベクトル計算
                    const theta = Math.random() * 2 * Math.PI;
                    const phi = Math.acos(Math.random() * 2 - 1);

                    let speed = explosionSpeed;

                    // 牡丹や菊のような広がり方のばらつき
                    if (Math.random() > 0.8) {
                        speed *= (Math.random() * 0.3 + 0.2); // 内側のパーティクル
                    } else {
                        speed *= (Math.random() * 0.2 + 0.8); // 外側の綺麗な円
                    }

                    this.velocities.push({
                        x: speed * Math.sin(phi) * Math.cos(theta),
                        y: speed * Math.sin(phi) * Math.sin(theta),
                        z: speed * Math.cos(phi)
                    });

                    // 色
                    const pColor = new THREE.Color();
                    if (isMultiColor) {
                        pColor.setHSL(Math.random(), 1, 0.6);
                    } else {
                        pColor.setHSL(hue + (Math.random() * 0.1 - 0.05), 1, 0.6 + Math.random() * 0.2);
                    }

                    // たまに白っぽく光らせる
                    if(Math.random() > 0.95) pColor.set(0xffffff);

                    colors[i*3] = pColor.r;
                    colors[i*3+1] = pColor.g;
                    colors[i*3+2] = pColor.b;
                }

                this.explosionGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
                this.explosionGeometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));

                this.explosion = new THREE.Points(this.explosionGeometry, this.explosionMaterial);
                scene.add(this.explosion);

                this.life = 1.0;
                this.decay = Math.random() * 0.008 + 0.012; // 寿命(フェード速度)
                this.gravity = 0.025; // 重力
                this.drag = 0.95; // 空気抵抗
            }

            dispose() {
                if(this.phase === 'launch' && this.rocket) {
                    scene.remove(this.rocket);
                    this.rocket.geometry.dispose();
                    this.rocketMaterial.dispose();
                } else if(this.explosion) {
                     scene.remove(this.explosion);
                     this.explosionGeometry.dispose();
                     this.explosionMaterial.dispose();
                }
            }
        }

        // 7. リサイズイベント
        window.addEventListener('resize', () => {
            camera.aspect = window.innerWidth / window.innerHeight;
            camera.updateProjectionMatrix();
            renderer.setSize(window.innerWidth, window.innerHeight);
        });

        // 8. アニメーションループ
        let lastTime = 0;
        let launchDelay = 0;

        function animate(time) {
            requestAnimationFrame(animate);

            controls.update();

            // 定期的に花火を打ち上げる
            if (time - lastTime > launchDelay) {
                // たまに複数同時に打ち上げ
                const count = Math.random() > 0.7 ? Math.floor(Math.random() * 3) + 1 : 1;
                for(let i=0; i<count; i++){
                    fireworks.push(new Firework());
                }
                lastTime = time;
                launchDelay = Math.random() * 1500 + 500; // 0.5〜2.0秒間隔
            }

            // パーティクル更新と寿命切れの削除
            for (let i = fireworks.length - 1; i >= 0; i--) {
                const fw = fireworks[i];
                if (!fw.update()) {
                    fw.dispose();
                    fireworks.splice(i, 1);
                }
            }

            // 星空をゆっくり回転
            starField.rotation.y -= 0.0003;

            renderer.render(scene, camera);
        }

        // 開始
        animate(0);
    </script>
</body>
</html>

生成にかかった時間:4m11s

OrbitControlsを使っているので、動かせたり,ズームできたりします。
全体的に表現がのっぺりしているのが残念ですが、コードも素直だと思います。

AGY CLIの結果

生成結果
AGY CLIの生成画面

コード
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>3D Beautiful Fireworks - Three.js</title>
    <style>
        body {
            margin: 0;
            overflow: hidden;
            background-color: #050510;
            font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
            user-select: none;
        }
        #ui {
            position: absolute;
            top: 30px;
            left: 30px;
            color: rgba(255, 255, 255, 0.9);
            pointer-events: none;
            z-index: 10;
            text-shadow: 0 0 15px rgba(255, 255, 255, 0.5);
            background: rgba(10, 10, 20, 0.4);
            padding: 20px 30px;
            border-radius: 12px;
            backdrop-filter: blur(10px);
            -webkit-backdrop-filter: blur(10px);
            border: 1px solid rgba(255, 255, 255, 0.1);
            box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
            transition: all 0.3s ease;
        }
        h1 {
            margin: 0;
            font-size: 28px;
            font-weight: 300;
            letter-spacing: 4px;
            background: linear-gradient(90deg, #fff, #aaffff);
            -webkit-background-clip: text;
            -webkit-text-fill-color: transparent;
        }
        p {
            margin: 8px 0 0;
            font-size: 14px;
            font-weight: 300;
            letter-spacing: 1px;
            color: rgba(255, 255, 255, 0.7);
        }
        #instructions {
            position: absolute;
            bottom: 30px;
            width: 100%;
            text-align: center;
            color: rgba(255, 255, 255, 0.5);
            font-size: 13px;
            letter-spacing: 2px;
            pointer-events: none;
            z-index: 10;
            animation: pulse 2s infinite;
        }
        @keyframes pulse {
            0% { opacity: 0.3; }
            50% { opacity: 0.8; }
            100% { opacity: 0.3; }
        }
        #canvas-container {
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            z-index: 1;
        }
    </style>
</head>
<body>
    <div id="ui">
        <h1>NIGHT SKY</h1>
        <p>3D Fireworks Experience</p>
    </div>
    <div id="instructions">CLICK ANYWHERE TO LAUNCH FIREWORKS</div>
    <div id="canvas-container"></div>

    <!-- Three.js and Post-Processing Dependencies -->
    <script src="https://cdn.jsdelivr.net/npm/three@0.128.0/build/three.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/postprocessing/EffectComposer.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/postprocessing/RenderPass.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/postprocessing/ShaderPass.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/shaders/CopyShader.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/shaders/LuminosityHighPassShader.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/postprocessing/UnrealBloomPass.js"></script>

    <script>
        // --- Initialization ---
        const container = document.getElementById('canvas-container');

        const scene = new THREE.Scene();
        scene.fog = new THREE.FogExp2(0x050510, 0.003);

        const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000);
        camera.position.set(0, 20, 160);

        const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: false });
        renderer.setSize(window.innerWidth, window.innerHeight);
        renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); // Limit pixel ratio for performance
        renderer.setClearColor(0x020208, 1);
        container.appendChild(renderer.domElement);

        // --- Resize Handler ---
        window.addEventListener('resize', () => {
            camera.aspect = window.innerWidth / window.innerHeight;
            camera.updateProjectionMatrix();
            renderer.setSize(window.innerWidth, window.innerHeight);
            composer.setSize(window.innerWidth, window.innerHeight);
        });

        // --- Post-Processing (Bloom) ---
        const renderScene = new THREE.RenderPass(scene, camera);
        const bloomPass = new THREE.UnrealBloomPass(
            new THREE.Vector2(window.innerWidth, window.innerHeight),
            2.0,  // strength
            0.5,  // radius
            0.1   // threshold
        );

        const composer = new THREE.EffectComposer(renderer);
        composer.addPass(renderScene);
        composer.addPass(bloomPass);

        // --- Environment ---
        // 1. Stars
        const createStars = () => {
            const starsGeometry = new THREE.BufferGeometry();
            const starsCount = 2500;
            const starsPositions = new Float32Array(starsCount * 3);
            const starsColors = new Float32Array(starsCount * 3);

            for(let i = 0; i < starsCount; i++) {
                const r = 300 + Math.random() * 300;
                const theta = 2 * Math.PI * Math.random();
                const phi = Math.acos(2 * Math.random() - 1);

                starsPositions[i * 3] = r * Math.sin(phi) * Math.cos(theta);
                starsPositions[i * 3 + 1] = Math.abs(r * Math.sin(phi) * Math.sin(theta)); // Keep stars in upper hemisphere mostly
                starsPositions[i * 3 + 2] = r * Math.cos(phi);

                const starType = Math.random();
                let color = new THREE.Color(0xffffff);
                if (starType > 0.8) color.setHex(0xbbddff);
                else if (starType > 0.6) color.setHex(0xffeedd);

                // Add random dimming to some stars
                const brightness = 0.5 + Math.random() * 0.5;
                starsColors[i * 3] = color.r * brightness;
                starsColors[i * 3 + 1] = color.g * brightness;
                starsColors[i * 3 + 2] = color.b * brightness;
            }

            starsGeometry.setAttribute('position', new THREE.BufferAttribute(starsPositions, 3));
            starsGeometry.setAttribute('color', new THREE.BufferAttribute(starsColors, 3));

            // Create circular texture for stars
            const canvas = document.createElement('canvas');
            canvas.width = 16;
            canvas.height = 16;
            const ctx = canvas.getContext('2d');
            ctx.beginPath();
            ctx.arc(8, 8, 6, 0, Math.PI * 2);
            ctx.fillStyle = '#ffffff';
            ctx.fill();
            const texture = new THREE.CanvasTexture(canvas);

            const starsMaterial = new THREE.PointsMaterial({
                size: 1.5,
                map: texture,
                vertexColors: true,
                transparent: true,
                opacity: 0.9,
                depthWrite: false,
                blending: THREE.AdditiveBlending
            });

            const stars = new THREE.Points(starsGeometry, starsMaterial);
            scene.add(stars);
        };
        createStars();

        // 2. City Skyline Silhouette
        const createCityline = () => {
            const canvas = document.createElement('canvas');
            canvas.width = 1024;
            canvas.height = 256;
            const ctx = canvas.getContext('2d');

            // Base dark color
            ctx.fillStyle = '#010103';
            ctx.fillRect(0, 0, 1024, 256);

            let x = 0;
            while(x < 1024) {
                const w = 15 + Math.random() * 40;
                const h = 30 + Math.random() * 120;

                // Building base
                ctx.fillStyle = '#020205';
                ctx.fillRect(x, 256 - h, w, h);

                // Edge highlight
                ctx.fillStyle = '#0a0a15';
                ctx.fillRect(x + w - 2, 256 - h, 2, h);

                // Windows
                if (Math.random() > 0.3) {
                    ctx.fillStyle = Math.random() > 0.5 ? '#1a1a2a' : '#2a2a1a';
                    for(let wy = 256 - h + 10; wy < 256; wy += 12) {
                        for(let wx = x + 4; wx < x + w - 4; wx += 8) {
                            if (Math.random() > 0.6) {
                                ctx.fillRect(wx, wy, 3, 5);
                            }
                        }
                    }
                }

                x += w + (Math.random() * 5);
            }

            const texture = new THREE.CanvasTexture(canvas);
            texture.wrapS = THREE.RepeatWrapping;
            texture.repeat.set(4, 1); // Repeat texture around the cylinder

            const material = new THREE.MeshBasicMaterial({
                map: texture,
                transparent: true,
                depthWrite: false,
                color: 0x555566
            });

            const geometry = new THREE.CylinderGeometry(250, 250, 80, 64, 1, true);
            const cityMesh = new THREE.Mesh(geometry, material);
            cityMesh.position.y = -20;
            scene.add(cityMesh);
        };
        createCityline();

        // --- Firework Particle Texture ---
        const createParticleTexture = () => {
            const canvas = document.createElement('canvas');
            canvas.width = 64;
            canvas.height = 64;
            const ctx = canvas.getContext('2d');

            const gradient = ctx.createRadialGradient(32, 32, 0, 32, 32, 32);
            gradient.addColorStop(0, 'rgba(255, 255, 255, 1)');
            gradient.addColorStop(0.1, 'rgba(255, 255, 255, 0.9)');
            gradient.addColorStop(0.4, 'rgba(255, 255, 255, 0.2)');
            gradient.addColorStop(1, 'rgba(0, 0, 0, 0)');

            ctx.fillStyle = gradient;
            ctx.fillRect(0, 0, 64, 64);
            return new THREE.CanvasTexture(canvas);
        };
        const particleTexture = createParticleTexture();

        // --- Fireworks Logic ---
        const fireworks = [];
        const gravity = new THREE.Vector3(0, -0.015, 0);

        // Rich color palettes for explosions
        const palettes = [
            [0xff2244, 0xff88aa], // Crimson / Pink
            [0x22ff44, 0xaaffaa], // Neon Green
            [0x2266ff, 0x88bbff], // Deep Blue
            [0xffcc22, 0xffffaa], // Gold
            [0xcc22ff, 0xee88ff], // Purple
            [0x22ffff, 0xccffff], // Cyan
            [0xff6622, 0xffcc88], // Orange
            [0xffffff, 0xdddddd]  // Pure White
        ];

        class Firework {
            constructor(manualX) {
                this.scene = scene;
                this.done = false;
                this.exploded = false;

                // Start position
                const startX = manualX !== undefined ? manualX : (Math.random() - 0.5) * 200;
                const startZ = (Math.random() - 0.5) * 100 - 50; // Keep fireworks slightly behind
                const startY = -40;

                this.position = new THREE.Vector3(startX, startY, startZ);
                this.targetY = 10 + Math.random() * 60; // Random explosion height

                // Launch velocity
                this.velocity = new THREE.Vector3(
                    (Math.random() - 0.5) * 0.4,
                    1.2 + Math.random() * 0.8,
                    (Math.random() - 0.5) * 0.2
                );

                const palette = palettes[Math.floor(Math.random() * palettes.length)];
                this.color = new THREE.Color(palette[0]);
                this.coreColor = new THREE.Color(palette[1]);

                // Rocket Mesh
                this.rocketGeometry = new THREE.BufferGeometry();
                this.rocketGeometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array([startX, startY, startZ]), 3));

                this.rocketMaterial = new THREE.PointsMaterial({
                    size: 4,
                    color: 0xffddaa, // Rocket trail color
                    map: particleTexture,
                    transparent: true,
                    opacity: 1,
                    blending: THREE.AdditiveBlending,
                    depthWrite: false
                });

                this.rocketMesh = new THREE.Points(this.rocketGeometry, this.rocketMaterial);
                scene.add(this.rocketMesh);

                // Particle properties for explosion
                this.particleCount = 200 + Math.floor(Math.random() * 300);
                this.particlesMesh = null;
                this.particleVelocities = [];
                this.particleLifespans = [];
            }

            explode() {
                scene.remove(this.rocketMesh);
                this.rocketGeometry.dispose();
                this.rocketMaterial.dispose();
                this.exploded = true;

                const geometry = new THREE.BufferGeometry();
                const positions = new Float32Array(this.particleCount * 3);
                const colorsArr = new Float32Array(this.particleCount * 3);

                const explosionType = Math.random();
                const hasTrail = Math.random() > 0.5;

                for (let i = 0; i < this.particleCount; i++) {
                    positions[i * 3] = this.position.x;
                    positions[i * 3 + 1] = this.position.y;
                    positions[i * 3 + 2] = this.position.z;

                    let vx, vy, vz;
                    const speedMultiplier = 0.8 + Math.random() * 1.5;

                    if (explosionType < 0.15) {
                        // Ring
                        const angle = Math.random() * Math.PI * 2;
                        vx = Math.cos(angle) * speedMultiplier;
                        vy = (Math.random() - 0.5) * 0.2;
                        vz = Math.sin(angle) * speedMultiplier;
                    } else if (explosionType < 0.3) {
                        // Double Ring
                        const angle = Math.random() * Math.PI * 2;
                        const isInner = Math.random() > 0.5;
                        const rSpeed = isInner ? speedMultiplier * 0.5 : speedMultiplier;
                        vx = Math.cos(angle) * rSpeed;
                        vy = (Math.random() - 0.5) * 0.2;
                        vz = Math.sin(angle) * rSpeed;
                    } else if (explosionType < 0.45) {
                        // Willow (heavy gravity)
                        const theta = Math.random() * Math.PI * 2;
                        const phi = Math.acos((Math.random() * 2) - 1);
                        vx = Math.sin(phi) * Math.cos(theta) * speedMultiplier * 0.5;
                        vy = Math.sin(phi) * Math.sin(theta) * speedMultiplier * 0.5 + 1.0; // Upward bias
                        vz = Math.cos(phi) * speedMultiplier * 0.5;
                    } else {
                        // Classic Peony / Sphere
                        const theta = Math.random() * Math.PI * 2;
                        const phi = Math.acos((Math.random() * 2) - 1);
                        vx = Math.sin(phi) * Math.cos(theta) * speedMultiplier;
                        vy = Math.sin(phi) * Math.sin(theta) * speedMultiplier;
                        vz = Math.cos(phi) * speedMultiplier;
                    }

                    this.particleVelocities.push(new THREE.Vector3(vx, vy, vz));

                    // Lifespan variation (some particles die faster)
                    this.particleLifespans.push(1.0 + Math.random() * 0.8);

                    // Mix core color and main color
                    const useCore = Math.random() > 0.7;
                    const c = useCore ? this.coreColor : this.color;
                    colorsArr[i * 3] = c.r;
                    colorsArr[i * 3 + 1] = c.g;
                    colorsArr[i * 3 + 2] = c.b;
                }

                geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
                geometry.setAttribute('color', new THREE.BufferAttribute(colorsArr, 3));

                const material = new THREE.PointsMaterial({
                    size: 5, // Slightly larger particles for better bloom
                    map: particleTexture,
                    vertexColors: true,
                    transparent: true,
                    opacity: 1,
                    depthWrite: false,
                    blending: THREE.AdditiveBlending
                });

                this.particlesMesh = new THREE.Points(geometry, material);
                scene.add(this.particlesMesh);

                // Add a subtle point light flash at the explosion point
                this.flashLight = new THREE.PointLight(this.color, 8, 150);
                this.flashLight.position.copy(this.position);
                scene.add(this.flashLight);
                this.flashLifespan = 1.0;

                // Save explosion type for update logic
                this.isWillow = explosionType >= 0.3 && explosionType < 0.45;
            }

            update() {
                if (!this.exploded) {
                    this.position.add(this.velocity);
                    this.velocity.add(gravity);

                    const positions = this.rocketMesh.geometry.attributes.position.array;
                    positions[0] = this.position.x;
                    positions[1] = this.position.y;
                    positions[2] = this.position.z;
                    this.rocketMesh.geometry.attributes.position.needsUpdate = true;

                    if (this.velocity.y <= 0 || this.position.y >= this.targetY) {
                        this.explode();
                    }
                } else {
                    const positions = this.particlesMesh.geometry.attributes.position.array;
                    const colorsArr = this.particlesMesh.geometry.attributes.color.array;

                    let allDead = true;

                    for (let i = 0; i < this.particleCount; i++) {
                        if (this.particleLifespans[i] > 0) {
                            allDead = false;

                            // Decay lifespan
                            this.particleLifespans[i] -= 0.012;

                            const vel = this.particleVelocities[i];

                            // Apply specific physics based on explosion type
                            if (this.isWillow) {
                                vel.add(gravity.clone().multiplyScalar(1.5)); // Heavier gravity
                                vel.multiplyScalar(0.92); // High air drag
                            } else {
                                vel.add(gravity.clone().multiplyScalar(0.8)); // Normal gravity
                                vel.multiplyScalar(0.95); // Normal drag
                            }

                            positions[i * 3] += vel.x;
                            positions[i * 3 + 1] += vel.y;
                            positions[i * 3 + 2] += vel.z;

                            const life = Math.max(0, this.particleLifespans[i]);
                            // Twinkle effect: random intensity drop
                            const twinkle = Math.random() > 0.05 ? 1 : 0.2;

                            // Color fades to black as life decreases (invisible in additive blending)
                            colorsArr[i * 3] = this.color.r * life * twinkle;
                            colorsArr[i * 3 + 1] = this.color.g * life * twinkle;
                            colorsArr[i * 3 + 2] = this.color.b * life * twinkle;
                        } else {
                            colorsArr[i * 3] = 0;
                            colorsArr[i * 3 + 1] = 0;
                            colorsArr[i * 3 + 2] = 0;
                        }
                    }

                    this.particlesMesh.geometry.attributes.position.needsUpdate = true;
                    this.particlesMesh.geometry.attributes.color.needsUpdate = true;

                    // Update flash light
                    if (this.flashLight) {
                        this.flashLifespan -= 0.03;
                        this.flashLight.intensity = Math.max(0, 8 * this.flashLifespan);
                        if (this.flashLifespan <= 0) {
                            scene.remove(this.flashLight);
                            this.flashLight = null;
                        }
                    }

                    if (allDead) {
                        this.done = true;
                        scene.remove(this.particlesMesh);
                        this.particlesMesh.geometry.dispose();
                        this.particlesMesh.material.dispose();
                    }
                }
            }
        }

        // --- Interaction ---
        let mouseX = 0;
        let mouseY = 0;

        window.addEventListener('mousemove', (e) => {
            mouseX = (e.clientX / window.innerWidth) * 2 - 1;
            mouseY = -(e.clientY / window.innerHeight) * 2 + 1;

            // Subtle UI tilt effect
            const ui = document.getElementById('ui');
            ui.style.transform = `perspective(1000px) rotateY(${mouseX * 10}deg) rotateX(${-mouseY * 10}deg)`;
        });

        window.addEventListener('pointerdown', (e) => {
            // Launch firework at mouse X position roughly
            const nx = (e.clientX / window.innerWidth) * 2 - 1;
            const targetX = nx * 100;

            const fw = new Firework(targetX);
            // Tilt velocity slightly towards cursor
            fw.velocity.x += nx * 0.5;
            fireworks.push(fw);
        });

        // --- Animation Loop ---
        let frameCount = 0;
        let autoLaunchTimer = 0;

        function animate() {
            requestAnimationFrame(animate);

            // Auto launch
            autoLaunchTimer++;
            if (autoLaunchTimer > 80) { // roughly every 1.3 seconds
                if (Math.random() > 0.4) {
                    fireworks.push(new Firework());
                    // Chance for multiple burst
                    if (Math.random() > 0.7) {
                        setTimeout(() => fireworks.push(new Firework()), 300);
                        setTimeout(() => fireworks.push(new Firework()), 600);
                    }
                }
                autoLaunchTimer = 0;
            }

            for (let i = fireworks.length - 1; i >= 0; i--) {
                fireworks[i].update();
                if (fireworks[i].done) {
                    fireworks.splice(i, 1);
                }
            }

            // Cinematic camera movement
            frameCount++;
            const targetCamX = Math.sin(frameCount * 0.001) * 60 + mouseX * 30;
            const targetCamY = 20 + mouseY * 20;
            const targetCamZ = 160 + Math.cos(frameCount * 0.001) * 30;

            camera.position.x += (targetCamX - camera.position.x) * 0.02;
            camera.position.y += (targetCamY - camera.position.y) * 0.02;
            camera.position.z += (targetCamZ - camera.position.z) * 0.02;

            camera.lookAt(0, 30, 0);

            composer.render();
        }

        // Initial grand finale burst
        setTimeout(() => {
            for(let i=0; i<3; i++) {
                setTimeout(() => fireworks.push(new Firework()), i * 200);
            }
        }, 500);

        animate();
    </script>
</body>
</html>

生成にかかった時間:2m20s
PointsMaterialに加えてUnrealBloomPassを使っているので、花火の表現がより繊細になっています。
また、setPixelRatioの設定で高DPI環境でも扱いやすいようにされています。
やや使っているモジュールが古いのと、コピー量が多いのがネックですが、描画されたものを綺麗です。

まとめ

名前が変わっただけでなく実装がTSからGoに変わったり、コマンドが変わったりしている事がわかりました。
個人的には、Gemini CLIはオープンソースでしたが、AGY CLIのコードが現状非公開なことが残念です。
まだ使い始めたばかりでサブエージェントを作ったり、Skillsでカスタマイズしたりできていないので、これからも試していきたいと思います。
皆さんも是非Gemini CLIからの移行を機に、AGY CLIを使ってみてください。

Discussion