mirror of
https://github.com/fawney19/Aether.git
synced 2026-01-04 08:42:27 +08:00
- 更新 Logo 相关组件 (AetherLogo, HeaderLogo, RippleLogo 等) - 优化图表组件 (BarChart, LineChart, ScatterChart) - 改进公共组件 (AlertDialog, EmptyState, LoadingState) - 调整布局组件 (AppShell, SidebarNav, PageHeader 等) - 优化 ActivityHeatmap 统计组件
523 lines
13 KiB
Vue
523 lines
13 KiB
Vue
<template>
|
|
<div
|
|
v-show="!isFullyHidden"
|
|
class="gemini-star-cluster absolute inset-0 overflow-hidden pointer-events-none"
|
|
:class="{ 'scattering': isScattering, 'fading-out': isFadingOut }"
|
|
>
|
|
<!-- SVG Defs for the Gemini multi-color gradient -->
|
|
<svg
|
|
class="absolute w-0 h-0 overflow-hidden"
|
|
aria-hidden="true"
|
|
>
|
|
<defs>
|
|
<!-- Main Gemini gradient (blue base with color overlays) -->
|
|
<linearGradient
|
|
id="gemini-base"
|
|
x1="0%"
|
|
y1="0%"
|
|
x2="100%"
|
|
y2="100%"
|
|
>
|
|
<stop
|
|
offset="0%"
|
|
stop-color="#1A73E8"
|
|
/>
|
|
<stop
|
|
offset="50%"
|
|
stop-color="#4285F4"
|
|
/>
|
|
<stop
|
|
offset="100%"
|
|
stop-color="#669DF6"
|
|
/>
|
|
</linearGradient>
|
|
<!-- Red accent overlay - from top -->
|
|
<linearGradient
|
|
id="gemini-red-overlay"
|
|
x1="50%"
|
|
y1="0%"
|
|
x2="50%"
|
|
y2="50%"
|
|
>
|
|
<stop
|
|
offset="0%"
|
|
stop-color="#EA4335"
|
|
/>
|
|
<stop
|
|
offset="100%"
|
|
stop-color="#EA4335"
|
|
stop-opacity="0"
|
|
/>
|
|
</linearGradient>
|
|
<!-- Yellow accent overlay - from left -->
|
|
<linearGradient
|
|
id="gemini-yellow-overlay"
|
|
x1="0%"
|
|
y1="50%"
|
|
x2="50%"
|
|
y2="50%"
|
|
>
|
|
<stop
|
|
offset="0%"
|
|
stop-color="#FBBC04"
|
|
/>
|
|
<stop
|
|
offset="100%"
|
|
stop-color="#FBBC04"
|
|
stop-opacity="0"
|
|
/>
|
|
</linearGradient>
|
|
<!-- Green accent overlay - from bottom -->
|
|
<linearGradient
|
|
id="gemini-green-overlay"
|
|
x1="50%"
|
|
y1="100%"
|
|
x2="50%"
|
|
y2="50%"
|
|
>
|
|
<stop
|
|
offset="0%"
|
|
stop-color="#34A853"
|
|
/>
|
|
<stop
|
|
offset="100%"
|
|
stop-color="#34A853"
|
|
stop-opacity="0"
|
|
/>
|
|
</linearGradient>
|
|
<!-- Glow filter -->
|
|
<filter
|
|
id="star-glow"
|
|
x="-50%"
|
|
y="-50%"
|
|
width="200%"
|
|
height="200%"
|
|
>
|
|
<feGaussianBlur
|
|
stdDeviation="2"
|
|
result="blur"
|
|
/>
|
|
<feFlood
|
|
flood-color="#4285F4"
|
|
flood-opacity="0.3"
|
|
/>
|
|
<feComposite
|
|
in2="blur"
|
|
operator="in"
|
|
/>
|
|
<feMerge>
|
|
<feMergeNode />
|
|
<feMergeNode in="SourceGraphic" />
|
|
</feMerge>
|
|
</filter>
|
|
</defs>
|
|
</svg>
|
|
|
|
<!-- Layer 1: Far background stars (smallest, lowest z-index) -->
|
|
<div class="stars-layer far-layer">
|
|
<div
|
|
v-for="star in farStars"
|
|
:key="`far-${star.id}`"
|
|
class="star-wrapper"
|
|
:class="{ 'star-visible': star.visible && hasScattered }"
|
|
:style="getStarStyle(star)"
|
|
>
|
|
<svg
|
|
viewBox="0 0 24 24"
|
|
class="star-svg"
|
|
>
|
|
<path
|
|
:d="starPath"
|
|
fill="url(#gemini-base)"
|
|
/>
|
|
<path
|
|
:d="starPath"
|
|
fill="url(#gemini-red-overlay)"
|
|
/>
|
|
<path
|
|
:d="starPath"
|
|
fill="url(#gemini-yellow-overlay)"
|
|
/>
|
|
<path
|
|
:d="starPath"
|
|
fill="url(#gemini-green-overlay)"
|
|
/>
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Layer 2: Mid-distance stars -->
|
|
<div class="stars-layer mid-layer">
|
|
<div
|
|
v-for="star in midStars"
|
|
:key="`mid-${star.id}`"
|
|
class="star-wrapper"
|
|
:class="{ 'star-visible': star.visible && hasScattered }"
|
|
:style="getStarStyle(star)"
|
|
>
|
|
<svg
|
|
viewBox="0 0 24 24"
|
|
class="star-svg"
|
|
style="filter: url(#star-glow)"
|
|
>
|
|
<path
|
|
:d="starPath"
|
|
fill="url(#gemini-base)"
|
|
/>
|
|
<path
|
|
:d="starPath"
|
|
fill="url(#gemini-red-overlay)"
|
|
/>
|
|
<path
|
|
:d="starPath"
|
|
fill="url(#gemini-yellow-overlay)"
|
|
/>
|
|
<path
|
|
:d="starPath"
|
|
fill="url(#gemini-green-overlay)"
|
|
/>
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Layer 3: Near stars (largest, highest z-index - in front, not occluded by small stars) -->
|
|
<div class="stars-layer near-layer">
|
|
<div
|
|
v-for="star in nearStars"
|
|
:key="`near-${star.id}`"
|
|
class="star-wrapper"
|
|
:class="{ 'star-visible': star.visible && hasScattered }"
|
|
:style="getStarStyle(star)"
|
|
>
|
|
<svg
|
|
viewBox="0 0 24 24"
|
|
class="star-svg"
|
|
style="filter: url(#star-glow)"
|
|
>
|
|
<path
|
|
:d="starPath"
|
|
fill="url(#gemini-base)"
|
|
/>
|
|
<path
|
|
:d="starPath"
|
|
fill="url(#gemini-red-overlay)"
|
|
/>
|
|
<path
|
|
:d="starPath"
|
|
fill="url(#gemini-yellow-overlay)"
|
|
/>
|
|
<path
|
|
:d="starPath"
|
|
fill="url(#gemini-green-overlay)"
|
|
/>
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, onMounted, onUnmounted, watch, type CSSProperties } from 'vue'
|
|
|
|
// Props for transition control
|
|
const props = withDefaults(defineProps<{
|
|
isVisible?: boolean
|
|
}>(), {
|
|
isVisible: true
|
|
})
|
|
|
|
// Gemini star SVG path (4-point star) - viewBox 0 0 24 24
|
|
const starPath = 'M12 1.5c.2 3.4 1.4 6.4 3.8 8.8 2.4 2.4 5.4 3.6 8.8 3.8-3.4.2-6.4 1.4-8.8 3.8-2.4 2.4-3.6 5.4-3.8 8.8-.2-3.4-1.4-6.4-3.8-8.8-2.4-2.4-5.4-3.6-8.8-3.8 3.4-.2 6.4-1.4 8.8-3.8 2.4-2.4 3.6-5.4 3.8-8.8z'
|
|
|
|
interface Star {
|
|
id: number
|
|
x: number
|
|
y: number
|
|
targetX: number
|
|
targetY: number
|
|
size: number
|
|
baseOpacity: number
|
|
visible: boolean
|
|
zIndex: number
|
|
// Animation props
|
|
twinkleDuration: number
|
|
twinkleDelay: number
|
|
}
|
|
|
|
const farStars = ref<Star[]>([])
|
|
const midStars = ref<Star[]>([])
|
|
const nearStars = ref<Star[]>([])
|
|
const hasScattered = ref(false)
|
|
const isScattering = ref(false)
|
|
const isFadingOut = ref(false)
|
|
const isFullyHidden = ref(false)
|
|
|
|
const CENTER_X = 50
|
|
const CENTER_Y = 50
|
|
|
|
// Generate constrained position to keep stars within bounds
|
|
const getConstrainedPosition = (size: number): { x: number; y: number } => {
|
|
// Calculate padding based on star size (in percentage)
|
|
// Assuming container is roughly 400-600px wide, use a safe estimate
|
|
const paddingPercent = Math.max(3, (size / 5))
|
|
const minPos = paddingPercent
|
|
const maxPos = 100 - paddingPercent
|
|
|
|
return {
|
|
x: minPos + Math.random() * (maxPos - minPos),
|
|
y: minPos + Math.random() * (maxPos - minPos)
|
|
}
|
|
}
|
|
|
|
const createStar = (id: number, sizeRange: [number, number, number, number, number, number], opacityBase: number, opacityRange: number, visibleThreshold: number, zIndexBase: number): Star => {
|
|
const sizeVariant = Math.random()
|
|
let size: number
|
|
if (sizeVariant < 0.4) {
|
|
size = sizeRange[0] + Math.random() * sizeRange[1]
|
|
} else if (sizeVariant < 0.7) {
|
|
size = sizeRange[2] + Math.random() * sizeRange[3]
|
|
} else {
|
|
size = sizeRange[4] + Math.random() * sizeRange[5]
|
|
}
|
|
|
|
const { x: targetX, y: targetY } = getConstrainedPosition(size)
|
|
|
|
return {
|
|
id,
|
|
x: CENTER_X,
|
|
y: CENTER_Y,
|
|
targetX,
|
|
targetY,
|
|
size,
|
|
baseOpacity: opacityBase + Math.random() * opacityRange,
|
|
visible: true,
|
|
zIndex: zIndexBase + Math.round(size),
|
|
twinkleDuration: 3 + Math.random() * 4, // 3-7s duration
|
|
twinkleDelay: Math.random() * 5 // 0-5s delay
|
|
}
|
|
}
|
|
|
|
const createStarLayers = () => {
|
|
const far: Star[] = []
|
|
for (let i = 0; i < 30; i++) {
|
|
far.push(createStar(i, [6, 4, 10, 6, 14, 6], 0.2, 0.25, 0.35, 0))
|
|
}
|
|
farStars.value = far
|
|
|
|
const mid: Star[] = []
|
|
for (let i = 0; i < 18; i++) {
|
|
mid.push(createStar(i, [18, 8, 24, 10, 32, 10], 0.35, 0.35, 0.45, 30))
|
|
}
|
|
midStars.value = mid
|
|
|
|
const near: Star[] = []
|
|
for (let i = 0; i < 10; i++) {
|
|
near.push(createStar(i, [40, 15, 55, 20, 70, 25], 0.5, 0.5, 0.55, 100))
|
|
}
|
|
nearStars.value = near
|
|
}
|
|
|
|
// Removed: handleAnimationIteration was causing stars to "teleport" visibly
|
|
// Stars now stay in place and only twinkle without changing position
|
|
|
|
// Scatter stars from center to their target positions
|
|
const scatterStars = () => {
|
|
isScattering.value = true
|
|
hasScattered.value = true
|
|
|
|
const allStars = [...farStars.value, ...midStars.value, ...nearStars.value]
|
|
allStars.forEach(star => {
|
|
star.x = star.targetX
|
|
star.y = star.targetY
|
|
})
|
|
|
|
setTimeout(() => {
|
|
isScattering.value = false
|
|
}, 2000)
|
|
}
|
|
|
|
// Reset stars to center
|
|
const resetStarsToCenter = () => {
|
|
hasScattered.value = false
|
|
isScattering.value = false
|
|
|
|
const allStars = [...farStars.value, ...midStars.value, ...nearStars.value]
|
|
allStars.forEach(star => {
|
|
star.x = CENTER_X
|
|
star.y = CENTER_Y
|
|
const { x, y } = getConstrainedPosition(star.size)
|
|
star.targetX = x
|
|
star.targetY = y
|
|
})
|
|
}
|
|
|
|
const getStarStyle = (star: Star): CSSProperties => {
|
|
return {
|
|
left: `${star.x}%`,
|
|
top: `${star.y}%`,
|
|
width: `${star.size}px`,
|
|
height: `${star.size}px`,
|
|
zIndex: star.zIndex,
|
|
'--base-opacity': star.baseOpacity,
|
|
'--twinkle-duration': `${star.twinkleDuration}s`,
|
|
'--twinkle-delay': `${star.twinkleDelay}s`
|
|
} as CSSProperties
|
|
}
|
|
|
|
// Watch for visibility changes
|
|
watch(() => props.isVisible, (newVal, oldVal) => {
|
|
if (newVal && !oldVal) {
|
|
// Entering: recreate stars if needed, reset to center then scatter
|
|
isFadingOut.value = false
|
|
isFullyHidden.value = false
|
|
// Recreate stars if they were cleared
|
|
if (farStars.value.length === 0) {
|
|
createStarLayers()
|
|
} else {
|
|
resetStarsToCenter()
|
|
}
|
|
setTimeout(() => {
|
|
scatterStars()
|
|
}, 50)
|
|
} else if (!newVal && oldVal) {
|
|
// Leaving: immediately stop animation and hide to release GPU resources
|
|
hasScattered.value = false
|
|
isScattering.value = false // Force stop scattering transition immediately
|
|
isFadingOut.value = true
|
|
|
|
// Wait for fade-out transition (150ms) to complete before cleanup
|
|
// Use a single timeout matching the CSS transition duration
|
|
setTimeout(() => {
|
|
if (!props.isVisible) {
|
|
isFullyHidden.value = true
|
|
isFadingOut.value = false
|
|
// Clear star arrays to fully release memory
|
|
farStars.value = []
|
|
midStars.value = []
|
|
nearStars.value = []
|
|
}
|
|
}, 180) // Slightly longer than CSS transition (150ms) to ensure smooth fade
|
|
}
|
|
}, { flush: 'post' }) // Change to post flush to ensure DOM is updated before our cleanup logic runs
|
|
|
|
onMounted(() => {
|
|
createStarLayers()
|
|
if (props.isVisible) {
|
|
isFullyHidden.value = false
|
|
setTimeout(() => {
|
|
scatterStars()
|
|
}, 100)
|
|
} else {
|
|
// Start hidden if not visible
|
|
isFullyHidden.value = true
|
|
}
|
|
})
|
|
|
|
onUnmounted(() => {
|
|
// No explicit cleanup needed for CSS animations
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
.gemini-star-cluster {
|
|
perspective: 800px;
|
|
}
|
|
|
|
.stars-layer {
|
|
position: absolute;
|
|
inset: 0;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.far-layer {
|
|
z-index: 1;
|
|
}
|
|
|
|
.mid-layer {
|
|
z-index: 2;
|
|
}
|
|
|
|
.near-layer {
|
|
z-index: 3;
|
|
}
|
|
|
|
.star-svg {
|
|
width: 100%;
|
|
height: 100%;
|
|
overflow: visible;
|
|
}
|
|
|
|
.star-wrapper {
|
|
position: absolute;
|
|
opacity: 0;
|
|
transform: scale(0.3) translate(-50%, -50%);
|
|
transition:
|
|
opacity 0.8s ease-out,
|
|
transform 0.8s ease-out;
|
|
/* Avoid persistent will-change to reduce GPU memory usage */
|
|
}
|
|
|
|
.star-wrapper.star-visible {
|
|
opacity: 0; /* Default to invisible, animation handles opacity */
|
|
transform: scale(1) translate(-50%, -50%);
|
|
animation: twinkle var(--twinkle-duration) ease-in-out infinite;
|
|
animation-delay: var(--twinkle-delay);
|
|
}
|
|
|
|
@keyframes twinkle {
|
|
0% {
|
|
opacity: 0;
|
|
transform: scale(0.5) translate(-50%, -50%);
|
|
}
|
|
50% {
|
|
opacity: var(--base-opacity);
|
|
transform: scale(1) translate(-50%, -50%);
|
|
filter: brightness(1.2);
|
|
}
|
|
100% {
|
|
opacity: 0;
|
|
transform: scale(0.5) translate(-50%, -50%);
|
|
}
|
|
}
|
|
|
|
/* Scatter animation - stars fly outward from center (2s duration) */
|
|
.scattering .star-wrapper {
|
|
transition:
|
|
opacity 0.8s ease-out,
|
|
transform 0.8s ease-out,
|
|
left 2s cubic-bezier(0.16, 1, 0.3, 1),
|
|
top 2s cubic-bezier(0.16, 1, 0.3, 1);
|
|
/* Only use will-change during active scatter animation */
|
|
will-change: opacity, transform, left, top;
|
|
}
|
|
|
|
/* Fade out animation - quick fade and disable child transitions */
|
|
.fading-out {
|
|
opacity: 0;
|
|
transition: opacity 0.15s ease-out;
|
|
pointer-events: none;
|
|
/* Force GPU layer removal */
|
|
will-change: auto;
|
|
}
|
|
|
|
.fading-out .star-wrapper {
|
|
transition: none !important;
|
|
will-change: auto !important;
|
|
animation: none !important;
|
|
opacity: 0 !important;
|
|
}
|
|
|
|
/* Depth blur effect */
|
|
.far-layer .star-wrapper {
|
|
filter: blur(0.5px);
|
|
}
|
|
|
|
.mid-layer .star-wrapper {
|
|
filter: blur(0.2px);
|
|
}
|
|
|
|
.near-layer .star-wrapper {
|
|
filter: none;
|
|
}
|
|
</style>
|