Skip to content

Commit

Permalink
Add some options for play method about Animation (google#3021)
Browse files Browse the repository at this point in the history
* add play once method

* add test

* add doc

* add test

* fix

* fix playOnce test

* add options for play method

* fix test

* fix default repetitions

* update docs

* fix default options

* change logic

* del unnecessary method call

* fix

* fix

* update test

* del

* revert

* fix

* update logic

* add event (draft)

* add e.detail to loop

* add docs

* add test

* fix

* use Punch anime

* wait for test finished

* passed test

* fix
  • Loading branch information
futahei authored Jan 4, 2022
1 parent a391324 commit 12179aa
Show file tree
Hide file tree
Showing 5 changed files with 111 additions and 19 deletions.
46 changes: 35 additions & 11 deletions packages/model-viewer/src/features/animation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
*/

import {property} from 'lit-element';
import {LoopOnce, LoopPingPong, LoopRepeat} from 'three';

import ModelViewerElementBase, {$hasTransitioned, $needsRender, $onModelLoad, $renderer, $scene, $tick, $updateSource} from '../model-viewer-base.js';
import {Constructor} from '../utilities.js';
Expand All @@ -23,6 +24,10 @@ const MILLISECONDS_PER_SECOND = 1000.0
const $changeAnimation = Symbol('changeAnimation');
const $paused = Symbol('paused');

interface PlayAnimationOptions {
repetitions: number, pingpong: boolean,
}

export declare interface AnimationInterface {
autoplay: boolean;
animationName: string|void;
Expand All @@ -32,20 +37,37 @@ export declare interface AnimationInterface {
readonly duration: number;
currentTime: number;
pause(): void;
play(): void;
play(options?: PlayAnimationOptions): void;
}

export const AnimationMixin = <T extends Constructor<ModelViewerElementBase>>(
ModelViewerElement: T): Constructor<AnimationInterface>&T => {
class AnimationModelViewerElement extends ModelViewerElement {
@property({type: Boolean}) autoplay: boolean = false;
@property({type: Boolean})
autoplay: boolean = false;
@property({type: String, attribute: 'animation-name'})
animationName: string|undefined = undefined;
@property({type: Number, attribute: 'animation-crossfade-duration'})
animationCrossfadeDuration: number = 300;

protected[$paused]: boolean = true;

constructor(...args: any[]) {
super(args)

this[$scene].subscribeMixerEvent('loop', (e) => {
const count = e.action._loopCount;
this.dispatchEvent(new CustomEvent('loop', {detail: { count }}));
})
this[$scene].subscribeMixerEvent('finished', () => {
this.currentTime = 0;
this[$paused] = true;
this[$renderer].threeRenderer.shadowMap.autoUpdate = false;
this[$changeAnimation]({repetitions: Infinity, pingpong: false});
this.dispatchEvent(new CustomEvent('finished'));
})
}

/**
* Returns an array
*/
Expand Down Expand Up @@ -85,14 +107,12 @@ export const AnimationMixin = <T extends Constructor<ModelViewerElementBase>>(
this.dispatchEvent(new CustomEvent('pause'));
}

play() {
if (this[$paused] && this.availableAnimations.length > 0) {
play(options: PlayAnimationOptions = {repetitions: Infinity, pingpong: false}) {
if (this.availableAnimations.length > 0) {
this[$paused] = false;
this[$renderer].threeRenderer.shadowMap.autoUpdate = true;

if (!this[$scene].hasActiveAnimation) {
this[$changeAnimation]();
}
this[$changeAnimation](options);

this.dispatchEvent(new CustomEvent('play'));
}
Expand All @@ -104,7 +124,7 @@ export const AnimationMixin = <T extends Constructor<ModelViewerElementBase>>(
this[$paused] = true;

if (this.autoplay) {
this[$changeAnimation]();
this[$changeAnimation]({repetitions: Infinity, pingpong: false});
this.play();
}
}
Expand All @@ -130,7 +150,7 @@ export const AnimationMixin = <T extends Constructor<ModelViewerElementBase>>(
}

if (changedProperties.has('animationName')) {
this[$changeAnimation]();
this[$changeAnimation]({repetitions: Infinity, pingpong: false});
}
}

Expand All @@ -144,10 +164,14 @@ export const AnimationMixin = <T extends Constructor<ModelViewerElementBase>>(
return super[$updateSource]();
}

[$changeAnimation]() {
[$changeAnimation](options: PlayAnimationOptions) {
const repetitions = options.repetitions ?? Infinity;
const mode = options.pingpong ? LoopPingPong : (repetitions === 1 ? LoopOnce : LoopRepeat);
this[$scene].playAnimation(
this.animationName,
this.animationCrossfadeDuration / MILLISECONDS_PER_SECOND);
this.animationCrossfadeDuration / MILLISECONDS_PER_SECOND,
mode,
repetitions);

// If we are currently paused, we need to force a render so that
// the scene updates to the first frame of the new animation
Expand Down
48 changes: 47 additions & 1 deletion packages/model-viewer/src/test/features/animation-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ suite('ModelViewerElementBase with AnimationMixin', () => {
expect(element.paused).to.be.true;
});

suite('when play is invoked', () => {
suite('when play is invoked with no options', () => {
setup(async () => {
const animationsPlay = waitForEvent(element, 'play');
element.play();
Expand Down Expand Up @@ -103,6 +103,52 @@ suite('ModelViewerElementBase with AnimationMixin', () => {
element.currentTime = 5;
expect(element[$scene].shouldRender()).to.be.true;
});

suite('when play is invoked again', () => {
setup(async () => {
const animationsPlay = waitForEvent(element, 'play');
element.play();
await animationsPlay;
});

test('animations play', () => {
expect(animationIsPlaying(element)).to.be.true;
});

test('has a duration greater than 0', () => {
expect(element.duration).to.be.greaterThan(0);
});
})
});
});

suite('when play is invoked with options', () => {
setup(async() => {
const animationsPlay = waitForEvent(element, 'play');
element.play({repetitions: 2, pingpong: true});
await animationsPlay;
});

suite('animations play at eash elapsed time', () => {
let t = 0;

test('at 80% duration', async() => {
await timePasses(element.duration * 0.8 * 1000);
expect(animationIsPlaying(element)).to.be.true;
t = element.currentTime;
});

test('at 180% duration', async() => {
await timePasses(element.duration * 1.8 * 1000);
expect(animationIsPlaying(element)).to.be.true;
expect(element.currentTime).to.be.lessThan(t)
});

test('at 220% duration', async() => {
await timePasses(element.duration * 2.2 * 1000);
expect(animationIsPlaying(element)).to.be.false;
expect(element.currentTime).to.be.equal(0);
});
});
});

Expand Down
18 changes: 15 additions & 3 deletions packages/model-viewer/src/three-components/ModelScene.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
* limitations under the License.
*/

import {AnimationAction, AnimationClip, AnimationMixer, Box3, Camera, Event as ThreeEvent, Matrix3, Object3D, PerspectiveCamera, Raycaster, Scene, Vector2, Vector3} from 'three';
import {AnimationAction, AnimationClip, AnimationMixer, Box3, Camera, Event as ThreeEvent, Matrix3, Object3D, PerspectiveCamera, Raycaster, Scene, Vector2, Vector3, LoopRepeat, LoopPingPong} from 'three';
import {CSS2DRenderer} from 'three/examples/jsm/renderers/CSS2DRenderer';

import ModelViewerElementBase, {$renderer, RendererInterface} from '../model-viewer-base.js';
Expand Down Expand Up @@ -490,7 +490,12 @@ export class ModelScene extends Scene {

get animationTime(): number {
if (this.currentAnimationAction != null) {
return this.currentAnimationAction.time;
const loopCount = Math.max((this.currentAnimationAction as any)._loopCount, 0);
if (this.currentAnimationAction.loop === LoopPingPong && (loopCount & 1) === 1) {
return this.duration - this.currentAnimationAction.time
} else {
return this.currentAnimationAction.time;
}
}

return 0;
Expand All @@ -515,7 +520,7 @@ export class ModelScene extends Scene {
* provided, or if no animation is found by the given name, always falls back
* to playing the first animation.
*/
playAnimation(name: string|null = null, crossfadeTime: number = 0) {
playAnimation(name: string|null = null, crossfadeTime: number = 0, loopMode: number = LoopRepeat, repetitionCount: number = Infinity) {
if (this._currentGLTF == null) {
return;
}
Expand Down Expand Up @@ -549,7 +554,10 @@ export class ModelScene extends Scene {
action.crossFadeFrom(lastAnimationAction, crossfadeTime, false);
}

action.setLoop(loopMode, repetitionCount);

action.enabled = true;

action.play();
} catch (error) {
console.error(error);
Expand All @@ -565,6 +573,10 @@ export class ModelScene extends Scene {
this.mixer.update(step);
}

subscribeMixerEvent(event: string, callback: (...args: any[]) => void) {
this.mixer.addEventListener(event, callback);
}

/**
* Call if the object has been changed in such a way that the shadow's shape
* has changed (not a rotation about the Y axis).
Expand Down
14 changes: 12 additions & 2 deletions packages/modelviewer.dev/data/docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -864,9 +864,9 @@
],
"Methods": [
{
"name": "play()",
"name": "play(options: {repetitions, pingpong})",
"htmlName": "play",
"description": "Causes animations to be played. Use the autoplay attribute if you want animations to be played automatically. If there are no animations, nothing will happen, so make sure that the model is loaded before invoking this method."
"description": "Causes animations to be played. You can specify the number of repetitions of the animation by setting the number of <i>repetitions</i> to any value greater than 0 (defaults to Infinity). Also if you set <i>pingpong</i> to true, alternately playing forward and backward (defaults to false). Use the autoplay attribute if you want animations to be played automatically. If there are no animations, nothing will happen, so make sure that the model is loaded before invoking this method."
},
{
"name": "pause()",
Expand All @@ -884,6 +884,16 @@
"name": "pause",
"htmlName": "pause",
"description": "Dispatched when animations are paused. A model always begins in the paused state, so it is worth mentioning that this event will not be dispatched until the the <span class='attribute'>.pause()</span> method is invoked after animations have begun playing."
},
{
"name": "loop",
"htmlName": "loop",
"description": "Dispatched when the current animation has looped. You can get the loop count with <span class='attribute'>e.detail.count</span>."
},
{
"name": "finished",
"htmlName": "finished",
"description": "Dispatched when the current animation has finished playing."
}
]
},
Expand Down
4 changes: 2 additions & 2 deletions packages/modelviewer.dev/examples/animation/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,10 @@
</head>
<body>

<div class="examples-page">
<div class="examples-page">
<div class="sidebar" id="sidenav"></div>
<div id="toggle"></div>

<div class="examples-container">
<div class="sample">
<div id="autoplay" class="demo"></div>
Expand Down

0 comments on commit 12179aa

Please sign in to comment.