開発アクティブ
Memento Mori - 名言機能 & シェア機能 実装ドキュメント
概要
既存のメメントモリアプリに以下の機能を追加する:
- 名言カスタマイズ機能
- シェア画像生成機能
- マルチプラットフォーム対応(Web / PC / スマホ)
技術スタック
| レイヤー | 技術 |
|---|---|
| フロントエンド | Vue 3 + Vite |
| 状態管理 | Pinia |
| スタイリング | Tailwind CSS |
| 画像生成 | HTML Canvas API |
| PC版 | Electron |
| スマホ版 | Capacitor |
| バックエンド | Firebase (Auth + Firestore) |
| ホスティング | Vercel or Firebase Hosting |
ディレクトリ構成(案)
memento-mori/
├── src/
│ ├── components/
│ │ ├── LifeGrid.vue # 人生グリッド表示
│ │ ├── QuoteDisplay.vue # 名言表示
│ │ ├── QuoteSettings.vue # 名言設定パネル
│ │ ├── ShareButton.vue # シェアボタン
│ │ └── SharePreview.vue # シェア画像プレビュー
│ ├── composables/
│ │ ├── useQuotes.ts # 名言データ管理
│ │ ├── useShareImage.ts # 画像生成ロジック
│ │ └── useUserSettings.ts # ユーザー設定
│ ├── data/
│ │ ├── quotes/
│ │ │ ├── stoicism.json # ストア派哲学
│ │ │ ├── zen.json # 禅
│ │ │ ├── entrepreneurs.json # 起業家
│ │ │ └── index.ts
│ │ └── categories.ts
│ ├── stores/
│ │ ├── quoteStore.ts
│ │ └── userStore.ts
│ ├── views/
│ │ ├── HomeView.vue
│ │ └── SettingsView.vue
│ └── utils/
│ ├── canvasRenderer.ts # Canvas描画
│ └── shareUtils.ts # シェア機能
├── electron/ # Electron設定
├── android/ # Capacitor (Android)
├── ios/ # Capacitor (iOS)
└── capacitor.config.ts
機能詳細
1. 名言カスタマイズ機能
1.1 プリセットカテゴリ
// src/data/categories.ts
export const QUOTE_CATEGORIES = [
{ id: 'stoicism', name: 'ストア派哲学', icon: '🏛️' },
{ id: 'zen', name: '禅', icon: '🧘' },
{ id: 'entrepreneurs', name: '起業家', icon: '🚀' },
{ id: 'literature', name: '文学', icon: '📚' },
{ id: 'custom', name: '自分で追加', icon: '✏️' },
] as const;
1.2 名言データ構造
// types/quote.ts
interface Quote {
id: string;
text: string;
author: string;
source?: string; // 出典(本のタイトルなど)
category: string;
language: 'ja' | 'en';
isFavorite?: boolean; // お気に入りフラグ
isCustom?: boolean; // ユーザー追加フラグ
}
1.3 名言表示コンポーネント
<!-- src/components/QuoteDisplay.vue -->
<template>
<div class="quote-container">
<transition name="fade" mode="out-in">
<div :key="currentQuote.id" class="quote-content">
<p class="quote-text">{{ currentQuote.text }}</p>
<p class="quote-author">── {{ currentQuote.author }}</p>
</div>
</transition>
<div class="quote-actions">
<button @click="toggleFavorite" :class="{ active: currentQuote.isFavorite }">
♡
</button>
<button @click="nextQuote">→</button>
<button @click="openShare">
<ShareIcon />
</button>
</div>
</div>
</template>
1.4 表示設定オプション
// types/settings.ts
interface QuoteSettings {
enabledCategories: string[]; // 有効なカテゴリ
displayMode: 'random' | 'sequential';
rotationInterval: number; // 秒(0 = 手動のみ)
showAuthor: boolean;
fontSize: 'small' | 'medium' | 'large';
}
2. シェア画像生成機能
2.1 Canvas描画ロジック
// src/utils/canvasRenderer.ts
interface ShareImageOptions {
quote: Quote;
lifeGrid: LifeGridData;
theme: 'dark' | 'light';
size: { width: number; height: number };
}
export async function generateShareImage(options: ShareImageOptions): Promise<Blob> {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d')!;
canvas.width = options.size.width; // 1200
canvas.height = options.size.height; // 630 (OGP推奨サイズ)
// 背景
ctx.fillStyle = options.theme === 'dark' ? '#1a1a1a' : '#ffffff';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// ミニグリッド描画(簡略版)
drawMiniLifeGrid(ctx, options.lifeGrid, {
x: 50,
y: 50,
width: 400,
height: 300
});
// 名言テキスト
ctx.font = '32px "Noto Sans JP"';
ctx.fillStyle = options.theme === 'dark' ? '#ffffff' : '#333333';
wrapText(ctx, options.quote.text, 500, 100, 650, 40);
// 著者名
ctx.font = '24px "Noto Sans JP"';
ctx.fillStyle = '#888888';
ctx.fillText(`── ${options.quote.author}`, 500, 350);
// ブランドロゴ
ctx.font = '18px monospace';
ctx.fillText('MEMENTO MORI', 50, canvas.height - 30);
return new Promise((resolve) => {
canvas.toBlob((blob) => resolve(blob!), 'image/png');
});
}
2.2 シェア機能(Web Share API + フォールバック)
// src/utils/shareUtils.ts
export async function shareToX(imageBlob: Blob, quote: Quote): Promise<void> {
const text = `「${quote.text}」── ${quote.author}\n\n#MementoMori`;
// Web Share API対応ブラウザ
if (navigator.share && navigator.canShare) {
const file = new File([imageBlob], 'memento-mori.png', { type: 'image/png' });
if (navigator.canShare({ files: [file] })) {
await navigator.share({
text,
files: [file],
});
return;
}
}
// フォールバック: 画像をダウンロード + X投稿画面を開く
downloadImage(imageBlob, 'memento-mori.png');
const xUrl = `https://twitter.com/intent/tweet?text=${encodeURIComponent(text)}`;
window.open(xUrl, '_blank');
}
export function downloadImage(blob: Blob, filename: string): void {
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
}
2.3 シェアプレビューモーダル
<!-- src/components/SharePreview.vue -->
<template>
<div v-if="isOpen" class="modal-overlay" @click.self="close">
<div class="modal-content">
<h3>シェア画像プレビュー</h3>
<div class="preview-container">
<img v-if="previewUrl" :src="previewUrl" alt="シェア画像" />
<div v-else class="loading">生成中...</div>
</div>
<div class="theme-toggle">
<button @click="theme = 'dark'" :class="{ active: theme === 'dark' }">
ダーク
</button>
<button @click="theme = 'light'" :class="{ active: theme === 'light' }">
ライト
</button>
</div>
<div class="share-buttons">
<button @click="shareX" class="btn-x">
𝕏 でシェア
</button>
<button @click="download" class="btn-download">
画像を保存
</button>
</div>
</div>
</div>
</template>
3. データ永続化
3.1 Firebase Firestore スキーマ
users/
{userId}/
settings/
quote: QuoteSettings
display: DisplaySettings
customQuotes/
{quoteId}: Quote
favorites/
{quoteId}: { addedAt: Timestamp }
3.2 オフライン対応(Capacitor)
// src/composables/useOfflineSync.ts
import { Preferences } from '@capacitor/preferences';
export function useOfflineSync() {
// ローカルにキャッシュ
async function cacheQuotes(quotes: Quote[]) {
await Preferences.set({
key: 'cached_quotes',
value: JSON.stringify(quotes),
});
}
// オフライン時はキャッシュから読み込み
async function loadCachedQuotes(): Promise<Quote[]> {
const { value } = await Preferences.get({ key: 'cached_quotes' });
return value ? JSON.parse(value) : [];
}
return { cacheQuotes, loadCachedQuotes };
}
4. マルチプラットフォーム対応
4.1 Capacitor設定
// capacitor.config.ts
import { CapacitorConfig } from '@capacitor/cli';
const config: CapacitorConfig = {
appId: 'com.example.mementomori',
appName: 'Memento Mori',
webDir: 'dist',
server: {
androidScheme: 'https'
},
plugins: {
SplashScreen: {
launchShowDuration: 2000,
backgroundColor: '#1a1a1a',
},
Share: {
// シェア機能用
},
Preferences: {
// ローカルストレージ
}
}
};
export default config;
4.2 Electron設定
// electron/main.js
const { app, BrowserWindow } = require('electron');
const path = require('path');
function createWindow() {
const win = new BrowserWindow({
width: 1200,
height: 800,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
},
// フレームレスでミニマルに
// frame: false,
// transparent: true,
});
// 本番
win.loadFile('dist/index.html');
// 開発時
// win.loadURL('http://localhost:5173');
}
app.whenReady().then(createWindow);
4.3 プラットフォーム判定
// src/utils/platform.ts
import { Capacitor } from '@capacitor/core';
export function getPlatform(): 'web' | 'ios' | 'android' | 'electron' {
if (typeof window !== 'undefined' && (window as any).electron) {
return 'electron';
}
if (Capacitor.isNativePlatform()) {
return Capacitor.getPlatform() as 'ios' | 'android';
}
return 'web';
}
export function isNative(): boolean {
return ['ios', 'android'].includes(getPlatform());
}
開発フロー
Phase 1: コア機能(1-2週間)
- Vue 3 + Viteでプロジェクトセットアップ
- 既存のLife Grid機能を移植
- 名言データ(JSON)の準備
- 名言表示コンポーネント実装
- カテゴリ選択UI
Phase 2: シェア機能(1週間)
- Canvas画像生成
- シェアプレビューモーダル
- Web Share API + フォールバック
- OGPメタタグ設定
Phase 3: ユーザー機能(1週間)
- Firebase Auth導入
- カスタム名言追加機能
- お気に入り機能
- 設定の永続化
Phase 4: マルチプラットフォーム(1-2週間)
- Capacitorセットアップ
- iOS/Androidビルド確認
- Electronセットアップ
- オフライン対応
名言データサンプル
// src/data/quotes/stoicism.json
{
"quotes": [
{
"id": "seneca-001",
"text": "私たちが生きる時間が短いのではない、それらその大部分を無駄にしてしまうのだ。",
"author": "セネカ",
"source": "人生の短さについて",
"category": "stoicism",
"language": "ja"
},
{
"id": "marcus-001",
"text": "今日という日を、最後の日のつもりで生きよ。",
"author": "マルクス・アウレリウス",
"source": "自省録",
"category": "stoicism",
"language": "ja"
},
{
"id": "epictetus-001",
"text": "我々を苦しめるのは物事そのものではなく、物事に対する我々の判断である。",
"author": "エピクテトス",
"source": "提要",
"category": "stoicism",
"language": "ja"
}
]
}