feat: add Night Shift layer (boot intro, ops strip, headlight, chapter numerals, film grain)
This commit is contained in:
@@ -2,10 +2,12 @@
|
||||
import { computed, onMounted, onUnmounted, shallowRef, watch } from 'vue';
|
||||
import ContactDestination from './ContactDestination.vue';
|
||||
import DriveControls from './DriveControls.vue';
|
||||
import BootSequence from './BootSequence.vue';
|
||||
import ExperienceTimeline from './ExperienceTimeline.vue';
|
||||
import HeroJourney from './HeroJourney.vue';
|
||||
import InfrastructureDiagram from './InfrastructureDiagram.vue';
|
||||
import JourneyHud from './JourneyHud.vue';
|
||||
import OperationsStrip from './OperationsStrip.vue';
|
||||
import PortfolioSection from './PortfolioSection.vue';
|
||||
import ProjectCard from './ProjectCard.vue';
|
||||
import ProjectCaseStudy from './ProjectCaseStudy.vue';
|
||||
@@ -16,6 +18,7 @@ import { useAdaptiveQuality } from '@/composables/useAdaptiveQuality';
|
||||
import { useAudioEngine } from '@/composables/useAudioEngine';
|
||||
import { useAudioIntent } from '@/composables/useAudioIntent';
|
||||
import { useJourneySections } from '@/composables/useJourneySections';
|
||||
import { useHeadlight } from '@/composables/useHeadlight';
|
||||
import { useKeyboardNavigation } from '@/composables/useKeyboardNavigation';
|
||||
import { useMotionPreferences } from '@/composables/useMotionPreferences';
|
||||
import { assetUrl } from '@/content/asset-url';
|
||||
@@ -59,6 +62,7 @@ const sectionLabel = (id: SectionId): string => {
|
||||
|
||||
const canonicalNavigation = computed(() => canonicalSectionIds.map((id) => ({ id, label: sectionLabel(id) })));
|
||||
const motion = useMotionPreferences();
|
||||
useHeadlight(computed(() => motion.policy.value.allowSceneRequest));
|
||||
const navigation = useKeyboardNavigation(canonicalSectionIds);
|
||||
const journeyBinding = useJourneySections(
|
||||
canonicalSectionIds,
|
||||
@@ -211,6 +215,8 @@ onUnmounted(() => removePageListeners());
|
||||
:sample-frame="sampleSceneFrame"
|
||||
@state-change="updateSceneState"
|
||||
/>
|
||||
<BootSequence :allow-motion="motion.policy.value.allowSceneRequest" />
|
||||
<OperationsStrip />
|
||||
<JourneyHud
|
||||
:journey="journey"
|
||||
:section-label="activeSectionLabel"
|
||||
|
||||
55
src/components/BootSequence.vue
Normal file
55
src/components/BootSequence.vue
Normal file
@@ -0,0 +1,55 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted, shallowRef } from 'vue';
|
||||
|
||||
const props = defineProps<{ allowMotion: boolean }>();
|
||||
|
||||
const SEEN_KEY = 'portfolio.boot.seen';
|
||||
const LINES = [
|
||||
'night-shift v1.0 bucharest 03:00',
|
||||
'ingress ........ tls ok',
|
||||
'identity ....... access boundaries enforced',
|
||||
'nextcloud ...... migrated, rollback ready',
|
||||
'backups ........ verified',
|
||||
'all systems quiet. starting route.',
|
||||
] as const;
|
||||
|
||||
const visible = shallowRef(false);
|
||||
const shown = shallowRef(0);
|
||||
const leaving = shallowRef(false);
|
||||
const timers: number[] = [];
|
||||
|
||||
const finish = (): void => {
|
||||
if (!visible.value || leaving.value) return;
|
||||
leaving.value = true;
|
||||
timers.push(window.setTimeout(() => { visible.value = false; }, 600));
|
||||
};
|
||||
|
||||
const onKey = (): void => finish();
|
||||
|
||||
onMounted(() => {
|
||||
let seen = false;
|
||||
try { seen = window.sessionStorage.getItem(SEEN_KEY) === '1'; } catch { seen = false; }
|
||||
if (!props.allowMotion || seen || navigator.webdriver) return;
|
||||
try { window.sessionStorage.setItem(SEEN_KEY, '1'); } catch { /* optional */ }
|
||||
visible.value = true;
|
||||
LINES.forEach((_, index) => {
|
||||
timers.push(window.setTimeout(() => { shown.value = index + 1; }, 260 + index * 280));
|
||||
});
|
||||
timers.push(window.setTimeout(finish, 260 + LINES.length * 280 + 500));
|
||||
window.addEventListener('keydown', onKey, { once: true });
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
timers.forEach((id) => window.clearTimeout(id));
|
||||
window.removeEventListener('keydown', onKey);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="visible" class="boot-sequence" :data-leaving="leaving || undefined" aria-hidden="true" @click="finish">
|
||||
<ol class="boot-sequence__log">
|
||||
<li v-for="(line, index) in LINES.slice(0, shown)" :key="index">{{ line }}</li>
|
||||
</ol>
|
||||
<span class="boot-sequence__skip">click or press any key to skip</span>
|
||||
</div>
|
||||
</template>
|
||||
33
src/components/OperationsStrip.vue
Normal file
33
src/components/OperationsStrip.vue
Normal file
@@ -0,0 +1,33 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted, shallowRef } from 'vue';
|
||||
import { bucharestClock, uptimeSince } from '@/lib/operations';
|
||||
|
||||
/** Start of the current hosting-admin role (content file: Trupa lui Răzuș, Aug 2025). */
|
||||
const SHIFT_START = new Date('2025-08-01T00:00:00Z');
|
||||
|
||||
const clock = shallowRef<string | null>(null);
|
||||
const shift = shallowRef<string | null>(null);
|
||||
let timer = 0;
|
||||
|
||||
const tick = (): void => {
|
||||
const now = new Date();
|
||||
const { months, days } = uptimeSince(SHIFT_START, now);
|
||||
clock.value = bucharestClock(now);
|
||||
shift.value = `${months} mo ${days} d`;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
tick();
|
||||
timer = window.setInterval(tick, 30_000);
|
||||
});
|
||||
onUnmounted(() => window.clearInterval(timer));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<aside class="ops-strip" aria-hidden="true">
|
||||
<span class="ops-strip__dot" />
|
||||
<span>Bucharest <strong>{{ clock ?? '--:--' }}</strong></span>
|
||||
<span class="ops-strip__sep">/</span>
|
||||
<span>On shift <strong>{{ shift ?? '-- mo' }}</strong></span>
|
||||
</aside>
|
||||
</template>
|
||||
Reference in New Issue
Block a user