feat: coordinate adaptive audio intent
This commit is contained in:
105
src/composables/useAudioIntent.ts
Normal file
105
src/composables/useAudioIntent.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import {
|
||||
onMounted,
|
||||
onUnmounted,
|
||||
readonly,
|
||||
shallowRef,
|
||||
watch,
|
||||
type Ref,
|
||||
} from 'vue';
|
||||
import {
|
||||
createAudioIntentCoordinator,
|
||||
type AudioIntentCoordinator,
|
||||
type AudioIntentUpdate,
|
||||
} from '@/audio/audio-intent-coordinator';
|
||||
import {
|
||||
DEFAULT_DRIVE_ENERGY,
|
||||
resolveChapterIntent,
|
||||
} from '@/audio/journey-music-state';
|
||||
import type { ChapterIntent, DriveEnergySample } from '@/audio/types';
|
||||
import type { JourneySectionsBinding } from '@/composables/useJourneySections';
|
||||
import type { SectionId } from '@/content/types';
|
||||
import type { NavigationState } from '@/journey/navigation-controller';
|
||||
import type { JourneySnapshot } from '@/journey/types';
|
||||
|
||||
export interface AudioIntentSources {
|
||||
readonly canonicalIds: readonly SectionId[];
|
||||
readonly journey: JourneySectionsBinding;
|
||||
readonly navigation: Readonly<Ref<NavigationState>>;
|
||||
}
|
||||
|
||||
export interface AudioIntentBinding {
|
||||
readonly chapter: Readonly<Ref<ChapterIntent>>;
|
||||
readonly drive: Readonly<Ref<DriveEnergySample>>;
|
||||
}
|
||||
|
||||
export function useAudioIntent(sources: AudioIntentSources): AudioIntentBinding {
|
||||
const firstId = sources.canonicalIds[0];
|
||||
if (firstId === undefined) throw new Error('Audio intent requires one section.');
|
||||
const chapter = shallowRef(resolveChapterIntent(firstId));
|
||||
const drive = shallowRef<DriveEnergySample>(DEFAULT_DRIVE_ENERGY);
|
||||
let coordinator: AudioIntentCoordinator | null = null;
|
||||
let removeFrame = (): void => {};
|
||||
|
||||
const publish = (update: AudioIntentUpdate): boolean => {
|
||||
if (update.chapter !== null) chapter.value = update.chapter;
|
||||
if (update.drive !== null) drive.value = update.drive;
|
||||
return update.needsFrame;
|
||||
};
|
||||
const sample = (now: number, snapshot: JourneySnapshot): boolean => {
|
||||
if (coordinator === null) {
|
||||
coordinator = createAudioIntentCoordinator(
|
||||
sources.canonicalIds,
|
||||
snapshot.activeSectionId,
|
||||
snapshot.sectionActivation.layoutRevision,
|
||||
snapshot.scrollY,
|
||||
);
|
||||
const seeded = coordinator.current();
|
||||
chapter.value = seeded.chapter;
|
||||
drive.value = seeded.drive;
|
||||
}
|
||||
return publish(coordinator.sample({
|
||||
now,
|
||||
visible: document.visibilityState !== 'hidden',
|
||||
scrollY: snapshot.scrollY,
|
||||
activeSectionId: snapshot.activeSectionId,
|
||||
acceleration: snapshot.acceleration,
|
||||
sectionActivation: snapshot.sectionActivation,
|
||||
navigation: sources.navigation.value,
|
||||
}));
|
||||
};
|
||||
|
||||
const stopNavigationWatch = watch(
|
||||
sources.navigation,
|
||||
() => sources.journey.requestFrame(),
|
||||
{ flush: 'sync' },
|
||||
);
|
||||
onMounted(() => {
|
||||
removeFrame = sources.journey.subscribeFrame(sample);
|
||||
const resetFromCurrentSnapshot = (): void => {
|
||||
if (coordinator === null) return;
|
||||
const snapshot = sources.journey.snapshot.value;
|
||||
publish(coordinator.reset({
|
||||
navigation: sources.navigation.value,
|
||||
scrollY: snapshot.scrollY,
|
||||
sectionActivation: snapshot.sectionActivation,
|
||||
}));
|
||||
};
|
||||
const onVisibility = (): void => {
|
||||
resetFromCurrentSnapshot();
|
||||
if (document.visibilityState !== 'hidden') sources.journey.requestFrame();
|
||||
};
|
||||
document.addEventListener('visibilitychange', onVisibility);
|
||||
removeFrame = combineCleanup(removeFrame, () =>
|
||||
document.removeEventListener('visibilitychange', onVisibility));
|
||||
sources.journey.requestFrame();
|
||||
});
|
||||
onUnmounted(() => {
|
||||
stopNavigationWatch();
|
||||
removeFrame();
|
||||
});
|
||||
return { chapter: readonly(chapter), drive: readonly(drive) };
|
||||
}
|
||||
|
||||
function combineCleanup(left: () => void, right: () => void): () => void {
|
||||
return () => { left(); right(); };
|
||||
}
|
||||
Reference in New Issue
Block a user