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.
- package/.eslintignore +1 -0
- package/.github/workflows/ci.yaml +14 -1
- package/LICENSE.md +1 -1
- package/docs/INDEX.md +15 -0
- package/jsdoc.conf.json +1 -1
- package/package.json +6 -3
- package/src/animation/animation.js +88 -25
- package/src/animation/property.js +412 -47
- package/src/animation/public.js +72 -2
- package/src/animator.js +2 -0
- package/src/chnobli.js +8 -3
- package/src/responsive/event.js +46 -0
- package/src/responsive/testevent.js +31 -0
- package/src/scroll/event.js +1 -1
- package/src/stagger/stagger.js +5 -8
- package/src/target/basetarget.js +92 -0
- package/src/target/domtarget.js +266 -0
- package/src/target/target.js +22 -0
- package/src/target/testdomnode.js +61 -0
- package/src/tests/properties.test.js +178 -0
- package/src/tests/timeline.test.js +234 -0
- package/src/tests/timing.test.js +270 -0
- package/src/tests/usecases.test.js +64 -0
- package/src/tests/values.test.js +162 -0
- package/src/timeline/public.js +192 -13
- package/src/timeline/timeline.js +76 -22
- package/src/timing/testticker.js +98 -0
- package/src/timing/ticker.js +49 -9
- package/src/timing/timing.js +121 -21
- package/src/utils/events.js +4 -0
- package/src/utils/motionpath.js +3 -0
- package/src/utils/utils.js +22 -2
- package/src/values/colorvalue.js +167 -0
- package/src/values/defaultcolors.js +148 -0
- package/src/values/stylevalue.js +148 -0
- package/src/{utils → values}/value.js +15 -7
- package/src/utils/target.js +0 -164
|
@@ -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
|
+
}
|
package/src/timing/ticker.js
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
134
|
+
unregister(target) {
|
|
135
|
+
const storedTarget = this._targets.get(target);
|
|
136
|
+
storedTarget.ref -= 1;
|
|
107
137
|
}
|
|
108
138
|
|
|
109
139
|
apply() {
|
|
110
|
-
for (const
|
|
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
|
}
|
package/src/timing/timing.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
80
|
-
|
|
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
|
|
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
|
|
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,
|
|
251
|
+
return Math.max(dur, 0);
|
|
152
252
|
}
|
|
153
253
|
|
|
154
254
|
export function parseEase(ease) {
|
package/src/utils/events.js
CHANGED
package/src/utils/motionpath.js
CHANGED
|
@@ -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');
|
package/src/utils/utils.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
+
}
|