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.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/Pacer.js +642 -0
  3. package/README.md +566 -0
  4. 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
+ }