Spaces:
Running
Running
| // ChronoBirth Magic - Interactive Birth Date Form | |
| // ==================== CONFIGURATION ==================== | |
| const CONFIG = { | |
| zodiacSigns: [ | |
| { name: 'Capricorn', symbol: 'β', element: 'Earth', start: [12, 22], end: [1, 19] }, | |
| { name: 'Aquarius', symbol: 'β', element: 'Air', start: [1, 20], end: [2, 18] }, | |
| { name: 'Pisces', symbol: 'β', element: 'Water', start: [2, 19], end: [3, 20] }, | |
| { name: 'Aries', symbol: 'β', element: 'Fire', start: [3, 21], end: [4, 19] }, | |
| { name: 'Taurus', symbol: 'β', element: 'Earth', start: [4, 20], end: [5, 20] }, | |
| { name: 'Gemini', symbol: 'β', element: 'Air', start: [5, 21], end: [6, 20] }, | |
| { name: 'Cancer', symbol: 'β', element: 'Water', start: [6, 21], end: [7, 22] }, | |
| { name: 'Leo', symbol: 'β', element: 'Fire', start: [7, 23], end: [8, 22] }, | |
| { name: 'Virgo', symbol: 'β', element: 'Earth', start: [8, 23], end: [9, 22] }, | |
| { name: 'Libra', symbol: 'β', element: 'Air', start: [9, 23], end: [10, 22] }, | |
| { name: 'Scorpio', symbol: 'β', element: 'Water', start: [10, 23], end: [11, 21] }, | |
| { name: 'Sagittarius', symbol: 'β', element: 'Fire', start: [11, 22], end: [12, 21] } | |
| ], | |
| seasons: [ | |
| { name: 'Spring', start: [3, 20], end: [6, 20], icon: 'πΈ' }, | |
| { name: 'Summer', start: [6, 21], end: [9, 22], icon: 'βοΈ' }, | |
| { name: 'Autumn', start: [9, 23], end: [12, 20], icon: 'π' }, | |
| { name: 'Winter', start: [12, 21], end: [3, 19], icon: 'βοΈ' } | |
| ], | |
| dayNames: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'] | |
| }; | |
| // ==================== STATE ==================== | |
| let currentStep = 1; | |
| const totalSteps = 3; | |
| let birthData = { | |
| day: null, | |
| month: null, | |
| year: null, | |
| time: null, | |
| timezone: 'UTC' | |
| }; | |
| // ==================== DOM ELEMENTS ==================== | |
| const elements = { | |
| form: document.getElementById('birthForm'), | |
| formCard: document.getElementById('birthFormCard'), | |
| resultsCard: document.getElementById('resultsCard'), | |
| prevBtn: document.getElementById('prevBtn'), | |
| nextBtn: document.getElementById('nextBtn'), | |
| submitBtn: document.getElementById('submitBtn'), | |
| themeToggle: document.getElementById('themeToggle'), | |
| toast: document.getElementById('toast'), | |
| confettiCanvas: document.getElementById('confettiCanvas') | |
| }; | |
| // ==================== INITIALIZATION ==================== | |
| document.addEventListener('DOMContentLoaded', () => { | |
| initializeEventListeners(); | |
| initializeTheme(); | |
| updateStepIndicators(); | |
| feather.replace(); | |
| }); | |
| // ==================== EVENT LISTENERS ==================== | |
| function initializeEventListeners() { | |
| // Navigation | |
| elements.nextBtn.addEventListener('click', nextStep); | |
| elements.prevBtn.addEventListener('click', prevStep); | |
| elements.form.addEventListener('submit', handleSubmit); | |
| // Theme | |
| elements.themeToggle.addEventListener('click', toggleTheme); | |
| // Preset buttons | |
| document.querySelectorAll('.preset-btn').forEach(btn => { | |
| btn.addEventListener('click', () => applyPreset(btn.dataset.preset)); | |
| }); | |
| // Input validation and auto-advance | |
| ['day', 'month', 'year'].forEach(id => { | |
| const input = document.getElementById(id); | |
| input.addEventListener('input', handleDateInput); | |
| input.addEventListener('blur', validateDate); | |
| }); | |
| // Reset | |
| document.getElementById('resetBtn').addEventListener('click', resetForm); | |
| // Share & Save | |
| document.getElementById('shareBtn').addEventListener('click', shareResults); | |
| document.getElementById('saveBtn').addEventListener('click', saveResults); | |
| } | |
| // ==================== THEME ==================== | |
| function initializeTheme() { | |
| const savedTheme = localStorage.getItem('chronobirth-theme'); | |
| const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; | |
| if (savedTheme === 'dark' || (!savedTheme && prefersDark)) { | |
| document.documentElement.classList.add('dark'); | |
| } | |
| } | |
| function toggleTheme() { | |
| const isDark = document.documentElement.classList.toggle('dark'); | |
| localStorage.setItem('chronobirth-theme', isDark ? 'dark' : 'light'); | |
| // Animate icon | |
| const icon = elements.themeToggle.querySelector('i'); | |
| icon.style.transform = 'rotate(360deg)'; | |
| setTimeout(() => { | |
| icon.style.transform = ''; | |
| feather.replace(); | |
| }, 300); | |
| } | |
| // ==================== STEP NAVIGATION ==================== | |
| function nextStep() { | |
| if (!validateCurrentStep()) return; | |
| if (currentStep < totalSteps) { | |
| goToStep(currentStep + 1); | |
| } | |
| } | |
| function prevStep() { | |
| if (currentStep > 1) { | |
| goToStep(currentStep - 1); | |
| } | |
| } | |
| function goToStep(step) { | |
| // Update form steps | |
| document.querySelectorAll('.form-step').forEach((el, i) => { | |
| el.classList.remove('active', 'prev'); | |
| if (i + 1 === step) { | |
| el.classList.add('active'); | |
| } else if (i + 1 < step) { | |
| el.classList.add('prev'); | |
| } | |
| }); | |
| currentStep = step; | |
| updateStepIndicators(); | |
| updateNavigationButtons(); | |
| // Update preview on step 3 | |
| if (step === 3) { | |
| updateZodiacPreview(); | |
| } | |
| } | |
| function updateStepIndicators() { | |
| document.querySelectorAll('.step-indicator').forEach((el, i) => { | |
| el.classList.remove('active', 'completed'); | |
| if (i + 1 === currentStep) { | |
| el.classList.add('active'); | |
| } else if (i + 1 < currentStep) { | |
| el.classList.add('completed'); | |
| el.querySelector('.step-number').innerHTML = '<i data-feather="check" class="w-4 h-4"></i>'; | |
| feather.replace(); | |
| } else { | |
| el.querySelector('.step-number').textContent = i + 1; | |
| } | |
| }); | |
| } | |
| function updateNavigationButtons() { | |
| elements.prevBtn.classList.toggle('hidden', currentStep === 1); | |
| elements.nextBtn.classList.toggle('hidden', currentStep === totalSteps); | |
| elements.submitBtn.classList.toggle('hidden', currentStep !== totalSteps); | |
| } | |
| function validateCurrentStep() { | |
| const currentStepEl = document.querySelector(`.form-step[data-step="${currentStep}"]`); | |
| const inputs = currentStepEl.querySelectorAll('input, select'); | |
| let isValid = true; | |
| inputs.forEach(input => { | |
| if (!input.checkValidity()) { | |
| input.reportValidity(); | |
| isValid = false; | |
| } | |
| }); | |
| return isValid; | |
| } | |
| // ==================== INPUT HANDLING ==================== | |
| function handleDateInput(e) { | |
| const input = e.target; | |
| const value = input.value; | |
| // Auto-advance for day and month | |
| if (input.id === 'day' && value.length === 2) { | |
| document.getElementById('month').focus(); | |
| } else if (input.id === 'year' && value.length === 4) { | |
| document.getElementById('time').focus(); | |
| } | |
| // Store data | |
| birthData[input.id] = input.id === 'month' ? parseInt(value) : value; | |
| } | |
| function validateDate() { | |
| const day = parseInt(document.getElementById('day').value); | |
| const month = parseInt(document.getElementById('month').value); | |
| const year = parseInt(document.getElementById('year').value); | |
| if (!day || !month || !year) return; | |
| const date = new Date(year, month - 1, day); | |
| const isValid = date.getDate() === day && date.getMonth() === month - 1; | |
| if (!isValid) { | |
| showToast('Please enter a valid date!', 'error'); | |
| } | |
| } | |
| function applyPreset(preset) { | |
| const now = new Date(); | |
| const year = now.getFullYear(); | |
| const presets = { | |
| today: { day: now.getDate(), month: now.getMonth() + 1, year: year }, | |
| newyear: { day: 1, month: 1, year: year }, | |
| leap: { day: 29, month: 2, year: 2020 }, | |
| halloween: { day: 31, month: 10, year: year } | |
| }; | |
| const data = presets[preset]; | |
| if (data) { | |
| document.getElementById('day').value = data.day.toString().padStart(2, '0'); | |
| document.getElementById('month').value = data.month; | |
| document.getElementById('year').value = data.year; | |
| birthData.day = data.day; | |
| birthData.month = data.month; | |
| birthData.year = data.year; | |
| // Visual feedback | |
| document.querySelectorAll('.preset-btn').forEach(btn => { | |
| btn.style.transform = btn.dataset.preset === preset ? 'scale(1.05)' : ''; | |
| btn.style.background = btn.dataset.preset === preset ? '#fef2f2' : ''; | |
| }); | |
| } | |
| } | |
| // ==================== ZODIAC & CALCULATIONS ==================== | |
| function getZodiacSign(day, month) { | |
| for (const sign of CONFIG.zodiacSigns) { | |
| const [startMonth, startDay] = sign.start; | |
| const [endMonth, endDay] = sign.end; | |
| if ((month === startMonth && day >= startDay) || | |
| (month === endMonth && day <= endDay) || | |
| (startMonth > endMonth && (month === startMonth && day >= startDay || month === endMonth && day <= endDay))) { | |
| return sign; | |
| } | |
| } | |
| return CONFIG.zodiacSigns[0]; | |
| } | |
| function getSeason(day, month) { | |
| for (const season of CONFIG.seasons) { | |
| const [startMonth, startDay] = season.start; | |
| const [endMonth, endDay] = season.end; | |
| const dateNum = month * 100 + day; | |
| const startNum = startMonth * 100 + startDay; | |
| const endNum = endMonth * 100 + endDay; | |
| if (startNum > endNum) { | |
| // Winter spans year boundary | |
| if (dateNum >= startNum || dateNum <= endNum) return season; | |
| } else { | |
| if (dateNum >= startNum && dateNum <= endNum) return season; | |
| } | |
| } | |
| return CONFIG.seasons[3]; // Default to winter | |
| } | |
| function calculateAge(birthDate) { | |
| const now = new Date(); | |
| const diff = now - birthDate; | |
| const years = Math.floor(diff / (365.25 * 24 * 60 * 60 * 1000)); | |
| const months = Math.floor((diff % (365.25 * 24 * 60 * 60 * 1000)) / (30.44 * 24 * 60 * 60 * 1000)); | |
| const days = Math.floor((diff % (30.44 * 24 * 60 * 60 * 1000)) / (24 * 60 * 60 * 1000)); | |
| const hours = Math.floor((diff % (24 * 60 * 60 * 1000)) / (60 * 60 * 1000)); | |
| return { years, months, days, hours, totalDays: Math.floor(diff / (24 * 60 * 60 * 1000)) }; | |
| } | |
| function getNextBirthday(birthDate) { | |
| const now = new Date(); | |
| const currentYear = now.getFullYear(); | |
| let nextBirthday = new Date(currentYear, birthDate.getMonth(), birthDate.getDate()); | |
| if (nextBirthday < now) { | |
| nextBirthday.setFullYear(currentYear + 1); | |
| } | |
| const daysUntil = Math.ceil((nextBirthday - now) / (24 * 60 * 60 * 1000)); | |
| return { date: nextBirthday, daysUntil }; | |
| } | |
| function updateZodiacPreview() { | |
| const day = parseInt(document.getElementById('day').value); | |
| const month = parseInt(document.getElementById('month').value); | |
| const year = parseInt(document.getElementById('year').value); | |
| if (!day || !month || !year) return; | |
| const zodiac = getZodiacSign(day, month); | |
| const birthDate = new Date(year, month - 1, day); | |
| const dayOfWeek = CONFIG.dayNames[birthDate.getDay()]; | |
| const age = calculateAge(birthDate); | |
| // Update preview | |
| document.getElementById('zodiacIcon').textContent = zodiac.symbol; | |
| document.getElementById('zodiacName').textContent = zodiac.name; | |
| document.getElementById('zodiacDates').textContent = `${zodiac.element} Element β’ ${zodiac.start[0]}/${zodiac.start[1]} - ${zodiac.end[0]}/${zodiac.end[1]}`; | |
| document.getElementById('dayOfWeek').textContent = dayOfWeek; | |
| document.getElementById('agePreview').textContent = `${age.years} years`; | |
| // Animate | |
| const preview = document.getElementById('zodiacPreview'); | |
| preview.style.opacity = '1'; | |
| preview.style.transform = 'scale(1.02)'; | |
| } | |
| // ==================== FORM SUBMISSION ==================== | |
| function handleSubmit(e) { | |
| e.preventDefault(); | |
| // Gather all data | |
| const day = parseInt(document.getElementById('day').value); | |
| const month = parseInt(document.getElementById('month').value); | |
| const year = parseInt(document.getElementById('year').value); | |
| const time = document.getElementById('time').value; | |
| const timezone = document.getElementById('timezone').value; | |
| const birthDate = new Date(year, month - 1, day); | |
| const zodiac = getZodiacSign(day, month); | |
| const season = getSeason(day, month); | |
| const age = calculateAge(birthDate); | |
| const nextBirthday = getNextBirthday(birthDate); | |
| // Calculate life progress (assuming 80 years) | |
| const lifeProgress = Math.min((age.years / 80) * 100, 100); | |
| // Display results | |
| displayResults({ | |
| birthDate, | |
| day, month, year, time, timezone, | |
| zodiac, | |
| season, | |
| age, | |
| nextBirthday, | |
| lifeProgress, | |
| dayOfWeek: CONFIG.dayNames[birthDate.getDay()] | |
| }); | |
| } | |
| function displayResults(data) { | |
| // Hide form, show results | |
| elements.formCard.style.display = 'none'; | |
| elements.resultsCard.classList.remove('hidden'); | |
| // Populate results | |
| document.getElementById('resultZodiacIcon').textContent = data.zodiac.symbol; | |
| document.getElementById('resultDate').textContent = `${data.dayOfWeek}, ${data.month}/${data.day}/${data.year}`; | |
| document.getElementById('resultZodiac').textContent = data.zodiac.name; | |
| document.getElementById('resultZodiacElement').textContent = `${data.zodiac.element} Element`; | |
| document.getElementById('resultAge').textContent = `${data.age.years} years`; | |
| document.getElementById('resultAgeDetailed').textContent = `${data.age.months}m ${data.age.days}d ${data.age.hours}h old`; | |
| document.getElementById('resultDayName').textContent = data.dayOfWeek; | |
| document.getElementById('resultDayType').textContent = getDayType(data.dayOfWeek); | |
| document.getElementById('resultNextBirthday').textContent = data.nextBirthday.date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); | |
| document.getElementById('resultDaysUntil').textContent = `${data.nextBirthday.daysUntil} days until party time!`; | |
| document.getElementById('resultSeason').textContent = `${data.season.icon} ${data.season.name}`; | |
| document.getElementById('resultSeasonDates').textContent = getSeasonPeriod(data.season); | |
| // Animate progress bar | |
| setTimeout(() => { | |
| document.getElementById('lifeProgressBar').style.width = `${data.lifeProgress}%`; | |
| }, 300); | |
| document.getElementById('resultLifeProgress').textContent = `${data.lifeProgress.toFixed(1)}% of estimated lifespan`; | |
| // Generate fun facts | |
| generateFunFacts(data); | |
| // Trigger confetti | |
| triggerConfetti(); | |
| // Scroll to top | |
| window.scrollTo({ top: 0, behavior: 'smooth' }); | |
| } | |
| function getDayType(day) { | |
| const types = { | |
| 'Monday': 'Moon Day π', | |
| 'Tuesday': 'Mars Day βοΈ', | |
| 'Wednesday': 'Mercury Day βΏοΈ', | |
| 'Thursday': 'Jupiter Day β', | |
| 'Friday': 'Venus Day βοΈ', | |
| 'Saturday': 'Saturn Day β', | |
| 'Sunday': 'Sun Day β' | |
| }; | |
| return types[day] || 'Special Day'; | |
| } | |
| function getSeasonPeriod(season) { | |
| const [startM, startD] = season.start; | |
| const [endM, endD] = season.end; | |
| return `${startM}/${startD} - ${endM}/${endD}`; | |
| } | |
| function generateFunFacts(data) { | |
| const facts = [ | |
| `You've been alive for approximately ${data.age.totalDays.toLocaleString()} days`, | |
| `Your heart has beaten about ${(data.age.totalDays * 115000).toLocaleString()} times`, | |
| `You've taken roughly ${(data.age.totalDays * 23000).toLocaleString()} breaths`, | |
| `The moon has orbited Earth ${Math.floor(data.age.years * 12.4)} times since you were born`, | |
| `Your zodiac sign ${data.zodiac.name} is known for being ${getZodiacTrait(data.zodiac.name)}`, | |
| `People born on ${data.dayOfWeek}s are said to be ${getDayTrait(data.dayOfWeek)}` | |
| ]; | |
| // Add historical events | |
| const historicalEvents = getHistoricalEvents(data.year); | |
| facts.push(...historicalEvents); | |
| const list = document.getElementById('funFactsList'); | |
| list.innerHTML = facts.map(fact => ` | |
| <li class="flex items-start gap-3 text-slate-700 dark:text-slate-300"> | |
| <span class="text-primary-500 mt-1">β’</span> | |
| <span>${fact}</span> | |
| </li> | |
| `).join(''); | |
| } | |
| function getZodiacTrait(sign) { | |
| const traits = { | |
| 'Aries': 'bold and ambitious', | |
| 'Taurus': 'reliable and patient', | |
| 'Gemini': 'adaptable and outgoing', | |
| 'Cancer': 'intuitive and sentimental', | |
| 'Leo': 'confident and charismatic', | |
| 'Virgo': 'analytical and hardworking', | |
| 'Libra': 'diplomatic and gracious', | |
| 'Scorpio': 'passionate and resourceful', | |
| 'Sagittarius': 'generous and idealistic', | |
| 'Capricorn': 'responsible and disciplined', | |
| 'Aquarius': 'progressive and original', | |
| 'Pisces': 'compassionate and artistic' | |
| }; | |
| return traits[sign] || 'unique and special'; | |
| } | |
| function getDayTrait(day) { | |
| const traits = { | |
| 'Monday': 'creative and intuitive', | |
| 'Tuesday': 'energetic and courageous', | |
| 'Wednesday': 'communicative and curious', | |
| 'Thursday': 'expansive and optimistic', | |
| 'Friday': 'loving and harmonious', | |
| 'Saturday': 'wise and disciplined', | |
| 'Sunday': 'vital and radiant' | |
| }; | |
| return traits[day] || 'wonderful'; | |
| } | |
| function getHistoricalEvents(year) { | |
| const events = { | |
| 2024: ['AI continues to transform technology', 'Space exploration reaches new heights'], | |
| 2023: ['ChatGPT became widely popular', 'James Webb Space Telescope discoveries'], | |
| 2020: ['COVID-19 pandemic changed the world', 'SpaceX launched first crewed mission'], | |
| 2000: ['Y2K bug was avoided', 'Human genome project completed'], | |
| 1990: ['World Wide Web was invented', 'Hubble Space Telescope launched'], | |
| 1980: ['Pac-Man was released', 'CNN launched as first 24-hour news channel'], | |
| 1970: ['First Earth Day celebrated', 'Floppy disk invented'], | |
| 1969: ['Humans first landed on the Moon', 'Internet precursor ARPANET launched'], | |
| 1960: ['Laser was invented', 'First weather satellite launched'] | |
| }; | |
| // Find closest year with events | |
| const years = Object.keys(events).map(Number).sort((a, b) => b - a); | |
| const closestYear = years.find(y => y <= year) || years[years.length - 1]; | |
| return events[closestYear] ? events[closestYear].map(e => `In ${closestYear}: ${e}`) : []; | |
| } | |
| // ==================== ACTIONS ==================== | |
| function resetForm() { | |
| elements.form.reset(); | |
| currentStep = 1; | |
| birthData = { day: null, month: null, year: null, time: null, timezone: 'UTC' }; | |
| elements.resultsCard.classList.add('hidden'); | |
| elements.formCard.style.display = 'block'; | |
| document.getElementById('lifeProgressBar').style.width = '0%'; | |
| goToStep(1); | |
| window.scrollTo({ top: 0, behavior: 'smooth' }); | |
| } | |
| async function shareResults() { | |
| const text = `I discovered my cosmic birthday profile with ChronoBirth! πβ¨`; | |
| try { | |
| if (navigator.share) { | |
| await navigator.share({ title: 'My ChronoBirth Profile', text }); | |
| } else { | |
| await navigator.clipboard.writeText(text + ' https://chronobirth.magic'); | |
| showToast('Link copied to clipboard!'); | |
| } | |
| } catch (err) { | |
| showToast('Unable to share', 'error'); | |
| } | |
| } | |
| function saveResults() { | |
| // Create a simple text representation | |
| const results = document.getElementById('resultsCard').innerText; | |
| const blob = new Blob([results], { type: 'text/plain' }); | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = 'my-chronobirth-profile.txt'; | |
| a.click(); | |
| URL.revokeObjectURL(url); | |
| showToast('Profile saved!'); | |
| } | |
| function showToast(message, type = 'success') { | |
| const toast = elements.toast; | |
| const toastMessage = document.getElementById('toastMessage'); | |
| toastMessage.textContent = message; | |
| toast.querySelector('i').className = type === 'error' | |
| ? 'w-5 h-5 text-rose-500' | |
| : 'w-5 h-5 text-emerald-400 dark:text-emerald-500'; | |
| toast.style.transform = 'translateX(-50%) translateY(0)'; | |
| toast.style.opacity = '1'; | |
| setTimeout(() => { | |
| toast.style.transform = 'translateX(-50%) translateY(20px)'; | |
| toast.style.opacity = '0'; | |
| }, 3000); | |
| } | |
| // ==================== CONFETTI ==================== | |
| function triggerConfetti() { | |
| const canvas = elements.confettiCanvas; | |
| const ctx = canvas.getContext('2d'); | |
| canvas.width = window.innerWidth; | |
| canvas.height = window.innerHeight; | |
| const particles = []; | |
| const colors = ['#ef4444', '#f59e0b', '#10b981', '#8b5cf6', '#ec4899', '#06b6d4']; | |
| // Create particles | |
| for (let i = 0; i < 150; i++) { | |
| particles.push({ | |
| x: canvas.width / 2, | |
| y: canvas.height / 2, | |
| vx: (Math.random() - 0.5) * 15, | |
| vy: (Math.random() - 0.5) * 15 - 5, | |
| color: colors[Math.floor(Math.random() * colors.length)], | |
| size: Math.random() * 8 + 4, | |
| rotation: Math.random() * 360, | |
| rotationSpeed: (Math.random() - 0.5) * 10, | |
| gravity: 0.3, | |
| drag: 0.98 | |
| }); | |
| } | |
| let animationId; | |
| function animate() { | |
| ctx.clearRect(0, 0, canvas.width, canvas.height); | |
| let activeParticles = 0; | |
| particles.forEach(p => { | |
| if (p.y < canvas.height + 50) { | |
| activeParticles++; | |
| p.x += p.vx; | |
| p.y += p.vy; | |
| p.vy += p.gravity; | |
| p.vx *= p.drag; | |
| p.vy *= p.drag; | |
| p.rotation += p.rotationSpeed; | |
| ctx.save(); | |
| ctx.translate(p.x, p.y); | |
| ctx.rotate(p.rotation * Math.PI / 180); | |
| ctx.fillStyle = p.color; | |
| ctx.fillRect(-p.size / 2, -p.size / 2, p.size, p.size); | |
| ctx.restore(); | |
| } | |
| }); | |
| if (activeParticles > 0) { | |
| animationId = requestAnimationFrame(animate); | |
| } else { | |
| ctx.clearRect(0, 0, canvas.width, canvas.height); | |
| } | |
| } | |
| animate(); | |
| // Cleanup on resize | |
| window.addEventListener('resize', () => { | |
| canvas.width = window.innerWidth; | |
| canvas.height = window.innerHeight; | |
| }, { once: true }); | |
| } | |
| // ==================== UTILITY ==================== | |
| // Add smooth scroll behavior | |
| document.documentElement.style.scrollBehavior = 'smooth'; | |
| // Prevent form submission on Enter for multi-step | |
| document.addEventListener('keydown', (e) => { | |
| if (e.key === 'Enter' && currentStep < totalSteps) { | |
| e.preventDefault(); | |
| nextStep(); | |
| } | |
| }); |