@xtia/timeline 1.2.3 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +2 -1
- package/{internal → lib}/animate.js +1 -1
- package/{internal → lib}/driver.js +6 -6
- package/lib/index.d.ts +6 -0
- package/lib/index.js +5 -0
- package/{internal → lib}/path.d.ts +10 -1
- package/lib/path.js +150 -0
- package/{internal → lib}/point.d.ts +2 -2
- package/{internal → lib}/point.js +2 -2
- package/{internal → lib}/timeline.d.ts +1 -1
- package/{internal → lib}/timeline.js +5 -5
- package/{internal → lib}/tween.js +4 -0
- package/package.json +22 -11
- package/index.d.ts +0 -6
- package/index.js +0 -5
- package/internal/path.js +0 -85
- /package/{internal → lib}/animate.d.ts +0 -0
- /package/{internal → lib}/driver.d.ts +0 -0
- /package/{internal → lib}/easing.d.ts +0 -0
- /package/{internal → lib}/easing.js +0 -0
- /package/{internal → lib}/emitters.d.ts +0 -0
- /package/{internal → lib}/emitters.js +0 -0
- /package/{internal → lib}/range.d.ts +0 -0
- /package/{internal → lib}/range.js +0 -0
- /package/{internal → lib}/tween.d.ts +0 -0
- /package/{internal → lib}/utils.d.ts +0 -0
- /package/{internal → lib}/utils.js +0 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024-2026 Aleta Lovelace
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -342,7 +342,7 @@ await timeline.seek(timeline.end, 400, "overshootIn");
|
|
|
342
342
|
|
|
343
343
|
## Backward-compatibility
|
|
344
344
|
|
|
345
|
-
|
|
345
|
+
The previous API is present and expanded, but deprecated.
|
|
346
346
|
|
|
347
347
|
#### Breaking changes
|
|
348
348
|
|
|
@@ -363,6 +363,7 @@ Despite the massive overhaul, the previous API is present and expanded and upgra
|
|
|
363
363
|
* `timeline.position` will be replaced with `timeline.currentTime` to be consistent with other seekable concepts.
|
|
364
364
|
* `"loop"` endAction is now `"restart"` to disambiguate from new looping strategies.
|
|
365
365
|
* `timeline.step()` is redundant now that `currentTime` is writable; use `timeline.currentTime += delta` instead.
|
|
366
|
+
* The legacy API (`tl.tween()`, `tl.at()`, `tl.position`) will be removed in 2.*
|
|
366
367
|
|
|
367
368
|
|
|
368
369
|
|
|
@@ -4,7 +4,7 @@ export function animate(duration, looping = false) {
|
|
|
4
4
|
const durationMs = typeof duration == "number"
|
|
5
5
|
? duration
|
|
6
6
|
: duration.asMilliseconds;
|
|
7
|
-
if (durationMs === Infinity || durationMs
|
|
7
|
+
if (durationMs === Infinity || durationMs <= 0)
|
|
8
8
|
throw new RangeError("animate() duration must be positive and finite");
|
|
9
9
|
let t = 0;
|
|
10
10
|
if (looping) {
|
|
@@ -19,7 +19,7 @@ const createIntervalDriver = (tick) => {
|
|
|
19
19
|
};
|
|
20
20
|
};
|
|
21
21
|
export const masterDriver = (() => {
|
|
22
|
-
const
|
|
22
|
+
const subscriptions = new Map();
|
|
23
23
|
let previousTime = null;
|
|
24
24
|
let pause = null;
|
|
25
25
|
const stepAll = (currentTime) => {
|
|
@@ -29,7 +29,7 @@ export const masterDriver = (() => {
|
|
|
29
29
|
}
|
|
30
30
|
const delta = currentTime - previousTime;
|
|
31
31
|
previousTime = currentTime;
|
|
32
|
-
|
|
32
|
+
subscriptions.forEach((step, tl) => {
|
|
33
33
|
step(delta);
|
|
34
34
|
});
|
|
35
35
|
};
|
|
@@ -38,14 +38,14 @@ export const masterDriver = (() => {
|
|
|
38
38
|
: createIntervalDriver(stepAll);
|
|
39
39
|
return (stepFn) => {
|
|
40
40
|
const key = Symbol();
|
|
41
|
-
|
|
42
|
-
if (
|
|
41
|
+
subscriptions.set(key, stepFn);
|
|
42
|
+
if (subscriptions.size === 1) {
|
|
43
43
|
previousTime = null;
|
|
44
44
|
pause = start();
|
|
45
45
|
}
|
|
46
46
|
return () => {
|
|
47
|
-
|
|
48
|
-
if (
|
|
47
|
+
subscriptions.delete(key);
|
|
48
|
+
if (subscriptions.size === 0) {
|
|
49
49
|
pause();
|
|
50
50
|
}
|
|
51
51
|
};
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { Timeline, ChainingInterface } from "./timeline.js";
|
|
2
|
+
export { animate } from "./animate.js";
|
|
3
|
+
export { TimelinePoint, PointEvent } from "./point.js";
|
|
4
|
+
export { TimelineRange } from "./range.js";
|
|
5
|
+
export { type Emitter, type RangeProgression, UnsubscribeFunc } from "./emitters.js";
|
|
6
|
+
export { easers } from "./easing.js";
|
package/lib/index.js
ADDED
|
@@ -18,7 +18,16 @@ type CurveSegment = {
|
|
|
18
18
|
speed?: number;
|
|
19
19
|
ease?: Easer | keyof typeof easers;
|
|
20
20
|
};
|
|
21
|
-
type
|
|
21
|
+
type ArcSegment = {
|
|
22
|
+
type: "arc";
|
|
23
|
+
from?: XY;
|
|
24
|
+
to: XY;
|
|
25
|
+
radius?: number;
|
|
26
|
+
direction: "clockwise" | "anticlockwise";
|
|
27
|
+
speed?: number;
|
|
28
|
+
ease?: Easer | keyof typeof easers;
|
|
29
|
+
};
|
|
30
|
+
type StaticSegment = LineSegment | CurveSegment | ArcSegment;
|
|
22
31
|
type Segment = StaticSegment | CustomSegment;
|
|
23
32
|
type CustomSegment = {
|
|
24
33
|
get: SegmentEvaluator;
|
package/lib/path.js
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { createListenable } from "./emitters.js";
|
|
2
|
+
import { Timeline } from "./timeline.js";
|
|
3
|
+
export function createPathEmitter(input) {
|
|
4
|
+
const { listen, emit } = createListenable();
|
|
5
|
+
const tl = new Timeline();
|
|
6
|
+
const firstItem = input[0];
|
|
7
|
+
let getCurrentPosition;
|
|
8
|
+
let items;
|
|
9
|
+
if (Array.isArray(firstItem)) {
|
|
10
|
+
// first is XY - use it as starting position and exclude it from iteration
|
|
11
|
+
items = input.slice(1);
|
|
12
|
+
getCurrentPosition = () => firstItem;
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
items = input;
|
|
16
|
+
getCurrentPosition = () => [0, 0];
|
|
17
|
+
}
|
|
18
|
+
items.forEach(item => {
|
|
19
|
+
const speed = typeof item === 'object' && !Array.isArray(item) && "speed" in item ? item.speed ?? 1 : 1;
|
|
20
|
+
if (typeof item == "function") {
|
|
21
|
+
const length = estimateLength(item);
|
|
22
|
+
tl.end.range(length / speed)
|
|
23
|
+
.apply(v => emit(item(v)));
|
|
24
|
+
getCurrentPosition = () => item(1);
|
|
25
|
+
}
|
|
26
|
+
else if (Array.isArray(item)) { // XY
|
|
27
|
+
const start = getCurrentPosition();
|
|
28
|
+
const length = distance(start, item);
|
|
29
|
+
tl.end.range(length / speed)
|
|
30
|
+
.tween(start, item)
|
|
31
|
+
.apply(emit);
|
|
32
|
+
getCurrentPosition = () => item;
|
|
33
|
+
}
|
|
34
|
+
else if ("get" in item) { // custom segment
|
|
35
|
+
const length = item.length ?? estimateLength(item.get);
|
|
36
|
+
tl.end.range(length / speed)
|
|
37
|
+
.ease(item.ease)
|
|
38
|
+
.apply(v => emit(item.get(v)));
|
|
39
|
+
getCurrentPosition = () => item.get(1);
|
|
40
|
+
}
|
|
41
|
+
else
|
|
42
|
+
switch (item.type) { // static segment
|
|
43
|
+
case "line": {
|
|
44
|
+
const start = item.from ?? getCurrentPosition();
|
|
45
|
+
const length = distance(start, item.to);
|
|
46
|
+
tl.end.range(length / speed)
|
|
47
|
+
.ease(item.ease)
|
|
48
|
+
.tween(start, item.to)
|
|
49
|
+
.apply(emit);
|
|
50
|
+
getCurrentPosition = () => item.to;
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
case "curve": {
|
|
54
|
+
const start = item.from ?? getCurrentPosition();
|
|
55
|
+
const curve = createCurve(start, item.to, item.control1, item.control2);
|
|
56
|
+
const length = estimateLength(curve);
|
|
57
|
+
tl.end.range(length / speed)
|
|
58
|
+
.ease(item.ease)
|
|
59
|
+
.apply(v => emit(curve(v)));
|
|
60
|
+
getCurrentPosition = () => item.to;
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
case "arc": {
|
|
64
|
+
const start = getCurrentPosition();
|
|
65
|
+
const arc = createArc(start, item.to, item.radius, item.direction);
|
|
66
|
+
const length = estimateLength(arc);
|
|
67
|
+
tl.end.range(length / (item.speed ?? 1))
|
|
68
|
+
.ease(item.ease)
|
|
69
|
+
.apply(v => emit(arc(v)));
|
|
70
|
+
getCurrentPosition = () => item.to;
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
return { listen, seek: t => {
|
|
76
|
+
tl.seek(t * tl.end.position);
|
|
77
|
+
} };
|
|
78
|
+
}
|
|
79
|
+
function createCurve([startX, startY], [endX, endY], [control1x, control1y], [control2x, control2y]) {
|
|
80
|
+
return (t) => {
|
|
81
|
+
const ti = 1 - t;
|
|
82
|
+
const x = ti ** 3 * startX +
|
|
83
|
+
3 * ti ** 2 * t * control1x +
|
|
84
|
+
3 * ti * t ** 2 * control2x +
|
|
85
|
+
t ** 3 * endX;
|
|
86
|
+
const y = ti ** 3 * startY +
|
|
87
|
+
3 * ti ** 2 * t * control1y +
|
|
88
|
+
3 * ti * t ** 2 * control2y +
|
|
89
|
+
t ** 3 * endY;
|
|
90
|
+
return [x, y];
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
function estimateLength(curve, samples = 100) {
|
|
94
|
+
let length = 0;
|
|
95
|
+
let prev = curve(0);
|
|
96
|
+
for (let i = 1; i <= samples; i++) {
|
|
97
|
+
const t = i / samples;
|
|
98
|
+
const current = curve(t);
|
|
99
|
+
length += Math.sqrt((current[0] - prev[0]) ** 2 + (current[1] - prev[1]) ** 2);
|
|
100
|
+
prev = current;
|
|
101
|
+
}
|
|
102
|
+
return length;
|
|
103
|
+
}
|
|
104
|
+
const distance = (a, b) => Math.sqrt((b[0] - a[0]) ** 2 + (b[1] - a[1]) ** 2);
|
|
105
|
+
function createArc([startX, startY], [endX, endY], radius, direction = "clockwise") {
|
|
106
|
+
const dx = endX - startX;
|
|
107
|
+
const dy = endY - startY;
|
|
108
|
+
const chordLength = Math.sqrt(dx * dx + dy * dy);
|
|
109
|
+
if (chordLength < 0.0001) {
|
|
110
|
+
return _ => [startX, startY];
|
|
111
|
+
}
|
|
112
|
+
const r = radius ?? chordLength / 2;
|
|
113
|
+
const minRadius = chordLength / 2;
|
|
114
|
+
let effectiveRadius = Math.max(r, minRadius);
|
|
115
|
+
const halfChord = chordLength / 2;
|
|
116
|
+
let centreOffset = Math.sqrt(effectiveRadius * effectiveRadius - halfChord * halfChord);
|
|
117
|
+
if (isNaN(centreOffset)) {
|
|
118
|
+
effectiveRadius = minRadius;
|
|
119
|
+
centreOffset = 0;
|
|
120
|
+
}
|
|
121
|
+
const chordMidX = (startX + endX) / 2;
|
|
122
|
+
const chordMidY = (startY + endY) / 2;
|
|
123
|
+
const perpX = -dy / chordLength;
|
|
124
|
+
const perpY = dx / chordLength;
|
|
125
|
+
const sign = direction === "clockwise" ? 1 : -1;
|
|
126
|
+
const centerX = chordMidX + perpX * sign * centreOffset;
|
|
127
|
+
const centerY = chordMidY + perpY * sign * centreOffset;
|
|
128
|
+
const startAngle = Math.atan2(startY - centerY, startX - centerX);
|
|
129
|
+
const endAngle = Math.atan2(endY - centerY, endX - centerX);
|
|
130
|
+
let angleDiff = endAngle - startAngle;
|
|
131
|
+
if (direction === "clockwise") {
|
|
132
|
+
if (angleDiff > 0)
|
|
133
|
+
angleDiff -= Math.PI * 2;
|
|
134
|
+
if (angleDiff > -Math.PI)
|
|
135
|
+
angleDiff -= Math.PI * 2;
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
if (angleDiff < 0)
|
|
139
|
+
angleDiff += Math.PI * 2;
|
|
140
|
+
if (angleDiff < Math.PI)
|
|
141
|
+
angleDiff += Math.PI * 2;
|
|
142
|
+
}
|
|
143
|
+
return (t) => {
|
|
144
|
+
const clampedT = Math.max(0, Math.min(1, t));
|
|
145
|
+
const angle = startAngle + angleDiff * clampedT;
|
|
146
|
+
const x = centerX + effectiveRadius * Math.cos(angle);
|
|
147
|
+
const y = centerY + effectiveRadius * Math.sin(angle);
|
|
148
|
+
return [x, y];
|
|
149
|
+
};
|
|
150
|
+
}
|
|
@@ -46,13 +46,13 @@ export declare class TimelinePoint extends Emitter<PointEvent> {
|
|
|
46
46
|
*/
|
|
47
47
|
seek(duration: number, easer?: Easer): Promise<void>;
|
|
48
48
|
/**
|
|
49
|
-
*
|
|
49
|
+
* A point emitter that only emits on forward-moving seeks
|
|
50
50
|
* @returns Listenable: emits forward-seeking point events
|
|
51
51
|
*/
|
|
52
52
|
get forwardOnly(): Emitter<PointEvent>;
|
|
53
53
|
private _forwardOnly?;
|
|
54
54
|
/**
|
|
55
|
-
*
|
|
55
|
+
* A point emitter that only emits on backward-moving seeks
|
|
56
56
|
* @returns Listenable: emits backward-seeking point events
|
|
57
57
|
*/
|
|
58
58
|
get reverseOnly(): Emitter<PointEvent>;
|
|
@@ -41,7 +41,7 @@ export class TimelinePoint extends Emitter {
|
|
|
41
41
|
return this.timeline.seek(this.position, duration, easer);
|
|
42
42
|
}
|
|
43
43
|
/**
|
|
44
|
-
*
|
|
44
|
+
* A point emitter that only emits on forward-moving seeks
|
|
45
45
|
* @returns Listenable: emits forward-seeking point events
|
|
46
46
|
*/
|
|
47
47
|
get forwardOnly() {
|
|
@@ -50,7 +50,7 @@ export class TimelinePoint extends Emitter {
|
|
|
50
50
|
return this._forwardOnly;
|
|
51
51
|
}
|
|
52
52
|
/**
|
|
53
|
-
*
|
|
53
|
+
* A point emitter that only emits on backward-moving seeks
|
|
54
54
|
* @returns Listenable: emits backward-seeking point events
|
|
55
55
|
*/
|
|
56
56
|
get reverseOnly() {
|
|
@@ -196,7 +196,7 @@ export declare class Timeline {
|
|
|
196
196
|
* @deprecated Legacy API may be absent in a future major version
|
|
197
197
|
*/
|
|
198
198
|
at(position: number | TimelinePoint, action?: () => void, reverse?: boolean | (() => void)): ChainingInterface;
|
|
199
|
-
private
|
|
199
|
+
private chain;
|
|
200
200
|
/**
|
|
201
201
|
* @deprecated use `timeline.currentTime`
|
|
202
202
|
*/
|
|
@@ -394,7 +394,7 @@ export class Timeline {
|
|
|
394
394
|
? durationOrToPoint
|
|
395
395
|
: (durationOrToPoint.position - startPosition);
|
|
396
396
|
this.range(startPosition, duration).ease(easer).tween(from, to).apply(apply);
|
|
397
|
-
return this.
|
|
397
|
+
return this.chain(startPosition + duration);
|
|
398
398
|
}
|
|
399
399
|
/**
|
|
400
400
|
* Adds an event at a specific position
|
|
@@ -414,7 +414,7 @@ export class Timeline {
|
|
|
414
414
|
throw new Error("Invalid call");
|
|
415
415
|
point.reverseOnly.apply(reverse);
|
|
416
416
|
}
|
|
417
|
-
return this.
|
|
417
|
+
return this.chain(point.position);
|
|
418
418
|
}
|
|
419
419
|
if (reverse) {
|
|
420
420
|
if (reverse === true) {
|
|
@@ -427,9 +427,9 @@ export class Timeline {
|
|
|
427
427
|
else {
|
|
428
428
|
point.forwardOnly.apply(action);
|
|
429
429
|
}
|
|
430
|
-
return this.
|
|
430
|
+
return this.chain(point.position);
|
|
431
431
|
}
|
|
432
|
-
|
|
432
|
+
chain(position) {
|
|
433
433
|
const chain = {
|
|
434
434
|
thenTween: (duration, apply, from, to, easer) => {
|
|
435
435
|
return this.tween(position, duration, apply, from, to, easer);
|
|
@@ -437,7 +437,7 @@ export class Timeline {
|
|
|
437
437
|
then: (action) => this.at(position, action),
|
|
438
438
|
thenWait: (delay) => {
|
|
439
439
|
this.point(position + delay);
|
|
440
|
-
return this.
|
|
440
|
+
return this.chain(position + delay);
|
|
441
441
|
},
|
|
442
442
|
fork: fn => {
|
|
443
443
|
fn(chain);
|
|
@@ -50,6 +50,10 @@ function createStringTween(from, to) {
|
|
|
50
50
|
const prefix = chunk.prefix;
|
|
51
51
|
if (chunk.type === TokenTypes.none)
|
|
52
52
|
return () => prefix;
|
|
53
|
+
if (fromToken === toToken) {
|
|
54
|
+
const full = prefix + fromToken;
|
|
55
|
+
return () => full;
|
|
56
|
+
}
|
|
53
57
|
if (chunk.type === TokenTypes.colour) {
|
|
54
58
|
const fromColour = parseColour(fromToken);
|
|
55
59
|
const toColour = parseColour(toToken);
|
package/package.json
CHANGED
|
@@ -1,30 +1,41 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xtia/timeline",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
|
+
"description": "A general-purpose, environment-agnostic choreography engine",
|
|
4
5
|
"repository": {
|
|
5
6
|
"url": "https://github.com/tiadrop/timeline",
|
|
6
7
|
"type": "github"
|
|
7
8
|
},
|
|
8
|
-
"description": "A general-purpose, environment-agnostic choreography engine",
|
|
9
9
|
"sideEffects": false,
|
|
10
|
-
"
|
|
11
|
-
|
|
10
|
+
"files": [
|
|
11
|
+
"lib/"
|
|
12
|
+
],
|
|
13
|
+
"types": "./lib/index.d.ts",
|
|
14
|
+
"main": "./lib/index.js",
|
|
12
15
|
"exports": {
|
|
13
16
|
".": {
|
|
14
|
-
"types": "./index.d.ts",
|
|
15
|
-
"default": "./index.js"
|
|
17
|
+
"types": "./lib/index.d.ts",
|
|
18
|
+
"default": "./lib/index.js"
|
|
16
19
|
},
|
|
17
|
-
"./
|
|
20
|
+
"./lib/*": null
|
|
18
21
|
},
|
|
19
22
|
"scripts": {
|
|
20
|
-
"
|
|
21
|
-
"
|
|
23
|
+
"build": "tsc",
|
|
24
|
+
"test": "jest",
|
|
25
|
+
"prepublishOnly": "tsc"
|
|
22
26
|
},
|
|
27
|
+
"author": "Aleta Lovelace",
|
|
23
28
|
"keywords": [
|
|
24
29
|
"animation",
|
|
25
30
|
"timeline",
|
|
26
31
|
"choreography"
|
|
27
32
|
],
|
|
28
|
-
"
|
|
29
|
-
"
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/jest": "^30.0.0",
|
|
36
|
+
"@xtia/mezr": "^0.1.2",
|
|
37
|
+
"jest": "^30.2.0",
|
|
38
|
+
"ts-jest": "^29.4.5",
|
|
39
|
+
"typescript": "^5.9.3"
|
|
40
|
+
}
|
|
30
41
|
}
|
package/index.d.ts
DELETED
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
export { Timeline, ChainingInterface } from "./internal/timeline.js";
|
|
2
|
-
export { animate } from "./internal/animate.js";
|
|
3
|
-
export { TimelinePoint, PointEvent } from "./internal/point.js";
|
|
4
|
-
export { TimelineRange } from "./internal/range.js";
|
|
5
|
-
export { type Emitter, type RangeProgression, UnsubscribeFunc } from "./internal/emitters.js";
|
|
6
|
-
export { easers } from "./internal/easing.js";
|
package/index.js
DELETED
package/internal/path.js
DELETED
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
import { createListenable } from "./emitters.js";
|
|
2
|
-
import { Timeline } from "./timeline.js";
|
|
3
|
-
export function createPathEmitter(input) {
|
|
4
|
-
const { listen, emit } = createListenable();
|
|
5
|
-
const tl = new Timeline();
|
|
6
|
-
let lastXY = [0, 0];
|
|
7
|
-
const firstItem = input[0];
|
|
8
|
-
let getCurrentPosition;
|
|
9
|
-
let items;
|
|
10
|
-
if (Array.isArray(firstItem)) {
|
|
11
|
-
// first is XY - use it as starting position and exclude it from iteration
|
|
12
|
-
items = input.slice(1);
|
|
13
|
-
getCurrentPosition = () => firstItem;
|
|
14
|
-
}
|
|
15
|
-
else {
|
|
16
|
-
items = input;
|
|
17
|
-
getCurrentPosition = () => [0, 0];
|
|
18
|
-
}
|
|
19
|
-
items.forEach(item => {
|
|
20
|
-
const speed = typeof item === 'object' && !Array.isArray(item) && "speed" in item ? item.speed ?? 1 : 1;
|
|
21
|
-
if (typeof item == "function") {
|
|
22
|
-
const length = estimateLength(item);
|
|
23
|
-
tl.end.range(length / speed).apply(v => lastXY = item(v));
|
|
24
|
-
getCurrentPosition = () => item(1);
|
|
25
|
-
}
|
|
26
|
-
else if (Array.isArray(item)) { // XY
|
|
27
|
-
const start = getCurrentPosition();
|
|
28
|
-
const length = distance(start, item);
|
|
29
|
-
tl.end.range(length / speed).tween(start, item).apply(v => lastXY = v);
|
|
30
|
-
getCurrentPosition = () => item;
|
|
31
|
-
}
|
|
32
|
-
else if ("get" in item) { // custom segment
|
|
33
|
-
const length = item.length ?? estimateLength(item.get);
|
|
34
|
-
tl.end.range(length / speed).ease(item.ease).apply(v => lastXY = item.get(v));
|
|
35
|
-
getCurrentPosition = () => item.get(1);
|
|
36
|
-
}
|
|
37
|
-
else
|
|
38
|
-
switch (item.type) { // static segment
|
|
39
|
-
case "line": {
|
|
40
|
-
const start = item.from ?? getCurrentPosition();
|
|
41
|
-
const length = distance(start, item.to);
|
|
42
|
-
tl.end.range(length / speed).ease(item.ease).tween(start, item.to).apply(v => lastXY = v);
|
|
43
|
-
getCurrentPosition = () => item.to;
|
|
44
|
-
break;
|
|
45
|
-
}
|
|
46
|
-
case "curve": {
|
|
47
|
-
const start = item.from ?? getCurrentPosition();
|
|
48
|
-
const curve = createCurve(start, item.to, item.control1, item.control2);
|
|
49
|
-
const length = estimateLength(curve);
|
|
50
|
-
tl.end.range(length / speed).ease(item.ease).map(curve).apply(v => lastXY = v);
|
|
51
|
-
getCurrentPosition = () => item.to;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
});
|
|
55
|
-
return { listen, seek: t => {
|
|
56
|
-
tl.seek(t * tl.end.position);
|
|
57
|
-
emit(lastXY);
|
|
58
|
-
} };
|
|
59
|
-
}
|
|
60
|
-
function createCurve([startX, startY], [endX, endY], [control1x, control1y], [control2x, control2y]) {
|
|
61
|
-
return (t) => {
|
|
62
|
-
const ti = 1 - t;
|
|
63
|
-
const x = ti ** 3 * startX +
|
|
64
|
-
3 * ti ** 2 * t * control1x +
|
|
65
|
-
3 * ti * t ** 2 * control2x +
|
|
66
|
-
t ** 3 * endX;
|
|
67
|
-
const y = ti ** 3 * startY +
|
|
68
|
-
3 * ti ** 2 * t * control1y +
|
|
69
|
-
3 * ti * t ** 2 * control2y +
|
|
70
|
-
t ** 3 * endY;
|
|
71
|
-
return [x, y];
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
function estimateLength(curve, samples = 100) {
|
|
75
|
-
let length = 0;
|
|
76
|
-
let prev = curve(0);
|
|
77
|
-
for (let i = 1; i <= samples; i++) {
|
|
78
|
-
const t = i / samples;
|
|
79
|
-
const current = curve(t);
|
|
80
|
-
length += Math.sqrt((current[0] - prev[0]) ** 2 + (current[1] - prev[1]) ** 2);
|
|
81
|
-
prev = current;
|
|
82
|
-
}
|
|
83
|
-
return length;
|
|
84
|
-
}
|
|
85
|
-
const distance = (a, b) => Math.sqrt((b[0] - a[0]) ** 2 + (b[1] - a[1]) ** 2);
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|