David Sehnal vor 6 Jahren
Ursprung
Commit
45c991b0a9

+ 3 - 1
src/mol-plugin/component.ts

@@ -12,12 +12,14 @@ export class PluginComponent<State> {
     private _state: BehaviorSubject<State>;
     private _updated = new Subject();
 
-    updateState(...states: Partial<State>[]) {
+    updateState(...states: Partial<State>[]): boolean {
         const latest = this.latestState;
         const s = shallowMergeArray(latest, states);
         if (s !== latest) {
             this._state.next(s);
+            return true;
         }
+        return false;
     }
 
     get states() {

+ 8 - 0
src/mol-plugin/context.ts

@@ -173,6 +173,13 @@ export class PluginContext {
         }
     }
 
+    private initAnimations() {
+        if (!this.spec.animations) return;
+        for (const anim of this.spec.animations) {
+            this.state.animation.register(anim);
+        }
+    }
+
     private initCustomParamEditors() {
         if (!this.spec.customParamEditors) return;
 
@@ -188,6 +195,7 @@ export class PluginContext {
 
         this.initBehaviors();
         this.initDataActions();
+        this.initAnimations();
         this.initCustomParamEditors();
 
         this.lociLabels = new LociLabelManager(this);

+ 4 - 0
src/mol-plugin/index.ts

@@ -14,6 +14,7 @@ import { PluginSpec } from './spec';
 import { DownloadStructure, CreateComplexRepresentation, OpenStructure, OpenVolume, DownloadDensity } from './state/actions/basic';
 import { StateTransforms } from './state/transforms';
 import { PluginBehaviors } from './behavior';
+import { AnimateModelIndex } from './state/animation/built-in';
 
 function getParam(name: string, regex: string): string {
     let r = new RegExp(`${name}=(${regex})[&]?`, 'i');
@@ -48,6 +49,9 @@ export const DefaultPluginSpec: PluginSpec = {
         PluginSpec.Behavior(PluginBehaviors.Labels.SceneLabels),
         PluginSpec.Behavior(PluginBehaviors.CustomProps.PDBeStructureQualityReport, { autoAttach: true }),
         PluginSpec.Behavior(PluginBehaviors.CustomProps.RCSBAssemblySymmetry, { autoAttach: true }),
+    ],
+    animations: [
+        AnimateModelIndex
     ]
 }
 

+ 3 - 1
src/mol-plugin/spec.ts

@@ -8,13 +8,15 @@ import { StateAction } from 'mol-state/action';
 import { Transformer } from 'mol-state';
 import { StateTransformParameters } from './ui/state/common';
 import { PluginLayoutStateProps } from './layout';
+import { PluginStateAnimation } from './state/animation/model';
 
 export { PluginSpec }
 
 interface PluginSpec {
     actions: PluginSpec.Action[],
     behaviors: PluginSpec.Behavior[],
-    customParamEditors?: [StateAction | Transformer, StateTransformParameters.Class][]
+    animations?: PluginStateAnimation[],
+    customParamEditors?: [StateAction | Transformer, StateTransformParameters.Class][],
     initialLayout?: PluginLayoutStateProps
 }
 

+ 4 - 0
src/mol-plugin/state.ts

@@ -13,6 +13,7 @@ import { PluginStateSnapshotManager } from './state/snapshots';
 import { RxEventHelper } from 'mol-util/rx-event-helper';
 import { Canvas3DProps } from 'mol-canvas3d/canvas3d';
 import { PluginCommands } from './command';
+import { PluginAnimationManager } from './state/animation/manager';
 export { PluginState }
 
 class PluginState {
@@ -20,6 +21,7 @@ class PluginState {
 
     readonly dataState: State;
     readonly behaviorState: State;
+    readonly animation: PluginAnimationManager;
     readonly cameraSnapshots = new CameraSnapshotManager();
 
     readonly snapshots = new PluginStateSnapshotManager();
@@ -81,6 +83,8 @@ class PluginState {
         });
 
         this.behavior.currentObject.next(this.dataState.behaviors.currentObject.value);
+
+        this.animation = new PluginAnimationManager(plugin);
     }
 }
 

+ 46 - 0
src/mol-plugin/state/animation/built-in.ts

@@ -0,0 +1,46 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { PluginStateAnimation } from './model';
+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';
+
+export const AnimateModelIndex = PluginStateAnimation.create({
+    name: 'built-in.animate-model-index',
+    display: { name: 'Animate Model Index' },
+    params: () => ({ maxFPS: ParamDefinition.Numeric(3, { min: 0.5, max: 30, step: 0.5 }) }),
+    initialState: () => ({ frame: 1 }),
+    async apply(animState, t, ctx) {
+        // limit fps
+        if (t.current > 0 && t.current - t.lastApplied < 1000 / ctx.params.maxFPS) {
+            return { kind: 'skip' };
+        }
+
+        const state = ctx.plugin.state.dataState;
+        const models = state.selectQ(q => q.rootsOfType(PluginStateObject.Molecule.Model)
+            .filter(c => c.transform.transformer === StateTransforms.Model.ModelFromTrajectory));
+
+        const update = state.build();
+
+        for (const m of models) {
+            const parent = StateSelection.findAncestorOfType(state.tree, state.cells, m.transform.ref, [PluginStateObject.Molecule.Trajectory]);
+            if (!parent || !parent.obj) continue;
+            const traj = parent.obj as PluginStateObject.Molecule.Trajectory;
+            update.to(m.transform.ref).update(StateTransforms.Model.ModelFromTrajectory,
+                old => {
+                    let modelIndex = animState.frame % traj.data.length;
+                    if (modelIndex < 0) modelIndex += traj.data.length;
+                    return { modelIndex };
+                });
+        }
+
+        await PluginCommands.State.Update.dispatch(ctx.plugin, { state, tree: update });
+        return { kind: 'next', state: { frame: animState.frame + 1 } };
+    }
+})

+ 116 - 1
src/mol-plugin/state/animation/manager.ts

@@ -4,4 +4,119 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
-// TODO
+import { PluginComponent } from 'mol-plugin/component';
+import { PluginContext } from 'mol-plugin/context';
+import { PluginStateAnimation } from './model';
+import { ParamDefinition as PD } from 'mol-util/param-definition';
+
+export { PluginAnimationManager }
+
+class PluginAnimationManager extends PluginComponent<PluginAnimationManager.State> {
+    private map = new Map<string, PluginStateAnimation>();
+    private animations: PluginStateAnimation[] = [];
+    private _current: PluginAnimationManager.Current;
+    private _params?: PD.For<PluginAnimationManager.State['params']> = void 0;
+
+    get isEmpty() { return this.animations.length === 0; }
+    get current() { return this._current!; }
+
+    getParams(): PD.Params {
+        if (!this._params) {
+            this._params = {
+                current: PD.Select(this.animations[0] && this.animations[0].name,
+                    this.animations.map(a => [a.name, a.display.name] as [string, string]),
+                    { label: 'Animation' })
+            };
+        }
+        return this._params as any as PD.Params;
+    }
+
+    updateParams(newParams: Partial<PluginAnimationManager.State['params']>) {
+        this.updateState({ params: { ...this.latestState.params, ...newParams } });
+        const anim = this.map.get(this.latestState.params.current)!;
+        const params = anim.params(this.context);
+        this._current = {
+            anim,
+            params,
+            paramValues: PD.getDefaultValues(params),
+            state: {},
+            startedTime: -1,
+            lastTime: 0
+        }
+        this.triggerUpdate();
+    }
+
+    updateCurrentParams(values: any) {
+        this._current.paramValues = { ...this._current.paramValues, ...values };
+        this.triggerUpdate();
+    }
+
+    register(animation: PluginStateAnimation) {
+        if (this.map.has(animation.name)) {
+            this.context.log.error(`Animation '${animation.name}' is already registered.`);
+            return;
+        }
+        this._params = void 0;
+        this.map.set(animation.name, animation);
+        this.animations.push(animation);
+        if (this.animations.length === 1) {
+            this.updateParams({ current: animation.name });
+        } else {
+            this.triggerUpdate();
+        }
+    }
+
+    start() {
+        this.updateState({ animationState: 'playing' });
+        this.triggerUpdate();
+
+        this._current.lastTime = 0;
+        this._current.startedTime = -1;
+        this._current.state = this._current.anim.initialState(this._current.paramValues, this.context);
+
+        requestAnimationFrame(this.animate);
+    }
+
+    stop() {
+        this.updateState({ animationState: 'stopped' });
+        this.triggerUpdate();
+    }
+
+    animate = async (t: number) => {
+        if (this._current.startedTime < 0) this._current.startedTime = t;
+        const newState = await this._current.anim.apply(
+            this._current.state,
+            { lastApplied: this._current.lastTime, current: t - this._current.startedTime },
+            { params: this._current.paramValues, plugin: this.context });
+
+        if (newState.kind === 'finished') {
+            this.stop();
+        } 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);
+        } else if (newState.kind === 'skip') {
+            if (this.latestState.animationState === 'playing') requestAnimationFrame(this.animate);
+        }
+    }
+
+    constructor(ctx: PluginContext) {
+        super(ctx, { params: { current: '' }, animationState: 'stopped' });
+    }
+}
+
+namespace PluginAnimationManager {
+    export interface Current {
+        anim: PluginStateAnimation
+        params: PD.Params,
+        paramValues: any,
+        state: any,
+        startedTime: number,
+        lastTime: number
+    }
+
+    export interface State {
+        params: { current: string },
+        animationState: 'stopped' | 'playing'
+    }
+}

+ 15 - 6
src/mol-plugin/state/animation/model.ts

@@ -9,8 +9,9 @@ import { PluginContext } from 'mol-plugin/context';
 
 export { PluginStateAnimation }
 
-interface PluginStateAnimation<P extends PD.Params, S> {
-    id: string,
+interface PluginStateAnimation<P extends PD.Params = any, S = any> {
+    name: string,
+    readonly display: { readonly name: string, readonly description?: string },
     params: (ctx: PluginContext) => P,
     initialState(params: PD.Values<P>, ctx: PluginContext): S,
 
@@ -18,20 +19,28 @@ interface PluginStateAnimation<P extends PD.Params, S> {
      * Apply the current frame and modify the state.
      * @param t Current absolute time since the animation started.
      */
-    apply(state: S, t: number, ctx: PluginStateAnimation.Context<P>): Promise<PluginStateAnimation.ApplyResult<S>>,
+    apply(state: S, t: PluginStateAnimation.Time, ctx: PluginStateAnimation.Context<P>): Promise<PluginStateAnimation.ApplyResult<S>>,
 
     /**
      * The state must be serializable to JSON. If JSON.stringify is not enough,
      * custom serializer can be provided.
      */
-    stateSerialization?: { toJson?(state: S): any, fromJson?(data: any): S }
+    stateSerialization?: { toJSON?(state: S): any, fromJSON?(data: any): S }
 }
 
 namespace PluginStateAnimation {
-    export type ApplyResult<S> = { kind: 'finished' } | { kind: 'next', state: S }
+    export interface Time {
+        lastApplied: number,
+        current: number
+    }
+
+    export type ApplyResult<S> = { kind: 'finished' } | { kind: 'skip' } | { kind: 'next', state: S }
     export interface Context<P extends PD.Params> {
         params: PD.Values<P>,
         plugin: PluginContext
     }
-}
 
+    export function create<P extends PD.Params, S>(params: PluginStateAnimation<P, S>) {
+        return params;
+    }
+}

+ 2 - 0
src/mol-plugin/ui/plugin.tsx

@@ -20,6 +20,7 @@ import { ApplyActionContol } from './state/apply-action';
 import { PluginState } from 'mol-plugin/state';
 import { UpdateTransformContol } from './state/update-transform';
 import { StateObjectCell } from 'mol-state';
+import { AnimationControls } from './state/animation';
 
 export class Plugin extends React.Component<{ plugin: PluginContext }, {}> {
 
@@ -62,6 +63,7 @@ class Layout extends PluginComponent {
                         <CurrentObject />
                         <Controls />
                         <CameraSnapshots />
+                        <AnimationControls />
                         <StateSnapshots />
                     </div>)}
                     {layout.showControls && this.region('bottom', <Log />)}

+ 50 - 0
src/mol-plugin/ui/state/animation.tsx

@@ -0,0 +1,50 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import * as React from 'react';
+import { PluginComponent } from '../base';
+import { ParameterControls, ParamOnChange } from '../controls/parameters';
+
+export class AnimationControls extends PluginComponent<{ }> {
+    componentDidMount() {
+        this.subscribe(this.plugin.state.animation.updated, () => this.forceUpdate());
+    }
+
+    updateParams: ParamOnChange = p => {
+        this.plugin.state.animation.updateParams({ [p.name]: p.value });
+    }
+
+    updateCurrentParams: ParamOnChange = p => {
+        this.plugin.state.animation.updateCurrentParams({ [p.name]: p.value });
+    }
+
+    startOrStop = () => {
+        const anim = this.plugin.state.animation;
+        if (anim.latestState.animationState === 'playing') anim.stop();
+        else anim.start();
+    }
+
+    render() {
+        const anim = this.plugin.state.animation;
+        if (anim.isEmpty) return null;
+
+        const isDisabled = anim.latestState.animationState === 'playing';
+
+        // TODO: give it its own style
+        return <div style={{ marginBottom: '10px' }}>
+            <div className='msp-section-header'>Animations</div>
+
+            <ParameterControls params={anim.getParams()} values={anim.latestState.params} onChange={this.updateParams} isDisabled={isDisabled} />
+            <ParameterControls params={anim.current.params} values={anim.current.paramValues} onChange={this.updateCurrentParams} isDisabled={isDisabled} />
+
+            <div className='msp-btn-row-group'>
+                <button className='msp-btn msp-btn-block msp-form-control' onClick={this.startOrStop}>
+                    {anim.latestState.animationState === 'playing' ? 'Stop' : 'Start'}
+                </button>
+            </div>
+        </div>
+    }
+}