猫のタロット占いアプリを制作したらこうなったの巻
78枚のタロットカードから3枚を引いて、過去・現在・未来を占う。それだけ。シンプルだから分かりやすく、シンプルだから使いやすい。midori215は、すべてのカードを猫をモチーフにしたオリジナルデザインで表現した、ブラウザ上で動作するタロット占いアプリです。カードを引くたびに、神秘的なアニメーションとともに、あなたへのメッセージが表示されます。
ℹ️
INFO
この記事について: この記事は技術的な詳細を丁寧に説明しているため、長い内容になっています(読了時間: 約25分)。12個のインタラクティブデモも含まれています。時間に余裕のあるときに、じっくり読んでいただけると幸いです。
対象読者
- ブラウザ上でタロット占いアプリを実装したいフロントエンドエンジニア
- JavaScriptでインタラクティブなUIを作りたい開発者
- カードゲームや占いアプリの基本的な実装方法を学びたい技術者
- シンプルなタロット占いシステムを短時間で構築したい人
記事に書いてあること
- 78枚のタロットカードデータの構造化と管理方法
- カードを引く処理と重複防止の実装
- 過去・現在・未来の3枚のカード配置とアニメーション
- カードの組み合わせパターン解析と解釈生成
- メッセージ生成エンジンの実装とパターンマッチング
- レスポンシブ対応のUIデザインとアニメーション効果
作ったもの
midori215は、Webブラウザ上で動作する猫のタロット占いアプリです。78枚のタロットカード(メジャーアルカナ22枚、マイナーアルカナ56枚)から3枚を引いて、過去・現在・未来を占います。
すべてのカードは猫をモチーフにしたオリジナルデザインで、各カードには「愚者の白猫」「魔術師の漆黒猫」といった名前が付けられています。カードを引くたびに、神秘的なアニメーションとともに、あなたへのメッセージが表示されます。
カードの組み合わせパターンを解析し、総合的な運勢メッセージも生成します。同じスートのカードが重なったり、特定のカードの組み合わせが出現したりすると、特別な解釈が表示されます。
なぜ作ったか
タロット占いというのは、ちょっとかっこいい。神秘的な雰囲気で、人を惹きつける。でも、実際のタロットカードを用意するのは大変だし、占い師に頼むのもお金がかかる。
ブラウザ上で動くタロット占いアプリを作れば、誰でも気軽に試せる。猫をモチーフにしたデザインなら、親しみやすく、楽しめる。シンプルだから分かりやすく、シンプルだから使いやすい。
まずは基本を押さえたい。78枚のカードから3枚を引いて、過去・現在・未来を占う。それだけ。複雑な機能は後回し。まずは、カードを引いて、メッセージを表示する基本機能を実装したい。
タロットカードデータの構造化
78枚のタロットカードをJavaScriptの配列として定義します。各カードには、名前、番号、意味、過去・現在・未来のメッセージが含まれます。
const tarotCards = [
{
name: "愚者の白猫",
number: 0,
meaning: "新たなる旅立ち、純真無垢な心、自由な精神",
pastMessages: [
"好奇心旺盛な子猫のように、新しい冒険に飛び込んでいった過去。その経験が今のあなたを形作っています。",
"自由気ままに道を選んできた過去。その純粋な選択が、今の可能性を広げています。"
],
presentMessages: [
"子猫のような好奇心で、新しい一歩を踏み出すべき時です。",
"先入観にとらわれず、自由な心で道を選ぶ時です。"
],
futureMessages: [
"予期せぬ冒険が、あなたを素晴らしい場所へ導くでしょう。",
"自由な精神が、新たな扉を開くきっかけとなるでしょう。"
]
},
// ... 他のカードも同様に定義
];
最初は、各カードに1つずつしかメッセージを持たせていませんでした。でも、これでは毎回同じメッセージが表示されてしまい、面白くない。
各カードに複数のメッセージを持たせることで、同じカードでも異なるメッセージが表示されるようにしました。過去・現在・未来それぞれに3-4個のメッセージを用意し、ランダムに選択するようにしています。
メジャーアルカナ(22枚)とマイナーアルカナ(56枚)を区別するため、マイナーアルカナにはsuitプロパティを追加しました。ワンド、カップ、ソード、ペンタクルの4つのスートがあります。
最初は、カードのデータをすべて1つの配列に書いていました。でも、78枚すべてを書くのは大変で、コードも長くなってしまいます。
カードのデータを構造化し、共通のパターンを抽出することで、コードを簡潔にしました。メジャーアルカナとマイナーアルカナで異なる処理をしつつ、共通のインターフェースを保つようにしています。
カードを引く処理の実装
カードを引く処理は、重複を防ぎながら、ランダムに1枚を選択します。
const cardManager = {
drawNewCard: () => {
if (drawnCards.length >= 3) return null;
const remainingCards = tarotCards.filter(card => !drawnCards.includes(card));
if (remainingCards.length === 0) return null;
return remainingCards[randomUtils.getRandomInt(remainingCards.length)];
}
};
drawnCards配列に、すでに引いたカードを保存します。filterメソッドで、まだ引いていないカードだけを抽出し、その中からランダムに1枚を選択します。
最初は、Math.random()を使って、配列のインデックスを直接生成していました。でも、これだと重複する可能性があります。
filterメソッドで、すでに引いたカードを除外してから、ランダムに選択するようにしました。これで、重複なく3枚のカードを引くことができます。
3枚引いた後は、drawNewCardがnullを返すようにしています。これにより、ボタンを無効化し、ユーザーが誤って4枚目を引くことを防ぎます。
カード表示のアニメーション
カードを引いたときのアニメーションは、回転効果とフェードイン効果を組み合わせています。
const displayManager = {
showCard: (card, position) => {
const slotElement = document.getElementById(`slot${position}`);
const placeholder = slotElement.querySelector('.tarot-placeholder');
// アニメーション: カードが回転して表示される
placeholder.style.transform = 'rotateY(180deg)';
placeholder.style.opacity = '0';
setTimeout(() => {
placeholder.style.background = cardColor;
placeholder.classList.add('card-revealed');
placeholder.innerHTML = `
<div class="card-content">
<div class="card-header">
<div class="card-number">${card.number}</div>
<div class="card-name">${card.name}</div>
</div>
<div class="card-body">
<div class="card-meaning">${card.meaning}</div>
<div class="card-message">
<div class="message-label">🐱 メッセージ</div>
<div class="message-text">${message}</div>
</div>
</div>
</div>`;
setTimeout(() => {
placeholder.style.transform = 'rotateY(0deg)';
placeholder.style.opacity = '1';
}, 50);
}, 300);
}
};
最初は、カードを即座に表示していました。でも、これでは面白くない。アニメーションを追加することで、神秘的な雰囲気を演出できます。
rotateY(180deg)でカードを裏返し、opacity: 0で透明にします。300ミリ秒待ってから、カードの内容を設定し、50ミリ秒後に元の状態に戻します。これで、カードが回転しながら表示されるアニメーションが完成します。
アニメーションのタイミングを調整するため、setTimeoutの待機時間を50ミリ秒、100ミリ秒、200ミリ秒、300ミリ秒と試しました。50ミリ秒では速すぎて、アニメーションが見えません。300ミリ秒では遅すぎて、ユーザーが待ちきれなくなります。200ミリ秒が、視認性と速度のバランスが最も良い値でした。
ただし、カードの内容を設定する前に、300ミリ秒待つ必要があります。これにより、カードが完全に裏返ってから、新しい内容が表示されます。50ミリ秒の待機は、ブラウザのレンダリングを待つためのものです。
カードの色分けとグラデーション
カードの種類に応じて、異なるグラデーション色を設定します。
const isMajor = !card.suit;
const cardColor = isMajor
? 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)'
: card.suit === 'ワンド'
? 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)'
: card.suit === 'カップ'
? 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)'
: card.suit === 'ソード'
? 'linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)'
: 'linear-gradient(135deg, #fa709a 0%, #fee140 100%)';
メジャーアルカナは紫系のグラデーション、ワンドはピンク系、カップは青系、ソードは緑系、ペンタクルはオレンジ系のグラデーションを使用しています。
最初は、すべてのカードを同じ色で表示していました。でも、これではカードの種類が分かりにくい。
カードの種類に応じて色を変えることで、視認性を向上させました。メジャーアルカナは特別感を出すため、紫系のグラデーションを使用しています。
グラデーションの色を選ぶ際は、視認性と美しさのバランスを重視しました。試行錯誤の過程では、以下の色を試しました:
- メジャーアルカナ: 紫系(#667eea → #764ba2)が、神秘的な雰囲気を演出できました。
- ワンド: ピンク系(#f093fb → #f5576c)が、情熱的な印象を与えました。
- カップ: 青系(#4facfe → #00f2fe)が、感情的な印象を与えました。
- ソード: 緑系(#43e97b → #38f9d7)が、知的な印象を与えました。
- ペンタクル: オレンジ系(#fa709a → #fee140)が、物質的な印象を与えました。
各色のコントラスト比を確認し、テキストが読みやすいことを確認しました。WCAG(Web Content Accessibility Guidelines)の基準に準拠するよう、コントラスト比を4.5以上に保っています。
カードの組み合わせパターン解析
3枚のカードの組み合わせを解析し、特別なパターンを検出します。
function analyzeCardPattern(cards) {
return {
suitPattern: cards.reduce((acc, card) => {
acc[card.suit] = (acc[card.suit] || 0) + 1;
return acc;
}, {}),
numberPattern: {
ascending: cards.every((card, i, arr) => i === 0 || card.number >= arr[i - 1].number),
descending: cards.every((card, i, arr) => i === 0 || card.number <= arr[i - 1].number),
sequential: cards.every((card, i, arr) => i === 0 || card.number === arr[i - 1].number + 1),
allEven: cards.every(card => card.number % 2 === 0),
allOdd: cards.every(card => card.number % 2 === 1),
sum: cards.reduce((sum, card) => sum + card.number, 0),
average: cards.reduce((sum, card) => sum + card.number, 0) / cards.length
},
specialPatterns: {
allSameSuit: Object.values(cards.reduce((acc, card) => {
if(card.suit) acc[card.suit] = (acc[card.suit] || 0) + 1;
return acc;
}, {})).some(count => count === cards.length),
hasReversePair: cards.some((card, i) =>
cards.some((other, j) => i !== j && card.number + other.number === 21)
)
}
};
}
スートパターン、数字パターン、特殊パターンを解析します。同じスートのカードが重なったり、数字が連続していたりすると、特別な解釈が表示されます。
最初は、カードの組み合わせを解析していませんでした。でも、これでは単純に3枚のカードの意味を表示するだけになってしまいます。
カードの組み合わせを解析することで、より深い解釈を提供できます。同じスートのカードが重なると、そのスートの影響力が増幅されることを示します。
数字パターンの解析では、昇順、降順、連続、偶数/奇数、合計、平均を計算します。これらのパターンに基づいて、特別なメッセージを生成します。
特殊パターンの検出では、同じスートのカードが3枚すべて揃った場合や、数字の合計が21になる組み合わせを検出します。これらのパターンは、タロット占いにおいて特別な意味を持ちます。
メッセージ生成エンジンの実装
カードの組み合わせに基づいて、総合的な運勢メッセージを生成します。
function generateOverallFortune(cards, pattern) {
const cardCombinations = {
spiritual: {
wisdom: {
cards: ['女教皇', '隠者', '星'],
message: {
past: '内なる叡智が重要な気づきをもたらしてきました。',
present: '直感と深い洞察があなたを導いています。',
future: '精神的な成長が新たな可能性を開くでしょう。'
}
}
},
material: {
success: {
cards: ['魔術師', '皇帝', '世界'],
message: {
past: '着実な努力が今の基盤を築いてきました。',
present: '実践的な行動が実を結びつつあります。',
future: '望ましい成果が形となって現れるでしょう。'
}
}
}
};
const cardNames = cards.map(card => card.name);
let foundPattern = false;
Object.entries(cardCombinations).forEach(([category, patterns]) => {
Object.entries(patterns).forEach(([type, pattern]) => {
const matchCount = pattern.cards.filter(name =>
cardNames.some(cardName => cardName && cardName.includes(name))
).length;
if (matchCount >= 2) {
foundPattern = true;
// メッセージを生成
}
});
});
return fortune;
}
カードの組み合わせパターンを定義し、3枚のカードの中から2枚以上が一致する場合に、特別なメッセージを生成します。
最初は、カードの組み合わせを考慮していませんでした。でも、これでは単純に3枚のカードの意味を表示するだけになってしまいます。
カードの組み合わせパターンを定義することで、より深い解釈を提供できます。精神的成長、現実的な成長、人間関係などのカテゴリに分けて、それぞれのパターンを定義しています。
パターンマッチングでは、カード名に特定のキーワードが含まれているかを確認します。例えば、「女教皇」「隠者」「星」のいずれかが含まれている場合、精神的成長のパターンとして認識します。
2枚以上が一致する場合に、特別なメッセージを生成します。これにより、偶然の一致ではなく、意味のある組み合わせとして認識されます。
パターンが見つからない場合は、個別のカードの意味を組み合わせて、総合的なメッセージを生成します。過去・現在・未来それぞれの時制に応じたメッセージを生成し、時系列の流れを表現します。
デッキの残り枚数表示
カードを引くたびに、デッキの残り枚数を更新します。
const remainingCards = 78 - drawnCards.length;
const deckCountEl = document.getElementById('deckCount');
if (deckCountEl) {
deckCountEl.textContent = remainingCards;
if (remainingCards <= 0) {
document.getElementById('drawCards').disabled = true;
document.getElementById('drawCards').classList.add('disabled');
}
}
78枚から、すでに引いたカードの数を引いて、残り枚数を計算します。3枚引いた後は、ボタンを無効化します。
最初は、デッキの残り枚数を表示していませんでした。でも、これではユーザーが何枚残っているか分かりません。
デッキの残り枚数を表示することで、ユーザーに視覚的なフィードバックを提供できます。カードを引くたびに、残り枚数が減っていく様子を見ることで、ゲーム性が向上します。
3枚引いた後は、ボタンを無効化します。これにより、ユーザーが誤って4枚目を引くことを防ぎます。リセットボタンを押すと、デッキの残り枚数が78に戻り、再度カードを引けるようになります。
実装のハマりポイント
実装中に遭遇した問題と、その解決方法をまとめます。
問題1: カードの重複
最初は、カードを引く処理で重複チェックをしていませんでした。結果として、同じカードが2回引かれることがありました。
解決方法: drawnCards配列に、すでに引いたカードを保存し、filterメソッドで除外するようにしました。
const remainingCards = tarotCards.filter(card => !drawnCards.includes(card));
includesメソッドで、オブジェクトの参照を比較します。これにより、同じカードオブジェクトが2回引かれることを防ぎます。
問題2: アニメーションのタイミング
カードを表示するアニメーションのタイミングが適切でなく、カードの内容が一瞬見えてしまうことがありました。
解決方法: opacity: 0とtransform: rotateY(180deg)を同時に設定し、カードの内容を設定する前に300ミリ秒待つようにしました。
placeholder.style.transform = 'rotateY(180deg)';
placeholder.style.opacity = '0';
setTimeout(() => {
placeholder.innerHTML = cardContent;
setTimeout(() => {
placeholder.style.transform = 'rotateY(0deg)';
placeholder.style.opacity = '1';
}, 50);
}, 300);
これにより、カードが完全に裏返ってから、新しい内容が表示されるようになりました。
問題3: メッセージの生成
カードの組み合わせパターンを解析する際、カード名の部分一致がうまくいかないことがありました。
解決方法: includesメソッドで、カード名に特定のキーワードが含まれているかを確認するようにしました。
const matchCount = pattern.cards.filter(name =>
cardNames.some(cardName => cardName && cardName.includes(name))
).length;
これにより、「女教皇の聖なる白猫」というカード名から「女教皇」を抽出し、パターンマッチングが正しく動作するようになりました。
実際に試した例
例1: デスクトップPCでの動作確認
デスクトップPC(1920×1080)で動作確認しました。カードのアニメーションは滑らかに動作し、メッセージの生成も即座に完了します。レスポンシブ対応により、画面サイズに応じてカードのサイズが自動調整されます。
例2: モバイルデバイスでの動作確認
モバイルデバイス(375px幅)でも動作確認しました。カードのサイズが自動調整され、タッチ操作でも問題なく動作します。アニメーションも滑らかに動作し、パフォーマンスに問題はありません。
例3: カードの組み合わせパターンテスト
同じスートのカードを3枚引いた場合、特別なメッセージが表示されることを確認しました。パターンマッチングが正しく動作し、適切なメッセージが生成されます。
使ってみて
midori215は、シンプルなタロット占いアプリとして、実用的な機能を詰め込んでいます。
78枚のカードから3枚を引いて、過去・現在・未来を占う基本機能から、カードの組み合わせパターン解析、メッセージ生成まで、必要な機能が揃っています。
猫をモチーフにしたデザインで、親しみやすく、楽しめるアプリになっています。カードを引くたびに、神秘的なアニメーションとともに、あなたへのメッセージが表示されます。
midori215を試す
実際のコードを見たい場合は、GitHubのリポジトリを参照してください。タロットカードデータの構造化から、カードを引く処理、アニメーション、メッセージ生成まで、すべてのコードが公開されています。
まとめ
midori215は、シンプルなタロット占いアプリとして、以下の機能を実現しました:
- 78枚のタロットカードデータの構造化: メジャーアルカナ22枚、マイナーアルカナ56枚をJavaScriptの配列として定義
- カードを引く処理: 重複を防ぎながら、ランダムに3枚のカードを選択
- カード表示のアニメーション: 回転効果とフェードイン効果を組み合わせた神秘的なアニメーション
- カードの色分けとグラデーション: カードの種類に応じて、異なるグラデーション色を設定
- カードの組み合わせパターン解析: スートパターン、数字パターン、特殊パターンを解析
- メッセージ生成エンジン: カードの組み合わせに基づいて、総合的な運勢メッセージを生成
シンプルだから分かりやすく、シンプルだから使いやすい。midori215は、タロット占いの基本を押さえた、実用的なアプリです。
さらに深く学ぶには
最後まで読んでいただき、ありがとうございました。 猫のタロット占いアプリを制作したらこうなったの巻が、あなたのタロット占いプロジェクトの出発点になれば幸いです。