timconspicuous.neocities.org
1class Tarot {
2 constructor() {
3 this.currentCardIndex = 0;
4 this.isTransitioning = false;
5 this.isFirstLoad = true;
6
7 // Full Major Arcana + back card data
8 this.allCardData = [
9 { id: "0", name: "The Fool" },
10 { id: "I", name: "The Magician" },
11 { id: "II", name: "The High Priestess" },
12 { id: "III", name: "The Empress" },
13 { id: "IV", name: "The Emperor" },
14 { id: "V", name: "The Hierophant" },
15 { id: "VI", name: "The Lovers" },
16 { id: "VII", name: "The Chariot" },
17 { id: "VIII", name: "Strength" },
18 { id: "IX", name: "The Hermit" },
19 { id: "X", name: "Wheel of Fortune" },
20 { id: "XI", name: "Justice" },
21 { id: "XII", name: "The Hanged Man" },
22 { id: "XIII", name: "Death" },
23 { id: "XIV", name: "Temperance" },
24 { id: "XV", name: "The Devil" },
25 { id: "XVI", name: "The Tower" },
26 { id: "XVII", name: "The Star" },
27 { id: "XVIII", name: "The Moon" },
28 { id: "XIX", name: "The Sun" },
29 { id: "XX", name: "Judgement" },
30 { id: "XXI", name: "The World" },
31
32 { id: "back", name: "Return" },
33 ];
34
35 // Available cards (loaded from page data)
36 this.availableCards = [];
37 this.loadAvailableCards();
38
39 this.init();
40 }
41
42 loadAvailableCards() {
43 const cardContentData = document.getElementById("card-content-data");
44 if (!cardContentData) {
45 console.error("Card content data not found");
46 return;
47 }
48
49 try {
50 const cardContent = JSON.parse(cardContentData.textContent);
51
52 // Map card content to available cards with full data
53 this.availableCards = cardContent.map((card) => {
54 const tarotCard = this.allCardData[card.cardIndex] || {
55 id: card.cardIndex.toString(),
56 name: "Unknown Card",
57 };
58
59 return {
60 ...tarotCard,
61 cardIndex: card.cardIndex,
62 slug: card.slug,
63 component: card.component,
64 };
65 });
66
67 console.log("Available cards:", this.availableCards);
68 } catch (error) {
69 console.error("Error parsing card content data:", error);
70 }
71 }
72
73 init() {
74 this.setupEventListeners();
75 this.setupTouchEvents();
76 this.loadFromHash();
77 this.updateImage();
78 this.updateCardIndicator();
79 this.loadComponent();
80
81 // Initial fade-in only on first load
82 if (this.isFirstLoad) {
83 this.initialFadeIn();
84 }
85 }
86
87 initialFadeIn() {
88 const contentContainer = document.getElementById("content-container");
89 const cardContainer = document.getElementById("card-container");
90
91 // Start both containers as invisible
92 if (contentContainer) {
93 contentContainer.style.opacity = "0";
94 contentContainer.style.transform = "translate(100px, -50%)";
95 }
96 if (cardContainer) {
97 cardContainer.style.opacity = "0";
98 cardContainer.style.transform = "translate(-100px, -50%)";
99 }
100
101 // Fade in after a short delay
102 setTimeout(() => {
103 if (contentContainer) {
104 contentContainer.style.transition = "opacity 0.8s ease-in-out";
105 contentContainer.style.opacity = "1";
106 }
107 if (cardContainer) {
108 cardContainer.style.transition = "opacity 0.8s ease-in-out";
109 cardContainer.style.opacity = "1";
110 }
111 }, 100);
112
113 this.isFirstLoad = false;
114 }
115
116 setupEventListeners() {
117 // Navigation buttons
118 document.getElementById("prev-card")?.addEventListener(
119 "click",
120 () => this.previousCard(),
121 );
122 document.getElementById("next-card")?.addEventListener(
123 "click",
124 () => this.nextCard(),
125 );
126
127 // Make card indicator clickable to return to first card (card 0)
128 document.getElementById("card-indicator")?.addEventListener(
129 "click",
130 () => this.goToFirstCard(),
131 );
132
133 // Keyboard navigation
134 document.addEventListener("keydown", (e) => {
135 if (e.key === "ArrowLeft") {
136 e.preventDefault();
137 this.previousCard();
138 } else if (e.key === "ArrowRight") {
139 e.preventDefault();
140 this.nextCard();
141 } else if (e.key === "Home") {
142 e.preventDefault();
143 this.goToFirstCard();
144 }
145 });
146
147 // Hash change listener
148 globalThis.addEventListener("hashchange", () => this.loadFromHash());
149 }
150
151 setupTouchEvents() {
152 let startX = 0;
153 let startY = 0;
154
155 document.addEventListener("touchstart", (e) => {
156 startX = e.touches[0].clientX;
157 startY = e.touches[0].clientY;
158 });
159
160 document.addEventListener("touchend", (e) => {
161 if (!startX || !startY) return;
162
163 const endX = e.changedTouches[0].clientX;
164 const endY = e.changedTouches[0].clientY;
165
166 const diffX = startX - endX;
167 const diffY = startY - endY;
168
169 // Only trigger if horizontal swipe is dominant
170 if (Math.abs(diffX) > Math.abs(diffY) && Math.abs(diffX) > 50) {
171 if (diffX > 0) {
172 this.nextCard();
173 } else {
174 this.previousCard();
175 }
176 }
177
178 startX = 0;
179 startY = 0;
180 });
181 }
182
183 loadFromHash() {
184 const hash = globalThis.location.hash.slice(1);
185 if (!hash) {
186 this.currentCardIndex = 0;
187 this.updateHash();
188 return;
189 }
190
191 // Try to find by card ID first, then by slug
192 let foundIndex = this.availableCards.findIndex((card) =>
193 card.id === hash || card.slug === hash
194 );
195
196 // If not found, try parsing as card index
197 if (foundIndex === -1) {
198 const cardIndex = parseInt(hash);
199 if (!isNaN(cardIndex)) {
200 foundIndex = this.availableCards.findIndex((card) =>
201 card.cardIndex === cardIndex
202 );
203 }
204 }
205
206 if (foundIndex !== -1) {
207 this.currentCardIndex = foundIndex;
208 } else {
209 // Default to first card if hash doesn't match anything
210 this.currentCardIndex = 0;
211 }
212
213 this.updateImage();
214 this.updateCardIndicator();
215 }
216
217 updateHash() {
218 const currentCard = this.availableCards[this.currentCardIndex];
219 if (currentCard) {
220 // Prefer slug over card ID for URL
221 globalThis.location.hash = currentCard.id || currentCard.slug;
222 }
223 }
224
225 async goToFirstCard() {
226 if (this.isTransitioning) return;
227
228 // Find the first card (card index 0)
229 const firstCardIndex = this.availableCards.findIndex((card) =>
230 card.cardIndex === 0
231 );
232 if (firstCardIndex !== -1 && firstCardIndex !== this.currentCardIndex) {
233 const direction = firstCardIndex < this.currentCardIndex
234 ? "right"
235 : "left";
236 this.currentCardIndex = firstCardIndex;
237 await this.transitionToCard(direction);
238 }
239 }
240
241 async previousCard() {
242 if (this.isTransitioning) return;
243
244 this.currentCardIndex = this.currentCardIndex > 0
245 ? this.currentCardIndex - 1
246 : this.availableCards.length - 1;
247 await this.transitionToCard("right");
248 }
249
250 async nextCard() {
251 if (this.isTransitioning) return;
252
253 this.currentCardIndex =
254 this.currentCardIndex < this.availableCards.length - 1
255 ? this.currentCardIndex + 1
256 : 0;
257 await this.transitionToCard("left");
258 }
259
260 async transitionToCard(direction) {
261 this.isTransitioning = true;
262
263 const contentContainer = document.getElementById("content-container");
264 const cardContainer = document.getElementById("card-container");
265
266 // Get viewport width for proper off-screen positioning
267 const viewportWidth = globalThis.innerWidth;
268
269 // Determine slide directions
270 const slideOutDirection = direction === "left"
271 ? `-${viewportWidth}px`
272 : `${viewportWidth}px`;
273 const slideInDirection = direction === "left"
274 ? `${viewportWidth}px`
275 : `-${viewportWidth}px`;
276
277 // Store the initial transforms for precise restoration
278 const initialContentTransform = "translate(100px, -50%)";
279 const initialCardTransform = "translate(-100px, -50%)";
280
281 // Reset transitions for smooth animation
282 if (contentContainer) {
283 contentContainer.style.transition = "transform 0.6s ease-in-out";
284 }
285 if (cardContainer) {
286 cardContainer.style.transition = "transform 0.6s ease-in-out";
287 }
288
289 // Phase 1: Slide content out first
290 if (contentContainer) {
291 contentContainer.style.transform =
292 `translateY(-50%) translateX(${slideOutDirection})`;
293 }
294
295 // Wait 150ms, then slide card out
296 await this.wait(150);
297 if (cardContainer) {
298 cardContainer.style.transform =
299 `translateY(-50%) translateX(${slideOutDirection})`;
300 }
301
302 // Wait for slide out to complete
303 await this.wait(300);
304
305 // Update content while off-screen
306 this.updateHash();
307 this.updateImage();
308 this.updateCardIndicator();
309 this.loadComponent();
310
311 // Position elements on the opposite side for slide in
312 if (contentContainer) {
313 contentContainer.style.transition = "none";
314 contentContainer.style.transform =
315 `translateY(-50%) translateX(${slideInDirection})`;
316 }
317 if (cardContainer) {
318 cardContainer.style.transition = "none";
319 cardContainer.style.transform =
320 `translateY(-50%) translateX(${slideInDirection})`;
321 }
322
323 // Small delay to ensure positioning is set
324 await this.wait(50);
325
326 // Phase 2: Slide card back in first
327 if (cardContainer) {
328 cardContainer.style.transition = "transform 0.6s ease-in-out";
329 cardContainer.style.transform = initialCardTransform;
330 }
331
332 // Wait 150ms, then slide content back in
333 await this.wait(150);
334 if (contentContainer) {
335 contentContainer.style.transition = "transform 0.6s ease-in-out";
336 contentContainer.style.transform = initialContentTransform;
337 }
338
339 // Wait for slide in to complete
340 await this.wait(400);
341
342 this.isTransitioning = false;
343 }
344
345 updateImage() {
346 const cardImage = document.getElementById("card-image");
347 if (!cardImage) return;
348
349 const currentCard = this.availableCards[this.currentCardIndex];
350 if (!currentCard) return;
351
352 // Handle special case for back card
353 if (currentCard.cardIndex === 22) {
354 cardImage.src = "/images/tarot/back.png";
355 cardImage.alt = "Card Back";
356 return;
357 }
358
359 // Set image source for current card (using PNG extension for pixel art)
360 const cardName = currentCard.name.toLowerCase().replace(/\s+/g, "-");
361 const newSrc = `/images/tarot/${currentCard.cardIndex}-${cardName}.png`;
362
363 // Update image source and alt text
364 cardImage.src = newSrc;
365 cardImage.alt = currentCard.name;
366 }
367
368 updateCardIndicator() {
369 const indicator = document.getElementById("current-card");
370 if (indicator) {
371 const currentCard = this.availableCards[this.currentCardIndex];
372 if (currentCard) {
373 indicator.textContent = currentCard.id;
374 }
375 }
376 }
377
378 loadComponent() {
379 // Hide all card content divs
380 document.querySelectorAll(".card-content").forEach((card) => {
381 card.classList.remove("active");
382 });
383
384 const currentCard = this.availableCards[this.currentCardIndex];
385 if (!currentCard) return;
386
387 // Update the content title
388 const contentTitle = document.getElementById("content-title");
389 if (contentTitle) {
390 if (currentCard.id != "back")
391 contentTitle.textContent = `${currentCard.id}: ${currentCard.name}`;
392 else
393 contentTitle.textContent = "About"
394 }
395
396 // Show the current card's content
397 const currentCardContent = document.getElementById(
398 `card-${currentCard.cardIndex}`,
399 );
400
401 if (currentCardContent) {
402 currentCardContent.classList.add("active");
403 }
404 }
405
406 wait(ms) {
407 return new Promise((resolve) => setTimeout(resolve, ms));
408 }
409}
410
411// Initialize when DOM is loaded
412document.addEventListener("DOMContentLoaded", () => {
413 new Tarot();
414});