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

wip animations

parent 3cc79388
Branches
Tags
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.
Please to comment