@ghia/native 0.0.0 → 0.11.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 +201 -0
- package/browser.mjs +7 -0
- package/dist/aria.d.ts +72 -0
- package/dist/aria.d.ts.map +1 -0
- package/dist/aria.js +2 -0
- package/dist/aria.js.map +1 -0
- package/dist/automation/client.d.ts +243 -0
- package/dist/automation/client.d.ts.map +1 -0
- package/dist/automation/client.js +801 -0
- package/dist/automation/client.js.map +1 -0
- package/dist/automation/index.d.ts +5 -0
- package/dist/automation/index.d.ts.map +1 -0
- package/dist/automation/index.js +4 -0
- package/dist/automation/index.js.map +1 -0
- package/dist/automation/protocol.d.ts +309 -0
- package/dist/automation/protocol.d.ts.map +1 -0
- package/dist/automation/protocol.js +320 -0
- package/dist/automation/protocol.js.map +1 -0
- package/dist/blob-images.d.ts +32 -0
- package/dist/blob-images.d.ts.map +1 -0
- package/dist/blob-images.js +55 -0
- package/dist/blob-images.js.map +1 -0
- package/dist/bounds.d.ts +61 -0
- package/dist/bounds.d.ts.map +1 -0
- package/dist/bounds.js +85 -0
- package/dist/bounds.js.map +1 -0
- package/dist/clipboard-global.d.ts +11 -0
- package/dist/clipboard-global.d.ts.map +1 -0
- package/dist/clipboard-global.js +2 -0
- package/dist/clipboard-global.js.map +1 -0
- package/dist/clipboard.d.ts +19 -0
- package/dist/clipboard.d.ts.map +1 -0
- package/dist/clipboard.js +41 -0
- package/dist/clipboard.js.map +1 -0
- package/dist/cpu-throttle.d.ts +6 -0
- package/dist/cpu-throttle.d.ts.map +1 -0
- package/dist/cpu-throttle.js +42 -0
- package/dist/cpu-throttle.js.map +1 -0
- package/dist/events/data-transfer.d.ts +23 -0
- package/dist/events/data-transfer.d.ts.map +1 -0
- package/dist/events/data-transfer.js +97 -0
- package/dist/events/data-transfer.js.map +1 -0
- package/dist/events/dispatch.d.ts +144 -0
- package/dist/events/dispatch.d.ts.map +1 -0
- package/dist/events/dispatch.js +241 -0
- package/dist/events/dispatch.js.map +1 -0
- package/dist/events/drag.d.ts +21 -0
- package/dist/events/drag.d.ts.map +1 -0
- package/dist/events/drag.js +111 -0
- package/dist/events/drag.js.map +1 -0
- package/dist/events/event-props.d.ts +19 -0
- package/dist/events/event-props.d.ts.map +1 -0
- package/dist/events/event-props.js +104 -0
- package/dist/events/event-props.js.map +1 -0
- package/dist/events/keyboard-defaults.d.ts +18 -0
- package/dist/events/keyboard-defaults.d.ts.map +1 -0
- package/dist/events/keyboard-defaults.js +121 -0
- package/dist/events/keyboard-defaults.js.map +1 -0
- package/dist/events/listeners.d.ts +18 -0
- package/dist/events/listeners.d.ts.map +1 -0
- package/dist/events/listeners.js +49 -0
- package/dist/events/listeners.js.map +1 -0
- package/dist/events/route.d.ts +17 -0
- package/dist/events/route.d.ts.map +1 -0
- package/dist/events/route.js +36 -0
- package/dist/events/route.js.map +1 -0
- package/dist/events/types.d.ts +269 -0
- package/dist/events/types.d.ts.map +1 -0
- package/dist/events/types.js +2 -0
- package/dist/events/types.js.map +1 -0
- package/dist/events/window-target.d.ts +120 -0
- package/dist/events/window-target.d.ts.map +1 -0
- package/dist/events/window-target.js +311 -0
- package/dist/events/window-target.js.map +1 -0
- package/dist/eyedropper.d.ts +33 -0
- package/dist/eyedropper.d.ts.map +1 -0
- package/dist/eyedropper.js +66 -0
- package/dist/eyedropper.js.map +1 -0
- package/dist/focus.d.ts +18 -0
- package/dist/focus.d.ts.map +1 -0
- package/dist/focus.js +26 -0
- package/dist/focus.js.map +1 -0
- package/dist/fonts.d.ts +12 -0
- package/dist/fonts.d.ts.map +1 -0
- package/dist/fonts.js +17 -0
- package/dist/fonts.js.map +1 -0
- package/dist/forms.d.ts +10 -0
- package/dist/forms.d.ts.map +1 -0
- package/dist/forms.js +9 -0
- package/dist/forms.js.map +1 -0
- package/dist/host-runtime.d.ts +108 -0
- package/dist/host-runtime.d.ts.map +1 -0
- package/dist/host-runtime.js +213 -0
- package/dist/host-runtime.js.map +1 -0
- package/dist/host.d.ts +2403 -0
- package/dist/host.d.ts.map +1 -0
- package/dist/host.js +30 -0
- package/dist/host.js.map +1 -0
- package/dist/idle-gc.d.ts +36 -0
- package/dist/idle-gc.d.ts.map +1 -0
- package/dist/idle-gc.js +70 -0
- package/dist/idle-gc.js.map +1 -0
- package/dist/keyframes.d.ts +108 -0
- package/dist/keyframes.d.ts.map +1 -0
- package/dist/keyframes.js +107 -0
- package/dist/keyframes.js.map +1 -0
- package/dist/layout-animation.d.ts +38 -0
- package/dist/layout-animation.d.ts.map +1 -0
- package/dist/layout-animation.js +41 -0
- package/dist/layout-animation.js.map +1 -0
- package/dist/match-media.d.ts +62 -0
- package/dist/match-media.d.ts.map +1 -0
- package/dist/match-media.js +272 -0
- package/dist/match-media.js.map +1 -0
- package/dist/media-query.d.ts +21 -0
- package/dist/media-query.d.ts.map +1 -0
- package/dist/media-query.js +599 -0
- package/dist/media-query.js.map +1 -0
- package/dist/motion-bindings.d.ts +36 -0
- package/dist/motion-bindings.d.ts.map +1 -0
- package/dist/motion-bindings.js +107 -0
- package/dist/motion-bindings.js.map +1 -0
- package/dist/motion-config.d.ts +20 -0
- package/dist/motion-config.d.ts.map +1 -0
- package/dist/motion-config.js +51 -0
- package/dist/motion-config.js.map +1 -0
- package/dist/motion-span.d.ts +57 -0
- package/dist/motion-span.d.ts.map +1 -0
- package/dist/motion-span.js +150 -0
- package/dist/motion-span.js.map +1 -0
- package/dist/motion-value.d.ts +269 -0
- package/dist/motion-value.d.ts.map +1 -0
- package/dist/motion-value.js +941 -0
- package/dist/motion-value.js.map +1 -0
- package/dist/motion-variants.d.ts +190 -0
- package/dist/motion-variants.d.ts.map +1 -0
- package/dist/motion-variants.js +249 -0
- package/dist/motion-variants.js.map +1 -0
- package/dist/mutations.d.ts +65 -0
- package/dist/mutations.d.ts.map +1 -0
- package/dist/mutations.js +320 -0
- package/dist/mutations.js.map +1 -0
- package/dist/nativeview.d.ts +10 -0
- package/dist/nativeview.d.ts.map +1 -0
- package/dist/nativeview.js +92 -0
- package/dist/nativeview.js.map +1 -0
- package/dist/observers.d.ts +92 -0
- package/dist/observers.d.ts.map +1 -0
- package/dist/observers.js +579 -0
- package/dist/observers.js.map +1 -0
- package/dist/presence.d.ts +63 -0
- package/dist/presence.d.ts.map +1 -0
- package/dist/presence.js +54 -0
- package/dist/presence.js.map +1 -0
- package/dist/renderer-state.d.ts +155 -0
- package/dist/renderer-state.d.ts.map +1 -0
- package/dist/renderer-state.js +302 -0
- package/dist/renderer-state.js.map +1 -0
- package/dist/reorder.d.ts +63 -0
- package/dist/reorder.d.ts.map +1 -0
- package/dist/reorder.js +303 -0
- package/dist/reorder.js.map +1 -0
- package/dist/runtime.d.ts +23 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +157 -0
- package/dist/runtime.js.map +1 -0
- package/dist/screen.d.ts +8 -0
- package/dist/screen.d.ts.map +1 -0
- package/dist/screen.js +16 -0
- package/dist/screen.js.map +1 -0
- package/dist/scroll.d.ts +79 -0
- package/dist/scroll.d.ts.map +1 -0
- package/dist/scroll.js +170 -0
- package/dist/scroll.js.map +1 -0
- package/dist/style-images.d.ts +20 -0
- package/dist/style-images.d.ts.map +1 -0
- package/dist/style-images.js +57 -0
- package/dist/style-images.js.map +1 -0
- package/dist/svg.d.ts +16 -0
- package/dist/svg.d.ts.map +1 -0
- package/dist/svg.js +119 -0
- package/dist/svg.js.map +1 -0
- package/dist/testing.d.ts +811 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +1187 -0
- package/dist/testing.js.map +1 -0
- package/dist/text-match.d.ts +8 -0
- package/dist/text-match.d.ts.map +1 -0
- package/dist/text-match.js +10 -0
- package/dist/text-match.js.map +1 -0
- package/dist/text-selection.d.ts +34 -0
- package/dist/text-selection.d.ts.map +1 -0
- package/dist/text-selection.js +23 -0
- package/dist/text-selection.js.map +1 -0
- package/dist/universal-style.d.ts +22 -0
- package/dist/universal-style.d.ts.map +1 -0
- package/dist/universal-style.js +58 -0
- package/dist/universal-style.js.map +1 -0
- package/dist/web-animations.d.ts +300 -0
- package/dist/web-animations.d.ts.map +1 -0
- package/dist/web-animations.js +1032 -0
- package/dist/web-animations.js.map +1 -0
- package/dist/webview.d.ts +39 -0
- package/dist/webview.d.ts.map +1 -0
- package/dist/webview.js +26 -0
- package/dist/webview.js.map +1 -0
- package/gpuix-native.darwin-arm64.node +4 -0
- package/index.cjs +789 -0
- package/index.d.ts +2603 -0
- package/index.js +729 -0
- package/package.json +96 -4
|
@@ -0,0 +1,1032 @@
|
|
|
1
|
+
import { flushPendingMutations } from "./bounds.js";
|
|
2
|
+
import { EVENT_PHASE_AT_TARGET, EVENT_PHASE_NONE, reportUncaught } from "./events/dispatch.js";
|
|
3
|
+
import { elementOf, GpuixEventTarget, setDocumentAnimations, } from "./events/window-target.js";
|
|
4
|
+
const HOST_METHODS = [
|
|
5
|
+
"getAnimations",
|
|
6
|
+
"getSubtreeAnimations",
|
|
7
|
+
"getDocumentAnimations",
|
|
8
|
+
"getAnimation",
|
|
9
|
+
"pauseAnimation",
|
|
10
|
+
"playAnimation",
|
|
11
|
+
"cancelAnimation",
|
|
12
|
+
"finishAnimation",
|
|
13
|
+
"reverseAnimation",
|
|
14
|
+
"setAnimationCurrentTime",
|
|
15
|
+
"setAnimationPlaybackRate",
|
|
16
|
+
"updateAnimationPlaybackRate",
|
|
17
|
+
"setAnimationStartTime",
|
|
18
|
+
"getAnimationTimelineTime",
|
|
19
|
+
"releaseAnimation",
|
|
20
|
+
"forgetAnimation",
|
|
21
|
+
"getAnimationEffect",
|
|
22
|
+
"getAnimationKeyframes",
|
|
23
|
+
"updateAnimationTiming",
|
|
24
|
+
];
|
|
25
|
+
const FINISH_EVENT = "webanimationfinish";
|
|
26
|
+
const CANCEL_EVENT = "webanimationcancel";
|
|
27
|
+
const READY_EVENT = "webanimationready";
|
|
28
|
+
/** What native says when a command refuses, before it is a `DOMException`. */
|
|
29
|
+
const INVALID_STATE_PREFIX = "InvalidStateError: ";
|
|
30
|
+
const TYPE_ERROR_PREFIX = "TypeError: ";
|
|
31
|
+
function deferred() {
|
|
32
|
+
let settle;
|
|
33
|
+
const promise = new Promise((resolve, reject) => {
|
|
34
|
+
settle = { resolve, reject };
|
|
35
|
+
});
|
|
36
|
+
// A cancel rejects it whether or not anyone is listening, and that is not an error to report.
|
|
37
|
+
promise.catch(() => { });
|
|
38
|
+
const result = {
|
|
39
|
+
promise,
|
|
40
|
+
settled: "pending",
|
|
41
|
+
resolve(animation) {
|
|
42
|
+
result.settled = "resolved";
|
|
43
|
+
settle.resolve(animation);
|
|
44
|
+
},
|
|
45
|
+
reject(error) {
|
|
46
|
+
result.settled = "rejected";
|
|
47
|
+
settle.reject(error);
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
return result;
|
|
51
|
+
}
|
|
52
|
+
function resolved(animation) {
|
|
53
|
+
const result = deferred();
|
|
54
|
+
result.resolve(animation);
|
|
55
|
+
return result;
|
|
56
|
+
}
|
|
57
|
+
const states = new WeakMap();
|
|
58
|
+
function stateOf(animation) {
|
|
59
|
+
const state = states.get(animation);
|
|
60
|
+
if (!state)
|
|
61
|
+
throw new TypeError("Illegal invocation");
|
|
62
|
+
return state;
|
|
63
|
+
}
|
|
64
|
+
function invalidState(message) {
|
|
65
|
+
return new DOMException(message, "InvalidStateError");
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* The timeline of every animation of a renderer: its time is the clock of the renderer, in
|
|
69
|
+
* milliseconds, at the last frame. `startTime` and the `timelineTime` of the events are on it.
|
|
70
|
+
* A renderer is the document of its animations, so there is one for each, and no
|
|
71
|
+
* `document.timeline`: the `document` of GPUIX spans every window.
|
|
72
|
+
*/
|
|
73
|
+
export class DocumentTimeline {
|
|
74
|
+
#host;
|
|
75
|
+
/** Not for apps: the timeline of an animation is `animation.timeline`. */
|
|
76
|
+
constructor(host) {
|
|
77
|
+
this.#host = host;
|
|
78
|
+
}
|
|
79
|
+
get currentTime() {
|
|
80
|
+
return this.#host.getAnimationTimelineTime();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/** `readLive` can find the run gone, which the earlier checks of the mode cannot see. */
|
|
84
|
+
function isGone(state) {
|
|
85
|
+
return state.mode === "gone";
|
|
86
|
+
}
|
|
87
|
+
function readLive(state) {
|
|
88
|
+
try {
|
|
89
|
+
const info = state.host.getAnimation(state.id);
|
|
90
|
+
if (info.playState === "idle" && state.last.playState !== "idle" && state.cancelEvents === 0) {
|
|
91
|
+
state.cancelNoticed = { pending: state.last.pending };
|
|
92
|
+
}
|
|
93
|
+
if (info.playState !== "finished")
|
|
94
|
+
state.holdFinished = false;
|
|
95
|
+
state.last = info;
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
if (!isUnknownAnimation(error))
|
|
99
|
+
throw error;
|
|
100
|
+
// The idle run of a `cancel()` that the style stopped naming: native dropped it without a word.
|
|
101
|
+
state.mode = "gone";
|
|
102
|
+
state.last = { ...state.last, playState: "idle", pending: false, startTime: undefined };
|
|
103
|
+
}
|
|
104
|
+
return state.last;
|
|
105
|
+
}
|
|
106
|
+
/** What native refused with, as the DOM would. */
|
|
107
|
+
function refusal(error) {
|
|
108
|
+
if (error instanceof Error && error.message.startsWith(INVALID_STATE_PREFIX)) {
|
|
109
|
+
return invalidState(error.message.slice(INVALID_STATE_PREFIX.length));
|
|
110
|
+
}
|
|
111
|
+
if (error instanceof Error && error.message.startsWith(TYPE_ERROR_PREFIX)) {
|
|
112
|
+
return new TypeError(error.message.slice(TYPE_ERROR_PREFIX.length));
|
|
113
|
+
}
|
|
114
|
+
return error;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Sends a command to native, then finds what it did: a play or pause that was
|
|
118
|
+
* waiting and is not (`ready`), a finish, and the end of one. A run native no
|
|
119
|
+
* longer has is one that was cancelled since the frame JS last heard of, as far
|
|
120
|
+
* as JS is concerned.
|
|
121
|
+
*/
|
|
122
|
+
function command(animation, action, send) {
|
|
123
|
+
const state = stateOf(animation);
|
|
124
|
+
const cancelled = () => invalidState(`Cannot ${action} an animation that was cancelled`);
|
|
125
|
+
if (state.mode === "gone")
|
|
126
|
+
throw cancelled();
|
|
127
|
+
let before;
|
|
128
|
+
try {
|
|
129
|
+
before = readLive(state);
|
|
130
|
+
if (isGone(state))
|
|
131
|
+
throw cancelled();
|
|
132
|
+
send(state);
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
throw isUnknownAnimation(error) ? cancelled() : refusal(error);
|
|
136
|
+
}
|
|
137
|
+
// A command can bring back an animation that was idle and only weakly held: it plays now, and events are owed to it.
|
|
138
|
+
register(state.registry, state.elementId, state.id, animation);
|
|
139
|
+
settle(animation, state, before);
|
|
140
|
+
}
|
|
141
|
+
/** What the state native is in says about the promises, after a command. */
|
|
142
|
+
function settle(animation, state, before) {
|
|
143
|
+
if (state.mode !== "live")
|
|
144
|
+
return;
|
|
145
|
+
const info = readLive(state);
|
|
146
|
+
state.renewedForCancel = false;
|
|
147
|
+
if (before.pending && !info.pending) {
|
|
148
|
+
if (state.ready.settled === "pending")
|
|
149
|
+
state.ready.resolve(animation);
|
|
150
|
+
}
|
|
151
|
+
else if (info.pending && !before.pending) {
|
|
152
|
+
state.ready = deferred();
|
|
153
|
+
}
|
|
154
|
+
if (info.playState === "finished") {
|
|
155
|
+
if (state.finished.settled === "pending")
|
|
156
|
+
state.finished.resolve(animation);
|
|
157
|
+
}
|
|
158
|
+
else if (state.finished.settled === "resolved") {
|
|
159
|
+
state.finished = deferred();
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
function snapshot(read) {
|
|
163
|
+
try {
|
|
164
|
+
return { value: read() };
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
if (isUnknownAnimation(error))
|
|
168
|
+
throw error;
|
|
169
|
+
return { error: new DOMException(error instanceof Error ? error.message : String(error), "NotSupportedError") };
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
function unwrap(snapshot) {
|
|
173
|
+
if ("error" in snapshot)
|
|
174
|
+
throw snapshot.error;
|
|
175
|
+
return snapshot.value;
|
|
176
|
+
}
|
|
177
|
+
const effects = new WeakMap();
|
|
178
|
+
function effectOf(effect) {
|
|
179
|
+
const init = effects.get(effect);
|
|
180
|
+
if (!init)
|
|
181
|
+
throw new TypeError("Illegal invocation");
|
|
182
|
+
return init;
|
|
183
|
+
}
|
|
184
|
+
/** What native says of the effect now; what it said last once it no longer has the run. */
|
|
185
|
+
function readEffect(state) {
|
|
186
|
+
let read = state.effect;
|
|
187
|
+
if (state.mode === "live") {
|
|
188
|
+
try {
|
|
189
|
+
read = state.effect = snapshot(() => state.host.getAnimationEffect(state.id));
|
|
190
|
+
}
|
|
191
|
+
catch (error) {
|
|
192
|
+
if (!isUnknownAnimation(error))
|
|
193
|
+
throw error;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
const info = unwrap(read);
|
|
197
|
+
// A run native let go of is idle, whatever it showed when it went.
|
|
198
|
+
if (state.mode === "gone" || readLive(state).playState === "idle") {
|
|
199
|
+
return { ...info, localTime: undefined, progress: undefined, currentIteration: undefined };
|
|
200
|
+
}
|
|
201
|
+
return info;
|
|
202
|
+
}
|
|
203
|
+
/** A member the DOM takes as a finite number, which `updateTiming()` refuses to leave anything else. */
|
|
204
|
+
function finite(name, value) {
|
|
205
|
+
const number = Number(value);
|
|
206
|
+
if (!Number.isFinite(number))
|
|
207
|
+
throw new TypeError(`Failed to execute 'updateTiming' on 'AnimationEffect': ${name} must be a finite number`);
|
|
208
|
+
return number;
|
|
209
|
+
}
|
|
210
|
+
function oneOf(name, value, names) {
|
|
211
|
+
if (typeof value !== "string" || !names.includes(value)) {
|
|
212
|
+
throw new TypeError(`Failed to execute 'updateTiming' on 'AnimationEffect': ${String(value)} is not a valid ${name}`);
|
|
213
|
+
}
|
|
214
|
+
return value;
|
|
215
|
+
}
|
|
216
|
+
/** The `updateTiming()` argument as native takes it, or the `TypeError` the DOM throws for a member that is wrong. */
|
|
217
|
+
function timingUpdate(timing) {
|
|
218
|
+
if (timing === undefined || timing === null)
|
|
219
|
+
return {};
|
|
220
|
+
if (typeof timing !== "object") {
|
|
221
|
+
throw new TypeError("Failed to execute 'updateTiming' on 'AnimationEffect': the timing is not an object");
|
|
222
|
+
}
|
|
223
|
+
const update = {};
|
|
224
|
+
if (timing.delay !== undefined)
|
|
225
|
+
update.delay = finite("delay", timing.delay);
|
|
226
|
+
if (timing.endDelay !== undefined)
|
|
227
|
+
update.endDelay = finite("endDelay", timing.endDelay);
|
|
228
|
+
if (timing.iterationStart !== undefined) {
|
|
229
|
+
update.iterationStart = finite("iterationStart", timing.iterationStart);
|
|
230
|
+
if (update.iterationStart < 0)
|
|
231
|
+
throw new TypeError("Failed to execute 'updateTiming' on 'AnimationEffect': iterationStart must not be negative");
|
|
232
|
+
}
|
|
233
|
+
if (timing.iterations !== undefined) {
|
|
234
|
+
const iterations = Number(timing.iterations);
|
|
235
|
+
if (Number.isNaN(iterations) || iterations < 0) {
|
|
236
|
+
throw new TypeError("Failed to execute 'updateTiming' on 'AnimationEffect': iterations must not be negative");
|
|
237
|
+
}
|
|
238
|
+
update.iterations = iterations;
|
|
239
|
+
}
|
|
240
|
+
if (timing.duration !== undefined) {
|
|
241
|
+
if (typeof timing.duration === "string") {
|
|
242
|
+
if (timing.duration !== "auto")
|
|
243
|
+
throw new TypeError(`Failed to execute 'updateTiming' on 'AnimationEffect': ${timing.duration} is not a valid duration`);
|
|
244
|
+
update.duration = "auto";
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
const duration = Number(timing.duration);
|
|
248
|
+
if (Number.isNaN(duration) || duration < 0) {
|
|
249
|
+
throw new TypeError("Failed to execute 'updateTiming' on 'AnimationEffect': duration must not be negative");
|
|
250
|
+
}
|
|
251
|
+
update.duration = duration;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
// `auto` is no fill: an effect has none of its own.
|
|
255
|
+
if (timing.fill !== undefined) {
|
|
256
|
+
const fill = oneOf("fill", timing.fill, ["none", "forwards", "backwards", "both", "auto"]);
|
|
257
|
+
update.fill = fill === "auto" ? "none" : fill;
|
|
258
|
+
}
|
|
259
|
+
if (timing.direction !== undefined) {
|
|
260
|
+
update.direction = oneOf("direction", timing.direction, ["normal", "reverse", "alternate", "alternate-reverse"]);
|
|
261
|
+
}
|
|
262
|
+
if (timing.easing !== undefined)
|
|
263
|
+
update.easing = String(timing.easing);
|
|
264
|
+
return update;
|
|
265
|
+
}
|
|
266
|
+
const EASING_KEYWORDS = ["linear", "ease", "ease-in", "ease-out", "ease-in-out", "step-start", "step-end"];
|
|
267
|
+
/** `updateTiming()` of a run native no longer has: it changes what the effect reports from now on. */
|
|
268
|
+
function updateGone(state, update) {
|
|
269
|
+
const info = unwrap(state.effect);
|
|
270
|
+
if (update.easing !== undefined && !EASING_KEYWORDS.includes(update.easing.trim())) {
|
|
271
|
+
throw new DOMException("The easing of an animation that was cancelled for good can be set to a keyword only", "NotSupportedError");
|
|
272
|
+
}
|
|
273
|
+
const next = { ...info };
|
|
274
|
+
if (update.delay !== undefined)
|
|
275
|
+
next.delay = update.delay;
|
|
276
|
+
if (update.endDelay !== undefined)
|
|
277
|
+
next.endDelay = update.endDelay;
|
|
278
|
+
if (update.fill !== undefined)
|
|
279
|
+
next.fill = update.fill;
|
|
280
|
+
if (update.iterationStart !== undefined)
|
|
281
|
+
next.iterationStart = update.iterationStart;
|
|
282
|
+
if (update.iterations !== undefined)
|
|
283
|
+
next.iterations = update.iterations;
|
|
284
|
+
if (update.direction !== undefined)
|
|
285
|
+
next.direction = update.direction;
|
|
286
|
+
if (update.duration !== undefined)
|
|
287
|
+
next.duration = update.duration === "auto" ? undefined : update.duration;
|
|
288
|
+
if (update.easing !== undefined)
|
|
289
|
+
next.easing = update.easing.trim() === "step-start" ? "steps(1, start)" : update.easing.trim() === "step-end" ? "steps(1)" : update.easing.trim();
|
|
290
|
+
const duration = next.duration ?? 0;
|
|
291
|
+
const active = duration === 0 || next.iterations === 0 ? 0 : duration * next.iterations;
|
|
292
|
+
next.activeDuration = active;
|
|
293
|
+
next.endTime = Math.max(next.delay + active + next.endDelay, 0);
|
|
294
|
+
state.effect = { value: next };
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* The effect of a CSS animation or transition, as the DOM's `KeyframeEffect`: the keyframes
|
|
298
|
+
* and the timing it plays by. `animation.effect` is the same object every time. It cannot be
|
|
299
|
+
* made, and there is no `setKeyframes()`, `composite` or `pseudoElement`.
|
|
300
|
+
*/
|
|
301
|
+
export class KeyframeEffect {
|
|
302
|
+
/** Not for apps: the effect of an animation is `animation.effect`. */
|
|
303
|
+
constructor(init) {
|
|
304
|
+
effects.set(this, init);
|
|
305
|
+
}
|
|
306
|
+
/** The element the run is of, also after the element is gone. */
|
|
307
|
+
get target() {
|
|
308
|
+
return effectOf(this).target;
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* The keyframes with computed values: those of the `@keyframes` rule with the ones Chrome adds
|
|
312
|
+
* for a property that has none at 0% or 100% (`composite: "replace"`, at the value the element
|
|
313
|
+
* has without the animation), or the start and the end of a transition. As of the last frame.
|
|
314
|
+
* Throws a `NotSupportedError` for a value whose computed text is not known yet.
|
|
315
|
+
*/
|
|
316
|
+
getKeyframes() {
|
|
317
|
+
const state = stateOf(effectOf(this).animation);
|
|
318
|
+
if (state.mode === "live") {
|
|
319
|
+
try {
|
|
320
|
+
state.keyframes = snapshot(() => state.host.getAnimationKeyframes(state.id));
|
|
321
|
+
}
|
|
322
|
+
catch (error) {
|
|
323
|
+
if (!isUnknownAnimation(error))
|
|
324
|
+
throw error;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return unwrap(state.keyframes).map((frame) => {
|
|
328
|
+
const keyframe = {
|
|
329
|
+
offset: frame.offset ?? null,
|
|
330
|
+
easing: frame.easing,
|
|
331
|
+
composite: frame.composite,
|
|
332
|
+
computedOffset: frame.computedOffset,
|
|
333
|
+
};
|
|
334
|
+
for (const value of frame.values)
|
|
335
|
+
keyframe[value.property] = value.value;
|
|
336
|
+
return keyframe;
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* The timing in the units of the DOM: milliseconds, `iterations` `Infinity` for an animation
|
|
341
|
+
* that repeats for ever. For a CSS animation the `easing` is `"linear"` and its
|
|
342
|
+
* `animation-timing-function` is the easing of its keyframes; for a transition it is the
|
|
343
|
+
* `transition-timing-function`.
|
|
344
|
+
*/
|
|
345
|
+
getTiming() {
|
|
346
|
+
const info = readEffect(stateOf(effectOf(this).animation));
|
|
347
|
+
return {
|
|
348
|
+
delay: info.delay,
|
|
349
|
+
endDelay: info.endDelay,
|
|
350
|
+
fill: info.fill,
|
|
351
|
+
iterationStart: info.iterationStart,
|
|
352
|
+
iterations: info.iterations,
|
|
353
|
+
duration: info.duration ?? "auto",
|
|
354
|
+
direction: info.direction,
|
|
355
|
+
easing: info.easing,
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* The timing with what it works out to at the current time of the animation: `localTime`,
|
|
360
|
+
* `progress` and `currentIteration` are `null` for an idle animation and `progress` and
|
|
361
|
+
* `currentIteration` outside the active phase unless a fill holds the effect.
|
|
362
|
+
*/
|
|
363
|
+
getComputedTiming() {
|
|
364
|
+
const info = readEffect(stateOf(effectOf(this).animation));
|
|
365
|
+
return {
|
|
366
|
+
delay: info.delay,
|
|
367
|
+
endDelay: info.endDelay,
|
|
368
|
+
fill: info.fill,
|
|
369
|
+
iterationStart: info.iterationStart,
|
|
370
|
+
iterations: info.iterations,
|
|
371
|
+
duration: info.duration ?? 0,
|
|
372
|
+
direction: info.direction,
|
|
373
|
+
easing: info.easing,
|
|
374
|
+
endTime: info.endTime,
|
|
375
|
+
activeDuration: info.activeDuration,
|
|
376
|
+
localTime: info.localTime ?? null,
|
|
377
|
+
progress: info.progress ?? null,
|
|
378
|
+
currentIteration: info.currentIteration ?? null,
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Sets the timing members given, for this animation only. What a CSS animation is set to here
|
|
383
|
+
* stays against a later change of its `animation-*` style, which still moves the members that
|
|
384
|
+
* were not set; a running transition never follows a change of its `transition-*` style. A
|
|
385
|
+
* member that is wrong throws a `TypeError` and changes nothing. An effect that ends later than
|
|
386
|
+
* the animation has gone makes a finished animation run again.
|
|
387
|
+
*/
|
|
388
|
+
updateTiming(timing) {
|
|
389
|
+
const owner = effectOf(this);
|
|
390
|
+
const update = timingUpdate(timing);
|
|
391
|
+
const state = stateOf(owner.animation);
|
|
392
|
+
if (state.mode === "live") {
|
|
393
|
+
try {
|
|
394
|
+
command(owner.animation, "update the timing of", (live) => live.host.updateAnimationTiming(live.id, update));
|
|
395
|
+
// What the animation keeps once native lets the run go.
|
|
396
|
+
try {
|
|
397
|
+
state.effect = snapshot(() => state.host.getAnimationEffect(state.id));
|
|
398
|
+
}
|
|
399
|
+
catch (error) {
|
|
400
|
+
if (!isUnknownAnimation(error))
|
|
401
|
+
throw error;
|
|
402
|
+
}
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
catch (error) {
|
|
406
|
+
// Cancelled since the frame JS last heard of: the effect is what it was.
|
|
407
|
+
if (!(error instanceof DOMException && error.name === "InvalidStateError"))
|
|
408
|
+
throw error;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
updateGone(state, update);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* A CSS animation or transition of an element, or an animation of a `motion` element that Motion would hand to the
|
|
416
|
+
* browser's Web Animations (an `opacity` or `backgroundColor` animation, a colour in `oklch()`, ...; a plain
|
|
417
|
+
* `Animation`, as the one `element.animate()` makes), as `element.getAnimations()`
|
|
418
|
+
* returns it: the Web Animations `Animation` of the DOM, for what the CSS
|
|
419
|
+
* engine runs. `getAnimations()` gives the same object for the same run every
|
|
420
|
+
* time. `playState`, `currentTime`, `playbackRate`, `startTime` and `pending`
|
|
421
|
+
* are read when asked, as of the last frame, which is also the time of
|
|
422
|
+
* `timeline`. The promises `ready` and `finished` and the `finish` and `cancel`
|
|
423
|
+
* events follow Chrome's order.
|
|
424
|
+
*
|
|
425
|
+
* Not built: `replaceState`, `persist()`, `commitStyles()`,
|
|
426
|
+
* `onremove`, and setting `effect`. Every command works on an animation of the motion engine as it does on the one Motion
|
|
427
|
+
* makes with the browser: the motion shows what the animation does, `finish()` completes it, `cancel()` takes it back to
|
|
428
|
+
* where it started without a completion (Motion never hears of a cancel). An animation the style or Motion cancelled is idle,
|
|
429
|
+
* and a command plays it again as in Chrome, without the style owning it any more, for as long as the app holds it.
|
|
430
|
+
*/
|
|
431
|
+
export class Animation extends GpuixEventTarget {
|
|
432
|
+
/** Empty for what `getAnimations()` lists: the animations of CSS and of Motion have no id. */
|
|
433
|
+
id = "";
|
|
434
|
+
constructor(init) {
|
|
435
|
+
super();
|
|
436
|
+
const state = {
|
|
437
|
+
host: init.host,
|
|
438
|
+
registry: init.registry,
|
|
439
|
+
timeline: init.timeline,
|
|
440
|
+
id: init.info.id,
|
|
441
|
+
elementId: init.elementId,
|
|
442
|
+
mode: "live",
|
|
443
|
+
last: init.info,
|
|
444
|
+
effect: snapshot(() => init.host.getAnimationEffect(init.info.id)),
|
|
445
|
+
keyframes: snapshot(() => init.host.getAnimationKeyframes(init.info.id)),
|
|
446
|
+
keyframeEffect: new KeyframeEffect({ animation: this, target: init.target }),
|
|
447
|
+
finished: deferred(),
|
|
448
|
+
ready: deferred(),
|
|
449
|
+
cancelEvents: 0,
|
|
450
|
+
renewedForCancel: false,
|
|
451
|
+
cancelNoticed: undefined,
|
|
452
|
+
holdFinished: false,
|
|
453
|
+
handlers: new Map(),
|
|
454
|
+
};
|
|
455
|
+
states.set(this, state);
|
|
456
|
+
// A finished animation that waits for its first frame is told of its finish by the first command or
|
|
457
|
+
// event, not when it is made.
|
|
458
|
+
if (init.info.playState === "finished" && !init.info.pending)
|
|
459
|
+
state.finished.resolve(this);
|
|
460
|
+
if (!init.info.pending)
|
|
461
|
+
state.ready.resolve(this);
|
|
462
|
+
// An animation made finished (an effect with no length) has nothing to wait a frame for: Chrome
|
|
463
|
+
// runs its play right after the script that first read it, which a seek to where it is does here.
|
|
464
|
+
if (init.info.pending && init.info.playState === "finished")
|
|
465
|
+
queueMicrotask(() => startFinished(this));
|
|
466
|
+
}
|
|
467
|
+
/** The timeline all the animations of a renderer share. */
|
|
468
|
+
get timeline() {
|
|
469
|
+
return stateOf(this).timeline;
|
|
470
|
+
}
|
|
471
|
+
/** The keyframes and the timing the run plays by. The same object every time. */
|
|
472
|
+
get effect() {
|
|
473
|
+
return stateOf(this).keyframeEffect;
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* Milliseconds from the start, the delay inside it: 0 at the start whatever
|
|
477
|
+
* the delay, and the end of the effect when it ends. `null` once the
|
|
478
|
+
* animation is cancelled. Setting it seeks at once and the next frame paints
|
|
479
|
+
* it; `null` cannot be set.
|
|
480
|
+
*/
|
|
481
|
+
get currentTime() {
|
|
482
|
+
const state = stateOf(this);
|
|
483
|
+
switch (state.mode) {
|
|
484
|
+
case "live": {
|
|
485
|
+
const info = readLive(state);
|
|
486
|
+
return info.playState === "idle" ? null : info.currentTime;
|
|
487
|
+
}
|
|
488
|
+
case "gone":
|
|
489
|
+
return null;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
set currentTime(value) {
|
|
493
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
494
|
+
throw new TypeError(`Failed to set currentTime: ${String(value)} is not a finite number`);
|
|
495
|
+
}
|
|
496
|
+
command(this, "set currentTime of", (state) => state.host.setAnimationCurrentTime(state.id, value));
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* When the animation started on `timeline`, in milliseconds: `currentTime` is
|
|
500
|
+
* `(timeline.currentTime - startTime) * playbackRate`. `null` while it is
|
|
501
|
+
* pending, paused or idle. Setting it moves the animation to that time; `null`
|
|
502
|
+
* holds the time it is at.
|
|
503
|
+
*/
|
|
504
|
+
get startTime() {
|
|
505
|
+
const state = stateOf(this);
|
|
506
|
+
switch (state.mode) {
|
|
507
|
+
case "live":
|
|
508
|
+
return readLive(state).startTime ?? null;
|
|
509
|
+
case "gone":
|
|
510
|
+
return null;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
set startTime(value) {
|
|
514
|
+
if (value !== null && (typeof value !== "number" || !Number.isFinite(value))) {
|
|
515
|
+
throw new TypeError(`Failed to set startTime: ${String(value)} is not a finite number or null`);
|
|
516
|
+
}
|
|
517
|
+
command(this, "set startTime of", (state) => state.host.setAnimationStartTime(state.id, value));
|
|
518
|
+
}
|
|
519
|
+
/** The speed of time, negative to run back. Changing it keeps `currentTime`. */
|
|
520
|
+
get playbackRate() {
|
|
521
|
+
const state = stateOf(this);
|
|
522
|
+
return state.mode === "live" ? readLive(state).playbackRate : state.last.playbackRate;
|
|
523
|
+
}
|
|
524
|
+
set playbackRate(value) {
|
|
525
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
526
|
+
throw new TypeError(`Failed to set playbackRate: ${String(value)} is not a finite number`);
|
|
527
|
+
}
|
|
528
|
+
command(this, "set playbackRate of", (state) => state.host.setAnimationPlaybackRate(state.id, value));
|
|
529
|
+
}
|
|
530
|
+
get playState() {
|
|
531
|
+
const state = stateOf(this);
|
|
532
|
+
switch (state.mode) {
|
|
533
|
+
case "live":
|
|
534
|
+
return readLive(state).playState;
|
|
535
|
+
case "gone":
|
|
536
|
+
return "idle";
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
/** Whether a play or pause waits for the next frame to run. */
|
|
540
|
+
get pending() {
|
|
541
|
+
const state = stateOf(this);
|
|
542
|
+
return state.mode === "live" && readLive(state).pending;
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* Resolves with the animation when the play or pause that is pending has
|
|
546
|
+
* run, at the next frame; already resolved when none is. A `pause()` of a
|
|
547
|
+
* running animation, a `play()` of a paused, finished or cancelled one and
|
|
548
|
+
* the others that wait for a frame make a new one, unless one is already
|
|
549
|
+
* waiting. A cancel rejects the one that is waiting with an `AbortError`.
|
|
550
|
+
*/
|
|
551
|
+
get ready() {
|
|
552
|
+
return stateOf(this).ready.promise;
|
|
553
|
+
}
|
|
554
|
+
/**
|
|
555
|
+
* Resolves with the animation when it finishes; rejects with an
|
|
556
|
+
* `AbortError` when it is cancelled (the style stopped naming it, a new
|
|
557
|
+
* transition replaced it, its element went). A new promise replaces a
|
|
558
|
+
* resolved one when the animation leaves `finished` again, and a cancel
|
|
559
|
+
* leaves a new pending one.
|
|
560
|
+
*/
|
|
561
|
+
get finished() {
|
|
562
|
+
const state = stateOf(this);
|
|
563
|
+
if (state.mode === "live" && state.finished.settled === "resolved") {
|
|
564
|
+
const playState = readLive(state).playState;
|
|
565
|
+
if (playState !== "finished") {
|
|
566
|
+
state.finished = deferred();
|
|
567
|
+
state.renewedForCancel = playState === "idle";
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
return state.finished.promise;
|
|
571
|
+
}
|
|
572
|
+
/** Runs when the animation finishes, in the order it was first set among the listeners. */
|
|
573
|
+
get onfinish() {
|
|
574
|
+
return this.#handler("finish");
|
|
575
|
+
}
|
|
576
|
+
set onfinish(handler) {
|
|
577
|
+
this.#setHandler("finish", handler);
|
|
578
|
+
}
|
|
579
|
+
/** Runs when the animation is cancelled, in the order it was first set among the listeners. */
|
|
580
|
+
get oncancel() {
|
|
581
|
+
return this.#handler("cancel");
|
|
582
|
+
}
|
|
583
|
+
set oncancel(handler) {
|
|
584
|
+
this.#setHandler("cancel", handler);
|
|
585
|
+
}
|
|
586
|
+
#handler(type) {
|
|
587
|
+
return stateOf(this).handlers.get(type) ?? null;
|
|
588
|
+
}
|
|
589
|
+
#setHandler(type, handler) {
|
|
590
|
+
const state = stateOf(this);
|
|
591
|
+
const registered = state.handlers.has(type);
|
|
592
|
+
state.handlers.set(type, typeof handler === "function" ? handler : null);
|
|
593
|
+
// The handler is a listener from the moment it is first set, as in the DOM, so the
|
|
594
|
+
// ones added after it come after it.
|
|
595
|
+
if (!registered) {
|
|
596
|
+
this.addEventListener(type, (event) => state.handlers.get(type)?.call(this, event));
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
/** Holds the current time at once. From here `animation-play-state` no longer decides. */
|
|
600
|
+
pause() {
|
|
601
|
+
command(this, "pause", (state) => state.host.pauseAnimation(state.id));
|
|
602
|
+
}
|
|
603
|
+
/**
|
|
604
|
+
* Continues from the held time at the next frame. A finished animation
|
|
605
|
+
* starts over: from 0, or from the end when the rate is negative; so does a
|
|
606
|
+
* cancelled one. From here `animation-play-state` no longer decides.
|
|
607
|
+
*/
|
|
608
|
+
play() {
|
|
609
|
+
command(this, "play", (state) => state.host.playAnimation(state.id));
|
|
610
|
+
}
|
|
611
|
+
/**
|
|
612
|
+
* Stops the animation: idle, with no current time, out of `getAnimations()`,
|
|
613
|
+
* its element as if it had none. A style change does not start it again;
|
|
614
|
+
* `play()` does, from the start. Its `finished` promise rejects with an
|
|
615
|
+
* `AbortError` and `ready` too if it was waiting; the `cancel` event comes
|
|
616
|
+
* with the next frame.
|
|
617
|
+
*/
|
|
618
|
+
cancel() {
|
|
619
|
+
const state = stateOf(this);
|
|
620
|
+
if (state.mode === "gone")
|
|
621
|
+
return;
|
|
622
|
+
const before = readLive(state);
|
|
623
|
+
if (before.playState === "idle")
|
|
624
|
+
return;
|
|
625
|
+
try {
|
|
626
|
+
state.host.cancelAnimation(state.id);
|
|
627
|
+
}
|
|
628
|
+
catch (error) {
|
|
629
|
+
// Cancelled since the frame JS last heard of: its cancel event is on its way.
|
|
630
|
+
if (isUnknownAnimation(error))
|
|
631
|
+
return;
|
|
632
|
+
throw error;
|
|
633
|
+
}
|
|
634
|
+
state.cancelEvents++;
|
|
635
|
+
abort(state, before.pending);
|
|
636
|
+
readLive(state);
|
|
637
|
+
}
|
|
638
|
+
/**
|
|
639
|
+
* Goes to the end the rate runs toward at once, the end of the effect or 0.
|
|
640
|
+
* Throws an `InvalidStateError` at a rate of 0 and for an animation that
|
|
641
|
+
* never ends running forward.
|
|
642
|
+
*/
|
|
643
|
+
finish() {
|
|
644
|
+
command(this, "finish", (state) => state.host.finishAnimation(state.id));
|
|
645
|
+
}
|
|
646
|
+
/**
|
|
647
|
+
* Plays the animation the other way: the rate flips when the next frame
|
|
648
|
+
* comes, and an animation at the end it was running from goes to the other.
|
|
649
|
+
* Throws an `InvalidStateError` for an animation that never ends, from its start.
|
|
650
|
+
*/
|
|
651
|
+
reverse() {
|
|
652
|
+
command(this, "reverse", (state) => state.host.reverseAnimation(state.id));
|
|
653
|
+
}
|
|
654
|
+
/**
|
|
655
|
+
* Sets the rate the way `playbackRate` does, but keeps the time continuous across the change:
|
|
656
|
+
* a running animation takes the new rate at the next frame, a paused, finished or cancelled
|
|
657
|
+
* one at once.
|
|
658
|
+
*/
|
|
659
|
+
updatePlaybackRate(rate) {
|
|
660
|
+
if (typeof rate !== "number" || !Number.isFinite(rate)) {
|
|
661
|
+
throw new TypeError(`Failed to execute updatePlaybackRate: ${String(rate)} is not a finite number`);
|
|
662
|
+
}
|
|
663
|
+
command(this, "update the playbackRate of", (state) => state.host.updateAnimationPlaybackRate(state.id, rate));
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
/** A CSS animation: the run of an `animation-name`. */
|
|
667
|
+
export class CSSAnimation extends Animation {
|
|
668
|
+
animationName;
|
|
669
|
+
constructor(init) {
|
|
670
|
+
super(init);
|
|
671
|
+
if (init.info.animationName === undefined) {
|
|
672
|
+
throw new TypeError(`Animation ${init.info.id} is not a CSS animation`);
|
|
673
|
+
}
|
|
674
|
+
this.animationName = init.info.animationName;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
/** A CSS transition: the run of one property. */
|
|
678
|
+
export class CSSTransition extends Animation {
|
|
679
|
+
/** The CSS name of the property, `background-color`. */
|
|
680
|
+
transitionProperty;
|
|
681
|
+
constructor(init) {
|
|
682
|
+
super(init);
|
|
683
|
+
if (init.info.transitionProperty === undefined) {
|
|
684
|
+
throw new TypeError(`Animation ${init.info.id} is not a CSS transition`);
|
|
685
|
+
}
|
|
686
|
+
this.transitionProperty = init.info.transitionProperty;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
/** The animation of a run native has, held or not. */
|
|
690
|
+
function lookup(registry, id) {
|
|
691
|
+
return registry.byId.get(id) ?? registry.weak.get(id)?.deref();
|
|
692
|
+
}
|
|
693
|
+
// Shared through `globalThis` like the renderer states: the JSX runtime can load
|
|
694
|
+
// a second copy of this module, and a copy that lists animations must be heard
|
|
695
|
+
// by the copy that routes the events.
|
|
696
|
+
const REGISTRIES_KEY = Symbol.for("@ghia/native/web-animations");
|
|
697
|
+
function registryOf(renderer) {
|
|
698
|
+
let all = Reflect.get(globalThis, REGISTRIES_KEY);
|
|
699
|
+
if (!all) {
|
|
700
|
+
all = new WeakMap();
|
|
701
|
+
Reflect.set(globalThis, REGISTRIES_KEY, all);
|
|
702
|
+
}
|
|
703
|
+
let registry = all.get(renderer);
|
|
704
|
+
if (!registry) {
|
|
705
|
+
const weak = new Map();
|
|
706
|
+
const made = {
|
|
707
|
+
byId: new Map(),
|
|
708
|
+
byElement: new Map(),
|
|
709
|
+
weak,
|
|
710
|
+
closed: false,
|
|
711
|
+
cancelTasks: new Set(),
|
|
712
|
+
finalizer: new FinalizationRegistry((id) => {
|
|
713
|
+
// A new object for the same run, made since, holds it.
|
|
714
|
+
if (weak.get(id)?.deref())
|
|
715
|
+
return;
|
|
716
|
+
weak.delete(id);
|
|
717
|
+
if (!made.closed)
|
|
718
|
+
renderer.forgetAnimation?.(id);
|
|
719
|
+
}),
|
|
720
|
+
};
|
|
721
|
+
registry = made;
|
|
722
|
+
all.set(renderer, registry);
|
|
723
|
+
}
|
|
724
|
+
return registry;
|
|
725
|
+
}
|
|
726
|
+
/** The window of `renderer` is gone, which the animations it handed out outlive: native has nothing left for them. */
|
|
727
|
+
export function closeWebAnimations(renderer) {
|
|
728
|
+
const all = Reflect.get(globalThis, REGISTRIES_KEY);
|
|
729
|
+
const registry = all?.get(renderer);
|
|
730
|
+
if (!registry)
|
|
731
|
+
return;
|
|
732
|
+
registry.closed = true;
|
|
733
|
+
for (const task of registry.cancelTasks)
|
|
734
|
+
clearTimeout(task);
|
|
735
|
+
registry.cancelTasks.clear();
|
|
736
|
+
}
|
|
737
|
+
function hostOf(renderer) {
|
|
738
|
+
for (const method of HOST_METHODS) {
|
|
739
|
+
if (!renderer[method])
|
|
740
|
+
throw new Error(`This renderer does not support ${method}`);
|
|
741
|
+
}
|
|
742
|
+
return renderer;
|
|
743
|
+
}
|
|
744
|
+
/** The error `getAnimation` throws for a run native no longer has. */
|
|
745
|
+
function isUnknownAnimation(error) {
|
|
746
|
+
return error instanceof Error && error.message.includes("No running CSS animation or transition has the id");
|
|
747
|
+
}
|
|
748
|
+
function unregister(registry, state, animation) {
|
|
749
|
+
registry.byId.delete(state.id);
|
|
750
|
+
if (registry.weak.delete(state.id))
|
|
751
|
+
registry.finalizer.unregister(animation);
|
|
752
|
+
const siblings = registry.byElement.get(state.elementId);
|
|
753
|
+
siblings?.delete(animation);
|
|
754
|
+
if (siblings?.size === 0)
|
|
755
|
+
registry.byElement.delete(state.elementId);
|
|
756
|
+
}
|
|
757
|
+
function register(registry, elementId, id, animation) {
|
|
758
|
+
if (registry.weak.delete(id))
|
|
759
|
+
registry.finalizer.unregister(animation);
|
|
760
|
+
registry.byId.set(id, animation);
|
|
761
|
+
const siblings = registry.byElement.get(elementId) ?? new Set();
|
|
762
|
+
siblings.add(animation);
|
|
763
|
+
registry.byElement.set(elementId, siblings);
|
|
764
|
+
}
|
|
765
|
+
/** The class of the animation of a run: the DOM makes an `Animation` for a script and a subclass for CSS. */
|
|
766
|
+
function animationOf(kind, init) {
|
|
767
|
+
switch (kind) {
|
|
768
|
+
case "transition":
|
|
769
|
+
return new CSSTransition(init);
|
|
770
|
+
case "script":
|
|
771
|
+
return new Animation(init);
|
|
772
|
+
default:
|
|
773
|
+
return new CSSAnimation(init);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
function adopt(registry, host, timeline, target, elementId, info) {
|
|
777
|
+
const known = lookup(registry, info.id);
|
|
778
|
+
if (known) {
|
|
779
|
+
register(registry, elementId, info.id, known);
|
|
780
|
+
return known;
|
|
781
|
+
}
|
|
782
|
+
const init = { host, registry, timeline, target, elementId, info };
|
|
783
|
+
const animation = animationOf(info.kind, init);
|
|
784
|
+
register(registry, elementId, info.id, animation);
|
|
785
|
+
return animation;
|
|
786
|
+
}
|
|
787
|
+
/**
|
|
788
|
+
* `element.getAnimations()`: the CSS transitions, then the CSS animations,
|
|
789
|
+
* that are running, paused or in effect through a fill, in composite order.
|
|
790
|
+
* With `subtree`, those of the element and everything below it: every
|
|
791
|
+
* transition of the elements in document order, then every animation.
|
|
792
|
+
* Pending mutations are sent first, so a style just set is seen.
|
|
793
|
+
*/
|
|
794
|
+
export function getElementAnimations(renderer, elementId, target, options) {
|
|
795
|
+
const host = hostOf(renderer);
|
|
796
|
+
flushPendingMutations(renderer);
|
|
797
|
+
const registry = registryOf(renderer);
|
|
798
|
+
const timeline = (registry.timeline ??= new DocumentTimeline(host));
|
|
799
|
+
if (!options?.subtree) {
|
|
800
|
+
return host.getAnimations(elementId).map((info) => adopt(registry, host, timeline, target, elementId, info));
|
|
801
|
+
}
|
|
802
|
+
return host
|
|
803
|
+
.getSubtreeAnimations(elementId)
|
|
804
|
+
.map((listed) => adoptListed(registry, host, timeline, renderer, listed, elementId, target));
|
|
805
|
+
}
|
|
806
|
+
/** `document.getAnimations()` of one renderer: the animations of its window, in document order. */
|
|
807
|
+
function getRendererAnimations(renderer) {
|
|
808
|
+
const host = hostOf(renderer);
|
|
809
|
+
flushPendingMutations(renderer);
|
|
810
|
+
const registry = registryOf(renderer);
|
|
811
|
+
const timeline = (registry.timeline ??= new DocumentTimeline(host));
|
|
812
|
+
return host
|
|
813
|
+
.getDocumentAnimations()
|
|
814
|
+
.map((listed) => adoptListed(registry, host, timeline, renderer, listed, undefined, undefined));
|
|
815
|
+
}
|
|
816
|
+
function adoptListed(registry, host, timeline, renderer, listed, readOn, readOnTarget) {
|
|
817
|
+
const known = lookup(registry, listed.info.id);
|
|
818
|
+
if (known) {
|
|
819
|
+
register(registry, listed.elementId, listed.info.id, known);
|
|
820
|
+
return known;
|
|
821
|
+
}
|
|
822
|
+
const target = listed.elementId === readOn ? readOnTarget : elementOf(renderer, listed.elementId);
|
|
823
|
+
if (target === undefined) {
|
|
824
|
+
throw new Error(`Element ${listed.elementId} has an animation but its root has no element for it`);
|
|
825
|
+
}
|
|
826
|
+
return adopt(registry, host, timeline, target, listed.elementId, listed.info);
|
|
827
|
+
}
|
|
828
|
+
setDocumentAnimations((renderer) => getRendererAnimations(renderer));
|
|
829
|
+
/** The cancel of an animation: its promises, as `cancel()` and a style that stops naming it leave them. */
|
|
830
|
+
function abort(state, wasPending) {
|
|
831
|
+
const error = () => new DOMException("The animation was cancelled", "AbortError");
|
|
832
|
+
if (wasPending && state.ready.settled === "pending") {
|
|
833
|
+
state.ready.reject(error());
|
|
834
|
+
state.ready = deferred();
|
|
835
|
+
}
|
|
836
|
+
if (state.finished.settled === "pending")
|
|
837
|
+
state.finished.reject(error());
|
|
838
|
+
state.finished = deferred();
|
|
839
|
+
}
|
|
840
|
+
/** Runs the play of an animation that was made finished, which has no frame to wait for. */
|
|
841
|
+
function startFinished(animation) {
|
|
842
|
+
const state = stateOf(animation);
|
|
843
|
+
if (state.mode !== "live" || state.registry.closed)
|
|
844
|
+
return;
|
|
845
|
+
try {
|
|
846
|
+
if (!readLive(state).pending)
|
|
847
|
+
return;
|
|
848
|
+
command(animation, "start", (live) => live.host.setAnimationCurrentTime(live.id, readLive(live).currentTime));
|
|
849
|
+
}
|
|
850
|
+
catch (error) {
|
|
851
|
+
// Cancelled since: there is nothing left to start.
|
|
852
|
+
if (!(error instanceof DOMException && error.name === "InvalidStateError"))
|
|
853
|
+
throw error;
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
/** The animation went for good: nothing native has stays to be played again. */
|
|
857
|
+
function lose(animation) {
|
|
858
|
+
const state = stateOf(animation);
|
|
859
|
+
state.mode = "gone";
|
|
860
|
+
cancelPromises(state);
|
|
861
|
+
}
|
|
862
|
+
function cancelPromises(state) {
|
|
863
|
+
const pending = state.cancelNoticed?.pending ?? state.last.pending;
|
|
864
|
+
state.cancelNoticed = undefined;
|
|
865
|
+
if (state.renewedForCancel) {
|
|
866
|
+
state.renewedForCancel = false;
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
abort(state, pending);
|
|
870
|
+
}
|
|
871
|
+
/**
|
|
872
|
+
* The style or Motion cancelled the animation and native keeps the run: it is idle, a command brings it back, and the
|
|
873
|
+
* registry holds it weakly until one does.
|
|
874
|
+
*/
|
|
875
|
+
function detach(animation) {
|
|
876
|
+
const state = stateOf(animation);
|
|
877
|
+
cancelPromises(state);
|
|
878
|
+
const now = readLive(state);
|
|
879
|
+
// A command can have played it again since the cancel, which the event only now tells of.
|
|
880
|
+
if (now.playState === "finished")
|
|
881
|
+
state.holdFinished = true;
|
|
882
|
+
if (now.playState !== "idle")
|
|
883
|
+
return;
|
|
884
|
+
const registry = state.registry;
|
|
885
|
+
registry.byId.delete(state.id);
|
|
886
|
+
const siblings = registry.byElement.get(state.elementId);
|
|
887
|
+
siblings?.delete(animation);
|
|
888
|
+
if (siblings?.size === 0)
|
|
889
|
+
registry.byElement.delete(state.elementId);
|
|
890
|
+
registry.weak.set(state.id, new WeakRef(animation));
|
|
891
|
+
registry.finalizer.register(animation, state.id, animation);
|
|
892
|
+
}
|
|
893
|
+
function dispatchPlaybackEvent(animation, type, currentTime, timelineTime) {
|
|
894
|
+
let stopped = false;
|
|
895
|
+
const event = {
|
|
896
|
+
type,
|
|
897
|
+
target: animation,
|
|
898
|
+
currentTarget: null,
|
|
899
|
+
currentTime,
|
|
900
|
+
timelineTime,
|
|
901
|
+
eventPhase: EVENT_PHASE_NONE,
|
|
902
|
+
bubbles: false,
|
|
903
|
+
cancelable: false,
|
|
904
|
+
defaultPrevented: false,
|
|
905
|
+
timeStamp: performance.now(),
|
|
906
|
+
composedPath: () => [animation],
|
|
907
|
+
stopPropagation() { },
|
|
908
|
+
stopImmediatePropagation() {
|
|
909
|
+
stopped = true;
|
|
910
|
+
},
|
|
911
|
+
preventDefault() { },
|
|
912
|
+
};
|
|
913
|
+
const errors = [];
|
|
914
|
+
animation.run(event, {
|
|
915
|
+
propagationStopped: false,
|
|
916
|
+
get immediatePropagationStopped() {
|
|
917
|
+
return stopped;
|
|
918
|
+
},
|
|
919
|
+
}, false, EVENT_PHASE_AT_TARGET, (error) => errors.push(error));
|
|
920
|
+
event.currentTarget = null;
|
|
921
|
+
for (const error of errors)
|
|
922
|
+
reportUncaught(error);
|
|
923
|
+
}
|
|
924
|
+
/** Whether the event is one of the internal ones `deliverWebAnimationEvent` takes. */
|
|
925
|
+
export function isWebAnimationEvent(eventType) {
|
|
926
|
+
return eventType === FINISH_EVENT || eventType === CANCEL_EVENT || eventType === READY_EVENT;
|
|
927
|
+
}
|
|
928
|
+
/**
|
|
929
|
+
* Takes the ready, finish or cancel of a CSS animation or transition for the
|
|
930
|
+
* `Animation` object listed for it, then tells native a finish or cancel is
|
|
931
|
+
* consumed, which stops answering for the run as it ended. Nothing else hears
|
|
932
|
+
* these events: they are not DOM events. A run no one listed is released all
|
|
933
|
+
* the same.
|
|
934
|
+
*/
|
|
935
|
+
export function deliverWebAnimationEvent(renderer, event) {
|
|
936
|
+
if (event.animationId === undefined) {
|
|
937
|
+
throw new Error(`Event "${event.eventType}" arrived without its animationId`);
|
|
938
|
+
}
|
|
939
|
+
let released = false;
|
|
940
|
+
try {
|
|
941
|
+
const registry = registryOf(renderer);
|
|
942
|
+
const animation = lookup(registry, event.animationId);
|
|
943
|
+
if (!animation) {
|
|
944
|
+
// No object is left to bring a cancelled run back.
|
|
945
|
+
if (event.eventType === CANCEL_EVENT)
|
|
946
|
+
renderer.forgetAnimation?.(event.animationId);
|
|
947
|
+
return;
|
|
948
|
+
}
|
|
949
|
+
const state = stateOf(animation);
|
|
950
|
+
const timelineTime = event.timelineTime ?? null;
|
|
951
|
+
switch (event.eventType) {
|
|
952
|
+
case READY_EVENT:
|
|
953
|
+
if (state.ready.settled === "pending")
|
|
954
|
+
state.ready.resolve(animation);
|
|
955
|
+
return;
|
|
956
|
+
case FINISH_EVENT:
|
|
957
|
+
// A command that finished the animation resolved it then. This is the finish by itself.
|
|
958
|
+
if (state.holdFinished) {
|
|
959
|
+
state.holdFinished = false;
|
|
960
|
+
}
|
|
961
|
+
else if (state.finished.settled === "pending" && readLive(state).playState === "finished") {
|
|
962
|
+
state.finished.resolve(animation);
|
|
963
|
+
}
|
|
964
|
+
dispatchPlaybackEvent(animation, "finish", event.currentTime ?? null, timelineTime);
|
|
965
|
+
return;
|
|
966
|
+
default:
|
|
967
|
+
if (state.cancelEvents > 0) {
|
|
968
|
+
state.cancelEvents--;
|
|
969
|
+
}
|
|
970
|
+
else {
|
|
971
|
+
renderer.releaseAnimation?.(event.animationId);
|
|
972
|
+
released = true;
|
|
973
|
+
if (keptByNative(renderer, state))
|
|
974
|
+
detach(animation);
|
|
975
|
+
else
|
|
976
|
+
lose(animation);
|
|
977
|
+
}
|
|
978
|
+
if (state.mode === "gone")
|
|
979
|
+
unregister(registry, state, animation);
|
|
980
|
+
dispatchPlaybackEvent(animation, "cancel", null, timelineTime);
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
finally {
|
|
984
|
+
if (event.eventType !== READY_EVENT && !released) {
|
|
985
|
+
renderer.releaseAnimation?.(event.animationId);
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
/**
|
|
990
|
+
* Whether native still has the run of a cancel it just told of, and not one that went with its element or was replaced.
|
|
991
|
+
* Until the event is released native answers for the run as it ended, so release it first.
|
|
992
|
+
*/
|
|
993
|
+
function keptByNative(renderer, state) {
|
|
994
|
+
try {
|
|
995
|
+
renderer.getAnimation?.(state.id);
|
|
996
|
+
return true;
|
|
997
|
+
}
|
|
998
|
+
catch (error) {
|
|
999
|
+
if (!isUnknownAnimation(error))
|
|
1000
|
+
throw error;
|
|
1001
|
+
return false;
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
/**
|
|
1005
|
+
* A removed element's animations are cancelled, as the DOM does: native sends
|
|
1006
|
+
* nothing when an element goes, so the ids the mutation queue was told are
|
|
1007
|
+
* destroyed end here. The `cancel` event comes in a task, as the DOM's comes at
|
|
1008
|
+
* its next frame.
|
|
1009
|
+
*/
|
|
1010
|
+
export function cancelAnimationsOfElements(renderer, elementIds) {
|
|
1011
|
+
const registry = registryOf(renderer);
|
|
1012
|
+
for (const elementId of elementIds) {
|
|
1013
|
+
// Chrome cancels the last of an element's animations first.
|
|
1014
|
+
for (const animation of [...(registry.byElement.get(elementId) ?? [])].reverse()) {
|
|
1015
|
+
const state = stateOf(animation);
|
|
1016
|
+
const wasIdle = state.mode === "gone" || state.last.playState === "idle";
|
|
1017
|
+
lose(animation);
|
|
1018
|
+
unregister(registry, state, animation);
|
|
1019
|
+
if (wasIdle)
|
|
1020
|
+
continue;
|
|
1021
|
+
const timelineTime = state.timeline.currentTime;
|
|
1022
|
+
if (registry.closed)
|
|
1023
|
+
continue;
|
|
1024
|
+
const task = setTimeout(() => {
|
|
1025
|
+
registry.cancelTasks.delete(task);
|
|
1026
|
+
dispatchPlaybackEvent(animation, "cancel", null, timelineTime);
|
|
1027
|
+
}, 0);
|
|
1028
|
+
registry.cancelTasks.add(task);
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
//# sourceMappingURL=web-animations.js.map
|