chnobli 0.1.2 → 0.1.4

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.
@@ -0,0 +1,64 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import TestTicker from '../timing/testticker.js';
3
+ import TestResponsiveEvent from '../responsive/testevent.js';
4
+ import { animate, timeline } from '../chnobli.js';
5
+ import { responsive } from '../utils/utils.js';
6
+ import { el } from '../target/testdomnode.js';
7
+
8
+ describe('usecases', () => {
9
+ it('accordion', () => {
10
+ const ticker = new TestTicker;
11
+ const respEv = new TestResponsiveEvent;
12
+
13
+ const itm = el();
14
+ const ctn = el();
15
+ ctn.computedStyle.maxHeight = 'none';
16
+ ctn.scrollHeight = '100px';
17
+
18
+ const tl = timeline()
19
+ .set(itm, {
20
+ cls: 'open'
21
+ })
22
+ .add(ctn, {
23
+ maxHeight: responsive(el => el.scrollHeight),
24
+ duration: 100
25
+ });
26
+
27
+ tl.play();
28
+ ticker.run(1);
29
+ expect(ctn.style.maxHeight).toBe('1.000px');
30
+ expect(itm.classList.contains('open')).toBe(true);
31
+
32
+ // // on click
33
+ tl.play();
34
+ ticker.run(4);
35
+ expect(ctn.style.maxHeight).toBe('5.000px');
36
+ expect(itm.classList.contains('open')).toBe(true);
37
+
38
+ ticker.run();
39
+ expect(ctn.style.maxHeight).toBe('100.000px');
40
+ expect(itm.classList.contains('open')).toBe(true);
41
+
42
+ // now a resize comes
43
+ ctn.scrollHeight = '200px';
44
+ tl.update();
45
+ expect(ctn.style.maxHeight).toBe('200.000px');
46
+ expect(itm.classList.contains('open')).toBe(true);
47
+
48
+ // on click
49
+ tl.reverse();
50
+ tl.play();
51
+ ticker.run(12);
52
+ expect(ctn.style.maxHeight).toBe('176.000px');
53
+ expect(itm.classList.contains('open')).toBe(true);
54
+
55
+ ctn.scrollHeight = '150px';
56
+ respEv.resize(1000, 1000);
57
+ expect(ctn.style.maxHeight).toBe('132.000px');
58
+ expect(itm.classList.contains('open')).toBe(true);
59
+
60
+ ticker.run();
61
+ expect(ctn.style.maxHeight).toBe(undefined);
62
+ expect(itm.classList.contains('open')).toBe(false);
63
+ });
64
+ });
@@ -0,0 +1,162 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import TestTicker from '../timing/testticker.js';
3
+ import TestResponsiveEvent from '../responsive/testevent.js';
4
+ import { animate, timeline } from '../chnobli.js';
5
+ import { responsive, from, fromTo } from '../utils/utils.js';
6
+ import { el, getRootElement } from '../target/testdomnode.js';
7
+ import { timeout } from 'fire/util.js';
8
+
9
+ describe('values', () => {
10
+ it('rem unit', () => {
11
+ const ticker = new TestTicker;
12
+
13
+ const root = getRootElement();
14
+ root.computedStyle.fontSize = '13px';
15
+
16
+ const div = el();
17
+ const tl = timeline()
18
+ .add(div, {
19
+ width: '10rem',
20
+ duration: 5
21
+ })
22
+ .add(div, {
23
+ width: 20,
24
+ duration: 5
25
+ })
26
+ .play();
27
+
28
+ ticker.run(0);
29
+ expect(div.style.width).toBe('0.000rem');
30
+
31
+ ticker.run();
32
+ expect(div.style.width).toBe('20.000rem');
33
+ });
34
+
35
+ it('percent unit', () => {
36
+ const ticker = new TestTicker;
37
+
38
+ const div = el();
39
+ div.computedStyle.width = '10px';
40
+ const tl = timeline()
41
+ .add(div, {
42
+ x: '10%',
43
+ width: '50%',
44
+ duration: 10
45
+ })
46
+ .play();
47
+
48
+ ticker.run(0);
49
+ expect(div.style.transform).toBe('translate3d(0.000%,0.000px,0.000px)');
50
+ expect(div.style.width).toBe('100.000%');
51
+
52
+ ticker.run();
53
+ expect(div.style.transform).toBe('translate3d(10.000%,0.000px,0.000px)');
54
+ expect(div.style.width).toBe('50.000%');
55
+ });
56
+
57
+ it('multiple values', () => {
58
+ const ticker = new TestTicker;
59
+
60
+ const root = getRootElement();
61
+ root.computedStyle.fontSize = '10px';
62
+
63
+ const div = el();
64
+ div.computedStyle.padding = '5px';
65
+ const tl = timeline()
66
+ .add(div, {
67
+ padding: '10px 20rem',
68
+ duration: 10
69
+ })
70
+ .play();
71
+
72
+ ticker.run(0);
73
+ expect(div.style.padding).toBe('5.000px 0.500rem');
74
+
75
+ ticker.run();
76
+ expect(div.style.padding).toBe('10.000px 20.000rem');
77
+ });
78
+
79
+ it('display', () => {
80
+ const ticker = new TestTicker;
81
+
82
+ const div = el();
83
+ div.computedStyle.display = 'block';
84
+ const tl = timeline()
85
+ .add(div, {
86
+ display: 'none',
87
+ position: from('absolute'),
88
+ visibility: fromTo('hidden', 'visible'),
89
+ duration: 10
90
+ })
91
+ .play();
92
+
93
+ ticker.run(0);
94
+ expect(div.style.display).toBe(undefined);
95
+ expect(div.style.position).toBe('absolute');
96
+ expect(div.style.visibility).toBe('hidden');
97
+
98
+ ticker.run();
99
+ expect(div.style.display).toBe('none');
100
+ expect(div.style.position).toBe(undefined);
101
+ expect(div.style.visibility).toBe('visible');
102
+
103
+ tl.seek(-1);
104
+ ticker.run();
105
+ expect(div.style.display).toBe(undefined);
106
+ expect(div.style.position).toBe(undefined);
107
+ expect(div.style.visibility).toBe(undefined);
108
+ });
109
+
110
+ it('colors', () => {
111
+ const ticker = new TestTicker;
112
+ const div = el();
113
+ div.computedStyle.color = 'rgb(120, 130, 20)';
114
+
115
+ const tl = timeline()
116
+ .add(div, {
117
+ color: 'black',
118
+ backgroundColor: 'hsl(10, 80%, 40%)',
119
+ duration: 10
120
+ })
121
+ .play();
122
+
123
+ ticker.run(0);
124
+ expect(div.style.color).toBe('rgba(120, 130, 20, 1.000)');
125
+ expect(div.style.backgroundColor).toBe('rgba(255, 255, 255, 1.000)');
126
+
127
+ ticker.run(5);
128
+ expect(div.style.color).toBe('rgba(60, 65, 10, 1.000)');
129
+ expect(div.style.backgroundColor).toBe('rgba(220, 152, 138, 1.000)');
130
+
131
+ ticker.run();
132
+ expect(div.style.color).toBe('rgba(0, 0, 0, 1.000)');
133
+ expect(div.style.backgroundColor).toBe('rgba(184, 48, 20, 1.000)');
134
+ });
135
+
136
+ it('css-vars', () => {
137
+ const ticker = new TestTicker;
138
+ const div = el();
139
+ div.computedStyle['--color'] = 'rgb(120, 130, 20)';
140
+ div.computedStyle['--border'] = '20px';
141
+
142
+ const tl = timeline()
143
+ .add(div, {
144
+ '--color': 'black',
145
+ '--border': '1px 20px',
146
+ duration: 10
147
+ })
148
+ .play();
149
+
150
+ ticker.run(0);
151
+ expect(div.style['--color']).toBe('rgba(120, 130, 20, 1.000)');
152
+ expect(div.style['--border']).toBe('20.000px 20.000px');
153
+
154
+ ticker.run(5);
155
+ expect(div.style['--color']).toBe('rgba(60, 65, 10, 1.000)');
156
+ expect(div.style['--border']).toBe('10.500px 20.000px');
157
+
158
+ ticker.run();
159
+ expect(div.style['--color']).toBe('rgba(0, 0, 0, 1.000)');
160
+ expect(div.style['--border']).toBe('1.000px 20.000px');
161
+ });
162
+ });
@@ -1,8 +1,9 @@
1
1
  import { takeProp } from '../utils/internal.js';
2
2
  import Timeline from './timeline.js';
3
- import { STATE_BEFORE, STATE_AFTER } from '../timing/timing.js';
3
+ import { STATE_ENDED } from '../timing/timing.js';
4
4
  import { callStagger } from '../stagger/stagger.js';
5
5
  import Events from '../utils/events.js';
6
+ import ResponsiveEvent from '../responsive/event.js';
6
7
 
7
8
  const STATE_PAUSED = 0;
8
9
  const STATE_RENDER_ONCE = 1;
@@ -11,20 +12,52 @@ const STATE_PLAYING = 2;
11
12
  export default class PublicTimeline {
12
13
  constructor(props = {}) {
13
14
  this._defaults = takeProp(props, 'defaults', {});
15
+ this._responsive = takeProp(props, 'responsive', true);
16
+
17
+ if (typeof this._responsive !== 'boolean')
18
+ throw new Error('responsive can only be a boolean');
19
+ if ('duration' in props)
20
+ throw new Error('a timeline does not accept a duration');
21
+
14
22
  this._inner = new Timeline(props);
15
23
 
16
24
  // 'start', 'end'
17
25
  this._events = new Events;
18
26
 
19
27
  this._state = STATE_PAUSED;
28
+ this._renderedOnce = false;
20
29
  this._runningTicker = null;
30
+ this._responsiveBlocks = [];
31
+ if (this._responsive) {
32
+ this._responsiveEvent = ResponsiveEvent.global()?.add((...args) => {
33
+ this._onResponsive(...args);
34
+ });
35
+
36
+ // setup callback so we can notify our listeners
37
+ this._inner.setBeforeResponsiveFn(() => {
38
+ this._responsiveBlocks.forEach(block => {
39
+ block.responsive();
40
+ });
41
+ });
42
+ } else {
43
+ this._responsiveEvent = null;
44
+ }
21
45
  }
22
46
 
47
+ /**
48
+ * Set's properties
49
+ * offset can be staggered, a number, a label or a string `+=10`
50
+ */
23
51
  set(targets, props, offset = null) {
24
52
  // for the moment let's just add as usual but set the duration to 0
25
53
  return this.add(targets, { ...props, duration: 0 }, offset);
26
54
  }
27
55
 
56
+ /**
57
+ * Add an animation to the timeline
58
+ *
59
+ * offset can be staggered, a number, a label or a string `+=10`
60
+ */
28
61
  add(targets, props, offset = null) {
29
62
  if (Array.from(targets).length === 0)
30
63
  targets = [targets];
@@ -52,12 +85,41 @@ export default class PublicTimeline {
52
85
  return this;
53
86
  }
54
87
 
88
+ /**
89
+ * Adds a label at to the current nOffse
90
+ *
91
+ * This label can then be used in offsets
92
+ */
55
93
  label(label, offset = null) {
56
94
  this._inner.label(label, offset);
57
95
 
58
96
  return this;
59
97
  }
60
98
 
99
+ /**
100
+ * Adds a responsive function call which get's called before the responsive
101
+ * functions in properties
102
+ */
103
+ addResponsive(responsive) {
104
+ if (!this._responsiveEvent)
105
+ throw new Error('this timeline is not responsive');
106
+
107
+ if (Array.isArray(responsive)) {
108
+ responsive.forEach(resp => this.addResponsive(resp));
109
+ return this;
110
+ }
111
+
112
+ if (typeof responsive.responsive !== 'function')
113
+ throw new Error('expected a responsive() function');
114
+
115
+ this._responsiveBlocks.push(responsive);
116
+
117
+ return this;
118
+ }
119
+
120
+ /**
121
+ * Starts to play the timeline if it hasn't started
122
+ */
61
123
  play() {
62
124
  if (this._state === STATE_PLAYING)
63
125
  return;
@@ -69,6 +131,9 @@ export default class PublicTimeline {
69
131
  return this;
70
132
  }
71
133
 
134
+ /**
135
+ * Pause the timeline at the current position
136
+ */
72
137
  pause() {
73
138
  if (this._state === STATE_PAUSED)
74
139
  return;
@@ -78,7 +143,25 @@ export default class PublicTimeline {
78
143
  this._stopTicker();
79
144
  }
80
145
 
81
- // 0-1
146
+ /**
147
+ * Seeks to a position in the timeline
148
+ */
149
+ seekMs(ms) {
150
+ this._inner.init();
151
+
152
+ this._inner.seekMs(ms);
153
+
154
+ if (this._state !== STATE_PLAYING)
155
+ this._state = STATE_RENDER_ONCE;
156
+
157
+ this._startTicker();
158
+ }
159
+
160
+ /**
161
+ * Seeks to a normalized position in the timeline
162
+ *
163
+ * @param pos = number between 0 and 1
164
+ */
82
165
  seek(pos) {
83
166
  this._inner.init();
84
167
 
@@ -90,23 +173,94 @@ export default class PublicTimeline {
90
173
  this._startTicker();
91
174
  }
92
175
 
176
+ // seek in Range (useful for scroll timelines)
177
+ // seek(startOffset, endOffset, pos)
178
+
179
+ /**
180
+ * Resets the current timeline to the start
181
+ * without changing anything
182
+ */
93
183
  reset() {
94
184
  this._inner.seek(0);
95
185
  }
96
186
 
187
+ /**
188
+ * Resets every used prop to it's previous value
189
+ */
190
+ resetProps() {
191
+ const reversed = this._inner.timing.reverse;
192
+ this._inner.timing.reverse = false;
193
+ this._inner.seek(-1);
194
+ this._inner.render();
195
+ this._inner.ticker.applyTargets();
196
+
197
+ this._inner.timing.reverse = reversed;
198
+ this._inner.seek(-1);
199
+ }
200
+
201
+ /**
202
+ * Recalculates all values this should be called for example after a resize
203
+ */
204
+ update() {
205
+ this._inner.update();
206
+ }
207
+
208
+ /**
209
+ * Returns wether the timeline is set to reversed
210
+ */
211
+ isReversed() {
212
+ return this._inner.timing.reversed;
213
+ }
214
+
215
+ /**
216
+ * Set's the timeline to a specific direction
217
+ */
218
+ setReversed(reversed) {
219
+ if (this.isReversed() == reversed)
220
+ return;
221
+ this.reverse();
222
+ }
223
+
224
+ /**
225
+ * Reverses the order of the current timeline
226
+ */
97
227
  reverse() {
98
228
  this._inner.timing.reverse();
99
229
  }
100
230
 
101
- // start, update, end -> () => // remove Event
231
+ /**
232
+ * Listen on events
233
+ *
234
+ * returns a function to remove the event listener
235
+ *
236
+ * ## Events
237
+ *
238
+ * ### end
239
+ * Get's executed once the timeline comes to the end
240
+ * Does not get executed during seeking
241
+ *
242
+ */
102
243
  on(event, fn) {
103
244
  return this._events.add(event, fn);
104
245
  }
105
246
 
247
+ // not wroking
106
248
  onPromise(event) {
107
249
  return this._events.wait(event);
108
250
  }
109
251
 
252
+ /**
253
+ * Destroys this timeline and resets all props
254
+ */
255
+ destroy() {
256
+ this._stopTicker();
257
+ this._inner.destroy();
258
+ this._events.destroy();
259
+ this._responsiveEvent?.remove();
260
+ this._responsiveBlocks = [];
261
+ this._inner = null;
262
+ }
263
+
110
264
  _startTicker() {
111
265
  if (this._runningTicker)
112
266
  return;
@@ -114,16 +268,30 @@ export default class PublicTimeline {
114
268
  this._inner.init();
115
269
 
116
270
  this._runningTicker = this._inner.ticker.add(change => {
117
- if (this._inner.timing.state === STATE_BEFORE) {
118
- this._events.trigger('start');
119
- }
271
+ if (this._inner.timing.state >= STATE_ENDED) {
272
+ if (!this._renderedOnce) {
273
+ this._renderedOnce = true;
274
+ this._inner.render();
275
+ }
120
276
 
121
- if (this._inner.timing.state === STATE_AFTER) {
277
+ if (this._state === STATE_PLAYING)
278
+ this._events.trigger('end');
279
+
280
+ this._state = STATE_PAUSED;
122
281
  this._stopTicker();
123
- this._events.trigger('end');
124
- return;
282
+ return
125
283
  }
126
284
 
285
+ // if (this._inner.timing.state <= STATE_START) {
286
+ // this._events.trigger('start');
287
+ // }
288
+
289
+ // if (this._inner.timing.state >= STATE_ENDED) {
290
+ // this._stopTicker();
291
+ // this._events.trigger('end');
292
+ // return;
293
+ // }
294
+
127
295
  if (this._state === STATE_PLAYING)
128
296
  this._inner.advance(change);
129
297
 
@@ -131,6 +299,15 @@ export default class PublicTimeline {
131
299
 
132
300
  this._inner.render();
133
301
 
302
+ if (this._inner.timing.state >= STATE_ENDED) {
303
+ if (this._state === STATE_PLAYING)
304
+ this._events.trigger('end');
305
+
306
+ this._state = STATE_PAUSED;
307
+ this._stopTicker();
308
+ return
309
+ }
310
+
134
311
  // we rendered once let's stop
135
312
  if (this._state === STATE_RENDER_ONCE) {
136
313
  this._state = STATE_PAUSED;
@@ -147,8 +324,10 @@ export default class PublicTimeline {
147
324
  this._runningTicker = null;
148
325
  }
149
326
 
150
- // update() {
151
- // console.log('render');
152
- // this._inner.render();
153
- // }
327
+ _onResponsive(_obj) {
328
+ // // make sure we don't render something if we never did
329
+ // if (!this._renderedOnce)
330
+ // return;
331
+ this._inner.update();
332
+ }
154
333
  }
@@ -1,11 +1,17 @@
1
1
  import { takeProp } from '../utils/internal.js';
2
2
  import Timing, {
3
3
  parseEase, parseRepeat, parseAlternate, parseReversed,
4
- STATE_RUNNING
4
+ STATE_BEFORE, STATE_ENDED
5
5
  } from '../timing/timing.js';
6
6
  import Ticker from '../timing/ticker.js';
7
7
  import Animation from '../animation/animation.js';
8
8
 
9
+ /**
10
+ * Timeline is the most versatile class
11
+ *
12
+ * A timeline can contain multiple animation, ordered one after the other or
13
+ * offset.
14
+ */
9
15
  export default class Timeline {
10
16
  /*
11
17
  {
@@ -29,6 +35,7 @@ export default class Timeline {
29
35
  // { type: label|animation, value: {}, offset, start: x, end: y, render }
30
36
  this.entries = [];
31
37
 
38
+ this._beforeResponsiveFn = () => {};
32
39
  this._initialized = false;
33
40
  this._renderQueue = null;
34
41
  }
@@ -62,12 +69,23 @@ export default class Timeline {
62
69
  return this;
63
70
  }
64
71
 
72
+ // set's a function which get's called before any responsive call
73
+ setBeforeResponsiveFn(fn) {
74
+ this._beforeResponsiveFn = fn;
75
+ }
76
+
65
77
  advance(change) {
66
78
  this.timing.advance(change);
67
79
 
68
80
  this._updateTimings();
69
81
  }
70
82
 
83
+ seekMs(ms) {
84
+ this.timing.seekMs(ms);
85
+
86
+ this._updateTimings();
87
+ }
88
+
71
89
  seek(pos) {
72
90
  this.timing.seek(pos);
73
91
 
@@ -116,43 +134,44 @@ export default class Timeline {
116
134
 
117
135
  this.timing.setDuration(duration);
118
136
 
119
- this._initAnimations();
137
+ this._beforeResponsiveFn();
120
138
 
121
- // console.log(this.entries.map(e => e.value._props[0]));
139
+ this._initAnimations();
122
140
  }
123
141
 
124
- _initAnimations() {
125
- // we don't won't to have a reversed timing
142
+ _initAnimations(reset = false) {
143
+ // we don't wan't to have a reversed timing
126
144
  const reversed = this.timing.reversed;
127
145
  this.timing.reversed = false;
128
146
 
129
- // now initialize all animtaions
147
+ // now initialize all animations
148
+ // initialization means
149
+ // we need to define which value should be used as the from and the to
150
+ // value
130
151
  const startOrdered = this.entries.filter(e => e.type === 'animation');
131
152
  startOrdered.sort((a, b) => a.start - b.start);
132
153
 
133
154
  for (let i = 0; i < startOrdered.length; i++) {
134
155
  const entry = startOrdered[i];
135
156
 
136
- this.timing.seek(entry.start / this.timing.iterDuration);
157
+ this.timing.seekMs(entry.start);
137
158
 
138
- const pos = this.timing.position * this.timing.iterDuration;
159
+ const pos = this.timing.positionMs();
139
160
 
140
161
  // seek all previous and render
141
162
  for (let y = 0; y < i; y++) {
142
163
  const prevEntry = startOrdered[y];
143
164
  const prevAnimation = prevEntry.value;
144
165
 
145
- const p = (pos - prevEntry.start) / prevAnimation.duration;
146
-
147
- prevAnimation.seek(p);
166
+ prevAnimation.seekMs(pos - prevEntry.start);
148
167
  prevAnimation.render();
149
168
  }
150
169
 
151
- entry.value.init();
170
+ entry.value.init(reset);
152
171
  }
153
172
 
154
- this.timing.seek(-1);
155
173
  this.timing.reversed = reversed;
174
+ this.timing.seek(-1);
156
175
  startOrdered.forEach(e => {
157
176
  e.value.seek(-1);
158
177
  e.value.render();
@@ -185,29 +204,64 @@ export default class Timeline {
185
204
  active: []
186
205
  };
187
206
 
188
- const pos = this.timing.position * this.timing.iterDuration;
207
+ const pos = this.timing.positionMsUnbounded();
189
208
 
190
209
  for (const entry of this.entries) {
191
210
  if (entry.type !== 'animation')
192
211
  continue;
193
212
 
194
213
  const animation = entry.value;
214
+ animation.seekMs(pos - entry.start);
195
215
 
196
- const p = (pos - entry.start) / animation.duration;
197
-
198
- const prevState = animation.timing.state;
199
-
200
- animation.seek(p);
201
-
202
- if (p > 1) {
216
+ if (animation.timing.state >= STATE_ENDED) {
203
217
  this._renderQueue.passed.push(animation);
204
- } else if (p < 0) {
218
+ } else if (animation.timing.state <= STATE_BEFORE) {
205
219
  this._renderQueue.upcoming.push(animation);
206
220
  } else {
207
221
  this._renderQueue.active.push(animation);
208
222
  }
209
223
  }
210
224
  }
225
+
226
+ update() {
227
+ if (!this._initialized)
228
+ return;
229
+
230
+ const pos = this.timing.savePosition();
231
+ const reversed = this.timing.reversed;
232
+ this.timing.reversed = false;
233
+
234
+ this.timing.seek(-1);
235
+ this._updateTimings();
236
+ this.render();
237
+ this.ticker.applyTargets();
238
+
239
+ this._beforeResponsiveFn();
240
+
241
+ // we now have removed every style we set
242
+ // we should be able to recalculate everything now
243
+ this._initAnimations(true);
244
+
245
+ this.timing.reversed = reversed;
246
+ this.timing.restorePosition(pos);
247
+ this._updateTimings();
248
+ this.render();
249
+ this.ticker.applyTargets();
250
+ }
251
+
252
+ destroy() {
253
+ for (const entry of this.entries) {
254
+ if (entry.type !== 'animation')
255
+ continue;
256
+
257
+ const animation = entry.value;
258
+ animation.destroy();
259
+ }
260
+
261
+ this.entries = [];
262
+ this._beforeResponsiveFn = null;
263
+ this.ticker.applyTargets();
264
+ }
211
265
  }
212
266
 
213
267