Skip to content
Snippets Groups Projects
Commit 45c991b0 authored by David Sehnal's avatar David Sehnal
Browse files

wip animations

parent 3cc79388
No related branches found
No related tags found
No related merge requests found
......@@ -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() {
......
......@@ -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);
......
......@@ -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
]
}
......
......@@ -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
}
......
......@@ -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);
}
}
......
/**
* 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 } };
}
})
\ No newline at end of file
......@@ -4,4 +4,119 @@
* @author David Sehnal <david.sehnal@gmail.com>
*/
// TODO
\ No newline at end of file
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'
}
}
\ No newline at end of file
......@@ -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;
}
}
\ No newline at end of file
......@@ -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 />)}
......
/**
* 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>
}
}
\ No newline at end of file
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment