@playcanvas/web-components 0.11.0 → 0.12.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.
Files changed (82) hide show
  1. package/README.md +1 -1
  2. package/dist/app.d.ts +31 -49
  3. package/dist/asset.d.ts +152 -12
  4. package/dist/async-element.d.ts +26 -9
  5. package/dist/components/button-component.d.ts +3 -7
  6. package/dist/components/camera-component.d.ts +3 -7
  7. package/dist/components/collision-component.d.ts +19 -7
  8. package/dist/components/component.d.ts +41 -4
  9. package/dist/components/element-component.d.ts +4 -8
  10. package/dist/components/gsplat-component.d.ts +2 -7
  11. package/dist/components/layoutchild-component.d.ts +2 -7
  12. package/dist/components/layoutgroup-component.d.ts +3 -7
  13. package/dist/components/light-component.d.ts +3 -7
  14. package/dist/components/listener-component.d.ts +1 -6
  15. package/dist/components/particlesystem-component.d.ts +2 -7
  16. package/dist/components/render-component.d.ts +2 -7
  17. package/dist/components/rigidbody-component.d.ts +3 -7
  18. package/dist/components/screen-component.d.ts +3 -7
  19. package/dist/components/script-component.d.ts +8 -20
  20. package/dist/components/script.d.ts +2 -22
  21. package/dist/components/scrollbar-component.d.ts +2 -7
  22. package/dist/components/scrollview-component.d.ts +3 -7
  23. package/dist/components/sound-component.d.ts +2 -7
  24. package/dist/components/sound-slot.d.ts +8 -6
  25. package/dist/custom-elements.json +5824 -10581
  26. package/dist/entity-base.d.ts +67 -0
  27. package/dist/entity.d.ts +7 -48
  28. package/dist/index.d.ts +41 -1
  29. package/dist/material.d.ts +14 -13
  30. package/dist/model.d.ts +43 -5
  31. package/dist/module.d.ts +0 -6
  32. package/dist/node.d.ts +253 -0
  33. package/dist/parse.d.ts +2 -1
  34. package/dist/pwc.cjs +1830 -274
  35. package/dist/pwc.cjs.map +1 -1
  36. package/dist/pwc.js +1830 -274
  37. package/dist/pwc.js.map +1 -1
  38. package/dist/pwc.min.js +1 -1
  39. package/dist/pwc.min.js.map +1 -1
  40. package/dist/pwc.min.mjs +1 -1
  41. package/dist/pwc.min.mjs.map +1 -1
  42. package/dist/pwc.mjs +1830 -276
  43. package/dist/pwc.mjs.map +1 -1
  44. package/dist/scene.d.ts +4 -7
  45. package/dist/sky.d.ts +13 -5
  46. package/dist/vscode.html-custom-data.json +148 -26
  47. package/dist/web-types.json +894 -581
  48. package/package.json +9 -8
  49. package/src/app.ts +163 -88
  50. package/src/asset.ts +472 -36
  51. package/src/async-element.ts +39 -12
  52. package/src/components/button-component.ts +5 -9
  53. package/src/components/camera-component.ts +24 -10
  54. package/src/components/collision-component.ts +61 -15
  55. package/src/components/component.ts +151 -11
  56. package/src/components/element-component.ts +26 -30
  57. package/src/components/gsplat-component.ts +4 -9
  58. package/src/components/layoutchild-component.ts +4 -9
  59. package/src/components/layoutgroup-component.ts +14 -9
  60. package/src/components/light-component.ts +42 -12
  61. package/src/components/listener-component.ts +1 -7
  62. package/src/components/particlesystem-component.ts +7 -15
  63. package/src/components/render-component.ts +5 -10
  64. package/src/components/rigidbody-component.ts +23 -16
  65. package/src/components/screen-component.ts +5 -9
  66. package/src/components/script-component.ts +108 -46
  67. package/src/components/script.ts +38 -33
  68. package/src/components/scrollbar-component.ts +6 -16
  69. package/src/components/scrollview-component.ts +16 -11
  70. package/src/components/sound-component.ts +10 -15
  71. package/src/components/sound-slot.ts +30 -20
  72. package/src/entity-base.ts +136 -0
  73. package/src/entity.ts +47 -118
  74. package/src/index.ts +50 -1
  75. package/src/loading-bar.ts +8 -8
  76. package/src/material.ts +65 -39
  77. package/src/model.ts +140 -17
  78. package/src/module.ts +8 -7
  79. package/src/node.ts +715 -0
  80. package/src/parse.ts +62 -17
  81. package/src/scene.ts +12 -9
  82. package/src/sky.ts +50 -10
@@ -0,0 +1,136 @@
1
+ import type { Entity } from 'playcanvas';
2
+
3
+ import type { AppElement } from './app';
4
+ import { AsyncElement } from './async-element';
5
+
6
+ /**
7
+ * The attribute names of the inline `onpointer*` event handlers, shared by every element that
8
+ * fronts an engine entity. Spread into `observedAttributes` by subclasses.
9
+ * @ignore
10
+ */
11
+ const POINTER_ATTRIBUTES = [
12
+ 'onpointerenter',
13
+ 'onpointerleave',
14
+ 'onpointerdown',
15
+ 'onpointerup',
16
+ 'onpointermove'
17
+ ] as const;
18
+
19
+ /**
20
+ * The base class for elements that front an engine {@link Entity}: `<pc-entity>`, which creates
21
+ * one, and `<pc-node>`, which binds to one inside a model's instantiated hierarchy. It carries
22
+ * what both need — the `entity` contract, registration with the owning application (which joins
23
+ * picked scene nodes back to elements by identity, never by name), and the pointer listener
24
+ * bookkeeping that lets the application lazily attach its canvas handlers.
25
+ */
26
+ class EntityBaseElement extends AsyncElement {
27
+ protected _entity: Entity | null = null;
28
+
29
+ /**
30
+ * The application element this entity is registered with, cached at registration time so the
31
+ * entity can be unregistered even once this element has left the DOM.
32
+ */
33
+ protected _appElement: AppElement | null = null;
34
+
35
+ /**
36
+ * The pointer event listeners for the entity.
37
+ */
38
+ private _listeners: Record<string, EventListener[]> = {};
39
+
40
+ /**
41
+ * The event types for which an inline `onpointer*` attribute is currently present.
42
+ */
43
+ private _inlineHandlerTypes = new Set<string>();
44
+
45
+ /**
46
+ * The PlayCanvas entity instance. `null` until the element is ready, and again once the
47
+ * entity is gone — await {@link whenReady} or the element's `ready()` promise before
48
+ * accessing it.
49
+ * @returns The entity instance, or `null`.
50
+ */
51
+ get entity(): Entity | null {
52
+ return this._entity;
53
+ }
54
+
55
+ /**
56
+ * Registers `entity` as this element's backing entity with the owning application, which
57
+ * joins engine nodes back to elements by identity (never by name).
58
+ *
59
+ * @param entity - The entity to register.
60
+ */
61
+ protected _registerEntity(entity: Entity) {
62
+ this._appElement = this.closestApp;
63
+ this._appElement?._registerEntityElement(entity, this);
64
+ }
65
+
66
+ /**
67
+ * Removes the registration for `entity`.
68
+ *
69
+ * @param entity - The entity to unregister.
70
+ */
71
+ protected _unregisterEntity(entity: Entity) {
72
+ this._appElement?._unregisterEntityElement(entity);
73
+ this._appElement = null;
74
+ }
75
+
76
+ /**
77
+ * Tracks whether an inline `onpointer*` attribute is present. The browser itself compiles and
78
+ * runs these attributes — they are standard `GlobalEventHandlers`, so setting one replaces
79
+ * the previous handler and removing it removes the handler, exactly like `onclick` on any
80
+ * HTML element. But because they bypass {@link addEventListener}, the connect/disconnect
81
+ * bookkeeping that lets the application lazily attach its canvas pointer handlers must be
82
+ * kept in sync here.
83
+ *
84
+ * @param name - The attribute name (e.g. 'onpointerdown').
85
+ * @param value - The attribute value, or `null` when the attribute has been removed.
86
+ */
87
+ protected _updateInlineHandler(name: string, value: string | null) {
88
+ const type = name.substring(2);
89
+ const had = this._inlineHandlerTypes.has(type);
90
+ const has = value !== null;
91
+
92
+ if (has && !had) {
93
+ this._inlineHandlerTypes.add(type);
94
+ this.dispatchEvent(new CustomEvent(`${type}:connect`, { bubbles: true }));
95
+ } else if (!has && had) {
96
+ this._inlineHandlerTypes.delete(type);
97
+ this.dispatchEvent(new CustomEvent(`${type}:disconnect`, { bubbles: true }));
98
+ }
99
+ }
100
+
101
+ addEventListener(type: string, listener: EventListener, options?: boolean | AddEventListenerOptions) {
102
+ if (!this._listeners[type]) {
103
+ this._listeners[type] = [];
104
+ }
105
+ this._listeners[type].push(listener);
106
+ super.addEventListener(type, listener, options);
107
+ if (type.startsWith('pointer')) {
108
+ this.dispatchEvent(new CustomEvent(`${type}:connect`, { bubbles: true }));
109
+ }
110
+ }
111
+
112
+ removeEventListener(type: string, listener: EventListener, options?: boolean | EventListenerOptions) {
113
+ if (this._listeners[type]) {
114
+ this._listeners[type] = this._listeners[type].filter((l) => l !== listener);
115
+ }
116
+ super.removeEventListener(type, listener, options);
117
+ if (type.startsWith('pointer')) {
118
+ this.dispatchEvent(new CustomEvent(`${type}:disconnect`, { bubbles: true }));
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Whether the element has a listener for an event type, registered either with
124
+ * {@link addEventListener} or with the matching inline `onpointer*` attribute. Read by the
125
+ * containing `<pc-app>` element to gate pointer event synthesis.
126
+ *
127
+ * @param type - The event type.
128
+ * @returns Whether a listener is registered.
129
+ * @internal
130
+ */
131
+ _hasListeners(type: string): boolean {
132
+ return Boolean(this._listeners[type]?.length) || this._inlineHandlerTypes.has(type);
133
+ }
134
+ }
135
+
136
+ export { EntityBaseElement, POINTER_ATTRIBUTES };
package/src/entity.ts CHANGED
@@ -1,7 +1,7 @@
1
- import { AppBase, Entity, Vec3 } from 'playcanvas';
1
+ import type { AppBase } from 'playcanvas';
2
+ import { Entity, Vec3 } from 'playcanvas';
2
3
 
3
- import type { AppElement } from './app';
4
- import { AsyncElement } from './async-element';
4
+ import { EntityBaseElement, POINTER_ATTRIBUTES } from './entity-base';
5
5
  import { parseBool, parseTags, parseVec3 } from './parse';
6
6
 
7
7
  /**
@@ -28,7 +28,7 @@ import { parseBool, parseTags, parseVec3 } from './parse';
28
28
  * @fires {PointerEvent} pointerdown - Fired when a pointer button is pressed over the entity.
29
29
  * @fires {PointerEvent} pointerup - Fired when a pointer button is released over the entity.
30
30
  */
31
- class EntityElement extends AsyncElement {
31
+ class EntityElement extends EntityBaseElement {
32
32
  /**
33
33
  * Whether the entity is enabled.
34
34
  */
@@ -59,40 +59,19 @@ class EntityElement extends AsyncElement {
59
59
  */
60
60
  private _tags: string[] = [];
61
61
 
62
- /**
63
- * The pointer event listeners for the entity.
64
- */
65
- private _listeners: { [key: string]: EventListener[] } = {};
66
-
67
- /**
68
- * The event types for which an inline `onpointer*` attribute is currently present.
69
- */
70
- private _inlineHandlerTypes = new Set<string>();
71
-
72
62
  /**
73
63
  * Whether the hierarchy has been built for this entity.
74
64
  */
75
65
  private _built = false;
76
66
 
77
- private _entity: Entity | null = null;
78
-
79
67
  /**
80
- * The application element this entity is registered with, cached at creation time so the
81
- * entity can be unregistered even once this element has left the DOM.
82
- */
83
- private _appElement: AppElement | null = null;
84
-
85
- /**
86
- * The PlayCanvas entity instance. `null` until the element is ready, and again once it has
87
- * been removed from the document — await {@link whenReady} or the element's `ready()`
88
- * promise before accessing it.
89
- * @returns The entity instance, or `null`.
68
+ * Creates the backing entity. Called by the containing `<pc-app>` element during its boot
69
+ * sweep, and on connection for elements inserted while the application is already running.
70
+ *
71
+ * @param app - The application to create the entity in.
72
+ * @internal
90
73
  */
91
- get entity(): Entity | null {
92
- return this._entity;
93
- }
94
-
95
- createEntity(app: AppBase) {
74
+ _createEntity(app: AppBase) {
96
75
  // Guard against double creation. When a subtree is inserted at runtime (e.g. cloning a
97
76
  // `<template>`), an ancestor's connectedCallback eagerly creates descendant entities; the
98
77
  // descendants' own connectedCallbacks would otherwise create them a second time.
@@ -116,35 +95,53 @@ class EntityElement extends AsyncElement {
116
95
  entity.tags.add(this._tags);
117
96
  }
118
97
 
119
- // Register with the owning application, which joins engine nodes back to elements by
120
- // identity (never by name), and hook the entity's destruction. The engine fires 'destroy'
121
- // for every entity in a destroyed subtree, so the element learns of its entity's death no
122
- // matter who causes it: this element, an ancestor, the whole application, or a user
123
- // script calling entity.destroy().
124
- this._appElement = this.closestApp;
125
- this._appElement?._registerEntityElement(entity, this);
98
+ // Register with the owning application and hook the entity's destruction. The engine
99
+ // fires 'destroy' for every entity in a destroyed subtree, so the element learns of its
100
+ // entity's death no matter who causes it: this element, an ancestor, the whole
101
+ // application, or a user script calling entity.destroy().
102
+ this._registerEntity(entity);
126
103
  entity.once('destroy', this._onEntityDestroy, this);
127
104
  }
128
105
 
129
106
  /**
130
107
  * Handles the destruction of the backing entity. Resets the element so a later re-insertion
131
- * starts clean: `_built` must be cleared alongside `_entity`, or buildHierarchy would bail
132
- * and a re-created entity would never be parented.
108
+ * starts clean: `_built` must be cleared alongside `_entity`, or _buildHierarchy would bail
109
+ * and a re-created entity would never be parented. Readiness is re-armed for the same
110
+ * reason — with the entity gone, a resolved ready promise would resume its awaiters against
111
+ * a null `entity`.
133
112
  *
134
113
  * @param entity - The entity that was destroyed.
135
114
  */
136
115
  private _onEntityDestroy(entity: Entity) {
137
- this._appElement?._unregisterEntityElement(entity);
138
- this._appElement = null;
116
+ this._unregisterEntity(entity);
139
117
  this._entity = null;
140
118
  this._built = false;
119
+ this._resetReady();
141
120
  }
142
121
 
143
- buildHierarchy(app: AppBase) {
122
+ /**
123
+ * Parents the backing entity: under the entity of the nearest ancestor `<pc-entity>` or
124
+ * `<pc-node>` when there is one, and under the application root otherwise. Called by the
125
+ * containing `<pc-app>` element once a sweep has created every entity, so a parent's
126
+ * existence never depends on document order.
127
+ *
128
+ * @param app - The application whose root adopts parentless entities.
129
+ * @internal
130
+ */
131
+ _buildHierarchy(app: AppBase) {
144
132
  if (!this.entity || this._built) return;
145
- this._built = true;
146
133
 
147
134
  const closestEntity = this.closestEntity;
135
+
136
+ // A host element without an entity is an unresolved `<pc-node>`: building now would
137
+ // mis-anchor this entity to the application root while the host is still resolving.
138
+ // Stay unbuilt - the host drives this subtree itself once it binds.
139
+ if (closestEntity && !closestEntity.entity) {
140
+ return;
141
+ }
142
+
143
+ this._built = true;
144
+
148
145
  if (closestEntity?.entity) {
149
146
  closestEntity.entity.addChild(this.entity);
150
147
  } else {
@@ -168,19 +165,19 @@ class EntityElement extends AsyncElement {
168
165
  }
169
166
 
170
167
  // If app is already running, create entity immediately
171
- if (closestApp.hierarchyReady) {
168
+ if (closestApp._hierarchyReady) {
172
169
  const app = closestApp.app!;
173
170
 
174
- this.createEntity(app);
175
- this.buildHierarchy(app);
171
+ this._createEntity(app);
172
+ this._buildHierarchy(app);
176
173
 
177
174
  // Handle any child entities that might exist
178
175
  const childEntities = this.querySelectorAll<EntityElement>('pc-entity');
179
176
  childEntities.forEach((child) => {
180
- child.createEntity(app);
177
+ child._createEntity(app);
181
178
  });
182
179
  childEntities.forEach((child) => {
183
- child.buildHierarchy(app);
180
+ child._buildHierarchy(app);
184
181
  });
185
182
  }
186
183
  }
@@ -308,45 +305,8 @@ class EntityElement extends AsyncElement {
308
305
  return this._tags;
309
306
  }
310
307
 
311
- /**
312
- * Tracks whether an inline `onpointer*` attribute is present. The browser itself compiles and
313
- * runs these attributes — they are standard `GlobalEventHandlers`, so setting one replaces
314
- * the previous handler and removing it removes the handler, exactly like `onclick` on any
315
- * HTML element. But because they bypass {@link addEventListener}, the connect/disconnect
316
- * bookkeeping that lets the application lazily attach its canvas pointer handlers must be
317
- * kept in sync here.
318
- *
319
- * @param name - The attribute name (e.g. 'onpointerdown').
320
- * @param value - The attribute value, or `null` when the attribute has been removed.
321
- */
322
- private _updateInlineHandler(name: string, value: string | null) {
323
- const type = name.substring(2);
324
- const had = this._inlineHandlerTypes.has(type);
325
- const has = value !== null;
326
-
327
- if (has && !had) {
328
- this._inlineHandlerTypes.add(type);
329
- this.dispatchEvent(new CustomEvent(`${type}:connect`, { bubbles: true }));
330
- } else if (!has && had) {
331
- this._inlineHandlerTypes.delete(type);
332
- this.dispatchEvent(new CustomEvent(`${type}:disconnect`, { bubbles: true }));
333
- }
334
- }
335
-
336
308
  static get observedAttributes() {
337
- return [
338
- 'enabled',
339
- 'name',
340
- 'position',
341
- 'rotation',
342
- 'scale',
343
- 'tags',
344
- 'onpointerenter',
345
- 'onpointerleave',
346
- 'onpointerdown',
347
- 'onpointerup',
348
- 'onpointermove'
349
- ];
309
+ return ['enabled', 'name', 'position', 'rotation', 'scale', 'tags', ...POINTER_ATTRIBUTES];
350
310
  }
351
311
 
352
312
  attributeChangedCallback(name: string, _oldValue: string | null, newValue: string | null) {
@@ -378,39 +338,8 @@ class EntityElement extends AsyncElement {
378
338
  break;
379
339
  }
380
340
  }
381
-
382
- addEventListener(type: string, listener: EventListener, options?: boolean | AddEventListenerOptions) {
383
- if (!this._listeners[type]) {
384
- this._listeners[type] = [];
385
- }
386
- this._listeners[type].push(listener);
387
- super.addEventListener(type, listener, options);
388
- if (type.startsWith('pointer')) {
389
- this.dispatchEvent(new CustomEvent(`${type}:connect`, { bubbles: true }));
390
- }
391
- }
392
-
393
- removeEventListener(type: string, listener: EventListener, options?: boolean | EventListenerOptions) {
394
- if (this._listeners[type]) {
395
- this._listeners[type] = this._listeners[type].filter(l => l !== listener);
396
- }
397
- super.removeEventListener(type, listener, options);
398
- if (type.startsWith('pointer')) {
399
- this.dispatchEvent(new CustomEvent(`${type}:disconnect`, { bubbles: true }));
400
- }
401
- }
402
-
403
- hasListeners(type: string): boolean {
404
- return Boolean(this._listeners[type]?.length) || this._inlineHandlerTypes.has(type);
405
- }
406
341
  }
407
342
 
408
343
  customElements.define('pc-entity', EntityElement);
409
344
 
410
- declare global {
411
- interface HTMLElementTagNameMap {
412
- 'pc-entity': EntityElement;
413
- }
414
- }
415
-
416
345
  export { EntityElement };
package/src/index.ts CHANGED
@@ -7,7 +7,7 @@
7
7
  * @module EngineWebComponents
8
8
  */
9
9
 
10
- /* eslint-disable import/order */
10
+ /* eslint-disable import-x/order */
11
11
 
12
12
  // Note that order matters here (e.g. pc-entity must be defined before components)
13
13
  import { AsyncElement, whenReady } from './async-element';
@@ -35,11 +35,58 @@ import { ScriptElement } from './components/script';
35
35
  import { SoundComponentElement } from './components/sound-component';
36
36
  import { SoundSlotElement } from './components/sound-slot';
37
37
  import { GSplatComponentElement } from './components/gsplat-component';
38
+ import { EntityBaseElement } from './entity-base';
38
39
  import { MaterialElement } from './material';
39
40
  import { ModelElement } from './model';
41
+ import { NodeElement } from './node';
40
42
  import { SceneElement } from './scene';
41
43
  import { SkyElement } from './sky';
42
44
 
45
+ import type {
46
+ ScriptAttributesChangeEvent,
47
+ ScriptEnableChangeEvent,
48
+ ScriptNameChangeEvent
49
+ } from './components/script-component';
50
+
51
+ declare global {
52
+ interface HTMLElementEventMap {
53
+ scriptattributeschange: ScriptAttributesChangeEvent;
54
+ scriptenablechange: ScriptEnableChangeEvent;
55
+ scriptnamechange: ScriptNameChangeEvent;
56
+ }
57
+
58
+ interface HTMLElementTagNameMap {
59
+ 'pc-app': AppElement;
60
+ 'pc-asset': AssetElement;
61
+ 'pc-button': ButtonComponentElement;
62
+ 'pc-camera': CameraComponentElement;
63
+ 'pc-collision': CollisionComponentElement;
64
+ 'pc-element': ElementComponentElement;
65
+ 'pc-entity': EntityElement;
66
+ 'pc-gsplat': GSplatComponentElement;
67
+ 'pc-layoutchild': LayoutChildComponentElement;
68
+ 'pc-layoutgroup': LayoutGroupComponentElement;
69
+ 'pc-light': LightComponentElement;
70
+ 'pc-listener': ListenerComponentElement;
71
+ 'pc-material': MaterialElement;
72
+ 'pc-model': ModelElement;
73
+ 'pc-module': ModuleElement;
74
+ 'pc-node': NodeElement;
75
+ 'pc-particles': ParticleSystemComponentElement;
76
+ 'pc-render': RenderComponentElement;
77
+ 'pc-rigidbody': RigidBodyComponentElement;
78
+ 'pc-scene': SceneElement;
79
+ 'pc-screen': ScreenComponentElement;
80
+ 'pc-script': ScriptElement;
81
+ 'pc-scripts': ScriptComponentElement;
82
+ 'pc-scrollbar': ScrollbarComponentElement;
83
+ 'pc-scrollview': ScrollViewComponentElement;
84
+ 'pc-sky': SkyElement;
85
+ 'pc-sound': SoundSlotElement;
86
+ 'pc-sounds': SoundComponentElement;
87
+ }
88
+ }
89
+
43
90
  export {
44
91
  AsyncElement,
45
92
  ModuleElement,
@@ -66,8 +113,10 @@ export {
66
113
  SoundComponentElement,
67
114
  SoundSlotElement,
68
115
  GSplatComponentElement,
116
+ EntityBaseElement,
69
117
  MaterialElement,
70
118
  ModelElement,
119
+ NodeElement,
71
120
  SceneElement,
72
121
  SkyElement,
73
122
  whenReady
@@ -59,14 +59,14 @@ class LoadingBar {
59
59
  // aria-valuenow is set, which is what marks a progressbar indeterminate. jsdom has no Web
60
60
  // Animations API, so the guard degrades to a static bar there rather than crashing boot.
61
61
  if (typeof this._fill.animate === 'function') {
62
- this._sweep = this._fill.animate([
63
- { transform: 'scaleX(0.25) translateX(-100%)' },
64
- { transform: 'scaleX(0.25) translateX(500%)' }
65
- ], {
66
- duration: 1000,
67
- iterations: Infinity,
68
- easing: 'ease-in-out'
69
- });
62
+ this._sweep = this._fill.animate(
63
+ [{ transform: 'scaleX(0.25) translateX(-100%)' }, { transform: 'scaleX(0.25) translateX(500%)' }],
64
+ {
65
+ duration: 1000,
66
+ iterations: Infinity,
67
+ easing: 'ease-in-out'
68
+ }
69
+ );
70
70
  }
71
71
  }
72
72