Spaces:
Running
Running
🐳 16/06 - 16:42 - in the hidden room, we should offer something special. a discount code for etsy https://www.softbitestudio.etsy.com but there needs to be something else. build a 3 card tarot layout
4b61a38 verified | // ============================================ | |
| // BUNNY RECKLESS — Warren Explorer Scripts | |
| // ============================================ | |
| // Initialize Lucide Icons | |
| document.addEventListener('DOMContentLoaded', () => { | |
| lucide.createIcons(); | |
| initParticles(); | |
| initSplash(); | |
| initDoNotPress(); | |
| initCopyCode(); | |
| initTarot(); | |
| }); | |
| // ---- Splash Screen ---- | |
| function initSplash() { | |
| const splash = document.getElementById('splash'); | |
| const enterBtn = document.getElementById('enter-btn'); | |
| const mainContent = document.getElementById('main-content'); | |
| enterBtn.addEventListener('click', () => { | |
| splash.style.opacity = '0'; | |
| splash.style.transform = 'scale(1.1)'; | |
| splash.style.filter = 'blur(10px)'; | |
| setTimeout(() => { | |
| splash.style.display = 'none'; | |
| mainContent.style.opacity = '1'; | |
| }, 600); | |
| }); | |
| } | |
| // ---- Discovery System ---- | |
| const discovered = new Set(); | |
| const ROOMS = ['burrow', 'network', 'studio', 'lab', 'coven']; | |
| const ROOM_NAMES = { | |
| burrow: 'The Burrow', | |
| network: 'The Network', | |
| studio: 'The Studio', | |
| lab: 'The Lab', | |
| coven: 'The Coven', | |
| vault: 'The Vault 🔒' | |
| }; | |
| const ROOM_COLORS = { | |
| burrow: '#ff2d95', | |
| network: '#00e5ff', | |
| studio: '#b44dff', | |
| lab: '#22c55e', | |
| coven: '#eab308', | |
| vault: '#ef4444' | |
| }; | |
| const ROOM_ICONS = { | |
| burrow: '🏠', | |
| network: '🌐', | |
| studio: '🎨', | |
| lab: '🧪', | |
| coven: '✨', | |
| vault: '🔓' | |
| }; | |
| function toggleRoom(roomId) { | |
| const content = document.getElementById(`content-${roomId}`); | |
| const door = document.querySelector(`.room-door[data-room="${roomId}"]`); | |
| const chevron = door.querySelector('.door-chevron'); | |
| if (content.classList.contains('open')) { | |
| // Close | |
| content.classList.remove('open'); | |
| door.classList.remove('opened'); | |
| chevron.classList.remove('rotated'); | |
| } else { | |
| // Open | |
| content.classList.add('open'); | |
| door.classList.add('opened'); | |
| chevron.classList.add('rotated'); | |
| // Track discovery | |
| if (!discovered.has(roomId)) { | |
| discovered.add(roomId); | |
| updateProgress(); | |
| showToast(`Discovered ${ROOM_NAMES[roomId]}!`, ROOM_COLORS[roomId], ROOM_ICONS[roomId]); | |
| // Check if all rooms discovered | |
| if (discovered.size === ROOMS.length) { | |
| setTimeout(() => { | |
| unlockVault(); | |
| }, 1500); | |
| } | |
| } | |
| // Scroll to room | |
| setTimeout(() => { | |
| document.getElementById(`room-${roomId}`).scrollIntoView({ | |
| behavior: 'smooth', | |
| block: 'start' | |
| }); | |
| }, 200); | |
| } | |
| } | |
| function updateProgress() { | |
| const count = discovered.size; | |
| document.getElementById('discovered-count').textContent = count; | |
| // Update progress fill | |
| const percentage = (count / ROOMS.length) * 100; | |
| document.getElementById('progress-fill').style.width = `${percentage}%`; | |
| // Update dots | |
| discovered.forEach(room => { | |
| const dot = document.querySelector(`.progress-dot[data-room="${room}"]`); | |
| if (dot) dot.classList.add('discovered'); | |
| }); | |
| } | |
| function unlockVault() { | |
| const vault = document.getElementById('room-vault'); | |
| vault.classList.remove('hidden'); | |
| vault.classList.add('reveal-animate'); | |
| // Update vault dot | |
| const vaultDot = document.querySelector('.progress-dot.secret'); | |
| if (vaultDot) vaultDot.classList.add('discovered'); | |
| // Flash effect | |
| document.body.classList.add('flash-effect'); | |
| setTimeout(() => document.body.classList.remove('flash-effect'), 1000); | |
| showToast('SECRET ROOM UNLOCKED! 🔓', '#ef4444', '💀'); | |
| // Initialize tarot cards | |
| setTimeout(() => { | |
| initTarot(); | |
| }, 100); | |
| // Scroll to vault | |
| setTimeout(() => { | |
| vault.scrollIntoView({ behavior: 'smooth', block: 'start' }); | |
| }, 800); | |
| } | |
| // ---- Toast System ---- | |
| function showToast(message, color = '#ff2d95', icon = '✨') { | |
| const container = document.getElementById('toast-container'); | |
| const toast = document.createElement('div'); | |
| toast.className = 'toast'; | |
| toast.innerHTML = ` | |
| <span class="text-lg">${icon}</span> | |
| <span class="text-sm font-medium" style="color: ${color}">${message}</span> | |
| `; | |
| toast.style.borderLeftColor = color; | |
| toast.style.borderLeftWidth = '3px'; | |
| container.appendChild(toast); | |
| setTimeout(() => { | |
| toast.classList.add('toast-exit'); | |
| setTimeout(() => toast.remove(), 300); | |
| }, 3000); | |
| } | |
| // ---- DO NOT PRESS Button ---- | |
| function initDoNotPress() { | |
| const btn = document.getElementById('do-not-press-btn'); | |
| if (!btn) return; | |
| btn.addEventListener('click', (e) => { | |
| e.preventDefault(); | |
| // If vault already visible, just scroll | |
| const vault = document.getElementById('room-vault'); | |
| if (!vault.classList.contains('hidden')) { | |
| vault.scrollIntoView({ behavior: 'smooth' }); | |
| return; | |
| } | |
| // Open the lab room first if not open | |
| const labContent = document.getElementById('content-lab'); | |
| if (!labContent.classList.contains('open')) { | |
| toggleRoom('lab'); | |
| } | |
| // Unlock vault | |
| setTimeout(() => { | |
| unlockVault(); | |
| }, 500); | |
| }); | |
| } | |
| // ---- Copy Discount Code ---- | |
| function initCopyCode() { | |
| const btn = document.getElementById('copy-code-btn'); | |
| if (!btn) return; | |
| btn.addEventListener('click', () => { | |
| navigator.clipboard.writeText('BITEBACK').then(() => { | |
| const span = btn.querySelector('span'); | |
| const originalText = span.textContent; | |
| span.textContent = 'Copied!'; | |
| btn.style.borderColor = 'rgba(255, 45, 149, 0.6)'; | |
| setTimeout(() => { | |
| span.textContent = originalText; | |
| btn.style.borderColor = ''; | |
| }, 2000); | |
| showToast('Discount code copied! 🎉', '#ff2d95', '📋'); | |
| }).catch(() => { | |
| // Fallback for older browsers | |
| const textArea = document.createElement('textarea'); | |
| textArea.value = 'BITEBACK'; | |
| document.body.appendChild(textArea); | |
| textArea.select(); | |
| document.execCommand('copy'); | |
| document.body.removeChild(textArea); | |
| const span = btn.querySelector('span'); | |
| span.textContent = 'Copied!'; | |
| setTimeout(() => { span.textContent = 'Copy'; }, 2000); | |
| }); | |
| }); | |
| } | |
| // ---- Particle System ---- | |
| function initParticles() { | |
| const canvas = document.getElementById('particles'); | |
| const ctx = canvas.getContext('2d'); | |
| let width, height; | |
| const particles = []; | |
| const PARTICLE_COUNT = 40; | |
| function resize() { | |
| width = canvas.width = window.innerWidth; | |
| height = canvas.height = window.innerHeight; | |
| } | |
| resize(); | |
| window.addEventListener('resize', resize); | |
| class Particle { | |
| constructor() { | |
| this.reset(); | |
| } | |
| reset() { | |
| this.x = Math.random() * width; | |
| this.y = Math.random() * height; | |
| this.size = Math.random() * 2 + 0.5; | |
| this.speedX = (Math.random() - 0.5) * 0.3; | |
| this.speedY = (Math.random() - 0.5) * 0.3; | |
| this.opacity = Math.random() * 0.4 + 0.1; | |
| this.color = Math.random() > 0.5 ? '#ff2d95' : '#00e5ff'; | |
| } | |
| update() { | |
| this.x += this.speedX; | |
| this.y += this.speedY; | |
| if (this.x < 0 || this.x > width) this.speedX *= -1; | |
| if (this.y < 0 || this.y > height) this.speedY *= -1; | |
| } | |
| draw() { | |
| ctx.beginPath(); | |
| ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2); | |
| ctx.fillStyle = this.color; | |
| ctx.globalAlpha = this.opacity; | |
| ctx.fill(); | |
| ctx.globalAlpha = 1; | |
| } | |
| } | |
| for (let i = 0; i < PARTICLE_COUNT; i++) { | |
| particles.push(new Particle()); | |
| } | |
| function animate() { | |
| ctx.clearRect(0, 0, width, height); | |
| particles.forEach(p => { | |
| p.update(); | |
| p.draw(); | |
| }); | |
| // Draw connections | |
| for (let i = 0; i < particles.length; i++) { | |
| for (let j = i + 1; j < particles.length; j++) { | |
| const dx = particles[i].x - particles[j].x; | |
| const dy = particles[i].y - particles[j].y; | |
| const dist = Math.sqrt(dx * dx + dy * dy); | |
| if (dist < 150) { | |
| ctx.beginPath(); | |
| ctx.moveTo(particles[i].x, particles[i].y); | |
| ctx.lineTo(particles[j].x, particles[j].y); | |
| ctx.strokeStyle = particles[i].color; | |
| ctx.globalAlpha = 0.05 * (1 - dist / 150); | |
| ctx.lineWidth = 0.5; | |
| ctx.stroke(); | |
| ctx.globalAlpha = 1; | |
| } | |
| } | |
| } | |
| requestAnimationFrame(animate); | |
| } | |
| animate(); | |
| } | |
| // ---- Keyboard Easter Egg ---- | |
| let konamiCode = []; | |
| const KONAMI = ['ArrowUp', 'ArrowUp', 'ArrowDown', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'ArrowLeft', 'ArrowRight', 'b', 'a']; | |
| document.addEventListener('keydown', (e) => { | |
| konamiCode.push(e.key); | |
| konamiCode = konamiCode.slice(-10); | |
| if (konamiCode.join(',') === KONAMI.join(',')) { | |
| const vault = document.getElementById('room-vault'); | |
| if (vault.classList.contains('hidden')) { | |
| unlockVault(); | |
| } else { | |
| vault.scrollIntoView({ behavior: 'smooth' }); | |
| } | |
| showToast('KONAMI CODE ACTIVATED! 🎮', '#eab308', '🎮'); | |
| } | |
| }); | |
| // ---- Scroll-triggered door glow ---- | |
| const observerOptions = { | |
| threshold: 0.2, | |
| rootMargin: '0px' | |
| }; | |
| const observer = new IntersectionObserver((entries) => { | |
| entries.forEach(entry => { | |
| if (entry.isIntersecting) { | |
| const door = entry.target.querySelector('.room-door'); | |
| if (door && !door.classList.contains('opened')) { | |
| door.style.animation = 'none'; | |
| door.offsetHeight; // Trigger reflow | |
| } | |
| } | |
| }); | |
| }, observerOptions); | |
| document.querySelectorAll('[id^="room-"]').forEach(room => { | |
| observer.observe(room); | |
| }); | |
| // ---- Subscribe Button ---- | |
| document.addEventListener('click', (e) => { | |
| if (e.target.closest('button') && e.target.closest('button').textContent.includes('Subscribe')) { | |
| const input = e.target.closest('div').querySelector('input[type="email"]'); | |
| if (input && input.value && input.value.includes('@')) { | |
| showToast('Welcome to the Fluff Coven! ✐꩙', '#ff2d95', '🐰'); | |
| input.value = ''; | |
| input.placeholder = 'You\'re in! 🎉'; | |
| } else { | |
| showToast('Please enter a valid email', '#ef4444', '⚠️'); | |
| input.focus(); | |
| } | |
| } | |
| }); | |
| // ---- Tarot Cards ---- | |
| const TAROT_PRIZES = [ | |
| { | |
| name: 'The Moon', | |
| symbol: '☽', | |
| prize: 'discount', | |
| title: '20% Off Etsy', | |
| description: 'A discount for the misfits & dreamers', | |
| code: 'BITEBACK', | |
| link: 'https://softbitestudio.etsy.com/', | |
| linkText: 'Visit Shop →', | |
| color: '#ff2d95', | |
| revealIcon: '🌙' | |
| }, | |
| { | |
| name: 'The Star', | |
| symbol: '★', | |
| prize: 'download', | |
| title: 'Free Digital Download', | |
| description: 'A gift from the warren, just for you', | |
| code: null, | |
| link: 'https://softbitestudio.etsy.com/', | |
| linkText: 'Claim Your Gift →', | |
| color: '#00e5ff', | |
| revealIcon: '⭐' | |
| }, | |
| { | |
| name: 'The Tower', | |
| symbol: '⚡', | |
| prize: 'secret', | |
| title: 'Secret Code', | |
| description: 'Whisper this to unlock hidden paths', | |
| code: 'FLUFFN FANGS', | |
| link: null, | |
| linkText: null, | |
| color: '#b44dff', | |
| revealIcon: '🔓' | |
| } | |
| ]; | |
| let shuffledCards = []; | |
| let tarotInitialized = false; | |
| let fortuneDrawn = false; | |
| function initTarot() { | |
| if (tarotInitialized) return; | |
| tarotInitialized = true; | |
| const container = document.getElementById('tarot-cards'); | |
| const sealedDiv = document.getElementById('fortune-sealed'); | |
| if (!container) return; | |
| // Check localStorage | |
| const savedFortune = localStorage.getItem('warren_fortune'); | |
| if (savedFortune) { | |
| fortuneDrawn = true; | |
| try { | |
| const fortune = JSON.parse(savedFortune); | |
| renderCards(container, true); | |
| sealedDiv.classList.remove('hidden'); | |
| showPreviousFortune(fortune); | |
| } catch (e) { | |
| localStorage.removeItem('warren_fortune'); | |
| fortuneDrawn = false; | |
| } | |
| } | |
| if (!fortuneDrawn) { | |
| shuffledCards = [...TAROT_PRIZES].sort(() => Math.random() - 0.5); | |
| renderCards(container, false); | |
| container.addEventListener('click', (e) => { | |
| const card = e.target.closest('.tarot-card'); | |
| if (card && !card.classList.contains('revealed') && !card.classList.contains('dimmed') && !fortuneDrawn) { | |
| flipCard(parseInt(card.dataset.index)); | |
| } | |
| }); | |
| } | |
| } | |
| function renderCards(container, sealed) { | |
| const cards = sealed ? TAROT_PRIZES : shuffledCards; | |
| container.innerHTML = cards.map((card, i) => ` | |
| <div class="tarot-card ${sealed ? 'dimmed' : ''}" data-index="${i}" style="animation-delay: ${i * 0.3}s"> | |
| <div class="tarot-card-inner"> | |
| <div class="tarot-card-front"> | |
| <div class="tarot-card-inner-border"></div> | |
| <div class="tarot-card-symbol">${card.symbol}</div> | |
| <div class="tarot-card-divider">⟡</div> | |
| <div class="tarot-card-label">${card.name}</div> | |
| </div> | |
| <div class="tarot-card-back" style="border: 1px solid ${card.color}40"> | |
| <div class="tarot-prize-icon">${card.revealIcon}</div> | |
| <div class="tarot-prize-title" style="color: ${card.color}">${card.title}</div> | |
| <div class="tarot-prize-desc">${card.description}</div> | |
| ${card.code ? `<div class="tarot-prize-code" onclick="event.stopPropagation(); copyTarotCode('${card.code}')">${card.code}</div>` : ''} | |
| ${card.link ? `<a href="${card.link}" target="_blank" class="tarot-prize-link" onclick="event.stopPropagation()">${card.linkText}</a>` : ''} | |
| </div> | |
| </div> | |
| </div> | |
| `).join(''); | |
| } | |
| function flipCard(index) { | |
| if (fortuneDrawn) return; | |
| fortuneDrawn = true; | |
| const card = document.querySelector(`.tarot-card[data-index="${index}"]`); | |
| const prize = shuffledCards[index]; | |
| // Add flash overlay | |
| const flash = document.createElement('div'); | |
| flash.className = 'tarot-flash'; | |
| card.appendChild(flash); | |
| // Flip the card | |
| card.classList.add('revealed'); | |
| card.style.animation = 'none'; | |
| // Particle burst | |
| setTimeout(() => { | |
| createParticleBurst(card); | |
| }, 250); | |
| // Dim other cards | |
| document.querySelectorAll('.tarot-card').forEach((c, i) => { | |
| if (i !== index) { | |
| c.classList.add('dimmed'); | |
| c.style.animation = 'none'; | |
| } | |
| }); | |
| // Save to localStorage | |
| localStorage.setItem('warren_fortune', JSON.stringify({ | |
| ...prize, | |
| timestamp: Date.now() | |
| })); | |
| // Toast | |
| showToast(`Fortune revealed: ${prize.title}!`, prize.color, prize.revealIcon); | |
| // Clean up flash | |
| setTimeout(() => flash.remove(), 800); | |
| } | |
| function createParticleBurst(element) { | |
| const rect = element.getBoundingClientRect(); | |
| const centerX = rect.left + rect.width / 2; | |
| const centerY = rect.top + rect.height / 2; | |
| const colors = ['#ff2d95', '#b44dff', '#00e5ff', '#eab308', '#ffffff']; | |
| for (let i = 0; i < 35; i++) { | |
| const particle = document.createElement('div'); | |
| particle.className = 'tarot-particle'; | |
| particle.style.left = centerX + 'px'; | |
| particle.style.top = centerY + 'px'; | |
| const angle = Math.random() * Math.PI * 2; | |
| const distance = Math.random() * 140 + 30; | |
| const dx = Math.cos(angle) * distance; | |
| const dy = Math.sin(angle) * distance; | |
| particle.style.setProperty('--dx', dx + 'px'); | |
| particle.style.setProperty('--dy', dy + 'px'); | |
| particle.style.backgroundColor = colors[Math.floor(Math.random() * colors.length)]; | |
| const size = Math.random() * 5 + 2; | |
| particle.style.width = size + 'px'; | |
| particle.style.height = size + 'px'; | |
| particle.style.animationDuration = (Math.random() * 0.5 + 0.7) + 's'; | |
| document.body.appendChild(particle); | |
| particle.addEventListener('animationend', () => particle.remove()); | |
| } | |
| } | |
| function copyTarotCode(code) { | |
| navigator.clipboard.writeText(code).then(() => { | |
| showToast(`Code "${code}" copied! 🎉`, '#ff2d95', '📋'); | |
| }).catch(() => { | |
| const textArea = document.createElement('textarea'); | |
| textArea.value = code; | |
| document.body.appendChild(textArea); | |
| textArea.select(); | |
| document.execCommand('copy'); | |
| document.body.removeChild(textArea); | |
| showToast(`Code "${code}" copied! 🎉`, '#ff2d95', '📋'); | |
| }); | |
| } | |
| function showPreviousFortune(fortune) { | |
| const prevFortune = document.getElementById('tarot-prev-fortune'); | |
| if (!prevFortune) return; | |
| prevFortune.innerHTML = ` | |
| <div style="font-size: 1.5rem; margin-bottom: 0.4rem;">${fortune.revealIcon}</div> | |
| <div style="color: ${fortune.color}; font-family: 'Syne', sans-serif; font-weight: 700; margin-bottom: 0.3rem;">${fortune.title}</div> | |
| <div style="font-size: 0.7rem; color: #6b6b8a; margin-bottom: 0.5rem;">${fortune.description}</div> | |
| ${fortune.code ? `<div style="font-family: 'Space Mono', monospace; font-size: 0.75rem; color: #ff2d95; cursor: pointer;" onclick="copyTarotCode('${fortune.code}')">${fortune.code}</div>` : ''} | |
| ${fortune.link ? `<a href="${fortune.link}" target="_blank" style="font-size: 0.65rem; color: #b44dff; text-decoration: underline;">${fortune.linkText}</a>` : ''} | |
| `; | |
| } | |
| // ---- Tarot Fortune System ---- | |
| const FORTUNES = [ | |
| { | |
| id: 'discount', | |
| icon: '🌟', | |
| label: 'Discount Code', | |
| value: 'SOFTBITE15', | |
| desc: '15% off at Soft Bite Studio on Etsy', | |
| color: '#ff2d95' | |
| }, | |
| { | |
| id: 'download', | |
| icon: '🌙', | |
| label: 'Digital Download', | |
| value: 'ART PACK UNLOCKED', | |
| desc: 'A free collection of digital art & stickers', | |
| color: '#00e5ff' | |
| }, | |
| { | |
| id: 'secret', | |
| icon: '☀️', | |
| label: 'Secret Code', | |
| value: 'FLUFF4EVER', | |
| desc: 'A cipher for the initiated... keep it close', | |
| color: '#b44dff' | |
| } | |
| ]; | |
| let tarotFlipped = false; | |
| let shuffledFortunes = []; | |
| function initTarot() { | |
| const savedFortune = localStorage.getItem('warren_fortune'); | |
| if (savedFortune) { | |
| showSealedFortune(savedFortune); | |
| return; | |
| } | |
| // Shuffle fortunes randomly | |
| shuffledFortunes = [...FORTUNES].sort(() => Math.random() - 0.5); | |
| // Populate card backs with fortune data | |
| shuffledFortunes.forEach((fortune, i) => { | |
| document.getElementById(`fortune-icon-${i}`).textContent = fortune.icon; | |
| document.getElementById(`fortune-label-${i}`).textContent = fortune.label; | |
| const valueEl = document.getElementById(`fortune-value-${i}`); | |
| valueEl.textContent = fortune.value; | |
| valueEl.style.color = fortune.color; | |
| document.getElementById(`fortune-desc-${i}`).textContent = fortune.desc; | |
| }); | |
| } | |
| function showSealedFortune(fortuneId) { | |
| const fortune = FORTUNES.find(f => f.id === fortuneId); | |
| if (!fortune) return; | |
| const cardsContainer = document.getElementById('tarot-cards'); | |
| const sealedContainer = document.getElementById('fortune-sealed'); | |
| const etsyLink = document.getElementById('tarot-etsy-link'); | |
| if (cardsContainer) cardsContainer.classList.add('hidden'); | |
| if (sealedContainer) sealedContainer.classList.remove('hidden'); | |
| if (etsyLink) etsyLink.classList.remove('hidden'); | |
| const display = document.getElementById('sealed-fortune-display'); | |
| if (display) { | |
| display.innerHTML = ` | |
| <div class="text-center"> | |
| <div class="text-5xl mb-4">${fortune.icon}</div> | |
| <div class="text-xs font-mono uppercase tracking-[0.2em] mb-2" style="color: ${fortune.color}">${fortune.label}</div> | |
| <div class="text-2xl font-display font-bold mb-2" style="color: ${fortune.color}">${fortune.value}</div> | |
| <div class="text-warren-muted text-sm">${fortune.desc}</div> | |
| <button onclick="copyFortuneCode('${fortune.value}')" class="mt-4 px-4 py-2 bg-warren-pink/20 border border-warren-pink/30 rounded-lg text-warren-pink text-xs font-mono hover:bg-warren-pink/30 transition-all inline-flex items-center gap-1.5"> | |
| <i data-lucide="copy" class="w-3.5 h-3.5"></i> | |
| <span>Copy</span> | |
| </button> | |
| </div> | |
| `; | |
| lucide.createIcons(); | |
| } | |
| } | |
| function flipCard(index) { | |
| if (tarotFlipped) return; | |
| // Double-check localStorage | |
| if (localStorage.getItem('warren_fortune')) { | |
| showSealedFortune(localStorage.getItem('warren_fortune')); | |
| return; | |
| } | |
| tarotFlipped = true; | |
| const fortune = shuffledFortunes[index]; | |
| // Flip animation | |
| const cards = document.querySelectorAll('.tarot-card'); | |
| cards[index].classList.add('flipped'); | |
| // Flash effect | |
| const flash = document.createElement('div'); | |
| flash.className = 'tarot-flash'; | |
| cards[index].closest('.tarot-card-wrapper').appendChild(flash); | |
| setTimeout(() => flash.remove(), 700); | |
| // Particle burst | |
| spawnTarotParticles(index); | |
| // Save to localStorage | |
| localStorage.setItem('warren_fortune', fortune.id); | |
| // Dim other cards | |
| document.querySelectorAll('.tarot-card-wrapper').forEach((wrapper, i) => { | |
| if (i !== index) { | |
| wrapper.style.opacity = '0.2'; | |
| wrapper.style.pointerEvents = 'none'; | |
| wrapper.style.transition = 'all 0.8s ease'; | |
| wrapper.style.filter = 'grayscale(1)'; | |
| } | |
| }); | |
| // Show Etsy link after a delay | |
| setTimeout(() => { | |
| const etsyLink = document.getElementById('tarot-etsy-link'); | |
| if (etsyLink) { | |
| etsyLink.classList.remove('hidden'); | |
| etsyLink.style.animation = 'reveal-bounce 0.5s cubic-bezier(0.16, 1, 0.3, 1)'; | |
| } | |
| lucide.createIcons(); | |
| }, 900); | |
| // Toast | |
| setTimeout(() => { | |
| showToast(`Fortune revealed: ${fortune.value}`, fortune.color, fortune.icon); | |
| }, 600); | |
| } | |
| function spawnTarotParticles(cardIndex) { | |
| const wrapper = document.querySelectorAll('.tarot-card-wrapper')[cardIndex]; | |
| const rect = wrapper.getBoundingClientRect(); | |
| const cx = rect.left + rect.width / 2; | |
| const cy = rect.top + rect.height / 2; | |
| const canvas = document.createElement('canvas'); | |
| canvas.style.cssText = 'position:fixed;inset:0;pointer-events:none;z-index:100;'; | |
| canvas.width = window.innerWidth; | |
| canvas.height = window.innerHeight; | |
| document.body.appendChild(canvas); | |
| const ctx = canvas.getContext('2d'); | |
| const particles = []; | |
| const COLORS = ['#ff2d95', '#b44dff', '#00e5ff', '#ffffff', '#ffd700']; | |
| // Main burst particles | |
| for (let i = 0; i < 80; i++) { | |
| const angle = (Math.PI * 2 / 80) * i + (Math.random() - 0.5) * 0.8; | |
| const speed = Math.random() * 8 + 2; | |
| particles.push({ | |
| x: cx, | |
| y: cy, | |
| vx: Math.cos(angle) * speed, | |
| vy: Math.sin(angle) * speed, | |
| size: Math.random() * 3.5 + 1, | |
| color: COLORS[Math.floor(Math.random() * COLORS.length)], | |
| opacity: 1, | |
| decay: Math.random() * 0.015 + 0.008, | |
| gravity: 0.08 | |
| }); | |
| } | |
| // Bigger sparkle particles | |
| for (let i = 0; i < 15; i++) { | |
| const angle = Math.random() * Math.PI * 2; | |
| const speed = Math.random() * 4 + 1; | |
| particles.push({ | |
| x: cx, | |
| y: cy, | |
| vx: Math.cos(angle) * speed, | |
| vy: Math.sin(angle) * speed, | |
| size: Math.random() * 6 + 3, | |
| color: '#ffffff', | |
| opacity: 0.8, | |
| decay: Math.random() * 0.02 + 0.015, | |
| gravity: 0.02 | |
| }); | |
| } | |
| function animate() { | |
| ctx.clearRect(0, 0, canvas.width, canvas.height); | |
| let alive = false; | |
| particles.forEach(p => { | |
| if (p.opacity <= 0) return; | |
| alive = true; | |
| p.x += p.vx; | |
| p.y += p.vy; | |
| p.vy += p.gravity; | |
| p.vx *= 0.98; | |
| p.opacity -= p.decay; | |
| // Glow | |
| ctx.beginPath(); | |
| ctx.arc(p.x, p.y, p.size * 4, 0, Math.PI * 2); | |
| ctx.fillStyle = p.color; | |
| ctx.globalAlpha = Math.max(0, p.opacity * 0.1); | |
| ctx.fill(); | |
| // Core | |
| ctx.beginPath(); | |
| ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2); | |
| ctx.fillStyle = p.color; | |
| ctx.globalAlpha = Math.max(0, p.opacity); | |
| ctx.fill(); | |
| }); | |
| ctx.globalAlpha = 1; | |
| if (alive) { | |
| requestAnimationFrame(animate); | |
| } else { | |
| canvas.remove(); | |
| } | |
| } | |
| animate(); | |
| } | |
| function copyFortuneCode(code) { | |
| navigator.clipboard.writeText(code).then(() => { | |
| showToast('Code copied! 🎉', '#ff2d95', '📋'); | |
| }).catch(() => { | |
| const ta = document.createElement('textarea'); | |
| ta.value = code; | |
| document.body.appendChild(ta); | |
| ta.select(); | |
| document.execCommand('copy'); | |
| document.body.removeChild(ta); | |
| showToast('Code copied! 🎉', '#ff2d95', '📋'); | |
| }); | |
| } |