timconspicuous.neocities.org
at main 414 lines 11 kB view raw
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});