@playcanvas/web-components 0.15.0 → 0.16.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/dist/components/anim-clip.d.cts +127 -0
- package/dist/components/anim-clip.d.ts +127 -0
- package/dist/components/anim-component.d.cts +207 -0
- package/dist/components/anim-component.d.ts +207 -0
- package/dist/custom-elements.json +532 -0
- package/dist/index.d.cts +5 -1
- package/dist/index.d.ts +5 -1
- package/dist/pwc.cjs +1527 -638
- package/dist/pwc.cjs.map +1 -1
- package/dist/pwc.js +1527 -638
- package/dist/pwc.js.map +1 -1
- package/dist/pwc.min.js +1 -1
- package/dist/pwc.min.js.map +1 -1
- package/dist/pwc.min.mjs +1 -1
- package/dist/pwc.min.mjs.map +1 -1
- package/dist/pwc.mjs +1527 -640
- package/dist/pwc.mjs.map +1 -1
- package/dist/vscode.html-custom-data.json +59 -0
- package/dist/web-types.json +156 -1
- package/package.json +1 -1
- package/src/components/anim-clip.ts +395 -0
- package/src/components/anim-component.ts +649 -0
- package/src/index.ts +6 -0
- package/src/model.ts +0 -7
|
@@ -0,0 +1,649 @@
|
|
|
1
|
+
import type { AnimComponent, Asset, ContainerResource } from 'playcanvas';
|
|
2
|
+
import { ANIM_CONTROL_STATES, AnimTrack } from 'playcanvas';
|
|
3
|
+
|
|
4
|
+
import { AssetElement } from '../asset';
|
|
5
|
+
import type { EntityBaseElement } from '../entity-base';
|
|
6
|
+
import { ModelElement } from '../model';
|
|
7
|
+
import { parseBool, parseNumber } from '../parse';
|
|
8
|
+
|
|
9
|
+
import type { AnimClipElement } from './anim-clip';
|
|
10
|
+
import { ComponentElement } from './component';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A container resource with the `animations` sub-assets the engine documents but does not type:
|
|
14
|
+
* one `Asset` of type `animation` per glTF animation, each holding an `AnimTrack` resource.
|
|
15
|
+
*/
|
|
16
|
+
type ContainerWithAnimations = ContainerResource & { animations: Asset[] };
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* A playback snapshot captured before a clip-set rebuild and restored afterwards, so a rebuild
|
|
20
|
+
* whose active clip survives it is seamless. Both playing flags are captured: the component's
|
|
21
|
+
* gates the system tick, the layer controller's gates the layer, and they legitimately diverge —
|
|
22
|
+
* {@link AnimComponentElement.pause} clears only the component's.
|
|
23
|
+
*/
|
|
24
|
+
type PlaybackState = {
|
|
25
|
+
state: string;
|
|
26
|
+
time: number;
|
|
27
|
+
playing: boolean;
|
|
28
|
+
layerPlaying: boolean;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The AnimComponentElement interface provides properties and methods for manipulating
|
|
33
|
+
* {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-anim/ | `<pc-anim>`} elements.
|
|
34
|
+
* The AnimComponentElement interface also inherits the properties and methods of the
|
|
35
|
+
* {@link HTMLElement} interface.
|
|
36
|
+
*
|
|
37
|
+
* The element drives animation clips over the host entity's hierarchy. Clips come from
|
|
38
|
+
* `<pc-anim-clip>` children — or, when the element is a direct child of a `<pc-model>` and
|
|
39
|
+
* declares no clips, every animation of that model's container asset is assigned, named by track
|
|
40
|
+
* name, in container order. The first clip plays automatically (opt out with `activate="false"`);
|
|
41
|
+
* switch clips declaratively through the `clip` attribute, or imperatively through {@link play}
|
|
42
|
+
* and {@link transition}. Tracks bind to scene nodes by name, so any hierarchy whose node names
|
|
43
|
+
* match a clip's curves can be animated — a model's skeleton is simply the common case.
|
|
44
|
+
*
|
|
45
|
+
* The engine reports no clip completion: a non-looping clip holds its last pose silently. Poll
|
|
46
|
+
* the underlying {@link AnimComponent} (via {@link component}) for playback state beyond what
|
|
47
|
+
* this element exposes.
|
|
48
|
+
*
|
|
49
|
+
* @category Components
|
|
50
|
+
*/
|
|
51
|
+
class AnimComponentElement extends ComponentElement {
|
|
52
|
+
/**
|
|
53
|
+
* Whether playback starts automatically once a clip is assigned.
|
|
54
|
+
*/
|
|
55
|
+
private _activate = true;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The clip elements whose states are currently assigned, by clip name. The single writer of
|
|
59
|
+
* a state: a later clip child re-using an adopted name is rejected as a duplicate.
|
|
60
|
+
*/
|
|
61
|
+
private _assignedClips = new Map<string, AnimClipElement>();
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Whether the current clip set was auto-assigned from the enclosing model rather than
|
|
65
|
+
* declared by clip children.
|
|
66
|
+
*/
|
|
67
|
+
private _autoAssigned = false;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The name of the active clip.
|
|
71
|
+
*/
|
|
72
|
+
private _clip = '';
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* The element the model-readiness listener is attached to, held so disconnection can detach
|
|
76
|
+
* it after `closestEntity` no longer resolves.
|
|
77
|
+
*/
|
|
78
|
+
private _modelListenerTarget: EntityBaseElement | null = null;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Incremented whenever the clip source changes, and captured by an auto-assign pass on
|
|
82
|
+
* entry — a pass resuming from an await abandons itself if the value has moved on, so a
|
|
83
|
+
* superseded pass cannot assign clips alongside declared children or a newer pass.
|
|
84
|
+
*/
|
|
85
|
+
private _sourceGeneration = 0;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The playback speed multiplier applied across all clips.
|
|
89
|
+
*/
|
|
90
|
+
private _speed = 1;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* The cross-fade duration of declarative clip switches, in seconds.
|
|
94
|
+
*/
|
|
95
|
+
private _transitionTime = 0;
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* The unknown clip name already warned about, so a repeated selection of the same missing
|
|
99
|
+
* name complains once.
|
|
100
|
+
*/
|
|
101
|
+
private _warnedClip: string | null = null;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Rebinds when a model under the host announces readiness. The engine resolves each curve
|
|
105
|
+
* once, at the first tick after assignment, and never retries — and its mesh-instance
|
|
106
|
+
* broadcast fires before an instantiated hierarchy is parented, so a model that loads after
|
|
107
|
+
* the clips were assigned would otherwise stay silently unbound. A re-instantiation of the
|
|
108
|
+
* implicit clip source (the parent `<pc-model>`) means a new container, so the clip set
|
|
109
|
+
* refreshes instead — unless every clip declares its own asset, where a rebind suffices.
|
|
110
|
+
*/
|
|
111
|
+
private _onModelReady = (event: Event) => {
|
|
112
|
+
if (!(event.target instanceof ModelElement) || !this.component) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (event.target === this.parentElement) {
|
|
116
|
+
const implicit = this._autoAssigned ||
|
|
117
|
+
[...this._assignedClips.values()].some(clip => !clip.asset);
|
|
118
|
+
if (implicit) {
|
|
119
|
+
this._refreshClips();
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
this.component.rebind();
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
/** @ignore */
|
|
127
|
+
constructor() {
|
|
128
|
+
super('anim');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
protected getInitialComponentData() {
|
|
132
|
+
// The engine assigns creation data in key order and `activate` gates playback, so it
|
|
133
|
+
// must precede any future key that builds layers (e.g. a state graph)
|
|
134
|
+
return {
|
|
135
|
+
activate: this._activate,
|
|
136
|
+
speed: this._speed
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
protected initComponent() {
|
|
141
|
+
if (!this.component) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// A host readiness cycle can re-run this. An identical re-add is deduped by the DOM;
|
|
146
|
+
// the explicit swap handles the listener target changing across connections.
|
|
147
|
+
const host = this.closestEntity;
|
|
148
|
+
if (host && host !== this._modelListenerTarget) {
|
|
149
|
+
this._modelListenerTarget?.removeEventListener('ready', this._onModelReady);
|
|
150
|
+
host.addEventListener('ready', this._onModelReady);
|
|
151
|
+
this._modelListenerTarget = host;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
this._applyClips();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
disconnectedCallback() {
|
|
158
|
+
this._modelListenerTarget?.removeEventListener('ready', this._onModelReady);
|
|
159
|
+
this._modelListenerTarget = null;
|
|
160
|
+
|
|
161
|
+
// Invalidate any auto-assign still awaiting its model, and drop the adoption
|
|
162
|
+
// bookkeeping so a reconnection starts clean
|
|
163
|
+
this._sourceGeneration++;
|
|
164
|
+
this._assignedClips.clear();
|
|
165
|
+
this._autoAssigned = false;
|
|
166
|
+
|
|
167
|
+
super.disconnectedCallback();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* The clip children in DOM order. Read afresh each pass — the DOM is the single source of
|
|
172
|
+
* truth for the declared clip set.
|
|
173
|
+
*/
|
|
174
|
+
private _clipElements(): AnimClipElement[] {
|
|
175
|
+
return Array.from(this.querySelectorAll<AnimClipElement>(':scope > pc-anim-clip'));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Assigns a clip's state. Until the clip's real track resolves, the engine's own placeholder
|
|
180
|
+
* track stands in — it keeps the layer playable, so `activate` can start playback and the
|
|
181
|
+
* declared `clip` selection can apply before any asset has loaded.
|
|
182
|
+
*/
|
|
183
|
+
private _assignClip(clip: AnimClipElement) {
|
|
184
|
+
this.component.assignAnimation(clip.name, clip._track ?? AnimTrack.EMPTY, undefined, clip.speed, clip.loop);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Validates a clip child and, when valid, assigns its state and starts its track resolution.
|
|
189
|
+
*
|
|
190
|
+
* @param clip - The clip element.
|
|
191
|
+
* @returns Whether the clip was adopted.
|
|
192
|
+
*/
|
|
193
|
+
private _adoptClip(clip: AnimClipElement): boolean {
|
|
194
|
+
const name = clip.name;
|
|
195
|
+
if (!name) {
|
|
196
|
+
clip._markInvalid('pc-anim-clip must have a name - clip not assigned');
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
if (name.indexOf('.') !== -1) {
|
|
200
|
+
clip._markInvalid(`pc-anim-clip '${name}' - '.' in a clip name is reserved for blend tree paths - clip not assigned`);
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
if (this._assignedClips.has(name)) {
|
|
204
|
+
clip._markInvalid(`pc-anim-clip '${name}' - an earlier clip already uses this name - clip not assigned`);
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
this._assignedClips.set(name, clip);
|
|
208
|
+
this._assignClip(clip);
|
|
209
|
+
clip._resolveTrack(this);
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Assigns the current clip set: the declared clip children when there are any, otherwise the
|
|
215
|
+
* enclosing model's clips. Runs against a fresh component after a host cycle, so the
|
|
216
|
+
* adoption bookkeeping rebuilds from scratch.
|
|
217
|
+
*/
|
|
218
|
+
private _applyClips(restore?: PlaybackState) {
|
|
219
|
+
if (!this.component) {
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
this._sourceGeneration++;
|
|
224
|
+
this._assignedClips.clear();
|
|
225
|
+
this._autoAssigned = false;
|
|
226
|
+
|
|
227
|
+
const clips = this._clipElements();
|
|
228
|
+
if (clips.length === 0) {
|
|
229
|
+
this._kickAutoAssign(restore);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
for (const clip of clips) {
|
|
234
|
+
this._adoptClip(clip);
|
|
235
|
+
}
|
|
236
|
+
this._applySelection(restore);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Assigns every clip of the enclosing model's container, named by track name, in container
|
|
241
|
+
* order. Names the engine cannot host — dotted (reserved for blend tree paths) or already
|
|
242
|
+
* taken — are skipped with a warning naming each.
|
|
243
|
+
*/
|
|
244
|
+
private async _kickAutoAssign(restore?: PlaybackState) {
|
|
245
|
+
const generation = this._sourceGeneration;
|
|
246
|
+
|
|
247
|
+
const model = this.parentElement;
|
|
248
|
+
if (!(model instanceof ModelElement)) {
|
|
249
|
+
// Not inside a model: an empty component, driven through the JS API
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
await model.ready();
|
|
254
|
+
|
|
255
|
+
// The source may have changed while the model loaded - a declared clip child appearing
|
|
256
|
+
// flips the element over to declared mode, and wins
|
|
257
|
+
const component = this.component;
|
|
258
|
+
if (generation !== this._sourceGeneration || !component || this._clipElements().length > 0) {
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const container = AssetElement.get(model.asset)?.resource as ContainerWithAnimations | undefined;
|
|
263
|
+
if (!container) {
|
|
264
|
+
// The load failed; the model already reported it
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const label = this.id ? ` '${this.id}'` : '';
|
|
269
|
+
if (container.animations.length === 0) {
|
|
270
|
+
console.warn(`pc-anim${label} - model '${model.asset}' has no animations`);
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const seen = new Set<string>();
|
|
275
|
+
for (const animationAsset of container.animations) {
|
|
276
|
+
const track = animationAsset.resource;
|
|
277
|
+
if (!(track instanceof AnimTrack)) {
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
if (track.name.indexOf('.') !== -1) {
|
|
281
|
+
console.warn(`pc-anim${label} - track '${track.name}' - '.' in a clip name is reserved for blend tree paths - track skipped`);
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
if (seen.has(track.name)) {
|
|
285
|
+
console.warn(`pc-anim${label} - duplicate track name '${track.name}' - track skipped`);
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
seen.add(track.name);
|
|
289
|
+
component.assignAnimation(track.name, track);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
this._autoAssigned = seen.size > 0;
|
|
293
|
+
this._applySelection(restore);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Applies the active-clip selection: the declared `clip` when it names an assigned state,
|
|
298
|
+
* else a captured pre-rebuild state when it survived, else the engine's default (the first
|
|
299
|
+
* assigned clip). A restore also reinstates the playhead and both playing flags exactly as
|
|
300
|
+
* captured — the reassignment that preceded it set both to the `activate` outcome, which is
|
|
301
|
+
* not necessarily the state the rebuild interrupted.
|
|
302
|
+
*/
|
|
303
|
+
private _applySelection(restore?: PlaybackState) {
|
|
304
|
+
const component = this.component;
|
|
305
|
+
const layer = component ? component.baseLayer : null;
|
|
306
|
+
if (!component || !layer) {
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (this._clip && !layer.states.includes(this._clip)) {
|
|
311
|
+
this._warnUnknownClip(this._clip);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
let target: string | null = null;
|
|
315
|
+
if (this._clip && layer.states.includes(this._clip)) {
|
|
316
|
+
target = this._clip;
|
|
317
|
+
} else if (restore && layer.states.includes(restore.state)) {
|
|
318
|
+
target = restore.state;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (target && layer.activeState !== target) {
|
|
322
|
+
layer.play(target);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (restore) {
|
|
326
|
+
if (target === restore.state) {
|
|
327
|
+
layer.activeStateCurrentTime = restore.time;
|
|
328
|
+
}
|
|
329
|
+
layer.playing = restore.layerPlaying;
|
|
330
|
+
component.playing = restore.playing;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
private _warnUnknownClip(name: string) {
|
|
335
|
+
if (this._warnedClip === name) {
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
this._warnedClip = name;
|
|
339
|
+
const label = this.id ? ` '${this.id}'` : '';
|
|
340
|
+
console.warn(`pc-anim${label} has no clip named '${name}' - selection unchanged`);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Rebuilds the clip set from the DOM, restoring the active clip and playhead when they
|
|
345
|
+
* survive the rebuild. The engine cannot remove a state from a loaded graph (unassigning
|
|
346
|
+
* only empties the state's tracks), so removals, renames and source changes drop the whole
|
|
347
|
+
* graph and reassign.
|
|
348
|
+
*
|
|
349
|
+
* @internal
|
|
350
|
+
*/
|
|
351
|
+
_refreshClips() {
|
|
352
|
+
const component = this.component;
|
|
353
|
+
if (!component) {
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
const layer = component.baseLayer;
|
|
357
|
+
const restore = layer ? {
|
|
358
|
+
state: layer.activeState,
|
|
359
|
+
time: layer.activeStateCurrentTime,
|
|
360
|
+
playing: component.playing,
|
|
361
|
+
layerPlaying: layer.playing
|
|
362
|
+
} : undefined;
|
|
363
|
+
component.removeStateGraph();
|
|
364
|
+
this._applyClips(restore);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Adopts a clip child announced by its connectedCallback. The initComponent sweep adopts
|
|
369
|
+
* children already present, so this is a no-op for those; it serves clips appended later,
|
|
370
|
+
* and flips an auto-assigned element over to its declared children — declared clips win.
|
|
371
|
+
*
|
|
372
|
+
* @param clip - The clip element.
|
|
373
|
+
* @internal
|
|
374
|
+
*/
|
|
375
|
+
_registerClip(clip: AnimClipElement) {
|
|
376
|
+
if (!this.component) {
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
if (this._autoAssigned) {
|
|
380
|
+
this._refreshClips();
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
if (this._assignedClips.get(clip.name) === clip) {
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
// A clip child appearing supersedes an auto-assign still awaiting its model
|
|
387
|
+
this._sourceGeneration++;
|
|
388
|
+
if (this._adoptClip(clip)) {
|
|
389
|
+
this._applySelection();
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Releases a disconnected clip child. Rebuilds the set — a state cannot be removed from a
|
|
395
|
+
* live graph — and the removal of the last child inside a `<pc-model>` flips the element
|
|
396
|
+
* back to auto-assigning the model's clips.
|
|
397
|
+
*
|
|
398
|
+
* @param clip - The clip element.
|
|
399
|
+
* @internal
|
|
400
|
+
*/
|
|
401
|
+
_unregisterClip(clip: AnimClipElement) {
|
|
402
|
+
if (!this.component) {
|
|
403
|
+
// The whole subtree is coming down (parents disconnect first) - nothing to rebuild
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
if (this._assignedClips.get(clip.name) !== clip) {
|
|
407
|
+
// The clip never held a state (invalid or duplicate name)
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
this._refreshClips();
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Swaps a clip's resolved track in for the placeholder (or for its previous track after an
|
|
415
|
+
* asset change). A swap of the active clip restarts it: the engine preserves the playhead
|
|
416
|
+
* through a track replacement, which would land mid-way into unrelated animation.
|
|
417
|
+
*
|
|
418
|
+
* @param clip - The clip element.
|
|
419
|
+
* @returns Whether the clip still owns its state — the resolution may have been superseded
|
|
420
|
+
* by a rebuild that dropped it.
|
|
421
|
+
* @internal
|
|
422
|
+
*/
|
|
423
|
+
_onClipResolved(clip: AnimClipElement): boolean {
|
|
424
|
+
const component = this.component;
|
|
425
|
+
if (!component || this._assignedClips.get(clip.name) !== clip) {
|
|
426
|
+
return false;
|
|
427
|
+
}
|
|
428
|
+
this._assignClip(clip);
|
|
429
|
+
const layer = component.baseLayer;
|
|
430
|
+
if (layer && layer.activeState === clip.name) {
|
|
431
|
+
layer.play(clip.name);
|
|
432
|
+
}
|
|
433
|
+
return true;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Applies a clip's changed speed or loop. The engine bakes both into the playback state it
|
|
438
|
+
* creates on state entry, so a live change re-enters the state and restores the playhead.
|
|
439
|
+
*
|
|
440
|
+
* @param clip - The clip element.
|
|
441
|
+
* @internal
|
|
442
|
+
*/
|
|
443
|
+
_onClipParamsChanged(clip: AnimClipElement) {
|
|
444
|
+
const component = this.component;
|
|
445
|
+
if (!component || this._assignedClips.get(clip.name) !== clip) {
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
this._assignClip(clip);
|
|
449
|
+
const layer = component.baseLayer;
|
|
450
|
+
if (layer && layer.activeState === clip.name) {
|
|
451
|
+
const time = layer.activeStateCurrentTime;
|
|
452
|
+
layer.play(clip.name);
|
|
453
|
+
layer.activeStateCurrentTime = time;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Resumes playback, optionally switching to a named clip first (a hard cut). A name that
|
|
459
|
+
* matches no clip leaves the selection unchanged.
|
|
460
|
+
*
|
|
461
|
+
* @param name - The name of the clip to play. Resumes the current clip when omitted.
|
|
462
|
+
*/
|
|
463
|
+
play(name?: string) {
|
|
464
|
+
const component = this.component;
|
|
465
|
+
const layer = component ? component.baseLayer : null;
|
|
466
|
+
if (!component || !layer) {
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
// layer.play sets the layer controller's playing flag; the component's is the system
|
|
470
|
+
// gate. Setting both is what makes this a resume regardless of how playback stopped.
|
|
471
|
+
if (name !== undefined) {
|
|
472
|
+
if (!layer.states.includes(name)) {
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
layer.play(name);
|
|
476
|
+
} else {
|
|
477
|
+
layer.play();
|
|
478
|
+
}
|
|
479
|
+
component.playing = true;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Pauses playback, preserving the playhead — {@link play} resumes from where it stopped.
|
|
484
|
+
*/
|
|
485
|
+
pause() {
|
|
486
|
+
if (!this.component) {
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
// Only the component flag - the single gate the system tick reads - is cleared. The
|
|
490
|
+
// layer controller's flag is left as-is so a pause is exactly reversible, whether
|
|
491
|
+
// resumed through play() (which sets both) or through the component API directly.
|
|
492
|
+
this.component.playing = false;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* Cross-fades to a named clip and ensures playback is running. A name that matches no clip
|
|
497
|
+
* leaves the selection unchanged.
|
|
498
|
+
*
|
|
499
|
+
* @param name - The name of the clip to fade to.
|
|
500
|
+
* @param time - The fade duration in seconds. Defaults to the `transition-time` attribute.
|
|
501
|
+
*/
|
|
502
|
+
transition(name: string, time?: number) {
|
|
503
|
+
const component = this.component;
|
|
504
|
+
const layer = component ? component.baseLayer : null;
|
|
505
|
+
if (!component || !layer || !layer.states.includes(name)) {
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
layer.transition(name, Math.max(0, time ?? this._transitionTime));
|
|
509
|
+
layer.playing = true;
|
|
510
|
+
component.playing = true;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Gets the underlying PlayCanvas anim component.
|
|
515
|
+
* @returns The anim component.
|
|
516
|
+
*/
|
|
517
|
+
get component(): AnimComponent {
|
|
518
|
+
return super.component as AnimComponent;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Gets the names of the assigned clips.
|
|
523
|
+
* @returns The clip names, in assignment order.
|
|
524
|
+
*/
|
|
525
|
+
get clips(): string[] {
|
|
526
|
+
const layer = this.component ? this.component.baseLayer : null;
|
|
527
|
+
return layer ? layer.states.filter(state => !ANIM_CONTROL_STATES.includes(state)) : [];
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Sets whether playback starts automatically once a clip is assigned. Defaults to `true`.
|
|
532
|
+
* Applies when clips are assigned — it does not stop a clip that is already playing.
|
|
533
|
+
* @param value - Whether playback starts automatically.
|
|
534
|
+
*/
|
|
535
|
+
set activate(value: boolean) {
|
|
536
|
+
this._activate = value;
|
|
537
|
+
if (this.component) {
|
|
538
|
+
this.component.activate = value;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* Gets whether playback starts automatically once a clip is assigned.
|
|
544
|
+
* @returns Whether playback starts automatically.
|
|
545
|
+
*/
|
|
546
|
+
get activate() {
|
|
547
|
+
return this._activate;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Sets the name of the active clip. Changing it switches playback, cross-fading over
|
|
552
|
+
* `transition-time` seconds (a hard cut at 0). An empty value leaves the current clip
|
|
553
|
+
* playing; a name that matches no clip warns and leaves the selection unchanged.
|
|
554
|
+
* @param value - The name of the active clip.
|
|
555
|
+
*/
|
|
556
|
+
set clip(value: string) {
|
|
557
|
+
this._clip = value;
|
|
558
|
+
const component = this.component;
|
|
559
|
+
const layer = component ? component.baseLayer : null;
|
|
560
|
+
if (!component || !layer || !value) {
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
if (!layer.states.includes(value)) {
|
|
564
|
+
this._warnUnknownClip(value);
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
if (layer.activeState === value) {
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
if (this._transitionTime > 0) {
|
|
571
|
+
this.transition(value);
|
|
572
|
+
} else {
|
|
573
|
+
this.play(value);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* Gets the name of the active clip.
|
|
579
|
+
* @returns The name of the active clip.
|
|
580
|
+
*/
|
|
581
|
+
get clip() {
|
|
582
|
+
return this._clip;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* Sets the playback speed multiplier applied across all clips, where 0 freezes playback.
|
|
587
|
+
* Defaults to 1.
|
|
588
|
+
* @param value - The playback speed multiplier.
|
|
589
|
+
*/
|
|
590
|
+
set speed(value: number) {
|
|
591
|
+
this._speed = value;
|
|
592
|
+
if (this.component) {
|
|
593
|
+
this.component.speed = value;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* Gets the playback speed multiplier applied across all clips.
|
|
599
|
+
* @returns The playback speed multiplier.
|
|
600
|
+
*/
|
|
601
|
+
get speed() {
|
|
602
|
+
return this._speed;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* Sets the cross-fade duration of clip switches made through the `clip` attribute, in
|
|
607
|
+
* seconds. Defaults to 0 (a hard cut).
|
|
608
|
+
* @param value - The cross-fade duration in seconds.
|
|
609
|
+
*/
|
|
610
|
+
set transitionTime(value: number) {
|
|
611
|
+
this._transitionTime = value;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* Gets the cross-fade duration of clip switches made through the `clip` attribute.
|
|
616
|
+
* @returns The cross-fade duration in seconds.
|
|
617
|
+
*/
|
|
618
|
+
get transitionTime() {
|
|
619
|
+
return this._transitionTime;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
static get observedAttributes() {
|
|
623
|
+
return [...super.observedAttributes, 'activate', 'clip', 'speed', 'transition-time'];
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
attributeChangedCallback(name: string, _oldValue: string | null, newValue: string | null) {
|
|
627
|
+
super.attributeChangedCallback(name, _oldValue, newValue);
|
|
628
|
+
|
|
629
|
+
switch (name) {
|
|
630
|
+
case 'activate':
|
|
631
|
+
this.activate = parseBool(newValue, true);
|
|
632
|
+
break;
|
|
633
|
+
case 'clip':
|
|
634
|
+
this.clip = newValue ?? '';
|
|
635
|
+
break;
|
|
636
|
+
case 'speed':
|
|
637
|
+
this.speed = parseNumber(newValue, 1, name);
|
|
638
|
+
break;
|
|
639
|
+
case 'transition-time':
|
|
640
|
+
this.transitionTime = parseNumber(newValue, 0, name);
|
|
641
|
+
break;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
customElements.define('pc-anim', AnimComponentElement);
|
|
647
|
+
|
|
648
|
+
export { AnimComponentElement };
|
|
649
|
+
export type { ContainerWithAnimations };
|
package/src/index.ts
CHANGED
|
@@ -15,6 +15,8 @@ import { ModuleElement } from './module';
|
|
|
15
15
|
import { AppElement } from './app';
|
|
16
16
|
import { EntityElement } from './entity';
|
|
17
17
|
import { AssetElement } from './asset';
|
|
18
|
+
import { AnimComponentElement } from './components/anim-component';
|
|
19
|
+
import { AnimClipElement } from './components/anim-clip';
|
|
18
20
|
import { ListenerComponentElement } from './components/listener-component';
|
|
19
21
|
import { ButtonComponentElement } from './components/button-component';
|
|
20
22
|
import { CameraComponentElement } from './components/camera-component';
|
|
@@ -58,6 +60,8 @@ declare global {
|
|
|
58
60
|
}
|
|
59
61
|
|
|
60
62
|
interface HTMLElementTagNameMap {
|
|
63
|
+
'pc-anim': AnimComponentElement;
|
|
64
|
+
'pc-anim-clip': AnimClipElement;
|
|
61
65
|
'pc-app': AppElement;
|
|
62
66
|
'pc-asset': AssetElement;
|
|
63
67
|
'pc-button': ButtonComponentElement;
|
|
@@ -96,6 +100,8 @@ export {
|
|
|
96
100
|
AppElement,
|
|
97
101
|
EntityElement,
|
|
98
102
|
AssetElement,
|
|
103
|
+
AnimComponentElement,
|
|
104
|
+
AnimClipElement,
|
|
99
105
|
ButtonComponentElement,
|
|
100
106
|
CameraComponentElement,
|
|
101
107
|
CollisionComponentElement,
|
package/src/model.ts
CHANGED
|
@@ -252,13 +252,6 @@ class ModelElement extends AsyncElement {
|
|
|
252
252
|
const entity = container.instantiateRenderEntity();
|
|
253
253
|
this._entity = entity;
|
|
254
254
|
|
|
255
|
-
// @ts-ignore
|
|
256
|
-
if (container.animations.length > 0) {
|
|
257
|
-
entity.addComponent('anim');
|
|
258
|
-
// @ts-ignore
|
|
259
|
-
entity.anim.assignAnimation('animation', container.animations[0].resource);
|
|
260
|
-
}
|
|
261
|
-
|
|
262
255
|
// The parent's readiness re-arms when it is torn down, so these can resume in a later
|
|
263
256
|
// connection cycle. The entity is captured above and the generation re-checked, so a
|
|
264
257
|
// stale resume cannot parent an entity a newer cycle has already destroyed.
|