タロット占いシステムの製作
同じ結果はほとんど出ません。78枚の意味を土台に、時間や選び方で占い文を個別化。UIは24枚から3枚、軽快さ優先で作りました。
ブラウザで動く本格的なタロット占いを作った。
78枚のカード。大アルカナ22枚、小アルカナ56枚。3枚選んで未来を占う。ランダム性と個別メッセージを組み合わせて、毎回違う結果を生成する仕組みにした。
作ったもの
タロット占いシステム。24枚のカードから3枚選ぶ。選んだカードが未来を示す。
最初は全78枚を表示しようと思った。ダメだった。
画面が狭すぎる。スマホだとカードが小さくなりすぎて操作できない。24枚に減らしたら、ちょうど良いサイズになった。6列×4行のグリッドで表示。
まずは試してみてください:
カードシステムの設計
78枚のカード全てを定義した。これが大変だった。
大アルカナは22枚。「愚者」「魔術師」「女教皇」から始まって「世界」まで。それぞれに名前、意味、運勢メッセージを設定。
小アルカナは56枚。ワンド(火)、カップ(水)、ソード(風)、ペンタクル(土)の4つのスート。各14枚。エース、数札(2-10)、コートカード(ページ、ナイト、クイーン、キング)。
全部手作業でデータを入力した。間違いがないか何度も確認。大変だったけど、完成した時は嬉しい。
コードの全体構造を見てみましょう:
データ構造はシンプルに:
const majorArcana = [
{ name: "愚者", meaning: "新しい始まり、純粋な心、冒険", fortune: "近い未来に新しい挑戦が待っています..." },
{ name: "魔術師", meaning: "創造力、意志力、技術", fortune: "あなたの能力が未来で開花します..." },
// ... 22枚
];
const minorArcana = [
// ワンド14枚
{ name: "ワンドのエース", meaning: "新しい始まり、創造力", fortune: "..." },
// カップ14枚
{ name: "カップのエース", meaning: "新しい愛、感情", fortune: "..." },
// ソード14枚
{ name: "ソードのエース", meaning: "新しいアイデア、真実", fortune: "..." },
// ペンタクル14枚
{ name: "ペンタクルのエース", meaning: "新しい機会、富", fortune: "..." }
];
const tarotCards = [...majorArcana, ...minorArcana]; // 全78枚
78枚のカードの意味を確認できます:
ランダム性の実装
占いは毎回違う結果を出す必要がある。完全なランダムにした。
78枚全部をシャッフル。その中から24枚をランダムに選択。画面に配置。
const shuffledCards = [...tarotCards]
.sort(() => Math.random() - 0.5)
.slice(0, 24);
この24枚から、ユーザーが3枚選ぶ。選んだ順番も記録。1枚目が「近い未来」、2枚目が「中期未来」、3枚目が「遠い未来」を示す。
カード選択の順番も重要な情報として使った。
個別メッセージの生成
ここが一番工夫したところ。ただランダムに選ぶだけじゃつまらない。
時間帯、曜日、カード選択速度、セッション時間を全部使って、個別のメッセージを生成する仕組みにした。
時間帯による変化
朝、昼、夕方、夜でメッセージが変わる:
const currentHour = now.getHours();
if (currentHour >= 5 && currentHour < 12) {
// 朝のメッセージ(8パターン)
timeMessage = timeMessages.morning[Math.floor(Math.random() * timeMessages.morning.length)];
} else if (currentHour >= 12 && currentHour < 17) {
// 午後のメッセージ(8パターン)
timeMessage = timeMessages.afternoon[...];
} else if (currentHour >= 17 && currentHour < 21) {
// 夕方のメッセージ(8パターン)
timeMessage = timeMessages.evening[...];
} else {
// 夜のメッセージ(8パターン)
timeMessage = timeMessages.night[...];
}
朝に占うと「新しい始まり」のエネルギー、夜に占うと「静寂と直感」のメッセージ。同じカードでも時間帯で印象が変わる。
曜日による変化
曜日ごとにも違うメッセージ。日曜から土曜まで、それぞれ8パターンずつ用意した。合計56パターン。
月曜は「新しい始まり」、火曜は「情熱と行動」、水曜は「コミュニケーション」。曜日の持つイメージをメッセージに反映させた。
カード選択速度による分析
これも面白い仕掛け。カードをクリックした時刻を記録:
let cardClickTimes = [];
function selectCard(card, element, cardInner) {
const clickTime = Date.now();
cardClickTimes.push(clickTime);
// ...
}
3枚選び終わったら、クリック間隔を分析:
const clickIntervals = [];
for (let i = 1; i < cardClickTimes.length; i++) {
clickIntervals.push(cardClickTimes[i] - cardClickTimes[i-1]);
}
const avgInterval = clickIntervals.reduce((a, b) => a + b, 0) / clickIntervals.length;
2秒以内なら「素早い直感力」、10秒以上なら「慎重な思考力」、その間なら「バランスの取れた判断力」というメッセージを追加。
選択の仕方が性格を表すという考え。
セッション時間による分析
占いページを開いてから結果が出るまでの時間も記録:
let sessionStartTime = Date.now();
function showResult() {
const sessionDuration = Date.now() - sessionStartTime;
if (sessionDuration > 60000) {
// じっくり考えた(60秒以上)
sessionMessage = sessionMessages.long[...];
} else if (sessionDuration < 30000) {
// 迅速に判断(30秒以内)
sessionMessage = sessionMessages.short[...];
} else {
// 程よい時間
sessionMessage = sessionMessages.medium[...];
}
}
未来に焦点を当てたメッセージ
最初は「現在」「過去」「未来」の3枚にしようと思った。でも、ユーザーが知りたいのは未来だと気づいた。
全部「未来」に変更。
1枚目:「近い未来(1-3ヶ月)」
2枚目:「中期未来(3-6ヶ月)」
3枚目:「遠い未来(6ヶ月-1年)」
こうしたら、占いの結果がずっと具体的になった。「来月何が起きるか」「半年後の自分」というイメージが湧きやすい。
カード選択のUX
カードをクリックした時の反応にこだわった。
即座のフィードバック
クリックした瞬間、カードが浮き上がる:
function selectCard(card, element, cardInner) {
element.style.pointerEvents = 'none'; // 重複クリック防止
element.style.transform = 'translateY(-15px) scale(1.1)'; // 即座に反応
element.classList.add('flipping');
// ...
}
視覚的フィードバックを即座に出す。ユーザーが「ちゃんと選べた」と分かる。
3Dフリップアニメーション
カードが裏返るアニメーション。CSS 3Dトランスフォームを使った:
.card-inner {
transform-style: preserve-3d;
transition: transform 0.6s ease-in-out;
}
.card-inner.flipped {
transform: rotateY(180deg);
}
.card-front, .card-back {
backface-visibility: hidden;
}
.card-back {
transform: rotateY(180deg);
}
表と裏をそれぞれ作って、180度回転させる。backface-visibility: hiddenで裏面を隠す。
音響フィードバック
ちょっと遊び心を入れた。カードをクリックした時、短い音を鳴らす:
try {
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
const oscillator = audioContext.createOscillator();
const gainNode = audioContext.createGain();
oscillator.connect(gainNode);
gainNode.connect(audioContext.destination);
oscillator.frequency.value = 800;
oscillator.type = 'sine';
gainNode.gain.setValueAtTime(0.1, audioContext.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.1);
oscillator.start(audioContext.currentTime);
oscillator.stop(audioContext.currentTime + 0.1);
} catch (e) {
// 音響フィードバックが失敗しても処理を継続
}
Web Audio APIで単純な正弦波を0.1秒鳴らす。周波数800Hzで、音量を徐々に減衰させる。
ブラウザがサポートしてなくても大丈夫なように、try-catchで囲んだ。音が鳴らなくても占いは動く。
モバイル最適化
最初、モバイルでタップしても反応しないことがあった。大きくつまずいた。
原因はイベントリスナーの設計。clickイベントだけでは不十分。
// NG例(動かないことがある)
cardElement.addEventListener('click', handleCardClick);
タッチイベントも追加:
let touchStartTime = 0;
let touchMoved = false;
const handleTouchStart = (event) => {
touchStartTime = Date.now();
touchMoved = false;
};
const handleTouchMove = (event) => {
touchMoved = true;
};
const handleTouchEnd = (event) => {
const touchDuration = Date.now() - touchStartTime;
// タッチが短時間で移動していない場合のみクリック
if (touchDuration < 500 && !touchMoved) {
event.preventDefault();
event.stopPropagation();
selectCard(card, cardElement, cardInner);
}
};
cardElement.addEventListener('click', handleCardClick, { passive: false });
cardElement.addEventListener('touchstart', handleTouchStart, { passive: true });
cardElement.addEventListener('touchmove', handleTouchMove, { passive: true });
cardElement.addEventListener('touchend', handleTouchEnd, { passive: false });
スワイプとタップを区別。タッチが短時間(500ms以内)で移動してない場合だけ、カード選択として扱う。
これで、スマホでも快適に操作できるようになった。
結果表示のレイアウト
3枚のカードを横並びに表示。グリッドレイアウトを使った:
.three-card-layout {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
max-width: 900px;
}
auto-fitとminmaxで、画面幅に応じて自動的にレイアウトが変わる。PCでは3列、タブレットでは2列、スマホでは1列。
レスポンシブ対応が楽になった。メディアクエリをあまり書かなくて済む。
深層分析の生成
単にカードの意味を表示するだけじゃつまらない。もっと深い分析を追加した。
カードの組み合わせを分析:
// majorArcana: 大アルカナ22枚の配列(前述のデータ構造)
// cards: ユーザーが選んだ3枚のカード
function analyzeCards(cards) {
const analysis = {
majorArcanaCount: 0,
fireElementCount: 0,
waterElementCount: 0,
airElementCount: 0,
earthElementCount: 0,
totalCardValue: 0,
timeComplexity: 0
};
cards.forEach(card => {
if (majorArcana.some(major => major.name === card.name)) {
analysis.majorArcanaCount++;
}
if (card.name.includes('ワンド')) analysis.fireElementCount++;
else if (card.name.includes('カップ')) analysis.waterElementCount++;
else if (card.name.includes('ソード')) analysis.airElementCount++;
else if (card.name.includes('ペンタクル')) analysis.earthElementCount++;
analysis.totalCardValue += card.name.length + card.meaning.length;
});
const now = new Date();
analysis.timeComplexity = (now.getHours() + now.getMinutes() + now.getDate()) % 100;
return analysis;
}
大アルカナの枚数、エレメントのバランス、カード名の複雑さ、現在時刻を全部使って、インデックスを計算:
function calculateIndex(cards, analysis, arrayLength) {
const cardNamesHash = cards.map(card => card.name).join('').length;
const meaningHash = cards.map(card => card.meaning).join('').length;
const elementBalance = Math.abs(analysis.fireElementCount - analysis.waterElementCount) +
Math.abs(analysis.airElementCount - analysis.earthElementCount);
const complexIndex = (cardNamesHash + meaningHash + elementBalance +
analysis.majorArcanaCount * 10 + analysis.timeComplexity) % arrayLength;
return complexIndex;
}
このインデックスを使って、用意した配列(機会、課題、道筋)から最適なメッセージを選ぶ。
完全なランダムではなく、カードの内容と時間情報を組み合わせた「意味のあるランダム性」。
メッセージのバリエーション
同じカードの組み合わせでも、時間帯や曜日で違うメッセージが出るようにした。メッセージパターンの数:
- 時間帯メッセージ:32パターン(朝8、昼8、夕方8、夜8)
- 曜日メッセージ:56パターン(各曜日8パターン)
- 選択速度メッセージ:9パターン(速い3、遅い3、普通3)
- セッション時間メッセージ:9パターン(長い3、短い3、普通3)
- 大アルカナメッセージ:4パターン
- 小アルカナメッセージ:4パターン
- 結びのメッセージ:8パターン
合計122パターン以上の組み合わせ。さらにカード自体が78枚。組み合わせは膨大。
何度占っても違う結果が出る。同じ結果が出る確率はほぼゼロ。
結果パターンの組み合わせ:
時間情報を使ったメッセージ生成を試せます:
カードアニメーション
カードが裏返る3Dアニメーション。試してみてください:
遊び心の要素
簡易版の占いも作ってみた。遊んでください:
現在の時間情報がどのようにメッセージに反映されるか確認できます:
スクロール処理の改善
結果表示時、自動的に結果エリアまでスクロールするようにした:
function showResult() {
// ...結果表示処理...
setTimeout(() => {
const resultRect = resultDiv.getBoundingClientRect();
const scrollPosition = window.pageYOffset + resultRect.top - 80;
window.scrollTo({
top: Math.max(0, scrollPosition),
behavior: 'smooth'
});
}, 100);
}
80pxの余裕を持たせて、結果エリアの少し上にスクロール。ユーザーが結果を見逃さない。
システム全体の流れを確認:
これだ。
使ってみて
実際に試してみてください:
デモを起動する(全画面表示)
ポイントは以下の3つ:
- 24枚グリッドから3枚選択(選択順も結果に反映)
- 時間帯/曜日/クリック間隔/滞在時間でメッセージを個別化
- モバイルはタッチ判定(500ms以内・移動なし)で誤反応を防止
手応え。
まとめ
タロット占いシステムを作った。ポイントは4つ:
- 78枚のカードデータ:大アルカナ22枚、小アルカナ56枚を全て定義
- 時間ベースの個別メッセージ:時間帯、曜日、選択速度、セッション時間を組み合わせて122パターン以上のメッセージを生成
- 3Dフリップアニメーション:CSS 3Dトランスフォームと音響フィードバックで直感的なUI
- モバイル最適化:タッチイベントの適切な処理とレスポンシブレイアウト
単純なランダム選択ではなく、時間情報とカード分析を組み合わせた「意味のあるランダム性」を実現できた。毎回違う結果が出て、個別化された占い体験を提供できる。
同じような占いシステムを作りたい方の参考になれば嬉しいです。
さらに深く学ぶには
この記事で興味を持った方におすすめのリンク:
最後まで読んでくださり、ありがとうございました。