mirror of
https://github.com/molstar/molstar.git
synced 2026-06-05 14:04:36 +08:00
Compare commits
6 Commits
v5.0.0-dev
...
experiment
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
14e619d6d2 | ||
|
|
42d969bbeb | ||
|
|
fdc33e44dc | ||
|
|
b0aa889a0a | ||
|
|
4d7bd53231 | ||
|
|
c11cf665c9 |
@@ -13,6 +13,7 @@ Note that since we don't clearly distinguish between a public and private interf
|
||||
- This change is breaking because all volume objects require the `instances` field now.
|
||||
- [Breaking] `Canvas3D.identify` now expects `Vec2` or `Ray3D`
|
||||
- [Breaking] `TrackballControlsParams.animate.spin.speed` now means "Number of rotations per second" instead of "radians per second"
|
||||
- [Breaking] `PluginStateSnapshotManager.play` now accepts an options object instead of a single boolean value
|
||||
- Update production build to use `esbuild`
|
||||
- Emit explicit paths in `import`s in `lib/`
|
||||
- Fix outlines on opaque elements using illumination mode
|
||||
|
||||
@@ -20,6 +20,10 @@ Generally, the command should be URL encoded, e.g., `a b` => `a%20b` (in JS, `en
|
||||
|
||||
- `center-camera` - Centers the camera
|
||||
- `apply-snapshot=key` - Loads snapshots with the provided key
|
||||
- `next-snapshot[=-1|1]` - Loads next/previous snapshot, the direction is optional and default to `1`
|
||||
- `play-snapshots` - Starts playback of state snapshots
|
||||
- `play-transition` - Plays an animation associated with the given snapshot
|
||||
- `stop-animation` - Stops currently playing animation
|
||||
- `focus-refs=ref1,ref2,...` - On click, focuses nodes with the provided refs
|
||||
- `highlight-refs=ref1,ref2,...` - On mouse over, highlights the provided refs
|
||||
- `query=...&lang=...&action=highlight,focus&focus-radius=...`
|
||||
@@ -28,7 +32,7 @@ Generally, the command should be URL encoded, e.g., `a b` => `a%20b` (in JS, `en
|
||||
- (optional) `action` is an array of `highlight` (default), `focus` (multiple actions can be specified)
|
||||
- (optional) `focus-radius` is extra distance applied when focusing the selection (default is `3`)
|
||||
- Example: `[HEM](!query=resn%20HEM%26lang=pymol&action=highlight,focus)` highlights or focuses the HEM residue (the query must be URL encoded because it contains spaces and possibly other special characters)
|
||||
- `play-audio=src`, `toggle-audio[=str]`, `stop-audio`, `pause-audio` - Audio playback support
|
||||
- `play-audio=src`, `toggle-audio[=src]`, `stop-audio`, `pause-audio`, `dispose-audio` - Audio playback support
|
||||
|
||||
## Custom Content
|
||||
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "molstar",
|
||||
"version": "5.0.0-dev.9",
|
||||
"version": "5.0.0-dev.10",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "molstar",
|
||||
"version": "5.0.0-dev.9",
|
||||
"version": "5.0.0-dev.10",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/argparse": "^2.0.17",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "molstar",
|
||||
"version": "5.0.0-dev.9",
|
||||
"version": "5.0.0-dev.10",
|
||||
"description": "A comprehensive macromolecular library.",
|
||||
"homepage": "https://github.com/molstar/molstar#readme",
|
||||
"repository": {
|
||||
@@ -122,7 +122,8 @@
|
||||
"Lukáš Polák <admin@lukaspolak.cz>",
|
||||
"Chetan Mishra <chetan.s115@gmail.com>",
|
||||
"Zach Charlop-Powers <zach.charlop.powers@gmail.com>",
|
||||
"Kim Juho <juho_kim@outlook.com>"
|
||||
"Kim Juho <juho_kim@outlook.com>",
|
||||
"Victoria Doshchenko <doshchenko.victoria@gmail.com>"
|
||||
],
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
@@ -203,4 +204,4 @@
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -182,6 +182,15 @@
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: #1d4ed7;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (orientation:portrait) {
|
||||
|
||||
@@ -22,7 +22,15 @@ const Steps = [
|
||||
header: 'Animation Demo',
|
||||
key: 'intro',
|
||||
description: `### Molecular Animation
|
||||
A story showcasing MolViewSpec animation capabilities.`,
|
||||
A story showcasing MolViewSpec animation capabilities.
|
||||
|
||||
[\[**🔄 Replay Intro**\]](!play-transition)
|
||||
[\[**⏵ Play Snapshots**\]](!play-snapshots)
|
||||
[\[**⏹ Stop Animation**\]](!stop-animation)
|
||||
|
||||
[\[**➡️ Next Snapshot**\]](!next-snapshot)
|
||||
|
||||
`,
|
||||
linger_duration_ms: 2000,
|
||||
transition_duration_ms: 500,
|
||||
state: (): Root => {
|
||||
@@ -41,7 +49,7 @@ A story showcasing MolViewSpec animation capabilities.`,
|
||||
custom: {
|
||||
molstar_trackball: {
|
||||
name: 'rock',
|
||||
params: { speed: 1.5 },
|
||||
params: { speed: 0.5 },
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -49,11 +57,23 @@ A story showcasing MolViewSpec animation capabilities.`,
|
||||
kind: 'scalar',
|
||||
ref: 'prims-opacity',
|
||||
target_ref: 'prims',
|
||||
duration_ms: 1000,
|
||||
start_ms: 500,
|
||||
duration_ms: 500,
|
||||
property: 'label_opacity',
|
||||
end: 1,
|
||||
});
|
||||
|
||||
anim.interpolate({
|
||||
kind: 'scalar',
|
||||
ref: 'prims-opacity',
|
||||
target_ref: 'prims',
|
||||
start_ms: 1500,
|
||||
duration_ms: 500,
|
||||
property: 'label_opacity',
|
||||
start: 1,
|
||||
end: 0.66,
|
||||
});
|
||||
|
||||
|
||||
// Uncomment this to make 2nd frame render much faster
|
||||
// It will cause shader compilation to happen during the 1st snapshot
|
||||
@@ -119,14 +139,32 @@ A story showcasing MolViewSpec animation capabilities.`,
|
||||
|
||||
anim.interpolate({
|
||||
kind: 'scalar',
|
||||
ref: 'clip-transition',
|
||||
target_ref: 'clip',
|
||||
duration_ms: 2000,
|
||||
duration_ms: 500,
|
||||
property: ['point', 2],
|
||||
end: 55,
|
||||
easing: 'sin-in',
|
||||
});
|
||||
|
||||
anim.interpolate({
|
||||
kind: 'scalar',
|
||||
target_ref: 'clip',
|
||||
start_ms: 600,
|
||||
duration_ms: 800,
|
||||
property: ['point', 2],
|
||||
end: 0,
|
||||
easing: 'sin-out',
|
||||
});
|
||||
|
||||
anim.interpolate({
|
||||
kind: 'scalar',
|
||||
target_ref: 'clip',
|
||||
start_ms: 1500,
|
||||
duration_ms: 500,
|
||||
property: ['point', 2],
|
||||
end: 55,
|
||||
});
|
||||
|
||||
anim.interpolate({
|
||||
kind: 'vec3',
|
||||
target_ref: 'xform',
|
||||
|
||||
@@ -22,15 +22,16 @@ const Steps = [
|
||||
{
|
||||
header: 'Audio Demo',
|
||||
key: 'intro',
|
||||
description: `### Audio Playback
|
||||
description: `
|
||||
**Audio**
|
||||
[‹ **▶ Play** ›](${encodeURIComponent(`!play-audio=${_Audio}`)})
|
||||
[‹ **⏸ Pause** ›](!pause-audio)
|
||||
[‹ **⏹ Stop** ›](!stop-audio)
|
||||
[‹ **Hide** ›](!dispose-audio)
|
||||
|
||||
### Audio Playback
|
||||
|
||||
A simple example showcasing audio playback.
|
||||
|
||||
Basic controls:
|
||||
|
||||
[Play](${encodeURIComponent(`!play-audio=${_Audio}`)})
|
||||
[Pause](!pause-audio)
|
||||
[Stop](!stop-audio)
|
||||
`,
|
||||
linger_duration_ms: 2000,
|
||||
transition_duration_ms: 500,
|
||||
@@ -50,7 +51,7 @@ Basic controls:
|
||||
}
|
||||
}
|
||||
});
|
||||
prims.label({ text: 'Click to Play', position: { label_asym_id: 'A' }, label_size: 10 });
|
||||
prims.label({ text: '▶ Click to Play', position: { label_asym_id: 'A' }, label_size: 10 });
|
||||
|
||||
return builder;
|
||||
},
|
||||
@@ -67,7 +68,7 @@ Basic controls:
|
||||
|
||||
If you clicked "Click to Play" in the previous snapshot, the audio should be playing now.
|
||||
|
||||
[Stop](!stop-audio)
|
||||
[Stop Audio](!stop-audio)
|
||||
`,
|
||||
linger_duration_ms: 2000,
|
||||
transition_duration_ms: 500,
|
||||
@@ -103,7 +104,7 @@ If you clicked "Click to Play" in the previous snapshot, the audio should be pla
|
||||
|
||||
If you browser security permissions allow it, the audio should start playing automatically when the snapshot gets loaded
|
||||
|
||||
[Stop](!stop-audio)
|
||||
[Stop Audio](!stop-audio)
|
||||
`,
|
||||
linger_duration_ms: 2000,
|
||||
transition_duration_ms: 500,
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
* @author Ludovic Autin <autin@scripps.edu>
|
||||
* @author Victoria Doshchenko <doshchenko.victoria@gmail.com>
|
||||
*/
|
||||
|
||||
import { decodeColor } from '../../../extensions/mvs/helpers/utils';
|
||||
@@ -84,15 +85,21 @@ const _Audio4 = audioPathBase + '/examples/audio/AudioMOM1_D.mp3';
|
||||
const q = (expr: string, lang = 'pymol') =>
|
||||
`!query=${encodeURIComponent(expr)}&lang=${lang}&action=highlight,focus`;
|
||||
|
||||
const description_intro = `
|
||||
# Molecule of the Month: Myoglobin
|
||||
const desc_intro = `
|
||||
# Introduction
|
||||
|
||||
A story based on the orginal [first Molecule of the Month](https://pdb101.rcsb.org/motm/1) made by David Goodsell in January 2000.
|
||||
|
||||
🔊 *This story includes short audio commentaries to guide you through the structures.*
|
||||
For the best experience, please keep your sound on or use headphones.
|
||||
|
||||
`;
|
||||
|
||||
const description_p0 = `
|
||||
# Molecule of the Month: Myoglobin
|
||||
|
||||
Basic controls for the audio comments:
|
||||
[Play](${encodeURIComponent(`!play-audio=${_Audio1}`)})
|
||||
[Pause](!pause-audio)
|
||||
[Stop](!stop-audio)
|
||||
${createAudioControls(_Audio1)}
|
||||
|
||||
Myoglobin was the first protein to have its atomic structure determined, revealing how it stores oxygen in muscle cells.
|
||||
|
||||
@@ -161,9 +168,7 @@ const charged_residues = q(formatMolScript(query3), 'mol-script');
|
||||
const description_p1 = `
|
||||
# Myoglobin and Whales
|
||||
Basic controls for the audio comments:
|
||||
[Play](${encodeURIComponent(`!play-audio=${_Audio2}`)})
|
||||
[Pause](!pause-audio)
|
||||
[Stop](!stop-audio)
|
||||
${createAudioControls(_Audio2)}
|
||||
|
||||
If you look at John Kendrew's PDB file, you'll notice that the myoglobin he used was taken
|
||||
from sperm whale muscles. Whales and dolphin have a great need for myoglobin, so that they can
|
||||
@@ -184,9 +189,7 @@ high concentrations.
|
||||
const description_p2 = `
|
||||
# Oxygen Bound to Myoglobin
|
||||
Basic controls for the audio comments:
|
||||
[Play](${encodeURIComponent(`!play-audio=${_Audio3}`)})
|
||||
[Pause](!pause-audio)
|
||||
[Stop](!stop-audio)
|
||||
${createAudioControls(_Audio3)}
|
||||
|
||||
A later structure of myoglobin, PDB entry [1mbo](https://www.rcsb.org/structure/1mbo),
|
||||
shows that [oxygen](${q('index 1276+1277')}) binds to
|
||||
@@ -202,9 +205,7 @@ appear and disappear, allowing oxygen in and out.
|
||||
const description_p3 = `
|
||||
# Molecule of the Month: Myoglobin
|
||||
Basic controls for the audio comments:
|
||||
[Play](${encodeURIComponent(`!play-audio=${_Audio1}`)})
|
||||
[Pause](!pause-audio)
|
||||
[Stop](!stop-audio)
|
||||
${createAudioControls(_Audio4)}
|
||||
|
||||
The atomic structure of myoglobin revealed many of the basic principles
|
||||
of protein structure and stability. For instance, the structure showed
|
||||
@@ -235,10 +236,67 @@ PDB entry [2jho](https://www.rcsb.org/structure/2jho) includes myoglobin poisone
|
||||
`;
|
||||
|
||||
const Steps = [
|
||||
{
|
||||
header: 'Introduction',
|
||||
key: 'first-slide',
|
||||
description: desc_intro,
|
||||
linger_duration_ms: 0,
|
||||
state: (): Root => {
|
||||
const builder = createMVSBuilder();
|
||||
const _1mbn = build1mbn(builder, '1MBN');
|
||||
|
||||
builder.extendRootCustomState({
|
||||
molstar_on_load_markdown_commands: {
|
||||
'dispose-audio': _Audio1,
|
||||
}
|
||||
});
|
||||
|
||||
const anim = builder.animation(
|
||||
{
|
||||
custom: {
|
||||
molstar_trackball: {
|
||||
name: 'spin',
|
||||
params: { speed: -0.05 },
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const prims = _1mbn.struct.primitives({
|
||||
ref: 'start-story',
|
||||
label_opacity: 0,
|
||||
label_background_color: 'grey',
|
||||
snapshot_key: 'intro'
|
||||
});
|
||||
prims.label({
|
||||
text: 'Start story',
|
||||
position: [13.5, -4, 7.7],
|
||||
label_size: 8
|
||||
});
|
||||
|
||||
anim.interpolate({
|
||||
kind: 'scalar',
|
||||
target_ref: 'start-story',
|
||||
duration_ms: 1000,
|
||||
start_ms: 1,
|
||||
property: 'label_opacity',
|
||||
start: 0.0,
|
||||
end: 1.0,
|
||||
});
|
||||
|
||||
|
||||
return builder;
|
||||
},
|
||||
camera: {
|
||||
position: [13.5, 21.1, 73.1],
|
||||
target: [13.5, 21.1, 7.7],
|
||||
up: [0, 1, 0],
|
||||
} satisfies MVSNodeParams<'camera'>,
|
||||
},
|
||||
{
|
||||
header: 'Molecule of the Month: Myoglobin',
|
||||
key: 'intro',
|
||||
description: description_intro,
|
||||
description: description_p0,
|
||||
linger_duration_ms: 45000,
|
||||
transition_duration_ms: 500,
|
||||
state: (): Root => {
|
||||
@@ -247,39 +305,10 @@ const Steps = [
|
||||
|
||||
builder.canvas({ custom: { molstar_postprocessing: { enable_outline: false } } });
|
||||
|
||||
const _1mbn = structure(builder, '1MBN');
|
||||
|
||||
_1mbn.component({ selector: 'ligand' })
|
||||
.representation({ ref: 'ligand', type: 'ball_and_stick' })
|
||||
.color({ color: 'orange' });
|
||||
// FE and O should be spacefill
|
||||
|
||||
_1mbn.component({ selector: { auth_seq_id: 155, label_atom_id: 'F' } })
|
||||
.representation({ type: 'spacefill' })
|
||||
.color({ color: 'yellow' });
|
||||
|
||||
_1mbn.component({ selector: { auth_seq_id: 154 } })
|
||||
.representation({ type: 'spacefill' })
|
||||
.color({ color: 'blue' });
|
||||
|
||||
_1mbn.component({ selector: { auth_seq_id: 154 } })
|
||||
.representation({ type: 'spacefill' })
|
||||
.color({ color: 'blue' });
|
||||
|
||||
const chA = _1mbn.component({ selector: { label_asym_id: 'A' } });
|
||||
chA.representation({ type: 'surface', surface_type: 'gaussian' })
|
||||
.color({ color: '#ff0303' })
|
||||
.opacity({ ref: 'surfopa', opacity: 0.0 });
|
||||
|
||||
chA.representation({ type: 'line' })
|
||||
.color({ custom: { molstar_color_theme_name: 'element-symbol' } })
|
||||
.opacity({ ref: 'lineopa', opacity: 0.0 });
|
||||
|
||||
chA.representation({ type: 'cartoon' })
|
||||
.color({ custom: { molstar_color_theme_name: 'secondary-structure' } });
|
||||
const _1mbn = build1mbn(builder, '1MBN');
|
||||
|
||||
// whale
|
||||
_1mbn.component({ selector: { label_asym_id: 'A' } })
|
||||
_1mbn.struct.component({ selector: { label_asym_id: 'A' } })
|
||||
.representation({ type: 'spacefill', custom: { molstar_representation_params: { ignoreLight: true } } })
|
||||
.colorFromSource({
|
||||
schema: 'all_atomic',
|
||||
@@ -296,29 +325,12 @@ const Steps = [
|
||||
}
|
||||
}).opacity({ ref: 'cpkopa1', opacity: 0.0 });
|
||||
|
||||
_1mbn.component({ selector: { auth_seq_id: 155 } })
|
||||
_1mbn.struct.component({ selector: { auth_seq_id: 155 } })
|
||||
.representation({ type: 'spacefill', custom: { molstar_representation_params: { ignoreLight: true } } })
|
||||
.color({ custom: GColors2 }).opacity({ ref: 'cpkopa2', opacity: 0.0 });
|
||||
|
||||
const prims = _1mbn.primitives({
|
||||
ref: 'prims',
|
||||
label_opacity: 1,
|
||||
label_background_color: 'grey',
|
||||
custom: {
|
||||
molstar_markdown_commands: {
|
||||
// 'apply-snapshot': 'interlude',
|
||||
'play-audio': _Audio1,
|
||||
}
|
||||
}
|
||||
});
|
||||
prims.label({
|
||||
text: 'Start Comments',
|
||||
position: [13.5, 45.1, 7.7],
|
||||
label_size: 5
|
||||
});
|
||||
addNextButton(builder, 'whale', [13.5, 0, 7.7]);
|
||||
addNextButton(builder, 'whale', [13.5, -4, 7.7]);
|
||||
|
||||
// doesnt work for first slide, but work afterward
|
||||
builder.extendRootCustomState({
|
||||
molstar_on_load_markdown_commands: {
|
||||
'play-audio': _Audio1,
|
||||
@@ -543,7 +555,7 @@ const Steps = [
|
||||
start: 0.0,
|
||||
end: 1.0,
|
||||
});
|
||||
addNextButton(builder, 'oxygen', [-18.9, 10, 7.3]);
|
||||
addNextButton(builder, 'oxygen', [-18.9, -4, 7.3]);
|
||||
anim.interpolate({
|
||||
kind: 'scalar',
|
||||
target_ref: 'next',
|
||||
@@ -897,7 +909,7 @@ function addNextButton(builder: any, snapshotKey: string, position: [number, num
|
||||
.label({
|
||||
ref: 'next_label',
|
||||
position: position,
|
||||
text: 'Click me to go next',
|
||||
text: 'Next Scene →',
|
||||
label_color: 'white',
|
||||
label_size: 5
|
||||
});
|
||||
@@ -955,4 +967,54 @@ export function buildStory(): MVSData_States {
|
||||
timestamp: new Date().toISOString(),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function build1mbn(builder: any, pdbId: string) {
|
||||
const struct = structure(builder, '1MBN');
|
||||
|
||||
struct.component({ selector: 'ligand' })
|
||||
.representation({ ref: 'ligand', type: 'ball_and_stick' })
|
||||
.color({ color: 'orange' });
|
||||
|
||||
// FE and O should be spacefill
|
||||
struct.component({ selector: { auth_seq_id: 155, label_atom_id: 'FE' } })
|
||||
.representation({ type: 'spacefill' })
|
||||
.color({ color: 'yellow' });
|
||||
|
||||
struct.component({ selector: { auth_seq_id: 154 } })
|
||||
.representation({ type: 'spacefill' })
|
||||
.color({ color: 'blue' });
|
||||
|
||||
struct.component({ selector: { auth_seq_id: 154 } })
|
||||
.representation({ type: 'spacefill' })
|
||||
.color({ color: 'blue' });
|
||||
|
||||
const chA = struct.component({ selector: { label_asym_id: 'A' } });
|
||||
chA.representation({ type: 'surface', surface_type: 'gaussian' })
|
||||
.color({ color: '#ff0303' })
|
||||
.opacity({ ref: 'surfopa', opacity: 0.0 });
|
||||
|
||||
chA.representation({ type: 'line' })
|
||||
.color({ custom: { molstar_color_theme_name: 'element-symbol' } })
|
||||
.opacity({ ref: 'lineopa', opacity: 0.0 });
|
||||
|
||||
chA.representation({ type: 'cartoon' })
|
||||
.color({ custom: { molstar_color_theme_name: 'secondary-structure' } });
|
||||
|
||||
return {
|
||||
struct,
|
||||
refs: {
|
||||
surfaceOpacity: 'surfopa',
|
||||
lineOpacity: 'lineopa',
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function createAudioControls(url: string) {
|
||||
return `
|
||||
[‹ **▶ Play** ›](${encodeURIComponent(`!play-audio=${url}`)})
|
||||
[‹ **⏸ Pause** ›](!pause-audio)
|
||||
[‹ **⏹ Stop** ›](!stop-audio)
|
||||
[‹ **Hide** ›](!dispose-audio)
|
||||
`;
|
||||
}
|
||||
@@ -5,21 +5,22 @@
|
||||
* @author Ludovic Autin <ludovic.autin@gmail.com>
|
||||
*/
|
||||
|
||||
import { Snapshot } from '../mvs-data';
|
||||
import { Tree } from '../tree/generic/tree-schema';
|
||||
import { clamp, lerp } from '../../../mol-math/interpolate';
|
||||
import { MVSAnimationEasing, MVSAnimationNode, MVSAnimationSchema } from '../tree/animation/animation-tree';
|
||||
import { MVSTree } from '../tree/mvs/mvs-tree';
|
||||
import { SortedArray } from '../../../mol-data/int';
|
||||
import * as EasingFns from '../../../mol-math/easing';
|
||||
import { addDefaults } from '../tree/generic/tree-utils';
|
||||
import { RuntimeContext } from '../../../mol-task';
|
||||
import { clamp, lerp } from '../../../mol-math/interpolate';
|
||||
import { EPSILON, Mat3, Mat4, Quat, Vec3 } from '../../../mol-math/linear-algebra';
|
||||
import { RuntimeContext } from '../../../mol-task';
|
||||
import { deepEqual } from '../../../mol-util';
|
||||
import { Color } from '../../../mol-util/color';
|
||||
import { produce } from '../../../mol-util/produce';
|
||||
import { makeContinuousPaletteCheckpoints, MVSContinuousPaletteProps, MVSDiscretePaletteProps } from '../components/annotation-color-theme';
|
||||
import { palettePropsFromMVSPalette } from '../load-helpers';
|
||||
import { SortedArray } from '../../../mol-data/int';
|
||||
import { Snapshot } from '../mvs-data';
|
||||
import { MVSAnimationEasing, MVSAnimationNode, MVSAnimationSchema } from '../tree/animation/animation-tree';
|
||||
import { Tree } from '../tree/generic/tree-schema';
|
||||
import { addDefaults } from '../tree/generic/tree-utils';
|
||||
import { MVSTree } from '../tree/mvs/mvs-tree';
|
||||
import { ColorT } from '../tree/mvs/param-types';
|
||||
import { produce } from '../../../mol-util/produce';
|
||||
|
||||
export async function generateStateTransition(ctx: RuntimeContext, snapshot: Snapshot, snapshotIndex: number, snapshotCount: number) {
|
||||
if (!snapshot.animation) return undefined;
|
||||
@@ -33,17 +34,27 @@ export async function generateStateTransition(ctx: RuntimeContext, snapshot: Sna
|
||||
...transitions.map(t => (t.params.start_ms ?? 0) + t.params.duration_ms)
|
||||
);
|
||||
|
||||
const frames: MVSTree[] = [];
|
||||
const frames: [tree: MVSTree, time: number][] = [];
|
||||
const dt = tree.params?.frame_time_ms ?? (1000 / 60);
|
||||
const N = Math.ceil(duration / dt);
|
||||
|
||||
const nodeMap = makeNodeMap(snapshot.root, new Map(), []);
|
||||
const cache = new Map<any, InterpolationCacheEntry>();
|
||||
|
||||
const transitionGroups = groupTranstions(transitions);
|
||||
|
||||
let prevRoot: MVSTree | undefined;
|
||||
for (let i = 0; i <= N; i++) {
|
||||
const t = i * dt;
|
||||
const root = createSnapshot(snapshot.root, transitions, t, cache, nodeMap);
|
||||
frames.push(root);
|
||||
const root = createSnapshot(snapshot.root, transitionGroups, t, cache, nodeMap);
|
||||
|
||||
if (root === prevRoot || (prevRoot && deepEqual(root, prevRoot))) {
|
||||
frames[frames.length - 1][1] += dt;
|
||||
} else {
|
||||
frames.push([root, dt]);
|
||||
}
|
||||
|
||||
prevRoot = root;
|
||||
|
||||
if (ctx.shouldUpdate) {
|
||||
await ctx.update({ message: `Generating transition for snapshot ${snapshotIndex + 1}/${snapshotCount}`, current: i + 1, max: N });
|
||||
@@ -80,71 +91,91 @@ interface InterpolationCacheEntry {
|
||||
rotation?: { axis: Vec3, angle: number, start: Quat, end: Quat },
|
||||
}
|
||||
|
||||
function createSnapshot(tree: MVSTree, transitions: MVSAnimationNode<'interpolate'>[], time: number, cache: Map<any, InterpolationCacheEntry>, nodeMap: Map<string, (string | number)[]>) {
|
||||
return produce(tree, (draft) => {
|
||||
for (const transition of transitions) {
|
||||
const nodePath = nodeMap.get(transition.params.target_ref);
|
||||
function getTransitionKey(transition: MVSAnimationNode<'interpolate'>) {
|
||||
const prop = transition.params.property;
|
||||
if (Array.isArray(prop)) {
|
||||
return `${transition.params.target_ref}:${prop.join('.')}`;
|
||||
}
|
||||
return `${transition.params.target_ref}:${prop}`;
|
||||
}
|
||||
|
||||
function groupTranstions(transitions: MVSAnimationNode<'interpolate'>[]) {
|
||||
const map = new Map<string, MVSAnimationNode<'interpolate'>[]>();
|
||||
const groups: MVSAnimationNode<'interpolate'>[][] = [];
|
||||
for (const t of transitions) {
|
||||
const key = getTransitionKey(t);
|
||||
if (!map.has(key)) {
|
||||
const group: MVSAnimationNode<'interpolate'>[] = [];
|
||||
map.set(key, group);
|
||||
groups.push(group);
|
||||
}
|
||||
map.get(key)!.push(t);
|
||||
}
|
||||
for (const group of groups) {
|
||||
group.sort((a, b) => {
|
||||
const s = (a.params.start_ms ?? 0) - (b.params.start_ms ?? 0);
|
||||
if (s !== 0) return s;
|
||||
return a.params.duration_ms - b.params.duration_ms;
|
||||
});
|
||||
}
|
||||
return groups;
|
||||
}
|
||||
|
||||
function createSnapshot(tree: MVSTree, transitionGroups: MVSAnimationNode<'interpolate'>[][], time: number, cache: Map<any, InterpolationCacheEntry>, nodeMap: Map<string, (string | number)[]>) {
|
||||
let modified = false;
|
||||
const ret = produce(tree, (draft) => {
|
||||
for (const transitionGroup of transitionGroups) {
|
||||
|
||||
const pivot = transitionGroup[0];
|
||||
const nodePath = nodeMap.get(pivot.params.target_ref);
|
||||
if (!nodePath) continue;
|
||||
|
||||
const node = select(draft, nodePath, 0);
|
||||
const target = transition.params.property[0] === 'custom' ? node?.custom : node?.params;
|
||||
const target = pivot.params.property[0] === 'custom' ? node?.custom : node?.params;
|
||||
if (!target) continue;
|
||||
|
||||
|
||||
const offset = pivot.params.property[0] === 'custom' ? 1 : 0;
|
||||
|
||||
let transition: MVSAnimationNode<'interpolate'> = pivot;
|
||||
let previous: MVSAnimationNode<'interpolate'> | undefined;
|
||||
|
||||
for (let i = transitionGroup.length - 1; i > 0; i--) {
|
||||
const current = transitionGroup[i];
|
||||
const currentStart = current.params.start_ms ?? 0;
|
||||
if (time >= currentStart) {
|
||||
transition = current;
|
||||
previous = i > 0 ? transitionGroup[i - 1] : undefined;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!cache.has(transition)) {
|
||||
cache.set(transition, {});
|
||||
}
|
||||
|
||||
const cacheEntry: InterpolationCacheEntry = cache.get(transition)!;
|
||||
|
||||
const startTime = transition.params.start_ms ?? 0;
|
||||
let t = clamp((time - startTime) / transition.params.duration_ms, 0, 1);
|
||||
|
||||
if (transition.params.kind === 'transform_matrix') {
|
||||
processTransformMatrix(transition, target, t, cacheEntry);
|
||||
continue;
|
||||
}
|
||||
|
||||
t = applyFrequency(t, transition.params.frequency ?? 1, !!transition.params.alternate_direction);
|
||||
|
||||
const offset = transition.params.property[0] === 'custom' ? 1 : 0;
|
||||
const startBase = transition.params.start ?? select(target, transition.params.property, offset);
|
||||
|
||||
if (transition.params.kind === 'color' && !cacheEntry.paletteFn) {
|
||||
cacheEntry.paletteFn = makePaletteFunction(transition, startBase, transition.params.end as ColorT | undefined);
|
||||
}
|
||||
|
||||
const paletteFn = cacheEntry.paletteFn!;
|
||||
|
||||
const startValue: any = transition.params.kind === 'color'
|
||||
? Color.toHexStyle(paletteFn(0))
|
||||
: startBase;
|
||||
const endValue: any = transition.params.kind === 'color'
|
||||
? Color.toHexStyle(paletteFn(1))
|
||||
: transition.params.end;
|
||||
|
||||
if (time <= startTime) {
|
||||
assign(target, transition.params.property, startValue, offset);
|
||||
continue;
|
||||
}
|
||||
|
||||
const easing = EasingFnMap[transition.params.easing ?? 'linear'] ?? EasingFnMap['linear'];
|
||||
t = easing(t);
|
||||
const startTime: number = transition.params.start_ms ?? 0;
|
||||
const durationMs: number = transition.params.duration_ms ?? 0;
|
||||
const t = (time - startTime) / durationMs;
|
||||
|
||||
let next: any;
|
||||
if (transition.params.kind === 'scalar') {
|
||||
next = interpolateScalars(startValue, endValue, t, transition.params.noise_magnitude ?? 0, !!transition.params.discrete);
|
||||
} else if (transition.params.kind === 'vec3') {
|
||||
next = interpolateVectors(startValue, endValue, t, transition.params.noise_magnitude ?? 0, !!transition.params.spherical);
|
||||
} else if (transition.params.kind === 'rotation_matrix') {
|
||||
next = interpolateRotation(startValue, endValue, t, transition.params.noise_magnitude ?? 0, cacheEntry);
|
||||
} else if (transition.params.kind === 'color') {
|
||||
const color = paletteFn(t);
|
||||
next = Color.toHexStyle(color);
|
||||
if (transition.params.kind === 'transform_matrix') {
|
||||
next = processTransformMatrix(transition, target, clamp(t, 0, 1), cacheEntry, offset, previous);
|
||||
} else {
|
||||
next = processScalarLike(transition, target, t, cacheEntry, offset, previous);
|
||||
}
|
||||
|
||||
if (next === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
modified = true;
|
||||
assign(target, transition.params.property, next, offset);
|
||||
}
|
||||
});
|
||||
return modified ? ret : tree;
|
||||
}
|
||||
|
||||
function applyFrequency(t: number, frequency: number, alternate: boolean) {
|
||||
@@ -163,6 +194,55 @@ function applyFrequency(t: number, frequency: number, alternate: boolean) {
|
||||
return v;
|
||||
}
|
||||
|
||||
function getPreviousScalarEnd(previous: MVSAnimationNode<'interpolate'> | undefined) {
|
||||
if (!previous || previous.params.kind === 'transform_matrix') return undefined;
|
||||
return previous.params.end;
|
||||
}
|
||||
|
||||
function processScalarLike(transition: MVSAnimationNode<'interpolate'>, target: any, time: number, cacheEntry: InterpolationCacheEntry, offset: number, previous: MVSAnimationNode<'interpolate'> | undefined) {
|
||||
if (transition.params.kind === 'transform_matrix') return;
|
||||
if (previous && previous.params.kind === 'transform_matrix') return;
|
||||
|
||||
const startBase = transition.params.start ?? getPreviousScalarEnd(previous) ?? select(target, transition.params.property, offset);
|
||||
if (transition.params.kind === 'color' && !cacheEntry.paletteFn) {
|
||||
cacheEntry.paletteFn = makePaletteFunction(transition, startBase, transition.params.end as ColorT | undefined);
|
||||
}
|
||||
|
||||
const paletteFn = cacheEntry.paletteFn!;
|
||||
|
||||
const startValue: any = transition.params.kind === 'color'
|
||||
? Color.toHexStyle(paletteFn(0))
|
||||
: startBase;
|
||||
const endValue: any = transition.params.kind === 'color'
|
||||
? Color.toHexStyle(paletteFn(1))
|
||||
: transition.params.end;
|
||||
|
||||
if (time <= 0) return startValue;
|
||||
else if (time >= 1 - EPSILON && !transition.params.alternate_direction) return endValue;
|
||||
|
||||
let t = clamp(time, 0, 1);
|
||||
t = applyFrequency(t, transition.params.frequency ?? 1, !!transition.params.alternate_direction);
|
||||
|
||||
const easing = EasingFnMap[transition.params.easing ?? 'linear'] ?? EasingFnMap['linear'];
|
||||
t = easing(t);
|
||||
|
||||
if (transition.params.kind === 'scalar') {
|
||||
return interpolateScalars(startValue, endValue, t, transition.params.noise_magnitude ?? 0, !!transition.params.discrete);
|
||||
} else if (transition.params.kind === 'vec3') {
|
||||
return interpolateVectors(startValue, endValue, t, transition.params.noise_magnitude ?? 0, !!transition.params.spherical);
|
||||
} else if (transition.params.kind === 'rotation_matrix') {
|
||||
return interpolateRotation(startValue, endValue, t, transition.params.noise_magnitude ?? 0, cacheEntry);
|
||||
} else if (transition.params.kind === 'color') {
|
||||
const color = paletteFn(t);
|
||||
return Color.toHexStyle(color);
|
||||
}
|
||||
}
|
||||
|
||||
function getPreviousMatrixEnd(previous: MVSAnimationNode<'interpolate'> | undefined, prop: 'rotation_start' | 'translation_start' | 'scale_start') {
|
||||
if (!previous || previous.params.kind !== 'transform_matrix') return undefined;
|
||||
return previous.params[prop];
|
||||
}
|
||||
|
||||
const TransformState = {
|
||||
pivotTranslation: Mat4(),
|
||||
pivotTranslationInv: Mat4(),
|
||||
@@ -172,31 +252,41 @@ const TransformState = {
|
||||
pivotNeg: Vec3(),
|
||||
temp: Mat4(),
|
||||
};
|
||||
function processTransformMatrix(transition: MVSAnimationNode<'interpolate'>, target: any, time: number, cache: InterpolationCacheEntry) {
|
||||
function processTransformMatrix(transition: MVSAnimationNode<'interpolate'>, target: any, time: number, cache: InterpolationCacheEntry, offset: number, previous: MVSAnimationNode<'interpolate'> | undefined) {
|
||||
if (transition.params.kind !== 'transform_matrix') return;
|
||||
if (previous && previous.params.kind !== 'transform_matrix') return;
|
||||
|
||||
const offset = transition.params.property[0] === 'custom' ? 1 : 0;
|
||||
const transform = select(target, transition.params.property, offset) ?? Mat4.identity();
|
||||
|
||||
const startRotation = transition.params.rotation_start ?? Mat3.fromMat4(Mat3(), transform);
|
||||
const startTranslation = transition.params.translation_start ?? Mat4.getTranslation(Vec3(), transform);
|
||||
const startScale = transition.params.scale_start ?? Mat4.getScaling(Vec3(), transform);
|
||||
const startRotation = transition.params.rotation_start ?? getPreviousMatrixEnd(previous, 'rotation_start') ?? Mat3.fromMat4(Mat3(), transform);
|
||||
const startTranslation = transition.params.translation_start ?? getPreviousMatrixEnd(previous, 'translation_start') ?? Mat4.getTranslation(Vec3(), transform);
|
||||
const startScale = transition.params.scale_start ?? getPreviousMatrixEnd(previous, 'scale_start') ?? Mat4.getScaling(Vec3(), transform);
|
||||
|
||||
const endRotation = transition.params.rotation_end;
|
||||
const endTranslation = transition.params.translation_end;
|
||||
const endScale = transition.params.scale_end;
|
||||
|
||||
let t = applyFrequency(time, transition.params.rotation_frequency ?? 1, !!transition.params.rotation_alternate_direction);
|
||||
let easing = EasingFnMap[transition.params.rotation_easing ?? 'linear'] ?? EasingFnMap['linear'];
|
||||
const rotation = interpolateRotation(startRotation as Mat3, endRotation as Mat3, easing(t), transition.params.rotation_noise_magnitude ?? 0, cache);
|
||||
let rotation, translation, scale;
|
||||
|
||||
t = applyFrequency(time, transition.params.translation_frequency ?? 1, !!transition.params.translation_alternate_direction);
|
||||
easing = EasingFnMap[transition.params.translation_easing ?? 'linear'] ?? EasingFnMap['linear'];
|
||||
const translation = interpolateVec3(startTranslation as Vec3, endTranslation as Vec3 | undefined, easing(t), transition.params.translation_noise_magnitude ?? 0, false);
|
||||
if (time <= 0) {
|
||||
rotation = startRotation as Mat3;
|
||||
translation = startTranslation as Vec3;
|
||||
scale = startScale as Vec3;
|
||||
} else {
|
||||
const clampedTime = clamp(time, 0, 1);
|
||||
|
||||
t = applyFrequency(time, transition.params.scale_frequency ?? 1, !!transition.params.scale_alternate_direction);
|
||||
easing = EasingFnMap[transition.params.scale_easing ?? 'linear'] ?? EasingFnMap['linear'];
|
||||
const scale = interpolateVec3(startScale as Vec3, endScale as Vec3 | undefined, easing(t), transition.params.scale_noise_magnitude ?? 0, false);
|
||||
let t = applyFrequency(clampedTime, transition.params.rotation_frequency ?? 1, !!transition.params.rotation_alternate_direction);
|
||||
let easing = EasingFnMap[transition.params.rotation_easing ?? 'linear'] ?? EasingFnMap['linear'];
|
||||
rotation = interpolateRotation(startRotation as Mat3, endRotation as Mat3, easing(t), transition.params.rotation_noise_magnitude ?? 0, cache);
|
||||
|
||||
t = applyFrequency(clampedTime, transition.params.translation_frequency ?? 1, !!transition.params.translation_alternate_direction);
|
||||
easing = EasingFnMap[transition.params.translation_easing ?? 'linear'] ?? EasingFnMap['linear'];
|
||||
translation = interpolateVec3(startTranslation as Vec3, endTranslation as Vec3 | undefined, easing(t), transition.params.translation_noise_magnitude ?? 0, false);
|
||||
|
||||
t = applyFrequency(clampedTime, transition.params.scale_frequency ?? 1, !!transition.params.scale_alternate_direction);
|
||||
easing = EasingFnMap[transition.params.scale_easing ?? 'linear'] ?? EasingFnMap['linear'];
|
||||
scale = interpolateVec3(startScale as Vec3, endScale as Vec3 | undefined, easing(t), transition.params.scale_noise_magnitude ?? 0, false);
|
||||
}
|
||||
|
||||
const pivot = transition.params.pivot ?? Vec3.zero();
|
||||
|
||||
@@ -213,7 +303,7 @@ function processTransformMatrix(transition: MVSAnimationNode<'interpolate'>, tar
|
||||
Mat4.mul(result, TransformState.rotation, result);
|
||||
Mat4.mul(result, TransformState.translation, result);
|
||||
|
||||
assign(target, transition.params.property, result, offset);
|
||||
return result;
|
||||
}
|
||||
|
||||
function interpolateScalars(start: number | number[], end: number | number[] | undefined, t: number, noise: number, discrete: boolean) {
|
||||
|
||||
@@ -136,7 +136,7 @@ async function assignStateTransition(ctx: RuntimeContext, plugin: PluginContext,
|
||||
|
||||
for (let i = 0; i < transitions.frames.length; i++) {
|
||||
const frame = transitions.frames[i];
|
||||
const molstarTree = convertMvsToMolstar(frame, options.sourceUrl);
|
||||
const molstarTree = convertMvsToMolstar(frame[0], options.sourceUrl);
|
||||
const entry = molstarTreeToEntry(
|
||||
plugin,
|
||||
molstarTree,
|
||||
@@ -148,7 +148,7 @@ async function assignStateTransition(ctx: RuntimeContext, plugin: PluginContext,
|
||||
StateTree.reuseTransformParams(entry.snapshot.data!.tree, parentEntry.snapshot.data!.tree);
|
||||
|
||||
animation.frames.push({
|
||||
durationInMs: transitions.frametimeMs,
|
||||
durationInMs: frame[1],
|
||||
data: entry.snapshot.data!,
|
||||
camera: transitions.tree.params?.include_camera ? entry.snapshot.camera : undefined,
|
||||
canvas3d: transitions.tree.params?.include_canvas ? entry.snapshot.canvas3d : undefined,
|
||||
|
||||
@@ -175,7 +175,7 @@ export const AnimateStateSnapshotTransition = PluginStateAnimation.create({
|
||||
if (t.current >= animState.totalDuration) {
|
||||
if (snapshot?.transition && animState.isInitial) {
|
||||
const frameIndex = snapshot.transition.frames.length - 1;
|
||||
ctx.plugin.managers.snapshot.setSnapshotAnimationFrame(frameIndex, false);
|
||||
ctx.plugin.managers.snapshot.setSnapshotAnimationFrame(animState.totalDuration, false);
|
||||
await setPartialSnapshot(ctx.plugin, snapshot.transition.frames[frameIndex]);
|
||||
}
|
||||
return { kind: 'finished' };
|
||||
@@ -193,7 +193,7 @@ export const AnimateStateSnapshotTransition = PluginStateAnimation.create({
|
||||
return { kind: 'skip' };
|
||||
}
|
||||
|
||||
ctx.plugin.managers.snapshot.setSnapshotAnimationFrame(frameIndex, false);
|
||||
ctx.plugin.managers.snapshot.setSnapshotAnimationFrame(t.current, false);
|
||||
if (frameIndex === 0) {
|
||||
await setPartialSnapshot(ctx.plugin, {
|
||||
...transition.frames[frameIndex],
|
||||
|
||||
@@ -11,6 +11,7 @@ import { PluginContext } from '../../mol-plugin/context';
|
||||
import { Script } from '../../mol-script/script';
|
||||
import { QueryContext, QueryFn, StructureElement, StructureSelection } from '../../mol-model/structure';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { AnimateStateSnapshotTransition } from '../animation/built-in/state-snapshots';
|
||||
|
||||
export type MarkdownExtensionEvent = 'click' | 'mouse-enter' | 'mouse-leave';
|
||||
|
||||
@@ -46,6 +47,17 @@ export const BuiltInMarkdownExtension: MarkdownExtension[] = [
|
||||
manager.plugin.managers.snapshot.applyKey(key);
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'next-snapshot',
|
||||
execute: ({ event, args, manager }) => {
|
||||
if (event !== 'click' || !('next-snapshot' in args)) return;
|
||||
let dir: -1 | 1 = (+args['next-snapshot'] || 1) as -1 | 1;
|
||||
if (!dir) return;
|
||||
if (dir < 0) dir = -1;
|
||||
else dir = 1;
|
||||
manager.plugin.managers.snapshot.applyNext(dir);
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'focus-refs',
|
||||
execute: ({ event, args, manager }) => {
|
||||
@@ -185,6 +197,34 @@ export const BuiltInMarkdownExtension: MarkdownExtension[] = [
|
||||
manager.audio.stop();
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'dispose-audio',
|
||||
execute: ({ event, args, manager }) => {
|
||||
if (event !== 'click' || !('dispose-audio' in args)) return;
|
||||
manager.audio.dispose();
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'play-transition',
|
||||
execute: ({ event, args, manager }) => {
|
||||
if (event !== 'click' || !('play-transition' in args)) return;
|
||||
manager.plugin.managers.animation.play(AnimateStateSnapshotTransition, {});
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'play-snapshots',
|
||||
execute: ({ event, args, manager }) => {
|
||||
if (event !== 'click' || !('play-snapshots' in args)) return;
|
||||
manager.plugin.managers.snapshot.play({ restart: true });
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'stop-animation',
|
||||
execute: ({ event, args, manager }) => {
|
||||
if (event !== 'click' || !('stop-animation' in args)) return;
|
||||
manager.plugin.managers.snapshot.stop();
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
export class MarkdownExtensionManager {
|
||||
|
||||
@@ -25,7 +25,7 @@ export { PluginStateSnapshotManager };
|
||||
|
||||
interface StateManagerState {
|
||||
current?: UUID,
|
||||
currentAnimationFrame?: number,
|
||||
currentAnimationTimeMs?: number,
|
||||
entries: List<PluginStateSnapshotManager.Entry>,
|
||||
isPlaying: boolean,
|
||||
nextSnapshotDelayInMs: number
|
||||
@@ -38,8 +38,8 @@ class PluginStateSnapshotManager extends StatefulPluginComponent<StateManagerSta
|
||||
private defaultSnapshotId: UUID | undefined = undefined;
|
||||
|
||||
protected updateState(state: Partial<StateManagerState>) {
|
||||
if ('current' in state && !('curentAnimationFrame' in state)) {
|
||||
return super.updateState({ ...state, currentAnimationFrame: 0 });
|
||||
if ('current' in state && !('currentAnimationTimeMs' in state)) {
|
||||
return super.updateState({ ...state, currentAnimationTimeMs: 0 });
|
||||
} else {
|
||||
return super.updateState(state);
|
||||
}
|
||||
@@ -168,18 +168,23 @@ class PluginStateSnapshotManager extends StatefulPluginComponent<StateManagerSta
|
||||
}
|
||||
|
||||
private animationFrameQueue = new SingleTaskQueue();
|
||||
setSnapshotAnimationFrame(frame: number, load = false) {
|
||||
if (this.updateState({ currentAnimationFrame: frame })) {
|
||||
setSnapshotAnimationFrame(currentAnimationTimeMs: number, load = false) {
|
||||
const entry = this.getEntry(this.state.current);
|
||||
if (!entry) return;
|
||||
|
||||
const frameIndex = PluginState.getStateTransitionFrameIndex(entry.snapshot, currentAnimationTimeMs) ?? 0;
|
||||
|
||||
if (this.updateState({ currentAnimationTimeMs })) {
|
||||
this.events.changed.next(void 0);
|
||||
}
|
||||
|
||||
if (load) {
|
||||
this.animationFrameQueue.run(() => {
|
||||
const entry = this.getEntry(this.state.current);
|
||||
if (!entry) return Promise.resolve();
|
||||
return this.plugin.state.setAnimationSnapshot(entry.snapshot, frame);
|
||||
return this.plugin.state.setAnimationSnapshot(entry.snapshot, frameIndex ?? 0);
|
||||
});
|
||||
}
|
||||
|
||||
return frameIndex;
|
||||
}
|
||||
|
||||
getNextId(id: string | undefined, dir: -1 | 1) {
|
||||
@@ -233,7 +238,7 @@ class PluginStateSnapshotManager extends StatefulPluginComponent<StateManagerSta
|
||||
const next = entry && entry.snapshot;
|
||||
if (!next) return;
|
||||
await this.plugin.state.setSnapshot(next);
|
||||
if (snapshot.playback && snapshot.playback.isPlaying) this.play(true);
|
||||
if (snapshot.playback?.isPlaying) this.play({ delayFirst: true });
|
||||
return next;
|
||||
}
|
||||
|
||||
@@ -380,10 +385,18 @@ class PluginStateSnapshotManager extends StatefulPluginComponent<StateManagerSta
|
||||
}
|
||||
}
|
||||
|
||||
play(delayFirst: boolean = false) {
|
||||
async play(options?: { delayFirst?: boolean, restart?: boolean }) {
|
||||
if (this.state.isPlaying && !options?.delayFirst) {
|
||||
if (options?.restart) {
|
||||
await this.stop();
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.updateState({ isPlaying: true });
|
||||
|
||||
if (delayFirst) {
|
||||
if (options?.delayFirst) {
|
||||
const e = this.getEntry(this.state.current);
|
||||
if (!e) {
|
||||
this.next();
|
||||
@@ -398,21 +411,20 @@ class PluginStateSnapshotManager extends StatefulPluginComponent<StateManagerSta
|
||||
}
|
||||
}
|
||||
|
||||
stop() {
|
||||
this.plugin.managers.animation.stop();
|
||||
async stop() {
|
||||
await this.plugin.managers.animation.stop();
|
||||
this.updateState({ isPlaying: false });
|
||||
if (typeof this.timeoutHandle !== 'undefined') clearTimeout(this.timeoutHandle);
|
||||
this.timeoutHandle = void 0;
|
||||
this.events.changed.next(void 0);
|
||||
}
|
||||
|
||||
togglePlay() {
|
||||
async togglePlay() {
|
||||
if (this.state.isPlaying) {
|
||||
this.stop();
|
||||
this.plugin.managers.animation.stop();
|
||||
await this.stop();
|
||||
this.plugin.managers.markdownExtensions.audio.pause();
|
||||
} else {
|
||||
this.play();
|
||||
await this.play();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import { StateTransformParameters } from './state/common';
|
||||
|
||||
export class PluginUIContext extends PluginContext {
|
||||
readonly customParamEditors = new Map<string, StateTransformParameters.Class>();
|
||||
readonly customUIState: Record<string, any> = {};
|
||||
|
||||
private initCustomParamEditors() {
|
||||
if (!this.spec.customParamEditors) return;
|
||||
|
||||
@@ -29,6 +29,7 @@ import { StructureQuickStylesControls } from './structure/quick-styles';
|
||||
import { Markdown } from './controls/markdown';
|
||||
import { Slider } from './controls/slider';
|
||||
import { AnimateStateSnapshotTransition } from '../mol-plugin-state/animation/built-in/state-snapshots';
|
||||
import { PluginState } from '../mol-plugin/state';
|
||||
|
||||
export class TrajectoryViewportControls extends PluginUIComponent<{}, { show: boolean, label: string }> {
|
||||
state = { show: false, label: '' };
|
||||
@@ -191,14 +192,12 @@ export class StateSnapshotViewportControls extends PluginUIComponent<{}, { isBus
|
||||
{hasAnimation && this.state.showAnimation && !isPlaying && <>
|
||||
<div className='msp-state-snapshot-animation-slider msp-form-control'>
|
||||
<Slider
|
||||
value={snapshots.state.currentAnimationFrame ?? 0}
|
||||
min={1}
|
||||
step={1}
|
||||
max={(entry?.snapshot.transition?.frames.length ?? 1)}
|
||||
value={Math.round(100 * (snapshots.state.currentAnimationTimeMs ?? 0)) /100}
|
||||
min={0}
|
||||
step={PluginState.getMinFrameDuration(entry?.snapshot)}
|
||||
max={PluginState.getStateTransitionDuration(entry?.snapshot) ?? 1000}
|
||||
onChange={() => { }}
|
||||
onChangeImmediate={v => {
|
||||
snapshots.setSnapshotAnimationFrame(v - 1, true);
|
||||
}}
|
||||
onChangeImmediate={v => snapshots.setSnapshotAnimationFrame(v, true)}
|
||||
hideInput
|
||||
disabled={this.state.isBusy}
|
||||
/>
|
||||
|
||||
@@ -24,6 +24,8 @@ import { elementLabel } from '../mol-theme/label';
|
||||
import { Icon, HelpOutlineSvg } from './controls/icons';
|
||||
import { StructureSelectionManager } from '../mol-plugin-state/manager/structure/selection';
|
||||
import { arrayEqual } from '../mol-util/array';
|
||||
import { Observable } from 'rxjs';
|
||||
import { ThemeProvider } from '../mol-theme/theme';
|
||||
|
||||
const MaxDisplaySequenceLength = 5000;
|
||||
// TODO: add virtualized Select controls (at best with a search box)?
|
||||
@@ -244,6 +246,13 @@ export class SequenceView extends PluginUIComponent<{ defaultMode?: SequenceView
|
||||
}
|
||||
});
|
||||
|
||||
const experimentalSequenceTheme = this.plugin.customUIState.experimentalSequenceTheme as Observable<ThemeProvider<any, any> | undefined> | undefined;
|
||||
if (experimentalSequenceTheme) {
|
||||
this.subscribe(experimentalSequenceTheme as Observable<ThemeProvider<any, any>>, theme => {
|
||||
// do stuff
|
||||
});
|
||||
}
|
||||
|
||||
const modeOptions = this.plugin.spec.components?.sequenceViewer?.modeOptions;
|
||||
if (modeOptions) {
|
||||
const modeSet = new Set(modeOptions);
|
||||
|
||||
@@ -27,6 +27,7 @@ import { PluginConfig } from './config';
|
||||
import { PluginContext } from './context';
|
||||
import { AnimateStateSnapshotTransition } from '../mol-plugin-state/animation/built-in/state-snapshots';
|
||||
import { Scheduler } from '../mol-task';
|
||||
import { memoizeLatest } from '../mol-util/memoize';
|
||||
|
||||
export { PluginState };
|
||||
|
||||
@@ -264,7 +265,23 @@ namespace PluginState {
|
||||
}[],
|
||||
}
|
||||
|
||||
export function getStateTransitionDuration(snapshot: Snapshot): number | undefined {
|
||||
export const getMinFrameDuration = memoizeLatest((snapshot: Snapshot | undefined): number => {
|
||||
if (!snapshot) return 1000 / 60;
|
||||
const { transition } = snapshot;
|
||||
if (!transition) return 1000 / 60;
|
||||
|
||||
let minDuration = Infinity;
|
||||
for (const frame of transition.frames) {
|
||||
if (frame.durationInMs > 0 && frame.durationInMs < minDuration) {
|
||||
minDuration = frame.durationInMs;
|
||||
}
|
||||
}
|
||||
if (!Number.isFinite(minDuration)) return 1000 / 60;
|
||||
return minDuration;
|
||||
});
|
||||
|
||||
export const getStateTransitionDuration = memoizeLatest((snapshot: Snapshot | undefined): number | undefined => {
|
||||
if (!snapshot) return undefined;
|
||||
const { transition } = snapshot;
|
||||
if (!transition) return undefined;
|
||||
let totalDuration = 0;
|
||||
@@ -273,7 +290,20 @@ namespace PluginState {
|
||||
totalDuration += frame.durationInMs;
|
||||
}
|
||||
return totalDuration;
|
||||
}
|
||||
});
|
||||
|
||||
export const getStateTransitionFrameTime = memoizeLatest((snapshot: Snapshot | undefined, frameIndex: number | undefined): number => {
|
||||
if (!snapshot || frameIndex === undefined) return 0;
|
||||
const { transition } = snapshot;
|
||||
if (!transition) return 0;
|
||||
let currentDuration = 0;
|
||||
for (let i = 0; i < frameIndex; i++) {
|
||||
if (transition.frames.length <= i) break;
|
||||
const frame = transition.frames[i];
|
||||
currentDuration += frame.durationInMs;
|
||||
}
|
||||
return currentDuration;
|
||||
});
|
||||
|
||||
export function getStateTransitionFrameIndex(snapshot: Snapshot, timestamp: number): number | undefined {
|
||||
const { transition } = snapshot;
|
||||
|
||||
Reference in New Issue
Block a user