Declarative THREE.js in Angular

Chau Tran

Sr Engineer @nrwl

Core team member @nestframework

Brand new father

Creator of @automapper/core

Agenda

  • What is THREE.js? Quick demo using Angular
  • How do we write it declaratively? Quick demo
  • How does it work?
  • Expand on demo (if time permits)
  • Q/A

Disclaimer

This is not a "how to". This presentation is to show an introduction to and what the library is leveraging (from Angular) to make everything work. So I hope everybody learns something from today's presentation.

This is how vanilla THREE.js code looks like

const scene = new THREE.Scene()
const camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000)

const renderer = new THREE.WebGLRenderer()
renderer.setSize(width, height)
document.querySelector('#canvas-container').appendChild(renderer.domElement)

const mesh = new THREE.Mesh()
mesh.geometry = new THREE.BoxGeometry()
mesh.material = new THREE.MeshPhongMaterial()

scene.add(mesh)

function animate() {
  requestAnimationFrame(animate)
  renderer.render(scene, camera)
}

animate()

This is how vanilla THREE.js code looks like

const scene = new THREE.Scene()
const camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000)

const renderer = new THREE.WebGLRenderer()
renderer.setSize(width, height)
document.querySelector('#canvas-container').appendChild(renderer.domElement)

const geometry = new THREE.BoxGeometry()
const material = new THREE.MeshPhongMaterial()
const mesh = new THREE.Mesh(geometry, material)

scene.add(mesh)

function animate() {
  requestAnimationFrame(animate)
  renderer.render(scene, camera)
}

animate()

This is how vanilla THREE.js code looks like

J/K it was alright

How do we make it better for Angular developers?

How do we make it better for Angular developers?

with Angular 3

How do we make it better for Angular developers?

with Angular 3

How do we make it better for Angular developers?

with Angular 3 THREE (.js)

Here's Angular Three

@Component({
  selector: 'app-root',
  template: `
    <ngt-canvas>
      <ngt-mesh 
         o3d 
         (animateReady)="onAnimateReady($event.animateObject)"
      >
        <ngt-box-geometry></ngt-box-geometry>
        <ngt-mesh-basic-material></ngt-mesh-basic-material>
      </ngt-mesh>
    </ngt-canvas>
  `
})
export class AppComponent {
  onAnimateReady(cube: THREE.Mesh) {
    cube.rotation.x += 0.01;
    cube.rotation.y += 0.01;
  }
}

and of course...the imports

Here's Angular Three

import { ThreeCoreModule } from '@angular-three/core';
import { ThreeMeshModule } from '@angular-three/core/meshes';
import { ThreeBoxBufferGeometryModule } from '@angular-three/core/geometries';
import { ThreeMeshBasicMaterialModule } from '@angular-three/core/materials';

@NgModule({
  imports: [
    /* ... */,
    ThreeCoreModule,
    ThreeMeshModule,
    ThreeBoxBufferGeometryModule,
    ThreeMeshBasicMaterialModule
  ]
})
export class AppModule {}

@Component({
  selector: 'app-root',
  template: `
    <ngt-canvas>
      <ngt-mesh>
        <ngt-box-geometry></ngt-box-geometry>
        <ngt-mesh-basic-material></ngt-mesh-basic-material>
      </ngt-mesh>
    </ngt-canvas>
  `
})
export class AppComponent {}

Here's Angular Three

<ngt-canvas>
  <ngt-mesh>
   <ngt-box-geometry></ngt-box-geometry>
   <ngt-mesh-basic-material></ngt-mesh-basic-material>
  </ngt-mesh>
</ngt-canvas>

Here's Angular Three

<ngt-canvas>
  <ngt-mesh>
   <ngt-box-geometry></ngt-box-geometry>
   <ngt-mesh-basic-material></ngt-mesh-basic-material>
  </ngt-mesh>
</ngt-canvas>

so how does it actually work?

Here's Angular Three

<ngt-canvas>
  <ngt-mesh>
   <ngt-box-geometry></ngt-box-geometry>
   <ngt-mesh-basic-material></ngt-mesh-basic-material>
  </ngt-mesh>
</ngt-canvas>

so how does it actually work?

  • Content Projection
  • Hierarchical Dependency Injection

Here's Angular Three

<ngt-canvas>
  <ngt-mesh>
   <ngt-box-geometry></ngt-box-geometry>
   <ngt-mesh-basic-material></ngt-mesh-basic-material>
  </ngt-mesh>
</ngt-canvas>

so how does it actually work?

  • Content Projection Querying
  • Hierarchical Dependency Injection

Content Projection/Querying

<parent>
  <child></child>
</parent>

Content Projection/Querying

<parent>
  <child></child>
</parent>
<parent>
  <!-- aka the content -->
  <child></child>
</parent>

Content Projection/Querying

<parent>
  <child></child>
</parent>
<parent>
  <!-- aka the content -->
  <child></child>
</parent>
@Component({
  selector: 'parent',
  template: `
    <ng-content></ng-content>
  `
})
export class ParentComponent {
  @ContentChild(ChildComponent) childComp: ChildComponent;
}

Content Projection/Querying

<parent>
  <child></child>
</parent>
<parent>
  <!-- aka the content -->
  <child></child>
</parent>
@Component({
  selector: 'parent',
  template: `
    <ng-content></ng-content>
  `
})
export class ParentComponent {
  @ContentChild(ChildComponent) childComp: ChildComponent;
}

Content Projection/Querying

<parent>
  <child></child>
</parent>
<parent>
  <!-- aka the content -->
  <child></child>
</parent>
@Component({
  selector: 'parent',
  template: `
    <!-- nothing to see here -->
  `
})
export class ParentComponent {
  @ContentChild(ChildComponent) childComp: ChildComponent;
}

Content Projection/Querying

<ngt-mesh>
  <ngt-box-geometry></ngt-box-geometry>
  <ngt-mesh-basic-material></ngt-mesh-basic-material>
</ngt-mesh>

Content Projection/Querying

<ngt-mesh>
  <ngt-box-geometry></ngt-box-geometry>
  <ngt-mesh-basic-material></ngt-mesh-basic-material>
</ngt-mesh>

Content Projection/Querying

<ngt-mesh>
  <ngt-box-geometry></ngt-box-geometry>
  <ngt-mesh-basic-material></ngt-mesh-basic-material>
</ngt-mesh>
@Directive({
  selector: 'ngt-mesh'
})
export class MeshDirective {
  @ContentChild(ThreeGeometry) geometryDirective?: ThreeGeometry;
  @ContentChild(ThreeMaterial) materialDirective?: ThreeMaterial;
}

Content Projection/Querying

<ngt-mesh>
  <ngt-box-geometry></ngt-box-geometry>
  <ngt-mesh-basic-material></ngt-mesh-basic-material>
</ngt-mesh>
@Directive({
  selector: 'ngt-mesh'
})
export class MeshDirective {
  @ContentChild(ThreeGeometry) geometryDirective?: ThreeGeometry;
  @ContentChild(ThreeMaterial) materialDirective?: ThreeMaterial;
}

Content Projection/Querying

<ngt-mesh>
  <ngt-box-geometry></ngt-box-geometry>
  <ngt-mesh-basic-material></ngt-mesh-basic-material>
</ngt-mesh>
@Directive({
  selector: 'ngt-mesh'
})
export class MeshDirective {
  @ContentChild(ThreeGeometry) geometryDirective?: ThreeGeometry;
  @ContentChild(ThreeMaterial) materialDirective?: ThreeMaterial;
  
  private mesh?: THREE.Mesh;
  
  ngOnInit() {
    this.mesh = new THREE.Mesh(
      this.geometryDirective.geometry, 
      this.materialDirective.material
    );
  }
}

Content Projection/Querying

<ngt-mesh>
  <ngt-box-geometry></ngt-box-geometry>
  <ngt-mesh-basic-material></ngt-mesh-basic-material>
</ngt-mesh>
@Directive({
  selector: 'ngt-mesh'
})
export class MeshDirective {
  @ContentChild(ThreeGeometry) geometryDirective?: ThreeGeometry;
  @ContentChild(ThreeMaterial) materialDirective?: ThreeMaterial;
  
  private mesh?: THREE.Mesh;
  
  ngOnInit() {
    this.mesh = new THREE.Mesh(
      this.geometryDirective.geometry, 
      this.materialDirective.material
    );
  }
}

Content Projection/Querying

<ngt-mesh>
  <ngt-box-geometry></ngt-box-geometry>
  <ngt-mesh-basic-material></ngt-mesh-basic-material>
</ngt-mesh>
@Directive({
  selector: 'ngt-mesh'
})
export class MeshDirective {
  @ContentChild(ThreeGeometry) geometryDirective?: ThreeGeometry;
  @ContentChild(ThreeMaterial) materialDirective?: ThreeMaterial;
  
  private mesh?: THREE.Mesh;
  
  ngOnInit() {
    this.mesh = new THREE.Mesh(
      this.geometryDirective.geometry, 
      this.materialDirective.material
    );
  }
}

Hierarchical Dependency Injection

@Component({
  selector: 'ngt-canvas',
  template: `<canvas #renderCanvas></canvas>`,
  providers: [
    CanvasStore,
    AnimationStore,
    EventsStore,
    LoopService,
    /* some other injectables */
  ]
})
export class CanvasComponent {}

Hierarchical Dependency Injection

@Component({
  selector: 'ngt-canvas',
  template: `<canvas #renderCanvas></canvas>`,
  providers: [
    CanvasStore,
    AnimationStore,
    EventsStore,
    LoopService,
    /* some other injectables */
  ]
})
export class CanvasComponent {}

Hierarchical Dependency Injection

// Remember?
const scene = new THREE.Scene()
const camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000)

const renderer = new THREE.WebGLRenderer()
renderer.setSize(width, height)
document.querySelector('#canvas-container').appendChild(renderer.domElement)

// setup Mesh

scene.add(mesh)

Hierarchical Dependency Injection

// Remember?
const scene = new THREE.Scene()
const camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000)

const renderer = new THREE.WebGLRenderer()
renderer.setSize(width, height)
document.querySelector('#canvas-container').appendChild(renderer.domElement)

// setup Mesh

scene.add(mesh)

CanvasComponent sets up:

  • Renderer
  • Camera
  • Scene

Hierarchical Dependency Injection

scene.add(mesh)

Hierarchical Dependency Injection

scene.add(mesh)
<ngt-canvas>
  <ngt-mesh>
    <!-- stuffs -->
  </ngt-mesh>
</ngt-canvas>

Hierarchical Dependency Injection

scene.add(mesh)
<ngt-canvas>
  <ngt-mesh>
    <!-- stuffs -->
  </ngt-mesh>
</ngt-canvas>
@Component({
  /* ... */
})
export class CanvasComponent {
  @ContentChild(MeshDirective) meshDirective: MeshDirective;
}

Hierarchical Dependency Injection

scene.add(mesh)
<ngt-canvas>
  <ngt-mesh>
    <!-- stuffs -->
  </ngt-mesh>
</ngt-canvas>
@Component({
  /* ... */
})
export class CanvasComponent {
  @ContentChild(MeshDirective) meshDirective: MeshDirective;
}

Hierarchical Dependency Injection

scene.add(mesh)
<ngt-canvas>
  <ngt-mesh>
    <!-- stuffs -->
  </ngt-mesh>
</ngt-canvas>
@Component({
  /* ... */,
  providers: [
    CanvasStore, // <-- this store initializes a THREE.Scene
    /* .. */
  ]
})
export class CanvasComponent {
  @ContentChild(MeshDirective) meshDirective: MeshDirective;
}

Hierarchical Dependency Injection

scene.add(mesh)
<ngt-canvas>
  <ngt-mesh>
    <!-- stuffs -->
  </ngt-mesh>
</ngt-canvas>
@Component({
  /* ... */,
  providers: [
    CanvasStore, // <-- this store initializes a THREE.Scene
    /* .. */
  ]
})
export class CanvasComponent {
  @ContentChild(MeshDirective) meshDirective: MeshDirective;
  
  /**
   * this.canvasStore.scene.add(this.meshDirective.mesh);
   * /
}

Hierarchical Dependency Injection

scene.add(mesh)
<ngt-canvas>
  <ngt-mesh>
    <!-- stuffs -->
  </ngt-mesh>
</ngt-canvas>
@Component({
  /* ... */,
  providers: [
    CanvasStore, // <-- this store initializes a THREE.Scene
    /* .. */
  ]
})
export class CanvasComponent {
  @ContentChild(MeshDirective) meshDirective: MeshDirective;
  
  /**
   * this.canvasStore.scene.add(this.meshDirective.mesh);
   * /
}

Hierarchical Dependency Injection

scene.add(mesh)
scene.add(literally_any_THREE_object)
<ngt-canvas>
  <ngt-mesh>
    <!-- stuffs -->
  </ngt-mesh>
  <ngt-anything></ngt-anything>
  <ngt-anything-2></ngt-anything-2>
</ngt-canvas>
@Component({
  /* ... */,
  providers: [
    CanvasStore,
    /* .. */
  ]
})
export class CanvasComponent {
  // WHAT ELSE DO WE NEED TO QUERY FOR???? NOBODY KNOWS
  @ContentChild(MeshDirective) meshDirective: MeshDirective;
}

Hierarchical Dependency Injection

<parent>
  <child></child>
</parent>

Hierarchical Dependency Injection

<parent>
  <child></child>
</parent>
@Component({
  selector: 'child',
  template: ``
})
export class ChildComponent {

}

Hierarchical Dependency Injection

<parent>
  <child></child>
</parent>
@Component({
  selector: 'child',
  template: ``
})
export class ChildComponent {
  constructor(private readonly parentComponent: ParentComponent) {}
}

Hierarchical Dependency Injection

<parent>
  <child></child>
</parent>
@Component({
  selector: 'child',
  template: ``
})
export class ChildComponent {
  constructor(private readonly parentComponent: ParentComponent) {}
}
// Remember:
@Component({
  providers: [/* ... */]
})
export class ParentComponent

Hierarchical Dependency Injection

<parent>
  <child></child>
</parent>
@Component({
  selector: 'child',
  template: ``
})
export class ChildComponent {
  constructor(
     private readonly parentComponent: ParentComponent, 
     /* any provider on the parent injector is available here */
  ) {}
}

Hierarchical Dependency Injection

<parent>
  <child></child>
</parent>
@Component({
  selector: 'child',
  template: ``
})
export class ChildComponent {
  constructor(
     private readonly parentComponent: ParentComponent, 
     /* any provider on the parent injector is available here */
  ) {}
}

Hierarchical Dependency Injection

<ngt-canvas>
  <ngt-mesh>
    <!-- stuffs -->
  </ngt-mesh>
</ngt-canvas>

Hierarchical Dependency Injection

<ngt-canvas>
  <ngt-mesh>
    <!-- stuffs -->
  </ngt-mesh>
</ngt-canvas>
@Directive({
  selector: 'ngt-mesh'
})
export class MeshDirective {
  
  ngOnInit() {
    this.mesh = new THREE.Mesh(/* ... */);
  }
}

Hierarchical Dependency Injection

<ngt-canvas>
  <ngt-mesh>
    <!-- stuffs -->
  </ngt-mesh>
</ngt-canvas>
@Directive({
  selector: 'ngt-mesh'
})
export class MeshDirective {
  constructor(private readonly canvasStore: CanvasStore) {}
  
  ngOnInit() {
    this.mesh = new THREE.Mesh(/* ... */);
    this.canvasStore.scene.add(this.mesh);
  }
}

Hierarchical Dependency Injection

<ngt-canvas>
  <ngt-mesh>
    <!-- stuffs -->
  </ngt-mesh>
</ngt-canvas>
@Directive({
  selector: 'ngt-mesh'
})
export class MeshDirective {
  constructor(private readonly canvasStore: CanvasStore) {}
  
  ngOnInit() {
    this.mesh = new THREE.Mesh(/* ... */);
    this.canvasStore.scene.add(this.mesh);
  }
}

Nx Workspace Generators

Nx Workspace Generators

@ContentChild(ThreeGeometry) geometryDirective?: ThreeGeometry;
@ContentChild(ThreeMaterial) materialDirective?: ThreeMaterial;

Nx Workspace Generators

@ContentChild(ThreeGeometry) geometryDirective?: ThreeGeometry;
@ContentChild(ThreeMaterial) materialDirective?: ThreeMaterial;

Nx Workspace Generators

@ContentChild(ThreeGeometry) geometryDirective?: ThreeGeometry;
@ContentChild(ThreeMaterial) materialDirective?: ThreeMaterial;

Nx Workspace Generators

@ContentChild(ThreeGeometry) geometryDirective?: ThreeGeometry;
@ContentChild(ThreeMaterial) materialDirective?: ThreeMaterial;

Nx Workspace Generators

@Directive({
  selector: 'ngt-box-geometry',
  exportAs: 'ngtBoxGeometry',
  providers: [{provide: ThreeGeometry, useExisting: BoxGeometryDirective}]
})
export class BoxGeometryDirective extends ThreeGeometry<THREE.BoxGeometry> {
  @Input() set args(v: ConstructorParameters<typeof THREE.BoxGeometry>) {
    /* ... */                                           
  }
                     
  ngOnInit() {
    this.init();
  }
  
  protected init() {
    this.geometry = new THREE.BoxGeometry(/* ... */);
  }
}

Nx Workspace Generators

Nx Workspace Generators

Generators Demo

Summary

  • Content Querying and Hierarchical Dependency Injection come out of the box

Summary

  • Content Querying and Hierarchical Dependency Injection come out of the box
  • Regardless of whether you use ng-content or not, Angular already structures your Parent-Content relationship.

Summary

  • Content Querying and Hierarchical Dependency Injection come out of the box
  • Regardless of whether you use ng-content or not, Angular already structures your Parent-Content relationship.
  • A content child can inherit the Injector of its parent thanks to Hierarchical Dependency Injection.
  • A content child can inherit the Injector of its parent thanks to Hierarchical Dependency Injection.

Summary

  • Content Querying and Hierarchical Dependency Injection come out of the box
  • Regardless of whether you use ng-content or not, Angular already structures your Parent-Content relationship.
  • A content child can inherit the Injector of its parent thanks to Hierarchical Dependency Injection.
  • A content child can inherit the Injector of its parent thanks to Hierarchical Dependency Injection.
  • (Nx only) Use Workspace Generators for boring repetitive tasks

DEMO

Q/A

Angular Three

By Chau Tran

Angular Three

  • 762