Custom Inject Function, or CIF, is my favorite way to compose functionalities in Angular nowadays
thanks to the inject and Signal. Check out one
of my previous blog posts on Abstract inject() the right way to learn more.
In this blog post, I’ll introduce some of my favorites CIFs that I implemented in angular-three,
an Angular Renderer for THREE.js
1. injectBeforeRender()
THREE.js is an abstraction over WebGL API and interacts
seamlessly with the Canvas API. To interact with Angular Three,
Angular developers utilize the ngt-canvas component. Each ngt-canvas component starts a separate Animation Loop
(i,e: requestAnimationFrame()).
When working on 3D projects, it is commonly, if not always, involving running animations on 3D objects.
1function animate() {2 // schedule the animate function in a animation loop3 requestAnimationFrame(animate);4
5 // change the properties of the 3D object bit by bit on every frame (before render step)6 cube.rotation.x += 0.01;7
8 // then render the scene9 renderer.render(scene, camera);10}11
12// starts the render13animate();In Angular Three, the naive way of opting into the Animation Loop is as follow:
1export class Model {2 #store = injectNgtStore();3 #destroyRef = inject(DestroyRef);4
5 constructor() {6 const unsubscribe = this.#store.get("internal").subscribe(() => {7 /* before render logic */8 });9 this.#destroyRef.onDestroy(unsubscribe);10 }11}This is quite verbose to have to inject two symbols to opt into the Animation Loop. Hence, angular-three exposes a CIF called
injectBeforeRender which composes injectNgtStore and inject(DestroyRef).
1export class Model {2 #store = injectNgtStore();3 #destroyRef = inject(DestroyRef);4
5 constructor() {6 const unsubscribe = this.#store.get("internal").subscribe(() => {7 /* before render logic */8 });9 this.#destroyRef.onDestroy(unsubscribe);10 injectBeforeRender(() => {11 /* before render logic */12 })13 }14}Code: https://github.com/angular-threejs/angular-three/blob/platform/libs/core/src/lib/before-render.ts
2. injectNgtsGLTFLoader()
In addition to running animations in 3D projects, we frequently load external assets like 3D models, textures etc… THREE.js deals with
external assets via a set of Loader. In this section, we’ll take a look at
GLTFLoader to load a .glb model
Let’s see how we can do it without injectNgtsGLTFLoader
1export class Car {2 #gltfLoader = new GLTFLoader();3 // Optional: Provide a DRACOLoader instance to decode compressed mesh data4 #dracoLoader = new DRACOLoader();5
6 #model = signal<GLTF | null>(null);7 scene = computed(() => this.#model()?.scene);8
9 constructor() {10 this.#dracoLoader.setDecoderPath("/examples/jsm/libs/draco/");11 this.#gltfLoader.setDRACOLoader(this.#dracoLoader);12
13 this.#gltfLoader.load("assets/car.glb", (gltf) => {14 this.#model.set(gltf);15 });16 }17}It is not bad but there are couple of things that are lacking:
- What if
assets/car.glbwas dynamic (viaInput)? gltfcontains more information than just thescene. Likeanimationsthat the GLTF model has. How can we run those animations?
Not to mention, we need to do the same thing for different models components in our 3D projects. Well, injectNgtsGLTFLoader solves
all (maybe) the problems. Let’s take a look
1export class Car {2 #gltfLoader = new GLTFLoader();3 // Optional: Provide a DRACOLoader instance to decode compressed mesh data4 #dracoLoader = new DRACOLoader();5
6 #model = signal<GLTF | null>(null);7 #model = injectNgtsGLTFLoader(() => 'assets/car.glb');8 scene = computed(() => this.#model()?.scene);9
10 constructor() {11 this.#dracoLoader.setDecoderPath("/examples/jsm/libs/draco/");12 this.#gltfLoader.setDRACOLoader(this.#dracoLoader);13
14 this.#gltfLoader.load("assets/car.glb", (gltf) => {15 this.#model.set(gltf);16 });17 }18}injectNgtsGLTFLoader does several things underneath:
-
Create a
Signalto store the 3D model data -
Set up an
effectto re-fetch if the input changes- Support Array input, Directory input, or single input
-
Allow consumers to specify whether they want to use
DRACOLoaderand/orMeshoptDecoder -
Support for preloading external assets OUTSIDE of Angular building blocks
1injectNgtsGLTFLoader.preload(() => "assets/car.glb");23@Component({})4export class Car {5#model = injectNgtsGLTFLoader(() => "assets/car.glb");6scene = computed(() => this.#model()?.scene);7}
In addition, the base injectNgtLoader() (which injectNgtsGLTFLoader utilizes) handles in-memory cache so the consumers
don’t have to load the same asset twice.
3. injectBody()
injectBody() is a CIF that deals with connecting the 3D objects to the Physics World.
injectBody Demo
Sorry for the low quality GIF, I had to tune it way down to display it in my blog.
As we can see from the GIF (I hope you could see it 😛), there are quite a few things that are happenning when we deal with Physics
- The physics engine itself (i.e:
cannonjs) - Attaching the 3D objects (the cubes and planes) to the Body in the Cannon World
- Reacting to inputs like
debugandgravityetc. - Expose APIs to update the properties of the Body in the Cannon World (via Web Worker)
Without injectBody(), the amount of code we need to tie 3D objects to the Cannon World would be tremendous and repetitive.
Code: https://github.com/angular-threejs/angular-three/blob/platform/libs/cannon/services/src/lib/body.ts
Conclusion
Above is my 3 favorite CIFs in angular-three. There are various other CIFs in angular-three but the concept they share is very similar:
- Compose other CIFs
- Make use of APIs that rely on the Injection Context (i.e:
inject,effect) - Are friendly with Signals. They accept Signals and return Signals
Do you have a favorite CIF or do you have something that could be turned into a CIF? I would love to know! Thanks for reading.