496 lines
14 KiB
Vue
496 lines
14 KiB
Vue
|
|
<template>
|
||
|
|
<Transition name="splash-fade">
|
||
|
|
<div v-if="showSplash" class="fixed inset-0 z-[2000] flex items-center justify-center bg-black" style="will-change: opacity, transform;">
|
||
|
|
<!-- Video background - shown during Welcome Noderunner and Logo (seamless, no zoom) -->
|
||
|
|
<video
|
||
|
|
v-if="showWelcome || showLogo"
|
||
|
|
ref="videoElement"
|
||
|
|
class="absolute inset-0 w-full h-full object-cover"
|
||
|
|
:style="{ opacity: backgroundOpacity, transform: 'scale(1)', transition: 'opacity 1.2s ease-out' }"
|
||
|
|
autoplay
|
||
|
|
loop
|
||
|
|
muted
|
||
|
|
playsinline
|
||
|
|
preload="auto"
|
||
|
|
poster="/assets/img/bg-4.jpg"
|
||
|
|
>
|
||
|
|
<source src="/assets/video/video-intro.mp4?v=7" type="video/mp4">
|
||
|
|
<!-- Fallback to image if video fails -->
|
||
|
|
<div
|
||
|
|
class="absolute inset-0"
|
||
|
|
:style="{
|
||
|
|
backgroundImage: 'url(/assets/img/bg-4.jpg)',
|
||
|
|
backgroundSize: 'auto 100vh',
|
||
|
|
backgroundPosition: 'center top',
|
||
|
|
backgroundRepeat: 'no-repeat',
|
||
|
|
}"
|
||
|
|
/>
|
||
|
|
</video>
|
||
|
|
|
||
|
|
<!-- Static image background - shown during alien intro -->
|
||
|
|
<div
|
||
|
|
v-else
|
||
|
|
class="absolute inset-0"
|
||
|
|
:style="{
|
||
|
|
backgroundImage: 'url(/assets/img/bg-4.jpg)',
|
||
|
|
backgroundSize: 'auto 100vh',
|
||
|
|
backgroundPosition: 'center top',
|
||
|
|
backgroundRepeat: 'no-repeat',
|
||
|
|
opacity: backgroundOpacity,
|
||
|
|
transform: 'scale(1)',
|
||
|
|
transition: 'opacity 1.2s ease-out',
|
||
|
|
}"
|
||
|
|
/>
|
||
|
|
|
||
|
|
<!-- Alien Intro -->
|
||
|
|
<Transition name="fade">
|
||
|
|
<div
|
||
|
|
v-if="!alienIntroComplete"
|
||
|
|
class="absolute inset-0 z-10 flex items-center justify-center transition-opacity duration-800"
|
||
|
|
:class="{ 'opacity-0': fadeAlienIntro }"
|
||
|
|
>
|
||
|
|
<div class="font-mono text-white px-4 sm:px-5 max-w-[95vw] sm:max-w-[90vw] md:max-w-[1200px] text-base sm:text-lg md:text-[24px] leading-relaxed break-words">
|
||
|
|
<div v-if="showLine1" class="flex items-start mb-4 sm:mb-6 opacity-0" :class="{ 'opacity-100': showLine1 }">
|
||
|
|
<span class="text-[#00ffff] mr-3 sm:mr-6 flex-shrink-0">></span>
|
||
|
|
<span class="text-white break-words" :class="{ 'typing-text': typingLine1 }">
|
||
|
|
In the future there will be 3 types of humans
|
||
|
|
</span>
|
||
|
|
</div>
|
||
|
|
<div v-if="showLine2" class="flex items-start mb-4 sm:mb-6 opacity-0" :class="{ 'opacity-100': showLine2 }">
|
||
|
|
<span class="text-[#00ffff] mr-3 sm:mr-6 flex-shrink-0">></span>
|
||
|
|
<span class="text-white break-words" :class="{ 'typing-text': typingLine2 }">
|
||
|
|
Government Employees
|
||
|
|
</span>
|
||
|
|
</div>
|
||
|
|
<div v-if="showLine3" class="flex items-start mb-4 sm:mb-6 opacity-0" :class="{ 'opacity-100': showLine3 }">
|
||
|
|
<span class="text-[#00ffff] mr-3 sm:mr-6 flex-shrink-0">></span>
|
||
|
|
<span class="text-white break-words" :class="{ 'typing-text': typingLine3 }">
|
||
|
|
Corporate Employees
|
||
|
|
</span>
|
||
|
|
</div>
|
||
|
|
<div v-if="showLine4" class="flex items-start mb-8 sm:mb-12 opacity-0" :class="{ 'opacity-100': showLine4 }">
|
||
|
|
<span class="text-[#00ffff] mr-3 sm:mr-6 flex-shrink-0">></span>
|
||
|
|
<span class="text-white break-words" :class="{ 'typing-text': typingLine4 }">
|
||
|
|
And Noderunners...
|
||
|
|
</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</Transition>
|
||
|
|
|
||
|
|
<!-- Welcome Message -->
|
||
|
|
<Transition name="welcome-fade">
|
||
|
|
<div
|
||
|
|
v-if="showWelcome"
|
||
|
|
class="absolute inset-0 z-[15] flex items-center justify-center font-mono text-3xl sm:text-4xl md:text-5xl px-4"
|
||
|
|
:class="{ 'welcome-fade-out': fadeWelcome }"
|
||
|
|
>
|
||
|
|
<div class="typing-container">
|
||
|
|
<span class="text-white" :class="{ 'typing-text': typingWelcome }">
|
||
|
|
Welcome Noderunner
|
||
|
|
</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</Transition>
|
||
|
|
|
||
|
|
<!-- Logo - Archipelago logo for splash -->
|
||
|
|
<Transition name="logo-zoom">
|
||
|
|
<div v-if="showLogo" class="relative z-20 logo-container">
|
||
|
|
<img
|
||
|
|
src="/assets/img/logo-archipelago.svg"
|
||
|
|
alt="Archipelago"
|
||
|
|
class="w-[min(80vw,900px)] max-w-[90vw] h-auto filter drop-shadow-[0_6px_24px_rgba(0,0,0,0.35)] m-5 object-contain logo-zoom-bounce"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
</Transition>
|
||
|
|
|
||
|
|
<!-- Skip Button -->
|
||
|
|
<button
|
||
|
|
v-if="!alienIntroComplete"
|
||
|
|
@click="skipIntro"
|
||
|
|
class="absolute bottom-8 right-8 z-20 bg-black/60 border border-white/30 text-white/70 font-mono text-xs px-4 py-2 rounded backdrop-blur-[10px] hover:bg-black/80 hover:text-white/90 hover:border-white/50 hover:-translate-y-0.5 active:translate-y-0 transition-all duration-300"
|
||
|
|
>
|
||
|
|
Skip Intro
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</Transition>
|
||
|
|
</template>
|
||
|
|
|
||
|
|
<script setup lang="ts">
|
||
|
|
import { ref, onMounted, watch } from 'vue'
|
||
|
|
|
||
|
|
const emit = defineEmits<{
|
||
|
|
complete: []
|
||
|
|
}>()
|
||
|
|
|
||
|
|
const showSplash = ref(true)
|
||
|
|
const backgroundOpacity = ref(0)
|
||
|
|
const alienIntroComplete = ref(false)
|
||
|
|
const fadeAlienIntro = ref(false)
|
||
|
|
const showWelcome = ref(false)
|
||
|
|
const fadeWelcome = ref(false)
|
||
|
|
const typingWelcome = ref(false)
|
||
|
|
const showLogo = ref(false)
|
||
|
|
const showLine1 = ref(false)
|
||
|
|
const showLine2 = ref(false)
|
||
|
|
const showLine3 = ref(false)
|
||
|
|
const showLine4 = ref(false)
|
||
|
|
const typingLine1 = ref(false)
|
||
|
|
const typingLine2 = ref(false)
|
||
|
|
const typingLine3 = ref(false)
|
||
|
|
const typingLine4 = ref(false)
|
||
|
|
const videoElement = ref<HTMLVideoElement | null>(null)
|
||
|
|
|
||
|
|
// Ensure video plays continuously from Welcome Noderunner through logo
|
||
|
|
watch([showWelcome, showLogo], ([welcome, logo]) => {
|
||
|
|
if ((welcome || logo) && videoElement.value) {
|
||
|
|
// Ensure video is playing and doesn't pause
|
||
|
|
if (videoElement.value.paused) {
|
||
|
|
videoElement.value.play().catch(err => {
|
||
|
|
console.warn('Video autoplay failed:', err)
|
||
|
|
})
|
||
|
|
}
|
||
|
|
// Keep video playing - prevent any pauses
|
||
|
|
videoElement.value.addEventListener('pause', (e) => {
|
||
|
|
if (welcome || logo) {
|
||
|
|
e.preventDefault()
|
||
|
|
videoElement.value?.play()
|
||
|
|
}
|
||
|
|
}, { once: false })
|
||
|
|
}
|
||
|
|
})
|
||
|
|
|
||
|
|
// Start video as soon as welcome appears
|
||
|
|
watch(showWelcome, (isShowing) => {
|
||
|
|
if (isShowing && videoElement.value) {
|
||
|
|
// Start video immediately when welcome appears
|
||
|
|
videoElement.value.play().catch(err => {
|
||
|
|
console.warn('Video autoplay failed on welcome:', err)
|
||
|
|
})
|
||
|
|
}
|
||
|
|
})
|
||
|
|
|
||
|
|
// Store video currentTime continuously and before unmounting for seamless transition
|
||
|
|
watch(showSplash, (isShowing) => {
|
||
|
|
if (!isShowing && videoElement.value) {
|
||
|
|
// Store current video time for seamless transition
|
||
|
|
const currentTime = videoElement.value.currentTime
|
||
|
|
const wasPlaying = !videoElement.value.paused
|
||
|
|
sessionStorage.setItem('video_intro_currentTime', currentTime.toString())
|
||
|
|
sessionStorage.setItem('video_intro_wasPlaying', wasPlaying.toString())
|
||
|
|
// Store video playback rate to maintain smooth playback
|
||
|
|
sessionStorage.setItem('video_intro_playbackRate', videoElement.value.playbackRate.toString())
|
||
|
|
}
|
||
|
|
})
|
||
|
|
|
||
|
|
// Continuously update video time while playing (for more accurate restoration)
|
||
|
|
let videoTimeUpdateInterval: number | null = null
|
||
|
|
watch([showWelcome, showLogo], ([welcome, logo]) => {
|
||
|
|
if ((welcome || logo) && videoElement.value) {
|
||
|
|
// Update stored time every 50ms for better accuracy and smoother transition
|
||
|
|
videoTimeUpdateInterval = window.setInterval(() => {
|
||
|
|
if (videoElement.value && !videoElement.value.paused) {
|
||
|
|
sessionStorage.setItem('video_intro_currentTime', videoElement.value.currentTime.toString())
|
||
|
|
sessionStorage.setItem('video_intro_wasPlaying', 'true')
|
||
|
|
sessionStorage.setItem('video_intro_playbackRate', videoElement.value.playbackRate.toString())
|
||
|
|
}
|
||
|
|
}, 50) // More frequent updates for smoother transition
|
||
|
|
} else {
|
||
|
|
if (videoTimeUpdateInterval) {
|
||
|
|
clearInterval(videoTimeUpdateInterval)
|
||
|
|
videoTimeUpdateInterval = null
|
||
|
|
}
|
||
|
|
}
|
||
|
|
})
|
||
|
|
|
||
|
|
// Check if user has seen intro
|
||
|
|
const seenIntro = localStorage.getItem('neode_intro_seen') === '1'
|
||
|
|
|
||
|
|
function skipIntro() {
|
||
|
|
// Jump to "Welcome Noderunner" part
|
||
|
|
alienIntroComplete.value = true
|
||
|
|
fadeAlienIntro.value = true
|
||
|
|
showWelcome.value = true
|
||
|
|
typingWelcome.value = true
|
||
|
|
|
||
|
|
// Stop alien intro typing animations
|
||
|
|
typingLine1.value = false
|
||
|
|
typingLine2.value = false
|
||
|
|
typingLine3.value = false
|
||
|
|
typingLine4.value = false
|
||
|
|
|
||
|
|
// Start background fade in at 0.3 opacity when welcome appears
|
||
|
|
setTimeout(() => {
|
||
|
|
backgroundOpacity.value = 0.3
|
||
|
|
}, 0)
|
||
|
|
|
||
|
|
// Continue with welcome fade out after typing (2s) + cursor continues (1.5s) + 3 blinks (1.35s)
|
||
|
|
setTimeout(() => {
|
||
|
|
fadeWelcome.value = true
|
||
|
|
typingWelcome.value = false
|
||
|
|
}, 4850)
|
||
|
|
|
||
|
|
// Show logo - no zoom, just fade
|
||
|
|
setTimeout(() => {
|
||
|
|
showLogo.value = true
|
||
|
|
// Keep background at 0.3 opacity during logo display
|
||
|
|
}, 5500)
|
||
|
|
|
||
|
|
// Hide welcome after logo starts appearing
|
||
|
|
setTimeout(() => {
|
||
|
|
showWelcome.value = false
|
||
|
|
}, 6000)
|
||
|
|
|
||
|
|
// Fade background to full opacity just before completing (for smooth transition to modal)
|
||
|
|
setTimeout(() => {
|
||
|
|
backgroundOpacity.value = 1
|
||
|
|
}, 9000)
|
||
|
|
|
||
|
|
// Complete splash with smooth transition - wait for zoom to complete
|
||
|
|
setTimeout(() => {
|
||
|
|
// Add a small delay to ensure smooth transition
|
||
|
|
setTimeout(() => {
|
||
|
|
showSplash.value = false
|
||
|
|
document.body.classList.add('splash-complete')
|
||
|
|
localStorage.setItem('neode_intro_seen', '1')
|
||
|
|
emit('complete')
|
||
|
|
}, 500)
|
||
|
|
}, 9500)
|
||
|
|
}
|
||
|
|
|
||
|
|
function startAlienIntro() {
|
||
|
|
// Line 1 - types and blinks
|
||
|
|
setTimeout(() => {
|
||
|
|
showLine1.value = true
|
||
|
|
typingLine1.value = true
|
||
|
|
}, 500)
|
||
|
|
|
||
|
|
// Line 2 - wait for line 1 typing (4s) + blinking (1.5s)
|
||
|
|
setTimeout(() => {
|
||
|
|
typingLine1.value = false
|
||
|
|
showLine2.value = true
|
||
|
|
typingLine2.value = true
|
||
|
|
}, 6000)
|
||
|
|
|
||
|
|
// Line 3 - wait for line 2 typing (4s) + blinking (1.5s)
|
||
|
|
setTimeout(() => {
|
||
|
|
typingLine2.value = false
|
||
|
|
showLine3.value = true
|
||
|
|
typingLine3.value = true
|
||
|
|
}, 11500)
|
||
|
|
|
||
|
|
// Line 4 - wait for line 3 typing (4s) + blinking (1.5s)
|
||
|
|
setTimeout(() => {
|
||
|
|
typingLine3.value = false
|
||
|
|
showLine4.value = true
|
||
|
|
typingLine4.value = true
|
||
|
|
}, 17000)
|
||
|
|
|
||
|
|
// Fade out alien intro - wait for line 4 typing (4s) + blinking (1.5s)
|
||
|
|
setTimeout(() => {
|
||
|
|
typingLine4.value = false
|
||
|
|
fadeAlienIntro.value = true
|
||
|
|
}, 22500)
|
||
|
|
|
||
|
|
// Show welcome and start video immediately
|
||
|
|
setTimeout(() => {
|
||
|
|
alienIntroComplete.value = true
|
||
|
|
showWelcome.value = true
|
||
|
|
typingWelcome.value = true
|
||
|
|
// Start video immediately when welcome appears
|
||
|
|
if (videoElement.value) {
|
||
|
|
videoElement.value.play().catch(err => {
|
||
|
|
console.warn('Video autoplay failed on welcome:', err)
|
||
|
|
})
|
||
|
|
}
|
||
|
|
}, 23300)
|
||
|
|
|
||
|
|
// Start background fade in at 0.3 opacity when welcome appears
|
||
|
|
setTimeout(() => {
|
||
|
|
backgroundOpacity.value = 0.3
|
||
|
|
}, 23300)
|
||
|
|
|
||
|
|
// Fade out welcome - typing (2s) + cursor continues (1.5s) + 3 blinks (1.35s) = 4.85s
|
||
|
|
setTimeout(() => {
|
||
|
|
fadeWelcome.value = true
|
||
|
|
typingWelcome.value = false
|
||
|
|
}, 28150)
|
||
|
|
|
||
|
|
// Show logo - background stays at 0.3 opacity
|
||
|
|
setTimeout(() => {
|
||
|
|
showLogo.value = true
|
||
|
|
}, 29000)
|
||
|
|
|
||
|
|
// Hide welcome after logo starts appearing
|
||
|
|
setTimeout(() => {
|
||
|
|
showWelcome.value = false
|
||
|
|
}, 30500)
|
||
|
|
|
||
|
|
// Fade background to full opacity just before completing (for smooth transition to modal)
|
||
|
|
setTimeout(() => {
|
||
|
|
backgroundOpacity.value = 1
|
||
|
|
}, 33000)
|
||
|
|
|
||
|
|
// Complete splash with smooth transition
|
||
|
|
setTimeout(() => {
|
||
|
|
// Store final video time right before unmounting
|
||
|
|
if (videoElement.value && !videoElement.value.paused) {
|
||
|
|
sessionStorage.setItem('video_intro_currentTime', videoElement.value.currentTime.toString())
|
||
|
|
sessionStorage.setItem('video_intro_wasPlaying', 'true')
|
||
|
|
}
|
||
|
|
showSplash.value = false
|
||
|
|
document.body.classList.add('splash-complete')
|
||
|
|
localStorage.setItem('neode_intro_seen', '1')
|
||
|
|
emit('complete')
|
||
|
|
}, 34500)
|
||
|
|
}
|
||
|
|
|
||
|
|
onMounted(() => {
|
||
|
|
if (seenIntro) {
|
||
|
|
// Skip intro if already seen
|
||
|
|
showSplash.value = false
|
||
|
|
document.body.classList.add('splash-complete')
|
||
|
|
emit('complete')
|
||
|
|
} else {
|
||
|
|
// Play intro
|
||
|
|
startAlienIntro()
|
||
|
|
}
|
||
|
|
})
|
||
|
|
</script>
|
||
|
|
|
||
|
|
<style scoped>
|
||
|
|
.splash-fade-enter-active {
|
||
|
|
transition: opacity 0.5s ease;
|
||
|
|
}
|
||
|
|
|
||
|
|
.splash-fade-leave-active {
|
||
|
|
transition: opacity 1s ease-out, transform 1s ease-out;
|
||
|
|
}
|
||
|
|
|
||
|
|
.splash-fade-enter-from,
|
||
|
|
.splash-fade-leave-to {
|
||
|
|
opacity: 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
.splash-fade-leave-to {
|
||
|
|
transform: scale(1.1);
|
||
|
|
}
|
||
|
|
|
||
|
|
.fade-enter-active,
|
||
|
|
.fade-leave-active {
|
||
|
|
transition: opacity 0.5s ease;
|
||
|
|
}
|
||
|
|
|
||
|
|
.fade-enter-from,
|
||
|
|
.fade-leave-to {
|
||
|
|
opacity: 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* Welcome message fade out */
|
||
|
|
.welcome-fade-enter-active {
|
||
|
|
transition: opacity 0.8s ease-out;
|
||
|
|
}
|
||
|
|
|
||
|
|
.welcome-fade-leave-active {
|
||
|
|
transition: opacity 0.6s ease-in;
|
||
|
|
}
|
||
|
|
|
||
|
|
.welcome-fade-enter-from,
|
||
|
|
.welcome-fade-leave-to {
|
||
|
|
opacity: 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
.welcome-fade-out {
|
||
|
|
opacity: 0;
|
||
|
|
transition: opacity 0.6s ease-in;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* Logo zoom bounce animation - smooth and buttery */
|
||
|
|
.logo-zoom-enter-active {
|
||
|
|
transition: all 1s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||
|
|
}
|
||
|
|
|
||
|
|
.logo-zoom-leave-active {
|
||
|
|
transition: opacity 0.5s ease-out, transform 0.5s ease-out;
|
||
|
|
}
|
||
|
|
|
||
|
|
.logo-zoom-enter-from {
|
||
|
|
opacity: 0;
|
||
|
|
transform: scale(0.7);
|
||
|
|
}
|
||
|
|
|
||
|
|
.logo-zoom-enter-to {
|
||
|
|
opacity: 1;
|
||
|
|
transform: scale(1);
|
||
|
|
}
|
||
|
|
|
||
|
|
.logo-zoom-leave-from {
|
||
|
|
opacity: 1;
|
||
|
|
transform: scale(1);
|
||
|
|
}
|
||
|
|
|
||
|
|
.logo-zoom-leave-to {
|
||
|
|
opacity: 0;
|
||
|
|
transform: scale(1.05);
|
||
|
|
}
|
||
|
|
|
||
|
|
.logo-zoom-bounce {
|
||
|
|
animation: logoZoomBounce 1.2s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards;
|
||
|
|
}
|
||
|
|
|
||
|
|
@keyframes logoZoomBounce {
|
||
|
|
0% {
|
||
|
|
transform: scale(0.85);
|
||
|
|
opacity: 0;
|
||
|
|
}
|
||
|
|
50% {
|
||
|
|
transform: scale(1.02);
|
||
|
|
opacity: 0.9;
|
||
|
|
}
|
||
|
|
75% {
|
||
|
|
transform: scale(0.98);
|
||
|
|
opacity: 0.95;
|
||
|
|
}
|
||
|
|
100% {
|
||
|
|
transform: scale(1);
|
||
|
|
opacity: 1;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/* Container to keep the typing text centered */
|
||
|
|
.typing-container {
|
||
|
|
display: flex;
|
||
|
|
justify-content: center;
|
||
|
|
align-items: center;
|
||
|
|
min-width: 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* Ensure text wraps smoothly on mobile */
|
||
|
|
.font-mono {
|
||
|
|
word-wrap: break-word;
|
||
|
|
overflow-wrap: break-word;
|
||
|
|
hyphens: auto;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* Smooth line transitions for mobile */
|
||
|
|
@media (max-width: 640px) {
|
||
|
|
.font-mono span {
|
||
|
|
display: inline-block;
|
||
|
|
max-width: 100%;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/* Background zoom transition - matches OnboardingWrapper style */
|
||
|
|
.bg-zoom-transition {
|
||
|
|
transition: transform 1.5s cubic-bezier(0.4, 0, 0.2, 1), opacity 1.2s ease-out;
|
||
|
|
transform: scale(1);
|
||
|
|
transform-origin: center center;
|
||
|
|
will-change: transform, opacity;
|
||
|
|
}
|
||
|
|
|
||
|
|
.bg-zoom-transition.bg-zoom-in {
|
||
|
|
transform: scale(1.15);
|
||
|
|
}
|
||
|
|
</style>
|
||
|
|
|