chnobli 0.1.2 → 0.1.3

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,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
 
@@ -0,0 +1,98 @@
1
+ import { GlobalTargets } from './ticker.js';
2
+
3
+ export default class TestTicker {
4
+ constructor() {
5
+ this._listeners = new Set;
6
+ this.targets = new GlobalTargets;
7
+
8
+ this.running = false;
9
+
10
+ this._previousTick = null;
11
+
12
+ globalThis.chnobliTicker = this;
13
+ }
14
+
15
+ registerTarget(target) {
16
+ return this.targets.register(target);
17
+ }
18
+
19
+ unregisterTarget(target) {
20
+ return this.targets.unregister(target);
21
+ }
22
+
23
+ applyTargets() {
24
+ return this.targets.apply();
25
+ }
26
+
27
+ add(fn) {
28
+ this._listeners.add(fn);
29
+
30
+ if (!this.running)
31
+ this.running = true;
32
+
33
+ return {
34
+ remove: () => {
35
+ this.remove(fn);
36
+ }
37
+ };
38
+ }
39
+
40
+ remove(fn) {
41
+ this._listeners.delete(fn);
42
+ }
43
+
44
+ /// max expects a number for how many ms the ticker should run
45
+ ///
46
+ /// runs until the ticker ends this allows to run
47
+ run(max = 3000, step = 1) {
48
+ if (!this.running)
49
+ return true;
50
+
51
+ if (this._previousTick === null)
52
+ this.tick(0);
53
+
54
+ let i = 0;
55
+ const start = this._previousTick ?? 0;
56
+
57
+ for (i = 1; i <= max; i += step) {
58
+ if (!this.tick(start + i))
59
+ return true;
60
+ }
61
+
62
+ return false;
63
+ }
64
+
65
+ // elapsed in ms
66
+ tick(elapsed) {
67
+ if (!this.running)
68
+ return false;
69
+
70
+ if (this._previousTick === null)
71
+ this._previousTick = elapsed;
72
+
73
+ let change = elapsed - this._previousTick;
74
+ // lagSmoothing
75
+ if (change > 500) {
76
+ console.log('ticker lagged');
77
+ change = 33;
78
+ }
79
+ this._previousTick = elapsed;
80
+
81
+ for (const fn of this._listeners) {
82
+ fn(change, {
83
+ // changes,
84
+ remove: () => this.remove(fn)
85
+ });
86
+ }
87
+
88
+ this.targets.apply();
89
+
90
+ if (this._listeners.size === 0) {
91
+ this.running = false;
92
+ this._previousTick = null;
93
+ return false;
94
+ }
95
+
96
+ return true;
97
+ }
98
+ }
@@ -1,7 +1,11 @@
1
- import Value from '../utils/value.js';
2
- import { newTarget } from '../utils/target.js';
3
-
4
-
1
+ import { newTarget } from '../target/target.js';
2
+
3
+ /**
4
+ * The ticker is the main driver for all animations
5
+ *
6
+ * it also manages all targets allowing to only call the dom api
7
+ * at the end of every frame
8
+ */
5
9
  export default class Ticker {
6
10
  constructor() {
7
11
  this._listeners = new Set;
@@ -13,14 +17,30 @@ export default class Ticker {
13
17
  }
14
18
 
15
19
  static global() {
20
+ // allow to define a test ticker
21
+ if (typeof globalThis !== 'undefined' && globalThis.chnobliTicker)
22
+ return globalThis.chnobliTicker;
23
+
16
24
  if (typeof window === 'undefined')
17
- return null;
25
+ throw new Error('can only be executed in a browser context');
18
26
 
19
27
  if (typeof window.chnobliTicker === 'undefined')
20
28
  window.chnobliTicker = new Ticker;
21
29
  return window.chnobliTicker;
22
30
  }
23
31
 
32
+ registerTarget(target) {
33
+ return this._targets.register(target);
34
+ }
35
+
36
+ unregisterTarget(target) {
37
+ return this._targets.unregister(target);
38
+ }
39
+
40
+ applyTargets() {
41
+ return this._targets.apply();
42
+ }
43
+
24
44
  add(fn) {
25
45
  this._listeners.add(fn);
26
46
 
@@ -91,24 +111,44 @@ export default class Ticker {
91
111
  // }
92
112
  // }
93
113
 
94
- class GlobalTargets {
114
+ export class GlobalTargets {
95
115
  constructor() {
116
+ // {target, ref}
96
117
  this._targets = new Map;
97
118
  }
98
119
 
99
120
  register(target) {
100
121
  let storedTarget = this._targets.get(target);
101
122
  if (!storedTarget) {
102
- storedTarget = newTarget(target);
123
+ storedTarget = {
124
+ target: newTarget(target),
125
+ ref: 0
126
+ };
103
127
  this._targets.set(target, storedTarget);
104
128
  }
129
+ storedTarget.ref += 1;
130
+
131
+ return storedTarget.target;
132
+ }
105
133
 
106
- return storedTarget;
134
+ unregister(target) {
135
+ const storedTarget = this._targets.get(target);
136
+ storedTarget.ref -= 1;
107
137
  }
108
138
 
109
139
  apply() {
110
- for (const target of this._targets.values()) {
140
+ for (const storedTarget of this._targets.values()) {
141
+ const { target, ref } = storedTarget;
111
142
  target.apply();
143
+
144
+ if (ref <= 0)
145
+ this._targets.delete(target.target);
112
146
  }
113
147
  }
148
+
149
+ size() {
150
+ return this._targets.size;
151
+ }
152
+
153
+ // todo we should probly run some kind of gc
114
154
  }