Skip to content

Commit

Permalink
initial impl
Browse files Browse the repository at this point in the history
  • Loading branch information
eneajaho committed Nov 30, 2023
1 parent 776f593 commit 41d4412
Show file tree
Hide file tree
Showing 14 changed files with 306 additions and 0 deletions.
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
18 changes: 18 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,18 @@
import { Component } from '@angular/core';
import { injectFps } from 'ngxtension/inject-fps';

@Component({
standalone: true,
host: {
style: 'display: block; margin: 12px',
},
template: `
<div>FPS: {{ fps() }}</div>
<button>btn1</button>
<button>btn2</button>
<button>btn3</button>
`,
})
export default class InjectFpsCmp {
fps = injectFps();
}
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

0 comments on commit 41d4412

Please sign in to comment.