diff --git a/src/apps/basic-wrapper/index.html b/src/apps/basic-wrapper/index.html index 3485302910860e68b898003c5bb71ae3ba0dba24..07592b99d310d9376a10be671fa594b4314e01e0 100644 --- a/src/apps/basic-wrapper/index.html +++ b/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> \ No newline at end of file diff --git a/src/apps/basic-wrapper/index.ts b/src/apps/basic-wrapper/index.ts index f5104d8e6b33ebb12aec3b904959a32e35c08e08..8635c265cfd7892a9d851b4ba3d9928724cac056 100644 --- a/src/apps/basic-wrapper/index.ts +++ b/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() } } diff --git a/src/mol-plugin/state/animation/built-in.ts b/src/mol-plugin/state/animation/built-in.ts index 4da3661e93236469794a049b77776fa29d03b22c..d22b1bf84be7ee3c408b24434026ea5a31cbde9f 100644 --- a/src/mol-plugin/state/animation/built-in.ts +++ b/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: {} }; } }) \ No newline at end of file diff --git a/src/mol-plugin/state/animation/manager.ts b/src/mol-plugin/state/animation/manager.ts index 616870275c1c7b33e5f1ef6ceeda863adf3e4d39..9ef6136f45d5a8ec448f30c513767199177c9d90 100644 --- a/src/mol-plugin/state/animation/manager.ts +++ b/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); } } diff --git a/src/mol-plugin/ui/controls/parameters.tsx b/src/mol-plugin/ui/controls/parameters.tsx index ad7ef082d48cf230cbe745d3e4bed7a610d6b545..03a22c1ab5aeb86e4f1b780d454dcfdc06d1ff36 100644 --- a/src/mol-plugin/ui/controls/parameters.tsx +++ b/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);