@xtia/timeline 1.0.9 → 1.0.11

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.
@@ -1,5 +1,5 @@
1
1
  import { easers } from "./easing";
2
- import { tweenValue } from "./tween";
2
+ import { createTween } from "./tween";
3
3
  import { clamp } from "./utils";
4
4
  export class Emitter {
5
5
  constructor(onListen) {
@@ -138,7 +138,8 @@ export class RangeProgression extends Emitter {
138
138
  })) : h => this.onListen(h));
139
139
  }
140
140
  tween(from, to) {
141
- return new Emitter(handler => this.onListen(progress => handler(tweenValue(from, to, progress))));
141
+ const tween = createTween(from, to);
142
+ return new Emitter(handler => this.onListen(progress => handler(tween(progress))));
142
143
  }
143
144
  /**
144
145
  * Creates a chainable progress emitter that quantises progress, as emitted by its parent, to the nearest of `steps` discrete values.
@@ -1,3 +1,4 @@
1
+ import { Easer } from "./easing";
1
2
  import { Emitter, ListenFunc } from "./emitters";
2
3
  import { TimelineRange } from "./range";
3
4
  import { Timeline } from "./timeline";
@@ -35,4 +36,6 @@ export declare class TimelinePoint extends Emitter<PointEvent> {
35
36
  * @returns Listenable: emits a PointEvent when the point is reached or passed by a Timeline seek
36
37
  */
37
38
  delta(timeOffset: number): TimelinePoint;
39
+ seek(): void;
40
+ seek(duration?: number, easer?: Easer): void;
38
41
  }
package/internal/point.js CHANGED
@@ -38,4 +38,7 @@ export class TimelinePoint extends Emitter {
38
38
  delta(timeOffset) {
39
39
  return this.timeline.point(this.position + timeOffset);
40
40
  }
41
+ seek(duration = 0, easer) {
42
+ this.timeline.seek(this.position, duration, easer);
43
+ }
41
44
  }
package/internal/range.js CHANGED
@@ -6,7 +6,11 @@ export class TimelineRange extends RangeProgression {
6
6
  constructor(onListen, timeline, startPosition,
7
7
  /** The duration of this range */
8
8
  duration) {
9
- super(onListen);
9
+ super(duration == 0
10
+ ? () => {
11
+ throw new Error("Zero-duration ranges may not be listened");
12
+ }
13
+ : onListen);
10
14
  this.timeline = timeline;
11
15
  this.startPosition = startPosition;
12
16
  this.duration = duration;
@@ -97,7 +101,6 @@ export class TimelineRange extends RangeProgression {
97
101
  const [start, end] = range instanceof TimelineRange
98
102
  ? [range.startPosition, range.endPosition]
99
103
  : [range.position, range.position + range.duration];
100
- return Math.min(this.startPosition, this.endPosition) <= Math.max(start, end) &&
101
- Math.max(this.startPosition, this.endPosition) >= Math.min(start, end);
104
+ return this.startPosition <= end && this.endPosition >= start;
102
105
  }
103
106
  }
@@ -189,7 +189,7 @@ export class Timeline {
189
189
  }
190
190
  if (duration === 0) {
191
191
  this.seekDirect(toPosition);
192
- return;
192
+ return Promise.resolve();
193
193
  }
194
194
  const seeker = new Timeline(true);
195
195
  this.smoothSeeker = seeker;
@@ -242,15 +242,17 @@ export class Timeline {
242
242
  });
243
243
  }
244
244
  seekRanges(to) {
245
- const seekRange = this.point(Math.min(this._currentTime, to))
246
- .to(Math.max(this._currentTime, to));
245
+ const fromTime = Math.min(this._currentTime, to);
246
+ const toTime = Math.max(this._currentTime, to);
247
247
  this.ranges.slice().forEach((range) => {
248
- if (seekRange.overlaps(range)) {
248
+ const rangeEnd = range.position + range.duration;
249
+ const overlaps = fromTime <= rangeEnd && toTime >= range.position;
250
+ if (overlaps) {
249
251
  let progress = clamp((to - range.position) / range.duration, 0, 1);
250
252
  range.handlers.slice().forEach(h => h(progress));
251
253
  }
252
254
  });
253
- this.progressionHandlers.slice().forEach(h => h(this._currentTime / this._endPosition));
255
+ this.progressionHandlers.slice().forEach(h => h(fromTime / this._endPosition));
254
256
  }
255
257
  sortEntries(direction) {
256
258
  this.currentSortDirection = direction;
@@ -1,5 +1,5 @@
1
1
  /** @internal */
2
- export type Tweenable = number | number[] | string | Blendable;
2
+ export type Tweenable = number | number[] | string | string[] | Blendable | Blendable[];
3
3
  /** @internal */
4
4
  export interface Blendable {
5
5
  blend(target: this, progress: number): this;
@@ -7,5 +7,5 @@ export interface Blendable {
7
7
  export interface BlendableWith<T, R> {
8
8
  blend(target: R, progress: number): T;
9
9
  }
10
- /** @internal */
11
- export declare function tweenValue<T extends Tweenable>(from: T, to: T, progress: number): T;
10
+ export declare function createTween<T extends Tweenable>(from: T, to: T): ((progress: number) => T);
11
+ export declare function createTween<T extends BlendableWith<T, R>, R>(from: T, to: R): ((progress: number) => T);
package/internal/tween.js CHANGED
@@ -1,45 +1,76 @@
1
1
  import { clamp } from "./utils";
2
- /** @internal */
3
- export function tweenValue(from, to, progress) {
2
+ export function createTween(from, to) {
3
+ if (from === to)
4
+ return () => from;
4
5
  if (Array.isArray(from)) {
5
- const toArr = to;
6
- if (from.length != toArr.length)
6
+ if (from.length != to.length) {
7
7
  throw new Error("Array size mismatch");
8
- return from.map((v, i) => tweenValue(v, toArr[i], progress));
8
+ }
9
+ const tweens = from.map((f, i) => createTween(f, to[i]));
10
+ return progress => tweens.map(t => t(progress));
9
11
  }
10
- if (typeof from == "string") {
11
- return blendStrings(from, to, progress);
12
+ switch (typeof from) {
13
+ case "number": return progress => blendNumbers(from, to, progress);
14
+ case "object": return progress => from.blend(to, progress);
15
+ case "string": return createStringTween(from, to);
16
+ default: throw new Error("Invalid tweening type");
17
+ }
18
+ }
19
+ function createStringTween(from, to) {
20
+ const fromChunks = tokenise(from);
21
+ const toChunks = tokenise(to);
22
+ const tokenCount = fromChunks.filter(c => c.token).length;
23
+ // where length mismatch, use merging
24
+ if (tokenCount !== toChunks.filter(c => c.token).length) {
25
+ return createStringMerge(from, to);
12
26
  }
13
- if (typeof from == "number") {
14
- return blendNumbers(from, to, progress);
27
+ // where token prefix mismatch, use merging
28
+ if (fromChunks.some((chunk, i) => toChunks[i].prefix !== chunk.prefix)) {
29
+ return createStringMerge(from, to);
15
30
  }
16
- if (from && typeof from == "object") {
17
- if ("blend" in from) {
18
- const blendableSource = from;
19
- return blendableSource.blend(to, progress);
31
+ // convert token chunks to individual string tween funcs
32
+ const tweenChunks = fromChunks.map((chunk, i) => {
33
+ const fromToken = chunk.token;
34
+ const toToken = toChunks[i].token;
35
+ const prefix = chunk.prefix;
36
+ if (!fromToken)
37
+ return () => prefix;
38
+ if (fromToken.startsWith("#")) {
39
+ const fromColour = parseColour(fromToken);
40
+ const toColour = parseColour(toToken);
41
+ return progress => prefix + blendColours(fromColour, toColour, progress);
20
42
  }
21
- }
22
- throw new Error("Value not recognised as Tweenable");
43
+ else {
44
+ const fromNum = parseFloat(fromToken);
45
+ const toNum = parseFloat(toToken);
46
+ return progress => {
47
+ const blendedNum = blendNumbers(fromNum, toNum, progress);
48
+ return prefix + blendedNum.toString();
49
+ };
50
+ }
51
+ });
52
+ if (tweenChunks.length == 1)
53
+ return tweenChunks[0];
54
+ return progress => tweenChunks.map(t => t(progress)).join("");
23
55
  }
24
56
  function blendNumbers(from, to, progress) {
25
57
  return from + progress * (to - from);
26
58
  }
27
- function mergeStrings(from, to, progress) {
28
- const p = Math.min(Math.max(progress, 0), 1);
29
- // Fast‑path: identical strings or one is empty
59
+ function createStringMerge(from, to) {
60
+ // fast‑path: identical strings or one is empty
30
61
  if (from === to)
31
- return from;
62
+ return () => from;
32
63
  if (!from)
33
- return to;
64
+ return () => to;
34
65
  if (!to)
35
- return from;
66
+ return () => from;
36
67
  const split = (s) => {
37
- // Prefer Intl.Segmenter if available (Node ≥ 14, modern browsers)
68
+ // prefer Intl.Segmenter if available (Node ≥ 14, modern browsers)
38
69
  if (typeof Intl !== "undefined" && Intl.Segmenter) {
39
70
  const seg = new Intl.Segmenter(undefined, { granularity: "grapheme" });
40
71
  return Array.from(seg.segment(s), (seg) => seg.segment);
41
72
  }
42
- // Fallback regex (covers surrogate pairs & combining marks)
73
+ // fallback regex (covers surrogate pairs & combining marks)
43
74
  const graphemeRegex = /(\P{Mark}\p{Mark}*|[\uD800-\uDBFF][\uDC00-\uDFFF])/gu;
44
75
  return s.match(graphemeRegex) ?? Array.from(s);
45
76
  };
@@ -54,15 +85,18 @@ function mergeStrings(from, to, progress) {
54
85
  };
55
86
  const fromP = pad(a);
56
87
  const toP = pad(b);
57
- const replaceCount = Math.floor(p * maxLen);
58
- const result = new Array(maxLen);
59
- for (let i = 0; i < maxLen; ++i) {
60
- result[i] = i < replaceCount ? toP[i] : fromP[i];
61
- }
62
- while (result.length && result[result.length - 1] === " ") {
63
- result.pop();
64
- }
65
- return result.join("");
88
+ return (progress) => {
89
+ const clampedProgress = clamp(progress, 0, 1);
90
+ const replaceCount = Math.floor(clampedProgress * maxLen);
91
+ const result = new Array(maxLen);
92
+ for (let i = 0; i < maxLen; ++i) {
93
+ result[i] = i < replaceCount ? toP[i] : fromP[i];
94
+ }
95
+ while (result.length && result[result.length - 1] === " ") {
96
+ result.pop();
97
+ }
98
+ return result.join("");
99
+ };
66
100
  }
67
101
  function parseColour(code) {
68
102
  if (code.length < 2 || !code.startsWith("#"))
@@ -84,9 +118,7 @@ function parseColour(code) {
84
118
  return [...rawHex.matchAll(/../g)].map(hex => parseInt(hex[0], 16));
85
119
  }
86
120
  function blendColours(from, to, bias) {
87
- const fromColour = parseColour(from);
88
- const toColour = parseColour(to);
89
- const blended = fromColour.map((val, i) => clamp(blendNumbers(val, toColour[i], bias), 0, 255));
121
+ const blended = from.map((val, i) => clamp(blendNumbers(val, to[i], bias), 0, 255));
90
122
  return ("#" + blended.map(n => Math.round(n).toString(16).padStart(2, "0")).join("")).replace(/ff$/, "");
91
123
  }
92
124
  const tweenableTokenRegex = /(#(?:[0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})\b|[-+]?\d*\.?\d+(?:[eE][-+]?\d+)?)/g;
@@ -101,51 +133,9 @@ const tokenise = (s) => {
101
133
  lastIdx = m.index + token.length;
102
134
  }
103
135
  // trailing literal after the last token – stored as a final chunk
104
- // with an empty token (so the consumer can easily append it)
105
136
  const tail = s.slice(lastIdx);
106
137
  if (tail.length) {
107
138
  chunks.push({ prefix: tail, token: "" });
108
139
  }
109
140
  return chunks;
110
141
  };
111
- function blendStrings(from, to, progress) {
112
- if (from === to || progress === 0)
113
- return from;
114
- const fromChunks = tokenise(from);
115
- const toChunks = tokenise(to);
116
- const tokenCount = fromChunks.filter(c => c.token).length;
117
- if (tokenCount !== toChunks.filter(c => c.token).length) {
118
- return mergeStrings(from, to, progress);
119
- }
120
- let result = "";
121
- for (let i = 0, j = 0; i < fromChunks.length && j < toChunks.length;) {
122
- const f = fromChunks[i];
123
- const t = toChunks[j];
124
- // The *prefix* (the text before the token) must be the same.
125
- if (f.prefix !== t.prefix) {
126
- return mergeStrings(from, to, progress);
127
- }
128
- // Append the unchanged prefix.
129
- result += f.prefix;
130
- // If we are at the *trailing* chunk (no token), just break.
131
- if (!f.token && !t.token) {
132
- break;
133
- }
134
- // Blend the token according to its kind.
135
- let blended;
136
- if (f.token.startsWith("#")) {
137
- blended = blendColours(f.token, t.token, progress);
138
- }
139
- else {
140
- const fNum = parseFloat(f.token);
141
- const tNum = parseFloat(t.token);
142
- const blendedNum = blendNumbers(fNum, tNum, progress);
143
- blended = blendedNum.toString();
144
- }
145
- result += blended;
146
- // Advance both pointers.
147
- i++;
148
- j++;
149
- }
150
- return result;
151
- }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xtia/timeline",
3
- "version": "1.0.9",
3
+ "version": "1.0.11",
4
4
  "repository": {
5
5
  "url": "https://github.com/tiadrop/timeline",
6
6
  "type": "github"