209 lines
6.0 KiB
Vue
209 lines
6.0 KiB
Vue
<template>
|
|
<Teleport to="body">
|
|
<Transition name="screensaver">
|
|
<div
|
|
v-if="store.isActive"
|
|
class="screensaver-container fixed inset-0 z-[3000] bg-black cursor-pointer"
|
|
@click="store.deactivate()"
|
|
@keydown.escape="store.deactivate()"
|
|
>
|
|
<!-- ASCII variant (every 3rd activation) -->
|
|
<div v-if="store.isAsciiMode" class="screensaver-ascii-content">
|
|
<BitcoinFaceAscii />
|
|
</div>
|
|
<!-- Normal logo with audio viz ring -->
|
|
<div v-else class="screensaver-content">
|
|
<!-- Radial audio visualization - bars around the logo -->
|
|
<div class="screensaver-viz-ring">
|
|
<div
|
|
v-for="(_, i) in segmentCount"
|
|
:key="i"
|
|
class="screensaver-viz-segment"
|
|
:style="getSegmentStyle(i)"
|
|
/>
|
|
</div>
|
|
<!-- Logo in center -->
|
|
<div class="screensaver-logo-wrapper">
|
|
<ScreensaverLogo />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Transition>
|
|
</Teleport>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { onMounted, onBeforeUnmount } from 'vue'
|
|
import ScreensaverLogo from '@/components/ScreensaverLogo.vue'
|
|
import BitcoinFaceAscii from '@/views/discover/BitcoinFaceAscii.vue'
|
|
import { useScreensaverStore } from '@/stores/screensaver'
|
|
|
|
const store = useScreensaverStore()
|
|
|
|
const segmentCount = 48
|
|
|
|
function getSegmentStyle(i: number) {
|
|
const deg = (i / segmentCount) * 360
|
|
return {
|
|
'--segment-index': i,
|
|
'--segment-deg': `${deg}deg`,
|
|
}
|
|
}
|
|
|
|
// Dismiss on any key (except when typing)
|
|
function onKeyDown(e: KeyboardEvent) {
|
|
if (store.isActive) {
|
|
store.deactivate()
|
|
e.preventDefault()
|
|
}
|
|
}
|
|
|
|
onMounted(() => {
|
|
window.addEventListener('keydown', onKeyDown)
|
|
})
|
|
onBeforeUnmount(() => {
|
|
window.removeEventListener('keydown', onKeyDown)
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
.screensaver-enter-active,
|
|
.screensaver-leave-active {
|
|
transition: opacity 0.5s ease;
|
|
}
|
|
.screensaver-enter-from,
|
|
.screensaver-leave-to {
|
|
opacity: 0;
|
|
}
|
|
|
|
/* Explicit viewport centering */
|
|
.screensaver-container {
|
|
position: fixed;
|
|
inset: 0;
|
|
display: grid;
|
|
place-items: center;
|
|
}
|
|
|
|
.screensaver-content {
|
|
position: relative;
|
|
width: 280px;
|
|
height: 280px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
@media (min-width: 640px) {
|
|
.screensaver-content {
|
|
width: 360px;
|
|
height: 360px;
|
|
}
|
|
}
|
|
|
|
@media (min-width: 768px) {
|
|
.screensaver-content {
|
|
width: 400px;
|
|
height: 400px;
|
|
}
|
|
}
|
|
|
|
/* Ring of segments around the logo - audio viz style (behind logo) */
|
|
.screensaver-viz-ring {
|
|
position: absolute;
|
|
inset: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
z-index: 0;
|
|
pointer-events: none;
|
|
--viz-radius: 140px;
|
|
}
|
|
|
|
@media (min-width: 640px) {
|
|
.screensaver-viz-ring {
|
|
--viz-radius: 180px;
|
|
}
|
|
}
|
|
|
|
@media (min-width: 768px) {
|
|
.screensaver-viz-ring {
|
|
--viz-radius: 200px;
|
|
}
|
|
}
|
|
|
|
.screensaver-viz-segment {
|
|
position: absolute;
|
|
left: 50%;
|
|
top: 50%;
|
|
width: 4px;
|
|
height: 24px;
|
|
margin-left: -2px;
|
|
margin-top: -12px;
|
|
background: linear-gradient(to bottom, rgba(255, 255, 255, 0.4), rgba(255, 255, 255, 0.1));
|
|
border-radius: 2px;
|
|
/* Origin at segment center = ring center (segment is centered via left/top 50%) */
|
|
transform-origin: center center;
|
|
transform: rotate(var(--segment-deg)) translateY(calc(-1 * var(--viz-radius)));
|
|
animation: segment-pulse 14s ease-in-out infinite;
|
|
animation-delay: calc(var(--segment-index) * 0.02s);
|
|
}
|
|
|
|
@media (min-width: 768px) {
|
|
.screensaver-viz-segment {
|
|
height: 28px;
|
|
margin-top: -14px;
|
|
}
|
|
}
|
|
|
|
/* 5 normal loops (10s) then stronger longer expression (4s) - total 14s */
|
|
@keyframes segment-pulse {
|
|
/* Loop 1 */
|
|
0% { opacity: 0.3; transform: rotate(var(--segment-deg)) translateY(calc(-1 * var(--viz-radius))) scaleY(0.4); }
|
|
7.1% { opacity: 0.9; transform: rotate(var(--segment-deg)) translateY(calc(-1 * var(--viz-radius))) scaleY(1); }
|
|
14.3% { opacity: 0.3; transform: rotate(var(--segment-deg)) translateY(calc(-1 * var(--viz-radius))) scaleY(0.4); }
|
|
/* Loop 2 */
|
|
21.4% { opacity: 0.9; transform: rotate(var(--segment-deg)) translateY(calc(-1 * var(--viz-radius))) scaleY(1); }
|
|
28.6% { opacity: 0.3; transform: rotate(var(--segment-deg)) translateY(calc(-1 * var(--viz-radius))) scaleY(0.4); }
|
|
/* Loop 3 */
|
|
35.7% { opacity: 0.9; transform: rotate(var(--segment-deg)) translateY(calc(-1 * var(--viz-radius))) scaleY(1); }
|
|
42.9% { opacity: 0.3; transform: rotate(var(--segment-deg)) translateY(calc(-1 * var(--viz-radius))) scaleY(0.4); }
|
|
/* Loop 4 */
|
|
50% { opacity: 0.9; transform: rotate(var(--segment-deg)) translateY(calc(-1 * var(--viz-radius))) scaleY(1); }
|
|
57.1% { opacity: 0.3; transform: rotate(var(--segment-deg)) translateY(calc(-1 * var(--viz-radius))) scaleY(0.4); }
|
|
/* Loop 5 */
|
|
64.3% { opacity: 0.9; transform: rotate(var(--segment-deg)) translateY(calc(-1 * var(--viz-radius))) scaleY(1); }
|
|
71.4% { opacity: 0.3; transform: rotate(var(--segment-deg)) translateY(calc(-1 * var(--viz-radius))) scaleY(0.4); }
|
|
/* Strong expression: ramp up (1.5s), hold (2s), ease back (0.5s) */
|
|
78.6% { opacity: 1; transform: rotate(var(--segment-deg)) translateY(calc(-1 * var(--viz-radius))) scaleY(1.5); }
|
|
85.7% { opacity: 1; transform: rotate(var(--segment-deg)) translateY(calc(-1 * var(--viz-radius))) scaleY(1.5); }
|
|
92.9% { opacity: 0.3; transform: rotate(var(--segment-deg)) translateY(calc(-1 * var(--viz-radius))) scaleY(0.4); }
|
|
100% { opacity: 0.3; transform: rotate(var(--segment-deg)) translateY(calc(-1 * var(--viz-radius))) scaleY(0.4); }
|
|
}
|
|
|
|
.screensaver-logo-wrapper {
|
|
position: absolute;
|
|
left: 50%;
|
|
top: 50%;
|
|
transform: translate(-50%, -50%);
|
|
z-index: 10;
|
|
filter: drop-shadow(0 0 40px rgba(255, 255, 255, 0.15));
|
|
}
|
|
|
|
/* ASCII variant — centered Bitcoin face animation */
|
|
.screensaver-ascii-content {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
transform: scale(2);
|
|
}
|
|
|
|
@media (min-width: 640px) {
|
|
.screensaver-ascii-content {
|
|
transform: scale(2.5);
|
|
}
|
|
}
|
|
|
|
@media (min-width: 768px) {
|
|
.screensaver-ascii-content {
|
|
transform: scale(3);
|
|
}
|
|
}
|
|
</style>
|