suzuna / splashscreen

Suzuna — Splashscreen 実装指示書

概要

アプリ起動時にスプラッシュスクリーンを表示し、 Pythonバックエンドの起動完了後にメインUIへ移行する。

ユーザー体験の目標: 起動ボタンを押した瞬間に鈴菜ちゃんが出迎える。 Pythonの起動待ち(10〜20秒)をストレスなく過ごせる。


実装方針

Tauriのsplashscreen機能を使う。 メインウィンドウは非表示のままスプラッシュを先に出し、 Python起動完了後にスプラッシュをフェードアウト→メインウィンドウを表示する。


起動フロー

Tauri起動

splashscreen window を即表示(public/images/Startup_bg.png)

メインwindowは非表示のまま

Python sidecar 起動開始

GET /health をポーリング(1秒間隔)

/health が {"status": "ok"} を返す

「起動完了!」表示 → 1秒待機

splashscreen フェードアウト(300ms)

メインwindow を表示・splashscreen を閉じる

ステータステキストの演出

Pythonの起動中に以下のテキストを順番に表示する(各フェーズで切り替え):

Phase 1(起動直後):    「Suzuna を起動しています...」
Phase 2(3秒後):       「音声エンジンを準備中...」
Phase 3(7秒後):       「もうすぐ準備できます...」
Phase 4(完了時):      「起動完了!」

ファイル構成

src-tauri/
  tauri.conf.json          # splashscreen window設定を追加
  src/
    main.rs                # splashscreen制御ロジックを追加

src/
  splashscreen/
    index.html             # スプラッシュスクリーンUI
    splashscreen.ts        # Tauriコマンド呼び出し

public/
  images/
    Startup_bg.png         # 背景画像(配置済み)

実装詳細

1. tauri.conf.json の変更

{
  "app": {
    "windows": [
      {
        "label": "splashscreen",
        "url": "splashscreen/index.html",
        "width": 1280,
        "height": 720,
        "resizable": false,
        "decorations": false,
        "alwaysOnTop": true,
        "center": true
      },
      {
        "label": "main",
        "url": "index.html",
        "width": 1200,
        "height": 800,
        "visible": false,
        "center": true
      }
    ]
  }
}

2. src-tauri/src/main.rs の変更

Pythonサーバー起動完了後にスプラッシュを閉じてメインを表示するコマンドを追加。

#[tauri::command]
async fn close_splashscreen(window: tauri::Window) {
    // splashscreen を閉じる
    if let Some(splashscreen) = window.get_webview_window("splashscreen") {
        splashscreen.close().unwrap();
    }
    // main windowを表示
    window.get_webview_window("main").unwrap().show().unwrap();
}

3. src/splashscreen/index.html

スプラッシュスクリーンのUI。 背景画像 + ステータステキスト + プログレスバー。

デザイン仕様:

  • 背景: public/images/Startup_bg.png をフルスクリーン表示(object-fit: cover)
  • 画面下部中央にステータスエリアを配置
  • ステータスエリアの背景: 半透明ブラック(rgba(0,0,0,0.45))、blur効果
  • テキスト: 白(#F7F4EF)、Noto Sans JP、16px
  • プログレスバー: くすみグリーン(#6B9B6F)、高さ4px
  • フェードアウト: opacity を300msでトランジション
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <style>
    * { margin: 0; padding: 0; box-sizing: border-box; }

    body {
      width: 100vw;
      height: 100vh;
      overflow: hidden;
      font-family: 'Noto Sans JP', sans-serif;
      transition: opacity 0.3s ease;
    }

    #bg {
      position: absolute;
      inset: 0;
      background: url('/images/Startup_bg.png') center/cover no-repeat;
    }

    #status-area {
      position: absolute;
      bottom: 48px;
      left: 50%;
      transform: translateX(-50%);
      width: 360px;
      background: rgba(0, 0, 0, 0.45);
      backdrop-filter: blur(8px);
      border-radius: 12px;
      padding: 16px 24px;
      text-align: center;
    }

    #app-name {
      color: #F7F4EF;
      font-size: 20px;
      font-weight: 600;
      letter-spacing: 0.05em;
      margin-bottom: 8px;
    }

    #status-text {
      color: rgba(255,255,255,0.8);
      font-size: 14px;
      margin-bottom: 12px;
      min-height: 20px;
      transition: opacity 0.3s ease;
    }

    #progress-track {
      background: rgba(255,255,255,0.2);
      border-radius: 9999px;
      height: 4px;
      overflow: hidden;
    }

    #progress-fill {
      background: #6B9B6F;
      height: 100%;
      width: 0%;
      border-radius: 9999px;
      transition: width 1s ease;
    }
  </style>
</head>
<body>
  <div id="bg"></div>
  <div id="status-area">
    <div id="app-name">Suzuna</div>
    <div id="status-text">Suzuna を起動しています...</div>
    <div id="progress-track">
      <div id="progress-fill"></div>
    </div>
  </div>
  <script src="splashscreen.js"></script>
</body>
</html>

4. src/splashscreen/splashscreen.ts

ステータステキストの切り替えとポーリング処理。

import { invoke } from "@tauri-apps/api/core";

const statusText = document.getElementById("status-text")!;
const progressFill = document.getElementById("progress-fill")!;

const phases = [
  { delay: 0,    text: "Suzuna を起動しています...", progress: 10 },
  { delay: 3000, text: "音声エンジンを準備中...",     progress: 40 },
  { delay: 7000, text: "もうすぐ準備できます...",     progress: 70 },
];

// ステータステキストを順番に切り替え
phases.forEach(({ delay, text, progress }) => {
  setTimeout(() => {
    statusText.style.opacity = "0";
    setTimeout(() => {
      statusText.textContent = text;
      progressFill.style.width = `${progress}%`;
      statusText.style.opacity = "1";
    }, 300);
  }, delay);
});

// /health ポーリング(1秒間隔)
async function pollHealth() {
  try {
    const res = await fetch("http://127.0.0.1:8000/health");
    if (res.ok) {
      // 完了演出
      statusText.style.opacity = "0";
      setTimeout(() => {
        statusText.textContent = "起動完了!";
        progressFill.style.width = "100%";
        statusText.style.opacity = "1";
      }, 300);

      // 1秒後にメインウィンドウへ移行
      setTimeout(async () => {
        document.body.style.opacity = "0";
        setTimeout(async () => {
          await invoke("close_splashscreen");
        }, 300);
      }, 1000);

      return;
    }
  } catch {
    // まだ起動中、継続ポーリング
  }
  setTimeout(pollHealth, 1000);
}

// 1秒後からポーリング開始(Pythonが起動し始める時間を考慮)
setTimeout(pollHealth, 1000);

完了条件

  • アプリ起動直後にスプラッシュスクリーンが表示される
  • public/images/Startup_bg.png が背景としてフルスクリーン表示される
  • ステータステキストが4フェーズで切り替わる
  • プログレスバーが進行する
  • /health 返答後に「起動完了!」表示 → フェードアウト → メインUI表示
  • メインウィンドウは起動完了まで非表示