Skip to content

packages/core/src/lib/hooks/use-playback.ts

Read as Markdown

This is the source snapshot used to build these API details. View this revision on GitHub.

Back to reference

1 import {
2   createAnimationPlaybackController,
3   normalizeAnimationPlaybackCandidate,
4   type AnimationPlaybackController,
5 } from '../animation/playback.js';
6 import type {
7   PibblAnimationProgram,
8   PibblPlayback,
9   PibblPlaybackEvent,
10   PibblPlaybackOptions,
11 } from '../animation/types.js';
12 import { GLOBAL_STATE } from '../global-state.js';
13 import { isSignal } from '../signals/graph.js';
14 import { resolveSignalValue } from '../signals/resolve-signal-value.js';
15 import type { SignalValue } from '../signals/types.js';
16 import type { PibblComponentRefs, PibblInstance } from '../types.js';
17 import { requireHookContext, useHookSlot } from './hook-slot.js';
18 
19 type PibblPlaybackOptionsInput = {
20   readonly [K in keyof PibblPlaybackOptions]: SignalValue<PibblPlaybackOptions[K]>;
21 };
22 
23 interface PibblPlaybackHookSlot {
24   readonly controller: AnimationPlaybackController;
25   readonly componentRefs: PibblComponentRefs<any>;
26   readonly pibblInstance: PibblInstance;
27   latestOptions: PibblPlaybackOptions;
28   mountSettled: boolean;
29   readonly onError: (error: unknown) => void;
30   readonly onEvent: (event: PibblPlaybackEvent) => void;
31   renderRevision: number;
32 }
33 
34 function isCurrentSlot(slot: PibblPlaybackHookSlot): boolean {
35   return !slot.componentRefs.teardowns.closed &&
36     !slot.pibblInstance.mainTeardowns.closed &&
37     GLOBAL_STATE.pibblInstances.get(slot.pibblInstance.canvas) === slot.pibblInstance &&
38     slot.pibblInstance.refs.get(slot.componentRefs.id) === slot.componentRefs;
39 }
40 
41 function controllerOptions(
42   candidate: PibblPlaybackOptions,
43   onEvent: (event: PibblPlaybackEvent) => void,
44   onError: (error: unknown) => void,
45 ): PibblPlaybackOptions {
46   return {
47     ...candidate,
48     onEvent: typeof candidate.onEvent === 'function' ?
49       onEvent : candidate.onEvent,
50     onError: typeof candidate.onError === 'function' ?
51       onError : candidate.onError,
52   };
53 }
54 
55 /**
56  * Creates one component-owned animation playback handle for a hook slot.
57  *
58  * @param programInput - Animation program, optionally supplied by a signal. See
59  * {@link SignalValue} , {@link PibblAnimationProgram} .
60  * @param optionsInput - Playback configuration, optionally supplied by a signal. See
61  * {@link SignalValue} , {@link PibblPlaybackOptionsInput} .
62  * @returns Stable mount-owned playback controls and reactive status/time values. See
63  * {@link PibblPlayback} .
64  *
65  * @see {@link SignalValue}
66  * @see {@link PibblAnimationProgram}
67  * @see {@link PibblPlayback}
68  */
69 export function usePlayback(
70   programInput: SignalValue<PibblAnimationProgram>,
71   optionsInput: SignalValue<PibblPlaybackOptionsInput> = {},
72 ): PibblPlayback {
73   const { componentRefs, pibblInstance } = requireHookContext();
74   const program = resolveSignalValue(programInput);
75   const options = resolvePlaybackOptions(optionsInput);
76   const candidate = normalizeAnimationPlaybackCandidate(program, options);
77   const candidateOptions = candidate.options;
78   const slot = useHookSlot<PibblPlaybackHookSlot>('playback', teardowns => {
79     let value!: PibblPlaybackHookSlot;
80     const controller = createAnimationPlaybackController(
81       candidate.program,
82       candidateOptions,
83     );
84     controller.setDeferredStatusCommandOwner(pibblInstance.mainTeardowns);
85     const onEvent = (event: PibblPlaybackEvent): void => {
86       if (isCurrentSlot(value)) value.latestOptions.onEvent?.(event);
87     };
88     const onError = (error: unknown): void => {
89       if (isCurrentSlot(value)) value.latestOptions.onError?.(error);
90     };
91     value = {
92       controller,
93       componentRefs,
94       pibblInstance,
95       latestOptions: {},
96       mountSettled: false,
97       onError,
98       onEvent,
99       renderRevision: 0,
100     };
101     controller.adopt(
102       candidate.program,
103       controllerOptions(candidateOptions, onEvent, onError),
104     );
105     teardowns.add(() => controller.dispose());
106     return value;
107   }).value;
108   const revision = ++slot.renderRevision;
109   const transaction = GLOBAL_STATE.renderTransaction;
110   if (!transaction) {
111     throw new Error('usePlayback requires an active Pibbl render transaction.');
112   }
113   transaction.onFinish(success => {
114     if (!success || revision !== slot.renderRevision || !isCurrentSlot(slot)) return;
115     slot.controller.adopt(
116       candidate.program,
117       controllerOptions(candidateOptions, slot.onEvent, slot.onError),
118     );
119     slot.latestOptions = candidateOptions;
120     if (!slot.mountSettled) {
121       slot.mountSettled = true;
122       if (candidateOptions.autoplay === true) slot.controller.handle.play();
123     }
124   });
125   return slot.controller.handle;
126 }
127 
128 function resolvePlaybackOptions(
129   input: SignalValue<PibblPlaybackOptionsInput>,
130 ): PibblPlaybackOptions {
131   const source = resolveSignalValue(input);
132   let resolved: Record<PropertyKey, unknown> | undefined;
133   for (const key in source) {
134     const value = source[key as keyof PibblPlaybackOptionsInput];
135     if (!isSignal(value)) continue;
136     resolved ??= { ...source };
137     resolved[key] = resolveSignalValue(value as SignalValue<unknown>);
138   }
139   return (resolved ?? source) as PibblPlaybackOptions;
140 }
141 

Documentation built with @pibbl/core 0.0.2, revision 272a94a. ALPHA — NOT FOR PRODUCTION USE.