Skip to content

packages/core/src/features/machines/animation.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   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 built with @pibbl/core 0.0.2, revision 272a94a. ALPHA — NOT FOR PRODUCTION USE.