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) |