Control 3D model animations with advanced playback options including crossfading, blending, and complex animation orchestration.
Basic animation playback and state management.
/**
* Automatically start playing animation when model loads
*/
autoplay: boolean;
/**
* Name of the animation to play (from availableAnimations)
* Set to undefined to stop current animation
*/
animationName: string | undefined;
/**
* Get list of all animations available in the loaded model
*/
readonly availableAnimations: Array<string>;
/**
* Whether animation playback is currently paused
*/
readonly paused: boolean;
/**
* Current playback time position in seconds
*/
currentTime: number;
/**
* Playback speed multiplier (1.0 = normal speed, 2.0 = double speed)
*/
timeScale: number;
/**
* Total duration of current animation in seconds
*/
readonly duration: number;
/**
* List of currently appended/layered animations
*/
readonly appendedAnimations: string[];Usage:
<!-- Auto-play named animation -->
<model-viewer
src="models/robot.glb"
animation-name="walking"
autoplay>
</model-viewer>// Check available animations
console.log('Available animations:', modelViewer.availableAnimations);
// Control playback
modelViewer.animationName = "running";
modelViewer.timeScale = 1.5; // 1.5x speed
modelViewer.currentTime = 2.0; // Start at 2 secondsDirect control over animation playback.
/**
* Start playing current animation with optional parameters
* @param options - Playback configuration options
*/
play(options?: PlayAnimationOptions): void;
/**
* Pause current animation playback
*/
pause(): void;
interface PlayAnimationOptions {
/** Number of times to repeat animation (Infinity for loop) */
repetitions?: number;
/** Whether to play animation back and forth (ping-pong) */
pingpong?: boolean;
}Usage:
// Play once
modelViewer.play({ repetitions: 1 });
// Loop forever with ping-pong
modelViewer.play({
repetitions: Infinity,
pingpong: true
});
// Pause playback
modelViewer.pause();Smooth transitions between different animations.
/**
* Duration in milliseconds for crossfading between animations
* When switching animations, they will blend smoothly over this period
*/
animationCrossfadeDuration: number;Usage:
<!-- Smooth 500ms transitions between animations -->
<model-viewer
src="models/character.glb"
animation-crossfade-duration="500">
</model-viewer>// Switch animations with crossfade
modelViewer.animationCrossfadeDuration = 300;
modelViewer.animationName = "idle";
// Later switch to running with smooth transition
setTimeout(() => {
modelViewer.animationName = "running";
}, 2000);Layer multiple animations with different weights and timing.
/**
* Append an animation to the current animation with blending options
* @param animationName - Name of animation to append
* @param options - Blending and timing options
*/
appendAnimation(animationName: string, options?: AppendAnimationOptions): void;
/**
* Remove an appended animation with optional fade out
* @param animationName - Name of animation to detach
* @param options - Detachment options
*/
detachAnimation(animationName: string, options?: DetachAnimationOptions): void;
interface AppendAnimationOptions {
/** Play animation back and forth */
pingpong?: boolean;
/** Number of repetitions (null for infinite) */
repetitions?: number | null;
/** Blend weight (0-1, higher = more influence) */
weight?: number;
/** Playback speed multiplier for this animation */
timeScale?: number;
/** Fade in duration (boolean for default, number for custom ms) */
fade?: boolean | number;
/** Time warping for synchronization (boolean or custom ms) */
warp?: boolean | number;
/** Whether warp timing is relative to other animations */
relativeWarp?: boolean;
/** Start time offset in seconds */
time?: number | null;
}
interface DetachAnimationOptions {
/** Fade out duration (boolean for default, number for custom ms) */
fade?: boolean | number;
}Usage:
// Start base idle animation
modelViewer.animationName = "idle";
modelViewer.play();
// Layer breathing animation at 50% weight
modelViewer.appendAnimation("breathing", {
weight: 0.5,
repetitions: null, // infinite
fade: true
});
// Add blinking with custom timing
modelViewer.appendAnimation("blinking", {
weight: 0.3,
timeScale: 2.0, // Blink twice as fast
fade: 200 // 200ms fade in
});
// Remove breathing animation later
setTimeout(() => {
modelViewer.detachAnimation("breathing", { fade: 500 });
}, 10000);Model Viewer dispatches events during animation playback for synchronization and state management.
Fired when an animation completes a loop iteration.
interface AnimationLoopEvent extends Event {
type: 'loop';
detail: {
animationName: string;
loopCount: number;
};
}Fired when an animation completes entirely (not fired for infinite loops).
interface AnimationFinishEvent extends Event {
type: 'finished';
detail: {
animationName: string;
totalLoops: number;
};
}Usage:
// Track animation loops
modelViewer.addEventListener('loop', (event) => {
console.log(`${event.detail.animationName} completed loop ${event.detail.loopCount}`);
});
// Handle animation completion
modelViewer.addEventListener('finished', (event) => {
console.log(`Animation ${event.detail.animationName} finished after ${event.detail.totalLoops} loops`);
// Start next animation in sequence
if (event.detail.animationName === "intro") {
modelViewer.animationName = "idle";
modelViewer.play();
}
});Create complex animation sequences with timing control:
class AnimationSequence {
constructor(modelViewer) {
this.modelViewer = modelViewer;
this.sequence = [];
this.currentIndex = 0;
}
add(animationName, duration, options = {}) {
this.sequence.push({ animationName, duration, options });
return this;
}
async play() {
for (const step of this.sequence) {
this.modelViewer.animationName = step.animationName;
this.modelViewer.play(step.options);
await new Promise(resolve => {
setTimeout(resolve, step.duration);
});
}
}
}
// Usage
const sequence = new AnimationSequence(modelViewer)
.add("intro", 3000, { repetitions: 1 })
.add("idle", 5000, { repetitions: 2 })
.add("wave", 2000, { repetitions: 1 });
sequence.play();Trigger animations based on user interaction:
// Animation state machine
const animationStates = {
idle: {
animation: "idle",
options: { repetitions: Infinity }
},
excited: {
animation: "jump",
options: { repetitions: 3, pingpong: false }
},
thinking: {
animation: "scratch_head",
options: { repetitions: 1 }
}
};
let currentState = "idle";
function setState(newState) {
if (animationStates[newState]) {
currentState = newState;
const state = animationStates[newState];
modelViewer.animationName = state.animation;
modelViewer.play(state.options);
}
}
// Button controls
document.getElementById('excite-btn').addEventListener('click', () => {
setState('excited');
});
// Return to idle after non-looping animations
modelViewer.addEventListener('finished', (event) => {
if (currentState !== 'idle') {
setState('idle');
}
});Synchronize animations across multiple model viewers:
class AnimationSync {
constructor(modelViewers) {
this.viewers = modelViewers;
this.syncTime = 0;
this.isPlaying = false;
}
play(animationName, options = {}) {
this.syncTime = performance.now();
this.isPlaying = true;
this.viewers.forEach(viewer => {
viewer.animationName = animationName;
viewer.currentTime = 0;
viewer.play(options);
});
// Keep models synchronized
this.syncLoop();
}
syncLoop() {
if (!this.isPlaying) return;
const elapsed = (performance.now() - this.syncTime) / 1000;
this.viewers.forEach(viewer => {
if (Math.abs(viewer.currentTime - elapsed) > 0.1) {
viewer.currentTime = elapsed;
}
});
requestAnimationFrame(() => this.syncLoop());
}
pause() {
this.isPlaying = false;
this.viewers.forEach(viewer => viewer.pause());
}
}
// Usage with multiple models
const models = document.querySelectorAll('model-viewer');
const sync = new AnimationSync(Array.from(models));
sync.play("dance", { repetitions: Infinity });