packages/core/src/features/machines/animation.ts
This is the source snapshot used to build these API details. View this revision on GitHub.
1 import {
2 createAnimationPlaybackController,
3 type AnimationPlaybackController,
4 } from '../../lib/animation/playback.js';
5 import type {
6 PibblAnimationProgram,
7 PibblPlaybackOptions,
8 } from '../../lib/animation/types.js';
9 import type {
10 MachineTask,
11 MachineTaskContext,
12 MachineEvent,
13 } from './types.js';
14
15 /**
16 * Playback options owned by a machine animation task.
17 *
18 * Lifecycle callbacks are deliberately omitted: task completion and failure
19 * become the invocation's `onDone` and `onError` transitions instead.
20 *
21 * @see {@link playAnimation}
22 */
23 export interface MachineAnimationOptions {
24 /**
25 * Selects the existing writer-conflict policy for this state-owned playback.
26 * Defaults to the animation runtime's explicit-error policy.
27 */
28 readonly conflict?: PibblPlaybackOptions['conflict'];
29 /** Optional label included in animation scheduler diagnostics. */
30 readonly debugName?: string;
31 }
32
33 /**
34 * Builds the animation program for one state entry.
35 *
36 * The callback runs once when the invocation starts. It receives the entry's
37 * immutable context and event, the machine input, the cancellation signal, and
38 * `send` for application-defined machine events.
39 *
40 * @typeParam Context - Context captured when this state entry began.
41 * @typeParam Event - Event type accepted by the owning machine.
42 * @typeParam Input - Input captured by the owning machine actor.
43 * @param context - Immutable state-entry values used to create this program.
44 * @returns The program that the machine invocation owns until it finishes or is cancelled.
45 * @see {@link MachineTaskContext}
46 */
47 export type MachineAnimationProgramFactory<Context, Event extends MachineEvent, Input> = (
48 context: MachineTaskContext<Context, Event, Input>,
49 ) => PibblAnimationProgram;
50
51 /**
52 * Adapts a Pibbl animation program into state-owned cancelable machine work.
53 *
54 * A finite program resolves the state invocation after the animation scheduler
55 * delivers its `finish` event, allowing the invocation's `onDone` transition to
56 * run. Repeating programs remain active until their state exits or the actor
57 * stops. Cancellation disposes only this adapter's playback and never sends a
58 * completion or error transition.
59 *
60 * @typeParam Context - Context captured when this state entry began.
61 * @typeParam Event - Event type accepted by the owning machine.
62 * @typeParam Input - Input captured by the owning machine actor.
63 * @param program - Factory that creates the program for this state entry.
64 * @param options - Playback conflict policy and diagnostic label.
65 * @returns A task suitable for a state's `invoke.task` field.
66 *
67 * @example
68 * ```ts
69 * running: {
70 * invoke: {
71 * task: playAnimation(({ input }) =>
72 * repeat(drive(input.frame, input.animations.run), { iterations: Infinity }),
73 * ),
74 * },
75 * }
76 * ```
77 *
78 * @see {@link MachineAnimationOptions}
79 */
80 export function playAnimation<Context, Event extends MachineEvent, Input>(
81 program: MachineAnimationProgramFactory<Context, Event, Input>,
82 options: MachineAnimationOptions = {},
83 ): MachineTask<Context, Event, Input, void> {
84 return taskContext => {
85 if (taskContext.signal.aborted) return;
86
87 const animation = program(taskContext);
88 return new Promise<void>((resolve, reject) => {
89 let settled = false;
90 let controller!: AnimationPlaybackController;
91 controller = createAnimationPlaybackController(animation, {
92 conflict: options.conflict,
93 debugName: options.debugName,
94 onEvent(event): void {
95 if (event.type !== 'finish' || settled) return;
96 settled = true;
97 detachAbort();
98 controller.dispose();
99 resolve();
100 },
101 onError(error): void {
102 if (settled) return;
103 settled = true;
104 detachAbort();
105 controller.dispose();
106 reject(error);
107 },
108 });
109
110 const abort = (): void => {
111 if (settled) return;
112 settled = true;
113 detachAbort();
114 controller.dispose();
115 };
116 const detachAbort = (): void => {
117 taskContext.signal.removeEventListener('abort', abort);
118 };
119
120 taskContext.signal.addEventListener('abort', abort, { once: true });
121 if (taskContext.signal.aborted) {
122 abort();
123 return;
124 }
125 try {
126 controller.handle.play();
127 } catch (error) {
128 abort();
129 reject(error);
130 }
131 });
132 };
133 }
134
Documentation version
Section titled “Documentation version”Documentation built with @pibbl/core 0.0.2, revision 2dccb19. ALPHA — NOT FOR PRODUCTION USE.