feat: integrate accessible journey world and controls
This commit is contained in:
@@ -1,14 +1,23 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { computed, onMounted, onUnmounted, shallowRef, watch } from 'vue';
|
||||
import ContactDestination from './ContactDestination.vue';
|
||||
import DriveControls from './DriveControls.vue';
|
||||
import ExperienceTimeline from './ExperienceTimeline.vue';
|
||||
import InfrastructureDiagram from './InfrastructureDiagram.vue';
|
||||
import JourneyHud from './JourneyHud.vue';
|
||||
import PortfolioSection from './PortfolioSection.vue';
|
||||
import ProjectCard from './ProjectCard.vue';
|
||||
import ProjectCaseStudy from './ProjectCaseStudy.vue';
|
||||
import SceneCanvas from './SceneCanvas.vue';
|
||||
import SiteNavigation from './SiteNavigation.vue';
|
||||
import ToolkitGrid from './ToolkitGrid.vue';
|
||||
import { useAdaptiveQuality } from '@/composables/useAdaptiveQuality';
|
||||
import { useJourneySections } from '@/composables/useJourneySections';
|
||||
import { useKeyboardNavigation } from '@/composables/useKeyboardNavigation';
|
||||
import { useMotionPreferences } from '@/composables/useMotionPreferences';
|
||||
import type { PortfolioContent, SectionId } from '@/content/types';
|
||||
import type { SceneHostState } from '@/composables/useSceneController';
|
||||
import type { MotionPolicy, QualityTier } from '@/journey/motion-policy';
|
||||
|
||||
const props = defineProps<{ content: Readonly<PortfolioContent> }>();
|
||||
|
||||
@@ -27,15 +36,98 @@ const sectionLabel = (id: SectionId): string => {
|
||||
};
|
||||
|
||||
const canonicalNavigation = computed(() => canonicalSectionIds.map((id) => ({ id, label: sectionLabel(id) })));
|
||||
const motion = useMotionPreferences();
|
||||
const navigation = useKeyboardNavigation(canonicalSectionIds);
|
||||
const journey = useJourneySections(
|
||||
canonicalSectionIds,
|
||||
navigation.state,
|
||||
computed(() => motion.policy.value.allowSceneRequest),
|
||||
);
|
||||
const sceneState = shallowRef<SceneHostState>('idle');
|
||||
const pageLifecycle = shallowRef<'active' | 'frozen' | 'disposed'>('active');
|
||||
const controlsReady = shallowRef(false);
|
||||
const adaptive = useAdaptiveQuality({
|
||||
maximum: computed(() => motion.policy.value.maximumTier),
|
||||
sceneState,
|
||||
pageLifecycle,
|
||||
});
|
||||
const actualQuality = computed(() => adaptive.snapshot.value.actual);
|
||||
const activeSectionLabel = computed(() => sectionLabel(journey.value.activeSectionId));
|
||||
const serverDefaultPolicy: MotionPolicy = {
|
||||
allowSceneRequest: false,
|
||||
allowAutomaticVideo: false,
|
||||
maximumTier: 'high',
|
||||
reason: 'full',
|
||||
};
|
||||
const displayedPolicy = computed(() => controlsReady.value ? motion.policy.value : serverDefaultPolicy);
|
||||
const displayedQuality = computed<QualityTier>(() => controlsReady.value ? actualQuality.value : 'high');
|
||||
const displayedSceneState = computed(() => controlsReady.value ? sceneState.value : 'running');
|
||||
let removePageListeners = (): void => {};
|
||||
|
||||
watch(
|
||||
() => journey.value.activeSectionId,
|
||||
(id) => navigation.notePassiveSection(id),
|
||||
);
|
||||
|
||||
watch(
|
||||
() => motion.policy.value.maximumTier,
|
||||
(tier, previous) => {
|
||||
if (tier !== previous) adaptive.setMaximum(tier, 'manual');
|
||||
},
|
||||
);
|
||||
|
||||
const setQuality = (tier: import('@/journey/motion-policy').QualityTier): void => {
|
||||
motion.setMaximumQuality(tier);
|
||||
adaptive.setMaximum(tier, 'manual');
|
||||
};
|
||||
|
||||
const enableMotion = (): void => {
|
||||
const tier = motion.maximumQuality.value === 'static' ? 'high' : motion.maximumQuality.value;
|
||||
motion.setMaximumQuality(tier);
|
||||
motion.setPaused(false);
|
||||
adaptive.setMaximum(tier, 'manual');
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
controlsReady.value = true;
|
||||
const onPageHide = (event: PageTransitionEvent): void => {
|
||||
pageLifecycle.value = event.persisted ? 'frozen' : 'disposed';
|
||||
};
|
||||
const onPageShow = (): void => { pageLifecycle.value = 'active'; };
|
||||
window.addEventListener('pagehide', onPageHide);
|
||||
window.addEventListener('pageshow', onPageShow);
|
||||
removePageListeners = () => {
|
||||
window.removeEventListener('pagehide', onPageHide);
|
||||
window.removeEventListener('pageshow', onPageShow);
|
||||
};
|
||||
});
|
||||
|
||||
onUnmounted(() => removePageListeners());
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a class="skip-link" href="#portfolio-content">Skip to portfolio content</a>
|
||||
<SceneCanvas
|
||||
:journey="journey"
|
||||
:policy="motion.policy.value"
|
||||
:quality="actualQuality"
|
||||
:scene-description="content.sceneDescription"
|
||||
@state-change="sceneState = $event"
|
||||
/>
|
||||
<JourneyHud
|
||||
:journey="journey"
|
||||
:section-label="activeSectionLabel"
|
||||
:quality="displayedQuality"
|
||||
/>
|
||||
<header class="site-header">
|
||||
<h1>{{ content.identity.name }}</h1>
|
||||
<p>{{ content.identity.role }}</p>
|
||||
<p class="print-portfolio-label" aria-hidden="true">Portfolio</p>
|
||||
<SiteNavigation :items="canonicalNavigation" />
|
||||
<SiteNavigation
|
||||
:items="canonicalNavigation"
|
||||
:journey="journey"
|
||||
:on-section-link-click="navigation.onSectionLinkClick"
|
||||
/>
|
||||
</header>
|
||||
|
||||
<main id="portfolio-content" class="portfolio-main" tabindex="-1">
|
||||
@@ -79,4 +171,13 @@ const canonicalNavigation = computed(() => canonicalSectionIds.map((id) => ({ id
|
||||
</footer>
|
||||
</PortfolioSection>
|
||||
</main>
|
||||
<DriveControls
|
||||
:policy="displayedPolicy"
|
||||
:maximum-quality="controlsReady ? motion.maximumQuality.value : 'high'"
|
||||
:scene-status="displayedSceneState"
|
||||
@pause="motion.setPaused(true)"
|
||||
@resume="motion.setPaused(false)"
|
||||
@enable="enableMotion"
|
||||
@quality-change="setQuality"
|
||||
/>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user