@xtia/timeline 1.0.7 → 1.0.8
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/README.md +57 -9
- package/index.js +5 -15
- package/internal/easing.js +1 -4
- package/internal/emitters.d.ts +3 -2
- package/internal/emitters.js +10 -15
- package/internal/point.js +2 -6
- package/internal/range.js +8 -12
- package/internal/timeline.js +8 -13
- package/internal/tween.d.ts +3 -0
- package/internal/tween.js +3 -6
- package/internal/utils.js +1 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -85,27 +85,41 @@ asPercent
|
|
|
85
85
|
n => progressBar.style.width = n
|
|
86
86
|
);
|
|
87
87
|
|
|
88
|
-
// apply easing
|
|
88
|
+
// apply easing
|
|
89
89
|
const eased = firstFiveSeconds.ease("easeInOut");
|
|
90
90
|
eased.listen(
|
|
91
91
|
v => console.log(`Eased value: ${v}`)
|
|
92
92
|
);
|
|
93
93
|
|
|
94
|
-
//
|
|
95
|
-
|
|
94
|
+
// chain them
|
|
95
|
+
range
|
|
96
96
|
.tween(0, 30)
|
|
97
97
|
.map(Math.floor)
|
|
98
98
|
.dedupe()
|
|
99
99
|
.tap(n => console.log("Showing frame #", n))
|
|
100
100
|
.map(n => `animation-frame-${n}.png`)
|
|
101
101
|
.listen(filename => img.src = filename);
|
|
102
|
+
|
|
103
|
+
// each step in the chain is a 'pure', independent emitter that emits
|
|
104
|
+
// a transformation of its parent's emissions
|
|
105
|
+
const filenameEmitter = range
|
|
106
|
+
.tween(0, 3)
|
|
107
|
+
.map(Math.floor)
|
|
108
|
+
.dedupe()
|
|
109
|
+
.map(n => `animation-frame-${n}.png`);
|
|
110
|
+
|
|
111
|
+
// filenameEmitter will emit filenames as the Timeline passes through 'range'.
|
|
112
|
+
// it can be listened directly or further transformed
|
|
113
|
+
const urlEmitter = filenameEmitter
|
|
114
|
+
.map(filename => `http://www.example.com/${filename}`);
|
|
115
|
+
|
|
102
116
|
```
|
|
103
117
|
|
|
104
118
|
Range objects also provide a `play()` method that instructs the Timeline to play through that particular range:
|
|
105
119
|
|
|
106
120
|
```ts
|
|
107
121
|
// play through the first two seconds of the Timeline
|
|
108
|
-
timeline
|
|
122
|
+
await timeline
|
|
109
123
|
.range(0, 2000)
|
|
110
124
|
.play();
|
|
111
125
|
```
|
|
@@ -158,7 +172,41 @@ timeline
|
|
|
158
172
|
|
|
159
173
|
## More on tweening
|
|
160
174
|
|
|
161
|
-
Tween emitters can interpolate numbers, arrays of numbers, strings, and objects with a method `blend(from: this, to: this): this
|
|
175
|
+
Tween emitters can interpolate numbers, arrays of numbers, strings, and objects with a method `blend(from: this, to: this): this`, by the progression value emitted by their parent.
|
|
176
|
+
|
|
177
|
+
```ts
|
|
178
|
+
const range = timeline.range(0, 2000);
|
|
179
|
+
|
|
180
|
+
// numbers
|
|
181
|
+
range
|
|
182
|
+
.ease("overshootIn")
|
|
183
|
+
.tween(300, 500)
|
|
184
|
+
.listen(v => element.scrollTop = v);
|
|
185
|
+
|
|
186
|
+
// number arrays
|
|
187
|
+
range
|
|
188
|
+
.tween([0, 180], [360, 180])
|
|
189
|
+
.listen((angles) => pieChart.setValues(angles));
|
|
190
|
+
|
|
191
|
+
// strings
|
|
192
|
+
range
|
|
193
|
+
.tween("#000000", "#ff00ff")
|
|
194
|
+
.listen(v => element.style.color = v);
|
|
195
|
+
|
|
196
|
+
// blendable objects
|
|
197
|
+
// (T extends { blend(from: this, to: this): this })
|
|
198
|
+
import { RGBA } from "@xtia/rgba";
|
|
199
|
+
range
|
|
200
|
+
.tween(RGBA.parse("#c971a7"), RGBA.parse("#fff"))
|
|
201
|
+
.listen(v => element.style.background = v.hexCode);
|
|
202
|
+
|
|
203
|
+
import { Angle } from "@xtia/mezr";
|
|
204
|
+
range
|
|
205
|
+
.tween(Angle.degrees(45), Angle.turns(.5))
|
|
206
|
+
.map(a => `rotate(${a.asDegrees}deg)`)
|
|
207
|
+
.listen(v => element.style.transform = v);
|
|
208
|
+
|
|
209
|
+
```
|
|
162
210
|
|
|
163
211
|
#### String interpolation
|
|
164
212
|
* If the strings contain tweenable tokens (numbers, colour codes) and are otherwise identical, those tokens are interpolated
|
|
@@ -180,7 +228,7 @@ timeline
|
|
|
180
228
|
.listen(v => document.title = v);
|
|
181
229
|
```
|
|
182
230
|
|
|
183
|
-
|
|
231
|
+
Try out the [shadow tweening example at StackBlitz](https://stackblitz.com/edit/timeline-string-tween?file=src%2Fmain.ts)
|
|
184
232
|
|
|
185
233
|
## Autoplay and Looping Strategies
|
|
186
234
|
|
|
@@ -190,13 +238,13 @@ To create a Timeline that immediately starts playing, pass `true` to its constru
|
|
|
190
238
|
// immediately fade in an element
|
|
191
239
|
new Timeline(true)
|
|
192
240
|
.range(0, 1000)
|
|
193
|
-
.
|
|
241
|
+
.listen(v => element.style.opacity = v);
|
|
194
242
|
|
|
195
243
|
// note, an `animate(duration)` function is exported for
|
|
196
244
|
// disposable, single-use animations such as this:
|
|
197
245
|
import { animate } from "@xtia/timeline";
|
|
198
246
|
animate(1000)
|
|
199
|
-
.
|
|
247
|
+
.listen(v => element.style.opacity = v);
|
|
200
248
|
```
|
|
201
249
|
|
|
202
250
|
Normally a Timeline will simply stop playing when it reaches the end. This can be changed by passing a second argument (`endAction`) to the constructor.
|
|
@@ -279,7 +327,7 @@ resourceUrls.forEach(url => {
|
|
|
279
327
|
We can pass a second argument to `seek()` to perform a 'smooth seek' over the given duration. A third argument can provide an easing function for the smooth seek process:
|
|
280
328
|
|
|
281
329
|
```ts
|
|
282
|
-
timeline.seek(timeline.end, 400, "overshootIn");
|
|
330
|
+
await timeline.seek(timeline.end, 400, "overshootIn");
|
|
283
331
|
```
|
|
284
332
|
|
|
285
333
|
## Backward-compatibility
|
package/index.js
CHANGED
|
@@ -1,15 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
Object.defineProperty(exports, "animate", { enumerable: true, get: function () { return timeline_1.animate; } });
|
|
7
|
-
var point_1 = require("./internal/point");
|
|
8
|
-
Object.defineProperty(exports, "TimelinePoint", { enumerable: true, get: function () { return point_1.TimelinePoint; } });
|
|
9
|
-
var range_1 = require("./internal/range");
|
|
10
|
-
Object.defineProperty(exports, "TimelineRange", { enumerable: true, get: function () { return range_1.TimelineRange; } });
|
|
11
|
-
var emitters_1 = require("./internal/emitters");
|
|
12
|
-
Object.defineProperty(exports, "Emitter", { enumerable: true, get: function () { return emitters_1.Emitter; } });
|
|
13
|
-
Object.defineProperty(exports, "RangeProgression", { enumerable: true, get: function () { return emitters_1.RangeProgression; } });
|
|
14
|
-
var easing_1 = require("./internal/easing");
|
|
15
|
-
Object.defineProperty(exports, "easers", { enumerable: true, get: function () { return easing_1.easers; } });
|
|
1
|
+
export { Timeline, animate } from "./internal/timeline";
|
|
2
|
+
export { TimelinePoint } from "./internal/point";
|
|
3
|
+
export { TimelineRange } from "./internal/range";
|
|
4
|
+
export { Emitter, RangeProgression } from "./internal/emitters";
|
|
5
|
+
export { easers } from "./internal/easing";
|
package/internal/easing.js
CHANGED
package/internal/emitters.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Easer, easers } from "./easing";
|
|
2
|
-
import { Tweenable } from "./tween";
|
|
2
|
+
import { BlendableWith, Tweenable } from "./tween";
|
|
3
3
|
type Handler<T> = (value: T) => void;
|
|
4
4
|
export type ListenFunc<T> = (handler: Handler<T>) => UnsubscribeFunc;
|
|
5
5
|
export type UnsubscribeFunc = () => void;
|
|
@@ -123,6 +123,7 @@ export declare class RangeProgression extends Emitter<number> {
|
|
|
123
123
|
* @returns Listenable: emits interpolated values
|
|
124
124
|
*/
|
|
125
125
|
tween<T extends Tweenable>(from: T, to: T): Emitter<T>;
|
|
126
|
+
tween<T extends BlendableWith<T, R>, R>(from: T, to: R): Emitter<T>;
|
|
126
127
|
/**
|
|
127
128
|
* Creates a chainable progress emitter that quantises progress, as emitted by its parent, to the nearest of `steps` discrete values.
|
|
128
129
|
*
|
|
@@ -182,7 +183,7 @@ export declare class RangeProgression extends Emitter<number> {
|
|
|
182
183
|
* 1
|
|
183
184
|
* | /
|
|
184
185
|
* o| /
|
|
185
|
-
* u|/
|
|
186
|
+
* u|/ __ delta=.5
|
|
186
187
|
* t| /
|
|
187
188
|
* | /
|
|
188
189
|
* |___/__
|
package/internal/emitters.js
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
const tween_1 = require("./tween");
|
|
6
|
-
const utils_1 = require("./utils");
|
|
7
|
-
class Emitter {
|
|
1
|
+
import { easers } from "./easing";
|
|
2
|
+
import { tweenValue } from "./tween";
|
|
3
|
+
import { clamp } from "./utils";
|
|
4
|
+
export class Emitter {
|
|
8
5
|
constructor(onListen) {
|
|
9
6
|
this.onListen = onListen;
|
|
10
7
|
/**
|
|
@@ -124,8 +121,7 @@ class Emitter {
|
|
|
124
121
|
return this;
|
|
125
122
|
}
|
|
126
123
|
}
|
|
127
|
-
|
|
128
|
-
class RangeProgression extends Emitter {
|
|
124
|
+
export class RangeProgression extends Emitter {
|
|
129
125
|
constructor() {
|
|
130
126
|
super(...arguments);
|
|
131
127
|
this.redirect = (listen) => new RangeProgression(listen);
|
|
@@ -134,14 +130,14 @@ class RangeProgression extends Emitter {
|
|
|
134
130
|
if (!easer)
|
|
135
131
|
return this;
|
|
136
132
|
const easerFunc = typeof easer == "string"
|
|
137
|
-
?
|
|
133
|
+
? easers[easer]
|
|
138
134
|
: easer;
|
|
139
135
|
return new RangeProgression(easer ? (handler => this.onListen((progress) => {
|
|
140
136
|
handler(easerFunc(progress));
|
|
141
137
|
})) : h => this.onListen(h));
|
|
142
138
|
}
|
|
143
139
|
tween(from, to) {
|
|
144
|
-
return new Emitter(handler => this.onListen(progress => handler(
|
|
140
|
+
return new Emitter(handler => this.onListen(progress => handler(tweenValue(from, to, progress))));
|
|
145
141
|
}
|
|
146
142
|
/**
|
|
147
143
|
* Creates a chainable progress emitter that quantises progress, as emitted by its parent, to the nearest of `steps` discrete values.
|
|
@@ -156,7 +152,7 @@ class RangeProgression extends Emitter {
|
|
|
156
152
|
}
|
|
157
153
|
return new RangeProgression(handler => this.onListen(progress => {
|
|
158
154
|
const snapped = Math.round(progress * steps) / steps;
|
|
159
|
-
handler(
|
|
155
|
+
handler(clamp(snapped, 0, 1));
|
|
160
156
|
}));
|
|
161
157
|
}
|
|
162
158
|
/**
|
|
@@ -177,7 +173,7 @@ class RangeProgression extends Emitter {
|
|
|
177
173
|
* @returns Listenable: emits clamped progression values
|
|
178
174
|
*/
|
|
179
175
|
clamp(min = 0, max = 1) {
|
|
180
|
-
return new RangeProgression(handler => this.onListen(progress => handler(
|
|
176
|
+
return new RangeProgression(handler => this.onListen(progress => handler(clamp(progress, min, max))));
|
|
181
177
|
}
|
|
182
178
|
/**
|
|
183
179
|
* Creates a chainable progress emitter that maps incoming values to a repeating linear scale
|
|
@@ -237,7 +233,7 @@ class RangeProgression extends Emitter {
|
|
|
237
233
|
* 1
|
|
238
234
|
* | /
|
|
239
235
|
* o| /
|
|
240
|
-
* u|/
|
|
236
|
+
* u|/ __ delta=.5
|
|
241
237
|
* t| /
|
|
242
238
|
* | /
|
|
243
239
|
* |___/__
|
|
@@ -251,4 +247,3 @@ class RangeProgression extends Emitter {
|
|
|
251
247
|
return new RangeProgression(handler => this.onListen(value => handler((value + delta) % 1)));
|
|
252
248
|
}
|
|
253
249
|
}
|
|
254
|
-
exports.RangeProgression = RangeProgression;
|
package/internal/point.js
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
exports.TimelinePoint = void 0;
|
|
4
|
-
const emitters_1 = require("./emitters");
|
|
5
|
-
class TimelinePoint extends emitters_1.Emitter {
|
|
1
|
+
import { Emitter } from "./emitters";
|
|
2
|
+
export class TimelinePoint extends Emitter {
|
|
6
3
|
/** @internal Manual construction of TimelinePoint is outside of the API contract and subject to undocumented change */
|
|
7
4
|
constructor(onListen, timeline,
|
|
8
5
|
/**
|
|
@@ -42,4 +39,3 @@ class TimelinePoint extends emitters_1.Emitter {
|
|
|
42
39
|
return this.timeline.point(this.position + timeOffset);
|
|
43
40
|
}
|
|
44
41
|
}
|
|
45
|
-
exports.TimelinePoint = TimelinePoint;
|
package/internal/range.js
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
const point_1 = require("./point");
|
|
6
|
-
const utils_1 = require("./utils");
|
|
7
|
-
class TimelineRange extends emitters_1.RangeProgression {
|
|
1
|
+
import { RangeProgression } from "./emitters";
|
|
2
|
+
import { TimelinePoint } from "./point";
|
|
3
|
+
import { clamp } from "./utils";
|
|
4
|
+
export class TimelineRange extends RangeProgression {
|
|
8
5
|
/** @internal Manual construction of RangeProgression is outside of the API contract and subject to undocumented change */
|
|
9
6
|
constructor(onListen, timeline, startPosition,
|
|
10
7
|
/** The duration of this range */
|
|
@@ -15,8 +12,8 @@ class TimelineRange extends emitters_1.RangeProgression {
|
|
|
15
12
|
this.duration = duration;
|
|
16
13
|
this.redirect = (listen) => new TimelineRange(listen, this.timeline, this.startPosition, this.duration);
|
|
17
14
|
this.start = timeline.point(startPosition);
|
|
18
|
-
this.end = timeline.point(startPosition + duration);
|
|
19
15
|
this.endPosition = startPosition + duration;
|
|
16
|
+
this.end = timeline.point(this.endPosition);
|
|
20
17
|
}
|
|
21
18
|
/**
|
|
22
19
|
* Creates two ranges by seperating one at a given point
|
|
@@ -58,7 +55,7 @@ class TimelineRange extends emitters_1.RangeProgression {
|
|
|
58
55
|
* @returns Listenable: this range will emit a progression value (0..1) when a `seek()` passes or intersects it
|
|
59
56
|
*/
|
|
60
57
|
grow(delta, anchor = 0) {
|
|
61
|
-
const clampedAnchor =
|
|
58
|
+
const clampedAnchor = clamp(anchor, 0, 1);
|
|
62
59
|
const leftDelta = -delta * (1 - clampedAnchor);
|
|
63
60
|
const rightDelta = delta * clampedAnchor;
|
|
64
61
|
const newStart = this.startPosition + leftDelta;
|
|
@@ -79,7 +76,7 @@ class TimelineRange extends emitters_1.RangeProgression {
|
|
|
79
76
|
if (factor <= 0) {
|
|
80
77
|
throw new RangeError('scale factor must be > 0');
|
|
81
78
|
}
|
|
82
|
-
const clampedAnchor =
|
|
79
|
+
const clampedAnchor = clamp(anchor, 0, 1);
|
|
83
80
|
const oldLen = this.endPosition - this.startPosition;
|
|
84
81
|
const pivot = this.startPosition + oldLen * clampedAnchor;
|
|
85
82
|
const newStart = pivot - (pivot - this.startPosition) * factor;
|
|
@@ -91,10 +88,9 @@ class TimelineRange extends emitters_1.RangeProgression {
|
|
|
91
88
|
return this.timeline.range(newStart, newEnd - newStart);
|
|
92
89
|
}
|
|
93
90
|
contains(target) {
|
|
94
|
-
const [targetStart, targetEnd] = target instanceof
|
|
91
|
+
const [targetStart, targetEnd] = target instanceof TimelinePoint
|
|
95
92
|
? [target.position, target.position]
|
|
96
93
|
: [target.startPosition, target.startPosition + target.duration];
|
|
97
94
|
return targetStart >= this.startPosition && targetEnd < this.endPosition;
|
|
98
95
|
}
|
|
99
96
|
}
|
|
100
|
-
exports.TimelineRange = TimelineRange;
|
package/internal/timeline.js
CHANGED
|
@@ -1,10 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
exports.animate = animate;
|
|
5
|
-
const point_1 = require("./point");
|
|
6
|
-
const range_1 = require("./range");
|
|
7
|
-
const utils_1 = require("./utils");
|
|
1
|
+
import { TimelinePoint } from "./point";
|
|
2
|
+
import { TimelineRange } from "./range";
|
|
3
|
+
import { clamp } from "./utils";
|
|
8
4
|
const default_fps = 60;
|
|
9
5
|
const EndAction = {
|
|
10
6
|
pause: 0,
|
|
@@ -17,10 +13,10 @@ const EndAction = {
|
|
|
17
13
|
* @param duration
|
|
18
14
|
* @returns Object representing a range on a single-use, autoplaying Timeline
|
|
19
15
|
*/
|
|
20
|
-
function animate(duration) {
|
|
16
|
+
export function animate(duration) {
|
|
21
17
|
return new Timeline(true).range(0, duration);
|
|
22
18
|
}
|
|
23
|
-
class Timeline {
|
|
19
|
+
export class Timeline {
|
|
24
20
|
get currentTime() { return this._currentTime; }
|
|
25
21
|
set currentTime(v) {
|
|
26
22
|
this.seek(v);
|
|
@@ -109,7 +105,7 @@ class Timeline {
|
|
|
109
105
|
}
|
|
110
106
|
};
|
|
111
107
|
};
|
|
112
|
-
return new
|
|
108
|
+
return new TimelinePoint(addHandler, this, position);
|
|
113
109
|
}
|
|
114
110
|
range(start = 0, optionalDuration) {
|
|
115
111
|
const startPoint = typeof start == "number"
|
|
@@ -145,7 +141,7 @@ class Timeline {
|
|
|
145
141
|
}
|
|
146
142
|
};
|
|
147
143
|
};
|
|
148
|
-
return new
|
|
144
|
+
return new TimelineRange(addHandler, this, startPosition, duration);
|
|
149
145
|
}
|
|
150
146
|
getWrappedPosition(n) {
|
|
151
147
|
if (this.endAction.type !== EndAction.wrap)
|
|
@@ -239,7 +235,7 @@ class Timeline {
|
|
|
239
235
|
// filter ranges that overlap seeked range
|
|
240
236
|
if (Math.min(position, end) <= Math.max(to, fromTime)
|
|
241
237
|
&& Math.min(to, fromTime) <= Math.max(position, end)) {
|
|
242
|
-
let progress =
|
|
238
|
+
let progress = clamp((to - range.position) / range.duration, 0, 1);
|
|
243
239
|
range.handlers.slice().forEach(h => h(progress));
|
|
244
240
|
}
|
|
245
241
|
});
|
|
@@ -344,7 +340,6 @@ class Timeline {
|
|
|
344
340
|
return this._currentTime;
|
|
345
341
|
}
|
|
346
342
|
}
|
|
347
|
-
exports.Timeline = Timeline;
|
|
348
343
|
const sortEvents = (a, b) => {
|
|
349
344
|
return a.position - b.position;
|
|
350
345
|
};
|
package/internal/tween.d.ts
CHANGED
|
@@ -4,5 +4,8 @@ export type Tweenable = number | number[] | string | Blendable;
|
|
|
4
4
|
export interface Blendable {
|
|
5
5
|
blend(target: this, progress: number): this;
|
|
6
6
|
}
|
|
7
|
+
export interface BlendableWith<T, R> {
|
|
8
|
+
blend(target: R, progress: number): T;
|
|
9
|
+
}
|
|
7
10
|
/** @internal */
|
|
8
11
|
export declare function tweenValue<T extends Tweenable>(from: T, to: T, progress: number): T;
|
package/internal/tween.js
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.tweenValue = tweenValue;
|
|
4
|
-
const utils_1 = require("./utils");
|
|
1
|
+
import { clamp } from "./utils";
|
|
5
2
|
/** @internal */
|
|
6
|
-
function tweenValue(from, to, progress) {
|
|
3
|
+
export function tweenValue(from, to, progress) {
|
|
7
4
|
if (Array.isArray(from)) {
|
|
8
5
|
const toArr = to;
|
|
9
6
|
if (from.length != toArr.length)
|
|
@@ -89,7 +86,7 @@ function parseColour(code) {
|
|
|
89
86
|
function blendColours(from, to, bias) {
|
|
90
87
|
const fromColour = parseColour(from);
|
|
91
88
|
const toColour = parseColour(to);
|
|
92
|
-
const blended = fromColour.map((val, i) =>
|
|
89
|
+
const blended = fromColour.map((val, i) => clamp(blendNumbers(val, toColour[i], bias), 0, 255));
|
|
93
90
|
return ("#" + blended.map(n => Math.round(n).toString(16).padStart(2, "0")).join("")).replace(/ff$/, "");
|
|
94
91
|
}
|
|
95
92
|
const tweenableTokenRegex = /(#(?:[0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})\b|[-+]?\d*\.?\d+(?:[eE][-+]?\d+)?)/g;
|
package/internal/utils.js
CHANGED
|
@@ -1,6 +1,2 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.clamp = void 0;
|
|
4
1
|
/** @internal */
|
|
5
|
-
const clamp = (value, min, max) => Math.min(Math.max(value, min), max);
|
|
6
|
-
exports.clamp = clamp;
|
|
2
|
+
export const clamp = (value, min, max) => Math.min(Math.max(value, min), max);
|