[Astro] #153 アナログ時計コンポーネントの高度カスタマイズ化 — JSONアセット管理、IndexedDBスキンギャラリー、SE/時報対応
はじめに
PROTOCOL.LAIN のサイバーパンクな世界観を彩る要素として、デスクトップ上に自由に移動・配置できる「浮遊型アナログ時計コンポーネント(AnalogClock.astro)」を運用している。
これまでは背景画像や効果音(SE)のパスがコード内に直接ハードコードされており、スキンの追加やデザイン調整の自由度が限られていた。今回、アセット情報を外部 JSON 化して保守性を高めるとともに、IndexedDB を活用したカスタム背景スキンギャラリー、文字盤(ローマ数字/アラビア数字)切り替え、および正時(0分0秒)の時報機能を新たに実装した。
スクリーンショット
機能を大幅位に拡張
右クリックメニューを拡張
時計のサイズ、位置、フォント、色などを変更可能
設定状態を「保存」&「リロード」
「インポート」「エクスポート」「リセット」
背景スキンの追加と、アップロード、データ管理可能に。
indexedDBに保存される為、サーバへのアップロードは無し、ブラウザ完結。
1. 課題と設計目標
従来の時計コンポーネントには以下の課題が存在していた:
- アセットパスのハードコード: 背景画像(
clock-bg-chibi.webp等)や SE パスが直接記述されており、画像や音源の変更時にソースコードの改修が必要だった。 - デザインの拡張性: 文字盤の数字(アラビア数字 / ローマ数字)、カラー、フォント、サイズなどをリアルタイムに調整する UI が未整備だった。
- ユーザー独自の画像保持: ユーザーが手元からアップロードした画像を背景スキンとして保存・削除・選択できる永続化ストレージの仕組みが必要だった。
これらを解決するため、外部 JSON によるアセット定義 + IndexedDB (Dexie.js) によるユーザーデータ保存 + 設定モーダル UI を組み合わせたモジュール構造へ刷新した。
2. アセット情報の一元管理 (list.json)
背景画像と音源(SE/時報)のパス情報を /public/assets/clockSkins/list.json に外部化した。
{
"defaultSkinId": "chibi",
"skins": [
{
"id": "chibi",
"name": "Chibi Main",
"url": "/assets/clockSkins/clock-bg-chibi.webp"
},
{
"id": "floral_gold",
"name": "Floral Gold",
"url": "/assets/clockSkins/clock-bg-floral.webp"
}
],
"sounds": {
"click": "/assets/se/決定ボタンを押す7.mp3",
"tick": "/assets/se/決定ボタンを押す38.mp3",
"chime": "/assets/se/ジングル10.mp3"
}
}
JSON の動的非同期ロードとフォールバック処理
コンポーネント初期化時に fetch() で list.json を読み込み、万が一読み込みに失敗した場合でもフォールバック音源が適用される安全設計を設けた。
interface ClockAssets {
defaultSkinId: string;
skins: { id: string; name: string; url: string }[];
sounds: { click: string; tick: string; chime: string };
}
let assetsConfig: ClockAssets | null = null;
let clickAudio: HTMLAudioElement;
let tickAudio: HTMLAudioElement;
let chimeAudio: HTMLAudioElement;
async function loadAssetsJson() {
try {
const res = await fetch('/assets/clockSkins/list.json');
assetsConfig = await res.json();
if (assetsConfig) {
clickAudio = new Audio(assetsConfig.sounds.click);
tickAudio = new Audio(assetsConfig.sounds.tick);
chimeAudio = new Audio(assetsConfig.sounds.chime);
}
} catch (e) {
console.warn('[Clock] Failed to load list.json, fallback to defaults:', e);
clickAudio = new Audio('/assets/se/決定ボタンを押す7.mp3');
tickAudio = new Audio('/assets/se/決定ボタンを押す38.mp3');
chimeAudio = new Audio('/assets/se/ジングル10.mp3');
}
}
3. スキンギャラリーと IndexedDB の統合
背景スキンは以下の優先順位で読み込まれる:
- IndexedDB (ユーザーアップロード画像): ローカルに保存されたカスタム画像 Blob
- JSON プリセット:
list.jsonで用意されている標準アセット - デフォルト画像: フォールバック画像
async function loadActiveSkin() {
const clockEl = document.getElementById('analog-clock');
if (!clockEl) return;
const defaultUrl = assetsConfig?.skins[0]?.url || '/assets/clockSkins/clock-bg-chibi.webp';
if (!config.activeSkinId || !db) {
clockEl.style.backgroundImage = `url("${defaultUrl}")`;
return;
}
// 1. IndexedDB から探す
try {
const skin = await db.clockSkins.get(config.activeSkinId);
if (skin && skin.data) {
const blobUrl = URL.createObjectURL(skin.data);
clockEl.style.backgroundImage = `url("${blobUrl}")`;
return;
}
} catch {}
// 2. JSON プリセットから探す
const preset = assetsConfig?.skins.find(s => s.id === config.activeSkinId);
if (preset) {
clockEl.style.backgroundImage = `url("${preset.url}")`;
return;
}
// 3. デフォルト画像
clockEl.style.backgroundImage = `url("${defaultUrl}")`;
}
4. 時報(Chime)と秒針効果音(SE)の発火ロジック
requestAnimationFrame ループの中で、秒の更新タイミングおよび正時(分 === 0 かつ 秒 === 0)のチェックを行い、時報音を鳴らす仕組みを構築した。
let lastTickSecond = -1;
let lastChimeMinute = -1;
function updateClock() {
const now = new Date();
const rawSec = now.getSeconds();
const m = now.getMinutes();
// 秒針 SE (ステップ移動時)
if (!config.isSmooth && rawSec !== lastTickSecond) {
playSE(tickAudio);
lastTickSecond = rawSec;
}
// 0分0秒の正時で時報チャイムを再生
if (m === 0 && rawSec === 0 && lastChimeMinute !== m) {
playSE(chimeAudio);
lastChimeMinute = m;
} else if (m !== 0) {
lastChimeMinute = -1;
}
animationFrameId = requestAnimationFrame(updateClock);
}
5. UI/UX の改善とモーダルのドラッグ移動
設定モーダル(⚙ SYSTEM CONFIGURATION)を表示している最中でも後ろのアナログ時計を確認・操作できるよう、暗幕(Backdrop)の透明化とモーダル自体のドラッグ移動機能を実装した。
/* 設定モーダル(背景を透明にし、後ろの視認性を確保) */
.config-modal {
position: fixed;
inset: 0;
background: transparent;
backdrop-filter: none;
z-index: 20000;
pointer-events: none; /* 背後のクリックを透過 */
}
.modal-content {
position: absolute;
pointer-events: auto; /* ウィンドウ内のみ操作有効化 */
}
6. まとめ
| 機能 | 概要・利点 |
|---|---|
| JSON アセット管理 | 背景スキンや効果音パスを list.json に集約し、コード改修なしでアセット追加可能 |
| IndexedDB ギャラリー | ユーザー独自の背景画像をローカルに永続化・動的カード生成 |
| 文字盤 & SVG カスタマイズ | ローマ数字 (Ⅰ〜Ⅻ) / アラビア数字、カラー、フォント、半径のリアルタイム調整 |
| 正時時報 (Chime) | 毎時 0分0秒に自動で時報 SE を再生 |
| ドラッグ可能モーダル | 透過背景とドラッグ移動により、デザイン調整中のリアルタイムプレビューを実現 |
背景スキン(花柄、ゴールド枠、木目調など)や文字盤フォントを変更するだけで、デスクトップ全体の雰囲気が一変するフレキシブルな時計コンポーネントへと進化させることができた。