controls.ts 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
  1. /**
  2. * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
  3. *
  4. * @author David Sehnal <david.sehnal@gmail.com>
  5. */
  6. import { debounceTime } from 'rxjs/operators';
  7. import { PluginStateAnimation } from '../../mol-plugin-state/animation/model';
  8. import { PluginComponent } from '../../mol-plugin-state/component';
  9. import { PluginContext } from '../../mol-plugin/context';
  10. import { Task } from '../../mol-task';
  11. import { ParamDefinition as PD } from '../../mol-util/param-definition';
  12. import { encodeMp4Animation } from './encoder';
  13. export interface Mp4AnimationInfo {
  14. width: number,
  15. height: number
  16. }
  17. export const Mp4AnimationParams = {
  18. quantization: PD.Numeric(18, { min: 10, max: 51 }, { description: 'Lower is better, but slower.' })
  19. };
  20. export class Mp4Controls extends PluginComponent {
  21. private currentNames = new Set<string>();
  22. private animations: PluginStateAnimation[] = [];
  23. readonly behaviors = {
  24. animations: this.ev.behavior<PD.Params>({ }),
  25. current: this.ev.behavior<{ anim: PluginStateAnimation, params: PD.Params, values: any } | undefined>(void 0),
  26. canApply: this.ev.behavior<PluginStateAnimation.CanApply>({ canApply: false }),
  27. info: this.ev.behavior<Mp4AnimationInfo>({ width: 0, height: 0 }),
  28. params: this.ev.behavior<PD.Values<typeof Mp4AnimationParams>>(PD.getDefaultValues(Mp4AnimationParams))
  29. };
  30. setCurrent(name?: string) {
  31. const anim = this.animations.find(a => a.name === name);
  32. if (!anim) {
  33. this.behaviors.current.next(anim);
  34. return;
  35. }
  36. const params = anim.params(this.plugin) as PD.Params;
  37. const values = PD.getDefaultValues(params);
  38. this.behaviors.current.next({ anim, params, values });
  39. this.behaviors.canApply.next(anim.canApply?.(this.plugin) ?? { canApply: true });
  40. }
  41. setCurrentParams(values: any) {
  42. this.behaviors.current.next({ ...this.behaviors.current.value!, values });
  43. }
  44. get current() {
  45. return this.behaviors.current.value;
  46. }
  47. render() {
  48. const task = Task.create('Export Animation', async ctx => {
  49. try {
  50. const resolution = this.plugin.helpers.viewportScreenshot?.getSizeAndViewport()!;
  51. const anim = this.current!;
  52. const movie = await encodeMp4Animation(this.plugin, ctx, {
  53. animation: {
  54. definition: anim.anim,
  55. params: anim.values,
  56. },
  57. ...resolution,
  58. quantizationParameter: this.behaviors.params.value.quantization,
  59. pass: this.plugin.helpers.viewportScreenshot?.imagePass!,
  60. });
  61. const filename = anim.anim.display.name.toLowerCase().replace(/\s/g, '-').replace(/[^a-z0-9_\-]/g, '');
  62. return { movie, filename: `${this.plugin.helpers.viewportScreenshot?.getFilename('')}_${filename}.mp4` };
  63. } catch (e) {
  64. this.plugin.log.error('Error during animation export');
  65. throw e;
  66. }
  67. });
  68. return this.plugin.runTask(task, { useOverlay: true });
  69. }
  70. private get manager() {
  71. return this.plugin.managers.animation;
  72. }
  73. private syncInfo() {
  74. const helper = this.plugin.helpers.viewportScreenshot;
  75. const size = helper?.getSizeAndViewport();
  76. if (!size) return;
  77. this.behaviors.info.next({ width: size.viewport.width, height: size.viewport.height });
  78. }
  79. private sync() {
  80. const animations = this.manager.animations.filter(a => a.isExportable);
  81. const hasAll = animations.every(a => this.currentNames.has(a.name));
  82. if (hasAll && this.currentNames.size === animations.length) {
  83. return;
  84. }
  85. const params = {
  86. current: PD.Select(animations[0]?.name,
  87. animations.map(a => [a.name, a.display.name] as [string, string]),
  88. { label: 'Animation' })
  89. };
  90. const current = this.behaviors.current.value;
  91. const hasCurrent = !!animations.find(a => a.name === current?.anim.name);
  92. this.animations = animations;
  93. if (!hasCurrent) {
  94. this.setCurrent(animations[0]?.name);
  95. }
  96. this.behaviors.animations.next(params);
  97. }
  98. private init() {
  99. if (!this.plugin.canvas3d) return;
  100. this.subscribe(this.plugin.managers.animation.events.updated.pipe(debounceTime(16)), () => {
  101. this.sync();
  102. });
  103. this.subscribe(this.plugin.canvas3d.resized, () => this.syncInfo());
  104. this.subscribe(this.plugin.helpers.viewportScreenshot?.events.previewed!, () => this.syncInfo());
  105. this.subscribe(this.plugin.behaviors.state.isBusy, b => this.updateCanApply(b));
  106. this.subscribe(this.plugin.managers.snapshot.events.changed, b => this.updateCanApply(b));
  107. this.sync();
  108. this.syncInfo();
  109. }
  110. private updateCanApply(b?: any) {
  111. const anim = this.current;
  112. if (!b && anim) {
  113. this.behaviors.canApply.next(anim.anim.canApply?.(this.plugin) ?? { canApply: true });
  114. }
  115. }
  116. constructor(private plugin: PluginContext) {
  117. super();
  118. this.init();
  119. }
  120. }