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,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
  }
@@ -1,12 +1,28 @@
1
- import Ease from './ease.js';
2
1
  import { takeProp } from '../utils/internal.js';
3
2
 
4
3
  export const STATE_BEFORE = 0;
4
+ /// start only occurs if the current position is exactly 0 or 1 if reversed
5
5
  export const STATE_START = 1;
6
6
  export const STATE_RUNNING = 2;
7
+ /// ended only occurs if the current position is exactly 1 or 0 if reversed
7
8
  export const STATE_ENDED = 3;
8
9
  export const STATE_AFTER = 4;
9
10
 
11
+ /**
12
+ * This class handles everything timing related
13
+ *
14
+ * It manages easing, keeps track of where we are at the moment
15
+ *
16
+ * The most important property is position which gives the calculated position
17
+ *
18
+ *
19
+ * Timing works the same as in after effects
20
+ * if the duration is not at least 1ms the position is always 1 and either
21
+ * STATE_BEFORE or STATE_AFTER
22
+ *
23
+ * if the duration is 1ms seek 0 or 1 will always result in position 1
24
+ * because it is like one frame, so it's an image
25
+ */
10
26
  export default class Timing {
11
27
  // removes the relevant properties from the props
12
28
 
@@ -26,23 +42,49 @@ export default class Timing {
26
42
  }
27
43
  */
28
44
  constructor(props) {
45
+ // these vars are only readonly
46
+ // use functions to update them
47
+
29
48
  this.setDuration(props.duration);
30
49
  this.ease = props.ease ?? (t => t);
31
50
  this.repeat = props.repeat ?? 0;
32
51
  this.alternate = props.alternate ?? true;
33
52
  this.reversed = props.reversed ?? false;
34
53
 
35
- this.position = 0;
54
+ this.position = this.duration > 0 ? 0 : 1;
36
55
  this.state = STATE_BEFORE;
37
56
 
38
57
  // progress does not take alternation into account and has no easing
39
- // this always increments up until it overflows
58
+ // this always increments up
40
59
  this._progress = 0;
41
- // this._reversed = false;
42
60
 
43
61
  // scrub
44
62
  }
45
63
 
64
+ positionMs() {
65
+ return this.position * this.iterDuration;
66
+ }
67
+
68
+ // todo find a better name
69
+ /**
70
+ * Returns the position in milliseconds
71
+ *
72
+ * If the state is before or after the position might be more/less than
73
+ * required
74
+ */
75
+ positionMsUnbounded() {
76
+ const outOfBounds = this.state <= STATE_BEFORE
77
+ || this.state >= STATE_AFTER;
78
+
79
+ if (!outOfBounds)
80
+ return this.position * this.iterDuration;
81
+
82
+ if (this.position === 0)
83
+ return -1;
84
+ return this.position * this.iterDuration + 1;
85
+ }
86
+
87
+ /// iterDuration should always be >=0
46
88
  setDuration(iterDuration) {
47
89
  this.iterDuration = iterDuration;
48
90
 
@@ -60,9 +102,23 @@ export default class Timing {
60
102
  if (this.state >= STATE_AFTER)
61
103
  return;
62
104
 
105
+ if (this.iterDuration <= 0) {
106
+ this._updateProgress(1);
107
+ return;
108
+ }
109
+
63
110
  this._updateProgress(this._progress + change / this.iterDuration);
64
111
  }
65
112
 
113
+ seekMs(ms) {
114
+ if (this.iterDuration <= 0) {
115
+ this._updateProgress(Math.sign(ms));
116
+ return;
117
+ }
118
+
119
+ this._updateProgress(ms / this.iterDuration);
120
+ }
121
+
66
122
  // should be between 0-1 if outside might change the state
67
123
  seek(pos) {
68
124
  if (this.repeat <= 0) {
@@ -73,19 +129,73 @@ export default class Timing {
73
129
  }
74
130
  }
75
131
 
132
+ savePosition() {
133
+ return {
134
+ progress: this._progress
135
+ };
136
+ }
137
+
138
+ restorePosition(pos) {
139
+ this._updateProgress(pos.progress);
140
+ }
141
+
142
+ setAlternate(alternate) {
143
+ if (this.alternate == alternate)
144
+ return;
145
+
146
+ // make sure the reversed is updated according to the current inversion
147
+ this.reversed = alternate == this._shouldInvert();
148
+ this.alternate = alternate;
149
+ }
150
+
76
151
  reverse() {
77
152
  this.reversed = !this.reversed;
78
153
 
79
- let p = Math.floor(this._progress);
80
- p = p + (1 - (this._progress - p));
154
+ const wrapAround = p => {
155
+ if (this.repeat > -1)
156
+ return p % (this.repeat + 1);
157
+ return p;
158
+ };
159
+
160
+ let p;
161
+ if (this.state <= STATE_BEFORE) {
162
+ p = 2;
163
+ } else if (this.state >= STATE_AFTER) {
164
+ p = -1;
165
+ // check if the values should be reversed
166
+ } else if (this.reversed != this._shouldInvert()) {
167
+ let decimal = 1 - this._progress % 1;
168
+ p = wrapAround(Math.floor(this._progress) + decimal);
169
+ } else {
170
+ p = wrapAround(this._progress);
171
+ }
81
172
 
82
173
  this._updateProgress(p);
83
174
  }
84
175
 
176
+ _shouldInvert() {
177
+ return this.alternate && Math.floor(this._progress) % 2 === 1;
178
+ }
179
+
85
180
  _updateProgress(newProgress) {
181
+ // if duration is zero the position is always 1
182
+ // and the state is either BEFORE or AFTER
183
+ if (this.iterDuration <= 0) {
184
+ newProgress = Math.sign(newProgress);
185
+ this._progress = newProgress;
186
+ this.position = 1;
187
+
188
+ if (newProgress < 0)
189
+ this.state = STATE_BEFORE;
190
+ else
191
+ this.state = STATE_AFTER;
192
+
193
+ return;
194
+ }
195
+
86
196
  this._progress = newProgress;
87
197
 
88
- // since infite repeat can never end let's ignore it
198
+ // since infinite repeat can never end let's ignore it
89
199
  if (this.repeat > -1) {
90
200
  const end = this.repeat + 1;
91
201
  if (this._progress > end) {
@@ -108,21 +218,12 @@ export default class Timing {
108
218
  this.state = STATE_START;
109
219
  }
110
220
 
111
-
112
- let reversed = this.reversed;
113
-
114
- // calc if we are in a reversed iteration or not
115
- if (this.alternate) {
116
- let shouldInverse = Math.floor(this._progress) % 2 === 1;
117
- if (shouldInverse)
118
- reversed = !reversed;
119
- }
120
-
121
221
  // now calculate the position
122
-
123
222
  this.position = this._progress % 1;
124
223
 
125
- if (reversed)
224
+ // calc if we are in a reversed iteration or not
225
+ // and we are we should invert the position
226
+ if (this.reversed != this._shouldInvert())
126
227
  this.position = 1 - this.position;
127
228
 
128
229
  this.position = this.ease(this.position);
@@ -145,10 +246,9 @@ export function parseDuration(dur) {
145
246
  if (typeof dur !== 'number')
146
247
  throw new Error('duration is not a number');
147
248
 
148
- // convert s to ms
149
249
  dur = Math.round(dur);
150
250
 
151
- return Math.max(dur, 1);
251
+ return Math.max(dur, 0);
152
252
  }
153
253
 
154
254
  export function parseEase(ease) {
@@ -32,4 +32,8 @@ export default class Events {
32
32
  return;
33
33
  list.trigger(...args);
34
34
  }
35
+
36
+ destroy() {
37
+ this.listeners = new Map;
38
+ }
35
39
  }
@@ -8,6 +8,9 @@ function catmullRom(p0, p1, p2, p3, t, p1Tension, p2Tension) {
8
8
  t3 + (-3 * p1 + 3 * p2 - 2 * v0 - v1) * t2 + v0 * t + p1;
9
9
  }
10
10
 
11
+ /**
12
+ * Calculates a curve between different points
13
+ */
11
14
  export function curve(points, tension = 1, opts = {}) {
12
15
  if (points.length <= 1)
13
16
  throw new Error('curve expects at least 2 points');
@@ -2,19 +2,37 @@ import { curve } from './motionpath.js';
2
2
 
3
3
  export { curve };
4
4
 
5
+ /**
6
+ * Explicitly tells a property to animate the to value
7
+ */
5
8
  export function to(to) {
6
9
  return { to };
7
10
  }
8
11
 
12
+ /**
13
+ * Explicitly tells a property to animate the from value
14
+ */
9
15
  export function from(from) {
10
16
  return { from };
11
17
  }
12
18
 
19
+ /**
20
+ * Explicitly tells a property to animate with the from and to values
21
+ */
13
22
  export function fromTo(from, to) {
14
23
  return { from, to };
15
24
  }
16
25
 
17
- // converts some element
26
+ /**
27
+ * makes a value reactive to `Timeline::update` calls or resizes
28
+ */
29
+ export function responsive(fn) {
30
+ return { responsive: fn };
31
+ }
32
+
33
+ /**
34
+ * Converts an element with text into span with chars
35
+ */
18
36
  export function chars(element) {
19
37
  if (element.dataset.processedChars)
20
38
  return Array.from(element.children);
@@ -34,7 +52,9 @@ export function chars(element) {
34
52
  });
35
53
  }
36
54
 
37
- // get the offset to the page
55
+ /**
56
+ * get the offset to the current page page
57
+ */
38
58
  export function pageOffset(el) {
39
59
  if ('offsetTop' in el) {
40
60
  return {
@@ -0,0 +1,167 @@
1
+ import { COLORS } from './defaultcolors.js';
2
+
3
+ export default class ColorValue {
4
+ // [r, g, b, a?]
5
+ constructor(v) {
6
+ if (v.length === 3)
7
+ v = [...v, 1];
8
+
9
+ // always [r, g, b, a]
10
+ this.values = v;
11
+ }
12
+
13
+ static parse(v) {
14
+ if (Array.isArray(v)) {
15
+ if (v.length < 3 || v.length > 4)
16
+ throw new Error('expected array [r, g, b, a]');
17
+
18
+ return new ColorValue(v);
19
+ }
20
+
21
+ if (typeof v !== 'string')
22
+ throw new Error('expected a string or an array');
23
+
24
+ v = v.trim();
25
+
26
+ if (v.startsWith('#'))
27
+ return new ColorValue(parseHex(v.substring(1)));
28
+ if (v.startsWith('rgb'))
29
+ return new ColorValue(parseRgb(v));
30
+ if (v.startsWith('hsl'))
31
+ return new ColorValue(parseHsl(v));
32
+
33
+ if (v in COLORS)
34
+ return new ColorValue(COLORS[v]);
35
+
36
+ throw new Error('unknown color value ' + v);
37
+ }
38
+
39
+ // v needs to be a string
40
+ static mightBeAColor(v) {
41
+ v = v.trim();
42
+
43
+ return v.startsWith('#')
44
+ || v.startsWith('rgb')
45
+ || v.startsWith('hsl')
46
+ || v in COLORS;
47
+ }
48
+
49
+ clone() {
50
+ return new ColorValue(this.values.slice());
51
+ }
52
+
53
+ toString() {
54
+ const [r, g, b, a] = this.values;
55
+ return `rgba(${[r, g, b].map(v => Math.round(v)).join(', ')}, ${
56
+ a.toFixed(3)
57
+ })`;
58
+ }
59
+ }
60
+
61
+ // hex without the hash
62
+ function parseHex(hex) {
63
+ if (hex.length !== 3 && hex.length !== 6 && hex.length !== 8)
64
+ throw new Error('unknown hex value ' + hex);
65
+
66
+ // Convert 3-character hex to 6-character hex
67
+ if (hex.length === 3)
68
+ hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
69
+
70
+ // Parse the hex values
71
+ const r = parseInt(hex.substring(0, 2), 16);
72
+ const g = parseInt(hex.substring(2, 4), 16);
73
+ const b = parseInt(hex.substring(4, 6), 16);
74
+ let a = 255;
75
+
76
+ if (hex.length === 8)
77
+ a = parseInt(hex.substring(6, 8), 16);
78
+
79
+ return [r, g, b, a / 255];
80
+ }
81
+
82
+ // rgb needs to start with rgb
83
+ function parseRgb(rgb) {
84
+ const rgba = rgb.startsWith('rgba');
85
+ // remove the function name
86
+ rgb = rgb.substring(3 + rgba);
87
+ if (!rgb.startsWith('(') && !rgb.endsWith(')'))
88
+ throw new Error('invalid rgb function ' + rgb);
89
+
90
+ rgb = rgb.substring(1, rgb.length - 1);
91
+ const vals = rgb.split(',');
92
+
93
+ if ((rgba && vals.length !== 4) || vals.length !== 3)
94
+ throw new Error('invalid rgb function ' + rgb);
95
+
96
+ const r = parseInt(vals[0].trim());
97
+ const g = parseInt(vals[1].trim());
98
+ const b = parseInt(vals[2].trim());
99
+ const a = rgba ? parseFloat(vals[3].trim()) : 1;
100
+
101
+ return [r, g, b, a];
102
+ }
103
+
104
+ // hsl needs to start with hsl
105
+ function parseHsl(hsl) {
106
+ const hsla = hsl.startsWith('hsla');
107
+ // Remove the function name
108
+ hsl = hsl.substring(3 + hsla);
109
+ if (!hsl.startsWith('(') || !hsl.endsWith(')'))
110
+ throw new Error('invalid hsl function ' + hsl);
111
+
112
+ hsl = hsl.substring(1, hsl.length - 1);
113
+ let vals = hsl.split(',');
114
+
115
+ if ((hsla && vals.length !== 4) || (!hsla && vals.length !== 3))
116
+ throw new Error('invalid hsl function ' + hsl);
117
+
118
+ vals = vals.map(v => v.trim());
119
+
120
+ if (!vals[1].endsWith('%') && !vals[2].endsWith('%'))
121
+ throw new Error('invalid hsl function ' + hsl);
122
+
123
+ const h = parseInt(vals[0]);
124
+ const s = parseInt(vals[1].substring(0, vals[1].length - 1));
125
+ const l = parseInt(vals[2].substring(0, vals[2].length - 1));
126
+ const a = hsla ? parseFloat(vals[3]) : 1;
127
+
128
+ return hslToRgba([h, s, l, a]);
129
+ }
130
+
131
+ /// expects [h, s, l, a]
132
+ function hslToRgba(hsla) {
133
+ let [h, s, l, a] = hsla;
134
+
135
+ // normalize
136
+ h /= 360;
137
+ s /= 100;
138
+ l /= 100;
139
+
140
+ let r, g, b;
141
+ if (s === 0) {
142
+ r = g = b = l;
143
+ } else {
144
+ const q = l < .5 ? l * (1 + s) : l + s - l * s;
145
+ const p = 2 * l - q;
146
+ r = Math.round(hueToRgb(p, q, h + 1 / 3) * 255, 0);
147
+ g = Math.round(hueToRgb(p, q, h) * 255, 0);
148
+ b = Math.round(hueToRgb(p, q, h - 1 / 3) * 255, 0);
149
+ }
150
+
151
+ return [r, g, b, a];
152
+ }
153
+
154
+ function hueToRgb(p, q, t) {
155
+ if (t < 0)
156
+ t += 1;
157
+ if (t > 1)
158
+ t -= 1;
159
+
160
+ if (t < 1 / 6)
161
+ return p + (q - p) * 6 * t;
162
+ if (t < 1 / 2)
163
+ return q;
164
+ if (t < 2 / 3)
165
+ return p + (q - p) * (2 / 3 - t) * 6;
166
+ return p;
167
+ }