Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Happy Birthday! 🎉</title> | |
| <style> | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| font-family: 'Comic Sans MS', cursive, sans-serif; | |
| overflow: hidden; | |
| background: radial-gradient(ellipse at center, #1a1f4d 0%, #0a0e27 50%, #000000 100%); | |
| height: 100vh; | |
| height: 100svh; | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| position: relative; | |
| animation: backgroundShimmer 8s ease-in-out infinite; | |
| } | |
| @keyframes backgroundShimmer { | |
| 0%, 100% { | |
| background: radial-gradient(ellipse at center, #1a1f4d 0%, #0a0e27 50%, #000000 100%); | |
| } | |
| 50% { | |
| background: radial-gradient(ellipse at center, #2d3561 0%, #141a3f 50%, #0a0e27 100%); | |
| } | |
| } | |
| /* Bầu trời đầy sao */ | |
| .star { | |
| position: absolute; | |
| background: white; | |
| border-radius: 50%; | |
| animation: twinkle 2s ease-in-out infinite; | |
| box-shadow: 0 0 6px rgba(255, 255, 255, 0.8); | |
| } | |
| @keyframes twinkle { | |
| 0%, 100% { | |
| opacity: 0.2; | |
| transform: scale(1); | |
| box-shadow: 0 0 6px rgba(255, 255, 255, 0.8); | |
| } | |
| 50% { | |
| opacity: 1; | |
| transform: scale(1.5); | |
| box-shadow: 0 0 15px rgba(255, 255, 255, 1), 0 0 25px rgba(200, 220, 255, 0.6); | |
| } | |
| } | |
| /* Thêm shooting stars */ | |
| .shooting-star { | |
| position: absolute; | |
| width: 2px; | |
| height: 2px; | |
| background: white; | |
| border-radius: 50%; | |
| box-shadow: 0 0 10px 2px white; | |
| animation: shoot 3s ease-out infinite; | |
| } | |
| @keyframes shoot { | |
| 0% { | |
| transform: translateX(0) translateY(0); | |
| opacity: 1; | |
| } | |
| 100% { | |
| transform: translateX(300px) translateY(300px); | |
| opacity: 0; | |
| } | |
| } | |
| /* Hoa đăng bay thẳng lên trời (glowing CSS lanterns) */ | |
| .lantern { | |
| position: absolute; | |
| width: 56px; | |
| height: 72px; | |
| animation: floatUpStraight var(--float-duration, 12s) linear infinite, swayX var(--sway-duration, 3s) ease-in-out infinite; | |
| filter: drop-shadow(0 0 20px rgba(255, 175, 0, 0.8)); | |
| will-change: transform, margin-left; | |
| } | |
| .lantern::before { | |
| content: ''; | |
| position: absolute; | |
| top: 50%; | |
| left: 50%; | |
| transform: translate(-50%, -50%); | |
| width: 100%; | |
| height: 100%; | |
| border-radius: 26px / 34px; | |
| background: radial-gradient(ellipse at 50% 70%, rgba(255, 245, 200, 0.95) 0 12%, #ffd66b 28%, #ff9f3b 52%, #ff7a2e 66%, #d4531b 78%, #86290a 100%); | |
| box-shadow: 0 0 24px rgba(255, 180, 0, 0.9), 0 0 60px rgba(255, 120, 0, 0.45); | |
| animation: lanternGlow 2s ease-in-out infinite; | |
| } | |
| .lantern::after { | |
| content: ''; | |
| position: absolute; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| top: 6%; | |
| width: 60%; | |
| height: 10%; | |
| background: #151515; | |
| border-radius: 2px 2px 8px 8px; | |
| opacity: 0.85; | |
| box-shadow: 0 2px 4px rgba(0,0,0,0.4); | |
| } | |
| @keyframes lanternGlow { | |
| 0%, 100% { | |
| filter: brightness(1) drop-shadow(0 0 10px rgba(255, 215, 0, 0.6)); | |
| } | |
| 50% { | |
| filter: brightness(1.5) drop-shadow(0 0 25px rgba(255, 165, 0, 1)); | |
| } | |
| } | |
| @keyframes floatUpStraight { | |
| 0% { | |
| transform: translateY(110vh) scale(var(--scale, 0.8)); | |
| opacity: 0; | |
| } | |
| 5% { | |
| opacity: 1; | |
| } | |
| 95% { | |
| opacity: 1; | |
| } | |
| 100% { | |
| transform: translateY(-10vh) scale(calc(var(--scale, 0.8) + 0.4)); | |
| opacity: 0; | |
| } | |
| } | |
| @keyframes swayX { | |
| 0%, 100% { margin-left: -10px; } | |
| 50% { margin-left: 10px; } | |
| } | |
| /* Thiệp */ | |
| .card { | |
| background: linear-gradient(135deg, rgba(255, 255, 255, 0.95), rgba(255, 250, 220, 0.95)); | |
| border-radius: 40px; | |
| padding: 50px 40px; | |
| box-shadow: 0 30px 80px rgba(255, 215, 0, 0.6), 0 0 100px rgba(255, 165, 0, 0.4); | |
| text-align: center; | |
| max-width: 600px; | |
| position: relative; | |
| animation: cardEntrance 1.5s ease-out, cardGlow 3s ease-in-out infinite; | |
| border: 3px solid rgba(255, 215, 0, 0.5); | |
| z-index: 2; | |
| } | |
| @keyframes cardEntrance { | |
| from { | |
| transform: scale(0) rotate(180deg); | |
| opacity: 0; | |
| } | |
| to { | |
| transform: scale(1) rotate(0deg); | |
| opacity: 1; | |
| } | |
| } | |
| @keyframes cardGlow { | |
| 0%, 100% { | |
| box-shadow: 0 30px 80px rgba(255, 215, 0, 0.6), 0 0 100px rgba(255, 165, 0, 0.4); | |
| } | |
| 50% { | |
| box-shadow: 0 30px 100px rgba(255, 215, 0, 0.9), 0 0 150px rgba(255, 140, 0, 0.6); | |
| } | |
| } | |
| /* Họa tiết mặt trời ở trên */ | |
| .sun-decoration { | |
| position: absolute; | |
| top: -30px; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| width: 100px; | |
| height: 100px; | |
| animation: rotateSun 10s linear infinite; | |
| } | |
| .sun-center { | |
| width: 60px; | |
| height: 60px; | |
| background: radial-gradient(circle, #ffd700, #ff8c00); | |
| border-radius: 50%; | |
| position: absolute; | |
| top: 50%; | |
| left: 50%; | |
| transform: translate(-50%, -50%); | |
| box-shadow: 0 0 30px #ffd700, 0 0 60px #ff8c00; | |
| animation: pulse 2s ease-in-out infinite; | |
| } | |
| @keyframes pulse { | |
| 0%, 100% { transform: translate(-50%, -50%) scale(1); } | |
| 50% { transform: translate(-50%, -50%) scale(1.2); } | |
| } | |
| .sun-ray { | |
| position: absolute; | |
| width: 4px; | |
| height: 25px; | |
| background: linear-gradient(to bottom, #ffd700, transparent); | |
| top: 0; | |
| left: 50%; | |
| transform-origin: bottom center; | |
| } | |
| @keyframes rotateSun { | |
| from { transform: translateX(-50%) rotate(0deg); } | |
| to { transform: translateX(-50%) rotate(360deg); } | |
| } | |
| .title { | |
| font-size: 52px; | |
| font-weight: bold; | |
| background: linear-gradient(45deg, #ff1744, #ff9100, #ffd700, #00e676, #00b0ff, #d500f9); | |
| background-size: 400% 400%; | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| background-clip: text; | |
| animation: gradientShift 3s ease infinite; | |
| margin-bottom: 20px; | |
| text-shadow: 0 0 20px rgba(255, 215, 0, 0.5); | |
| filter: drop-shadow(0 0 10px rgba(255, 215, 0, 0.8)); | |
| } | |
| @keyframes gradientShift { | |
| 0%, 100% { background-position: 0% 50%; } | |
| 50% { background-position: 100% 50%; } | |
| } | |
| /* Biển hoa hướng dương */ | |
| .sunflower-field { | |
| display: flex; | |
| justify-content: center; | |
| gap: 8px; | |
| margin: 20px 0; | |
| flex-wrap: wrap; | |
| animation: fadeIn 2s ease-in; | |
| } | |
| .sunflower { | |
| font-size: 50px; | |
| animation: swayFlower 3s ease-in-out infinite; | |
| display: inline-block; | |
| filter: drop-shadow(0 0 10px rgba(255, 215, 0, 0.6)); | |
| } | |
| .sunflower:nth-child(even) { | |
| animation-delay: 1s; | |
| } | |
| @keyframes swayFlower { | |
| 0%, 100% { transform: rotate(-5deg); } | |
| 50% { transform: rotate(5deg); } | |
| } | |
| .message { | |
| font-size: 24px; | |
| color: #d35400; | |
| margin: 20px 0; | |
| animation: fadeIn 2.5s ease-in; | |
| font-weight: bold; | |
| text-shadow: 2px 2px 4px rgba(255, 215, 0, 0.3); | |
| } | |
| @keyframes fadeIn { | |
| from { opacity: 0; transform: translateY(20px); } | |
| to { opacity: 1; transform: translateY(0); } | |
| } | |
| .sparkle { | |
| position: absolute; | |
| font-size: 30px; | |
| animation: sparkleAnim 1.5s ease-in-out infinite; | |
| } | |
| @keyframes sparkleAnim { | |
| 0%, 100% { | |
| opacity: 0; | |
| transform: scale(0) rotate(0deg); | |
| } | |
| 50% { | |
| opacity: 1; | |
| transform: scale(1.5) rotate(180deg); | |
| } | |
| } | |
| .wish { | |
| font-size: 20px; | |
| color: #e67e22; | |
| margin-top: 20px; | |
| font-style: italic; | |
| font-weight: bold; | |
| animation: fadeIn 3s ease-in; | |
| } | |
| .hearts { | |
| display: flex; | |
| justify-content: center; | |
| gap: 15px; | |
| margin-top: 25px; | |
| font-size: 40px; | |
| } | |
| .heart { | |
| animation: heartbeat 1.5s ease-in-out infinite; | |
| display: inline-block; | |
| filter: drop-shadow(0 0 10px rgba(255, 0, 100, 0.6)); | |
| } | |
| .heart:nth-child(2) { | |
| animation-delay: 0.3s; | |
| } | |
| .heart:nth-child(3) { | |
| animation-delay: 0.6s; | |
| } | |
| @keyframes heartbeat { | |
| 0%, 100% { | |
| transform: scale(1); | |
| } | |
| 25% { | |
| transform: scale(1.4); | |
| } | |
| 50% { | |
| transform: scale(1); | |
| } | |
| } | |
| /* Hiệu ứng ánh sáng xung quanh thiệp */ | |
| .glow-particle { | |
| position: absolute; | |
| border-radius: 50%; | |
| background: radial-gradient(circle, rgba(255, 215, 0, 0.8), transparent); | |
| animation: floatGlow 4s ease-in-out infinite; | |
| } | |
| /* Bảo đảm các hiệu ứng nổi ở trên video nền */ | |
| .star, .shooting-star, .lantern, .sparkle, .glow-particle { z-index: 2; } | |
| @keyframes floatGlow { | |
| 0%, 100% { | |
| transform: translate(0, 0) scale(1); | |
| opacity: 0.6; | |
| } | |
| 50% { | |
| transform: translate(20px, -20px) scale(1.5); | |
| opacity: 1; | |
| } | |
| } | |
| /* Canvas cho pháo hoa */ | |
| #fireworksCanvas { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| pointer-events: none; | |
| z-index: 1000; | |
| } | |
| /* Video nền toàn màn hình */ | |
| #bgVideo { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| object-fit: cover; | |
| z-index: 0; | |
| pointer-events: none; | |
| } | |
| body { | |
| cursor: crosshair; | |
| } | |
| /* Responsive adjustments for phones */ | |
| @media (max-width: 480px) { | |
| .card { | |
| padding: 24px 20px; | |
| border-radius: 24px; | |
| max-width: calc(100vw - 24px); | |
| } | |
| .title { | |
| font-size: 36px; | |
| margin-bottom: 12px; | |
| } | |
| .sunflower-field { | |
| margin: 12px 0; | |
| gap: 6px; | |
| } | |
| .sunflower { | |
| font-size: 36px; | |
| } | |
| .message { | |
| font-size: 18px; | |
| margin: 12px 0; | |
| } | |
| .hearts { | |
| font-size: 32px; | |
| margin-top: 16px; | |
| gap: 10px; | |
| } | |
| .wish { | |
| font-size: 16px; | |
| } | |
| .sun-decoration { | |
| width: 72px; | |
| height: 72px; | |
| top: -20px; | |
| } | |
| .sun-center { | |
| width: 44px; | |
| height: 44px; | |
| } | |
| .sun-ray { | |
| height: 18px; | |
| } | |
| } | |
| @media (max-height: 600px) { | |
| .card { | |
| padding: 20px 18px; | |
| } | |
| .sunflower-field { | |
| margin: 10px 0; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <!-- Video nền --> | |
| <video id="bgVideo" autoplay muted playsinline loop poster="bg_poster.png"> | |
| <source src="bg.mp4" type="video/mp4"> | |
| </video> | |
| <div id="bgImageFallback" style="position:fixed;inset:0;background:url('bg_poster.png') center/cover no-repeat;z-index:-1;display:none;"></div> | |
| <div class="card"> | |
| <!-- Mặt trời trang trí --> | |
| <div class="sun-decoration"> | |
| <div class="sun-center"></div> | |
| <div class="sun-ray" style="transform: rotate(0deg) translateX(-50%);"></div> | |
| <div class="sun-ray" style="transform: rotate(45deg) translateX(-50%);"></div> | |
| <div class="sun-ray" style="transform: rotate(90deg) translateX(-50%);"></div> | |
| <div class="sun-ray" style="transform: rotate(135deg) translateX(-50%);"></div> | |
| <div class="sun-ray" style="transform: rotate(180deg) translateX(-50%);"></div> | |
| <div class="sun-ray" style="transform: rotate(225deg) translateX(-50%);"></div> | |
| <div class="sun-ray" style="transform: rotate(270deg) translateX(-50%);"></div> | |
| <div class="sun-ray" style="transform: rotate(315deg) translateX(-50%);"></div> | |
| </div> | |
| <!-- Hiệu ứng lấp lánh --> | |
| <div class="sparkle" style="top: 60px; left: 30px;">✨</div> | |
| <div class="sparkle" style="top: 70px; right: 40px; animation-delay: 0.5s;">⭐</div> | |
| <div class="sparkle" style="top: 100px; left: 60px; animation-delay: 1s;">💫</div> | |
| <div class="sparkle" style="top: 100px; right: 60px; animation-delay: 1.5s;">✨</div> | |
| <div class="sparkle" style="bottom: 100px; left: 50px; animation-delay: 0.7s;">⭐</div> | |
| <div class="sparkle" style="bottom: 100px; right: 50px; animation-delay: 1.2s;">💫</div> | |
| <h1 class="title">HAPPY BIRTHDAY!</h1> | |
| <!-- Biển hoa hướng dương --> | |
| <div class="sunflower-field"> | |
| <span class="sunflower">🌻</span> | |
| <span class="sunflower">🌻</span> | |
| <span class="sunflower">🌻</span> | |
| <span class="sunflower">🌻</span> | |
| <span class="sunflower">🌻</span> | |
| <span class="sunflower">🌻</span> | |
| <span class="sunflower">🌻</span> | |
| <span class="sunflower">🌻</span> | |
| <span class="sunflower">🌻</span> | |
| <span class="sunflower">🌻</span> | |
| <span class="sunflower">🌻</span> | |
| <span class="sunflower">🌻</span> | |
| </div> | |
| <p class="message">Chúc mừng sinh nhật Nhật Tiếp! Chúc em luôn tràn đầy niềm vui và hạnh phúc!</p> | |
| <div class="hearts"> | |
| <span class="heart">💖</span> | |
| <span class="heart">💝</span> | |
| <span class="heart">💕</span> | |
| </div> | |
| <p class="wish">Chúc mọi điều ước của em đều trở thành hiện thực! ✨🌟</p> | |
| </div> | |
| <!-- Canvas cho pháo hoa --> | |
| <canvas id="fireworksCanvas"></canvas> | |
| <!-- Nhạc nền --> | |
| <audio id="bgMusic" src="justsayhello.mp3" preload="auto" loop></audio> | |
| <script> | |
| // Tạo ngàn ngôi sao trên bầu trời | |
| function createStars() { | |
| for (let i = 0; i < 300; i++) { | |
| const star = document.createElement('div'); | |
| star.className = 'star'; | |
| const size = Math.random() * 4 + 1; | |
| star.style.width = size + 'px'; | |
| star.style.height = size + 'px'; | |
| star.style.left = Math.random() * 100 + 'vw'; | |
| star.style.top = Math.random() * 100 + 'vh'; | |
| star.style.animationDelay = Math.random() * 3 + 's'; | |
| star.style.animationDuration = (Math.random() * 3 + 1) + 's'; | |
| document.body.appendChild(star); | |
| } | |
| } | |
| // Tạo sao băng | |
| function createShootingStar() { | |
| const shootingStar = document.createElement('div'); | |
| shootingStar.className = 'shooting-star'; | |
| shootingStar.style.left = Math.random() * 50 + 'vw'; | |
| shootingStar.style.top = Math.random() * 30 + 'vh'; | |
| shootingStar.style.animationDelay = Math.random() * 2 + 's'; | |
| shootingStar.style.animationDuration = (Math.random() * 2 + 2) + 's'; | |
| document.body.appendChild(shootingStar); | |
| setTimeout(() => shootingStar.remove(), 5000); | |
| } | |
| // Tạo nhiều hoa đăng bay thẳng lên | |
| function createLantern() { | |
| const lantern = document.createElement('div'); | |
| lantern.className = 'lantern'; | |
| const baseLeft = (Math.random() * 90 + 5); | |
| lantern.style.left = baseLeft + 'vw'; | |
| lantern.style.setProperty('--sway-duration', (Math.random() * 2 + 2.5) + 's'); | |
| lantern.style.setProperty('--float-duration', (Math.random() * 6 + 10) + 's'); | |
| lantern.style.setProperty('--scale', (Math.random() * 0.6 + 0.7).toFixed(2)); | |
| lantern.style.animationDelay = (Math.random() * 2) + 's'; | |
| // Wind parameters (vw-based) | |
| lantern.dataset.baseLeft = String(baseLeft); | |
| lantern.dataset.offset = '0'; | |
| lantern.dataset.vx = (Math.random() * 0.8 - 0.4).toFixed(3); // constant drift vw/s | |
| lantern.dataset.amp = (Math.random() * 3 + 1.5).toFixed(2); // sway amplitude vw | |
| lantern.dataset.freq = (Math.random() * 0.25 + 0.15).toFixed(3); // sway frequency Hz | |
| lantern.dataset.seed = (Math.random() * Math.PI * 2).toFixed(3); // gust phase seed | |
| document.body.appendChild(lantern); | |
| setTimeout(() => lantern.remove(), 18000); | |
| } | |
| // Wind-driven drift for lanterns | |
| (function windLanterns(){ | |
| let last = performance.now(); | |
| function step(now){ | |
| const dt = Math.min(0.05, (now - last) / 1000); // clamp large jumps | |
| last = now; | |
| const nodes = document.querySelectorAll('.lantern'); | |
| nodes.forEach(el => { | |
| const baseLeft = parseFloat(el.dataset.baseLeft || '50'); | |
| let offset = parseFloat(el.dataset.offset || '0'); | |
| const vx = parseFloat(el.dataset.vx || '0'); | |
| const amp = parseFloat(el.dataset.amp || '2'); | |
| const freq = parseFloat(el.dataset.freq || '0.2'); | |
| const seed = parseFloat(el.dataset.seed || '0'); | |
| // Global gusty wind component (vw/s), varies with time and per-lantern phase | |
| const gust = Math.sin(now * 0.0012 + seed) * 0.6 + Math.sin(now * 0.0005 + seed * 1.7) * 0.4; | |
| // Integrate horizontal speed | |
| offset += (vx + gust) * dt; | |
| // Gentle oscillation around the drift | |
| const t = now / 1000; | |
| const oscillation = Math.sin((t + seed) * (Math.PI * 2) * freq) * amp; | |
| const left = baseLeft + offset + oscillation; | |
| el.style.left = left + 'vw'; | |
| el.dataset.offset = offset.toFixed(3); | |
| // Remove if far out of screen horizontally | |
| if (left < -20 || left > 120) { | |
| el.remove(); | |
| } | |
| }); | |
| requestAnimationFrame(step); | |
| } | |
| requestAnimationFrame(step); | |
| })(); | |
| // Bật nhạc khi có tương tác người dùng (tuân thủ autoplay policy) | |
| let __bgAudioEnabled = false; | |
| function tryEnableAudio() { | |
| if (__bgAudioEnabled) return; | |
| const audio = document.getElementById('bgMusic'); | |
| if (!audio) return; | |
| audio.volume = 0.6; | |
| audio.play().then(() => { __bgAudioEnabled = true; }).catch(() => {}); | |
| } | |
| // Fallback hình nếu video không tải được hoặc không có file | |
| (function setupVideoFallback(){ | |
| const video = document.getElementById('bgVideo'); | |
| const fallback = document.getElementById('bgImageFallback'); | |
| if (!video || !fallback) return; | |
| let showedFallback = false; | |
| function showFallback(){ | |
| if (showedFallback) return; | |
| showedFallback = true; | |
| fallback.style.display = 'block'; | |
| video.style.display = 'none'; | |
| } | |
| video.addEventListener('error', showFallback, { once: true }); | |
| video.addEventListener('stalled', showFallback, { once: true }); | |
| video.addEventListener('abort', showFallback, { once: true }); | |
| // Nếu sau 2s không có enough data để phát, dùng fallback | |
| setTimeout(() => { | |
| const ready = video.readyState >= 2; // HAVE_CURRENT_DATA | |
| if (!ready || isNaN(video.duration)) showFallback(); | |
| }, 2000); | |
| })(); | |
| // Hệ thống pháo hoa Canvas mới | |
| class FireworksSystem { | |
| constructor() { | |
| this.canvas = document.getElementById('fireworksCanvas'); | |
| this.ctx = this.canvas.getContext('2d'); | |
| this.fireworks = []; | |
| this.particles = []; | |
| this.counter = 0; | |
| this.resize(); | |
| this.setupEventListeners(); | |
| // Tạo một vài pháo hoa ban đầu để hiển thị ngay | |
| for (let i = 0; i < 3; i++) { | |
| this.fireworks.push(new Firework( | |
| this.random(this.spawnA, this.spawnB), | |
| this.height, | |
| this.random(0, this.width), | |
| this.random(this.spawnC, this.spawnD), | |
| this.random(0, 360), | |
| this.random(30, 110) | |
| )); | |
| } | |
| this.animate(); | |
| } | |
| resize() { | |
| const dpr = Math.max(1, Math.min(2, window.devicePixelRatio || 1)); | |
| const cssWidth = window.innerWidth; | |
| const cssHeight = window.innerHeight; | |
| this.canvas.style.width = cssWidth + 'px'; | |
| this.canvas.style.height = cssHeight + 'px'; | |
| this.canvas.width = Math.floor(cssWidth * dpr); | |
| this.canvas.height = Math.floor(cssHeight * dpr); | |
| this.ctx.setTransform(dpr, 0, 0, dpr, 0, 0); | |
| this.width = cssWidth; | |
| this.height = cssHeight; | |
| // Vùng spawn pháo hoa | |
| let center = this.width / 2; | |
| this.spawnA = center - center / 4; | |
| this.spawnB = center + center / 4; | |
| this.spawnC = this.height * 0.1; | |
| this.spawnD = this.height * 0.5; | |
| } | |
| setupEventListeners() { | |
| window.addEventListener('resize', () => this.resize()); | |
| document.addEventListener('click', (e) => { tryEnableAudio(); this.onClick(e); }); | |
| document.addEventListener('touchstart', (e) => { tryEnableAudio(); this.onClick(e); }); | |
| } | |
| onClick(event) { | |
| let x = event.clientX || (event.touches && event.touches[0].pageX); | |
| let y = event.clientY || (event.touches && event.touches[0].pageY); | |
| let count = this.random(3, 5); | |
| for (let i = 0; i < count; i++) { | |
| this.fireworks.push(new Firework( | |
| this.random(this.spawnA, this.spawnB), | |
| this.height, | |
| x, | |
| y, | |
| this.random(0, 360), | |
| this.random(30, 110) | |
| )); | |
| } | |
| this.counter = -1; | |
| } | |
| random(min, max) { | |
| return Math.random() * (max - min + 1) + min | 0; | |
| } | |
| update(delta) { | |
| // Làm mờ vệt cũ mà không che thiệp (canvas trong suốt) | |
| this.ctx.globalCompositeOperation = 'destination-out'; | |
| this.ctx.fillStyle = `rgba(0,0,0,${Math.min(0.3, 7 * delta)})`; | |
| this.ctx.fillRect(0, 0, this.width, this.height); | |
| this.ctx.globalCompositeOperation = 'lighter'; | |
| // Cập nhật pháo hoa | |
| for (let firework of this.fireworks) { | |
| firework.update(delta, this.ctx, this.fireworks); | |
| } | |
| // Tạo pháo hoa tự động | |
| this.counter += delta * 3; | |
| if (this.counter >= 1) { | |
| this.fireworks.push(new Firework( | |
| this.random(this.spawnA, this.spawnB), | |
| this.height, | |
| this.random(0, this.width), | |
| this.random(this.spawnC, this.spawnD), | |
| this.random(0, 360), | |
| this.random(30, 110) | |
| )); | |
| this.counter = 0; | |
| } | |
| // Dọn dẹp pháo hoa đã chết | |
| if (this.fireworks.length > 1000) { | |
| this.fireworks = this.fireworks.filter(firework => !firework.dead); | |
| } | |
| } | |
| animate() { | |
| let then = performance.now(); | |
| const loop = () => { | |
| requestAnimationFrame(loop); | |
| let now = performance.now(); | |
| let delta = (now - then) / 1000; | |
| then = now; | |
| this.update(delta); | |
| }; | |
| loop(); | |
| } | |
| } | |
| class Firework { | |
| constructor(x, y, targetX, targetY, shade, offsprings) { | |
| this.dead = false; | |
| this.offsprings = offsprings; | |
| this.madeChilds = false; | |
| this.x = x; | |
| this.y = y; | |
| this.targetX = targetX; | |
| this.targetY = targetY; | |
| this.shade = shade; | |
| this.history = []; | |
| } | |
| update(delta, ctx, fireworks) { | |
| if (this.dead) return; | |
| let xDiff = this.targetX - this.x; | |
| let yDiff = this.targetY - this.y; | |
| if (Math.abs(xDiff) > 3 || Math.abs(yDiff) > 3) { | |
| // Vẫn đang bay | |
| this.x += xDiff * 2 * delta; | |
| this.y += yDiff * 2 * delta; | |
| this.history.push({ | |
| x: this.x, | |
| y: this.y | |
| }); | |
| if (this.history.length > 20) this.history.shift(); | |
| } else { | |
| // Đã đến đích, tạo pháo hoa con | |
| if (this.offsprings && !this.madeChilds) { | |
| let babies = this.offsprings / 2; | |
| for (let i = 0; i < babies; i++) { | |
| let targetX = this.x + this.offsprings * Math.cos(Math.PI * 2 * i / babies); | |
| let targetY = this.y + this.offsprings * Math.sin(Math.PI * 2 * i / babies); | |
| fireworks.push(new Firework(this.x, this.y, targetX, targetY, this.shade, 0)); | |
| } | |
| } | |
| this.madeChilds = true; | |
| this.history.shift(); | |
| } | |
| if (this.history.length === 0) { | |
| this.dead = true; | |
| } else if (this.offsprings) { | |
| // Vẽ trail | |
| for (let i = 0; i < this.history.length; i++) { | |
| let point = this.history[i]; | |
| ctx.beginPath(); | |
| ctx.fillStyle = `hsl(${this.shade},100%,${i}%)`; | |
| ctx.arc(point.x, point.y, 1, 0, Math.PI * 2, false); | |
| ctx.fill(); | |
| } | |
| } else { | |
| // Vẽ pháo hoa con | |
| ctx.beginPath(); | |
| ctx.fillStyle = `hsl(${this.shade},100%,50%)`; | |
| ctx.arc(this.x, this.y, 1, 0, Math.PI * 2, false); | |
| ctx.fill(); | |
| } | |
| } | |
| } | |
| // Tạo các hạt sáng lung linh xung quanh thiệp | |
| function createGlowParticles() { | |
| for (let i = 0; i < 30; i++) { | |
| setTimeout(() => { | |
| const particle = document.createElement('div'); | |
| particle.className = 'glow-particle'; | |
| const size = Math.random() * 40 + 15; | |
| particle.style.width = size + 'px'; | |
| particle.style.height = size + 'px'; | |
| particle.style.left = Math.random() * 100 + 'vw'; | |
| particle.style.top = Math.random() * 100 + 'vh'; | |
| particle.style.animationDelay = Math.random() * 2 + 's'; | |
| particle.style.animationDuration = (Math.random() * 2 + 3) + 's'; | |
| document.body.appendChild(particle); | |
| setTimeout(() => particle.remove(), 8000); | |
| }, i * 80); | |
| } | |
| } | |
| // Khởi tạo | |
| createStars(); | |
| // Tạo hoa đăng liên tục - giảm xuống | |
| setInterval(createLantern, 800); | |
| // Tạo sao băng | |
| setInterval(createShootingStar, 4000); | |
| // Tạo các hạt sáng liên tục | |
| createGlowParticles(); | |
| setInterval(createGlowParticles, 4000); | |
| // Khởi tạo hệ thống pháo hoa Canvas | |
| const fireworksSystem = new FireworksSystem(); | |
| </script> | |
| </body> | |
| </html> |