mid coding commit
This commit is contained in:
@@ -0,0 +1,471 @@
|
||||
<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>
|
||||
|
||||
Reference in New Issue
Block a user