animations
import { ChangeDetectionStrategy, Component, ElementRef, inject, signal } from '@angular/core';import { SobaWrapper } from '@soba/wrapper.ts';import { TweakpaneList, TweakpanePane } from 'angular-three-tweakpane';import { NgtCanvas, provideNgtRenderer } from 'angular-three/dom';import { SceneGraph } from './scene-graph';
@Component({ selector: 'app-soba-animations', template: ` <ngt-canvas [camera]="{ position: [0, 2, 4] }"> <app-soba-wrapper *canvasContent> <app-scene-graph [animation]="animation()" /> </app-soba-wrapper> </ngt-canvas>
<tweakpane-pane title="Animations" [container]="host"> <tweakpane-list [(value)]="animation" label="animation" [options]="['Dance', 'Idle', 'Strut']" /> </tweakpane-pane> `, imports: [NgtCanvas, SobaWrapper, SceneGraph, TweakpaneList, TweakpanePane], changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'animations-demo relative block h-full' },})export default class Animations { static clientProviders = [provideNgtRenderer()];
protected host = inject(ElementRef); protected animation = signal<'Dance' | 'Idle' | 'Strut'>('Strut');}import { ChangeDetectionStrategy, Component, computed, CUSTOM_ELEMENTS_SCHEMA, effect, type ElementRef, input, viewChild,} from '@angular/core';import { gltfResource } from 'angular-three-soba/loaders';import { animations, type NgtsAnimationClips } from 'angular-three-soba/misc';import { matcapTextureResource } from 'angular-three-soba/staging';import * as THREE from 'three';import type { GLTF } from 'three-stdlib';
import botGLB from '@common-assets/ybot.glb' with { loader: 'file' };import { NgtArgs } from 'angular-three';
type BotGLTF = GLTF & { animations: NgtsAnimationClips<'Dance' | 'Idle' | 'Strut'>[]; nodes: { 'Y-Bot': THREE.Object3D; YB_Body: THREE.SkinnedMesh; YB_Joints: THREE.SkinnedMesh; mixamorigHips: THREE.Bone; }; materials: { YB_Body: THREE.MeshStandardMaterial; YB_Joints: THREE.MeshStandardMaterial };};
@Component({ selector: 'app-scene-graph', template: ` @if (gltf.value(); as gltf) { <ngt-group #group [dispose]="null"> <ngt-group [rotation]="[Math.PI / 2, 0, 0]" [scale]="0.01"> <ngt-primitive #bone *args="[gltf.nodes.mixamorigHips]" /> <ngt-skinned-mesh [geometry]="gltf.nodes.YB_Body.geometry" [skeleton]="gltf.nodes.YB_Body.skeleton"> <ngt-mesh-matcap-material [matcap]="matcapBody.resource.value()" /> </ngt-skinned-mesh> <ngt-skinned-mesh [geometry]="gltf.nodes.YB_Joints.geometry" [skeleton]="gltf.nodes.YB_Joints.skeleton" > <ngt-mesh-matcap-material [matcap]="matcapJoints.resource.value()" /> </ngt-skinned-mesh> </ngt-group> </ngt-group> } `, changeDetection: ChangeDetectionStrategy.OnPush, schemas: [CUSTOM_ELEMENTS_SCHEMA], imports: [NgtArgs],})export class SceneGraph { animation = input<'Dance' | 'Idle' | 'Strut'>('Strut');
private boneRef = viewChild<ElementRef<THREE.Bone>>('bone'); private groupRef = viewChild.required<ElementRef<THREE.Group>>('group');
protected gltf = gltfResource<BotGLTF>(() => botGLB); protected matcapBody = matcapTextureResource(() => '293534_B2BFC5_738289_8A9AA7', { onLoad: (texture) => { texture.colorSpace = THREE.SRGBColorSpace; }, }); protected matcapJoints = matcapTextureResource(() => '3A2412_A78B5F_705434_836C47', { onLoad: (texture) => { texture.colorSpace = THREE.SRGBColorSpace; }, }); protected readonly Math = Math;
private animationHost = computed(() => (this.boneRef() ? this.groupRef() : null)); private animations = animations(this.gltf.value, this.animationHost);
constructor() { effect((onCleanup) => { if (!this.animations.isReady) return;
const action = this.animations.actions[this.animation()]; action.reset().fadeIn(0.5).play(); onCleanup(() => action.fadeOut(0.5)); }); }}animations is an abstraction around THREE.AnimationMixer that provides type-safe animations handling.
Usage
import { animations } from 'angular-three-soba/misc';export class MyCmp { protected gltf = gltfResource(() => 'my/gltf.glb'); protected animations = animations(this.gltf.value, this.gltf.scene);
constructor() { effect(() => { if (!this.animations.isReady) return; this.animations.actions; // }) }}isReady is a signal-getter (getter that returns a signal) that indicates whether the animations are ready to use. This also acts as a type-guard to ensure the animations are typed correctly. You should always check isReady before accessing actions.
Providing generics
With GLTF type
Usually, you will want to provide the GLTF type to gltfResource then animations will be able to infer the animations type from this.gltf.value
import { NgtsAnimationClips } from 'angular-three-soba/misc';
interface MyGLTF extends GLTF { animations: NgtsAnimationClips<'Dance'>[];}
export class MyCmp { protected gltf = gltfResource<MyGLTF>(() => 'my/gltf.glb'); protected animations = animations(this.gltf.value, this.gltf.scene); // this.animations.actions.Dance is strongly-typed}With animations type
If you don’t want to or don’t have the GLTF type, you can provide the animations type directly to animations
import { NgtsAnimation } from 'angular-three-soba/misc';
export class MyCmp { protected animations = animations<NgtsAnimation<'Dance'>>(...); // this.animations.actions.Dance is strongly-typed}Automatic cleanup
The animations are automatically cleaned up when the component is destroyed:
- Cached actions are cleared
- All actions are stopped
- Actions are uncached from the mixer
Arguments
| name | type | description | required |
|---|---|---|---|
| animations | () => NgtsAnimation<TAnimation> | undefined | null | A signal/function that returns either an array of AnimationClips or an object with animations property containing an array of AnimationClips | yes |
| object | ElementRef<Object3D> | Object3D | (() => ElementRef<Object3D> | Object3D | undefined | null) | The Object3D instance that will be animated | yes |
| options | { injector?: Injector } | Optional configuration object with injector | no |
Returns
| type | description |
|---|---|
| NgtsAnimationApi | The animations API object |
Properties
| name | type | description |
|---|---|---|
| clips | T[] | Array of animation clips |
| mixer | THREE.AnimationMixer | The AnimationMixer instance |
| names | T['name'][] | Array of animation names |
| isReady | boolean | Whether the animations are initialized and ready to use |
| actions | { [key in T['name']]: THREE.AnimationAction } | Map of animation names to their corresponding AnimationAction (only available when isReady is true) |