Web component for easily displaying interactive 3D models with AR support across browsers and devices
—
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Pending
The risk profile of this skill
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 });