-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsteploop.ts
More file actions
667 lines (609 loc) · 21.2 KB
/
steploop.ts
File metadata and controls
667 lines (609 loc) · 21.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
/**
* @fileoverview Provides the {@link StepLoop} class, a foundation for building loops that execute at a consistent, specified rate.
*
* To define a new loop, extend the {@link StepLoop} class and override its methods to implement custom behavior.
*
* ### Lifecycle
*
* The {@link StepLoop} class executes in three distinct stages, with hooks that can be overridden to add custom logic:
*
* 1. **Initialization:** Runs once at the beginning of the loop
* - {@link StepLoop.initial()}
* 2. **Looping:** The core of the loop, which repeatedly executes the following sequence:
* - {@link StepLoop.background()} (async)
* - {@link StepLoop.before()}
* - {@link StepLoop.step()}
* - {@link StepLoop.after()}
* 3. **Termination:** Runs once when the loop ends, either by reaching the end of its lifespan or being manually stopped
* - {@link StepLoop.final()}
*
* The loop can run indefinitely or for a set number of steps, and its execution can be precisely controlled, allowing it to be paused, resumed, and dynamically modified at runtime.
*
* @module steploop
*/
/**
* A base class for building loops that execute at a consistent, specified rate.
*
* {@link StepLoop} provides a structured lifecycle with methods that can be overridden to implement custom behavior.
*
* The {@link StepLoop} class manages the timing and execution flow, supporting both fixed-step updates via {@link setTimeout()} and smoother, display-synchronized updates using {@link window.requestAnimationFrame()}.
*
* The loop can run indefinitely or for a set number of steps, and its execution can be precisely controlled, allowing it to be paused, resumed, and dynamically modified at runtime.
*
* @example
* ```ts
* import { StepLoop } from "steploop";
*
* class App extends StepLoop {
* override initial(): void {
* console.log("Loop starting");
* }
*
* override step(): void {
* console.log(`Executing step: ${this.get_step()}`);
* }
*
* override final(): void {
* console.log("Loop finished");
* }
* }
*
* // Create a new loop that runs at 60 steps-per-second for 100 steps
* const loop = new App(60, 100);
* loop.start();
* ```
* @class
*/
export class StepLoop {
private _step_num: number = 0;
private _lifespan: number | undefined;
private _sps: number;
private _interval: number;
private _startTime: number = 0;
private _lastStepTime: number = 0;
private _lastStepDuration: number = 0;
private _timeoutId: ReturnType<typeof setTimeout> | undefined;
private _initialized: boolean = false;
private _running: boolean = false;
private _paused: boolean = false;
private _kill: boolean = false;
/**
* Create a `StepLoop`, with options to define the steps-per-second and the lifespan of the loop.
* @param {number} sps - the steps-per-second of the loop (note: values that are greater than about 250 may result in unexpected behavior); default value is 60
* @param {number | undefined} lifespan - the number of steps that are executed before the loop ends; setting to `undefined` will result in an unlimited lifespan; default value is `undefined`
*/
constructor(sps: number = 60, lifespan: number | undefined = undefined, RAF: boolean = false) {
this._lifespan = lifespan;
this._sps = sps;
this._interval = 1000 / this._sps;
//this._lastTime = performance.now();
this._timeoutId = undefined;
this._RAFActive = RAF;
}
/**
* Override {@link StepLoop.initial()} to add an initial block of code to execute at the very beginning of the loop.
*
* The first code executed in the {@link StepLoop}. Called once at the beginning of the {@link StepLoop} lifecycle, and then moves on to the first {@link StepLoop.background()} call in the looping stage after resolving. Executed right after {@link StepLoop.start()} is called.
*
* @returns {void} `void`
* @example
* ```ts
* class App extends StepLoop {
* public override initial(): void {
* console.log(`initial: ${Date.now()}`);
* }
* }
* ```
* @instance
*/
public initial(): void {
return;
}
/**
* Override {@link StepLoop.background()} to add a block of code to run in the background of each step of your loop.
*
* Executed in the background at the beginning of the looping stage. Called asynchronously before the rest of the loop, executes while the rest of the loop does. Starts before {@link StepLoop.before()} but may not resolve before it is called.
*
* @returns {Promise<void>} `Promise<void>`
* @example
* ```js
* class App extends StepLoop {
* public override async background(): Promise<void> {
* console.log(`background: ${this.get_step()}`);
* }
* }
* ```
* @instance
*/
public async background(): Promise<void> {
return;
}
/**
* Override {@link StepLoop.before()} to add a block of code to run before each step of your loop.
*
* Executed in the looping stage before the main {@link StepLoop.step()} code. Resolves before calling {@link StepLoop.step()}. Use this function to set up anything you need before {@link StepLoop.step()} is called.
*
* @returns {void} `void`
* @example
* ```ts
* class App extends StepLoop {
* public override before(): void {
* console.log(`before: ${this.get_step()}`);
* }
* }
* ```
* @instance
*/
public before(): void {
return;
}
/**
* Override {@link StepLoop.step()} to add the code for the main step of your loop.
*
* The main loop code executed in the looping stage. Called after {@link StepLoop.before()} resolves, and resolves before {@link StepLoop.after()} is called. Use {@link StepLoop.step()} as the main update function of your {@link StepLoop}.
*
* @returns {void} `void`
* @example
* ```ts
* class App extends StepLoop {
* public override step(): void {
* console.log(`step: ${this.get_step()}`);
* }
* }
* ```
* @instance
*/
public step(): void {
return;
}
/**
* Override {@link StepLoop.after()} to add a block of code to run after each step of your loop.
*
* Executed in the looping stage after the main {@link StepLoop.step()} code. Called after {@link StepLoop.step()} resolves. Use this function to clean up anything after {@link StepLoop.step()} resolves.
*
* @returns {void} `void`
* @example
* ```ts
* class App extends StepLoop {
* public override after(): void {
* console.log(`after: ${this.get_step()}`);
* }
* }
* ```
* @instance
*/
public after(): void {
return;
}
/**
* Override {@link StepLoop.final()} to add a final block of code to run at the very end of the loop.
*
* The last code executed in the {@link StepLoop}, called after the looping stage is done. Executed once at the end of the {@link StepLoop} lifecycle, and then kills the loop. Called when the number of steps executed is greater than the lifespan of the {@link StepLoop} (i. e. {@link StepLoop.get_step()} `>` {@link StepLoop.get_lifespan()}) or when {@link StepLoop.finish()} is called.
*
* @returns {void} `void`
* @example
* ```ts
* class App extends StepLoop {
* public override final(): void {
* console.log(`final: ${Date.now()}`);
* }
* }
* ```
* @instance
*/
public final(): void {
return;
}
/**
* Override {@link StepLoop.on_pause()} to add a block of code to execute immediately after calling {@link StepLoop.pause()}.
*
* Called only when the {@link StepLoop} is paused, then stops executing until {@link StepLoop.play()} is called.
*
* @returns {void} `void`
* @example
* ```ts
* class App extends StepLoop {
* public override on_pause(): void {
* console.log(`paused`);
* }
* }
* ```
* @instance
*/
public on_pause(): void {
return;
}
/**
* Override {@link StepLoop.on_play()} to add a block of code to execute immediately after calling {@link StepLoop.play()}.
*
* Called only when the {@link StepLoop} is played, then proceeds with the rest of the loop.
*
* @returns {void} `void`
* @example
* ```ts
* class App extends StepLoop {
* public override on_play(): void {
* console.log(`played`);
* }
* }
* ```
* @instance
*/
public on_play(): void {
return;
}
/**
* Returns `true` if the {@link StepLoop} is running and false otherwise.
*
* @returns {boolean} `true` if the loop is currently running
* @example
* ```ts
* class App extends StepLoop {}
* let app: App = new App();
* app.start()
*
* console.log(app.is_running()) // Output -> `true`
* ```
* @instance
*/
public is_running(): boolean {
return this._running;
}
/**
* Returns `true` if the {@link StepLoop} is paused and false otherwise.
*
* @returns {boolean} `true` if the loop is currently paused
* @example
* ```ts
* class App extends StepLoop {}
* let app: App = new App();
* app.start()
*
* console.log(app.is_paused()) // Output -> `false`
* ```
* @instance
*/
public is_paused(): boolean {
return this._paused;
}
/**
* Returns the current step number (the number of times the loop has run).
*
* @returns {number} the current step number
* @example
* ```ts
* class App extends StepLoop {}
* let app: App = new App();
* app.start()
*
* console.log(app.get_step()) // Output -> `1`
* ```
* @instance
*/
public get_step(): number {
return this._step_num;
}
/**
* Returns the current steps-per-second (sps).
*
* @returns {number} the current steps-per-second (sps)
* @example
* ```ts
* class App extends StepLoop {}
* let app: App = new App();
* app.start()
*
* console.log(app.get_sps()) // Output -> `60`
* ```
* @instance
*/
public get_sps(): number {
return this._sps;
}
/**
* Returns the real steps-per-second (sps) based on the time between the last two steps. This value may not be accurate until after the first few steps have completed.
*
* @returns {number} the real steps-per-second (sps)
* @example
* ```ts
* class App extends StepLoop {}
* let app: App = new App();
* app.start()
*
* console.log(app.get_real_sps())
* ```
* @instance
*/
public get_real_sps(): number {
if (this._lastStepDuration === 0) {
return 0;
}
return 1000 / this._lastStepDuration;
}
/**
* Returns the current lifespan of the {@link StepLoop} (in steps).
*
* @returns {number | undefined} the current loop lifespan; returns `undefined` if the lifespan is unlimited
* @example
* ```ts
* class App extends StepLoop {}
* let app: App = new App(500);
* app.start()
*
* console.log(app.get_lifespan()) // Output -> `500`
* ```
* @instance
*/
public get_lifespan(): number | undefined {
return this._lifespan;
}
/**
* Sets the current steps-per-second (sps). Alters the speed at which the {@link StepLoop} runs: higher values will result in more steps in a faster step-speed and lower values will result in a lower step-speed. Default speed is 60 steps-per-second.
*
* @param {number} sps - the target steps-per-second; default value is `60`
* @returns {number} the new steps-per-second
* @example
* ```ts
* class App extends StepLoop {}
* let app: App = new App();
* app.start()
*
* console.log(app.set_sps(120)) // Output -> `120`
* ```
* @instance
*/
public set_sps(sps: number): number {
//if (this._initialized) return this._sps;;
this._sps = sps;
this._interval = 1000/this._sps
return this._sps;
}
/**
* Set whether or not to use {@link window.requestAnimationFrame()} for the {@link StepLoop}. When set to `true`, the loop will synchronize with the browser's rendering cycle (if the loop is running in a browser), which can result in smoother animations and better performance. When disabled, the loop will use a step-scheduler based on {@link setTimeout()}, which may be less efficient but more predictable.
*
* @param {boolean} status - `true` to use `requestAnimationFrame`, `false` to use the step scheduler.
* @returns {boolean} the new status of `requestAnimationFrame`
* @example
* ```ts
* class App extends StepLoop {}
* let app: App = new App();
*
* app.set_use_RAF(true)
* app.start()
* ```
* @instance
*/
public set_use_RAF(status: boolean): boolean {
this._RAFActive = status;
return this._RAFActive;
}
/**
* Set the lifespan of the {@link StepLoop} to the specified number of steps, or removes the limit on the {@link StepLoop}'s lifespan (will run until {@link StepLoop.finish()} is called).
*
* If {@link StepLoop.set_lifespan()} is called after the lifespan limit is reached, {@link StepLoop.play()} can be called to resume executing the {@link StepLoop}. The termination stage will be executed again when the limit is reached again.
*
* @param {number} [steps] - the target lifespan (in number of steps); if `undefined` the lifespan becomes unlimited; default value is `undefined` if not provided
* @returns {number | undefined} the new lifespan
* @example
* ```ts
* class App extends StepLoop {}
* let app: App = new App();
* app.start()
*
* console.log(app.set_lifespan(100)) // Output -> `100`
* ```
* @instance
*/
public set_lifespan(steps?: number ): number | undefined {
if (!this._initialized) return undefined;
if (typeof steps != "number") {
this._lifespan = undefined;
} else {
this._lifespan = steps;
if(this._kill && (this._lifespan > this._step_num)){
this._kill = false
}
}
return this._lifespan;
}
/**
* Extend (or reduce) the lifespan of the {@link StepLoop}. Adds the specified number of steps to the current lifespan.
*
* If {@link StepLoop.extend_lifespan()} is called after the lifespan limit is reached, {@link StepLoop.play()} can be called to resume executing the {@link StepLoop}. The termination stage will be executed again when the limit is reached again.
*
* @param {number} steps - the number of steps to add to the lifespan
* @returns {number | undefined} the new lifespan; returns undefined if the loop is uninitialized
* @example
* ```ts
* class App extends StepLoop {}
* let app: App = new App();
* app.start()
*
* console.log(app.extend_lifespan(100)) // Output -> `100`
* ```
* @instance
*/
public extend_lifespan(steps: number ): number | undefined {
if (!this._initialized) return undefined;
this._lifespan = (this._lifespan || 0) + steps;
if(this._kill && (this._lifespan > this._step_num)){
this._kill = false
}
return this._lifespan;
}
/**
* Pause the execution of the {@link StepLoop} after the current step resolves. Steps will not advance and the current step ({@link StepLoop.get_step()}) will not increase while the {@link StepLoop} is paused. Use {@link StepLoop.play()} to resume execution and continue the loop.
*
* @returns {void} `void`
* @example
* ```ts
* class App extends StepLoop {}
* let app: App = new App();
* app.start()
*
* app.pause()
* ```
* @instance
*/
public pause(): void {
if (!this._initialized || !this._running || this._kill) return;
this._running = false;
this._paused = true;
this._cancel_next_step();
this.on_pause()
}
/**
* Resume execution of the {@link StepLoop} after calling {@link StepLoop.pause()} to pause it. Will resume execution on the next step in the {@link StepLoop} lifespan. Use {@link StepLoop.pause()} to pause execution and stop the loop.
*
* @returns {void} `void`
* @example
* ```ts
* class App extends StepLoop {}
* let app: App = new App();
* app.start()
*
* app.pause()
* app.play()
* ```
* @instance
*/
public play(): void {
if (!this._initialized || this._running || this._kill) return;
this._running = true;
this._paused = false;
this._startTime = performance.now() - (this._step_num * this._interval);
this.on_play();
this._run(performance.now());
}
/**
* Begin execution of the {@link StepLoop} lifecycle. Calls {@link StepLoop.initial()} to execute the initialization stage, then proceeds to the looping stage. The termination stage will not execute until {@link StepLoop.finish()} is called.
*
* If {@link StepLoop.start()} is called after the termination stage has ended, the loop will restart at the beginning of the initialization stage.
*
* @returns {void} `void`
* @example
* ```ts
* class App extends StepLoop {}
* let app: App = new App();
*
* app.start()
* ```
* @instance
*/
public start(): void{
this._running = true;
this._startTime = performance.now();
this._main();
}
/**
* Ends the {@link StepLoop}. Executes the termination stage of the {@link StepLoop} lifecycle. Calls {@link StepLoop.final()} and then kills the loop.
*
* @returns {void} `void`
* @example
* ```ts
* class App extends StepLoop {}
* let app: App = new App();
* app.start()
*
* app.finish()
* ```
* @instance
*/
public finish(): void {
if (!this._initialized || this._kill) return;
this._running = false;
this._paused = false;
this._kill = true;
this._cancel_next_step();
this._term()
}
private _RAFAvailable: boolean = typeof requestAnimationFrame !== 'undefined';
private _RAFActive: boolean;
private _RAFId: number | undefined;
private _request_next_step(timestamp: DOMHighResTimeStamp | number): void {
if (!this._running) return;
if (this._RAFActive && this._RAFAvailable) {
this._RAFId = requestAnimationFrame((nextTimestamp) => {
this._run(nextTimestamp);
});
return;
}
const now = performance.now();
const nextStepTime = this._startTime + (this._step_num * this._interval);
const delay = Math.max(0, nextStepTime - now);
this._timeoutId = setTimeout(() => {
this._run(performance.now());
}, delay);
}
private _cancel_next_step(): void {
if (this._timeoutId && !this._RAFActive) {
clearTimeout(this._timeoutId);
this._timeoutId = undefined;
} else if (this._RAFId && this._RAFActive) {
cancelAnimationFrame(this._RAFId);
this._RAFId = undefined;
}
}
private _check_for_end_trigger(): boolean {
if (this._kill || (this._lifespan && (this._step_num >= this._lifespan))){
return true;
} else {
return false;
}
}
private _init(): void {
this._kill = false
this._initialized = true;
this._step_num = 0;
try {
this.initial();
} catch (error) {
console.error('Error in initial():', error);
}
}
private _run(timestamp: number): void {
if (this._running) {
if (this._check_for_end_trigger()) {
this._term();
return;
}
this.background().catch(error => {
console.error('Error in background():', error);
});
try {
this.before();
} catch (error) {
console.error('Error in before():', error);
}
try {
this.step();
} catch (error) {
console.error('Error in step():', error);
}
try {
this.after();
} catch (error) {
console.error('Error in after():', error);
}
this._step_num++;
this._lastStepDuration = timestamp - this._lastStepTime;
this._lastStepTime = timestamp;
this._request_next_step(timestamp)
}
}
private _term(): void {
this._running = false
this._paused = false;
this._cancel_next_step();
this._kill = true;
try {
this.final();
} catch (error) {
console.error('Error in final():', error);
}
}
private _main(): void{
this._init();
this._run(performance.now());
}
}