Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

wip: initial impl injectFps #183

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions apps/test-app/src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ import { RouterLink, RouterOutlet } from '@angular/router';
<li>
<a routerLink="/active-element">Active Element</a>
</li>

<li>
<a routerLink="/fps">Fps</a>
</li>
</ul>

<hr />
Expand Down
4 changes: 4 additions & 0 deletions apps/test-app/src/app/app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ export const appConfig: ApplicationConfig = {
path: 'drag',
loadComponent: () => import('./drag/drag.component'),
},
{
path: 'fps',
loadComponent: () => import('./inject-fps/inject-fps.component'),
},
{
path: 'active-element',
loadComponent: () =>
Expand Down
23 changes: 23 additions & 0 deletions apps/test-app/src/app/inject-fps/inject-fps.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Component, ElementRef, ViewChild, effect } from '@angular/core';
import { injectFps } from 'ngxtension/inject-fps';

@Component({
standalone: true,
host: {
style: 'display: block; margin: 12px',
},
template: `
<div #fpsDiv></div>
`,
})
export default class InjectFpsCmp {
fps = injectFps();

@ViewChild('fpsDiv', { static: true }) fpsDiv!: ElementRef<HTMLDivElement>;

constructor() {
effect(() => {
this.fpsDiv.nativeElement.innerHTML = `FPS: ${this.fps()}`;
});
}
}
3 changes: 3 additions & 0 deletions libs/ngxtension/inject-fps/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# ngxtension/inject-fps

Secondary entry point of `ngxtension`. It can be used by importing from `ngxtension/inject-fps`.
5 changes: 5 additions & 0 deletions libs/ngxtension/inject-fps/ng-package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"lib": {
"entryFile": "src/index.ts"
}
}
33 changes: 33 additions & 0 deletions libs/ngxtension/inject-fps/project.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"name": "ngxtension/inject-fps",
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
"projectType": "library",
"sourceRoot": "libs/ngxtension/inject-fps/src",
"targets": {
"test": {
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
"jestConfig": "libs/ngxtension/jest.config.ts",
"testPathPattern": ["inject-fps"],
"passWithNoTests": true
},
"configurations": {
"ci": {
"ci": true,
"codeCoverage": true
}
}
},
"lint": {
"executor": "@nx/eslint:lint",
"outputs": ["{options.outputFile}"],
"options": {
"lintFilePatterns": [
"libs/ngxtension/inject-fps/**/*.ts",
"libs/ngxtension/inject-fps/**/*.html"
]
}
}
}
}
1 change: 1 addition & 0 deletions libs/ngxtension/inject-fps/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './inject-fps';
51 changes: 51 additions & 0 deletions libs/ngxtension/inject-fps/src/inject-fps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import {
runInInjectionContext,
signal,
type Injector,
type Signal,
} from '@angular/core';
import { assertInjector } from 'ngxtension/assert-injector';
import { injectRafFn } from 'ngxtension/inject-raf-fn';

export interface InjectFpsOptions {
/**
* Calculate the FPS on every x frames.
* @default 10
*/
every?: number;

/**
* The injector to use.
*/
injector?: Injector;
}

export function injectFps(options?: InjectFpsOptions): Signal<number> {
const injector = assertInjector(injectFps, options?.injector);

return runInInjectionContext(injector, () => {
const fps = signal(0);
if (typeof performance === 'undefined') return fps.asReadonly();

const every = options?.every ?? 10;

let last = performance.now();
let ticks = 0;

injectRafFn(
() => {
ticks += 1;
if (ticks >= every) {
const now = performance.now();
const diff = now - last;
fps.set(Math.round(1000 / (diff / ticks)));
last = now;
ticks = 0;
}
},
{ injector }
);

return fps;
});
}
3 changes: 3 additions & 0 deletions libs/ngxtension/inject-raf-fn/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# ngxtension/inject-raf-fn

Secondary entry point of `ngxtension`. It can be used by importing from `ngxtension/inject-raf-fn`.
5 changes: 5 additions & 0 deletions libs/ngxtension/inject-raf-fn/ng-package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"lib": {
"entryFile": "src/index.ts"
}
}
33 changes: 33 additions & 0 deletions libs/ngxtension/inject-raf-fn/project.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"name": "ngxtension/inject-raf-fn",
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
"projectType": "library",
"sourceRoot": "libs/ngxtension/inject-raf-fn/src",
"targets": {
"test": {
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
"jestConfig": "libs/ngxtension/jest.config.ts",
"testPathPattern": ["inject-raf-fn"],
"passWithNoTests": true
},
"configurations": {
"ci": {
"ci": true,
"codeCoverage": true
}
}
},
"lint": {
"executor": "@nx/eslint:lint",
"outputs": ["{options.outputFile}"],
"options": {
"lintFilePatterns": [
"libs/ngxtension/inject-raf-fn/**/*.ts",
"libs/ngxtension/inject-raf-fn/**/*.html"
]
}
}
}
}
1 change: 1 addition & 0 deletions libs/ngxtension/inject-raf-fn/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './inject-raf-fn';
141 changes: 141 additions & 0 deletions libs/ngxtension/inject-raf-fn/src/inject-raf-fn.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import { isPlatformServer } from '@angular/common';
import {
NgZone,
PLATFORM_ID,
inject,
runInInjectionContext,
signal,
type Injector,
type Signal,
} from '@angular/core';
import { assertInjector } from 'ngxtension/assert-injector';

export interface InjectRafFnCallbackArguments {
/**
* Time elapsed between this and the last frame.
*/
delta: number;

/**
* Time elapsed since the creation of the web page. See {@link https://developer.mozilla.org/en-US/docs/Web/API/DOMHighResTimeStamp#the_time_origin Time origin}.
*/
timestamp: DOMHighResTimeStamp;
}

export interface UseRafFnOptions {
/**
* Start the requestAnimationFrame loop immediately on creation
*
* @default true
*/
immediate?: boolean;
/**
* The maximum frame per second to execute the function.
* Set to `undefined` to disable the limit.
*
* @default undefined
*/
fpsLimit?: number;

/**
* The injector to use.
*/
injector?: Injector;
}

export interface RafFn {
/**
* A signal to indicate whether an instance is active
*/
isActive: Signal<boolean>;

/**
* Temporary pause the effect from executing
*/
pause: () => void;

/**
* Resume the effects
*/
resume: () => void;
}

/**
* Call function on every `requestAnimationFrame`. With controls of pausing and resuming.
*
* @param fn
* @param options
*/
export function injectRafFn(
fn: (args: InjectRafFnCallbackArguments) => void,
options: UseRafFnOptions = {}
): RafFn {
const injector = assertInjector(injectRafFn, options?.injector);

return runInInjectionContext(injector, () => {
const ngZone = inject(NgZone);
const isServer = isPlatformServer(inject(PLATFORM_ID));

if (isServer) {
return {
isActive: signal(false).asReadonly(),
pause: () => {},
resume: () => {},
};
}

const { immediate = true, fpsLimit = undefined } = options;

const isActive = signal(false);

const intervalLimit = fpsLimit ? 1000 / fpsLimit : null;
let previousFrameTimestamp = 0;
let rafId: null | number = null;

const loop = (timestamp: DOMHighResTimeStamp) => {
if (!isActive() || !window) return;

const delta = timestamp - (previousFrameTimestamp || timestamp);

if (intervalLimit && delta < intervalLimit) {
rafId = ngZone.runOutsideAngular(() =>
window.requestAnimationFrame(loop)
);
return;
}

fn({ delta, timestamp });

previousFrameTimestamp = timestamp;

rafId = ngZone.runOutsideAngular(() =>
window.requestAnimationFrame(loop)
);
};

const resume = () => {
if (!isActive() && window) {
isActive.set(true);
rafId = ngZone.runOutsideAngular(() =>
window.requestAnimationFrame(loop)
);
}
};

const pause = () => {
isActive.set(false);
if (rafId && typeof rafId === 'number' && window) {
ngZone.runOutsideAngular(() => window.cancelAnimationFrame(rafId!));
rafId = null;
}
};

if (immediate) resume();

return {
isActive: isActive.asReadonly(),
pause,
resume,
};
});
}
4 changes: 4 additions & 0 deletions tsconfig.base.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
"ngxtension/inject-destroy": [
"libs/ngxtension/inject-destroy/src/index.ts"
],
"ngxtension/inject-fps": ["libs/ngxtension/inject-fps/src/index.ts"],
"ngxtension/inject-is-intersecting": [
"libs/ngxtension/inject-is-intersecting/src/index.ts"
],
Expand All @@ -62,6 +63,9 @@
"ngxtension/inject-query-params": [
"libs/ngxtension/inject-query-params/src/index.ts"
],
"ngxtension/inject-raf-fn": [
"libs/ngxtension/inject-raf-fn/src/index.ts"
],
"ngxtension/intl": ["libs/ngxtension/intl/src/index.ts"],
"ngxtension/map-array": ["libs/ngxtension/map-array/src/index.ts"],
"ngxtension/map-skip-undefined": [
Expand Down