@replayablejs/tween 0.1.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +35 -0
- package/dist/index.d.ts +53 -0
- package/dist/index.js +230 -0
- package/dist/index.js.map +1 -0
- package/package.json +55 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Replayable contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# @replayablejs/tween
|
|
2
|
+
|
|
3
|
+
Animate DOM elements and object properties using lifecycle-aware Motion playback.
|
|
4
|
+
|
|
5
|
+
Part of [Replayable](https://github.com/replayablejs/replayable) **0.1.0-alpha.0**.
|
|
6
|
+
APIs may change during the alpha series.
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
pnpm add @replayablejs/tween@0.1.0-alpha.0
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Public surface
|
|
15
|
+
|
|
16
|
+
`animate`, `stagger`, `TweenPlaybackControls`.
|
|
17
|
+
|
|
18
|
+
[Usage and reference](https://github.com/replayablejs/replayable/blob/main/docs/reference/tween.md).
|
|
19
|
+
The package manifest defines supported import paths; internal source files are not public APIs.
|
|
20
|
+
|
|
21
|
+
## Development
|
|
22
|
+
|
|
23
|
+
From the repository root, install with `pnpm install --frozen-lockfile` and build dependencies
|
|
24
|
+
with `pnpm build`. Run `pnpm --filter @replayablejs/tween test` for this package's tests.
|
|
25
|
+
|
|
26
|
+
## Peer dependencies
|
|
27
|
+
|
|
28
|
+
- `@replayablejs/runtime`: `workspace:*`
|
|
29
|
+
|
|
30
|
+
Workspace ranges are converted to package versions when packed.
|
|
31
|
+
|
|
32
|
+
## License
|
|
33
|
+
|
|
34
|
+
Original code is [MIT licensed](https://github.com/replayablejs/replayable/blob/main/LICENSE). Bundled third-party resources retain
|
|
35
|
+
their accompanying license terms.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { AnimationOptions, AnimationSequence, DOMKeyframesDefinition, ElementOrSelector, MotionValue, ObjectTarget, SequenceOptions, UnresolvedValueKeyframe, ValueAnimationTransition, stagger } from "motion";
|
|
2
|
+
//#region src/types/playback.d.ts
|
|
3
|
+
/** Public controls for one tween created through `@replayablejs/tween`. */
|
|
4
|
+
interface TweenPlaybackControls {
|
|
5
|
+
/** Current animation position in seconds. Assigning a value seeks the tween. */
|
|
6
|
+
time: number;
|
|
7
|
+
/** Playback-rate multiplier, where `1` is normal speed and `-1` plays in reverse. */
|
|
8
|
+
speed: number;
|
|
9
|
+
/** Total animation duration in seconds. */
|
|
10
|
+
readonly duration: number;
|
|
11
|
+
/** Resumes playback from the current position. */
|
|
12
|
+
play(): void;
|
|
13
|
+
/** Pauses playback at the current position. */
|
|
14
|
+
pause(): void;
|
|
15
|
+
/** Stops playback permanently at its current visual state. */
|
|
16
|
+
stop(): void;
|
|
17
|
+
/** Cancels playback and restores its initial state. */
|
|
18
|
+
cancel(): void;
|
|
19
|
+
/** Finishes playback immediately and applies its final state. */
|
|
20
|
+
complete(): void;
|
|
21
|
+
/**
|
|
22
|
+
* Makes the controls awaitable and resolves after normal or forced completion.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
*
|
|
26
|
+
* ```ts
|
|
27
|
+
* const controls = animate(element, { opacity: 1 });
|
|
28
|
+
* await controls;
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
then(onResolve: () => void, onReject?: () => void): Promise<void>;
|
|
32
|
+
}
|
|
33
|
+
//#endregion
|
|
34
|
+
//#region src/animate.d.ts
|
|
35
|
+
/**
|
|
36
|
+
* Animates values through Motion while applying Replayable's host lifecycle.
|
|
37
|
+
*
|
|
38
|
+
* The overloads intentionally mirror Motion's hybrid `animate()` function:
|
|
39
|
+
* sequences, individual values, Motion values, DOM/SVG targets, and plain
|
|
40
|
+
* objects all retain their native keyframe and option inference. Only the
|
|
41
|
+
* returned controls are narrowed to Replayable's stable public contract.
|
|
42
|
+
*
|
|
43
|
+
* @throws When called before `await playable.ready()`.
|
|
44
|
+
*/
|
|
45
|
+
declare function animate(sequence: AnimationSequence, options?: SequenceOptions): TweenPlaybackControls;
|
|
46
|
+
declare function animate(value: string | MotionValue<string>, keyframes: string | UnresolvedValueKeyframe<string>[], options?: ValueAnimationTransition<string>): TweenPlaybackControls;
|
|
47
|
+
declare function animate(value: number | MotionValue<number>, keyframes: number | UnresolvedValueKeyframe<number>[], options?: ValueAnimationTransition<number>): TweenPlaybackControls;
|
|
48
|
+
declare function animate<Value extends string | number>(value: Value | MotionValue<Value>, keyframes: Value | UnresolvedValueKeyframe<Value>[], options?: ValueAnimationTransition<Value>): TweenPlaybackControls;
|
|
49
|
+
declare function animate(element: ElementOrSelector, keyframes: DOMKeyframesDefinition, options?: AnimationOptions): TweenPlaybackControls;
|
|
50
|
+
declare function animate<Object extends object>(object: Object | Object[], keyframes: ObjectTarget<Object>, options?: AnimationOptions): TweenPlaybackControls;
|
|
51
|
+
//#endregion
|
|
52
|
+
export { type TweenPlaybackControls, animate, stagger };
|
|
53
|
+
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { animate as animate$1, stagger } from "motion";
|
|
2
|
+
import { playable } from "@replayablejs/runtime";
|
|
3
|
+
//#region src/lifecycle/create-managed-tween.ts
|
|
4
|
+
/**
|
|
5
|
+
* Wraps one Motion animation while preserving playback intent across visibility.
|
|
6
|
+
*
|
|
7
|
+
* Visibility retains only running children; finished children stay finished.
|
|
8
|
+
* An explicit hidden-time `play()` instead requests playback of the whole group.
|
|
9
|
+
* Manual pause or termination clears both forms of deferred playback intent.
|
|
10
|
+
*/
|
|
11
|
+
function createManagedTween(motionControls, animations, isVisible, registry) {
|
|
12
|
+
const pausedForVisibility = /* @__PURE__ */ new Set();
|
|
13
|
+
let playWhenVisible = false;
|
|
14
|
+
let stopped = false;
|
|
15
|
+
return {
|
|
16
|
+
get hasDeferredPlay() {
|
|
17
|
+
return playWhenVisible;
|
|
18
|
+
},
|
|
19
|
+
controls: {
|
|
20
|
+
get duration() {
|
|
21
|
+
return motionControls.duration;
|
|
22
|
+
},
|
|
23
|
+
get speed() {
|
|
24
|
+
return motionControls.speed;
|
|
25
|
+
},
|
|
26
|
+
set speed(speed) {
|
|
27
|
+
motionControls.speed = speed;
|
|
28
|
+
},
|
|
29
|
+
get time() {
|
|
30
|
+
return motionControls.time;
|
|
31
|
+
},
|
|
32
|
+
set time(time) {
|
|
33
|
+
motionControls.time = time;
|
|
34
|
+
},
|
|
35
|
+
cancel() {
|
|
36
|
+
terminate("cancel");
|
|
37
|
+
},
|
|
38
|
+
complete() {
|
|
39
|
+
terminate("complete");
|
|
40
|
+
},
|
|
41
|
+
pause() {
|
|
42
|
+
clearVisibilityIntent();
|
|
43
|
+
motionControls.pause();
|
|
44
|
+
},
|
|
45
|
+
play() {
|
|
46
|
+
if (stopped) return;
|
|
47
|
+
if (!isVisible()) {
|
|
48
|
+
playWhenVisible = true;
|
|
49
|
+
registry.retain();
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
clearVisibilityIntent();
|
|
53
|
+
motionControls.play();
|
|
54
|
+
registry.trackPlayback();
|
|
55
|
+
},
|
|
56
|
+
stop() {
|
|
57
|
+
stopped = true;
|
|
58
|
+
terminate("stop");
|
|
59
|
+
},
|
|
60
|
+
then(onResolve, onReject) {
|
|
61
|
+
return motionControls.then(onResolve, onReject);
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
/** Pauses a running tween without changing the application's play intent. */
|
|
65
|
+
pauseForVisibility() {
|
|
66
|
+
for (const animation of animations) if (animation.state === "running") {
|
|
67
|
+
animation.pause();
|
|
68
|
+
pausedForVisibility.add(animation);
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
/** Resumes only playback that Replayable deferred because the host was hidden. */
|
|
72
|
+
resumeFromVisibility() {
|
|
73
|
+
if (playWhenVisible) {
|
|
74
|
+
clearVisibilityIntent();
|
|
75
|
+
motionControls.play();
|
|
76
|
+
registry.trackPlayback();
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (pausedForVisibility.size === 0) return;
|
|
80
|
+
for (const animation of pausedForVisibility) animation.play();
|
|
81
|
+
pausedForVisibility.clear();
|
|
82
|
+
registry.trackPlayback();
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
/** Removes deferred intent before invoking any potentially reentrant Motion callback. */
|
|
86
|
+
function clearVisibilityIntent() {
|
|
87
|
+
playWhenVisible = false;
|
|
88
|
+
pausedForVisibility.clear();
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Attempts every child before releasing registry ownership. A throwing Motion
|
|
92
|
+
* callback must not skip later children or retain this terminated tween.
|
|
93
|
+
*/
|
|
94
|
+
function terminate(operation) {
|
|
95
|
+
clearVisibilityIntent();
|
|
96
|
+
const errors = [];
|
|
97
|
+
for (const animation of animations) try {
|
|
98
|
+
animation[operation]();
|
|
99
|
+
} catch (error) {
|
|
100
|
+
errors.push(error);
|
|
101
|
+
}
|
|
102
|
+
registry.release();
|
|
103
|
+
if (errors.length === 1) throw errors[0];
|
|
104
|
+
if (errors.length > 1) throw new AggregateError(errors, `Unable to ${operation} every tween animation.`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
//#endregion
|
|
108
|
+
//#region src/lifecycle/create-playback-observer.ts
|
|
109
|
+
/**
|
|
110
|
+
* Observes child completion promises, not Motion's fresh aggregate per getter.
|
|
111
|
+
* Pausing/resuming keeps these promises, so it adds no completion subscriptions.
|
|
112
|
+
* Replaying a finished child replaces its promise and starts a new observation.
|
|
113
|
+
*/
|
|
114
|
+
function createPlaybackObserver(animations, onComplete) {
|
|
115
|
+
let observed;
|
|
116
|
+
return {
|
|
117
|
+
observe,
|
|
118
|
+
clear
|
|
119
|
+
};
|
|
120
|
+
/** Subscribes only when at least one child has entered a new playback cycle. */
|
|
121
|
+
function observe() {
|
|
122
|
+
const completions = animations.map((animation) => animation.finished);
|
|
123
|
+
if (observed?.every((completion, index) => completion === completions[index])) return;
|
|
124
|
+
observed = completions;
|
|
125
|
+
Promise.all(completions).then(handleComplete, handleComplete);
|
|
126
|
+
/** An older playback must never release a newer or explicitly terminated one. */
|
|
127
|
+
function handleComplete() {
|
|
128
|
+
if (observed !== completions) return;
|
|
129
|
+
observed = void 0;
|
|
130
|
+
onComplete();
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/** Discards ownership without trying to cancel Motion's native promises. */
|
|
134
|
+
function clear() {
|
|
135
|
+
observed = void 0;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
//#endregion
|
|
139
|
+
//#region src/lifecycle/resolve-motion-animations.ts
|
|
140
|
+
/**
|
|
141
|
+
* Resolves the individual animations behind Motion's public group controls.
|
|
142
|
+
*
|
|
143
|
+
* Motion 13 groups expose `animations`, but their `state` getter reports only
|
|
144
|
+
* the first child. Lifecycle decisions must inspect children independently:
|
|
145
|
+
* one property can finish while another continues or repeats indefinitely.
|
|
146
|
+
* Keep this integration boundary aligned with Motion when upgrading it.
|
|
147
|
+
*/
|
|
148
|
+
function resolveMotionAnimations(controls) {
|
|
149
|
+
return controls.animations?.flatMap(resolveMotionAnimations) ?? [controls];
|
|
150
|
+
}
|
|
151
|
+
//#endregion
|
|
152
|
+
//#region src/lifecycle/create-tween-lifecycle.ts
|
|
153
|
+
let tweenLifecycle;
|
|
154
|
+
/**
|
|
155
|
+
* Returns the shared lifecycle registry, creating it on the first animation.
|
|
156
|
+
*
|
|
157
|
+
* `animate()` calls this before invoking Motion. The initial `playable.state`
|
|
158
|
+
* read therefore rejects animation creation before `await playable.ready()`
|
|
159
|
+
* without leaving an untracked Motion animation running in the background.
|
|
160
|
+
*/
|
|
161
|
+
function getTweenLifecycle() {
|
|
162
|
+
tweenLifecycle ??= createTweenLifecycle(playable);
|
|
163
|
+
return tweenLifecycle;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Creates the lifecycle registry shared by every tween in one playable.
|
|
167
|
+
*
|
|
168
|
+
* Creation reads `playable.state`, which intentionally throws when application
|
|
169
|
+
* code requests an animation before `await playable.ready()`. Its single
|
|
170
|
+
* visibility subscription then lives with this singleton for the lifetime of
|
|
171
|
+
* the playable; completed tweens are still removed from the active registry.
|
|
172
|
+
*/
|
|
173
|
+
function createTweenLifecycle(runtime) {
|
|
174
|
+
const managedTweens = /* @__PURE__ */ new Set();
|
|
175
|
+
let visible = runtime.state.visible;
|
|
176
|
+
runtime.on("visibilitychange", handleVisibilityChange);
|
|
177
|
+
return {
|
|
178
|
+
/** Wraps one newly created Motion animation in Replayable's public controls. */
|
|
179
|
+
manage(motionControls) {
|
|
180
|
+
const animations = resolveMotionAnimations(motionControls);
|
|
181
|
+
const observer = createPlaybackObserver(animations, handleComplete);
|
|
182
|
+
const managedTween = createManagedTween(motionControls, animations, () => visible, {
|
|
183
|
+
release,
|
|
184
|
+
retain,
|
|
185
|
+
trackPlayback
|
|
186
|
+
});
|
|
187
|
+
trackPlayback();
|
|
188
|
+
if (!visible) managedTween.pauseForVisibility();
|
|
189
|
+
return managedTween.controls;
|
|
190
|
+
/** Deferred replay survives settlement of the previous playback cycle. */
|
|
191
|
+
function handleComplete() {
|
|
192
|
+
if (!managedTween.hasDeferredPlay) release();
|
|
193
|
+
}
|
|
194
|
+
/** Visibility deferral keeps ownership without adding completion observers. */
|
|
195
|
+
function retain() {
|
|
196
|
+
managedTweens.add(managedTween);
|
|
197
|
+
}
|
|
198
|
+
/** Retains playback and observes it only if its child promises changed. */
|
|
199
|
+
function trackPlayback() {
|
|
200
|
+
retain();
|
|
201
|
+
observer.observe();
|
|
202
|
+
}
|
|
203
|
+
/** Invalidates older completion callbacks and forgets terminated playback. */
|
|
204
|
+
function release() {
|
|
205
|
+
observer.clear();
|
|
206
|
+
managedTweens.delete(managedTween);
|
|
207
|
+
}
|
|
208
|
+
} };
|
|
209
|
+
/** Applies one committed host-visibility transition to every active tween. */
|
|
210
|
+
function handleVisibilityChange(nextVisible) {
|
|
211
|
+
visible = nextVisible;
|
|
212
|
+
const operation = visible ? "resumeFromVisibility" : "pauseForVisibility";
|
|
213
|
+
for (const managedTween of managedTweens) managedTween[operation]();
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
//#endregion
|
|
217
|
+
//#region src/animate.ts
|
|
218
|
+
function animate(subjectOrSequence, keyframesOrOptions, options) {
|
|
219
|
+
const lifecycle = getTweenLifecycle();
|
|
220
|
+
const motionControls = Reflect.apply(animate$1, void 0, [
|
|
221
|
+
subjectOrSequence,
|
|
222
|
+
keyframesOrOptions,
|
|
223
|
+
options
|
|
224
|
+
]);
|
|
225
|
+
return lifecycle.manage(motionControls);
|
|
226
|
+
}
|
|
227
|
+
//#endregion
|
|
228
|
+
export { animate, stagger };
|
|
229
|
+
|
|
230
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":["motionAnimate"],"sources":["../src/lifecycle/create-managed-tween.ts","../src/lifecycle/create-playback-observer.ts","../src/lifecycle/resolve-motion-animations.ts","../src/lifecycle/create-tween-lifecycle.ts","../src/animate.ts"],"sourcesContent":["import type { ManagedTween, ManagedTweenRegistry, TweenTermination } from '#types/lifecycle.js';\nimport type { MotionAnimation, MotionPlaybackControls } from '#types/motion-playback.js';\nimport type { TweenPlaybackControls } from '#types/playback.js';\n\n/**\n * Wraps one Motion animation while preserving playback intent across visibility.\n *\n * Visibility retains only running children; finished children stay finished.\n * An explicit hidden-time `play()` instead requests playback of the whole group.\n * Manual pause or termination clears both forms of deferred playback intent.\n */\nexport function createManagedTween(\n motionControls: MotionPlaybackControls,\n animations: readonly MotionAnimation[],\n isVisible: () => boolean,\n registry: ManagedTweenRegistry,\n): ManagedTween {\n const pausedForVisibility = new Set<MotionAnimation>();\n let playWhenVisible = false;\n let stopped = false;\n\n return {\n get hasDeferredPlay(): boolean {\n return playWhenVisible;\n },\n controls: {\n get duration(): number {\n return motionControls.duration;\n },\n\n get speed(): number {\n return motionControls.speed;\n },\n\n set speed(speed: number) {\n motionControls.speed = speed;\n },\n\n get time(): number {\n return motionControls.time;\n },\n\n set time(time: number) {\n motionControls.time = time;\n },\n\n cancel(): void {\n terminate('cancel');\n },\n\n complete(): void {\n terminate('complete');\n },\n\n pause(): void {\n clearVisibilityIntent();\n motionControls.pause();\n },\n\n play(): void {\n // Motion permanently detaches stopped animations; a later play() is a\n // no-op and must not reintroduce dead controls into the active registry.\n if (stopped) {\n return;\n }\n\n if (!isVisible()) {\n playWhenVisible = true;\n registry.retain();\n return;\n }\n\n clearVisibilityIntent();\n motionControls.play();\n registry.trackPlayback();\n },\n\n stop(): void {\n stopped = true;\n terminate('stop');\n },\n\n then(onResolve, onReject): Promise<void> {\n return motionControls.then(onResolve, onReject);\n },\n } satisfies TweenPlaybackControls,\n\n /** Pauses a running tween without changing the application's play intent. */\n pauseForVisibility(): void {\n for (const animation of animations) {\n if (animation.state === 'running') {\n animation.pause();\n pausedForVisibility.add(animation);\n }\n }\n },\n\n /** Resumes only playback that Replayable deferred because the host was hidden. */\n resumeFromVisibility(): void {\n if (playWhenVisible) {\n clearVisibilityIntent();\n motionControls.play();\n registry.trackPlayback();\n return;\n }\n\n if (pausedForVisibility.size === 0) {\n return;\n }\n\n for (const animation of pausedForVisibility) {\n animation.play();\n }\n pausedForVisibility.clear();\n registry.trackPlayback();\n },\n };\n\n /** Removes deferred intent before invoking any potentially reentrant Motion callback. */\n function clearVisibilityIntent(): void {\n playWhenVisible = false;\n pausedForVisibility.clear();\n }\n\n /**\n * Attempts every child before releasing registry ownership. A throwing Motion\n * callback must not skip later children or retain this terminated tween.\n */\n function terminate(operation: TweenTermination): void {\n clearVisibilityIntent();\n const errors: unknown[] = [];\n for (const animation of animations) {\n try {\n animation[operation]();\n } catch (error) {\n errors.push(error);\n }\n }\n registry.release();\n\n if (errors.length === 1) {\n throw errors[0];\n }\n if (errors.length > 1) {\n throw new AggregateError(errors, `Unable to ${operation} every tween animation.`);\n }\n }\n}\n","import type { PlaybackObserver } from '#types/lifecycle.js';\nimport type { MotionAnimation } from '#types/motion-playback.js';\n\n/**\n * Observes child completion promises, not Motion's fresh aggregate per getter.\n * Pausing/resuming keeps these promises, so it adds no completion subscriptions.\n * Replaying a finished child replaces its promise and starts a new observation.\n */\nexport function createPlaybackObserver(\n animations: readonly MotionAnimation[],\n onComplete: () => void,\n): PlaybackObserver {\n let observed: readonly Promise<void>[] | undefined;\n\n return { observe, clear };\n\n /** Subscribes only when at least one child has entered a new playback cycle. */\n function observe(): void {\n const completions = animations.map((animation) => animation.finished);\n if (observed?.every((completion, index) => completion === completions[index])) {\n return;\n }\n\n observed = completions;\n void Promise.all(completions).then(handleComplete, handleComplete);\n\n /** An older playback must never release a newer or explicitly terminated one. */\n function handleComplete(): void {\n if (observed !== completions) {\n return;\n }\n observed = undefined;\n onComplete();\n }\n }\n\n /** Discards ownership without trying to cancel Motion's native promises. */\n function clear(): void {\n observed = undefined;\n }\n}\n","import type { MotionAnimation } from '#types/motion-playback.js';\n\n/**\n * Resolves the individual animations behind Motion's public group controls.\n *\n * Motion 13 groups expose `animations`, but their `state` getter reports only\n * the first child. Lifecycle decisions must inspect children independently:\n * one property can finish while another continues or repeats indefinitely.\n * Keep this integration boundary aligned with Motion when upgrading it.\n */\nexport function resolveMotionAnimations(controls: MotionAnimation): readonly MotionAnimation[] {\n return controls.animations?.flatMap(resolveMotionAnimations) ?? [controls];\n}\n","import { playable } from '@replayablejs/runtime';\n\nimport { createManagedTween } from '#lifecycle/create-managed-tween.js';\nimport { createPlaybackObserver } from '#lifecycle/create-playback-observer.js';\nimport { resolveMotionAnimations } from '#lifecycle/resolve-motion-animations.js';\nimport type { ManagedTween, TweenLifecycle, TweenRuntime } from '#types/lifecycle.js';\nimport type { MotionPlaybackControls } from '#types/motion-playback.js';\nimport type { TweenPlaybackControls } from '#types/playback.js';\n\nlet tweenLifecycle: TweenLifecycle | undefined;\n\n/**\n * Returns the shared lifecycle registry, creating it on the first animation.\n *\n * `animate()` calls this before invoking Motion. The initial `playable.state`\n * read therefore rejects animation creation before `await playable.ready()`\n * without leaving an untracked Motion animation running in the background.\n */\nexport function getTweenLifecycle(): TweenLifecycle {\n tweenLifecycle ??= createTweenLifecycle(playable);\n\n return tweenLifecycle;\n}\n\n/**\n * Creates the lifecycle registry shared by every tween in one playable.\n *\n * Creation reads `playable.state`, which intentionally throws when application\n * code requests an animation before `await playable.ready()`. Its single\n * visibility subscription then lives with this singleton for the lifetime of\n * the playable; completed tweens are still removed from the active registry.\n */\nexport function createTweenLifecycle(runtime: TweenRuntime): TweenLifecycle {\n const managedTweens = new Set<ManagedTween>();\n let visible = runtime.state.visible;\n\n // A playable owns one lifecycle for its entire document lifetime. Retaining\n // this single listener is simpler than repeatedly subscribing between tweens.\n runtime.on('visibilitychange', handleVisibilityChange);\n\n return {\n /** Wraps one newly created Motion animation in Replayable's public controls. */\n manage(motionControls: MotionPlaybackControls): TweenPlaybackControls {\n const animations = resolveMotionAnimations(motionControls);\n const observer = createPlaybackObserver(animations, handleComplete);\n const managedTween = createManagedTween(motionControls, animations, () => visible, {\n release,\n retain,\n trackPlayback,\n });\n\n trackPlayback();\n\n if (!visible) {\n managedTween.pauseForVisibility();\n }\n\n return managedTween.controls;\n\n /** Deferred replay survives settlement of the previous playback cycle. */\n function handleComplete(): void {\n if (!managedTween.hasDeferredPlay) {\n release();\n }\n }\n\n /** Visibility deferral keeps ownership without adding completion observers. */\n function retain(): void {\n managedTweens.add(managedTween);\n }\n\n /** Retains playback and observes it only if its child promises changed. */\n function trackPlayback(): void {\n retain();\n observer.observe();\n }\n\n /** Invalidates older completion callbacks and forgets terminated playback. */\n function release(): void {\n observer.clear();\n managedTweens.delete(managedTween);\n }\n },\n };\n\n /** Applies one committed host-visibility transition to every active tween. */\n function handleVisibilityChange(nextVisible: boolean): void {\n visible = nextVisible;\n const operation = visible ? 'resumeFromVisibility' : 'pauseForVisibility';\n\n for (const managedTween of managedTweens) {\n managedTween[operation]();\n }\n }\n}\n","import {\n animate as motionAnimate,\n type AnimationOptions,\n type AnimationPlaybackControlsWithThen,\n type AnimationSequence,\n type DOMKeyframesDefinition,\n type ElementOrSelector,\n type MotionValue,\n type ObjectTarget,\n type SequenceOptions,\n type UnresolvedValueKeyframe,\n type ValueAnimationTransition,\n} from 'motion';\n\nimport { getTweenLifecycle } from '#lifecycle/create-tween-lifecycle.js';\nimport type { TweenPlaybackControls } from '#types/playback.js';\n\n/**\n * Animates values through Motion while applying Replayable's host lifecycle.\n *\n * The overloads intentionally mirror Motion's hybrid `animate()` function:\n * sequences, individual values, Motion values, DOM/SVG targets, and plain\n * objects all retain their native keyframe and option inference. Only the\n * returned controls are narrowed to Replayable's stable public contract.\n *\n * @throws When called before `await playable.ready()`.\n */\nexport function animate(\n sequence: AnimationSequence,\n options?: SequenceOptions,\n): TweenPlaybackControls;\nexport function animate(\n value: string | MotionValue<string>,\n keyframes: string | UnresolvedValueKeyframe<string>[],\n options?: ValueAnimationTransition<string>,\n): TweenPlaybackControls;\nexport function animate(\n value: number | MotionValue<number>,\n keyframes: number | UnresolvedValueKeyframe<number>[],\n options?: ValueAnimationTransition<number>,\n): TweenPlaybackControls;\nexport function animate<Value extends string | number>(\n value: Value | MotionValue<Value>,\n keyframes: Value | UnresolvedValueKeyframe<Value>[],\n options?: ValueAnimationTransition<Value>,\n): TweenPlaybackControls;\nexport function animate(\n element: ElementOrSelector,\n keyframes: DOMKeyframesDefinition,\n options?: AnimationOptions,\n): TweenPlaybackControls;\nexport function animate<Object extends object>(\n object: Object | Object[],\n keyframes: ObjectTarget<Object>,\n options?: AnimationOptions,\n): TweenPlaybackControls;\nexport function animate(\n subjectOrSequence: unknown,\n keyframesOrOptions?: unknown,\n options?: unknown,\n): TweenPlaybackControls {\n // Readiness is checked before Motion creates a running animation, ensuring a\n // rejected early call cannot leave untracked browser work behind.\n const lifecycle = getTweenLifecycle();\n const motionControls: AnimationPlaybackControlsWithThen = Reflect.apply(\n motionAnimate,\n undefined,\n [subjectOrSequence, keyframesOrOptions, options],\n );\n\n return lifecycle.manage(motionControls);\n}\n"],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@replayablejs/tween",
|
|
3
|
+
"version": "0.1.0-alpha.0",
|
|
4
|
+
"description": "Lifecycle-aware animation for Replayable playable ads",
|
|
5
|
+
"homepage": "https://github.com/replayablejs/replayable#readme",
|
|
6
|
+
"bugs": {
|
|
7
|
+
"url": "https://github.com/replayablejs/replayable/issues"
|
|
8
|
+
},
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/replayablejs/replayable.git",
|
|
13
|
+
"directory": "packages/tween"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"type": "module",
|
|
19
|
+
"sideEffects": false,
|
|
20
|
+
"imports": {
|
|
21
|
+
"#lifecycle/*": "./src/lifecycle/*",
|
|
22
|
+
"#types/*": "./src/types/*"
|
|
23
|
+
},
|
|
24
|
+
"exports": {
|
|
25
|
+
".": {
|
|
26
|
+
"types": "./dist/index.d.ts",
|
|
27
|
+
"default": "./dist/index.js"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"motion": "13.1.1"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"tsdown": "0.22.14",
|
|
38
|
+
"typescript": "7.0.2",
|
|
39
|
+
"vitest": "4.1.10",
|
|
40
|
+
"@replayablejs/runtime": "0.1.0-alpha.0"
|
|
41
|
+
},
|
|
42
|
+
"peerDependencies": {
|
|
43
|
+
"@replayablejs/runtime": "0.1.0-alpha.0"
|
|
44
|
+
},
|
|
45
|
+
"engines": {
|
|
46
|
+
"node": ">=24.0.0"
|
|
47
|
+
},
|
|
48
|
+
"scripts": {
|
|
49
|
+
"build": "tsdown",
|
|
50
|
+
"dev": "tsdown --watch",
|
|
51
|
+
"lint": "oxlint --type-aware --max-warnings 0 .",
|
|
52
|
+
"test": "vitest run",
|
|
53
|
+
"typecheck": "tsc --noEmit"
|
|
54
|
+
}
|
|
55
|
+
}
|