manager.ts 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  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 { PluginComponent } from 'mol-plugin/component';
  7. import { PluginContext } from 'mol-plugin/context';
  8. import { PluginStateAnimation } from './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 PluginComponent<PluginAnimationManager.State> {
  15. private map = new Map<string, PluginStateAnimation>();
  16. private animations: PluginStateAnimation[] = [];
  17. private _current: PluginAnimationManager.Current;
  18. private _params?: PD.For<PluginAnimationManager.State['params']> = void 0;
  19. readonly events = {
  20. updated: this.ev()
  21. };
  22. get isEmpty() { return this.animations.length === 0; }
  23. get current() { return this._current!; }
  24. private triggerUpdate() {
  25. this.events.updated.next();
  26. }
  27. getParams(): PD.Params {
  28. if (!this._params) {
  29. this._params = {
  30. current: PD.Select(this.animations[0] && this.animations[0].name,
  31. this.animations.map(a => [a.name, a.display.name] as [string, string]),
  32. { label: 'Animation' })
  33. };
  34. }
  35. return this._params as any as PD.Params;
  36. }
  37. updateParams(newParams: Partial<PluginAnimationManager.State['params']>) {
  38. this.updateState({ params: { ...this.state.params, ...newParams } });
  39. const anim = this.map.get(this.state.params.current)!;
  40. const params = anim.params(this.context) as PD.Params;
  41. this._current = {
  42. anim,
  43. params,
  44. paramValues: PD.getDefaultValues(params),
  45. state: {},
  46. startedTime: -1,
  47. lastTime: 0
  48. }
  49. this.triggerUpdate();
  50. }
  51. updateCurrentParams(values: any) {
  52. this._current.paramValues = { ...this._current.paramValues, ...values };
  53. this.triggerUpdate();
  54. }
  55. register(animation: PluginStateAnimation) {
  56. if (this.map.has(animation.name)) {
  57. this.context.log.error(`Animation '${animation.name}' is already registered.`);
  58. return;
  59. }
  60. this._params = void 0;
  61. this.map.set(animation.name, animation);
  62. this.animations.push(animation);
  63. if (this.animations.length === 1) {
  64. this.updateParams({ current: animation.name });
  65. } else {
  66. this.triggerUpdate();
  67. }
  68. }
  69. play<P>(animation: PluginStateAnimation<P>, params: P) {
  70. this.stop();
  71. if (!this.map.has(animation.name)) {
  72. this.register(animation);
  73. }
  74. this.updateParams({ current: animation.name });
  75. this.updateCurrentParams(params);
  76. this.start();
  77. }
  78. start() {
  79. this.context.canvas3d.setSceneAnimating(true);
  80. this.updateState({ animationState: 'playing' });
  81. this.triggerUpdate();
  82. this._current.lastTime = 0;
  83. this._current.startedTime = -1;
  84. this._current.state = this._current.anim.initialState(this._current.paramValues, this.context);
  85. requestAnimationFrame(this.animate);
  86. }
  87. stop() {
  88. this.context.canvas3d.setSceneAnimating(false);
  89. if (typeof this._frame !== 'undefined') cancelAnimationFrame(this._frame);
  90. this.updateState({ animationState: 'stopped' });
  91. this.triggerUpdate();
  92. }
  93. get isAnimating() {
  94. return this.state.animationState === 'playing';
  95. }
  96. private _frame: number | undefined = void 0;
  97. private animate = async (t: number) => {
  98. this._frame = void 0;
  99. if (this._current.startedTime < 0) this._current.startedTime = t;
  100. const newState = await this._current.anim.apply(
  101. this._current.state,
  102. { lastApplied: this._current.lastTime, current: t - this._current.startedTime },
  103. { params: this._current.paramValues, plugin: this.context });
  104. if (newState.kind === 'finished') {
  105. this.stop();
  106. } else if (newState.kind === 'next') {
  107. this._current.state = newState.state;
  108. this._current.lastTime = t - this._current.startedTime;
  109. if (this.state.animationState === 'playing') this._frame = requestAnimationFrame(this.animate);
  110. } else if (newState.kind === 'skip') {
  111. if (this.state.animationState === 'playing') this._frame = requestAnimationFrame(this.animate);
  112. }
  113. }
  114. getSnapshot(): PluginAnimationManager.Snapshot {
  115. if (!this.current) return { state: this.state };
  116. return {
  117. state: this.state,
  118. current: {
  119. paramValues: this._current.paramValues,
  120. state: this._current.anim.stateSerialization ? this._current.anim.stateSerialization.toJSON(this._current.state) : this._current.state
  121. }
  122. };
  123. }
  124. setSnapshot(snapshot: PluginAnimationManager.Snapshot) {
  125. this.updateState({ animationState: snapshot.state.animationState });
  126. this.updateParams(snapshot.state.params);
  127. if (snapshot.current) {
  128. this.current.paramValues = snapshot.current.paramValues;
  129. this.current.state = this._current.anim.stateSerialization
  130. ? this._current.anim.stateSerialization.fromJSON(snapshot.current.state)
  131. : snapshot.current.state;
  132. this.triggerUpdate();
  133. if (this.state.animationState === 'playing') this.resume();
  134. }
  135. }
  136. private resume() {
  137. this._current.lastTime = 0;
  138. this._current.startedTime = -1;
  139. requestAnimationFrame(this.animate);
  140. }
  141. constructor(private context: PluginContext) {
  142. super({ params: { current: '' }, animationState: 'stopped' });
  143. }
  144. }
  145. namespace PluginAnimationManager {
  146. export interface Current {
  147. anim: PluginStateAnimation
  148. params: PD.Params,
  149. paramValues: any,
  150. state: any,
  151. startedTime: number,
  152. lastTime: number
  153. }
  154. export interface State {
  155. params: { current: string },
  156. animationState: 'stopped' | 'playing'
  157. }
  158. export interface Snapshot {
  159. state: State,
  160. current?: {
  161. paramValues: any,
  162. state: any
  163. }
  164. }
  165. }