packages/core/src/lib/hooks/use-playback.ts
This is the source snapshot used to build these API details. View this revision on GitHub.
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 version
Section titled “Documentation version”Documentation built with @pibbl/core 0.0.2, revision 272a94a. ALPHA — NOT FOR PRODUCTION USE.