animation.ts 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257
  1. /**
  2. * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
  3. *
  4. * @author David Sehnal <david.sehnal@gmail.com>
  5. */
  6. import { StatefulPluginComponent } from '../component';
  7. import { PluginContext } from '../../mol-plugin/context';
  8. import { PluginStateAnimation } from '../animation/model';
  9. import { ParamDefinition as PD } from '../../mol-util/param-definition';
  10. export { PluginAnimationManager };
  11. // TODO: pause functionality (this needs to reset if the state tree changes)
  12. // TODO: handle unregistered animations on state restore
  13. // TODO: better API
  14. class PluginAnimationManager extends StatefulPluginComponent<PluginAnimationManager.State> {
  15. private map = new Map<string, PluginStateAnimation>();
  16. private _animations: PluginStateAnimation[] = [];
  17. private currentTime: number = 0;
  18. private _current: PluginAnimationManager.Current;
  19. private _params?: PD.For<PluginAnimationManager.State['params']> = void 0;
  20. readonly events = {
  21. updated: this.ev(),
  22. applied: this.ev(),
  23. };
  24. get isEmpty() { return this._animations.length === 0; }
  25. get current() { return this._current!; }
  26. get animations() { return this._animations; }
  27. private triggerUpdate() {
  28. this.events.updated.next(void 0);
  29. }
  30. private triggerApply() {
  31. this.events.applied.next(void 0);
  32. }
  33. getParams(): PD.Params {
  34. if (!this._params) {
  35. this._params = {
  36. current: PD.Select(this._animations[0] && this._animations[0].name,
  37. this._animations.map(a => [a.name, a.display.name] as [string, string]),
  38. { label: 'Animation' })
  39. };
  40. }
  41. return this._params as any as PD.Params;
  42. }
  43. updateParams(newParams: Partial<PluginAnimationManager.State['params']>) {
  44. if (this.isEmpty) return;
  45. this.updateState({ params: { ...this.state.params, ...newParams } });
  46. const anim = this.map.get(this.state.params.current)!;
  47. const params = anim.params(this.context) as PD.Params;
  48. this._current = {
  49. anim,
  50. params,
  51. paramValues: PD.getDefaultValues(params),
  52. state: {},
  53. startedTime: -1,
  54. lastTime: 0
  55. };
  56. this.triggerUpdate();
  57. }
  58. updateCurrentParams(values: any) {
  59. if (this.isEmpty) return;
  60. this._current.paramValues = { ...this._current.paramValues, ...values };
  61. this.triggerUpdate();
  62. }
  63. register(animation: PluginStateAnimation) {
  64. if (this.map.has(animation.name)) {
  65. this.context.log.error(`Animation '${animation.name}' is already registered.`);
  66. return;
  67. }
  68. this._params = void 0;
  69. this.map.set(animation.name, animation);
  70. this._animations.push(animation);
  71. if (this._animations.length === 1) {
  72. this.updateParams({ current: animation.name });
  73. } else {
  74. this.triggerUpdate();
  75. }
  76. }
  77. async play<P>(animation: PluginStateAnimation<P>, params: P) {
  78. await this.stop();
  79. if (!this.map.has(animation.name)) {
  80. this.register(animation);
  81. }
  82. this.updateParams({ current: animation.name });
  83. this.updateCurrentParams(params);
  84. await this.start();
  85. }
  86. async tick(t: number, isSynchronous?: boolean, animation?: PluginAnimationManager.AnimationInfo) {
  87. this.currentTime = t;
  88. if (this.isStopped) return;
  89. if (isSynchronous || animation) {
  90. await this.applyFrame(animation);
  91. } else {
  92. this.applyAsync();
  93. }
  94. }
  95. private isStopped = true;
  96. private isApplying = false;
  97. async start() {
  98. this.updateState({ animationState: 'playing' });
  99. if (!this.context.behaviors.state.isAnimating.value) {
  100. this.context.behaviors.state.isAnimating.next(true);
  101. }
  102. this.triggerUpdate();
  103. const anim = this._current.anim;
  104. let initialState = this._current.anim.initialState(this._current.paramValues, this.context);
  105. if (anim.setup) {
  106. const state = await anim.setup(this._current.paramValues, initialState, this.context);
  107. if (state) initialState = state;
  108. }
  109. this._current.lastTime = 0;
  110. this._current.startedTime = -1;
  111. this._current.state = initialState;
  112. this.isStopped = false;
  113. }
  114. async stop() {
  115. this.isStopped = true;
  116. if (this.state.animationState !== 'stopped') {
  117. const anim = this._current.anim;
  118. if (anim.teardown) {
  119. await anim.teardown(this._current.paramValues, this._current.state, this.context);
  120. }
  121. this.updateState({ animationState: 'stopped' });
  122. this.triggerUpdate();
  123. }
  124. if (this.context.behaviors.state.isAnimating.value) {
  125. this.context.behaviors.state.isAnimating.next(false);
  126. }
  127. }
  128. get isAnimating() {
  129. return this.state.animationState === 'playing';
  130. }
  131. private async applyAsync() {
  132. if (this.isApplying) return;
  133. this.isApplying = true;
  134. try {
  135. await this.applyFrame();
  136. } finally {
  137. this.isApplying = false;
  138. }
  139. }
  140. private async applyFrame(animation?: PluginAnimationManager.AnimationInfo) {
  141. const t = this.currentTime;
  142. if (this._current.startedTime < 0) this._current.startedTime = t;
  143. const newState = await this._current.anim.apply(
  144. this._current.state,
  145. { lastApplied: this._current.lastTime, current: t - this._current.startedTime, animation },
  146. { params: this._current.paramValues, plugin: this.context });
  147. if (newState.kind === 'finished') {
  148. this.stop();
  149. } else if (newState.kind === 'next') {
  150. this._current.state = newState.state;
  151. this._current.lastTime = t - this._current.startedTime;
  152. }
  153. this.triggerApply();
  154. }
  155. getSnapshot(): PluginAnimationManager.Snapshot {
  156. if (!this.current) return { state: this.state };
  157. return {
  158. state: this.state,
  159. current: {
  160. paramValues: this._current.paramValues,
  161. state: this._current.anim.stateSerialization ? this._current.anim.stateSerialization.toJSON(this._current.state) : this._current.state
  162. }
  163. };
  164. }
  165. setSnapshot(snapshot: PluginAnimationManager.Snapshot) {
  166. if (this.isEmpty) return;
  167. this.updateState({ animationState: snapshot.state.animationState });
  168. this.updateParams(snapshot.state.params);
  169. if (snapshot.current) {
  170. this.current.paramValues = snapshot.current.paramValues;
  171. this.current.state = this._current.anim.stateSerialization
  172. ? this._current.anim.stateSerialization.fromJSON(snapshot.current.state)
  173. : snapshot.current.state;
  174. this.triggerUpdate();
  175. if (this.state.animationState === 'playing') this.resume();
  176. }
  177. }
  178. private async resume() {
  179. this._current.lastTime = 0;
  180. this._current.startedTime = -1;
  181. const anim = this._current.anim;
  182. if (!this.context.behaviors.state.isAnimating.value) {
  183. this.context.behaviors.state.isAnimating.next(true);
  184. }
  185. if (anim.setup) {
  186. await anim.setup(this._current.paramValues, this._current.state, this.context);
  187. }
  188. this.isStopped = false;
  189. }
  190. constructor(private context: PluginContext) {
  191. super({ params: { current: '' }, animationState: 'stopped' });
  192. }
  193. }
  194. namespace PluginAnimationManager {
  195. export interface AnimationInfo {
  196. currentFrame: number,
  197. frameCount: number
  198. }
  199. export interface Current {
  200. anim: PluginStateAnimation
  201. params: PD.Params,
  202. paramValues: any,
  203. state: any,
  204. startedTime: number,
  205. lastTime: number
  206. }
  207. export interface State {
  208. params: { current: string },
  209. animationState: 'stopped' | 'playing'
  210. }
  211. export interface Snapshot {
  212. state: State,
  213. current?: {
  214. paramValues: any,
  215. state: any
  216. }
  217. }
  218. }