chnobli 0.1.10 → 0.1.12

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 (120) hide show
  1. package/LICENSE.md +21 -0
  2. package/dist/animation/Animation.d.ts +2 -1
  3. package/dist/animation/Animation.d.ts.map +1 -1
  4. package/dist/animation/Animation.js +5 -3
  5. package/dist/animation/Animation.js.map +1 -1
  6. package/dist/animator/Animator.d.ts +30 -0
  7. package/dist/animator/Animator.d.ts.map +1 -0
  8. package/dist/animator/Animator.js +97 -0
  9. package/dist/animator/Animator.js.map +1 -0
  10. package/dist/animator/index.d.ts +5 -0
  11. package/dist/animator/index.d.ts.map +1 -0
  12. package/dist/animator/index.js +53 -0
  13. package/dist/animator/index.js.map +1 -0
  14. package/dist/animator/public.d.ts +16 -0
  15. package/dist/animator/public.d.ts.map +1 -0
  16. package/dist/animator/public.js +50 -0
  17. package/dist/animator/public.js.map +1 -0
  18. package/dist/chnobli.d.ts +3 -3
  19. package/dist/chnobli.d.ts.map +1 -1
  20. package/dist/chnobli.js.map +1 -1
  21. package/dist/property/ClassProperty.js +1 -1
  22. package/dist/property/ClassProperty.js.map +1 -1
  23. package/dist/property/TransformXYProperty.d.ts.map +1 -1
  24. package/dist/scroll/Scroll.d.ts +24 -3
  25. package/dist/scroll/Scroll.d.ts.map +1 -1
  26. package/dist/scroll/Scroll.js +157 -43
  27. package/dist/scroll/Scroll.js.map +1 -1
  28. package/dist/scroll/ScrollEvent.d.ts.map +1 -1
  29. package/dist/scroll/ScrollEvent.js +1 -0
  30. package/dist/scroll/ScrollEvent.js.map +1 -1
  31. package/dist/scroll/index.d.ts +2 -0
  32. package/dist/scroll/index.d.ts.map +1 -1
  33. package/dist/scroll/index.js +1 -0
  34. package/dist/scroll/index.js.map +1 -1
  35. package/dist/scroll/public.d.ts +19 -1
  36. package/dist/scroll/public.d.ts.map +1 -1
  37. package/dist/scroll/public.js +26 -1
  38. package/dist/scroll/public.js.map +1 -1
  39. package/dist/target/DomTarget.js +1 -1
  40. package/dist/target/DomTarget.js.map +1 -1
  41. package/dist/target/ObjectTarget.d.ts +11 -10
  42. package/dist/target/ObjectTarget.d.ts.map +1 -1
  43. package/dist/target/ObjectTarget.js +20 -13
  44. package/dist/target/ObjectTarget.js.map +1 -1
  45. package/dist/target/TestDomNode.d.ts.map +1 -1
  46. package/dist/timeline/Timeline.d.ts +2 -1
  47. package/dist/timeline/Timeline.d.ts.map +1 -1
  48. package/dist/timeline/Timeline.js +4 -4
  49. package/dist/timeline/Timeline.js.map +1 -1
  50. package/dist/timeline/public.d.ts +11 -4
  51. package/dist/timeline/public.d.ts.map +1 -1
  52. package/dist/timeline/public.js +19 -11
  53. package/dist/timeline/public.js.map +1 -1
  54. package/dist/timing/AnimationFrameTicker.d.ts.map +1 -1
  55. package/dist/timing/AnimationFrameTicker.js +5 -1
  56. package/dist/timing/AnimationFrameTicker.js.map +1 -1
  57. package/dist/timing/TestTicker.d.ts.map +1 -1
  58. package/dist/timing/TestTicker.js +5 -1
  59. package/dist/timing/TestTicker.js.map +1 -1
  60. package/dist/timing/Ticker.d.ts +4 -2
  61. package/dist/timing/Ticker.d.ts.map +1 -1
  62. package/dist/timing/Timing.d.ts +47 -2
  63. package/dist/timing/Timing.d.ts.map +1 -1
  64. package/dist/timing/Timing.js +118 -84
  65. package/dist/timing/Timing.js.map +1 -1
  66. package/dist/timing/easing.d.ts +1 -2
  67. package/dist/timing/easing.d.ts.map +1 -1
  68. package/dist/timing/easing.js.map +1 -1
  69. package/dist/utils/Events.js +1 -1
  70. package/dist/utils/Events.js.map +1 -1
  71. package/dist/utils/motionpath.js +5 -5
  72. package/dist/utils/motionpath.js.map +1 -1
  73. package/dist/utils/utils.d.ts.map +1 -1
  74. package/dist/utils/utils.js +0 -8
  75. package/dist/utils/utils.js.map +1 -1
  76. package/dist/values/ColorValue.d.ts.map +1 -1
  77. package/dist/values/ColorValue.js +1 -0
  78. package/dist/values/ColorValue.js.map +1 -1
  79. package/dist/values/StyleValue.d.ts.map +1 -1
  80. package/dist/values/Value.d.ts.map +1 -1
  81. package/dist/values/Value.js +1 -1
  82. package/dist/values/Value.js.map +1 -1
  83. package/eslint.config.js +23 -0
  84. package/jsr.json +16 -0
  85. package/package.json +12 -4
  86. package/src/animation/Animation.ts +6 -3
  87. package/src/animator/Animator.ts +130 -0
  88. package/src/animator/index.ts +56 -0
  89. package/src/animator/public.ts +66 -0
  90. package/src/chnobli.ts +3 -2
  91. package/src/property/ClassProperty.ts +1 -1
  92. package/src/property/TransformXYProperty.ts +1 -1
  93. package/src/scroll/Scroll.ts +204 -51
  94. package/src/scroll/ScrollEvent.ts +1 -0
  95. package/src/scroll/index.ts +3 -0
  96. package/src/scroll/public.ts +35 -2
  97. package/src/target/DomTarget.ts +1 -1
  98. package/src/target/ObjectTarget.ts +19 -13
  99. package/src/target/TestDomNode.ts +3 -3
  100. package/src/tests/properties.test.ts +86 -3
  101. package/src/tests/scroll.test.ts +36 -0
  102. package/src/tests/timeline.test.ts +1 -1
  103. package/src/tests/timing.test.ts +32 -0
  104. package/src/tests/usecases.test.ts +40 -2
  105. package/src/tests/values.test.ts +9 -9
  106. package/src/timeline/Timeline.ts +5 -4
  107. package/src/timeline/public.ts +34 -12
  108. package/src/timing/AnimationFrameTicker.ts +6 -1
  109. package/src/timing/TestTicker.ts +7 -1
  110. package/src/timing/Ticker.ts +5 -1
  111. package/src/timing/Timing.ts +133 -97
  112. package/src/timing/easing.ts +6 -1
  113. package/src/utils/Events.ts +1 -1
  114. package/src/utils/motionpath.ts +5 -5
  115. package/src/utils/utils.ts +13 -16
  116. package/src/values/ColorValue.ts +5 -4
  117. package/src/values/StyleValue.ts +4 -4
  118. package/src/values/Value.ts +8 -8
  119. package/.eslintrc.cjs +0 -39
  120. package/src/animator.js +0 -187
@@ -1,7 +1,18 @@
1
- import PublicTimeline, { Offset } from '../timeline/public.js';
1
+ import ResponsiveEvent from '../responsive/ResponsiveEvent.js';
2
+ import PublicTimeline from '../timeline/public.js';
3
+ import Events from '../utils/Events.js';
2
4
  import { pageOffset } from '../utils/utils.js';
3
5
  import ScrollEvent from './ScrollEvent.js';
4
6
 
7
+ export type DestroyOptions = {
8
+ /// defaults to true
9
+ timelines?: boolean;
10
+ };
11
+
12
+ const DEF_DESTROY_OPTS: DestroyOptions = {
13
+ timelines: true,
14
+ };
15
+
5
16
  /*
6
17
  run timelines based on the scroll position
7
18
 
@@ -54,11 +65,13 @@ export type ScrollTrigger =
54
65
 
55
66
  export type OffsetValue = 'top' | 'center' | 'bottom' | number;
56
67
 
68
+ export type State = 'before' | 'in' | 'after';
69
+
57
70
  export type ParsedScrollTrigger = {
58
71
  target: HTMLElement;
59
72
  offset: number;
60
73
  view: number;
61
- position: Position;
74
+ position: Position | undefined;
62
75
  };
63
76
 
64
77
  export type ScrollOffset = {
@@ -72,35 +85,45 @@ export type ScrollOffset = {
72
85
 
73
86
  export type Position = number | string;
74
87
 
88
+ type LastSeek = {
89
+ pos: number;
90
+ state: State;
91
+ };
92
+
75
93
  export default class ScrollTimeline {
76
94
  private _initialized: boolean;
77
- private _globalEvent: ScrollEvent;
78
- private _rmEvent: () => void;
95
+ private _globalEvent: ScrollEvent | null;
96
+ private _scrollEvent: () => void;
97
+ private _responsiveEvent: any;
79
98
 
80
99
  private _triggers: ParsedScrollTrigger[];
100
+ private _events: Events;
101
+ private lastEvPos: number;
81
102
  private timelines: PublicTimeline[];
103
+ private state: State;
104
+ private lastSeek: LastSeek | undefined;
82
105
  offsets: ScrollOffset[];
83
106
 
107
+ private debugMarkers: HTMLElement[];
108
+ private debugPrefix?: string;
109
+
84
110
  constructor() {
85
111
  this._initialized = false;
86
112
  this._globalEvent = ScrollEvent.global();
87
- this._rmEvent = this._globalEvent.add(y => this._onScroll(y)).remove;
113
+ this._scrollEvent =
114
+ this._globalEvent?.add(y => this._onScroll(y)).remove ?? (() => {});
115
+ this._responsiveEvent = ResponsiveEvent.global()?.add((a, b) => {
116
+ this._onResponsive(a, b);
117
+ });
88
118
 
89
119
  this._triggers = [];
120
+ this._events = new Events();
121
+ this.lastEvPos = NaN;
90
122
  this.timelines = [];
123
+ this.state = 'in';
91
124
  this.offsets = [];
92
125
 
93
- // this.timeline = timeline;
94
- // this._triggers = triggers.map(parseTrigger);
95
- // this.offsets = this._triggers.map(t => {
96
- // return {
97
- // y: 0,
98
- // view: t.view,
99
- // };
100
- // });
101
-
102
- // if (this.offsets.length < 2)
103
- // throw new Error('scroll timeline needs at least two triggers');
126
+ this.debugMarkers = [];
104
127
  }
105
128
 
106
129
  /**
@@ -120,21 +143,38 @@ export default class ScrollTimeline {
120
143
  this.timelines.push(timeline);
121
144
  }
122
145
 
123
- init() {
146
+ on(event: string, fn: (...args: any[]) => void): () => void {
147
+ return this._events.add(event, fn);
148
+ }
149
+
150
+ /// Returns true if the init function was executed
151
+ init(): boolean {
152
+ if (this._initialized) return false;
153
+
154
+ this._initialized = true;
155
+
124
156
  // todo we need to calculate the position for each timeline
125
157
  //
126
158
  // calculate the positions and offsets
127
159
  // if we are missing one extend them
128
160
 
129
- let previousY = -1;
130
-
131
- this.offsets = this._triggers.map(t => {
132
- const offset = pageOffset(t.target);
161
+ if (this._triggers.length < 1)
162
+ throw new Error('scroll timeline needs at least one trigger');
133
163
 
134
- const y = offset.top + offset.height * t.offset;
164
+ if (this._triggers.length === 1) {
165
+ this._triggers.push({
166
+ target: this._triggers[0].target,
167
+ offset: 1,
168
+ view: 1,
169
+ position: 1,
170
+ });
171
+ }
135
172
 
136
- if (y < previousY) throw new Error('offsets are not in order');
173
+ const positionStep = 1 / (this._triggers.length - 1);
137
174
 
175
+ // first calculate position and view
176
+ let prevPos = 0;
177
+ this.offsets = this._triggers.map((t, i) => {
138
178
  let position;
139
179
  if (typeof t.position === 'string') {
140
180
  if (this.timelines.length !== 1)
@@ -143,59 +183,172 @@ export default class ScrollTimeline {
143
183
  );
144
184
 
145
185
  position = this.timelines[0].labelPosition(t.position);
146
- } else if (t.position >= 0 || t.position <= 1) {
186
+ } else if (typeof t.position === 'number') {
147
187
  position = t.position;
148
188
  } else {
149
- throw new Error('position should be between 0 and 1');
189
+ position = i * positionStep;
150
190
  }
151
191
 
192
+ if (position < prevPos)
193
+ throw new Error('positions are not in order');
194
+ prevPos = position;
195
+
152
196
  return {
153
- y,
197
+ y: 0,
154
198
  view: t.view,
155
199
  position,
156
200
  };
157
201
  });
158
202
 
159
- if (this.offsets.length < 1)
160
- throw new Error('scroll timeline needs at least one trigger');
203
+ this.initY();
161
204
 
162
- if (this.offsets.length === 1) {
163
- this.offsets.push({
164
- y:
165
- this.offsets[0].y +
166
- pageOffset(this._triggers[0].target).height,
167
- view: 1,
168
- position: 1,
169
- });
205
+ return true;
206
+ }
207
+
208
+ private initY() {
209
+ let previousY = -1;
210
+ this.offsets.forEach((offset, i) => {
211
+ const t = this._triggers[i];
212
+
213
+ const pOffset = pageOffset(t.target);
214
+ const y = pOffset.top + pOffset.height * t.offset;
215
+
216
+ if (y < previousY) throw new Error('offsets are not in order');
217
+ previousY = y;
218
+
219
+ offset.y = y;
220
+ });
221
+ }
222
+
223
+ update() {
224
+ // execute initY to check if elements have moved
225
+ if (!this.init()) this.initY();
226
+
227
+ // todo this position should come from the GlobalEvent
228
+ this._onScroll({
229
+ y: window.scrollY,
230
+ height: window.innerHeight,
231
+ });
232
+ }
233
+
234
+ destroy(opts: DestroyOptions = {}) {
235
+ opts = { ...DEF_DESTROY_OPTS, ...opts };
236
+
237
+ this._scrollEvent();
238
+ this._events.destroy();
239
+ this._responsiveEvent?.remove();
240
+
241
+ if (opts.timelines) {
242
+ for (const timeline of this.timelines) {
243
+ timeline.destroy();
244
+ }
245
+ }
246
+
247
+ for (const marker of this.debugMarkers) {
248
+ marker.remove();
170
249
  }
250
+ this.debugMarkers = [];
251
+ }
252
+
253
+ debug(prefix?: string) {
254
+ this.init();
255
+
256
+ this.debugPrefix = prefix;
257
+
258
+ const height = window.innerHeight;
259
+
260
+ this.offsets.forEach((offset, i) => {
261
+ const y = offset.y - height * offset.view;
262
+
263
+ const marker = document.createElement('span');
264
+ marker.textContent =
265
+ (prefix ? prefix + ' ' : '') + i + ': ' + offset.position;
266
+ marker.style.position = 'absolute';
267
+ marker.style.display = 'block';
268
+ marker.style.right = '0';
269
+ marker.style.top = y + 'px';
270
+ marker.style.minWidth = '50px';
271
+ marker.style.height = '2px';
272
+ marker.style.paddingRight = '10px';
273
+ marker.style.backgroundColor = 'red';
274
+ marker.style.zIndex = '100000';
275
+
276
+ document.body.appendChild(marker);
277
+ this.debugMarkers.push(marker);
278
+ });
171
279
  }
172
280
 
173
281
  private _onScroll({ y, height }: { y: number; height: number }) {
174
- if (!this._initialized) {
175
- this.init();
176
- this._initialized = true;
282
+ this.init();
283
+
284
+ const pos = this.calcPosition({ y, height });
285
+ const clampPos = Math.min(1, Math.max(0, pos));
286
+ const state = pos < 0 ? 'before' : pos > 1 ? 'after' : 'in';
287
+
288
+ // we need to trigger once
289
+ // if the pos is not the same and but pos can have 3 states
290
+ // under (<0), in (0-1) or over (>1)
291
+ // each state should be triggered once
292
+ if (this.lastSeek?.pos === clampPos && this.lastSeek?.state === state) {
293
+ return;
177
294
  }
178
295
 
296
+ this.lastSeek = { pos: clampPos, state };
297
+
298
+ this._events.trigger('update', clampPos, state);
299
+
300
+ for (const timeline of this.timelines) {
301
+ timeline.seek(pos);
302
+ }
303
+ }
304
+
305
+ /**
306
+ * @ignore
307
+ */
308
+ private _onResponsive(
309
+ _a: { width: number; height: number },
310
+ _b: { remove: () => void },
311
+ ) {
312
+ this.update();
313
+
314
+ // reposition debug Markers
315
+ if (this.debugMarkers.length) {
316
+ for (const marker of this.debugMarkers) {
317
+ marker.remove();
318
+ }
319
+
320
+ this.debug(this.debugPrefix);
321
+ }
322
+ }
323
+
324
+ private calcPosition({ y, height }: { y: number; height: number }): number {
325
+ // check if it's outside of the bounds
326
+ const first = this.offsets[0];
327
+ if (y < first.y - height * first.view) return -1;
328
+
329
+ const last = this.offsets[this.offsets.length - 1];
330
+ if (y > last.y - height * last.view) return 2;
331
+
179
332
  // find the matching offset
180
333
  // rolling window
181
334
  for (let i = 1; i < this.offsets.length; i++) {
182
335
  const start = this.offsets[i - 1];
183
336
  const end = this.offsets[i];
184
337
 
185
- const startY = start.y + height * start.view;
186
- const endY = end.y + height * end.view;
187
-
188
- const dif = endY - startY;
189
- const x = 1 - (endY - y) / dif;
338
+ const startY = start.y - height * start.view;
339
+ const endY = end.y - height * end.view;
190
340
 
191
- for (const timeline of this.timelines) {
192
- // calc pos range
193
- const pos =
194
- start.position + (end.position - start.position) * x;
341
+ // the end is included, at the bottom of the page y can't get
342
+ // past the last offset
343
+ if (y >= startY && y <= endY) {
344
+ const dif = endY - startY;
345
+ const x = dif === 0 ? 1 : 1 - (endY - y) / dif;
195
346
 
196
- timeline.seek(pos);
347
+ return start.position + (end.position - start.position) * x;
197
348
  }
198
349
  }
350
+
351
+ return -1;
199
352
  }
200
353
  }
201
354
 
@@ -239,7 +392,7 @@ function parseTrigger(
239
392
  target: element,
240
393
  offset: parseOffset(offset),
241
394
  view: parseOffset(view),
242
- position: position ?? (i == 0 ? 0 : 1),
395
+ position,
243
396
  };
244
397
  }
245
398
 
@@ -261,7 +414,7 @@ function parseOffset(val: OffsetValue): number {
261
414
  throw new Error('unknown view value ' + val);
262
415
  }
263
416
 
264
- if (typeof val !== 'number' || val < 0 || val > 1) {
417
+ if (typeof val !== 'number') {
265
418
  throw new Error(
266
419
  'view expecting to be a top/center/bottom or a percent value',
267
420
  );
@@ -10,6 +10,7 @@ export default class ScrollEvent {
10
10
  this._listeners = new Set();
11
11
 
12
12
  window.addEventListener('scroll', e => this._onScroll(e));
13
+ window.addEventListener('resize', e => this._onScroll(e));
13
14
  }
14
15
 
15
16
  static global() {
@@ -1,5 +1,8 @@
1
+ import type { Position, ScrollTrigger } from './Scroll.js';
1
2
  import Scroll from './public.js';
2
3
 
4
+ export { Scroll, ScrollTrigger, Position };
5
+
3
6
  /**
4
7
  * Create a scroll animation
5
8
  */
@@ -1,6 +1,9 @@
1
1
  import { Timeline } from '../chnobli.js';
2
- // import Scroll from './Scroll.js';
3
- import ScrollTimeline, { Position, ScrollTrigger } from './Scroll.js';
2
+ import ScrollTimeline, {
3
+ DestroyOptions,
4
+ Position,
5
+ ScrollTrigger,
6
+ } from './Scroll.js';
4
7
 
5
8
  export default class Scroll {
6
9
  private _inner: ScrollTimeline;
@@ -28,4 +31,34 @@ export default class Scroll {
28
31
 
29
32
  return this;
30
33
  }
34
+
35
+ /**
36
+ * Listen on events
37
+ *
38
+ * returns a function to remove the event listener
39
+ *
40
+ * ## Events
41
+ *
42
+ * ### Update
43
+ * The scroll position has been updated passes in a number between 0 and 1
44
+ *
45
+ */
46
+ on(event: string, fn: (...args: any[]) => void): () => void {
47
+ return this._inner.on(event, fn);
48
+ }
49
+
50
+ update() {
51
+ this._inner.update();
52
+ }
53
+
54
+ /**
55
+ * Destroy the scroll
56
+ */
57
+ destroy(opts: DestroyOptions = {}) {
58
+ this._inner.destroy(opts);
59
+ }
60
+
61
+ debug(prefix?: string) {
62
+ this._inner.debug(prefix);
63
+ }
31
64
  }
@@ -84,7 +84,7 @@ export default class DomTarget implements Target {
84
84
  return;
85
85
  }
86
86
 
87
- let vals = this.transformValues.get(fn);
87
+ const vals = this.transformValues.get(fn);
88
88
  if (!vals) return;
89
89
 
90
90
  vals[idx] = null;
@@ -9,6 +9,7 @@ export default class ObjectTarget implements Target {
9
9
 
10
10
  // private transformValues: Map<string, (Value | null)[]>;
11
11
  private styleValues: Map<string, StyleValue | undefined>;
12
+ private firstValues: Map<string, StyleValue | undefined>;
12
13
  // private values: Map<string, Value>;
13
14
 
14
15
  /**
@@ -20,6 +21,7 @@ export default class ObjectTarget implements Target {
20
21
  this.target = target;
21
22
 
22
23
  this.styleValues = new Map();
24
+ this.firstValues = new Map();
23
25
  // this.values = new Map();
24
26
  }
25
27
 
@@ -31,30 +33,30 @@ export default class ObjectTarget implements Target {
31
33
  return 'object';
32
34
  }
33
35
 
34
- getValue(name: string): Value | null {
36
+ getValue(_name: string): Value | null {
35
37
  // return this.values.get(name) ?? null;
36
38
  throw new Error('not supported');
37
39
  }
38
40
 
39
- setValue(name: string, value: Value) {
41
+ setValue(_name: string, _value: Value) {
40
42
  // this.values.set(name, value);
41
43
  throw new Error('not supported');
42
44
  }
43
45
 
44
- removeValue(name: string) {
46
+ removeValue(_name: string) {
45
47
  // this.values.delete(name);
46
48
  throw new Error('not supported');
47
49
  }
48
50
 
49
- getTransformValue(name: string): Value | null {
51
+ getTransformValue(_name: string): Value | null {
50
52
  throw new Error('not supported');
51
53
  }
52
54
 
53
- setTransformValue(name: string, value: Value) {
55
+ setTransformValue(_name: string, _value: Value) {
54
56
  throw new Error('not supported');
55
57
  }
56
58
 
57
- removeTransformValue(name: string) {
59
+ removeTransformValue(_name: string) {
58
60
  throw new Error('not supported');
59
61
  }
60
62
 
@@ -71,6 +73,7 @@ export default class ObjectTarget implements Target {
71
73
  try {
72
74
  const value = StyleValue.parse(styleV);
73
75
  this.styleValues.set(name, value);
76
+ this.firstValues.set(name, value);
74
77
 
75
78
  return value;
76
79
  } catch (e) {
@@ -87,19 +90,19 @@ export default class ObjectTarget implements Target {
87
90
  this.styleValues.set(name, undefined);
88
91
  }
89
92
 
90
- hasClass(name: string): boolean {
93
+ hasClass(_name: string): boolean {
91
94
  throw new Error('not supported');
92
95
  }
93
96
 
94
- addClass(name: string): void {
97
+ addClass(_name: string): void {
95
98
  throw new Error('not supported');
96
99
  }
97
100
 
98
- removeClass(name: string): void {
101
+ removeClass(_name: string): void {
99
102
  throw new Error('not supported');
100
103
  }
101
104
 
102
- unifyValues(name: string, a: Value, b: Value): [Value, Value] {
105
+ unifyValues(_name: string, a: Value, b: Value): [Value, Value] {
103
106
  if (a.unit === b.unit) return [a, b];
104
107
 
105
108
  if (!a.unit && b.unit) a.unit = b.unit;
@@ -113,9 +116,12 @@ export default class ObjectTarget implements Target {
113
116
  // style
114
117
  for (const [k, v] of this.styleValues.entries()) {
115
118
  if (v === undefined) {
116
- console.log('delete', k);
117
- delete this.target[k];
118
- this.styleValues.delete(k);
119
+ const first = this.firstValues.get(k);
120
+ if (typeof first !== 'undefined') {
121
+ this.target[k] = first.toMixed();
122
+ } else {
123
+ delete this.target[k];
124
+ }
119
125
  } else {
120
126
  this.target[k] = v.toMixed();
121
127
  }
@@ -45,11 +45,11 @@ export default class TestDomNode {
45
45
 
46
46
  __simulatedDom__() {}
47
47
 
48
- __getComputedStyleFn__() {
48
+ __getComputedStyleFn__(): typeof TestDomNode.getComputedStyle {
49
49
  return TestDomNode.getComputedStyle;
50
50
  }
51
51
 
52
- __getRootElementFn__() {
52
+ __getRootElementFn__(): typeof getRootElement {
53
53
  return getRootElement;
54
54
  }
55
55
  }
@@ -58,7 +58,7 @@ export function el() {
58
58
  return new TestDomNode();
59
59
  }
60
60
 
61
- export function getRootElement() {
61
+ export function getRootElement(): TestDomNode {
62
62
  if (!ROOT_ELEMENT) ROOT_ELEMENT = el();
63
63
  return ROOT_ELEMENT;
64
64
  }
@@ -1,7 +1,7 @@
1
1
  import { describe, expect, it } from 'vitest';
2
2
  import TestTicker from '../timing/TestTicker.js';
3
3
  import TestResponsiveEvent from '../responsive/TestEvent.js';
4
- import { animate, timeline } from '../chnobli.js';
4
+ import { timeline } from '../chnobli.js';
5
5
  import { responsive, curve, fromTo } from '../utils/utils.js';
6
6
  import { el } from '../target/TestDomNode.js';
7
7
  import { timeout } from 'fire/utils';
@@ -60,7 +60,7 @@ describe('properties', () => {
60
60
  }
61
61
 
62
62
  let resolveCleanupProm: any;
63
- let cleanupProm = new Promise(
63
+ const cleanupProm = new Promise(
64
64
  resolve => (resolveCleanupProm = resolve),
65
65
  );
66
66
  const cleanup = new FinalizationRegistry(key => {
@@ -79,6 +79,47 @@ describe('properties', () => {
79
79
  expect(res).toBe('div');
80
80
  });
81
81
 
82
+ it('keep values', () => {
83
+ const ticker = new TestTicker();
84
+
85
+ const div = el();
86
+ const tl = timeline()
87
+ .add(div, {
88
+ x: 100,
89
+ duration: 10,
90
+ })
91
+ .play();
92
+
93
+ ticker.run(5);
94
+ expect(div.style.transform).toBe(
95
+ 'translate3d(50.000px,0.000px,0.000px)',
96
+ );
97
+
98
+ tl.destroy({ keepValues: true });
99
+ expect(div.style.transform).toBe(
100
+ 'translate3d(50.000px,0.000px,0.000px)',
101
+ );
102
+
103
+ const tl2 = timeline()
104
+ .add(div, {
105
+ x: 100,
106
+ duration: 10,
107
+ })
108
+ .play();
109
+ expect(div.style.transform).toBe(
110
+ 'translate3d(50.000px,0.000px,0.000px)',
111
+ );
112
+
113
+ ticker.run();
114
+ expect(div.style.transform).toBe(
115
+ 'translate3d(100.000px,0.000px,0.000px)',
116
+ );
117
+ tl2.destroy({ keepValues: true });
118
+ expect(div.style.transform).toBe(
119
+ 'translate3d(100.000px,0.000px,0.000px)',
120
+ );
121
+ });
122
+
82
123
  it('update timeline', () => {
83
124
  const ticker = new TestTicker();
84
125
  const respEv = new TestResponsiveEvent();
@@ -206,7 +247,7 @@ describe('properties', () => {
206
247
  tl.reverse();
207
248
  tl.play();
208
249
  ticker.run();
209
- expect(obj.x).toBe(undefined);
250
+ expect(obj.x).toBe(0);
210
251
  expect(obj.width).toBe(undefined);
211
252
  });
212
253
 
@@ -230,4 +271,46 @@ describe('properties', () => {
230
271
  ticker.run();
231
272
  expect(div.style.width).toBe(undefined);
232
273
  });
274
+
275
+ // fails until #7 is fixed
276
+ it.fails('restore before', () => {
277
+ const ticker = new TestTicker();
278
+
279
+ const div = el();
280
+ const tl = timeline().add(div, {
281
+ x: 100,
282
+ duration: 10,
283
+ });
284
+
285
+ tl.seek(0.95);
286
+
287
+ ticker.run();
288
+ expect(div.style.transform).toBe(
289
+ 'translate3d(95.000px,0.000px,0.000px)',
290
+ );
291
+
292
+ const tl2 = timeline().add(div, {
293
+ x: 100,
294
+ duration: 10,
295
+ });
296
+ tl2._inner.init();
297
+
298
+ ticker.run();
299
+ expect(div.style.transform).toBe(
300
+ 'translate3d(95.000px,0.000px,0.000px)',
301
+ );
302
+
303
+ timeline()
304
+ .add(
305
+ { opacity: 0 },
306
+ {
307
+ opacity: 1,
308
+ },
309
+ )
310
+ .play();
311
+ ticker.run();
312
+ expect(div.style.transform).toBe(
313
+ 'translate3d(95.000px,0.000px,0.000px)',
314
+ );
315
+ });
233
316
  });