Implement Lucide Vue icon system and UI improvements
- Replace emoji icons with professional SVG icons from Lucide Vue - Add collapsible MusicControls with compact top-right collapse button - Improve icon system with dynamic sizing and proper prop handling - Disable SSR to prevent hydration issues with audio APIs - Update IconButton to support both emoji strings and SVG components - Optimize bottom positioning for expanded vs collapsed states - Document new icon system in DESIGN_SYSTEM.md
This commit is contained in:
360
components/AuroraBackground.client.vue
Normal file
360
components/AuroraBackground.client.vue
Normal file
@@ -0,0 +1,360 @@
|
||||
<template>
|
||||
<div class="aurora-background">
|
||||
<!-- Main Aurora Orbs -->
|
||||
<div
|
||||
v-for="(orb, index) in auroraOrbs"
|
||||
:key="`orb-${index}`"
|
||||
class="aurora-orb"
|
||||
:class="`orb-${index + 1}`"
|
||||
:style="orbStyle(orb, index)"
|
||||
></div>
|
||||
|
||||
<!-- Interactive Orbs (respond to music) -->
|
||||
<div
|
||||
v-for="(orb, index) in interactiveOrbs"
|
||||
:key="`interactive-${index}`"
|
||||
class="interactive-orb"
|
||||
:style="interactiveOrbStyle(orb, index)"
|
||||
:class="{ pulsing: isPlaying }"
|
||||
></div>
|
||||
|
||||
<!-- Particle System -->
|
||||
<div class="particles">
|
||||
<div
|
||||
v-for="(particle, index) in particles"
|
||||
:key="`particle-${index}`"
|
||||
class="particle"
|
||||
:style="particleStyle(particle, index)"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<!-- Gradient Overlay -->
|
||||
<div class="gradient-overlay" :class="{ active: isPlaying }"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
isPlaying: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
currentTrack: {
|
||||
type: Object,
|
||||
default: null
|
||||
}
|
||||
})
|
||||
|
||||
// Aurora orbs configuration
|
||||
const auroraOrbs = ref([
|
||||
{
|
||||
colors: ['var(--aurora-1)', 'var(--aurora-2)'],
|
||||
size: 300,
|
||||
position: { top: '10%', left: '10%' },
|
||||
duration: 8,
|
||||
delay: 0
|
||||
},
|
||||
{
|
||||
colors: ['var(--aurora-3)', 'var(--aurora-4)'],
|
||||
size: 250,
|
||||
position: { top: '60%', right: '20%' },
|
||||
duration: 10,
|
||||
delay: 2
|
||||
},
|
||||
{
|
||||
colors: ['var(--aurora-5)', 'var(--aurora-1)'],
|
||||
size: 200,
|
||||
position: { bottom: '20%', left: '50%' },
|
||||
duration: 12,
|
||||
delay: 4
|
||||
},
|
||||
{
|
||||
colors: ['var(--aurora-2)', 'var(--aurora-5)'],
|
||||
size: 180,
|
||||
position: { top: '30%', right: '10%' },
|
||||
duration: 9,
|
||||
delay: 6
|
||||
}
|
||||
])
|
||||
|
||||
// Interactive orbs that respond to music
|
||||
const interactiveOrbs = ref([
|
||||
{
|
||||
colors: ['var(--accent-primary)', 'var(--accent-secondary)'],
|
||||
size: 120,
|
||||
position: { top: '20%', left: '70%' },
|
||||
intensity: 0.5
|
||||
},
|
||||
{
|
||||
colors: ['var(--aurora-3)', 'var(--aurora-5)'],
|
||||
size: 90,
|
||||
position: { bottom: '30%', left: '20%' },
|
||||
intensity: 0.3
|
||||
},
|
||||
{
|
||||
colors: ['var(--aurora-1)', 'var(--aurora-4)'],
|
||||
size: 100,
|
||||
position: { top: '70%', right: '30%' },
|
||||
intensity: 0.4
|
||||
}
|
||||
])
|
||||
|
||||
// Floating particles
|
||||
const particles = ref([])
|
||||
|
||||
// Animation frame reference
|
||||
let animationFrame = null
|
||||
|
||||
// Generate particles
|
||||
const generateParticles = () => {
|
||||
particles.value = Array.from({ length: 20 }, (_, index) => ({
|
||||
size: Math.random() * 4 + 1,
|
||||
position: {
|
||||
left: Math.random() * 100 + '%',
|
||||
top: Math.random() * 100 + '%'
|
||||
},
|
||||
delay: Math.random() * 10,
|
||||
duration: Math.random() * 20 + 15,
|
||||
opacity: Math.random() * 0.3 + 0.1
|
||||
}))
|
||||
}
|
||||
|
||||
// Compute styles for orbs
|
||||
const orbStyle = (orb, index) => ({
|
||||
'--orb-size': orb.size + 'px',
|
||||
'--animation-duration': orb.duration + 's',
|
||||
'--animation-delay': orb.delay + 's',
|
||||
'--color-1': orb.colors[0],
|
||||
'--color-2': orb.colors[1],
|
||||
...orb.position
|
||||
})
|
||||
|
||||
const interactiveOrbStyle = (orb, index) => ({
|
||||
'--orb-size': orb.size + 'px',
|
||||
'--color-1': orb.colors[0],
|
||||
'--color-2': orb.colors[1],
|
||||
'--intensity': orb.intensity,
|
||||
...orb.position
|
||||
})
|
||||
|
||||
const particleStyle = (particle, index) => ({
|
||||
'--particle-size': particle.size + 'px',
|
||||
'--animation-delay': particle.delay + 's',
|
||||
'--animation-duration': particle.duration + 's',
|
||||
'--particle-opacity': particle.opacity,
|
||||
...particle.position
|
||||
})
|
||||
|
||||
// Watch for track changes to trigger special effects
|
||||
watch(() => props.currentTrack, (newTrack, oldTrack) => {
|
||||
if (newTrack && newTrack !== oldTrack) {
|
||||
triggerTrackChangeEffect()
|
||||
}
|
||||
})
|
||||
|
||||
const triggerTrackChangeEffect = () => {
|
||||
// Create a burst effect when track changes
|
||||
const burstOrb = document.createElement('div')
|
||||
burstOrb.className = 'track-change-burst'
|
||||
burstOrb.style.cssText = `
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
background: radial-gradient(circle, var(--accent-primary), transparent);
|
||||
border-radius: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
animation: burst 1s ease-out forwards;
|
||||
pointer-events: none;
|
||||
z-index: -1;
|
||||
`
|
||||
|
||||
document.body.appendChild(burstOrb)
|
||||
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(burstOrb)
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
generateParticles()
|
||||
|
||||
// Add burst animation keyframes to document
|
||||
const style = document.createElement('style')
|
||||
style.textContent = `
|
||||
@keyframes burst {
|
||||
0% { transform: translate(-50%, -50%) scale(0); opacity: 1; }
|
||||
100% { transform: translate(-50%, -50%) scale(20); opacity: 0; }
|
||||
}
|
||||
`
|
||||
document.head.appendChild(style)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.aurora-background {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: -1;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.aurora-orb {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
filter: blur(80px);
|
||||
opacity: 0.6;
|
||||
background: linear-gradient(45deg, var(--color-1), var(--color-2));
|
||||
width: var(--orb-size);
|
||||
height: var(--orb-size);
|
||||
animation: float var(--animation-duration) ease-in-out infinite;
|
||||
animation-delay: var(--animation-delay);
|
||||
}
|
||||
|
||||
.interactive-orb {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
filter: blur(60px);
|
||||
opacity: calc(var(--intensity) * 0.8);
|
||||
background: linear-gradient(45deg, var(--color-1), var(--color-2));
|
||||
width: var(--orb-size);
|
||||
height: var(--orb-size);
|
||||
transition: all 0.5s ease;
|
||||
animation: gentle-float 6s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.interactive-orb.pulsing {
|
||||
opacity: calc(var(--intensity) * 1.2);
|
||||
filter: blur(40px);
|
||||
animation: music-pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.particles {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.particle {
|
||||
position: absolute;
|
||||
width: var(--particle-size);
|
||||
height: var(--particle-size);
|
||||
background: radial-gradient(circle, var(--accent-primary), transparent);
|
||||
border-radius: 50%;
|
||||
opacity: var(--particle-opacity);
|
||||
animation: float-particle var(--animation-duration) linear infinite;
|
||||
animation-delay: var(--animation-delay);
|
||||
}
|
||||
|
||||
.gradient-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: radial-gradient(
|
||||
ellipse at center,
|
||||
transparent 0%,
|
||||
rgba(59, 130, 246, 0.05) 50%,
|
||||
transparent 100%
|
||||
);
|
||||
transition: opacity 0.5s ease;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.gradient-overlay.active {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
@keyframes float {
|
||||
0%, 100% {
|
||||
transform: translate(0, 0) scale(1);
|
||||
}
|
||||
33% {
|
||||
transform: translate(30px, -30px) scale(1.1);
|
||||
}
|
||||
66% {
|
||||
transform: translate(-20px, 20px) scale(0.9);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes gentle-float {
|
||||
0%, 100% {
|
||||
transform: translate(0, 0) scale(1);
|
||||
}
|
||||
50% {
|
||||
transform: translate(15px, -15px) scale(1.05);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes music-pulse {
|
||||
0%, 100% {
|
||||
transform: translate(0, 0) scale(1);
|
||||
}
|
||||
25% {
|
||||
transform: translate(10px, -10px) scale(1.2);
|
||||
}
|
||||
50% {
|
||||
transform: translate(-5px, 5px) scale(0.8);
|
||||
}
|
||||
75% {
|
||||
transform: translate(8px, -8px) scale(1.1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes float-particle {
|
||||
0% {
|
||||
transform: translateY(100vh) rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: translateY(-10vh) rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.aurora-orb {
|
||||
filter: blur(60px);
|
||||
}
|
||||
|
||||
.interactive-orb {
|
||||
filter: blur(40px);
|
||||
}
|
||||
|
||||
.interactive-orb.pulsing {
|
||||
filter: blur(30px);
|
||||
}
|
||||
}
|
||||
|
||||
/* Reduce motion for accessibility */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.aurora-orb,
|
||||
.interactive-orb,
|
||||
.particle {
|
||||
animation-duration: 20s;
|
||||
}
|
||||
|
||||
.interactive-orb.pulsing {
|
||||
animation: none;
|
||||
opacity: calc(var(--intensity) * 0.9);
|
||||
}
|
||||
}
|
||||
|
||||
/* Performance optimization */
|
||||
.aurora-orb,
|
||||
.interactive-orb,
|
||||
.particle {
|
||||
will-change: transform;
|
||||
transform: translateZ(0);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user