manager.ts 6.0 KB

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