@xtia/timeline 1.1.4 → 1.1.6

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 CHANGED
@@ -464,6 +464,26 @@ timeline
464
464
 
465
465
  Returns a [`ChainingInterface`](#chaininginterface-interface) representing the point at which the tween ends.
466
466
 
467
+ ##### `apply(handler)`
468
+
469
+ Registers a handler to be invoked on every seek, after points and ranges are applied.
470
+
471
+ This is useful for systems that use Timeline's point and range emissions to manipulate state that is to be applied *at once* to another system.
472
+
473
+ ```ts
474
+ // don't wastefully render the scene for every entity update
475
+ timeline
476
+ .range(0, 1000)
477
+ .tween(10, 30)
478
+ .apply(v => scene.hero.x = v);
479
+ timeline
480
+ .range(500, 1000)
481
+ .tween(15, 50)
482
+ .apply(v => scene.monster.x = v);
483
+ // render when all updates for a frame are done:
484
+ timeline.apply(() => renderScene(scene));
485
+ ```
486
+
467
487
  ##### `tween<T>(start, end, apply, from, to, easer?): `[`ChainingInterface`](#chaininginterface-interface)
468
488
 
469
489
  As above, but if the second argument is a [`TimelinePoint`](#timelinepoint-class), it will specify when on the Timeline the tween will *end*.
@@ -68,11 +68,10 @@ export declare class Emitter<T> {
68
68
  * ```ts
69
69
  * range
70
70
  * .tween("0%", "100%")
71
- * .fork(branch => {
72
- * branch
71
+ * .fork(branch => branch
73
72
  * .map(s => `Loading: ${s}`)
74
73
  * .apply(s => document.title = s)
75
- * })
74
+ * )
76
75
  * .apply(v => progressBar.style.width = v);
77
76
  * ```
78
77
  * @param cb
@@ -216,4 +215,8 @@ export declare class RangeProgression extends Emitter<number> {
216
215
  */
217
216
  offset(delta: number): RangeProgression;
218
217
  }
218
+ export declare function createListenable<T>(onAddFirst?: () => void, onRemoveLast?: () => void): {
219
+ listen: (fn: (v: T) => void) => UnsubscribeFunc;
220
+ emit: (value: T) => void;
221
+ };
219
222
  export {};
@@ -90,26 +90,15 @@ export class Emitter {
90
90
  * @returns A new emitter that forwards all values from the parent, invoking `cb` as a side effect.
91
91
  */
92
92
  tap(cb) {
93
- const listeners = [];
94
93
  let parentUnsubscribe = null;
95
- const tappedListen = (handler) => {
96
- listeners.push(handler);
97
- if (listeners.length === 1) {
98
- parentUnsubscribe = this.onListen(value => {
99
- cb(value);
100
- listeners.slice().forEach(fn => fn(value));
101
- });
102
- }
103
- return () => {
104
- const idx = listeners.indexOf(handler);
105
- listeners.splice(idx, 1);
106
- if (listeners.length === 0 && parentUnsubscribe) {
107
- parentUnsubscribe();
108
- parentUnsubscribe = null;
109
- }
110
- };
111
- };
112
- return this.redirect(tappedListen);
94
+ const { emit, listen } = createListenable(() => parentUnsubscribe = this.onListen(value => {
95
+ cb(value);
96
+ emit(value);
97
+ }), () => {
98
+ parentUnsubscribe();
99
+ parentUnsubscribe = null;
100
+ });
101
+ return this.redirect(listen);
113
102
  }
114
103
  /**
115
104
  * Immediately passes this emitter to a callback and returns this emitter
@@ -120,11 +109,10 @@ export class Emitter {
120
109
  * ```ts
121
110
  * range
122
111
  * .tween("0%", "100%")
123
- * .fork(branch => {
124
- * branch
112
+ * .fork(branch => branch
125
113
  * .map(s => `Loading: ${s}`)
126
114
  * .apply(s => document.title = s)
127
- * })
115
+ * )
128
116
  * .apply(v => progressBar.style.width = v);
129
117
  * ```
130
118
  * @param cb
@@ -280,3 +268,24 @@ export class RangeProgression extends Emitter {
280
268
  return new RangeProgression(handler => this.onListen(value => handler((value + delta) % 1)));
281
269
  }
282
270
  }
271
+ export function createListenable(onAddFirst, onRemoveLast) {
272
+ const handlers = [];
273
+ const addListener = (fn) => {
274
+ const unique = (v) => fn(v);
275
+ handlers.push(unique);
276
+ if (onAddFirst && handlers.length == 1)
277
+ onAddFirst();
278
+ return () => {
279
+ const idx = handlers.indexOf(unique);
280
+ if (idx === -1)
281
+ throw new Error("Handler already unsubscribed");
282
+ handlers.splice(idx, 1);
283
+ if (onRemoveLast && handlers.length == 0)
284
+ onRemoveLast();
285
+ };
286
+ };
287
+ return {
288
+ listen: addListener,
289
+ emit: (value) => handlers.forEach(h => h(value)),
290
+ };
291
+ }
@@ -4,18 +4,20 @@ import { TimelinePoint } from "./point";
4
4
  import { Timeline } from "./timeline";
5
5
  export declare class TimelineRange extends RangeProgression {
6
6
  private timeline;
7
- private startPosition;
8
- /** The duration of this range */
9
- readonly duration: number;
10
- private endPosition;
11
7
  /** The point on the Timeline at which this range begins */
12
8
  readonly start: TimelinePoint;
13
9
  /** The point on the Timeline at which this range ends */
14
10
  readonly end: TimelinePoint;
15
- /** @internal Manual construction of RangeProgression is outside of the API contract and subject to undocumented change */
16
- constructor(onListen: ListenFunc<number>, timeline: Timeline, startPosition: number,
11
+ private startPosition;
12
+ private endPosition;
17
13
  /** The duration of this range */
18
- duration: number);
14
+ readonly duration: number;
15
+ /** @internal Manual construction of RangeProgression is outside of the API contract and subject to undocumented change */
16
+ constructor(onListen: ListenFunc<number>, timeline: Timeline,
17
+ /** The point on the Timeline at which this range begins */
18
+ start: TimelinePoint,
19
+ /** The point on the Timeline at which this range ends */
20
+ end: TimelinePoint);
19
21
  protected redirect(listen: ListenFunc<number>): TimelineRange;
20
22
  /**
21
23
  * Creates two ranges by seperating one at a given point
package/internal/range.js CHANGED
@@ -2,23 +2,21 @@ import { RangeProgression } from "./emitters";
2
2
  import { TimelinePoint } from "./point";
3
3
  export class TimelineRange extends RangeProgression {
4
4
  /** @internal Manual construction of RangeProgression is outside of the API contract and subject to undocumented change */
5
- constructor(onListen, timeline, startPosition,
6
- /** The duration of this range */
7
- duration) {
8
- super(duration == 0
9
- ? () => {
10
- throw new Error("Zero-duration ranges may not be listened");
11
- }
12
- : onListen);
5
+ constructor(onListen, timeline,
6
+ /** The point on the Timeline at which this range begins */
7
+ start,
8
+ /** The point on the Timeline at which this range ends */
9
+ end) {
10
+ super(onListen);
13
11
  this.timeline = timeline;
14
- this.startPosition = startPosition;
15
- this.duration = duration;
16
- this.endPosition = startPosition + duration;
17
- this.end = timeline.point(this.endPosition);
18
- this.start = timeline.point(startPosition);
12
+ this.start = start;
13
+ this.end = end;
14
+ this.startPosition = start.position;
15
+ this.endPosition = end.position;
16
+ this.duration = this.endPosition - this.startPosition;
19
17
  }
20
18
  redirect(listen) {
21
- return new TimelineRange(listen, this.timeline, this.startPosition, this.duration);
19
+ return new TimelineRange(listen, this.timeline, this.start, this.end);
22
20
  }
23
21
  /**
24
22
  * Creates two ranges by seperating one at a given point
@@ -28,6 +26,9 @@ export class TimelineRange extends RangeProgression {
28
26
  * @returns Tuple of two ranges
29
27
  */
30
28
  bisect(position = this.duration / 2) {
29
+ if (position >= this.endPosition) {
30
+ throw new RangeError("Bisection position is beyond end of range");
31
+ }
31
32
  return [
32
33
  this.timeline.range(this.startPosition, position),
33
34
  this.timeline.range(position + this.startPosition, this.duration - position),
@@ -1,5 +1,5 @@
1
1
  import { Easer, easers } from "./easing";
2
- import { RangeProgression } from "./emitters";
2
+ import { RangeProgression, UnsubscribeFunc } from "./emitters";
3
3
  import { TimelinePoint } from "./point";
4
4
  import { TimelineRange } from "./range";
5
5
  import { Tweenable } from "./tween";
@@ -41,13 +41,15 @@ export declare class Timeline {
41
41
  private smoothSeeker;
42
42
  private seeking;
43
43
  readonly start: TimelinePoint;
44
- private progressionHandlers;
44
+ private _frameEvents;
45
+ /**
46
+ * Registers a handler to be invoked on every seek, after points and ranges are applied
47
+ */
48
+ apply(handler: () => void): UnsubscribeFunc;
45
49
  private _progression;
46
50
  /**
47
- * Listenable: emits a progression value (0..1) when the Timeline's internal
48
- * position changes, and when the Timeline's total duration is extended
49
- *
50
- * **Experimental**
51
+ * Listenable: emits a progression value (0..1), representing progression through the entire Timeline,
52
+ * when the Timeline's internal position changes, and when the Timeline's total duration is extended
51
53
  */
52
54
  get progression(): RangeProgression;
53
55
  constructor();
@@ -100,10 +102,9 @@ export declare class Timeline {
100
102
  */
101
103
  range(start: number | TimelinePoint, duration: number): TimelineRange;
102
104
  /**
103
- * Creates an observable range from position 0 to the Timeline's **current** final position
105
+ * Defines a range from position 0 to the Timeline's **current** final position
104
106
  */
105
107
  range(): TimelineRange;
106
- private getWrappedPosition;
107
108
  /**
108
109
  * Seeks the Timeline to a specified position, triggering in order any point and range subscriptions between its current and new positions
109
110
  * @param toPosition
@@ -121,6 +122,7 @@ export declare class Timeline {
121
122
  seek(toPosition: number | TimelinePoint, durationMs: number, easer?: Easer | keyof typeof easers): Promise<void>;
122
123
  seek(toPosition: number | TimelinePoint, duration: Period, easer?: Easer | keyof typeof easers): Promise<void>;
123
124
  private seekDirect;
125
+ private seekWrapped;
124
126
  private seekPoints;
125
127
  private seekRanges;
126
128
  private sortEntries;
@@ -1,4 +1,4 @@
1
- import { RangeProgression } from "./emitters";
1
+ import { createListenable, RangeProgression } from "./emitters";
2
2
  import { TimelinePoint } from "./point";
3
3
  import { TimelineRange } from "./range";
4
4
  import { clamp } from "./utils";
@@ -27,15 +27,31 @@ export class Timeline {
27
27
  return this.point(this._endPosition);
28
28
  }
29
29
  /**
30
- * Listenable: emits a progression value (0..1) when the Timeline's internal
31
- * position changes, and when the Timeline's total duration is extended
32
- *
33
- * **Experimental**
30
+ * Registers a handler to be invoked on every seek, after points and ranges are applied
31
+ */
32
+ apply(handler) {
33
+ if (this._frameEvents === null) {
34
+ const { emit, listen } = createListenable();
35
+ this._frameEvents = {
36
+ listen,
37
+ emit,
38
+ };
39
+ }
40
+ return this._frameEvents.listen(handler);
41
+ }
42
+ /**
43
+ * Listenable: emits a progression value (0..1), representing progression through the entire Timeline,
44
+ * when the Timeline's internal position changes, and when the Timeline's total duration is extended
34
45
  */
35
46
  get progression() {
36
- if (this._progression === null)
37
- this._progression = new TimelineProgressionEmitter(this.progressionHandlers);
38
- return this._progression;
47
+ if (this._progression === null) {
48
+ const { emit, listen } = createListenable();
49
+ this._progression = {
50
+ emitter: new TimelineProgressionEmitter(listen),
51
+ emit,
52
+ };
53
+ }
54
+ return this._progression.emitter;
39
55
  }
40
56
  constructor(autoplay = false, endAction = "pause") {
41
57
  /**
@@ -53,7 +69,7 @@ export class Timeline {
53
69
  this.smoothSeeker = null;
54
70
  this.seeking = false;
55
71
  this.start = this.point(0);
56
- this.progressionHandlers = [];
72
+ this._frameEvents = null;
57
73
  this._progression = null;
58
74
  if (endAction == "loop")
59
75
  endAction = "restart";
@@ -91,32 +107,25 @@ export class Timeline {
91
107
  point(position) {
92
108
  if (position > this._endPosition) {
93
109
  this._endPosition = position;
94
- this.progressionHandlers.slice().forEach(h => h(this._currentTime / position));
110
+ this._progression?.emit(this._currentTime / position);
95
111
  }
96
- const handlers = [];
97
- const data = {
98
- handlers,
99
- position,
100
- };
112
+ const { emit, listen } = createListenable(() => this.points.push(data), () => {
113
+ const idx = this.points.indexOf(data);
114
+ this.points.splice(idx, 1);
115
+ });
101
116
  const addHandler = (handler) => {
102
117
  if (this.seeking)
103
118
  throw new Error("Can't add a listener while seeking");
104
- // we're adding and removing points and ranges to the internal registry according to whether any subscriptions are active, to allow obsolete points and ranges to be garbage-collected
105
- if (handlers.length == 0) {
106
- this.points.push(data);
107
- this.currentSortDirection = 0;
119
+ if (position == this._currentTime) {
120
+ emit({
121
+ direction: 1
122
+ });
108
123
  }
109
- handlers.push(handler);
110
- return () => {
111
- const idx = handlers.indexOf(handler);
112
- if (idx === -1)
113
- throw new Error("Internal error: attempting to remove a non-present handler");
114
- handlers.splice(idx, 1);
115
- if (handlers.length == 0) {
116
- const idx = this.points.indexOf(data);
117
- this.points.splice(idx, 1);
118
- }
119
- };
124
+ return listen(handler);
125
+ };
126
+ const data = {
127
+ emit,
128
+ position,
120
129
  };
121
130
  return new TimelinePoint(addHandler, this, position);
122
131
  }
@@ -126,62 +135,36 @@ export class Timeline {
126
135
  : start;
127
136
  const startPosition = startPoint.position;
128
137
  const duration = optionalDuration ?? this._endPosition - startPosition;
129
- // const endPosition = startPosition + duration;
130
- //if (endPosition > this._endPosition) this._endPosition = endPosition;
131
- // ^ leave this to range's point() calls
132
- const handlers = [];
138
+ const endPoint = this.point(startPosition + duration);
139
+ const { emit, listen } = createListenable(() => this.ranges.push(rangeData), () => {
140
+ const idx = this.ranges.indexOf(rangeData);
141
+ this.ranges.splice(idx, 1);
142
+ });
133
143
  const rangeData = {
134
144
  position: startPosition,
135
145
  duration,
136
- handlers,
146
+ emit,
137
147
  };
138
- const addHandler = (handler) => {
139
- if (this.seeking)
140
- throw new Error("Can't add a listener while seeking");
141
- if (handlers.length == 0) {
142
- this.ranges.push(rangeData);
143
- this.currentSortDirection = 0;
144
- }
145
- handlers.push(handler);
146
- // if currentTime is in this range, apply immediately
147
- if (range.contains(this._currentTime)) {
148
- let progress = clamp((this._currentTime - startPosition) / duration, 0, 1);
149
- handler(progress);
148
+ const addHandler = duration == 0
149
+ ? () => {
150
+ throw new Error("Zero-duration ranges may not be listened");
150
151
  }
151
- return () => {
152
- const idx = handlers.indexOf(handler);
153
- if (idx === -1)
154
- throw new Error("Internal error: attempting to remove a non-present handler");
155
- handlers.splice(idx, 1);
156
- if (handlers.length == 0) {
157
- const idx = this.ranges.indexOf(rangeData);
158
- this.ranges.splice(idx, 1);
152
+ : (handler) => {
153
+ if (this.seeking)
154
+ throw new Error("Can't add a listener while seeking");
155
+ if (range.contains(this._currentTime)) {
156
+ let progress = clamp((this._currentTime - startPosition) / duration, 0, 1);
157
+ handler(progress);
159
158
  }
159
+ return listen(handler);
160
160
  };
161
- };
162
- const range = new TimelineRange(addHandler, this, startPosition, duration);
161
+ const range = new TimelineRange(addHandler, this, startPoint, endPoint);
163
162
  return range;
164
163
  }
165
- getWrappedPosition(n) {
166
- if (this.endAction.type !== EndAction.wrap)
167
- return n;
168
- const wrapAt = this.endAction.at?.position ?? 0;
169
- if (wrapAt == 0)
170
- return n % this._endPosition;
171
- if (n <= this._endPosition)
172
- return n;
173
- const loopStart = wrapAt;
174
- const segment = this._endPosition - loopStart;
175
- if (segment <= 0)
176
- return Math.min(n, this._endPosition);
177
- const overflow = n - this._endPosition;
178
- const remainder = overflow % segment;
179
- return loopStart + remainder;
180
- }
181
- seek(to, duration = 0, easer) {
182
- const durationMs = typeof duration == "number"
183
- ? duration
184
- : duration.asMilliseconds;
164
+ seek(to, duration, easer) {
165
+ const durationMs = typeof duration == "object"
166
+ ? duration.asMilliseconds
167
+ : duration;
185
168
  const toPosition = typeof to == "number"
186
169
  ? to
187
170
  : to.position;
@@ -192,21 +175,24 @@ export class Timeline {
192
175
  this.smoothSeeker.pause();
193
176
  // ensure any awaits are resolved for the interrupted seek
194
177
  const interruptPosition = this._currentTime;
195
- this.smoothSeeker.seek(this.smoothSeeker.end);
178
+ this.smoothSeeker.seekDirect(this.smoothSeeker.end.position);
196
179
  this.smoothSeeker = null;
197
180
  // and jump back to where we were interrupted
198
- this.seek(interruptPosition);
181
+ this.seekDirect(interruptPosition);
199
182
  }
200
- if (durationMs === 0) {
183
+ if (!durationMs) {
184
+ const fromTime = this._currentTime;
201
185
  this.seekDirect(toPosition);
202
- return Promise.resolve();
186
+ this._frameEvents?.emit();
187
+ // only add Promise overhead if duration is explicitly 0
188
+ return durationMs === 0 ? Promise.resolve() : undefined;
203
189
  }
204
190
  const seeker = new Timeline(true);
205
191
  this.smoothSeeker = seeker;
206
192
  seeker
207
193
  .range(0, durationMs)
208
194
  .ease(easer)
209
- .tween(this.currentTime, toPosition)
195
+ .tween(this._currentTime, toPosition)
210
196
  .apply(v => this.seekDirect(v));
211
197
  return seeker.end.promise();
212
198
  }
@@ -214,31 +200,65 @@ export class Timeline {
214
200
  const fromPosition = this._currentTime;
215
201
  if (toPosition === fromPosition)
216
202
  return;
217
- const loopingTo = this.getWrappedPosition(toPosition);
218
- const loopingFrom = this.getWrappedPosition(fromPosition);
219
- let virtualFrom = loopingFrom;
220
- let virtualTo = loopingTo;
221
203
  const direction = toPosition > fromPosition ? 1 : -1;
222
204
  if (direction !== this.currentSortDirection)
223
205
  this.sortEntries(direction);
224
- if (direction === 1 && loopingTo < loopingFrom) {
225
- virtualFrom = loopingFrom - this._endPosition;
226
- }
227
- else if (direction === -1 && loopingTo > loopingFrom) {
228
- virtualFrom = loopingFrom + this._endPosition;
229
- }
230
206
  this.seeking = true;
231
- this._currentTime = virtualFrom;
232
207
  try {
233
- this.seekPoints(virtualTo);
234
- this.seekRanges(virtualTo);
208
+ // use wrapping logic?
209
+ if (this.endAction.type === EndAction.wrap && (fromPosition > this._endPosition || toPosition > this._endPosition)) {
210
+ this.seekWrapped(toPosition);
211
+ }
212
+ else {
213
+ this.seekPoints(toPosition);
214
+ this.seekRanges(toPosition);
215
+ }
235
216
  }
236
217
  catch (e) {
237
218
  this.pause();
238
219
  throw e;
239
220
  }
221
+ finally {
222
+ this.seeking = false;
223
+ }
224
+ this._currentTime = toPosition;
225
+ }
226
+ seekWrapped(toPosition) {
227
+ const fromPosition = this._currentTime;
228
+ const timelineEnd = this._endPosition;
229
+ const wrapAt = "at" in this.endAction ? this.endAction.at.position : 0;
230
+ const loopLen = timelineEnd - wrapAt;
231
+ const getWrappedPosition = (pos) => ((pos - wrapAt) % loopLen + loopLen) % loopLen + wrapAt;
232
+ const realDelta = toPosition - fromPosition;
233
+ const direction = realDelta >= 0 ? 1 : -1;
234
+ let remaining = Math.abs(realDelta);
235
+ let virtualFrom = getWrappedPosition(fromPosition);
236
+ while (remaining > 0) {
237
+ let virtualTo;
238
+ if (direction > 0) {
239
+ const wrapSize = timelineEnd - virtualFrom;
240
+ virtualTo = remaining <= wrapSize
241
+ ? virtualFrom + remaining
242
+ : timelineEnd;
243
+ }
244
+ else {
245
+ const wrapSize = virtualFrom - wrapAt;
246
+ virtualTo = remaining <= wrapSize
247
+ ? virtualFrom - remaining
248
+ : wrapAt;
249
+ }
250
+ this._currentTime = virtualFrom;
251
+ this.seekPoints(virtualTo);
252
+ remaining -= Math.abs(virtualTo - virtualFrom);
253
+ if (remaining > 0) {
254
+ virtualFrom = direction > 0 ? wrapAt : timelineEnd;
255
+ }
256
+ else {
257
+ virtualFrom = virtualTo;
258
+ }
259
+ }
260
+ this.seekRanges(getWrappedPosition(toPosition));
240
261
  this._currentTime = toPosition;
241
- this.seeking = false;
242
262
  }
243
263
  seekPoints(to) {
244
264
  const from = this._currentTime;
@@ -252,7 +272,7 @@ export class Timeline {
252
272
  pointsBetween.slice().forEach(p => {
253
273
  this.seekRanges(p.position);
254
274
  this._currentTime = p.position;
255
- p.handlers.slice().forEach(h => h(eventData));
275
+ p.emit(eventData);
256
276
  });
257
277
  }
258
278
  seekRanges(to) {
@@ -265,10 +285,10 @@ export class Timeline {
265
285
  const overlaps = fromTime <= rangeEnd && toTime >= range.position;
266
286
  if (overlaps) {
267
287
  let progress = clamp((to - range.position) / range.duration, 0, 1);
268
- range.handlers.slice().forEach(h => h(progress));
288
+ range.emit(progress);
269
289
  }
270
290
  });
271
- this.progressionHandlers.slice().forEach(h => h(toTime / this._endPosition));
291
+ this._progression?.emit(toTime / this._endPosition);
272
292
  }
273
293
  sortEntries(direction) {
274
294
  this.currentSortDirection = direction;
@@ -391,15 +411,8 @@ export class Timeline {
391
411
  }
392
412
  }
393
413
  class TimelineProgressionEmitter extends RangeProgression {
394
- constructor(handlers) {
395
- super((handler) => {
396
- const unique = (n) => handler(n);
397
- handlers.push(unique);
398
- return () => {
399
- const idx = handlers.indexOf(unique);
400
- handlers.splice(idx, 1);
401
- };
402
- });
414
+ constructor(listen) {
415
+ super(listen);
403
416
  }
404
417
  }
405
418
  const sortEvents = (a, b) => {
package/internal/tween.js CHANGED
@@ -19,8 +19,11 @@ export function createTween(from, to) {
19
19
  switch (typeof from) {
20
20
  case "number": return progress => blendNumbers(from, to, progress);
21
21
  case "object": {
22
- if (from instanceof Date)
23
- return progress => new Date(blendNumbers(from.getTime(), to.getTime(), progress));
22
+ if (from instanceof Date) {
23
+ const fromStamp = from.getTime();
24
+ const toStamp = to.getTime();
25
+ return progress => new Date(blendNumbers(fromStamp, toStamp, progress));
26
+ }
24
27
  return progress => from.blend(to, progress);
25
28
  }
26
29
  case "string": return createStringTween(from, to);
@@ -30,9 +33,9 @@ export function createTween(from, to) {
30
33
  function createStringTween(from, to) {
31
34
  const fromChunks = tokenise(from);
32
35
  const toChunks = tokenise(to);
33
- const tokenCount = fromChunks.filter(c => c.token).length;
34
- // where length mismatch, use merging
35
- if (tokenCount !== toChunks.filter(c => c.token).length) {
36
+ const tokenCount = fromChunks.length;
37
+ // where token count mismatch, use merging
38
+ if (tokenCount !== toChunks.length) {
36
39
  return createStringMerge(from, to);
37
40
  }
38
41
  // where token prefix/type mismatch, use merging
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xtia/timeline",
3
- "version": "1.1.4",
3
+ "version": "1.1.6",
4
4
  "repository": {
5
5
  "url": "https://github.com/tiadrop/timeline",
6
6
  "type": "github"
@@ -22,7 +22,8 @@
22
22
  },
23
23
  "keywords": [
24
24
  "animation",
25
- "timeline"
25
+ "timeline",
26
+ "choreography"
26
27
  ],
27
28
  "author": "Aleta Lovelace",
28
29
  "license": "MIT"