πŸš€ Socket Launch Week Day 5:Introducing Repository Access Permissions and Custom Roles.Learn more β†’
Sign In

@armniko/ticker

Package Overview
Dependencies
Maintainers
1
Versions
7
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@armniko/ticker - npm Package Compare versions

Comparing version
2.2.0
to
2.3.0
+4
-0
dist/fps/fps.d.ts

@@ -8,3 +8,7 @@ import { Time } from '../types';

private _msSinceLastCalculation;
private _lowFpsStreakThreshold;
private _recoveryStreakThreshold;
private _lowFpsInRow;
private _highFpsInRow;
private _isLowFps;
private _lowFpsCallback?;

@@ -11,0 +15,0 @@ currentFps(): number;

+99
-54

@@ -8,3 +8,7 @@ //#region src/fps/fps.ts

_msSinceLastCalculation = 0;
_lowFpsStreakThreshold = 5;
_recoveryStreakThreshold = 5;
_lowFpsInRow = 0;
_highFpsInRow = 0;
_isLowFps = false;
_lowFpsCallback;

@@ -16,3 +20,3 @@ currentFps() {

this.collectFps(time);
this._msSinceLastCalculation += time.deltaMs;
this._msSinceLastCalculation += time.unscaledDeltaMs;
if (this._msSinceLastCalculation >= this._calculateFpsEachMs) {

@@ -32,3 +36,3 @@ this._msSinceLastCalculation = 0;

collectFps(time) {
this._lastChecks.push(Math.round(1e3 / time.deltaMs));
this._lastChecks.push(Math.round(1e3 / time.unscaledDeltaMs));
}

@@ -41,8 +45,18 @@ calculateAverageFps() {

checkAndNotifyLowFps() {
if (this._minFps && this._currentFps < this._minFps) {
if (this._minFps) if (this._currentFps < this._minFps) {
this._highFpsInRow = 0;
this._lowFpsInRow++;
if (this._lowFpsInRow === 5) {
this._lowFpsInRow = 0;
if (this._lowFpsCallback) this._lowFpsCallback();
if (this._lowFpsInRow >= this._lowFpsStreakThreshold && !this._isLowFps) {
this._isLowFps = true;
this._lowFpsCallback?.();
}
} else {
this._lowFpsInRow = 0;
if (this._isLowFps) {
this._highFpsInRow++;
if (this._highFpsInRow >= this._recoveryStreakThreshold) {
this._highFpsInRow = 0;
this._isLowFps = false;
}
}
}

@@ -55,19 +69,45 @@ }

_fps = new Fps();
_drawQueue = [];
_logicQueue = [];
_lowFpsQueue = [];
_drawQueue = /* @__PURE__ */ new Map();
_logicQueue = /* @__PURE__ */ new Map();
_lowFpsQueue = /* @__PURE__ */ new Map();
_maxFps;
_expectedFps;
_timeScale = 1;
_time = {
delta: 0,
deltaMs: 0
deltaMs: 0,
unscaledDelta: 0,
unscaledDeltaMs: 0
};
_lastTimestamp = 0;
_logicTicksAccumulatedMs = 0;
_maxAccumulatedMs = 250;
_isStarted = false;
_requestFrameId;
_nextTaskId = 0;
_loop = (currentTimestamp) => {
if (this._isStarted) {
const maxDeltaMs = 1e3 / this._maxFps;
let unscaledDeltaMs = currentTimestamp - this._lastTimestamp;
if (unscaledDeltaMs >= maxDeltaMs) {
if (unscaledDeltaMs > this._maxAccumulatedMs) unscaledDeltaMs = this._maxAccumulatedMs;
const scaledDeltaMs = unscaledDeltaMs * this._timeScale;
this._time = {
delta: scaledDeltaMs / 1e3,
deltaMs: scaledDeltaMs,
unscaledDelta: unscaledDeltaMs / 1e3,
unscaledDeltaMs
};
this._lastTimestamp = currentTimestamp;
this._fps.calculate(this._time);
this.runQueue();
}
this._requestFrameId = window.requestAnimationFrame(this._loop);
}
};
constructor(options) {
this.setFps({
min: options?.minFps || 0,
max: options?.maxFps || 60,
expected: options?.expectedFps || 60
min: options?.minFps ?? 0,
max: options?.maxFps ?? 60,
expected: options?.expectedFps ?? 60
});

@@ -82,7 +122,11 @@ if (options?.onDrawTick) this.addDrawTask(options.onDrawTick);

start() {
this._lastTimestamp = performance.now();
this.loop(this._lastTimestamp);
if (!this._isStarted) {
this._isStarted = true;
this._lastTimestamp = performance.now();
this._loop(this._lastTimestamp);
}
}
stop() {
if (this._requestFrameId) {
this._isStarted = false;
if (this._requestFrameId !== void 0) {
window.cancelAnimationFrame(this._requestFrameId);

@@ -93,3 +137,3 @@ this._requestFrameId = void 0;

isStarted() {
return !!this._requestFrameId;
return this._isStarted;
}

@@ -102,5 +146,11 @@ setFps(options) {

}
setTimeScale(scale) {
this._timeScale = Math.max(0, scale);
return this;
}
addDrawTask(callback) {
const id = this._nextTaskId++;
this._drawQueue.set(id, { callback });
return {
index: this._drawQueue.push({ callback }) - 1,
ref: id,
type: "draw"

@@ -110,4 +160,6 @@ };

addLogicTask(callback) {
const id = this._nextTaskId++;
this._logicQueue.set(id, { callback });
return {
index: this._logicQueue.push({ callback }) - 1,
ref: id,
type: "logic"

@@ -117,4 +169,6 @@ };

addLowFpsTask(callback) {
const id = this._nextTaskId++;
this._lowFpsQueue.set(id, { callback });
return {
index: this._lowFpsQueue.push({ callback }) - 1,
ref: id,
type: "low-fps"

@@ -126,9 +180,9 @@ };

case "draw":
this._drawQueue.splice(taskId.index, 1);
this._drawQueue.delete(taskId.ref);
break;
case "logic":
this._logicQueue.splice(taskId.index, 1);
this._logicQueue.delete(taskId.ref);
break;
case "low-fps":
this._lowFpsQueue.splice(taskId.index, 1);
this._lowFpsQueue.delete(taskId.ref);
break;

@@ -140,2 +194,5 @@ }

}
timeScale() {
return this._timeScale;
}
/**

@@ -145,3 +202,3 @@ * @deprecated Use `time.deltaMs` provided in draw/logic task callbacks instead.

msBetweenTicks() {
return this._time.deltaMs;
return this._time.unscaledDeltaMs;
}

@@ -152,21 +209,7 @@ /**

ticksMissed() {
return this._time.deltaMs / (1e3 / this._expectedFps);
return this._time.unscaledDeltaMs / (1e3 / this._expectedFps);
}
loop(currentTimestamp) {
const maxDeltaMs = 1e3 / this._maxFps;
const deltaMs = currentTimestamp - this._lastTimestamp;
if (deltaMs >= maxDeltaMs) {
this._time = {
delta: deltaMs / 1e3,
deltaMs
};
this._lastTimestamp = currentTimestamp;
this._fps.calculate(this._time);
this.runQueue();
}
this._requestFrameId = window.requestAnimationFrame(this.loop.bind(this));
}
runQueue() {
if (this._logicQueue.length > 0) this.runLogicQueue();
if (this._drawQueue.length > 0) this.runDrawQueue();
if (this._logicQueue.size > 0) this.runLogicQueue();
if (this._drawQueue.size > 0) this.runDrawQueue();
}

@@ -178,3 +221,5 @@ runLogicQueue() {

delta: expectedDeltaMs / 1e3,
deltaMs: expectedDeltaMs
deltaMs: expectedDeltaMs,
unscaledDelta: expectedDeltaMs / 1e3,
unscaledDeltaMs: expectedDeltaMs
};

@@ -196,11 +241,17 @@ while (this._logicTicksAccumulatedMs >= expectedDeltaMs) {

var TickerMock = class extends Ticker {
_isStarted = false;
_isStartedMock = false;
_defaultTimeMock = {
delta: 1 / this.fps(),
deltaMs: 1e3 / this.fps(),
unscaledDelta: 1 / this.fps(),
unscaledDeltaMs: 1e3 / this.fps()
};
start() {
this._isStarted = true;
this._isStartedMock = true;
}
stop() {
this._isStarted = false;
this._isStartedMock = false;
}
isStarted() {
return this._isStarted;
return this._isStartedMock;
}

@@ -217,12 +268,6 @@ fps() {

simulateLogicTick(time = void 0) {
this._logicQueue.forEach((item) => item.callback(time || {
delta: 1 / this.fps(),
deltaMs: 1e3 / this.fps()
}));
this._logicQueue.forEach((item) => item.callback(time || this._defaultTimeMock));
}
simulateDrawTick(time = void 0) {
this._drawQueue.forEach((item) => item.callback(time || {
delta: 1 / this.fps(),
deltaMs: 1e3 / this.fps()
}));
this._drawQueue.forEach((item) => item.callback(time || this._defaultTimeMock));
}

@@ -229,0 +274,0 @@ simulateLowFps() {

@@ -12,3 +12,7 @@ (function(global, factory) {

_msSinceLastCalculation = 0;
_lowFpsStreakThreshold = 5;
_recoveryStreakThreshold = 5;
_lowFpsInRow = 0;
_highFpsInRow = 0;
_isLowFps = false;
_lowFpsCallback;

@@ -20,3 +24,3 @@ currentFps() {

this.collectFps(time);
this._msSinceLastCalculation += time.deltaMs;
this._msSinceLastCalculation += time.unscaledDeltaMs;
if (this._msSinceLastCalculation >= this._calculateFpsEachMs) {

@@ -36,3 +40,3 @@ this._msSinceLastCalculation = 0;

collectFps(time) {
this._lastChecks.push(Math.round(1e3 / time.deltaMs));
this._lastChecks.push(Math.round(1e3 / time.unscaledDeltaMs));
}

@@ -45,8 +49,18 @@ calculateAverageFps() {

checkAndNotifyLowFps() {
if (this._minFps && this._currentFps < this._minFps) {
if (this._minFps) if (this._currentFps < this._minFps) {
this._highFpsInRow = 0;
this._lowFpsInRow++;
if (this._lowFpsInRow === 5) {
this._lowFpsInRow = 0;
if (this._lowFpsCallback) this._lowFpsCallback();
if (this._lowFpsInRow >= this._lowFpsStreakThreshold && !this._isLowFps) {
this._isLowFps = true;
this._lowFpsCallback?.();
}
} else {
this._lowFpsInRow = 0;
if (this._isLowFps) {
this._highFpsInRow++;
if (this._highFpsInRow >= this._recoveryStreakThreshold) {
this._highFpsInRow = 0;
this._isLowFps = false;
}
}
}

@@ -59,19 +73,45 @@ }

_fps = new Fps();
_drawQueue = [];
_logicQueue = [];
_lowFpsQueue = [];
_drawQueue = /* @__PURE__ */ new Map();
_logicQueue = /* @__PURE__ */ new Map();
_lowFpsQueue = /* @__PURE__ */ new Map();
_maxFps;
_expectedFps;
_timeScale = 1;
_time = {
delta: 0,
deltaMs: 0
deltaMs: 0,
unscaledDelta: 0,
unscaledDeltaMs: 0
};
_lastTimestamp = 0;
_logicTicksAccumulatedMs = 0;
_maxAccumulatedMs = 250;
_isStarted = false;
_requestFrameId;
_nextTaskId = 0;
_loop = (currentTimestamp) => {
if (this._isStarted) {
const maxDeltaMs = 1e3 / this._maxFps;
let unscaledDeltaMs = currentTimestamp - this._lastTimestamp;
if (unscaledDeltaMs >= maxDeltaMs) {
if (unscaledDeltaMs > this._maxAccumulatedMs) unscaledDeltaMs = this._maxAccumulatedMs;
const scaledDeltaMs = unscaledDeltaMs * this._timeScale;
this._time = {
delta: scaledDeltaMs / 1e3,
deltaMs: scaledDeltaMs,
unscaledDelta: unscaledDeltaMs / 1e3,
unscaledDeltaMs
};
this._lastTimestamp = currentTimestamp;
this._fps.calculate(this._time);
this.runQueue();
}
this._requestFrameId = window.requestAnimationFrame(this._loop);
}
};
constructor(options) {
this.setFps({
min: options?.minFps || 0,
max: options?.maxFps || 60,
expected: options?.expectedFps || 60
min: options?.minFps ?? 0,
max: options?.maxFps ?? 60,
expected: options?.expectedFps ?? 60
});

@@ -86,7 +126,11 @@ if (options?.onDrawTick) this.addDrawTask(options.onDrawTick);

start() {
this._lastTimestamp = performance.now();
this.loop(this._lastTimestamp);
if (!this._isStarted) {
this._isStarted = true;
this._lastTimestamp = performance.now();
this._loop(this._lastTimestamp);
}
}
stop() {
if (this._requestFrameId) {
this._isStarted = false;
if (this._requestFrameId !== void 0) {
window.cancelAnimationFrame(this._requestFrameId);

@@ -97,3 +141,3 @@ this._requestFrameId = void 0;

isStarted() {
return !!this._requestFrameId;
return this._isStarted;
}

@@ -106,5 +150,11 @@ setFps(options) {

}
setTimeScale(scale) {
this._timeScale = Math.max(0, scale);
return this;
}
addDrawTask(callback) {
const id = this._nextTaskId++;
this._drawQueue.set(id, { callback });
return {
index: this._drawQueue.push({ callback }) - 1,
ref: id,
type: "draw"

@@ -114,4 +164,6 @@ };

addLogicTask(callback) {
const id = this._nextTaskId++;
this._logicQueue.set(id, { callback });
return {
index: this._logicQueue.push({ callback }) - 1,
ref: id,
type: "logic"

@@ -121,4 +173,6 @@ };

addLowFpsTask(callback) {
const id = this._nextTaskId++;
this._lowFpsQueue.set(id, { callback });
return {
index: this._lowFpsQueue.push({ callback }) - 1,
ref: id,
type: "low-fps"

@@ -130,9 +184,9 @@ };

case "draw":
this._drawQueue.splice(taskId.index, 1);
this._drawQueue.delete(taskId.ref);
break;
case "logic":
this._logicQueue.splice(taskId.index, 1);
this._logicQueue.delete(taskId.ref);
break;
case "low-fps":
this._lowFpsQueue.splice(taskId.index, 1);
this._lowFpsQueue.delete(taskId.ref);
break;

@@ -144,2 +198,5 @@ }

}
timeScale() {
return this._timeScale;
}
/**

@@ -149,3 +206,3 @@ * @deprecated Use `time.deltaMs` provided in draw/logic task callbacks instead.

msBetweenTicks() {
return this._time.deltaMs;
return this._time.unscaledDeltaMs;
}

@@ -156,21 +213,7 @@ /**

ticksMissed() {
return this._time.deltaMs / (1e3 / this._expectedFps);
return this._time.unscaledDeltaMs / (1e3 / this._expectedFps);
}
loop(currentTimestamp) {
const maxDeltaMs = 1e3 / this._maxFps;
const deltaMs = currentTimestamp - this._lastTimestamp;
if (deltaMs >= maxDeltaMs) {
this._time = {
delta: deltaMs / 1e3,
deltaMs
};
this._lastTimestamp = currentTimestamp;
this._fps.calculate(this._time);
this.runQueue();
}
this._requestFrameId = window.requestAnimationFrame(this.loop.bind(this));
}
runQueue() {
if (this._logicQueue.length > 0) this.runLogicQueue();
if (this._drawQueue.length > 0) this.runDrawQueue();
if (this._logicQueue.size > 0) this.runLogicQueue();
if (this._drawQueue.size > 0) this.runDrawQueue();
}

@@ -182,3 +225,5 @@ runLogicQueue() {

delta: expectedDeltaMs / 1e3,
deltaMs: expectedDeltaMs
deltaMs: expectedDeltaMs,
unscaledDelta: expectedDeltaMs / 1e3,
unscaledDeltaMs: expectedDeltaMs
};

@@ -200,11 +245,17 @@ while (this._logicTicksAccumulatedMs >= expectedDeltaMs) {

var TickerMock = class extends Ticker {
_isStarted = false;
_isStartedMock = false;
_defaultTimeMock = {
delta: 1 / this.fps(),
deltaMs: 1e3 / this.fps(),
unscaledDelta: 1 / this.fps(),
unscaledDeltaMs: 1e3 / this.fps()
};
start() {
this._isStarted = true;
this._isStartedMock = true;
}
stop() {
this._isStarted = false;
this._isStartedMock = false;
}
isStarted() {
return this._isStarted;
return this._isStartedMock;
}

@@ -221,12 +272,6 @@ fps() {

simulateLogicTick(time = void 0) {
this._logicQueue.forEach((item) => item.callback(time || {
delta: 1 / this.fps(),
deltaMs: 1e3 / this.fps()
}));
this._logicQueue.forEach((item) => item.callback(time || this._defaultTimeMock));
}
simulateDrawTick(time = void 0) {
this._drawQueue.forEach((item) => item.callback(time || {
delta: 1 / this.fps(),
deltaMs: 1e3 / this.fps()
}));
this._drawQueue.forEach((item) => item.callback(time || this._defaultTimeMock));
}

@@ -233,0 +278,0 @@ simulateLowFps() {

import { Time } from '../types';
import { Ticker } from './ticker';
export declare class TickerMock extends Ticker {
private _isStarted;
private _isStartedMock;
private _defaultTimeMock;
start(): void;

@@ -6,0 +7,0 @@ stop(): void;

import { LowFpsQueueItem, LogicQueueItem, DrawQueueItem, TickerTaskId, TickerOptions } from '../types';
export declare class Ticker {
private readonly _fps;
private readonly _drawQueue;
private readonly _logicQueue;
private readonly _lowFpsQueue;
private _drawQueue;
private _logicQueue;
private _lowFpsQueue;
private _maxFps;
private _expectedFps;
private _timeScale;
private _time;
private _lastTimestamp;
private _logicTicksAccumulatedMs;
private _maxAccumulatedMs;
private _isStarted;
private _requestFrameId?;
private _nextTaskId;
private _loop;
constructor(options?: TickerOptions);

@@ -22,2 +27,3 @@ start(): void;

}): this;
setTimeScale(scale: number): this;
addDrawTask(callback: DrawQueueItem['callback']): TickerTaskId;

@@ -28,2 +34,3 @@ addLogicTask(callback: LogicQueueItem['callback']): TickerTaskId;

fps(): number;
timeScale(): number;
/**

@@ -37,3 +44,2 @@ * @deprecated Use `time.deltaMs` provided in draw/logic task callbacks instead.

ticksMissed(): number;
private loop;
private runQueue;

@@ -40,0 +46,0 @@ private runLogicQueue;

@@ -37,3 +37,3 @@ export interface DrawQueueItem {

export interface TickerTaskId {
index: number;
ref: number;
type: 'draw' | 'logic' | 'low-fps';

@@ -44,2 +44,4 @@ }

deltaMs: number;
unscaledDelta: number;
unscaledDeltaMs: number;
}
{
"name": "@armniko/ticker",
"version": "2.2.0",
"description": "A lightweight, zero-dependency JavaScript/TypeScript library for running an application loop with separate logic and draw ticks, FPS limiting, and low-FPS detection.",
"version": "2.3.0",
"description": "A lightweight, zero-dependency JavaScript/TypeScript library for running an application loop with separate logic and draw ticks, time scaling, FPS limiting, and low-FPS detection.",
"author": "ArmΔ«ns Nikolajevs <armins.nikolajevs@gmail.com>",

@@ -15,2 +15,3 @@ "license": "MIT",

"requestanimationframe",
"rAF",
"fps",

@@ -20,2 +21,3 @@ "fps-limit",

"delta-time",
"time-scale",
"tick",

@@ -22,0 +24,0 @@ "render-loop",

+27
-15

@@ -9,3 +9,3 @@ # Ticker

A lightweight, zero-dependency JavaScript/TypeScript library for running an application loop with **separate logic and
draw ticks**, **FPS limiting**, and **low-FPS detection**.
draw ticks**, **time scaling**, **FPS limiting** and **low-FPS detection**.
<hr>

@@ -17,2 +17,3 @@

- 🎚️ **FPS control** β€” cap your draw rate to save CPU/battery
- ⏱️ **Time scaling** β€” slow down or speed up time without touching your game logic
- πŸ“‰ **Low-FPS callbacks** β€” react when performance degrades (e.g. lower visual quality)

@@ -50,14 +51,16 @@ - πŸ§ͺ **Test-friendly** β€” ships with `TickerMock` for deterministic unit tests

- start() - starts ticker.
- stop() - stops ticker.
- isStarted() - checks if ticker is started.
- start() – starts ticker.
- stop() – stops ticker.
- isStarted() – checks if ticker is started.
- setFps(options: { min?: number; max?: number; expected?: number }) - set min, max or expected FPS
- min (default: 0) - defines value at which lowFps task callbacks will be called.
- max (default: 60) - defines drawing FPS limit.
- min (default: 0) – defines value at which lowFps task callbacks will be called.
- max (default: 60) – defines drawing FPS limit.
- expected (default: 60) - defines expected logical and drawing FPS at which app should work in normal conditions.
- addLogicTask(callback) - register callback for update app logic. Returns TickerTaskId.
- addDrawTask(callback) - register callback for update app screen. Returns TickerTaskId.
- addLowFpsTask(callback) - register callback that will be called when reached min FPS. Returns TickerTaskId.
- remove(taskId: TickerTaskId) - removes provided task.
- fps() - current FPS at which app operate.
- setTimeScale(scale: number) – set time scale.
- addLogicTask(callback) – register callback for update app logic. Returns TickerTaskId.
- addDrawTask(callback) – register callback for update app screen. Returns TickerTaskId.
- addLowFpsTask(callback) – register callback that will be called when reached min FPS. Returns TickerTaskId.
- remove(taskId: TickerTaskId) – removes the provided task.
- fps() – current FPS at which app operates.
- timeScale() – current time scale at which app operates.

@@ -71,5 +74,5 @@ ### Migration

```typescript
import {Ticker} from '@armniko/ticker';
import { Ticker } from '@armniko/ticker';
const element: { position: { x: number, y: number } } = {position: {x: 0, y: 0}};
const element: { position: { x: number, y: number } } = { position: { x: 0, y: 0 } };
const animation: { durationMs: number, distancePx: number } = {

@@ -94,5 +97,5 @@ durationMs: 2000,

```typescript
import {Ticker, Time} from '@armniko/ticker';
import { Ticker, Time } from '@armniko/ticker';
const element: { position: { x: number, y: number } } = {position: {x: 0, y: 0}};
const element: { position: { x: number, y: number } } = { position: { x: 0, y: 0 } };
const animation: { durationMs: number, distancePx: number } = {

@@ -117,2 +120,11 @@ durationMs: 2000,

<tr>
<td>v2.3.0</td>
<td>
Added time scale feature.<br>
Minor performance improvements.<br>
Fixed low-fps callback to be called gain after fps recovery.<br>
Fixed edge case bug with a removing task where an incorrect task could be removed.<br>
</td>
</tr>
<tr>
<td>v2.2.0</td>

@@ -119,0 +131,0 @@ <td>