chnobli 0.1.0 → 0.1.2
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/README.md +3 -10
- package/package.json +9 -3
- package/src/animation/animation.js +203 -0
- package/src/animation/property.js +191 -0
- package/src/animation/public.js +53 -0
- package/src/animator.js +185 -0
- package/src/chnobli.js +64 -0
- package/src/scroll/event.js +41 -0
- package/src/scroll/public.js +17 -0
- package/src/scroll/scroll.js +169 -0
- package/src/stagger/stagger.js +45 -0
- package/src/timeline/public.js +154 -0
- package/src/timeline/timeline.js +250 -0
- package/src/timing/ease.js +16 -0
- package/src/timing/easing.js +292 -0
- package/src/timing/ticker.js +114 -0
- package/src/timing/timing.js +195 -0
- package/src/utils/events.js +35 -0
- package/src/utils/internal.js +7 -0
- package/src/utils/motionpath.js +44 -0
- package/src/utils/target.js +164 -0
- package/src/utils/utils.js +55 -0
- package/src/utils/value.js +38 -0
- package/LICENSE-APACHE +0 -201
- /package/{LICENSE-MIT → LICENSE.md} +0 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import Ease from './ease.js';
|
|
2
|
+
import { takeProp } from '../utils/internal.js';
|
|
3
|
+
|
|
4
|
+
export const STATE_BEFORE = 0;
|
|
5
|
+
export const STATE_START = 1;
|
|
6
|
+
export const STATE_RUNNING = 2;
|
|
7
|
+
export const STATE_ENDED = 3;
|
|
8
|
+
export const STATE_AFTER = 4;
|
|
9
|
+
|
|
10
|
+
export default class Timing {
|
|
11
|
+
// removes the relevant properties from the props
|
|
12
|
+
|
|
13
|
+
/*
|
|
14
|
+
seeking
|
|
15
|
+
if repeat is infinite
|
|
16
|
+
seek will only control the current iteration
|
|
17
|
+
if repeat is finite
|
|
18
|
+
seek will control the entire time
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
{
|
|
22
|
+
duration: in ms
|
|
23
|
+
ease: easeFunction
|
|
24
|
+
repeat: -1 - x,
|
|
25
|
+
alternate
|
|
26
|
+
}
|
|
27
|
+
*/
|
|
28
|
+
constructor(props) {
|
|
29
|
+
this.setDuration(props.duration);
|
|
30
|
+
this.ease = props.ease ?? (t => t);
|
|
31
|
+
this.repeat = props.repeat ?? 0;
|
|
32
|
+
this.alternate = props.alternate ?? true;
|
|
33
|
+
this.reversed = props.reversed ?? false;
|
|
34
|
+
|
|
35
|
+
this.position = 0;
|
|
36
|
+
this.state = STATE_BEFORE;
|
|
37
|
+
|
|
38
|
+
// progress does not take alternation into account and has no easing
|
|
39
|
+
// this always increments up until it overflows
|
|
40
|
+
this._progress = 0;
|
|
41
|
+
// this._reversed = false;
|
|
42
|
+
|
|
43
|
+
// scrub
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
setDuration(iterDuration) {
|
|
47
|
+
this.iterDuration = iterDuration;
|
|
48
|
+
|
|
49
|
+
this.duration = this.iterDuration;
|
|
50
|
+
if (this.repeat > -1) {
|
|
51
|
+
this.duration = (this.repeat + 1) * this.iterDuration;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
isFinite() {
|
|
56
|
+
return this.repeat > -1;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
advance(change) {
|
|
60
|
+
if (this.state >= STATE_AFTER)
|
|
61
|
+
return;
|
|
62
|
+
|
|
63
|
+
this._updateProgress(this._progress + change / this.iterDuration);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// should be between 0-1 if outside might change the state
|
|
67
|
+
seek(pos) {
|
|
68
|
+
if (this.repeat <= 0) {
|
|
69
|
+
this._updateProgress(pos);
|
|
70
|
+
} else {
|
|
71
|
+
// finite
|
|
72
|
+
this._updateProgress(pos * (this.repeat + 1));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
reverse() {
|
|
77
|
+
this.reversed = !this.reversed;
|
|
78
|
+
|
|
79
|
+
let p = Math.floor(this._progress);
|
|
80
|
+
p = p + (1 - (this._progress - p));
|
|
81
|
+
|
|
82
|
+
this._updateProgress(p);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
_updateProgress(newProgress) {
|
|
86
|
+
this._progress = newProgress;
|
|
87
|
+
|
|
88
|
+
// since infite repeat can never end let's ignore it
|
|
89
|
+
if (this.repeat > -1) {
|
|
90
|
+
const end = this.repeat + 1;
|
|
91
|
+
if (this._progress > end) {
|
|
92
|
+
this._progress = end;
|
|
93
|
+
this.state = STATE_AFTER;
|
|
94
|
+
} else if (this._progress === end) {
|
|
95
|
+
this.state = STATE_ENDED;
|
|
96
|
+
} else {
|
|
97
|
+
this.state = STATE_RUNNING;
|
|
98
|
+
}
|
|
99
|
+
} else {
|
|
100
|
+
this.state = STATE_RUNNING;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// override state if not running
|
|
104
|
+
if (this._progress < 0) {
|
|
105
|
+
this._progress = 0;
|
|
106
|
+
this.state = STATE_BEFORE;
|
|
107
|
+
} else if (this._progress === 0) {
|
|
108
|
+
this.state = STATE_START;
|
|
109
|
+
}
|
|
110
|
+
|
|
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
|
+
// now calculate the position
|
|
122
|
+
|
|
123
|
+
this.position = this._progress % 1;
|
|
124
|
+
|
|
125
|
+
if (reversed)
|
|
126
|
+
this.position = 1 - this.position;
|
|
127
|
+
|
|
128
|
+
this.position = this.ease(this.position);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function newTiming(props) {
|
|
133
|
+
const nProps = {
|
|
134
|
+
duration: parseDuration(takeProp(props, 'duration', 1000)),
|
|
135
|
+
ease: parseEase(takeProp(props, 'ease', null)),
|
|
136
|
+
repeat: parseRepeat(takeProp(props, 'repeat', false)),
|
|
137
|
+
alternate: parseAlternate(takeProp(props, 'alternate', true))
|
|
138
|
+
};
|
|
139
|
+
// this._delay = parseDelay(takeProp(props, 'delay', 0));
|
|
140
|
+
|
|
141
|
+
return new Timing(nProps);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function parseDuration(dur) {
|
|
145
|
+
if (typeof dur !== 'number')
|
|
146
|
+
throw new Error('duration is not a number');
|
|
147
|
+
|
|
148
|
+
// convert s to ms
|
|
149
|
+
dur = Math.round(dur);
|
|
150
|
+
|
|
151
|
+
return Math.max(dur, 1);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function parseEase(ease) {
|
|
155
|
+
if (ease !== null && typeof ease !== 'function')
|
|
156
|
+
throw new Error('ease needs to be null or a function');
|
|
157
|
+
|
|
158
|
+
return ease;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function parseRepeat(repeat) {
|
|
162
|
+
if (
|
|
163
|
+
repeat !== true && repeat !== false &&
|
|
164
|
+
(typeof repeat !== 'number' || repeat < 0)
|
|
165
|
+
)
|
|
166
|
+
throw new Error('repeat needs to be true|false|0+');
|
|
167
|
+
|
|
168
|
+
if (repeat === true)
|
|
169
|
+
repeat = -1;
|
|
170
|
+
else if (repeat === false)
|
|
171
|
+
repeat = 0;
|
|
172
|
+
|
|
173
|
+
return repeat;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function parseDelay(delay) {
|
|
177
|
+
if (typeof delay !== 'number' || delay < 0)
|
|
178
|
+
throw new Error('delay needs to be 0+');
|
|
179
|
+
|
|
180
|
+
return delay;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function parseAlternate(alternate) {
|
|
184
|
+
if (alternate !== true && alternate !== false)
|
|
185
|
+
throw new Error('alternate needs to be true|false');
|
|
186
|
+
|
|
187
|
+
return alternate;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function parseReversed(reversed) {
|
|
191
|
+
if (reversed !== true && reversed !== false)
|
|
192
|
+
throw new Error('reversed needs to be true|false');
|
|
193
|
+
|
|
194
|
+
return reversed;
|
|
195
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import Listeners from 'fire/util/listeners.js';
|
|
2
|
+
|
|
3
|
+
export default class Events {
|
|
4
|
+
constructor() {
|
|
5
|
+
this.listeners = new Map;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
add(event, fn) {
|
|
9
|
+
let list = this.listeners.get(event);
|
|
10
|
+
if (!list) {
|
|
11
|
+
list = new Listeners;
|
|
12
|
+
this.listeners.set(event, list);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return list.add(fn);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
wait(event) {
|
|
19
|
+
return new Promise(resolve => {
|
|
20
|
+
let rm = () => {};
|
|
21
|
+
const call = a => {
|
|
22
|
+
rm();
|
|
23
|
+
resolve(a);
|
|
24
|
+
};
|
|
25
|
+
rm = this.add(event, call);
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
trigger(event, ...args) {
|
|
30
|
+
let list = this.listeners.get(event);
|
|
31
|
+
if (!list)
|
|
32
|
+
return;
|
|
33
|
+
list.trigger(...args);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
function catmullRom(p0, p1, p2, p3, t, p1Tension, p2Tension) {
|
|
2
|
+
let v0 = (p2 - p0) * p1Tension;
|
|
3
|
+
let v1 = (p3 - p1) * p2Tension;
|
|
4
|
+
let t2 = t * t;
|
|
5
|
+
let t3 = t2 * t;
|
|
6
|
+
|
|
7
|
+
return (2 * p1 - 2 * p2 + v0 + v1) *
|
|
8
|
+
t3 + (-3 * p1 + 3 * p2 - 2 * v0 - v1) * t2 + v0 * t + p1;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function curve(points, tension = 1, opts = {}) {
|
|
12
|
+
if (points.length <= 1)
|
|
13
|
+
throw new Error('curve expects at least 2 points');
|
|
14
|
+
|
|
15
|
+
let startEndStraight = opts.startEndStraight ?? true;
|
|
16
|
+
if (startEndStraight) {
|
|
17
|
+
points[0].straight = true;
|
|
18
|
+
points[points.length - 1].straight = true;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return t => {
|
|
22
|
+
const segment = Math.floor(t * (points.length - 1));
|
|
23
|
+
const localT = (t * (points.length - 1)) - segment;
|
|
24
|
+
|
|
25
|
+
const p0 = points[Math.max(0, segment - 1)];
|
|
26
|
+
const p1 = points[segment];
|
|
27
|
+
const p2 = points[Math.min(points.length - 1, segment + 1)];
|
|
28
|
+
const p3 = points[Math.min(points.length - 1, segment + 2)];
|
|
29
|
+
|
|
30
|
+
let p1Tension = tension;
|
|
31
|
+
let p2Tension = tension;
|
|
32
|
+
|
|
33
|
+
if (p1.straight)
|
|
34
|
+
p1Tension = 0;
|
|
35
|
+
|
|
36
|
+
if (p2.straight)
|
|
37
|
+
p2Tension = 0;
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
x: catmullRom(p0.x, p1.x, p2.x, p3.x, localT, p1Tension, p2Tension),
|
|
41
|
+
y: catmullRom(p0.y, p1.y, p2.y, p3.y, localT, p1Tension, p2Tension)
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import Value from './value.js';
|
|
2
|
+
|
|
3
|
+
const DEBUG_SET_GET = false;
|
|
4
|
+
|
|
5
|
+
export default class Target {
|
|
6
|
+
constructor(target) {
|
|
7
|
+
this.target = target;
|
|
8
|
+
|
|
9
|
+
// this might be an object, a dom node
|
|
10
|
+
// what about arrays?
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
type() {
|
|
14
|
+
return 'unknown';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
getValue(name) {
|
|
18
|
+
throw new Error('cannot get value of ' + name);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
setValue(name, value) {
|
|
22
|
+
throw new Error('cannot set value of ' + name);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
removeValue(name) {
|
|
26
|
+
throw new Error('cannot remove value of ' + name);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
getTransformValue(name) {
|
|
30
|
+
return this.getValue(name);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
setTransformValue(name, value) {
|
|
34
|
+
return this.setValue(name, value);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
removeTransformValue(name) {
|
|
38
|
+
return this.removeValue(name);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
getStyleValue(name) {
|
|
42
|
+
return this.getValue(name);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
setStyleValue(name, value) {
|
|
46
|
+
return this.setValue(name, value);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
removeStyleValue(name) {
|
|
50
|
+
return this.removeValue(name);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
apply() {}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export class DomTarget extends Target {
|
|
57
|
+
constructor(target) {
|
|
58
|
+
super(target);
|
|
59
|
+
|
|
60
|
+
// for the moment
|
|
61
|
+
|
|
62
|
+
this.transformValues = new Map;
|
|
63
|
+
this.styleValues = new Map;
|
|
64
|
+
this.values = new Map;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
type() {
|
|
68
|
+
return 'dom';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
getValue(name) {
|
|
72
|
+
if (DEBUG_SET_GET)
|
|
73
|
+
console.log('get', name);
|
|
74
|
+
|
|
75
|
+
return this.values.get(name);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
setValue(name, value) {
|
|
79
|
+
if (DEBUG_SET_GET)
|
|
80
|
+
console.log('set', name, value);
|
|
81
|
+
|
|
82
|
+
this.values.set(name, value);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
removeValue(name) {
|
|
86
|
+
this.values.delete(name);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
getTransformValue(name) {
|
|
90
|
+
if (DEBUG_SET_GET)
|
|
91
|
+
console.log('get', name);
|
|
92
|
+
|
|
93
|
+
return this.transformValues.get(name);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
setTransformValue(name, value) {
|
|
97
|
+
if (DEBUG_SET_GET)
|
|
98
|
+
console.log('set', name, value);
|
|
99
|
+
|
|
100
|
+
this.transformValues.set(name, value);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
removeTransformValue(name) {
|
|
104
|
+
this.transformValues.delete(name);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
getStyleValue(name) {
|
|
108
|
+
if (DEBUG_SET_GET)
|
|
109
|
+
console.log('get', name);
|
|
110
|
+
|
|
111
|
+
const v = this.styleValues.get(name);
|
|
112
|
+
|
|
113
|
+
if (typeof v === 'undefined' || v === null) {
|
|
114
|
+
// let's try to get it from the dom
|
|
115
|
+
const style = getComputedStyle(this.target);
|
|
116
|
+
const styleV = style[name];
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
const value = Value.parse(styleV);
|
|
120
|
+
this.styleValues.set(name, value);
|
|
121
|
+
|
|
122
|
+
return value;
|
|
123
|
+
} catch (e) {
|
|
124
|
+
console.log('could not parse value', styleV);
|
|
125
|
+
return v;
|
|
126
|
+
}
|
|
127
|
+
} else
|
|
128
|
+
return v;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
setStyleValue(name, value) {
|
|
132
|
+
if (DEBUG_SET_GET)
|
|
133
|
+
console.log('set', name, value);
|
|
134
|
+
|
|
135
|
+
this.styleValues.set(name, value);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
removeStyleValue(name) {
|
|
139
|
+
this.styleValues.delete(name);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
apply() {
|
|
143
|
+
this.target.style.transform = Array.from(this.transformValues.entries())
|
|
144
|
+
.map(([k, v]) => `${k}(${v})`)
|
|
145
|
+
.join(' ');
|
|
146
|
+
|
|
147
|
+
// style
|
|
148
|
+
for (const [k, v] of this.styleValues.entries()) {
|
|
149
|
+
this.target.style[k] = v;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// values
|
|
153
|
+
for (const [k, v] of this.values.entries()) {
|
|
154
|
+
console.log('what todo with this ' + k);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function newTarget(target) {
|
|
160
|
+
if (target instanceof HTMLElement || target instanceof SVGElement)
|
|
161
|
+
return new DomTarget(target);
|
|
162
|
+
|
|
163
|
+
throw new Error('unknown target');
|
|
164
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { curve } from './motionpath.js';
|
|
2
|
+
|
|
3
|
+
export { curve };
|
|
4
|
+
|
|
5
|
+
export function to(to) {
|
|
6
|
+
return { to };
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function from(from) {
|
|
10
|
+
return { from };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function fromTo(from, to) {
|
|
14
|
+
return { from, to };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// converts some element
|
|
18
|
+
export function chars(element) {
|
|
19
|
+
if (element.dataset.processedChars)
|
|
20
|
+
return Array.from(element.children);
|
|
21
|
+
|
|
22
|
+
const chars = element.textContent.split('');
|
|
23
|
+
|
|
24
|
+
element.textContent = '';
|
|
25
|
+
element.dataset.processedChars = 'yes';
|
|
26
|
+
|
|
27
|
+
return chars.map(c => {
|
|
28
|
+
const span = document.createElement('span');
|
|
29
|
+
span.textContent = c;
|
|
30
|
+
span.style.display = 'inline-block';
|
|
31
|
+
element.appendChild(span);
|
|
32
|
+
|
|
33
|
+
return span;
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// get the offset to the page
|
|
38
|
+
export function pageOffset(el) {
|
|
39
|
+
if ('offsetTop' in el) {
|
|
40
|
+
return {
|
|
41
|
+
top: el.offsetTop,
|
|
42
|
+
left: el.offsetLeft,
|
|
43
|
+
width: el.offsetWidth,
|
|
44
|
+
height: el.offsetHeight
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const rect = el.getBoundingClientRect();
|
|
49
|
+
return {
|
|
50
|
+
top: window.scrollY + rect.top,
|
|
51
|
+
left: window.scrollX + rect.left,
|
|
52
|
+
width: rect.width,
|
|
53
|
+
height: rect.height
|
|
54
|
+
};
|
|
55
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const UNITS = ['px', 'vw', 'vh', 'rem', 'em', 'rem', 'deg', 'rad'];
|
|
2
|
+
|
|
3
|
+
const REGEX = /^(\d*\.?\d*)(\w*)$/;
|
|
4
|
+
|
|
5
|
+
export default class Value {
|
|
6
|
+
constructor(num, unit = null) {
|
|
7
|
+
this.num = num;
|
|
8
|
+
this.unit = unit;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
static parse(v, defUnit = null) {
|
|
12
|
+
if (typeof v === 'number') {
|
|
13
|
+
return new Value(v, defUnit);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const [_v, num, unit] = REGEX.exec(v);
|
|
17
|
+
|
|
18
|
+
if (unit && !UNITS.includes(unit))
|
|
19
|
+
throw new Error('unknown unit ' + unit);
|
|
20
|
+
|
|
21
|
+
return new Value(parseFloat(num), unit ? unit : null);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
clone(newNum = null) {
|
|
25
|
+
if (newNum !== null)
|
|
26
|
+
return new Value(newNum, this.unit);
|
|
27
|
+
|
|
28
|
+
return new Value(this.num, this.unit);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
cloneAdd(add) {
|
|
32
|
+
return new Value(this.num + add, this.unit);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
toString() {
|
|
36
|
+
return this.num.toFixed(3) + (this.unit ?? '');
|
|
37
|
+
}
|
|
38
|
+
}
|