archy/neode-ui/src/views/OnboardingWrapper.vue

472 lines
14 KiB
Vue
Raw Normal View History

2026-01-24 22:59:20 +00:00
<template>
<div class="min-h-screen relative overflow-hidden">
<!-- Background layers with 3D perspective and zoom effect -->
<div class="bg-perspective-container">
<!-- Video background for intro/login routes (smooth transition from splash) -->
<video
v-if="useVideoBackground"
ref="videoElement"
class="bg-layer"
autoplay
loop
muted
playsinline
preload="auto"
poster="/assets/img/bg-4.jpg"
style="width: 100%; height: 100%; object-fit: cover; object-position: center; position: absolute; inset: 0; transform: scale(1); transition: none;"
@pause.prevent="handleVideoPause"
@ended="handleVideoEnded"
>
<source src="/assets/video/video-intro.mp4?v=7" type="video/mp4">
</video>
<!-- Static image background for other routes (not using video) -->
<div
v-else
class="bg-layer bg-zoom"
:class="{ 'bg-zoom-in': isTransitioning }"
:style="{ backgroundImage: `url('/assets/img/${currentBackground}')` }"
:key="currentBackground"
></div>
<!-- Glitch overlay layer - only for non-video backgrounds -->
<div v-show="isGlitching && !useVideoBackground" class="bg-glitch-layer"></div>
</div>
<!-- Content with 3D transitions -->
<div class="perspective-container-wrapper">
<div class="perspective-container">
<RouterView v-slot="{ Component, route }">
<Transition name="depth-forward">
<div :key="route.path" class="view-wrapper">
<component :is="Component" class="view-container" />
</div>
</Transition>
</RouterView>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, watch, onMounted, computed } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
const currentBackground = ref('bg-4.jpg')
const isGlitching = ref(false)
const isTransitioning = ref(false)
const videoElement = ref<HTMLVideoElement | null>(null)
// Routes that should use video background (smooth transition from splash)
const videoBackgroundRoutes = ['/onboarding/intro', '/login']
// Check if current route should use video background
const useVideoBackground = computed(() => {
return videoBackgroundRoutes.includes(route.path)
})
// Map each route to a specific background image
// Note: bg-4.jpg is used for splash and /onboarding/intro for seamless transition
const routeBackgrounds: Record<string, string> = {
'/onboarding/intro': 'bg-4.jpg', // Video will be used instead
'/onboarding/options': 'bg-5.jpg',
'/onboarding/path': 'bg-3.jpg',
'/onboarding/did': 'bg-6.jpg',
'/onboarding/backup': 'bg-7.jpg',
'/onboarding/verify': 'bg-2.jpg',
'/onboarding/done': 'bg-1.jpg',
'/login': 'bg-1.jpg' // Video will be used instead
}
// Restore video time from splash screen for seamless transition
function restoreVideoTime() {
if (videoElement.value && useVideoBackground.value) {
const savedTime = sessionStorage.getItem('video_intro_currentTime')
const wasPlaying = sessionStorage.getItem('video_intro_wasPlaying') === 'true'
const savedPlaybackRate = sessionStorage.getItem('video_intro_playbackRate')
if (savedTime) {
const time = parseFloat(savedTime)
const playbackRate = savedPlaybackRate ? parseFloat(savedPlaybackRate) : 1.0
const setVideoTime = () => {
if (videoElement.value) {
// Set playback rate first for smooth playback
videoElement.value.playbackRate = playbackRate
// Set time with slight offset to ensure smooth transition (avoid frame boundary issues)
videoElement.value.currentTime = Math.max(0, time - 0.05)
// If video was playing, ensure it continues playing immediately
if (wasPlaying) {
// Use requestAnimationFrame for smoother transition
requestAnimationFrame(() => {
if (videoElement.value) {
videoElement.value.play().catch(err => {
console.warn('Video play failed after time restore:', err)
})
}
})
}
// Clean up session storage after successful restore
sessionStorage.removeItem('video_intro_currentTime')
sessionStorage.removeItem('video_intro_wasPlaying')
sessionStorage.removeItem('video_intro_playbackRate')
}
}
if (videoElement.value.readyState >= 2) {
// Video can play through current position, set time immediately
setVideoTime()
} else if (videoElement.value.readyState >= 1) {
// Video metadata loaded, set time immediately
setVideoTime()
} else {
// Wait for metadata to load
const handleLoadedMetadata = () => {
setVideoTime()
if (videoElement.value) {
videoElement.value.removeEventListener('loadedmetadata', handleLoadedMetadata)
}
}
videoElement.value.addEventListener('loadedmetadata', handleLoadedMetadata, { once: true })
}
}
}
}
// Ensure video plays when route uses video background
watch([useVideoBackground, route], ([useVideo]) => {
if (useVideo && videoElement.value) {
// Use requestAnimationFrame for smoother transition
requestAnimationFrame(() => {
if (videoElement.value) {
// Restore video time for seamless transition first
restoreVideoTime()
// Then ensure it's playing - use double RAF for smoother transition
requestAnimationFrame(() => {
if (videoElement.value && videoElement.value.paused) {
videoElement.value.play().catch(err => {
console.warn('Video autoplay failed:', err)
})
}
})
}
})
}
}, { immediate: true })
// Also ensure video plays on mount if route uses video
onMounted(() => {
if (useVideoBackground.value && videoElement.value) {
// Use requestAnimationFrame for smoother transition
requestAnimationFrame(() => {
if (videoElement.value) {
// Restore video time for seamless transition
restoreVideoTime()
// Use double RAF for smoother playback start
requestAnimationFrame(() => {
if (videoElement.value) {
videoElement.value.play().catch(err => {
console.warn('Video autoplay failed on mount:', err)
})
}
})
}
})
}
})
// Watch for video element to restore time when it becomes available
watch(videoElement, (element) => {
if (element && useVideoBackground.value) {
// Use requestAnimationFrame for smoother transition
requestAnimationFrame(() => {
if (element) {
// Try to restore immediately if metadata already loaded
if (element.readyState >= 2) {
restoreVideoTime()
} else if (element.readyState >= 1) {
restoreVideoTime()
} else {
// Wait for metadata to load
const handleLoadedMetadata = () => {
restoreVideoTime()
element.removeEventListener('loadedmetadata', handleLoadedMetadata)
}
element.addEventListener('loadedmetadata', handleLoadedMetadata, { once: true })
}
}
})
}
})
// Watch route changes for background swaps, zoom, and glitch
watch(() => route.path, (newPath, oldPath) => {
const newBg = routeBackgrounds[newPath]
const oldUsesVideo = videoBackgroundRoutes.includes(oldPath || '')
const newUsesVideo = videoBackgroundRoutes.includes(newPath)
// If both old and new routes use video, don't restart video - keep it playing seamlessly
if (oldUsesVideo && newUsesVideo && videoElement.value) {
// Video continues seamlessly, just ensure it's playing
if (videoElement.value.paused) {
videoElement.value.play().catch(err => {
console.warn('Video play failed on route change:', err)
})
}
// No glitch effect, no zoom, no transitions for video-to-video
isGlitching.value = false
isTransitioning.value = false
return // Skip background change logic for video-to-video transitions
}
// If transitioning from video to non-video or vice versa, no glitch, no zoom (smooth transition)
if (oldUsesVideo || newUsesVideo) {
isGlitching.value = false
isTransitioning.value = false
}
// Only update if we have a defined background for this route and it's different
if (newBg && newBg !== currentBackground.value) {
// Trigger zoom animation ONLY for non-video routes (never for video)
if (!newUsesVideo && !oldUsesVideo) {
isTransitioning.value = true
// Change background
currentBackground.value = newBg
// Only trigger glitch for non-video background changes
setTimeout(() => {
isGlitching.value = true
setTimeout(() => {
isGlitching.value = false
}, 500) // Match glitch duration
// Reset zoom after glitch
isTransitioning.value = false
}, 1500 + 50) // Wait for 3D transition (1500ms) + small delay - matches splash timing
} else {
// Smooth transition for video routes - no glitch, no zoom, no effects at all
currentBackground.value = newBg
isTransitioning.value = false
isGlitching.value = false
}
}
})
// Prevent video from pausing during transitions
function handleVideoPause(event: Event) {
if (useVideoBackground.value && videoElement.value) {
// Prevent pause and immediately resume playback
event.preventDefault()
videoElement.value.play().catch(err => {
console.warn('Video play failed after pause prevention:', err)
})
}
}
// Handle video ended - restart immediately for seamless loop
function handleVideoEnded() {
if (useVideoBackground.value && videoElement.value) {
videoElement.value.currentTime = 0
videoElement.value.play().catch(err => {
console.warn('Video play failed after loop:', err)
})
}
}
// Update body class to disable global glitch effects ONLY for video backgrounds
// This class is ONLY added on /onboarding/intro and /login routes
// All other routes will have glitch effects enabled (normal behavior)
watch(useVideoBackground, (usesVideo) => {
if (usesVideo) {
// Add class ONLY on video background screens (/onboarding/intro, /login)
// This disables glitch effects ONLY on these screens
document.body.classList.add('video-background-active')
} else {
// Remove class on all other screens to re-enable glitch effects
document.body.classList.remove('video-background-active')
}
}, { immediate: true })
// Initialize background on mount based on current route
onMounted(() => {
const bg = routeBackgrounds[route.path]
if (bg) {
currentBackground.value = bg
}
// Ensure no transitions or effects on mount for video backgrounds
if (useVideoBackground.value) {
isTransitioning.value = false
isGlitching.value = false
document.body.classList.add('video-background-active')
}
})
</script>
<style scoped>
/* Wrapper to contain perspective without clipping */
.perspective-container-wrapper {
position: relative;
overflow: hidden;
min-height: 100vh;
}
/* Perspective container for 3D depth effect */
.perspective-container {
perspective: 1200px;
perspective-origin: 50% 50%;
position: relative;
min-height: 100vh;
overflow: hidden;
}
/* View wrapper - allows smooth transitions with absolute positioning */
.view-wrapper {
position: absolute;
inset: 0;
transform-style: preserve-3d;
backface-visibility: hidden;
will-change: transform, opacity;
}
.view-container {
height: 100%;
}
/* Forward transition: Current screen pulls forward, new screen emerges from back */
.depth-forward-enter-active.view-wrapper,
.depth-forward-leave-active.view-wrapper {
transition: all 0.7s cubic-bezier(0.68, -0.55, 0.265, 1.55);
}
.depth-forward-enter-from.view-wrapper {
opacity: 0;
transform: translateZ(-1500px) scale(0.5);
filter: blur(8px);
}
.depth-forward-enter-to.view-wrapper {
opacity: 1;
transform: translateZ(0) scale(1);
filter: blur(0px);
}
.depth-forward-leave-from.view-wrapper {
opacity: 1;
transform: translateZ(0) scale(1);
filter: blur(0px);
}
.depth-forward-leave-to.view-wrapper {
opacity: 0;
transform: translateZ(600px) scale(1.4);
filter: blur(12px);
}
/* Background zoom effect - makes you feel like you're going deeper */
.bg-zoom {
transition: transform 1.5s cubic-bezier(0.4, 0, 0.2, 1);
transform: scale(1);
}
.bg-zoom-in {
transform: scale(1.15);
}
/* Enhanced effect with rotation for more console-like feel */
@media (min-width: 768px) {
.depth-forward-enter-from.view-wrapper {
transform: translateZ(-1500px) scale(0.5) rotateX(15deg);
}
.depth-forward-leave-to.view-wrapper {
transform: translateZ(600px) scale(1.4) rotateX(-10deg);
}
}
/* Background 3D container */
.bg-perspective-container {
position: fixed;
inset: 0;
perspective: 1000px;
perspective-origin: 50% 50%;
z-index: -10;
overflow: hidden;
}
.bg-layer {
position: absolute;
inset: 0;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
transform-style: preserve-3d;
backface-visibility: hidden;
}
/* Video background styling - video element itself has bg-layer class */
.bg-layer video,
video.bg-layer {
width: 100%;
height: 100%;
object-fit: cover;
object-position: center;
position: absolute;
inset: 0;
}
.bg-static {
opacity: 1;
transform: translateZ(0) scale(1);
}
/* Glitch overlay layer */
.bg-glitch-layer {
position: absolute;
inset: 0;
pointer-events: none;
z-index: 10;
background: rgba(255, 255, 255, 0.1);
mix-blend-mode: overlay;
opacity: 0;
animation: bg-glitch-flash 500ms ease-in-out;
}
@keyframes bg-glitch-flash {
0%, 100% {
opacity: 0;
transform: translateX(0);
}
10% {
opacity: 0.3;
transform: translateX(-3px);
}
20% {
opacity: 0;
transform: translateX(3px);
}
30% {
opacity: 0.4;
transform: translateX(-2px);
}
40% {
opacity: 0;
transform: translateX(2px);
}
50% {
opacity: 0.2;
transform: translateX(-1px);
}
60% {
opacity: 0;
transform: translateX(1px);
}
}
</style>