qarl 1.0.0 → 1.1.1

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/LICENSE CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2024 qarl
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) 2024 ThreePixDroid
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,8 +1,16 @@
1
1
  # QARL (Quantum Animation Remixable Library) 😎
2
2
 
3
+ *Description generated by GPT while we were busy doing more important things.*
4
+
3
5
  QARL is an animation library for my game engine.
4
6
 
5
- New project! 💡 It surely has some bugs, unfinished features, and odd solutions, but I've put a lot of heart and effort into it. Thanks for stopping by — I hope you'll enjoy it! 🙏✨
7
+ ⚠️ **Attention!** ⚠️
8
+
9
+ New project! 💡 It surely has some bugs, unfinished features, and odd solutions, but I've put a lot of heart and effort into it. Thanks for stopping by—I hope you'll enjoy it! 🙏✨
10
+
11
+ ## Why QARL? 🏎️💨
12
+
13
+ Looking for performance? QARL's FromTo animation computes so fast, it feels like it bends space-time! 💫 No more waiting for slow calculations—just smooth, seamless transitions that keep up with the pace of your game.
6
14
 
7
15
  ## Installation
8
16
 
@@ -10,3 +18,51 @@ Install the library via npm:
10
18
 
11
19
  ```bash
12
20
  npm install qarl
21
+ ```
22
+
23
+ ## FromTo example
24
+
25
+ ```js
26
+ const cube3 = new THREE.Mesh(
27
+ new THREE.BoxGeometry(),
28
+ new THREE.MeshBasicMaterial({ color: 0x00ffff })
29
+ )
30
+
31
+ new QARL.FromTo({
32
+ target: cube3,
33
+ dynamic: true,
34
+ loop: true,
35
+ time: 3000,
36
+ mode: QARL.modes.pingPong, // bounce, yoyo, pingPong
37
+ easing: QARL.easings.outQuad,
38
+ from: { rotation: { x: 1, y: 2 }, position: { x: 2, z: 2 }, scale: { x: .01, y: 1, z: 1 } },
39
+ to: { rotation: { x: 3, y: -5 }, position: { x: -2, z: -2 }, scale: { x: 3, y: .5, z: .5 } },
40
+ })
41
+ ```
42
+
43
+ ## Curve example
44
+
45
+ ```js
46
+ const cube3 = new THREE.Mesh(
47
+ new THREE.BoxGeometry(),
48
+ new THREE.MeshBasicMaterial({ color: 0xff00ff })
49
+ )
50
+
51
+ new QARL.Curve({
52
+ target: cube3,
53
+ loop: true,
54
+ time: 10000,
55
+ mode: QARL.modes.yoyo,
56
+ easing: QARL.easings.inOutBack,
57
+ smoothing: 10,
58
+ properties: ['position.x', 'position.y', 'position.z', 'scale.x', 'scale.y', 'scale.z'],
59
+ points: [
60
+ [-2, -2, 0, .1, .5, .5],
61
+ [ 2, -2, 0, .5, .2, .2],
62
+ [ 2, 0, 1, .1, .5, .5],
63
+ [-2, 0, 1, .5, .2, .2],
64
+ [-2, 2, 0, .1, .5, .5],
65
+ [ 2, 2, 0, .5, .2, .2],
66
+ ],
67
+ })
68
+ ```
package/index.js CHANGED
@@ -1,7 +1,8 @@
1
- export { Core } from './core/Core'
2
- export { EVENTS } from './core/events'
3
- export { DEFAULTS } from './core/defaults'
4
- export { Notifier } from './emitter/Notifier'
5
- export { FromTo } from './controllers/FromTo'
6
- export { easings } from './behaviors/easings'
7
- export { modes } from './behaviors/modes'
1
+ export { Core } from './src/core/Core'
2
+ export { Curve } from './src/controllers/Curve'
3
+ export { FromTo } from './src/controllers/FromTo'
4
+ export { Notifier } from './src/emitter/Notifier'
5
+ export { DEFAULTS } from './src/core/defaults'
6
+ export { easings } from './src/behaviors/easings'
7
+ export { EVENTS } from './src/core/events'
8
+ export { modes } from './src/behaviors/modes'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qarl",
3
- "version": "1.0.0",
3
+ "version": "1.1.1",
4
4
  "description": "simple animation library",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -0,0 +1,123 @@
1
+ import { Core } from "../core/Core"
2
+
3
+ export class Curve extends Core {
4
+ static DEFAULTS = {
5
+ ...Core.DEFAULTS,
6
+ properties: [],
7
+ points: [],
8
+ smoothing: 20,
9
+ // useLerp: true,
10
+ // path: [],
11
+ // speed: 1,
12
+ };
13
+
14
+ _preparePropertySetters() {
15
+ this.propertySetters = this.settings.properties.map(property => {
16
+ const keys = property.split('.')
17
+ const lastKey = keys.pop()
18
+
19
+ return (value) => {
20
+ let obj = this.target
21
+ for (let i = 0; i < keys.length; i++) {
22
+ obj = obj[keys[i]]
23
+ }
24
+ obj[lastKey] = value
25
+ }
26
+ })
27
+ }
28
+
29
+ _generatePath() {
30
+ const points = this.settings.points
31
+ const smoothing = Math.max(this.settings.smoothing, 1)
32
+
33
+ const result = []
34
+ this.totalLength = 0
35
+
36
+ function interpolate(p0, p1, p2, p3, t) {
37
+ const t2 = t * t
38
+ const t3 = t2 * t
39
+
40
+ return p0.map((_, i) =>
41
+ 0.5 * (
42
+ (2 * p1[i]) +
43
+ (-p0[i] + p2[i]) * t +
44
+ (2 * p0[i] - 5 * p1[i] + 4 * p2[i] - p3[i]) * t2 +
45
+ (-p0[i] + 3 * p1[i] - 3 * p2[i] + p3[i]) * t3
46
+ )
47
+ )
48
+ }
49
+
50
+ for (let i = 0; i < points.length - 1; i++) {
51
+ const p0 = points[i === 0 ? i : i - 1]
52
+ const p1 = points[i]
53
+ const p2 = points[i + 1]
54
+ const p3 = points[i + 2 < points.length ? i + 2 : i + 1]
55
+
56
+ for (let t = 0; t < smoothing; t++) {
57
+ const pt = interpolate(p0, p1, p2, p3, t / smoothing)
58
+ result.push(pt)
59
+
60
+ if (result.length > 1) {
61
+ this.totalLength += this._calculateDistance(result[result.length - 2], pt)
62
+ }
63
+ }
64
+ }
65
+
66
+ result.push(points[points.length - 1])
67
+
68
+ return result
69
+ }
70
+
71
+ _calculateDistance(p1, p2) {
72
+ return Math.sqrt(p1.reduce((acc, _, i) => acc + Math.pow(p2[i] - p1[i], 2), 0))
73
+ }
74
+
75
+ _refreshDynamicProps() {
76
+ super._refreshDynamicProps()
77
+ this.path = this._generatePath()
78
+
79
+ if (!this.target || this.path.length === 0 || this.settings.properties.length === 0) {
80
+ this._setTargetProperties = Core._noop
81
+ } else {
82
+ this._preparePropertySetters()
83
+ }
84
+ }
85
+
86
+ _setTargetProperties(values) {
87
+ for (let i = 0; i < this.propertySetters.length; i++) {
88
+ this.propertySetters[i](values[i])
89
+ }
90
+ }
91
+
92
+ _clamp(value, min, max) {
93
+ return Math.max(min, Math.min(value, max))
94
+ }
95
+
96
+ _lerp(a, b, t) {
97
+ return a + (b - a) * t
98
+ }
99
+
100
+ _getInterpolatedPosition() {
101
+ const maxIndex = this.path.length - 1
102
+ const exactIndex = this.easeValue * maxIndex
103
+ const clampedExactIndex = this._clamp(exactIndex, 0, maxIndex)
104
+
105
+ // Determine the indices of the two neighboring points
106
+ const lowerIndex = clampedExactIndex >= maxIndex ? maxIndex - 1 : Math.floor(clampedExactIndex)
107
+ const upperIndex = Math.min(lowerIndex + 1, maxIndex)
108
+
109
+ const interpolationFactor = exactIndex - lowerIndex
110
+
111
+ const lowerPoint = this.path[lowerIndex]
112
+ const upperPoint = this.path[upperIndex]
113
+
114
+ return lowerPoint.map((coord, i) =>
115
+ this._lerp(coord, upperPoint[i], interpolationFactor)
116
+ )
117
+ }
118
+
119
+ _update() {
120
+ super._update()
121
+ this._setTargetProperties(this._getInterpolatedPosition())
122
+ }
123
+ }
@@ -5,7 +5,6 @@ export class FromTo extends Core {
5
5
  static DEFAULTS = {
6
6
  ...Core.DEFAULTS,
7
7
  dynamic: false, // в динамическом состоянии можно обновлять значения from и to во время анимации
8
- target: null,
9
8
  from: null,
10
9
  to: null
11
10
  }
@@ -16,23 +15,24 @@ export class FromTo extends Core {
16
15
  }
17
16
 
18
17
  _processFromTo() {
19
- this.target = this.settings.target
20
-
21
- this._from = this.settings.from || this._createState(this.settings.to || {}, this.target)
22
- this._to = this.settings.to || this._createState(this.settings.from || {}, this.target)
23
-
24
18
  if (!this.target) {
25
19
  this._updateFromTo = Core._noop
26
- } else if (this.settings.dynamic) {
27
- this._updateFromTo = this._updateDynamic.bind(this)
20
+ this._from = {}
21
+ this._to = {}
28
22
  } else {
29
- this._lerps = []
30
- this._createLerps(this.target, this._from, this._to)
31
- this._updateFromTo = this._updateBaked.bind(this)
23
+ this._from = this.settings.from || this._createState(this.settings.to || {}, this.target)
24
+ this._to = this.settings.to || this._createState(this.settings.from || {}, this.target)
25
+
26
+ if (this.settings.dynamic) {
27
+ this._updateFromTo = this._updateDynamic.bind(this)
28
+ } else {
29
+ this._lerps = []
30
+ this._createLerps(this.target, this._from, this._to)
31
+ this._updateFromTo = this._updateStatic.bind(this)
32
+ }
32
33
  }
33
34
  }
34
35
 
35
-
36
36
  _createState(origPattern = {}, origSource = {}, origTarget = {}) {
37
37
  const _copyProperty = (pattern, source, target) => {
38
38
  for (const key in pattern) {
@@ -67,7 +67,7 @@ export class FromTo extends Core {
67
67
  this._updateFromTo()
68
68
  }
69
69
 
70
- _updateBaked() {
70
+ _updateStatic() {
71
71
  this._lerps.forEach(lerpStep => lerpStep(this.easeValue))
72
72
  }
73
73
 
@@ -103,6 +103,4 @@ export class FromTo extends Core {
103
103
 
104
104
  return this
105
105
  }
106
-
107
106
  }
108
-
@@ -0,0 +1,372 @@
1
+ import { Notifier } from "../emitter/Notifier"
2
+ import { DEFAULTS } from "./defaults"
3
+ import { EVENTS } from "./events"
4
+
5
+ /**
6
+ * Core - Main class for managing animations.
7
+ *
8
+ * @class Core
9
+ * @param {Object} [overrides={}] - Object for overriding default settings.
10
+ * @param {Object} [EmitterClass=null] - Class for managing animation events.
11
+ *
12
+ * @example
13
+ * const animation = new Core({ time: 1000, easing: easings.inOutQuad });
14
+ * animation.play();
15
+ *
16
+ * @property {Object} DEFAULTS - Default settings.
17
+ * @property {number} DEFAULTS.time - Animation duration.
18
+ * @property {boolean} DEFAULTS.loop - Flag for infinite looping.
19
+ * @property {function} DEFAULTS.easing - Function for controlling the animation effect.
20
+ * @property {boolean} DEFAULTS.reversed - Flag for playing the animation in reverse.
21
+ * @property {number} DEFAULTS.repeat - Number of animation repetitions.
22
+ * @property {number} DEFAULTS.delay - Delay before the animation starts.
23
+ *
24
+ * @property {Object} settings - Current animation settings.
25
+ * @property {number} progress - Current animation progress.
26
+ * @property {boolean} reversed - Indicates if the animation is played in reverse.
27
+ */
28
+ export class Core {
29
+ static DEFAULTS = DEFAULTS
30
+
31
+ static _noop() { }
32
+
33
+ static mergeConfigs(target, source) {
34
+ for (let key in source) {
35
+ if (typeof source[key] === 'object' && source[key] !== null) {
36
+ if (!target[key] || typeof target[key] !== 'object') {
37
+ target[key] = {}
38
+ }
39
+ Core.mergeConfigs(target[key], source[key])
40
+ } else {
41
+ target[key] = source[key]
42
+ }
43
+ }
44
+ }
45
+
46
+ constructor(overrides = {}, EmitterClass = Notifier) {
47
+ this.settings = {}
48
+ this._emitter = EmitterClass ? new EmitterClass() : null
49
+ this.reset(overrides)
50
+ }
51
+
52
+ /**
53
+ * Sets internal animation states.
54
+ * @private
55
+ */
56
+ _processState() {
57
+ this.step = Core._noop
58
+ this.progress = 0
59
+ this.easeValue = 0
60
+ this.elapsedTime = 0
61
+ this.promise = null
62
+ this._resolve = Core._noop
63
+ this._refreshDynamicProps()
64
+ }
65
+
66
+ /**
67
+ * Updates dynamic animation states.
68
+ * @private
69
+ */
70
+ _refreshDynamicProps() {
71
+ this.target = this.settings.target
72
+
73
+ this.time = Math.max(this.settings.time, 0)
74
+ this.repeat = this.settings.repeat > 0 ? this.settings.repeat : this.settings.loop ? Infinity : 0
75
+ this.reversed = this.settings.reversed
76
+ this.remainingDelay = this.settings.delay
77
+
78
+ this._emitEvent = this._emitEvent || Core._noop
79
+ this._emitUpdate = this._emitUpdate || Core._noop
80
+
81
+ this._processEasing()
82
+ }
83
+
84
+ /**
85
+ * Sets the easing function for the animation.
86
+ * @private
87
+ */
88
+ _processEasing() {
89
+ this._easing = this.reversed ? this._reversedEasing : this.settings.easing
90
+ this._calculateEasing = this.settings.mode ? this.settings.mode.bind(this) : this._easing
91
+ }
92
+
93
+ /**
94
+ * Calculates the reversed easing value.
95
+ * @param {number} t - Progress time.
96
+ * @returns {number} - Easing result for the reversed direction.
97
+ * @private
98
+ */
99
+ _reversedEasing(t) {
100
+ return this.settings.easing(1 - t)
101
+ }
102
+
103
+ /**
104
+ * Updates animation progress and triggers the update event.
105
+ * @private
106
+ */
107
+ _update() {
108
+ this.progress = this.elapsedTime / this.time
109
+ this.easeValue = this._calculateEasing(this.progress)
110
+ this._emitUpdate(EVENTS.UPDATE, { progress: this.progress, ease: this.easeValue })
111
+ }
112
+
113
+ /**
114
+ * Handles the delay before the animation starts.
115
+ * @param {number} deltaTime - Time passed since the last step.
116
+ * @private
117
+ */
118
+ _stepDelay(deltaTime) {
119
+ this.remainingDelay -= deltaTime
120
+
121
+ if (this.remainingDelay > 0) return
122
+
123
+ this.step = this._stepTime
124
+ this.step(Math.abs(this.remainingDelay))
125
+
126
+ this.remainingDelay = this.settings.delay
127
+ }
128
+
129
+ /**
130
+ * Handles animation time.
131
+ * @param {number} [deltaTime=0] - Time passed since the last step.
132
+ * @private
133
+ */
134
+ _stepTime(deltaTime = 0) {
135
+ this.elapsedTime += deltaTime
136
+
137
+ if (this.elapsedTime >= this.time) {
138
+ this.elapsedTime = this.time
139
+ this._update()
140
+ this._complete()
141
+ } else {
142
+ this._update()
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Called when the animation is complete.
148
+ * @private
149
+ */
150
+ _complete() {
151
+ if (this.repeat-- > 0) {
152
+ this._repeat()
153
+ } else {
154
+ this._resolve()
155
+ this.stop(false)
156
+ this._emitEvent(EVENTS.COMPLETE)
157
+ }
158
+ }
159
+
160
+ /**
161
+ * Repeats the animation if repetitions are set.
162
+ * @param {boolean} [withEvent=true] - Flag for triggering the repeat event.
163
+ * @private
164
+ */
165
+ _repeat(withEvent = true) {
166
+ this.remainingDelay = this.settings.repeatDelay
167
+ this.elapsedTime = 0
168
+
169
+ if (this.remainingDelay > 0) {
170
+ this.step = this._stepDelay
171
+ }
172
+
173
+ this._update()
174
+ withEvent && this._emitEvent(EVENTS.REPEAT)
175
+ }
176
+
177
+ /**
178
+ * Logs a warning if the event emitter is not defined.
179
+ * @returns {Core} The current instance for chaining.
180
+ * @private
181
+ */
182
+ _noEmitter() {
183
+ console.warn('Event emitter is not defined')
184
+ return this
185
+ }
186
+
187
+ /**
188
+ * Subscribes a handler to the specified event.
189
+ * @param {string} event - Event name.
190
+ * @param {function} handler - Event handler.
191
+ * @param {boolean} [once=false] - Flag for one-time subscription.
192
+ * @returns {Core} The current instance for chaining.
193
+ */
194
+ on(event, handler, once = false) {
195
+ if (!this._emitter) return this._noEmitter()
196
+
197
+ if (event === EVENTS.UPDATE) {
198
+ this._emitUpdate = this._emitter.emit(event, handler)
199
+ } else {
200
+ this._emit = this._emitter.emit(event, handler)
201
+ }
202
+
203
+ once
204
+ ? this._emitter.once(event, handler)
205
+ : this._emitter.on(event, handler)
206
+
207
+ return this
208
+ }
209
+
210
+ /**
211
+ * Subscribes a handler to the specified event once.
212
+ * @param {string} event - Event name.
213
+ * @param {function} handler - Event handler.
214
+ * @returns {Core} The current instance for chaining.
215
+ */
216
+ once(event, handler) {
217
+ return this.on(event, handler, true)
218
+ }
219
+
220
+ /**
221
+ * Unsubscribes a handler from the specified event.
222
+ * @param {string} event - Event name.
223
+ * @param {function} handler - Event handler.
224
+ * @returns {Core} The current instance for chaining.
225
+ */
226
+ off(event, handler) {
227
+ if (!this._emitter) return this._noEmitter()
228
+
229
+ this._emitter.off(event, handler)
230
+ return this
231
+ }
232
+
233
+ /**
234
+ * Removes all handlers for the specified event.
235
+ * @param {string} event - Event name.
236
+ * @returns {Core} The current instance for chaining.
237
+ */
238
+ removeEvents(event) {
239
+ if (!this._emitter) return this._noEmitter()
240
+
241
+ this._emitEvent = Core._noop
242
+ this._emitUpdate = Core._noop
243
+
244
+ this._emitter.removeAllListeners(event)
245
+ return this
246
+ }
247
+
248
+ /**
249
+ * Resets settings to default values.
250
+ * @param {Object} [newSettings=Core.DEFAULTS] - New settings.
251
+ * @returns {Core} The current instance for chaining.
252
+ */
253
+ reset(newSettings = this.constructor.DEFAULTS) {
254
+ this.settings = { ...this.constructor.DEFAULTS, ...newSettings }
255
+ this._processState()
256
+ return this
257
+ }
258
+
259
+ /**
260
+ * Modifies the current animation settings.
261
+ * @param {Object} [newSettings={}] - New settings.
262
+ * @returns {Core} The current instance for chaining.
263
+ */
264
+ tweak(newSettings = {}) {
265
+ Core.mergeConfigs(this.settings, newSettings)
266
+ this._refreshDynamicProps()
267
+ return this
268
+ }
269
+
270
+ /**
271
+ * Sets the animation time.
272
+ * @param {number} [time=0] - Time to set.
273
+ * @returns {Core} The current instance for chaining.
274
+ */
275
+ seek(time = 0, callUpdate = true) {
276
+ this.elapsedTime = Math.min(Math.max(time, 0), this.settings.time)
277
+ callUpdate && this._update()
278
+ return this
279
+ }
280
+
281
+ /**
282
+ * Sets the animation progress.
283
+ * @param {number} [progress=0] - Animation progress (from 0 to 1).
284
+ * @returns {Core} The current instance for chaining.
285
+ */
286
+ setProgress(progress = 0, callUpdate = true) {
287
+ this.seek(this.settings.time * progress, callUpdate)
288
+ return this
289
+ }
290
+
291
+ /**
292
+ * Toggles the animation direction (forward/reverse).
293
+ * @returns {Core} The current instance for chaining.
294
+ */
295
+ reverse(callUpdate = false) {
296
+ this.reversed = !this.reversed
297
+ this.seek(this.settings.time - this.elapsedTime, callUpdate)
298
+ this._processEasing()
299
+
300
+ return this
301
+ }
302
+
303
+ /**
304
+ * Starts the animation.
305
+ * @param {boolean} [withEvent=true] - Flag for triggering the play event.
306
+ * @returns {Core} The current instance for chaining.
307
+ */
308
+ play(withEvent = true) {
309
+ if (this.isPlaying) return this
310
+
311
+ withEvent && this._emitEvent(EVENTS.PLAY)
312
+ if (this.remainingDelay > 0) {
313
+ this.step = this._stepDelay
314
+ } else {
315
+ this.step = this._stepTime
316
+ this._emitEvent(EVENTS.BEGIN)
317
+ }
318
+
319
+ return this
320
+ }
321
+
322
+ /**
323
+ * Starts the animation and returns a Promise.
324
+ * @param {Promise} [promise=this.promise] - Promise object to resolve on completion.
325
+ * @returns {Promise} - A promise that resolves when the animation is complete.
326
+ */
327
+ playPromise(promise = this.promise) {
328
+ this.play()
329
+ this.promise = promise || new Promise(resolve => this._resolve = resolve)
330
+ return this.promise
331
+ }
332
+
333
+ /**
334
+ * Stops the animation.
335
+ * @param {boolean} [withEvent=true] - Flag for triggering the stop event.
336
+ * @returns {Core} The current instance for chaining.
337
+ */
338
+ stop(withEvent = true) {
339
+ this._processState()
340
+ withEvent && this._emitEvent(EVENTS.STOP)
341
+ return this
342
+ }
343
+
344
+ /**
345
+ * Pauses the animation.
346
+ * @returns {Core} The current instance for chaining.
347
+ */
348
+ pause() {
349
+ this.step = Core._noop
350
+ this._emitEvent(EVENTS.PAUSE)
351
+ return this
352
+ }
353
+
354
+ /**
355
+ * Restarts the animation from the beginning.
356
+ * @param {boolean} [withEvent=true] - Flag for triggering the replay event.
357
+ * @returns {Core} The current instance for chaining.
358
+ */
359
+ replay(withEvent = true) {
360
+ this.stop(withEvent)
361
+ this.play(withEvent)
362
+ return this
363
+ }
364
+
365
+ /**
366
+ * Returns whether the animation is currently playing.
367
+ * @returns {boolean} - true if the animation is playing, otherwise false.
368
+ */
369
+ get isPlaying() {
370
+ return this.step !== Core._noop
371
+ }
372
+ }
@@ -6,6 +6,7 @@ export const DEFAULTS = {
6
6
  mode: null,
7
7
  delay: 0,
8
8
  repeat: 0,
9
+ target: null,
9
10
  easing: easings.linear,
10
11
  reversed: false,
11
12
  repeatDelay: 0,
@@ -0,0 +1,97 @@
1
+ export class Notifier {
2
+ constructor() {
3
+ this.events = {}
4
+ }
5
+
6
+ /**
7
+ * Subscribe to an event
8
+ * @param {string} eventName - Name of the event
9
+ * @param {function} listener - Event handler function
10
+ */
11
+ on(eventName, listener) {
12
+ if (!this.events[eventName]) {
13
+ this.events[eventName] = []
14
+ }
15
+ this.events[eventName].push(listener)
16
+ }
17
+
18
+ /**
19
+ * Unsubscribe from an event
20
+ * @param {string} eventName - Name of the event
21
+ * @param {function} listener - Event handler function
22
+ */
23
+ off(eventName, listener) {
24
+ if (!this.events[eventName]) return
25
+ this.events[eventName] = this.events[eventName].filter(l => l !== listener)
26
+ }
27
+
28
+ /**
29
+ * Emit an event
30
+ * @param {string} eventName - Name of the event
31
+ * @param {...any} args - Arguments passed to the event handler
32
+ */
33
+ emit(eventName, ...args) {
34
+ if (!this.events[eventName]) return
35
+ this.events[eventName].forEach(listener => listener(...args))
36
+ }
37
+
38
+ /**
39
+ * Subscribe to an event once
40
+ * @param {string} eventName - Name of the event
41
+ * @param {function} listener - Event handler function
42
+ */
43
+ once(eventName, listener) {
44
+ const onceListener = (...args) => {
45
+ this.off(eventName, onceListener)
46
+ listener(...args)
47
+ }
48
+ this.on(eventName, onceListener)
49
+ }
50
+
51
+ /**
52
+ * Get the list of event listeners
53
+ * @param {string} eventName - Name of the event
54
+ * @returns {function[]} List of event handlers
55
+ */
56
+ getListeners(eventName) {
57
+ return this.events[eventName] || []
58
+ }
59
+
60
+ /**
61
+ * Get the number of event listeners
62
+ * @param {string} eventName - Name of the event
63
+ * @returns {number} Number of event listeners
64
+ */
65
+ getListenerCount(eventName) {
66
+ return this.events[eventName] ? this.events[eventName].length : 0
67
+ }
68
+
69
+ /**
70
+ * Remove all listeners or listeners of a specific event type
71
+ * @param {string} [eventName] - Name of the event (optional)
72
+ */
73
+ removeAllListeners(eventName) {
74
+ if (eventName) {
75
+ delete this.events[eventName]
76
+ } else {
77
+ this.events = {}
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Get the list of all event names
83
+ * @returns {string[]} List of event names
84
+ */
85
+ getEventNames() {
86
+ return Object.keys(this.events)
87
+ }
88
+
89
+ /**
90
+ * Check if there are any listeners for an event
91
+ * @param {string} eventName - Name of the event
92
+ * @returns {boolean} True if there are listeners, otherwise False
93
+ */
94
+ hasListeners(eventName) {
95
+ return this.getListenerCount(eventName) > 0
96
+ }
97
+ }
package/core/Core.js DELETED
@@ -1,371 +0,0 @@
1
- import { Notifier } from "../emitter/Notifier"
2
- import { DEFAULTS } from "../core/defaults"
3
- import { EVENTS } from "../core/events"
4
-
5
-
6
- /**
7
- * Core - Основной класс для управления анимациями.
8
- *
9
- * @class Core
10
- * @param {Object} [overrides={}] - Объект для переопределения настроек по умолчанию.
11
- * @param {Object} [EmitterClass=null] - Класс для управления событиями анимации.
12
- *
13
- * @example
14
- * const animation = new Core({ time: 1000, easing: easings.inOutQuad });
15
- * animation.play();
16
- *
17
- * @property {Object} DEFAULTS - Настройки по умолчанию.
18
- * @property {number} DEFAULTS.time - Время анимации.
19
- * @property {boolean} DEFAULTS.loop - Флаг бесконечного повторения анимации.
20
- * @property {function} DEFAULTS.easing - Функция для управления эффектом анимации.
21
- * @property {boolean} DEFAULTS.reversed - Флаг обратного проигрывания анимации.
22
- * @property {number} DEFAULTS.repeat - Количество повторений анимации.
23
- * @property {number} DEFAULTS.delay - Задержка перед началом анимации.
24
- *
25
- * @property {Object} settings - Текущие настройки анимации.
26
- * @property {number} progress - Текущий прогресс анимации.
27
- * @property {boolean} reversed - Указывает, проигрывается ли анимация в обратном направлении.
28
- */
29
- export class Core {
30
- static DEFAULTS = DEFAULTS
31
-
32
- static _noop() { }
33
-
34
- static mergeConfigs(target, source) {
35
- for (let key in source) {
36
- if (typeof source[key] === 'object' && source[key] !== null) {
37
- if (!target[key] || typeof target[key] !== 'object') {
38
- target[key] = {}
39
- }
40
- Core.mergeConfigs(target[key], source[key])
41
- } else {
42
- target[key] = source[key]
43
- }
44
- }
45
- }
46
-
47
- constructor(overrides = {}, EmitterClass = Notifier) {
48
- this.settings = {}
49
- this._emitter = EmitterClass ? new EmitterClass() : null
50
- this.reset(overrides)
51
- }
52
-
53
- /**
54
- * Устанавливает внутренние состояния анимации.
55
- * @private
56
- */
57
- _processState() {
58
- this.step = Core._noop
59
- this.progress = 0
60
- this.easeValue = 0
61
- this.elapsedTime = 0
62
- this.promise = null
63
- this._resolve = Core._noop
64
- this._refreshDynamicProps()
65
- }
66
-
67
- /**
68
- * Обновляет динамические состояния анимации.
69
- * @private
70
- */
71
- _refreshDynamicProps() {
72
- this.time = Math.max(this.settings.time, 0)
73
- this.repeat = this.settings.repeat > 0 ? this.settings.repeat : this.settings.loop ? Infinity : 0
74
- this.reversed = this.settings.reversed
75
- this.remainingDelay = this.settings.delay
76
-
77
- this._emitEvent = this._emitEvent || Core._noop
78
- this._emitUpdate = this._emitUpdate || Core._noop
79
-
80
- this._processEasing()
81
- }
82
-
83
- /**
84
- * Устанавливает функцию easing для анимации.
85
- * @private
86
- */
87
- _processEasing() {
88
- this._easing = this.reversed ? this._reversedEasing : this.settings.easing
89
- this._calculateEasing = this.settings.mode ? this.settings.mode.bind(this) : this._easing
90
- }
91
-
92
- /**
93
- * Рассчитывает обратное значение easing.
94
- * @param {number} t - Время прогресса.
95
- * @returns {number} - Результат функции easing для обратного направления.
96
- * @private
97
- */
98
- _reversedEasing(t) {
99
- return this.settings.easing(1 - t)
100
- }
101
-
102
- /**
103
- * Обновляет прогресс анимации и вызывает событие обновления.
104
- * @private
105
- */
106
- _update() {
107
- this.progress = this.elapsedTime / this.time
108
- this.easeValue = this._calculateEasing(this.progress)
109
- this._emitUpdate(EVENTS.UPDATE, { progress: this.progress, ease: this.easeValue })
110
- }
111
-
112
- /**
113
- * Обрабатывает задержку перед началом анимации.
114
- * @param {number} deltaTime - Время, прошедшее с предыдущего шага.
115
- * @private
116
- */
117
- _stepDelay(deltaTime) {
118
- this.remainingDelay -= deltaTime
119
-
120
- if (this.remainingDelay > 0) return
121
-
122
- this.step = this._stepTime
123
- this.step(Math.abs(this.remainingDelay))
124
-
125
- this.remainingDelay = this.settings.delay
126
- }
127
-
128
- /**
129
- * Обрабатывает время анимации.
130
- * @param {number} [deltaTime=0] - Время, прошедшее с предыдущего шага.
131
- * @private
132
- */
133
- _stepTime(deltaTime = 0) {
134
- this.elapsedTime += deltaTime
135
-
136
- if (this.elapsedTime >= this.time) {
137
- this.elapsedTime = this.time
138
- this._update()
139
- this._complete()
140
- } else {
141
- this._update()
142
- }
143
- }
144
-
145
- /**
146
- * Вызывается при завершении анимации.
147
- * @private
148
- */
149
- _complete() {
150
- if (this.repeat-- > 0) {
151
- this._repeat()
152
- } else {
153
- this._resolve()
154
- this.stop(false)
155
- this._emitEvent(EVENTS.COMPLETE)
156
- }
157
- }
158
-
159
- /**
160
- * Повторяет анимацию, если установлены повторения.
161
- * @param {boolean} [withEvent=true] - Флаг для вызова события повторения.
162
- * @private
163
- */
164
- _repeat(withEvent = true) {
165
- this.remainingDelay = this.settings.repeatDelay
166
- this.elapsedTime = 0
167
-
168
- if (this.remainingDelay > 0) {
169
- this.step = this._stepDelay
170
- }
171
-
172
- this._update()
173
- withEvent && this._emitEvent(EVENTS.REPEAT)
174
- }
175
-
176
- /**
177
- * Логирует предупреждение, если эмиттер событий не определен.
178
- * @returns {Core} Текущий экземпляр для цепочек вызовов.
179
- * @private
180
- */
181
- _noEmitter() {
182
- console.warn('Event emitter is not defined')
183
- return this
184
- }
185
-
186
- /**
187
- * Подписывает обработчик на указанное событие.
188
- * @param {string} event - Имя события.
189
- * @param {function} handler - Обработчик события.
190
- * @param {boolean} [once=false] - Флаг для одноразовой подписки.
191
- * @returns {Core} Текущий экземпляр для цепочек вызовов.
192
- */
193
- on(event, handler, once = false) {
194
- if (!this._emitter) return this._noEmitter()
195
-
196
- if (event === EVENTS.UPDATE) {
197
- this._emitUpdate = this._emitter.emit(event, handler)
198
- } else {
199
- this._emit = this._emitter.emit(event, handler)
200
- }
201
-
202
- once
203
- ? this._emitter.once(event, handler)
204
- : this._emitter.on(event, handler)
205
-
206
- return this
207
- }
208
-
209
- /**
210
- * Подписывает одноразовый обработчик на указанное событие.
211
- * @param {string} event - Имя события.
212
- * @param {function} handler - Обработчик события.
213
- * @returns {Core} Текущий экземпляр для цепочек вызовов.
214
- */
215
- once(event, handler) {
216
- return this.on(event, handler, true)
217
- }
218
-
219
- /**
220
- * Отписывает обработчик от указанного события.
221
- * @param {string} event - Имя события.
222
- * @param {function} handler - Обработчик события.
223
- * @returns {Core} Текущий экземпляр для цепочек вызовов.
224
- */
225
- off(event, handler) {
226
- if (!this._emitter) return this._noEmitter()
227
-
228
- this._emitter.off(event, handler)
229
- return this
230
- }
231
-
232
- /**
233
- * Удаляет все обработчики для указанного события.
234
- * @param {string} event - Имя события.
235
- * @returns {Core} Текущий экземпляр для цепочек вызовов.
236
- */
237
- removeEvents(event) {
238
- if (!this._emitter) return this._noEmitter()
239
-
240
- this._emitEvent = Core._noop
241
- this._emitUpdate = Core._noop
242
-
243
- this._emitter.removeAllListeners(event)
244
- return this
245
- }
246
-
247
- /**
248
- * Сбрасывает настройки к значениям по умолчанию.
249
- * @param {Object} [newSettings=Core.DEFAULTS] - Новые настройки.
250
- * @returns {Core} Текущий экземпляр для цепочек вызовов.
251
- */
252
- reset(newSettings = this.constructor.DEFAULTS) {
253
- this.settings = { ...this.constructor.DEFAULTS, ...newSettings }
254
- this._processState()
255
- return this
256
- }
257
-
258
- /**
259
- * Изменяет текущие настройки анимации.
260
- * @param {Object} [newSettings={}] - Новые настройки.
261
- * @returns {Core} Текущий экземпляр для цепочек вызовов.
262
- */
263
- tweak(newSettings = {}) {
264
- Core.mergeConfigs(this.settings, newSettings)
265
- this._refreshDynamicProps()
266
- return this
267
- }
268
-
269
- /**
270
- * Устанавливает время анимации.
271
- * @param {number} [time=0] - Время для установки.
272
- * @returns {Core} Текущий экземпляр для цепочек вызовов.
273
- */
274
- seek(time = 0, callUpdate = true) {
275
- this.elapsedTime = Math.min(Math.max(time, 0), this.settings.time)
276
- callUpdate && this._update()
277
- return this
278
- }
279
-
280
- /**
281
- * Устанавливает прогресс анимации.
282
- * @param {number} [progress=0] - Прогресс анимации (от 0 до 1).
283
- * @returns {Core} Текущий экземпляр для цепочек вызовов.
284
- */
285
- setProgress(progress = 0, callUpdate = true) {
286
- this.seek(this.settings.time * progress, callUpdate)
287
- return this
288
- }
289
-
290
- /**
291
- * Меняет направление анимации (прямое/обратное).
292
- * @returns {Core} Текущий экземпляр для цепочек вызовов.
293
- */
294
- reverse(callUpdate = false) {
295
- this.reversed = !this.reversed
296
- this.seek(this.settings.time - this.elapsedTime, callUpdate)
297
- this._processEasing()
298
-
299
- return this
300
- }
301
-
302
- /**
303
- * Запускает анимацию.
304
- * @param {boolean} [withEvent=true] - Флаг для вызова события при запуске.
305
- * @returns {Core} Текущий экземпляр для цепочек вызовов.
306
- */
307
- play(withEvent = true) {
308
- if (this.isPlaying) return this
309
-
310
- withEvent && this._emitEvent(EVENTS.PLAY)
311
- if (this.remainingDelay > 0) {
312
- this.step = this._stepDelay
313
- } else {
314
- this.step = this._stepTime
315
- this._emitEvent(EVENTS.BEGIN)
316
- }
317
-
318
- return this
319
- }
320
-
321
- /**
322
- * Запускает анимацию с возвращением Promise.
323
- * @param {Promise} [promise=this.promise] - Объект Promise для разрешения по завершению.
324
- * @returns {Promise} - Promise, который разрешится при завершении анимации.
325
- */
326
- playPromise(promise = this.promise) {
327
- this.play()
328
- this.promise = promise || new Promise(resolve => this._resolve = resolve)
329
- return this.promise
330
- }
331
-
332
- /**
333
- * Останавливает анимацию.
334
- * @param {boolean} [withEvent=true] - Флаг для вызова события при остановке.
335
- * @returns {Core} Текущий экземпляр для цепочек вызовов.
336
- */
337
- stop(withEvent = true) {
338
- this._processState()
339
- withEvent && this._emitEvent(EVENTS.STOP)
340
- return this
341
- }
342
-
343
- /**
344
- * Приостанавливает анимацию.
345
- * @returns {Core} Текущий экземпляр для цепочек вызовов.
346
- */
347
- pause() {
348
- this.step = Core._noop
349
- this._emitEvent(EVENTS.PAUSE)
350
- return this
351
- }
352
-
353
- /**
354
- * Повторно запускает анимацию с начального состояния.
355
- * @param {boolean} [withEvent=true] - Флаг для вызова события при перезапуске.
356
- * @returns {Core} Текущий экземпляр для цепочек вызовов.
357
- */
358
- replay(withEvent = true) {
359
- this.stop(withEvent)
360
- this.play(withEvent)
361
- return this
362
- }
363
-
364
- /**
365
- * Возвращает, активна ли анимация.
366
- * @returns {boolean} - true, если анимация воспроизводится, иначе false.
367
- */
368
- get isPlaying() {
369
- return this.step !== Core._noop
370
- }
371
- }
@@ -1,97 +0,0 @@
1
- export class Notifier {
2
- constructor() {
3
- this.events = {}
4
- }
5
-
6
- /**
7
- * Подписка на событие
8
- * @param {string} eventName - Название события
9
- * @param {function} listener - Функция обработчик события
10
- */
11
- on(eventName, listener) {
12
- if (!this.events[eventName]) {
13
- this.events[eventName] = []
14
- }
15
- this.events[eventName].push(listener)
16
- }
17
-
18
- /**
19
- * Отписка от события
20
- * @param {string} eventName - Название события
21
- * @param {function} listener - Функция обработчик события
22
- */
23
- off(eventName, listener) {
24
- if (!this.events[eventName]) return
25
- this.events[eventName] = this.events[eventName].filter(l => l !== listener)
26
- }
27
-
28
- /**
29
- * Генерация события
30
- * @param {string} eventName - Название события
31
- * @param {...any} args - Аргументы, передаваемые обработчику события
32
- */
33
- emit(eventName, ...args) {
34
- if (!this.events[eventName]) return
35
- this.events[eventName].forEach(listener => listener(...args))
36
- }
37
-
38
- /**
39
- * Разовая подписка на событие
40
- * @param {string} eventName - Название события
41
- * @param {function} listener - Функция обработчик события
42
- */
43
- once(eventName, listener) {
44
- const onceListener = (...args) => {
45
- this.off(eventName, onceListener)
46
- listener(...args)
47
- }
48
- this.on(eventName, onceListener)
49
- }
50
-
51
- /**
52
- * Получить список обработчиков события
53
- * @param {string} eventName - Название события
54
- * @returns {function[]} Список обработчиков
55
- */
56
- getListeners(eventName) {
57
- return this.events[eventName] || []
58
- }
59
-
60
- /**
61
- * Получить количество обработчиков события
62
- * @param {string} eventName - Название события
63
- * @returns {number} Количество обработчиков
64
- */
65
- getListenerCount(eventName) {
66
- return this.events[eventName] ? this.events[eventName].length : 0
67
- }
68
-
69
- /**
70
- * Удаление всех подписок или подписок определенного типа
71
- * @param {string} [eventName] - Название события (опционально)
72
- */
73
- removeAllListeners(eventName) {
74
- if (eventName) {
75
- delete this.events[eventName]
76
- } else {
77
- this.events = {}
78
- }
79
- }
80
-
81
- /**
82
- * Получить список всех событий
83
- * @returns {string[]} Список названий событий
84
- */
85
- getEventNames() {
86
- return Object.keys(this.events)
87
- }
88
-
89
- /**
90
- * Проверить наличие подписчиков на событие
91
- * @param {string} eventName - Название события
92
- * @returns {boolean} True, если есть подписчики, иначе False
93
- */
94
- hasListeners(eventName) {
95
- return this.getListenerCount(eventName) > 0
96
- }
97
- }
File without changes
File without changes
File without changes