pacer-js 1.0.0
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 +21 -0
- package/Pacer.js +642 -0
- package/README.md +566 -0
- package/package.json +31 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Stewart Smith
|
|
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/Pacer.js
ADDED
|
@@ -0,0 +1,642 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
/////// // ////// /////// ///////
|
|
6
|
+
// // //// // // // // //
|
|
7
|
+
// // // // // ////// // //
|
|
8
|
+
/////// /////// // // // ///////
|
|
9
|
+
// // // ////// /////// // //
|
|
10
|
+
// // //
|
|
11
|
+
// //
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
// Pacer.js is ©️ Stewart Smith, 2025.
|
|
17
|
+
// All Rights Reserved. See LICENSE for details.
|
|
18
|
+
|
|
19
|
+
// If my whitespace makes you uncomfortable,
|
|
20
|
+
// go weep into the bosom of your favorite dominatrix linter,
|
|
21
|
+
// you feeble coward.
|
|
22
|
+
|
|
23
|
+
// Do you feel that every function must be an arrow function?
|
|
24
|
+
// I’m sorry you feel that way. Cry harder, feely boi.
|
|
25
|
+
|
|
26
|
+
// Line-ending semicolons are for perverts.
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
import {
|
|
32
|
+
|
|
33
|
+
isUsefulNumber,
|
|
34
|
+
isNotUsefulNumber,
|
|
35
|
+
normalize,
|
|
36
|
+
lerp
|
|
37
|
+
|
|
38
|
+
} from 'shoes-js'
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class Key {
|
|
44
|
+
|
|
45
|
+
constructor( timeAbsolute, values ){
|
|
46
|
+
|
|
47
|
+
this.timeAbsolute = timeAbsolute
|
|
48
|
+
this.values = values instanceof Object ? values : {}
|
|
49
|
+
this.tween = Pacer.linear// Default tween method is linear interpolation.
|
|
50
|
+
this.guarantee = true
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class Pacer {
|
|
58
|
+
|
|
59
|
+
constructor( label ){
|
|
60
|
+
|
|
61
|
+
this._label = label
|
|
62
|
+
|
|
63
|
+
this.keys = []
|
|
64
|
+
this.keyIndex = -1
|
|
65
|
+
this.lastTouchedKey = null
|
|
66
|
+
|
|
67
|
+
this.values = {}
|
|
68
|
+
this.n = 0
|
|
69
|
+
this._label = ''
|
|
70
|
+
|
|
71
|
+
this.instanceIndex = Pacer.all.length
|
|
72
|
+
this.isEnabled = true
|
|
73
|
+
Pacer.all.push( this )
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
// Non-chainable.
|
|
80
|
+
|
|
81
|
+
inspect(){
|
|
82
|
+
|
|
83
|
+
return [ this.timeStart, this.keys ]
|
|
84
|
+
}
|
|
85
|
+
getFirstKey(){
|
|
86
|
+
|
|
87
|
+
return this.keys[ 0 ]
|
|
88
|
+
}
|
|
89
|
+
getLastKey(){
|
|
90
|
+
|
|
91
|
+
return this.keys[ this.keys.length - 1 ]
|
|
92
|
+
}
|
|
93
|
+
getCurrentKey(){
|
|
94
|
+
|
|
95
|
+
return this.keys[ this.keyIndex ]
|
|
96
|
+
}
|
|
97
|
+
tweenKeys( keyA, keyB, now ){
|
|
98
|
+
|
|
99
|
+
const
|
|
100
|
+
n = normalize(// Do NOT constrain this! Out of range values needed for .onBefore() and .onAfter().
|
|
101
|
+
|
|
102
|
+
now,
|
|
103
|
+
keyA.timeAbsolute,
|
|
104
|
+
keyB.timeAbsolute
|
|
105
|
+
),
|
|
106
|
+
a = Object.keys( keyA.values ),
|
|
107
|
+
b = Object.keys( keyB.values ),
|
|
108
|
+
c = a.reduce( function( output, key ){
|
|
109
|
+
|
|
110
|
+
const a = keyA.values[ key ]
|
|
111
|
+
if( isNotUsefulNumber( a )) return output
|
|
112
|
+
const b = keyB.values[ key ]
|
|
113
|
+
if( isNotUsefulNumber( b )) return output
|
|
114
|
+
output[ key ] = lerp( keyA.tween( n ), a, b )
|
|
115
|
+
return output
|
|
116
|
+
|
|
117
|
+
}, {})
|
|
118
|
+
|
|
119
|
+
this.values = c
|
|
120
|
+
this.n = n
|
|
121
|
+
return n
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
// Chainable key-focussed methods.
|
|
128
|
+
|
|
129
|
+
setTimeBounds(){
|
|
130
|
+
|
|
131
|
+
this.timeStart = this.getFirstKey().timeAbsolute
|
|
132
|
+
this.timeStop = this.getLastKey().timeAbsolute
|
|
133
|
+
this.duration = this.timeStop - this.timeStart
|
|
134
|
+
return this
|
|
135
|
+
}
|
|
136
|
+
sortKeys(){
|
|
137
|
+
|
|
138
|
+
this.keys
|
|
139
|
+
.sort( function( a, b ){
|
|
140
|
+
|
|
141
|
+
return a.timeAbsolute < b.timeAbsolute
|
|
142
|
+
})
|
|
143
|
+
.forEach( function( key, i, keys ){
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
// Yes, we have to operate directly on the `keys` Array
|
|
147
|
+
// rather than `key` element reference
|
|
148
|
+
// if we want to actually write this property.
|
|
149
|
+
// Otherwise it silently fails. Lovely.
|
|
150
|
+
// And we don’t want to use Array.map
|
|
151
|
+
// in case we ever need a DEEP copy of an element’s properties.
|
|
152
|
+
|
|
153
|
+
keys[ i ].index = i
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
// Also, here’s a nicety:
|
|
157
|
+
// if there’s a key with no values,
|
|
158
|
+
// let’s just copy the values from the previous key.
|
|
159
|
+
|
|
160
|
+
if( key.values instanceof Object !== true &&
|
|
161
|
+
typeof keys[ i - 1 ] !== 'undefined' &&
|
|
162
|
+
keys[ i - 1 ].values instanceof Object === true ){
|
|
163
|
+
|
|
164
|
+
keys[ i ].values = keys[ i - 1 ].values
|
|
165
|
+
// keys[ i ].values = Object.assign( {}, keys[ i - 1 ].values )
|
|
166
|
+
}
|
|
167
|
+
})
|
|
168
|
+
this.setTimeBounds()
|
|
169
|
+
return this
|
|
170
|
+
}
|
|
171
|
+
key( time, values, isAbsolute ){
|
|
172
|
+
|
|
173
|
+
if( isAbsolute !== true &&// Making a theoretical `isRelative` the default for backwards compatibility.
|
|
174
|
+
this.keys.length > 0 ){
|
|
175
|
+
|
|
176
|
+
time += this.getLastKey().timeAbsolute
|
|
177
|
+
}
|
|
178
|
+
if( this.keys.length === 0 ) this.values = values
|
|
179
|
+
const key = new Key( time, values )
|
|
180
|
+
this.lastTouchedKey = key
|
|
181
|
+
this.keys.push( key )
|
|
182
|
+
this.sortKeys()
|
|
183
|
+
if( this.keys.length === 1 ) this.timeCursor = this.timeStart - 1
|
|
184
|
+
return this
|
|
185
|
+
}
|
|
186
|
+
rel( timeRelative, values ){
|
|
187
|
+
|
|
188
|
+
return this.key( timeRelative, values, false )
|
|
189
|
+
}
|
|
190
|
+
abs( timeAbsolute, values ){
|
|
191
|
+
|
|
192
|
+
return this.key( timeAbsolute, values, true )
|
|
193
|
+
}
|
|
194
|
+
tween( fn ){
|
|
195
|
+
|
|
196
|
+
this.lastTouchedKey.tween = fn
|
|
197
|
+
return this
|
|
198
|
+
}
|
|
199
|
+
label( x ){
|
|
200
|
+
|
|
201
|
+
this.lastTouchedKey.label = x
|
|
202
|
+
return this
|
|
203
|
+
}
|
|
204
|
+
onKey( fn ){
|
|
205
|
+
|
|
206
|
+
this.lastTouchedKey._onKey = fn
|
|
207
|
+
return this
|
|
208
|
+
}
|
|
209
|
+
onTween( fn ){
|
|
210
|
+
|
|
211
|
+
this.lastTouchedKey._onTween = fn
|
|
212
|
+
return this
|
|
213
|
+
}
|
|
214
|
+
onCancel( fn ){
|
|
215
|
+
|
|
216
|
+
this.lastTouchedKey._onCancel = fn
|
|
217
|
+
return this
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
// Chainable instance-wide methods.
|
|
222
|
+
|
|
223
|
+
onBeforeAll( fn ){
|
|
224
|
+
|
|
225
|
+
this._onBefore = fn
|
|
226
|
+
return this
|
|
227
|
+
}
|
|
228
|
+
onAfterAll( fn ){
|
|
229
|
+
|
|
230
|
+
this._onAfter = fn
|
|
231
|
+
return this
|
|
232
|
+
}
|
|
233
|
+
onEveryKey( fn ){
|
|
234
|
+
|
|
235
|
+
this._onEveryKey = fn
|
|
236
|
+
return this
|
|
237
|
+
}
|
|
238
|
+
onEveryTween( fn ){
|
|
239
|
+
|
|
240
|
+
this._onEveryTween = fn
|
|
241
|
+
return this
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
// Chainable commands.
|
|
246
|
+
|
|
247
|
+
update( now ){
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
// We only need to update
|
|
251
|
+
// if we are enabled
|
|
252
|
+
// and we have at least one keyframe
|
|
253
|
+
// that might have either a values object,
|
|
254
|
+
// an onKey callback,
|
|
255
|
+
// or would be included if there’s an onEveryKey callback.
|
|
256
|
+
|
|
257
|
+
if( this.isEnabled !== true ) return this
|
|
258
|
+
if( this.keys.length < 1 ) return this
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
// So I guess we’re doing this.
|
|
262
|
+
// What time is it?
|
|
263
|
+
// And what direction are we flowing?
|
|
264
|
+
|
|
265
|
+
if( isNotUsefulNumber( now )) now = Date.now()
|
|
266
|
+
if( now === this.timeCursor ) return this// Unlikely for standalone animations, but very likely for scroll animations.
|
|
267
|
+
const direction = now < this.timeCursor ? -1 : 1
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
// We know our keys are already sorted by time,
|
|
271
|
+
// and we’ve previously set the convenience variables
|
|
272
|
+
// `timeStart` and `timeStop`.
|
|
273
|
+
|
|
274
|
+
// Note 1: Direction has no effect on the order of
|
|
275
|
+
// keyA and keyB, because we always want this to be true:
|
|
276
|
+
// keyA.timeAbsolute < keyB.timeAbsolute.
|
|
277
|
+
// And we always use the tween attached to keyA!
|
|
278
|
+
|
|
279
|
+
// Note 2: For this step, it is perfectly reasonable
|
|
280
|
+
// for keyA or keyB to be undefined.
|
|
281
|
+
|
|
282
|
+
let
|
|
283
|
+
targetIndex = 0,
|
|
284
|
+
keyA,
|
|
285
|
+
keyB
|
|
286
|
+
|
|
287
|
+
if( now < this.timeStart ){
|
|
288
|
+
|
|
289
|
+
targetIndex = -1// Intentionally out of range.
|
|
290
|
+
keyA = this.keys[ 0 ]
|
|
291
|
+
keyB = this.keys[ 1 ]
|
|
292
|
+
}
|
|
293
|
+
else if( now > this.timeStop ){
|
|
294
|
+
|
|
295
|
+
targetIndex = this.keys.length// Intentionally out of range.
|
|
296
|
+
keyA = this.keys[ this.keys.length - 2 ]
|
|
297
|
+
keyB = this.keys[ this.keys.length - 1 ]
|
|
298
|
+
}
|
|
299
|
+
else {
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
// Price-is-Right rules:
|
|
303
|
+
// CLOSEST WITHOUT GOING OVER.
|
|
304
|
+
// If our direction is +1, we want the LATEST keyframe
|
|
305
|
+
// where `now` is still >= keyframe.timeAbsolute.
|
|
306
|
+
// If our direction is -1, we want the EARLIEST keyframe
|
|
307
|
+
// where `now` is still <= keyframe.timeAbsolute.
|
|
308
|
+
// That index gives us KeyA,
|
|
309
|
+
// and keys[ index + direction ] give us keyB.
|
|
310
|
+
|
|
311
|
+
let i = Math.min( Math.max( 0, this.keyIndex ), this.keys.length - 1 )
|
|
312
|
+
keyA = this.keys[ i ]
|
|
313
|
+
keyB = this.keys[ i + 1 ]
|
|
314
|
+
if( direction > 0 ){
|
|
315
|
+
|
|
316
|
+
while(
|
|
317
|
+
keyB instanceof Key &&
|
|
318
|
+
keyB.timeAbsolute < now ){
|
|
319
|
+
|
|
320
|
+
i ++
|
|
321
|
+
keyA = this.keys[ i ]
|
|
322
|
+
keyB = this.keys[ i + 1 ]
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
else if( direction < 0 ){
|
|
326
|
+
|
|
327
|
+
while(
|
|
328
|
+
keyA instanceof Key &&
|
|
329
|
+
keyA.timeAbsolute > now ){
|
|
330
|
+
|
|
331
|
+
i --
|
|
332
|
+
keyA = this.keys[ i ]
|
|
333
|
+
keyB = this.keys[ i + 1 ]
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
targetIndex = i
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
///////////////
|
|
345
|
+
// //
|
|
346
|
+
// onKey //
|
|
347
|
+
// //
|
|
348
|
+
///////////////
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
// Ok. Perhaps you were wondering
|
|
352
|
+
// why we hold onto a targetIndex value at all.
|
|
353
|
+
// We already have keyA and keyB -- just tween, right?
|
|
354
|
+
// Well... We’re in the business of
|
|
355
|
+
// GUARANTEEING keyframe onKey callbacks.
|
|
356
|
+
// That means, unless told otherwise, we need to hit
|
|
357
|
+
// each of those key frames and onKey() callbacks
|
|
358
|
+
// between wherever we were previously, and now.
|
|
359
|
+
|
|
360
|
+
// I had originally combined the following logic into one block,
|
|
361
|
+
// but debugging the subtleties became a true ass pain,
|
|
362
|
+
// so for clarity I separated them back out based on direction.
|
|
363
|
+
|
|
364
|
+
if( direction > 0 ){
|
|
365
|
+
|
|
366
|
+
for( let i = this.keyIndex + 1; i <= targetIndex; i ++ ){
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
// Yes, we do expect (and are accounting for!)
|
|
370
|
+
// a moment where i > this.keys.length - 1
|
|
371
|
+
// and therefore tempKey === undefined.
|
|
372
|
+
// This is expected behavior!
|
|
373
|
+
// You are going to be ok. Okay. O.K. OK.
|
|
374
|
+
|
|
375
|
+
const tempKey = this.keys[ i ]
|
|
376
|
+
if( tempKey instanceof Key &&
|
|
377
|
+
tempKey.guarantee === true ){
|
|
378
|
+
|
|
379
|
+
if( typeof tempKey._onKey === 'function' ){
|
|
380
|
+
|
|
381
|
+
tempKey._onKey( tempKey.values, this )
|
|
382
|
+
}
|
|
383
|
+
if( typeof this._onEveryKey === 'function' ){
|
|
384
|
+
|
|
385
|
+
this._onEveryKey( this.values, this )
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
else if( direction < 0 ){
|
|
391
|
+
|
|
392
|
+
for( let i = this.keyIndex; i > targetIndex; i -- ){
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
// See above disclaimer about expecting
|
|
396
|
+
// tempKey === undefined when at the edge
|
|
397
|
+
// of this.keys[].
|
|
398
|
+
|
|
399
|
+
const tempKey = this.keys[ i ]
|
|
400
|
+
if( tempKey instanceof Key &&
|
|
401
|
+
tempKey.guarantee === true ){
|
|
402
|
+
|
|
403
|
+
if( typeof tempKey._onKey === 'function' ){
|
|
404
|
+
|
|
405
|
+
tempKey._onKey( tempKey.values, this )
|
|
406
|
+
}
|
|
407
|
+
if( typeof this._onEveryKey === 'function' ){
|
|
408
|
+
|
|
409
|
+
this._onEveryKey( this.values, this )
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
this.keyIndex = targetIndex
|
|
415
|
+
this.timeCursor = now
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
/////////////////
|
|
423
|
+
// //
|
|
424
|
+
// onTween //
|
|
425
|
+
// //
|
|
426
|
+
/////////////////
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
// We need TWO valid keyframes in order to tween anything.
|
|
430
|
+
// If we don’t got, we bail now.
|
|
431
|
+
|
|
432
|
+
if( keyA instanceof Key !== true ||
|
|
433
|
+
keyB instanceof Key !== true ){
|
|
434
|
+
|
|
435
|
+
console.log( 'one of the keyframes was fucked.', keyA, keyB )
|
|
436
|
+
return this
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
// Do we need to implement onBefore with no valid keyB?
|
|
441
|
+
// Just pass keyA vals???+++
|
|
442
|
+
|
|
443
|
+
if( targetIndex < 0 &&
|
|
444
|
+
typeof this._onBefore === 'function' ){
|
|
445
|
+
|
|
446
|
+
this.tweenKeys( keyA, keyB, now )
|
|
447
|
+
this._onBefore( this.values, this )
|
|
448
|
+
// Note: We are NOT calling this._onEveryTween().
|
|
449
|
+
return this
|
|
450
|
+
}
|
|
451
|
+
if( targetIndex > this.keys.length - 1 &&
|
|
452
|
+
typeof this._onAfter === 'function' ){
|
|
453
|
+
|
|
454
|
+
this.tweenKeys( keyA, keyB, now )
|
|
455
|
+
this._onAfter( this.values, this )
|
|
456
|
+
// Note: We are NOT calling this._onEveryTween().
|
|
457
|
+
return this
|
|
458
|
+
}
|
|
459
|
+
if( targetIndex >= 0 &&
|
|
460
|
+
targetIndex < this.keys.length ){
|
|
461
|
+
|
|
462
|
+
this.tweenKeys( keyA, keyB, now )
|
|
463
|
+
if( typeof keyA._onTween === 'function' ){
|
|
464
|
+
|
|
465
|
+
keyA._onTween( this.values, this )
|
|
466
|
+
}
|
|
467
|
+
if( typeof this._onEveryTween === 'function' ){
|
|
468
|
+
|
|
469
|
+
this._onEveryTween( this.values, this )
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
return this
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
reset( newTimeStartAbsolute ){
|
|
481
|
+
|
|
482
|
+
this.disable()
|
|
483
|
+
if( isNotUsefulNumber( newTimeStartAbsolute )) newTimeStartAbsolute = Date.now()
|
|
484
|
+
|
|
485
|
+
let timeCursor = newTimeStartAbsolute
|
|
486
|
+
this.keys
|
|
487
|
+
.forEach( function( key, i, keys ){
|
|
488
|
+
|
|
489
|
+
timeCursor += key.timeRelative
|
|
490
|
+
keys[ i ].timeAbsolute = timeCursor// Again, see reasoning above for using .forEach rather than .reduce or .map here.
|
|
491
|
+
})
|
|
492
|
+
this.setTimeBounds()
|
|
493
|
+
this.keyIndex = -1
|
|
494
|
+
this.timeCursor = this.timeStart - 1
|
|
495
|
+
this.enable()
|
|
496
|
+
return this
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
// A quick way to turn individual pacers on/off,
|
|
501
|
+
// particuarly convenient if doing builk updates
|
|
502
|
+
// like Pacer.update() ← Note that’s the Class method itself,
|
|
503
|
+
// not an instance method.
|
|
504
|
+
|
|
505
|
+
enable(){
|
|
506
|
+
|
|
507
|
+
this.isEnabled = true
|
|
508
|
+
return this
|
|
509
|
+
}
|
|
510
|
+
disable(){
|
|
511
|
+
|
|
512
|
+
this.isEnabled = false
|
|
513
|
+
return this
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
// All those moments will be lost in time,
|
|
518
|
+
// like tears in rain.
|
|
519
|
+
// Time to die.
|
|
520
|
+
|
|
521
|
+
remove(){
|
|
522
|
+
|
|
523
|
+
Pacer.remove( this )
|
|
524
|
+
return this
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
// STATICS: `this === Pacer`
|
|
531
|
+
|
|
532
|
+
static all = []
|
|
533
|
+
static update( now ){
|
|
534
|
+
|
|
535
|
+
this.all.forEach( function( p ){
|
|
536
|
+
|
|
537
|
+
p.update( now )
|
|
538
|
+
})
|
|
539
|
+
}
|
|
540
|
+
static remove( instance ){
|
|
541
|
+
|
|
542
|
+
instance.isEnabled = false// Immediately prevents update() calls on the instance itself.
|
|
543
|
+
const index = this.all.indexOf( instance )
|
|
544
|
+
this.all.splice( index, 1 )
|
|
545
|
+
instance = null
|
|
546
|
+
}
|
|
547
|
+
static removeAll(){
|
|
548
|
+
|
|
549
|
+
this.all.forEach( function( p ){
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
// Immediately prevents update() calls on the instance itself,
|
|
553
|
+
// which may be in the process of being called
|
|
554
|
+
// by some outside bit of script’s looping update() function
|
|
555
|
+
// that is holding a reference to the instance itself.
|
|
556
|
+
// And we of course do not want that.
|
|
557
|
+
|
|
558
|
+
p.isEnabled = false
|
|
559
|
+
})
|
|
560
|
+
this.all = []
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
// Default tween.
|
|
565
|
+
// (Other tweens will be unpacked and added programmatically.)
|
|
566
|
+
|
|
567
|
+
static linear( n ){ return n }
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
// Tweening functions, aka Easing functions.
|
|
574
|
+
// “Tween” is of course short for “between”, as in _between_ the keyframes.
|
|
575
|
+
// Look how ’purty these symetric functions are boxed up.
|
|
576
|
+
// Down the road we ought to add Bezier() and Custom options.
|
|
577
|
+
|
|
578
|
+
Object.entries({
|
|
579
|
+
|
|
580
|
+
sine: n => 1 - Math.cos(( n * Math.PI ) / 2 ),
|
|
581
|
+
quadratic: n => Math.pow( n, 2 ),
|
|
582
|
+
cubic: n => Math.pow( n, 3 ),
|
|
583
|
+
quartic: n => Math.pow( n, 4 ),
|
|
584
|
+
quintic: n => Math.pow( n, 5 ),
|
|
585
|
+
exponential: n => x === 0 ? 0 : Math.pow( 2, 10 * x - 10 ),
|
|
586
|
+
circular: n => 1 - Math.sqrt( 1 - Math.pow( n, 2 )),
|
|
587
|
+
elastic: n => {
|
|
588
|
+
|
|
589
|
+
const c4 = ( 2 * Math.PI ) / 3
|
|
590
|
+
return n === 0
|
|
591
|
+
? 0
|
|
592
|
+
: n === 1
|
|
593
|
+
? 1
|
|
594
|
+
: -Math.pow( 2, 10 * n - 10 ) * Math.sin(( n * 10 - 10.75 ) * c4 )
|
|
595
|
+
},
|
|
596
|
+
back: ( n, c1, c3 )=> {
|
|
597
|
+
|
|
598
|
+
if( isNotUsefulNumber( c1 )) c1 = 1.70158
|
|
599
|
+
c3 = c1 + 1
|
|
600
|
+
return c3 * Math.pow( n, 3 ) - c1 * Math.pow( n, 2 )
|
|
601
|
+
}
|
|
602
|
+
})
|
|
603
|
+
.forEach( function( entry ){
|
|
604
|
+
|
|
605
|
+
const
|
|
606
|
+
key = entry[ 0 ],
|
|
607
|
+
val = entry[ 1 ]
|
|
608
|
+
|
|
609
|
+
Pacer[ key ] = {
|
|
610
|
+
|
|
611
|
+
in: val,
|
|
612
|
+
out: n => 1 - val( 1 - n ),
|
|
613
|
+
inOut: n => n < 0.5 ? val( n * 2 ) / 2 : val( n * 2 - 1 ) / 2 + 0.5
|
|
614
|
+
}
|
|
615
|
+
})
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
// Bounce doesn’t really make sense for “in” or “inOut”
|
|
619
|
+
// but I’m including it here for completeness.
|
|
620
|
+
|
|
621
|
+
Pacer.bounce = {
|
|
622
|
+
|
|
623
|
+
in: n => 1 - val( 1 - n ),
|
|
624
|
+
out: function( n, n1, d1 ){
|
|
625
|
+
|
|
626
|
+
if( isNotUsefulNumber( n1 )) n1 = 7.5625
|
|
627
|
+
if( isNotUsefulNumber( d1 )) d1 = 2.75
|
|
628
|
+
if( n < 1 / d1 ) return n1 * Math.pow( n, 2 )
|
|
629
|
+
else if( n < 2 / d1 ) return n1 * ( n -= 1.5 / d1 ) * n + 0.75
|
|
630
|
+
else if( n < 2.5 / d1 ) return n1 * ( n -= 2.25 / d1 ) * n + 0.9375
|
|
631
|
+
else return n1 * ( n -= 2.625 / d1 ) * n + 0.984375
|
|
632
|
+
},
|
|
633
|
+
inOut: n => n < 0.5 ? val( n * 2 ) / 2 : val( n * 2 - 1 ) / 2 + 0.5
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
|
|
637
|
+
|
|
638
|
+
|
|
639
|
+
export default Pacer
|
|
640
|
+
|
|
641
|
+
|
|
642
|
+
|
package/README.md
ADDED
|
@@ -0,0 +1,566 @@
|
|
|
1
|
+
```javascript
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
/////// // ////// /////// ///////
|
|
6
|
+
// // //// // // // // //
|
|
7
|
+
// // // // // ////// // //
|
|
8
|
+
/////// /////// // // // ///////
|
|
9
|
+
// // // ////// /////// // //
|
|
10
|
+
// // //
|
|
11
|
+
// //
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
Getting you from A to B since 2025.
|
|
16
|
+
|
|
17
|
+
<br>
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
## TL;DR
|
|
23
|
+
|
|
24
|
+
__Pacer__ is a light-weight keyframing toolkit inspired by [Soledad Penadés](https://soledadpenades.com/)’ original [tween.js](https://soledadpenades.com/projects/tween-js/) masterpiece. List your keyframes as time / value pairs, and __Pacer__ will ✨ tween your numbers and 📞 call your callbacks. __It’s minimal__. Only does what it needs to. __It’s reliable__. We use this in our own professional projects. We found the bumps and sanded them down so you won’t have to ✔️
|
|
25
|
+
|
|
26
|
+
```javascript
|
|
27
|
+
var p = new Pacer()
|
|
28
|
+
|
|
29
|
+
.key( Date.now(), { n: 0 })
|
|
30
|
+
.onKey(( e )=> console.log( '1st keyframe', e.n ))
|
|
31
|
+
|
|
32
|
+
.key( 2000, { n: 1 })
|
|
33
|
+
.onKey(( e )=> console.log( '2 seconds later', e.n ))
|
|
34
|
+
.onTween(( e )=> console.log( 'Ooooh!', e.n ))
|
|
35
|
+
|
|
36
|
+
.key( 2000, { n: 2 })
|
|
37
|
+
.onKey(( e )=> console.log( '2 more later', e.n ))
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Just stick this in your animation loop:
|
|
41
|
+
|
|
42
|
+
```javascript
|
|
43
|
+
p.update()
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
<br><br><br><br>
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
## Pacer features
|
|
54
|
+
|
|
55
|
+
With all the tweening and keyframing libraries already out there, why build a new one? Well, we write _a lot_ of JavaScript and we have _strong opinions_ about the libraries we use and the code we write. Sometimes that drives us to rip it all up and start afresh. Here are some aspects we gave particular attention to:
|
|
56
|
+
|
|
57
|
+
1. [Legible code](#legible-code)
|
|
58
|
+
2. [Function chaining](#function-chaining)
|
|
59
|
+
3. [Relative _and_ absolute timestamps](#relative-and-absolute-timestamps)
|
|
60
|
+
4. [Tweening](#tweening)
|
|
61
|
+
5. [Every key / every tween](#every-key--every-tween)
|
|
62
|
+
6. [Access within callbacks](#access-within-callbacks)
|
|
63
|
+
7. [Update all instances at once](#update-all-instances-at-once)
|
|
64
|
+
8. [Updating time](#updating-time)
|
|
65
|
+
9. [Forward _and_ backward](#forward-and-backward)
|
|
66
|
+
10. [Reduce, reuse, recycle](#reduce-reuse-recycle)
|
|
67
|
+
11. [Burn it to the ground](#burn-it-to-the-ground)
|
|
68
|
+
12. [Guaranteed keyframes](#guaranteed-keyframes)
|
|
69
|
+
13. [Outside the box](#outside-the-box)
|
|
70
|
+
14. [Verbose example](#verbose-example)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
### Legible code
|
|
76
|
+
|
|
77
|
+
Your __Pacer__ code says what it does. We wanted it to read like a short story. Animating is hard enough. It’s an iterative process that requires making, testing, and then _remaking._ You shouldn’t have to spend half your energy on deciphering your own code just to track down where that one keyframe is that you’re aiming to edit.
|
|
78
|
+
|
|
79
|
+
We did shorten some words, like “keyframe” → `key` and “between” → `tween`, but in each case we debated and only accepted the shortened terms when we felt the tradeoff between immediate clarity and simple brevity was acceptable. __Commands__ like `key` read as terse verbs and focus on simple assignment. (_“Keyframe_ this for me.”) __Event hooks__ like `onKey` always begin with an `on` prefix and facilitate callback functions. (“_On this keyframe,_ do this…”)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
### Function chaining
|
|
84
|
+
|
|
85
|
+
Expanding on the above, a code block should read like a normal paragraph of text—one idea following another in a logical sequence. With __Pacer__ you declare a keyframe, and [chain](https://en.wikipedia.org/wiki/Method_chaining) another right onto it. Perhaps you add an `onTween` callback _between_ those keyframes. Just about every __Pacer__ method returns its own instance, so you can chain from one method to another, to another—like writing the sentences of a short story.
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
### Relative _and_ absolute timestamps
|
|
91
|
+
|
|
92
|
+
By default, keyframes are specificed by _relative_ time. (“Do this two seconds after that last keyframe.”) This makes it trivial to swap pieces of an animation around—just cut and paste—without having to redo all the keyframe timings. Our TL;DR example uses the `key` command to illustrate this workflow, but we could have also used the slightly more descriptive `rel` (“relative”) alias to accomplish the exact same thing. All relative times are relative to the chronologically-latest keyframe as determined the moment the `key` or `rel` command is processed. (And yes, you can specify a _negative_ relative time—if you’re into that sort of thing.) What about your _first_ keyframe—which has no prior keyframe to be relative to? Consider it relative to _zero_—which makes it both relative _and_ absolute. Note the use of the alias `rel` here rather than `key`:
|
|
93
|
+
|
|
94
|
+
```javascript
|
|
95
|
+
var
|
|
96
|
+
now = Date.now(),
|
|
97
|
+
p = new Pacer()
|
|
98
|
+
|
|
99
|
+
.rel( now )
|
|
100
|
+
.onKey(()=> console.log( '1st keyframe' ))
|
|
101
|
+
|
|
102
|
+
.rel( 2000 )
|
|
103
|
+
.onKey(()=> console.log( '3rd keyframe' ))
|
|
104
|
+
|
|
105
|
+
.rel( -1000 )
|
|
106
|
+
.onKey(()=> console.log( '2nd keyframe' ))
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Meanwhile, specifying an absolute time for your keyframe is as easy as using the `abs` command instead of `key` or `rel`. Immediately after your new keyframe has been created, all keyframes are re-sorted in chronological order; ready for your next command.
|
|
110
|
+
|
|
111
|
+
```javascript
|
|
112
|
+
var
|
|
113
|
+
now = Date.now(),
|
|
114
|
+
p = new Pacer()
|
|
115
|
+
|
|
116
|
+
.abs( now )
|
|
117
|
+
.onKey(()=> console.log( '1st keyframe' ))
|
|
118
|
+
|
|
119
|
+
.abs( now + 2000 )
|
|
120
|
+
.onKey(()=> console.log( '3rd keyframe' ))
|
|
121
|
+
|
|
122
|
+
.abs( now + 1000 )
|
|
123
|
+
.onKey(()=> console.log( '2nd keyframe' ))
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Mix and match `key`, `rel`, and `abs` if it makes you smile.
|
|
127
|
+
|
|
128
|
+
```javascript
|
|
129
|
+
var
|
|
130
|
+
now = Date.now(),
|
|
131
|
+
p = new Pacer()
|
|
132
|
+
|
|
133
|
+
.key( now )
|
|
134
|
+
.onKey(()=> console.log( '1st keyframe' ))
|
|
135
|
+
|
|
136
|
+
.rel( 2000 )
|
|
137
|
+
.onKey(()=> console.log( '3rd keyframe' ))
|
|
138
|
+
|
|
139
|
+
.abs( now + 1000 )
|
|
140
|
+
.onKey(()=> console.log( '2nd keyframe' ))
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
### Tweening
|
|
147
|
+
|
|
148
|
+
By default your values are [linear interpolated](https://en.wikipedia.org/wiki/Linear_interpolation) (“lerped”) between keyframes. If you’re reading this and evaluating if __Pacer__ is the right solution for you, then I’m sure I don’t have to explain the importance of easing equations. We have the goods. Use `tween()` to pick from our built-in easing equations, and `onTween()` to register a callback function that will execute on each `update()` call that lands between your specified keyframes. Check how easy it is:
|
|
149
|
+
|
|
150
|
+
```javascript
|
|
151
|
+
var p = new Pacer()
|
|
152
|
+
|
|
153
|
+
.key( Date.now(), { n: 0 })
|
|
154
|
+
.onKey( ( e )=> console.log( 'KEY 1:', e.n ))
|
|
155
|
+
.onTween(( e )=> console.log( '1 → 2:', e.n ))
|
|
156
|
+
|
|
157
|
+
.key( 1000, { n: 100 })
|
|
158
|
+
.tween( Pacer.quadratic.out )
|
|
159
|
+
.onKey( ( e )=> console.log( 'KEY 2:', e.n ))
|
|
160
|
+
.onTween(( e )=> console.log( '2 → 3:', e.n ))
|
|
161
|
+
|
|
162
|
+
.key( 1000, { n: 200 })
|
|
163
|
+
.onKey( ( e )=> console.log( 'KEY 3:', e.n ))
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Just like `onKey`, the `tween` function applies to your _most recently declared_ keyframe. __Pacer__ includes dear [Robert Penner’s basic easing equations](https://robertpenner.com/easing/). Those are tacked directly onto the `Pacer` object, eg. `Pacer.cubic.*`. That makes them easy to find and include—even in non-__Pacer__ contexts. Here’s our list of easings:
|
|
167
|
+
`sine`,
|
|
168
|
+
`quadratic`,
|
|
169
|
+
`cubic`,
|
|
170
|
+
`quartic`,
|
|
171
|
+
`quintic`,
|
|
172
|
+
`exponential`,
|
|
173
|
+
`circular`,
|
|
174
|
+
`elastic`,
|
|
175
|
+
`back`, and
|
|
176
|
+
`bounce`.
|
|
177
|
+
|
|
178
|
+
Each easing equation includes its `in`, `out`, and `inOut` variants, eg. `Pacer.cubic.in`, `Pacer.cubic.out`, and `Pacer.cubic.inOut`, so you can hit the ground running. But like… ease into it, tho.
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
### Every key / every tween
|
|
184
|
+
|
|
185
|
+
If you find you’re running the same callback over and over, perhaps you’d prefer to declare that just once? We’ve got you covered. Use `onEveryKey` to declare a callback that will fire on _every_ keyframe, and `onEveryTween` to do the same for all tweens. [Something borrowed, something blue. Every tween callback for you](https://youtu.be/4YR_Mft7yIM).
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
```javascript
|
|
189
|
+
var p = new Pacer()
|
|
190
|
+
.key( Date.now(), { n: 0 })
|
|
191
|
+
.key( 1000, { n: 100 })
|
|
192
|
+
.key( 1000, { n: 200 })
|
|
193
|
+
.onEveryKey( ( e )=> console.log( e.n, 'KEY!' ))
|
|
194
|
+
.onEveryTween(( e )=> console.log( e.n ))
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
### Access within callbacks
|
|
201
|
+
|
|
202
|
+
__Pacer__’s `onKey` and `onTween` methods provide a reference to its own instance as a callback argument. The instance includes potentially useful properties, like `keyIndex` which tells you which keyframe in the sequence you are currently on.
|
|
203
|
+
|
|
204
|
+
```javascript
|
|
205
|
+
var p = new Pacer()
|
|
206
|
+
.key( Date.now() )
|
|
207
|
+
.key( 1000 )
|
|
208
|
+
.key( 1000 )
|
|
209
|
+
.onEveryKey(( e, p )=> console.log(
|
|
210
|
+
|
|
211
|
+
'Step #', p.keyIndex + 1,
|
|
212
|
+
'of', p.keys.length
|
|
213
|
+
))
|
|
214
|
+
.onEveryTween(( e, p )=> console.log(
|
|
215
|
+
|
|
216
|
+
'Between #', p.keyIndex + 1,
|
|
217
|
+
'and', p.keyIndex + 2
|
|
218
|
+
))
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
This means in theory you don’t even have to name your __Pacer__ instance if all you want to do is reference that instance within your callbacks. Note the lack of assignment here:
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
```javascript
|
|
225
|
+
new Pacer()
|
|
226
|
+
.key( Date.now() )
|
|
227
|
+
.key( 1000 )
|
|
228
|
+
.key( 1000 )
|
|
229
|
+
.onEveryKey(( e, p )=> console.log(
|
|
230
|
+
|
|
231
|
+
'Step #', p.keyIndex + 1,
|
|
232
|
+
'of', p.keys.length
|
|
233
|
+
))
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
### Update all instances at once
|
|
240
|
+
|
|
241
|
+
But how do you update an _unnamed_ instance? Under the hood, __Pacer__ keeps a reference to all created instances in its `Pacer.all` array. You can update every single instance at once by sticking this in your animation loop:
|
|
242
|
+
|
|
243
|
+
```javascript
|
|
244
|
+
Pacer.update()
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
And because `onKey` and `onTween` provide the same callback arguments, it’s trivial to use the same callback for both.
|
|
248
|
+
|
|
249
|
+
```javascript
|
|
250
|
+
var myCallback = ( e, p )=> console.log(
|
|
251
|
+
|
|
252
|
+
'Step:', p.keyIndex + 1,
|
|
253
|
+
'value:', e.n
|
|
254
|
+
)
|
|
255
|
+
new Pacer()
|
|
256
|
+
.key( Date.now(), { n: 0 })
|
|
257
|
+
.key( 1000, { n: 100 })
|
|
258
|
+
.key( 1000, { n: 200 })
|
|
259
|
+
.onEveryKey( myCallback )
|
|
260
|
+
.onEveryTween( myCallback )
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
### Updating time
|
|
267
|
+
|
|
268
|
+
You’ve seen that you can update your instance with `p.update()`, or all instances at once with `Pacer.update()`. But now you’re interested in finer control of your timing. When either the class or instance `update` method is called without arguments, __Pacer__ defaults to `Date.now()`, but you are free to use any numeric progression you choose. Perhaps you want to key off of `window.performance.now()` for finer accuraccy. Or maybe you’re building a scroll-based animation and you’re substituting `scrollY` (pixels) for time. Just pass your value via update:
|
|
269
|
+
|
|
270
|
+
```javascript
|
|
271
|
+
p.update( numericValue )
|
|
272
|
+
```
|
|
273
|
+
Be sure you’re consistent with your units. __Pacer__ isn’t going to magically understand that you’ve used seconds to declare keyframes, but milliseconds in your `update` call. That’s on you.
|
|
274
|
+
|
|
275
|
+
Another thing to note is that `update` expects an _absolute_ number, rather than a _relative_ one. Repeatedly calling `p.update( 1000 )` will _not_ advance your animation by one second with each call. Instead it will lock your animation at its one second mark. Relative units are enormously useful for crafting (and recrafting) keyframes, but slightly less useful within the context of synchronization. It’s taken years of building projects like this to be able to feel confident in asserting this subtlety.
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
### Forward _and_ backward
|
|
281
|
+
|
|
282
|
+
Mathematically, [time can flow both forward _and_ backward](https://en.wikipedia.org/wiki/Tenet_(film)). Why would __Pacer__ ignore that reality? The ability to scrub a timeline back and forth is incredibly valuable, and literally the mechanism that our __ScrollPacer__ toolkit uses for scroll-based animations. (More on this to come.) Rest assured that your `update` call can handle time flowing in either direction (and at any speed). It just works.
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
### Enable / disable
|
|
288
|
+
|
|
289
|
+
Need to gate your __Pacer__ instance? (Let’s assume you’ve named it `p`.) Prevent it from chewing `update` cycles:
|
|
290
|
+
|
|
291
|
+
```javascript
|
|
292
|
+
p.disable()
|
|
293
|
+
```
|
|
294
|
+
Ready to return to service?
|
|
295
|
+
|
|
296
|
+
```javascript
|
|
297
|
+
p.enable()
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
### Reduce, reuse, recycle
|
|
304
|
+
|
|
305
|
+
Re-running an animation is easy. The `reset` method recalculates the timing of all of your instance’s keyframes based on the numeric argument provided. (With no arguments, the `reset` method defaults to `Date.now()`.) Here’s an example of taking a previously used animation and restarting it two seconds from now:
|
|
306
|
+
|
|
307
|
+
```javascript
|
|
308
|
+
p.reset( Date.now() + 2000 )
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
### Burn it to the ground
|
|
315
|
+
|
|
316
|
+
Done with your instance for good? (We’re not talking about “pausing” your instance—we’re about to _destroy_ your instance.) Remove all of __Pacer__’s references to it and set the instance to `null` with:
|
|
317
|
+
|
|
318
|
+
```javascript
|
|
319
|
+
p.remove()
|
|
320
|
+
```
|
|
321
|
+
Or via the class itself:
|
|
322
|
+
|
|
323
|
+
```javascript
|
|
324
|
+
Pacer.remove( p )
|
|
325
|
+
```
|
|
326
|
+
Seeking total destruction?
|
|
327
|
+
|
|
328
|
+
```javascript
|
|
329
|
+
Pacer.removeAll()
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
### Guaranteed keyframes
|
|
336
|
+
|
|
337
|
+
We pledge to deliver all of your keyframes with a money-back guarantee. (Reminder: You have paid zero dollars for this toolkit. Donations don’t count.) By default, each keyframe has a `guarantee` Boolean set to `true` that assures `onKey` will be called when calculating the gulf between “now” and our animation loop’s prior execution. Let’s say you have keyframes spaced very close together in time—tighter than your animation loop is able to execute. In this example, our last `update` call determined that we were between Key Frame __A__ and Key Frame __B__:
|
|
338
|
+
|
|
339
|
+
```
|
|
340
|
+
KEY KEY KEY KEY
|
|
341
|
+
FRAME FRAME FRAME FRAME
|
|
342
|
+
A B C D
|
|
343
|
+
|
|
344
|
+
┄┄┼─────────┼─────────┼─────────┼┄┄
|
|
345
|
+
|
|
346
|
+
prior ↑
|
|
347
|
+
update │ this ↑
|
|
348
|
+
update │
|
|
349
|
+
```
|
|
350
|
+
However, on this current call to `update`, we have not merely reached Key Frame __B__, but have passed both it and Key Frame __C__ to arrive between __C__ and __D__. __Pacer__ ensures that if `onKey` callbacks exist for __B__ and __C__ they will be honored—and in order. Flowing backward through time? Sleep tight knowing they’ll be called in an order that respects your flow of time, eg. __C__ _then_ __B__ when flowing backward. That’s the __Pacer__ Keyframe Guarantee™.
|
|
351
|
+
|
|
352
|
+
As you’d hope, __Pacer__ will also call `onEveryKey` when it honors `onKey` for __B__ and __C__. Note that `onTween` and `onEveryTween` will _not_ be called for any values between __B__ and __C__ as we are not experiencing time between those keyframes.
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
### Outside the box
|
|
358
|
+
|
|
359
|
+
What happens outside of your declared keyframes? __Pacer__ automatically extrapolates your first and last tweens forward and backward in time, beyond your declared keyframes. You often don’t need this—but when you do, you do. Let’s say you have two keyframes, __A__ at time __0__, and __B__ at time __2__. They’re tweening a value, `n`, from `0` to `1` using the default linear interpolation easing function. As a result you can see that at time __1__, the tweened value of `n` will be `0.5`—halfway between its keyframed values of `0` and `1`. So far so good?
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
```
|
|
363
|
+
KEY KEY
|
|
364
|
+
FRAME FRAME
|
|
365
|
+
A B
|
|
366
|
+
|
|
367
|
+
┄┼┄┄┄┄┄┄┄┄┄╞═════════╪═════════╡┄┄┄┄┄┄┄┄┄┼┄
|
|
368
|
+
t 0 1 2
|
|
369
|
+
|
|
370
|
+
n 0.0 0.5 1.0
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
But what if we wanted to know the tweened value of `n` beyond the specified keyframes? What if we want to know `n` at time __-1__? Or at time __3__? __Pacer__ extends the value of `n` infinitely outward on either side of the timeline using the existing tweening functions on either end of the keyframe sequence. In this simple case we’re using the default linear interpolation on both ends, so it’s trivial to see that at time __-1__ `n` ought to be `-0.5`. This is consistent with its declared trajectory between time __0__ and __1__—or __0__ and __2__, for that matter. Similarly, at time __3__, `n` will be `1.5`.
|
|
374
|
+
|
|
375
|
+
```
|
|
376
|
+
KEY KEY
|
|
377
|
+
FRAME FRAME
|
|
378
|
+
A B
|
|
379
|
+
|
|
380
|
+
┄┼┄┄┄┄┄┄┄┄┄╞═════════╪═════════╡┄┄┄┄┄┄┄┄┄┼┄
|
|
381
|
+
t -1 0 1 2 3
|
|
382
|
+
|
|
383
|
+
n -0.5 0.0 0.5 1.0 1.5
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
Because these times exist beyond our declared keyframes, `onEveryTween` will _not_ fire. (Just imagine how annoying that would become—requiring you to gate all of your `onEveryTween` callbacks based on whether or not the current time was actually within your expected range.) So how do we make use of this tween extrapolation? Here’s an example pre-history callback:
|
|
387
|
+
|
|
388
|
+
```javascript
|
|
389
|
+
p.onBeforeAll(( e, p )=> console.log(
|
|
390
|
+
|
|
391
|
+
'Pre-history value:', e.n,
|
|
392
|
+
'Current key index:', p.keyIndex,
|
|
393
|
+
'Current keyframe: ', p.getCurrentKey()
|
|
394
|
+
))
|
|
395
|
+
```
|
|
396
|
+
And here’s the post-history complement:
|
|
397
|
+
|
|
398
|
+
```javascript
|
|
399
|
+
p.onAfterAll(( e, p )=> console.log(
|
|
400
|
+
|
|
401
|
+
'Post-history value:', e.n,
|
|
402
|
+
'Current key index: ', p.keyIndex,
|
|
403
|
+
'Current keyframe: ', p.getCurrentKey()
|
|
404
|
+
))
|
|
405
|
+
```
|
|
406
|
+
Note that for both of these, `p.keyIndex` will be _out of range_ of `p.keys` (`-1` and `keys.length`, respectively.) Consequently, `p.getCurrentKey()` will return `undefined`. This is expected behavior—you are beyond the timeline of the keyframes, after all. Here’s some pseudocode for additional clarity:
|
|
407
|
+
|
|
408
|
+
```
|
|
409
|
+
if keyIndex === -1 → onBeforeAll()
|
|
410
|
+
if keyIndex 0..keys.length-1 → onEveryTween()
|
|
411
|
+
if keyIndex === keys.length → onAfterAll()
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
<br><br>
|
|
418
|
+
<hr>
|
|
419
|
+
<br><br>
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
## Verbose example
|
|
425
|
+
|
|
426
|
+
Let’s cram in a bunch of different feature highlights into this one verbose example.
|
|
427
|
+
|
|
428
|
+
```javascript
|
|
429
|
+
// We’ll start off with the basics.
|
|
430
|
+
// Did you know you can label a Pacer instance
|
|
431
|
+
// by passing it a String?
|
|
432
|
+
// That’s useful for debugging later.
|
|
433
|
+
|
|
434
|
+
var p = new Pacer( 'My first Pacer' )
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
// Three keyframes, alike in dignity.
|
|
438
|
+
// Note how we’re starting at time === 0.
|
|
439
|
+
// Well that’s sliiightly earlier than Date.now()!
|
|
440
|
+
// Don’t worry, we’ll fix it below
|
|
441
|
+
// when we demonstrate reset().
|
|
442
|
+
|
|
443
|
+
.key( 0, { n: 0 })
|
|
444
|
+
.onKey(( e )=> console.log( '1st keyframe.', e.n ))
|
|
445
|
+
|
|
446
|
+
.key( 2000, { n: 100 })
|
|
447
|
+
.onKey(( e )=> console.log( '2 seconds later.', e.n ))
|
|
448
|
+
|
|
449
|
+
.key( 2000, { n: 200 })
|
|
450
|
+
.onKey(( e )=> console.log( '+2 more seconds.', e.n ))
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
// Now let’s have some fun with tweening.
|
|
454
|
+
|
|
455
|
+
.key( 2000, { n: 300 })
|
|
456
|
+
.label( 'My first tween!' )
|
|
457
|
+
.tween( Pacer.sine.in )
|
|
458
|
+
.onKey(( e, p )=> console.log( p.getCurrentKey().label ))
|
|
459
|
+
.onTween(( e )=> console.log( 'Tweened value:', e.n ))
|
|
460
|
+
|
|
461
|
+
.key( 2000, { n: 400 })
|
|
462
|
+
.label( 'My second tween' )
|
|
463
|
+
.tween( Pacer.quadratic.out )
|
|
464
|
+
.onKey(( e, p )=> console.log( p.getCurrentKey().label ))
|
|
465
|
+
.onTween(( e )=> console.log( 'Tweened value:', e.n ))
|
|
466
|
+
|
|
467
|
+
.key( 2000, { n: 500 })
|
|
468
|
+
.label( 'Let’s stop labeling things now.' )
|
|
469
|
+
.tween( Pacer.bounce.inOut )
|
|
470
|
+
.onTween(( e, p )=> console.log(
|
|
471
|
+
|
|
472
|
+
'Step:', p.keyIndex + 1,
|
|
473
|
+
'value:', e.n
|
|
474
|
+
))
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
// Can haz multiple tweens at once?
|
|
478
|
+
// Of course you can!
|
|
479
|
+
|
|
480
|
+
.key( 2000, { n: 600, x: 100 })
|
|
481
|
+
.onTween(( e )=> console.log( e.n, e.x ))
|
|
482
|
+
.key( 2000, { n: 700, x: -100 })
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
// Totally commenting these out
|
|
486
|
+
// in case you copy and paste this whole thing
|
|
487
|
+
// into your console.
|
|
488
|
+
// You see what it is. You see how it works.
|
|
489
|
+
// I think we’re good.
|
|
490
|
+
|
|
491
|
+
.onEveryKey(( e, p )=>{
|
|
492
|
+
|
|
493
|
+
// console.log( 'Step:', p.keyIndex + 1, 'values:', e )
|
|
494
|
+
})
|
|
495
|
+
.onEveryTween(( e, p )=>{
|
|
496
|
+
|
|
497
|
+
// console.log( 'Step:', p.keyIndex + 1, 'values:', e )
|
|
498
|
+
})
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
// Note that for “before” `keyIndex` will be
|
|
502
|
+
// “out of bounds” with a value of -1,
|
|
503
|
+
// so `getCurrentKey()` will return undefined.
|
|
504
|
+
// This is intended. We’re out of keyframes!
|
|
505
|
+
// We are extrapolating our first tween.
|
|
506
|
+
// Also note that `onEveryTween` will NOT
|
|
507
|
+
// be called in these before / after cases.
|
|
508
|
+
|
|
509
|
+
.onBeforeAll(( e, p )=> console.log(
|
|
510
|
+
|
|
511
|
+
'“Theoretical” step:', p.keyIndex + 1,
|
|
512
|
+
'value:', e.n
|
|
513
|
+
))
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
// Similarly, for “after” `keyIndex` will be
|
|
517
|
+
// “out of bounds” with a value of keys.length,
|
|
518
|
+
// so `getCurrentKey()` will return undefined.
|
|
519
|
+
// We are extrapolating our final tween.
|
|
520
|
+
// `onEveryTween` will NOT be called.
|
|
521
|
+
|
|
522
|
+
.onAfterAll(( e, p )=> console.log(
|
|
523
|
+
|
|
524
|
+
'“Theoretical” step:', p.keyIndex + 1,
|
|
525
|
+
'value:', e.n
|
|
526
|
+
))
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
// This sequence effectively does nothing
|
|
530
|
+
// to our animation execution,
|
|
531
|
+
// but does demonstrate the existence
|
|
532
|
+
// of these features,
|
|
533
|
+
// and the beauty of function chaining.
|
|
534
|
+
|
|
535
|
+
.disable()
|
|
536
|
+
.enable()
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
// Remember how we declared this instance
|
|
540
|
+
// starts at time === 0?
|
|
541
|
+
// Let’s fix that to start at 2 seconds from now.
|
|
542
|
+
// YES -- you can add this reset()
|
|
543
|
+
// within a keyframe callback! Get loopy!
|
|
544
|
+
|
|
545
|
+
.reset( Date.now() + 2000 )
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
<!--
|
|
551
|
+
|
|
552
|
+
## Commands
|
|
553
|
+
|
|
554
|
+
key
|
|
555
|
+
tween
|
|
556
|
+
rel
|
|
557
|
+
abs
|
|
558
|
+
etc
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
## Event hooks
|
|
562
|
+
|
|
563
|
+
onKey
|
|
564
|
+
onTween
|
|
565
|
+
|
|
566
|
+
-->
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pacer-js",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Getting you from A to B since 2025.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"animation",
|
|
7
|
+
"keyframe",
|
|
8
|
+
"tween",
|
|
9
|
+
"ease",
|
|
10
|
+
"pace",
|
|
11
|
+
"shoes"
|
|
12
|
+
],
|
|
13
|
+
"homepage": "https://github.com/stewdio/pacer-jsr#readme",
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/stewdio/pacer-js/issues"
|
|
16
|
+
},
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/stewdio/pacer-js.git"
|
|
20
|
+
},
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"author": "Stewart Smith",
|
|
23
|
+
"type": "module",
|
|
24
|
+
"main": "Pacer.js",
|
|
25
|
+
"scripts": {
|
|
26
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"shoes-js": "^1.0.1"
|
|
30
|
+
}
|
|
31
|
+
}
|