Browse Source

mol-plugin: model index animation modes

David Sehnal 6 years ago
parent
commit
9854f4b6f3

+ 25 - 6
src/apps/basic-wrapper/index.html

@@ -29,6 +29,7 @@
             #controls > button {
                 display: block;
                 width: 100%;
+                text-align: left;
             }
 
             #controls > hr {
@@ -57,18 +58,30 @@
             BasicMolStarWrapper.load({ url: url, format: format, assemblyId: assemblyId });
             BasicMolStarWrapper.toggleSpin();
 
-            addControl('Toggle Spin', () => BasicMolStarWrapper.toggleSpin());
-
-            addSeparator();
-
+            addHeader('Source');
             addControl('Load Asym Unit', () => BasicMolStarWrapper.load({ url: url, format: format }));
             addControl('Load Assembly 1', () => BasicMolStarWrapper.load({ url: url, format: format, assemblyId: assemblyId }));
 
             addSeparator();
 
-            addControl('Play Forward', () => BasicMolStarWrapper.animate.modelIndex.forward(8 /** maxFPS */));
+            addHeader('Camera');
+            addControl('Toggle Spin', () => BasicMolStarWrapper.toggleSpin());
+            
+            addSeparator();
+
+            addHeader('Animation');
+
+            // adjust this number to make the animation faster or slower
+            // requires to "restart" the animation if changed
+            BasicMolStarWrapper.animate.modelIndex.maxFPS = 4;
+
+            addControl('Play To End', () => BasicMolStarWrapper.animate.modelIndex.onceForward());
+            addControl('Play To Start', () => BasicMolStarWrapper.animate.modelIndex.onceBackward());
+            addControl('Play Palindrome', () => BasicMolStarWrapper.animate.modelIndex.palindrome());
+            addControl('Play Loop', () => BasicMolStarWrapper.animate.modelIndex.loop());
             addControl('Stop', () => BasicMolStarWrapper.animate.modelIndex.stop());
-            addControl('Play Backward', () => BasicMolStarWrapper.animate.modelIndex.backward(8 /** maxFPS */));
+
+            ////////////////////////////////////////////////////////
 
             function addControl(label, action) {
                 var btn = document.createElement('button');
@@ -81,6 +94,12 @@
                 var hr = document.createElement('hr');
                 document.getElementById('controls').appendChild(hr);
             }
+
+            function addHeader(header) {
+                var h = document.createElement('h3');
+                h.innerText = header;
+                document.getElementById('controls').appendChild(h);
+            }
         </script>
     </body>
 </html>

+ 5 - 2
src/apps/basic-wrapper/index.ts

@@ -102,8 +102,11 @@ class BasicWrapper {
 
     animate = {
         modelIndex: {
-            forward: (maxFPS = 8) => { this.plugin.state.animation.play(AnimateModelIndex, { maxFPS: Math.max(0.5, maxFPS | 0), direction: 'forward' }) },
-            backward: (maxFPS = 8) => { this.plugin.state.animation.play(AnimateModelIndex, { maxFPS: Math.max(0.5, maxFPS | 0), direction: 'backward' }) },
+            maxFPS: 8,
+            onceForward: () => { this.plugin.state.animation.play(AnimateModelIndex, { maxFPS: Math.max(0.5, this.animate.modelIndex.maxFPS | 0), mode: { name: 'once', params: { direction: 'forward' } } }) },
+            onceBackward: () => { this.plugin.state.animation.play(AnimateModelIndex, { maxFPS: Math.max(0.5, this.animate.modelIndex.maxFPS | 0), mode: { name: 'once', params: { direction: 'backward' } } }) },
+            palindrome: () => { this.plugin.state.animation.play(AnimateModelIndex, { maxFPS: Math.max(0.5, this.animate.modelIndex.maxFPS | 0), mode: { name: 'palindrome', params: {} } }) },
+            loop: () => { this.plugin.state.animation.play(AnimateModelIndex, { maxFPS: Math.max(0.5, this.animate.modelIndex.maxFPS | 0), mode: { name: 'loop', params: {} } }) },
             stop: () => this.plugin.state.animation.stop()
         }
     }

+ 35 - 7
src/mol-plugin/state/animation/built-in.ts

@@ -9,16 +9,20 @@ import { PluginStateObject } from '../objects';
 import { StateTransforms } from '../transforms';
 import { StateSelection } from 'mol-state/state/selection';
 import { PluginCommands } from 'mol-plugin/command';
-import { ParamDefinition } from 'mol-util/param-definition';
+import { ParamDefinition as PD } from 'mol-util/param-definition';
 
 export const AnimateModelIndex = PluginStateAnimation.create({
     name: 'built-in.animate-model-index',
     display: { name: 'Animate Model Index' },
     params: () => ({
-        direction: ParamDefinition.Select('forward', [['forward', 'Forward'], ['backward', 'Backward']]),
-        maxFPS: ParamDefinition.Numeric(3, { min: 0.5, max: 30, step: 0.5 })
+        mode: PD.MappedStatic('once', {
+            once: PD.Group({ direction: PD.Select('forward', [['forward', 'Forward'], ['backward', 'Backward']]) }),
+            palindrome: PD.Group({ }),
+            loop: PD.Group({ }),
+        }, { options: [['once', 'Once'], ['palindrome', 'Palindrome'], ['loop', 'Loop']] }),
+        maxFPS: PD.Numeric(3, { min: 0.5, max: 30, step: 0.5 })
     }),
-    initialState: () => ({ }),
+    initialState: () => ({} as { palindromeDirections?: { [id: string]: -1 | 1 | undefined } }),
     async apply(animState, t, ctx) {
         // limit fps
         if (t.current > 0 && t.current - t.lastApplied < 1000 / ctx.params.maxFPS) {
@@ -31,7 +35,9 @@ export const AnimateModelIndex = PluginStateAnimation.create({
 
         const update = state.build();
 
-        const dir = ctx.params.direction === 'backward' ? -1 : 1;
+        const params = ctx.params;
+        const palindromeDirections = animState.palindromeDirections || { };
+        let isEnd = false;
 
         for (const m of models) {
             const parent = StateSelection.findAncestorOfType(state.tree, state.cells, m.transform.ref, [PluginStateObject.Molecule.Trajectory]);
@@ -39,13 +45,35 @@ export const AnimateModelIndex = PluginStateAnimation.create({
             const traj = parent.obj as PluginStateObject.Molecule.Trajectory;
             update.to(m.transform.ref).update(StateTransforms.Model.ModelFromTrajectory,
                 old => {
-                    let modelIndex = (old.modelIndex + dir) % traj.data.length;
-                    if (modelIndex < 0) modelIndex += traj.data.length;
+                    const len = traj.data.length;
+                    let dir: -1 | 1 = 1;
+                    if (params.mode.name === 'once') {
+                        dir = params.mode.params.direction === 'backward' ? -1 : 1;
+                        // if we are at start or end already, do nothing.
+                        if ((dir === -1 && old.modelIndex === 0) || (dir === 1 && old.modelIndex === len - 1)) {
+                            isEnd = true;
+                            return old;
+                        }
+                    } else if (params.mode.name === 'palindrome') {
+                        if (old.modelIndex === 0) dir = 1;
+                        else if (old.modelIndex === len - 1) dir = -1;
+                        else dir = palindromeDirections[m.transform.ref] || 1;
+                    }
+                    palindromeDirections[m.transform.ref] = dir;
+
+                    let modelIndex = (old.modelIndex + dir) % len;
+                    if (modelIndex < 0) modelIndex += len;
+
+                    isEnd = isEnd || (dir === -1 && modelIndex === 0) || (dir === 1 && modelIndex === len - 1);
+
                     return { modelIndex };
                 });
         }
 
         await PluginCommands.State.Update.dispatch(ctx.plugin, { state, tree: update });
+
+        if (params.mode.name === 'once' && isEnd) return { kind: 'finished' };
+        if (params.mode.name === 'palindrome') return { kind: 'next', state: { palindromeDirections } };
         return { kind: 'next', state: {} };
     }
 })

+ 7 - 3
src/mol-plugin/state/animation/manager.ts

@@ -76,7 +76,7 @@ class PluginAnimationManager extends PluginComponent<PluginAnimationManager.Stat
             this.register(animation);
         }
         this.updateParams({ current: animation.name });
-        this.updateParams(params);
+        this.updateCurrentParams(params);
         this.start();
     }
 
@@ -92,6 +92,7 @@ class PluginAnimationManager extends PluginComponent<PluginAnimationManager.Stat
     }
 
     stop() {
+        if (typeof this._frame !== 'undefined') cancelAnimationFrame(this._frame);
         this.updateState({ animationState: 'stopped' });
         this.triggerUpdate();
     }
@@ -100,7 +101,10 @@ class PluginAnimationManager extends PluginComponent<PluginAnimationManager.Stat
         return this.latestState.animationState === 'playing';
     }
 
+    private _frame: number | undefined = void 0;
     private animate = async (t: number) => {
+        this._frame = void 0;
+
         if (this._current.startedTime < 0) this._current.startedTime = t;
         const newState = await this._current.anim.apply(
             this._current.state,
@@ -112,9 +116,9 @@ class PluginAnimationManager extends PluginComponent<PluginAnimationManager.Stat
         } else if (newState.kind === 'next') {
             this._current.state = newState.state;
             this._current.lastTime = t - this._current.startedTime;
-            if (this.latestState.animationState === 'playing') requestAnimationFrame(this.animate);
+            if (this.latestState.animationState === 'playing') this._frame = requestAnimationFrame(this.animate);
         } else if (newState.kind === 'skip') {
-            if (this.latestState.animationState === 'playing') requestAnimationFrame(this.animate);
+            if (this.latestState.animationState === 'playing') this._frame = requestAnimationFrame(this.animate);
         }
     }
 

+ 3 - 1
src/mol-plugin/ui/controls/parameters.tsx

@@ -29,8 +29,10 @@ export class ParameterControls<P extends PD.Params> extends React.PureComponent<
     render() {
         const params = this.props.params;
         const values = this.props.values;
+        const keys = Object.keys(params);
+        if (keys.length === 0) return null;
         return <div style={{ width: '100%' }}>
-            {Object.keys(params).map(key => {
+            {keys.map(key => {
                 const param = params[key];
                 if (param.isHidden) return null;
                 const Control = controlFor(param);