Ver Fonte

Merge pull request #337 from molstar/anim-rock

Add rock animation
Alexander Rose há 3 anos atrás
pai
commit
9459af46b8

+ 4 - 0
CHANGELOG.md

@@ -11,6 +11,10 @@ Note that since we don't clearly distinguish between a public and private interf
     - Fix camera stutter for "camera spin" animation
 - Add partial charge parsing support for MOL/SDF files (thanks @ptourlas)
 - [Breaking] Cleaner looking ``MembraneOrientationVisuals`` defaults
+- [Breaking] Add rock animation to trackball controls
+    - Add ``animate`` to ``TrackballControlsParams``, remove ``spin`` and ``spinSpeed``
+    - Add ``animate`` to ``SimpleSettingsParams``, remove ``spin``
+- Add "camera rock" state animation
 - Add support for custom colors to "molecule-type" theme
 - [Breaking] Add style parameter to "illustrative" color theme
     - Defaults to "entity-id" style instad of "chain-id"

+ 11 - 3
src/examples/basic-wrapper/index.ts

@@ -74,12 +74,20 @@ class BasicWrapper {
     toggleSpin() {
         if (!this.plugin.canvas3d) return;
 
+        const trackball = this.plugin.canvas3d.props.trackball;
         PluginCommands.Canvas3D.SetSettings(this.plugin, {
-            settings: props => {
-                props.trackball.spin = !props.trackball.spin;
+            settings: {
+                trackball: {
+                    ...trackball,
+                    animate: trackball.animate.name === 'spin'
+                        ? { name: 'off', params: {} }
+                        : { name: 'spin', params: { speed: 1 } }
+                }
             }
         });
-        if (!this.plugin.canvas3d.props.trackball.spin) PluginCommands.Camera.Reset(this.plugin, {});
+        if (this.plugin.canvas3d.props.trackball.animate.name !== 'spin') {
+            PluginCommands.Camera.Reset(this.plugin, {});
+        }
     }
 
     private animateModelIndexTargetFps() {

+ 10 - 1
src/examples/proteopedia-wrapper/index.ts

@@ -256,7 +256,16 @@ class MolStarProteopediaWrapper {
     toggleSpin() {
         if (!this.plugin.canvas3d) return;
         const trackball = this.plugin.canvas3d.props.trackball;
-        PluginCommands.Canvas3D.SetSettings(this.plugin, { settings: { trackball: { ...trackball, spin: !trackball.spin } } });
+        PluginCommands.Canvas3D.SetSettings(this.plugin, {
+            settings: {
+                trackball: {
+                    ...trackball,
+                    animate: trackball.animate.name === 'spin'
+                        ? { name: 'off', params: {} }
+                        : { name: 'spin', params: { speed: 1 } }
+                }
+            }
+        });
     }
 
     viewport = {

+ 1 - 1
src/mol-canvas3d/canvas3d.ts

@@ -404,7 +404,7 @@ namespace Canvas3D {
 
                 const ctx = { renderer, camera: cam, scene, helper };
                 if (MultiSamplePass.isEnabled(p.multiSample)) {
-                    const forceOn = !cameraChanged && allowMulti && !controls.props.spin;
+                    const forceOn = !cameraChanged && allowMulti && !controls.isAnimating;
                     multiSampleHelper.render(ctx, p, true, forceOn);
                 } else {
                     passes.draw.render(ctx, p, true);

+ 55 - 11
src/mol-canvas3d/controls/trackball.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  * @author David Sehnal <david.sehnal@gmail.com>
@@ -13,7 +13,7 @@ import { Viewport } from '../camera/util';
 import { InputObserver, DragInput, WheelInput, PinchInput, ButtonsType, ModifiersKeys, GestureInput } from '../../mol-util/input/input-observer';
 import { ParamDefinition as PD } from '../../mol-util/param-definition';
 import { Camera } from '../camera';
-import { absMax } from '../../mol-math/misc';
+import { absMax, degToRad } from '../../mol-math/misc';
 import { Binding } from '../../mol-util/binding';
 
 const B = ButtonsType;
@@ -40,8 +40,16 @@ export const TrackballControlsParams = {
     zoomSpeed: PD.Numeric(7.0, { min: 1, max: 15, step: 1 }),
     panSpeed: PD.Numeric(1.0, { min: 0.1, max: 5, step: 0.1 }),
 
-    spin: PD.Boolean(false, { description: 'Spin the 3D scene around the x-axis in view space' }),
-    spinSpeed: PD.Numeric(1, { min: -20, max: 20, step: 1 }),
+    animate: PD.MappedStatic('off', {
+        off: PD.EmptyGroup(),
+        spin: PD.Group({
+            speed: PD.Numeric(1, { min: -20, max: 20, step: 1 }),
+        }, { description: 'Spin the 3D scene around the x-axis in view space' }),
+        rock: PD.Group({
+            speed: PD.Numeric(0.3, { min: -5, max: 5, step: 0.1 }),
+            angle: PD.Numeric(10, { min: 0, max: 90, step: 1 }, { description: 'How many degrees to rotate in each direction.' }),
+        }, { description: 'Rock the 3D scene around the x-axis in view space' })
+    }),
 
     staticMoving: PD.Boolean(true, { isHidden: true }),
     dynamicDampingFactor: PD.Numeric(0.2, {}, { isHidden: true }),
@@ -72,7 +80,8 @@ export type TrackballControlsProps = PD.Values<typeof TrackballControlsParams>
 
 export { TrackballControls };
 interface TrackballControls {
-    viewport: Viewport
+    readonly viewport: Viewport
+    readonly isAnimating: boolean
 
     readonly props: Readonly<TrackballControlsProps>
     setProps: (props: Partial<TrackballControlsProps>) => void
@@ -144,6 +153,11 @@ namespace TrackballControls {
             );
         }
 
+        function getRotateFactor() {
+            const aspectRatio = input.width / input.height;
+            return p.rotateSpeed * input.pixelRatio * aspectRatio;
+        }
+
         const rotAxis = Vec3();
         const rotQuat = Quat();
         const rotEyeDir = Vec3();
@@ -156,8 +170,7 @@ namespace TrackballControls {
             const dy = _rotCurr[1] - _rotPrev[1];
             Vec3.set(rotMoveDir, dx, dy, 0);
 
-            const aspectRatio = input.width / input.height;
-            const angle = Vec3.magnitude(rotMoveDir) * p.rotateSpeed * input.pixelRatio * aspectRatio;
+            const angle = Vec3.magnitude(rotMoveDir) * getRotateFactor();
 
             if (angle) {
                 Vec3.sub(_eye, camera.position, camera.target);
@@ -306,7 +319,10 @@ namespace TrackballControls {
         /** Update the object's position, direction and up vectors */
         function update(t: number) {
             if (lastUpdated === t) return;
-            if (p.spin && lastUpdated > 0) spin(t - lastUpdated);
+            if (lastUpdated > 0) {
+                if (p.animate.name === 'spin') spin(t - lastUpdated);
+                else if (p.animate.name === 'rock') rock(t - lastUpdated);
+            }
 
             Vec3.sub(_eye, camera.position, camera.target);
 
@@ -345,6 +361,7 @@ namespace TrackballControls {
             if (!isStart && !_isInteracting) return;
 
             _isInteracting = true;
+            resetRock(); // start rocking from the center after interactions
 
             const dragRotate = Binding.match(p.bindings.dragRotate, buttons, modifiers);
             const dragRotateZ = Binding.match(p.bindings.dragRotateZ, buttons, modifiers);
@@ -434,11 +451,34 @@ namespace TrackballControls {
 
         const _spinSpeed = Vec2.create(0.005, 0);
         function spin(deltaT: number) {
-            if (p.spinSpeed === 0) return;
+            if (p.animate.name !== 'spin' || p.animate.params.speed === 0 || _isInteracting) return;
 
-            const frameSpeed = (p.spinSpeed || 0) / 1000;
+            const frameSpeed = p.animate.params.speed / 1000;
             _spinSpeed[0] = 60 * Math.min(Math.abs(deltaT), 1000 / 8) / 1000 * frameSpeed;
-            if (!_isInteracting) Vec2.add(_rotCurr, _rotPrev, _spinSpeed);
+            Vec2.add(_rotCurr, _rotPrev, _spinSpeed);
+        }
+
+        let _rockPhase = 0;
+        const _rockSpeed = Vec2.create(0.005, 0);
+        function rock(deltaT: number) {
+            if (p.animate.name !== 'rock' || p.animate.params.speed === 0 || _isInteracting) return;
+
+            const dt = deltaT / 1000 * p.animate.params.speed;
+            const maxAngle = degToRad(p.animate.params.angle) / getRotateFactor();
+            const angleA = Math.sin(_rockPhase * Math.PI * 2) * maxAngle;
+            const angleB = Math.sin((_rockPhase + dt) * Math.PI * 2) * maxAngle;
+
+            _rockSpeed[0] = angleB - angleA;
+            Vec2.add(_rotCurr, _rotPrev, _rockSpeed);
+
+            _rockPhase += dt;
+            if (_rockPhase >= 1) {
+                _rockPhase = 0;
+            }
+        }
+
+        function resetRock() {
+            _rockPhase = 0;
         }
 
         function start(t: number) {
@@ -448,9 +488,13 @@ namespace TrackballControls {
 
         return {
             viewport,
+            get isAnimating() { return p.animate.name !== 'off'; },
 
             get props() { return p as Readonly<TrackballControlsProps>; },
             setProps: (props: Partial<TrackballControlsProps>) => {
+                if (props.animate?.name === 'rock' && p.animate.name !== 'rock') {
+                    resetRock(); // start rocking from the center
+                }
                 Object.assign(p, props);
             },
 

+ 62 - 0
src/mol-plugin-state/animation/built-in/camera-rock.ts

@@ -0,0 +1,62 @@
+/**
+ * Copyright (c) 2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { Camera } from '../../../mol-canvas3d/camera';
+import { clamp } from '../../../mol-math/interpolate';
+import { Quat } from '../../../mol-math/linear-algebra/3d/quat';
+import { Vec3 } from '../../../mol-math/linear-algebra/3d/vec3';
+import { degToRad } from '../../../mol-math/misc';
+import { ParamDefinition as PD } from '../../../mol-util/param-definition';
+import { PluginStateAnimation } from '../model';
+
+const _dir = Vec3(), _axis = Vec3(), _rot = Quat();
+
+type State = { snapshot: Camera.Snapshot };
+
+export const AnimateCameraRock = PluginStateAnimation.create({
+    name: 'built-in.animate-camera-rock',
+    display: { name: 'Camera Rock', description: 'Rock the 3D scene around the x-axis in view space' },
+    isExportable: true,
+    params: () => ({
+        durationInMs: PD.Numeric(4000, { min: 100, max: 20000, step: 100 }),
+        speed: PD.Numeric(1, { min: 1, max: 10, step: 1 }, { description: 'How many times to rock from side to side.' }),
+        angle: PD.Numeric(10, { min: 0, max: 180, step: 1 }, { description: 'How many degrees to rotate in each direction.' }),
+    }),
+    initialState: (p, ctx) => ({ snapshot: ctx.canvas3d!.camera.getSnapshot() }) as State,
+    getDuration: p => ({ kind: 'fixed', durationMs: p.durationInMs }),
+    teardown: (_, state: State, ctx) => {
+        ctx.canvas3d?.requestCameraReset({ snapshot: state.snapshot, durationMs: 0 });
+    },
+
+    async apply(animState: State, t, ctx) {
+        if (t.current === 0) {
+            return { kind: 'next', state: animState };
+        }
+
+        const snapshot = animState.snapshot;
+        if (snapshot.radiusMax < 0.0001) {
+            return { kind: 'finished' };
+        }
+
+        const phase = t.animation
+            ? t.animation?.currentFrame / (t.animation.frameCount + 1)
+            : clamp(t.current / ctx.params.durationInMs, 0, 1);
+        const angle = Math.sin(phase * ctx.params.speed * Math.PI * 2) * degToRad(ctx.params.angle);
+
+        Vec3.sub(_dir, snapshot.position, snapshot.target);
+        Vec3.normalize(_axis, snapshot.up);
+        Quat.setAxisAngle(_rot, _axis, angle);
+        Vec3.transformQuat(_dir, _dir, _rot);
+        const position = Vec3.add(Vec3(), snapshot.target, _dir);
+        ctx.plugin.canvas3d?.requestCameraReset({ snapshot: { ...snapshot, position }, durationMs: 0 });
+
+        if (phase >= 0.99999) {
+            return { kind: 'finished' };
+        }
+
+        return { kind: 'next', state: animState };
+    }
+});

+ 3 - 3
src/mol-plugin-state/animation/built-in/camera-spin.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2020-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author David Sehnal <david.sehnal@gmail.com>
  */
@@ -17,11 +17,11 @@ type State = { snapshot: Camera.Snapshot };
 
 export const AnimateCameraSpin = PluginStateAnimation.create({
     name: 'built-in.animate-camera-spin',
-    display: { name: 'Camera Spin' },
+    display: { name: 'Camera Spin', description: 'Spin the 3D scene around the x-axis in view space' },
     isExportable: true,
     params: () => ({
         durationInMs: PD.Numeric(4000, { min: 100, max: 20000, step: 100 }),
-        speed: PD.Numeric(1, { min: 1, max: 10, step: 1 }, { description: 'How many times to spin in the specified dutation.' }),
+        speed: PD.Numeric(1, { min: 1, max: 10, step: 1 }, { description: 'How many times to spin in the specified duration.' }),
         direction: PD.Select<'cw' | 'ccw'>('cw', [['cw', 'Clockwise'], ['ccw', 'Counter Clockwise']], { cycle: true })
     }),
     initialState: (_, ctx) => ({ snapshot: ctx.canvas3d?.camera.getSnapshot()! }) as State,

+ 4 - 8
src/mol-plugin-ui/viewport/simple-settings.tsx

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2019-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  * @author David Sehnal <david.sehnal@gmail.com>
@@ -46,10 +46,7 @@ const LayoutOptions = {
 type LayoutOptions = keyof typeof LayoutOptions
 
 const SimpleSettingsParams = {
-    spin: PD.Group({
-        spin: Canvas3DParams.trackball.params.spin,
-        speed: Canvas3DParams.trackball.params.spinSpeed
-    }, { pivot: 'spin' }),
+    animate: Canvas3DParams.trackball.params.animate,
     camera: Canvas3DParams.camera,
     background: PD.Group({
         color: PD.Color(Color(0xFCFBF9), { label: 'Background', description: 'Custom background color' }),
@@ -96,7 +93,7 @@ const SimpleSettingsMapping = ParamMapping({
 
         return {
             layout: props.layout,
-            spin: { spin: !!canvas.trackball.spin, speed: canvas.trackball.spinSpeed },
+            animate: canvas.trackball.animate,
             camera: canvas.camera,
             background: {
                 color: renderer.backgroundColor,
@@ -114,8 +111,7 @@ const SimpleSettingsMapping = ParamMapping({
     },
     update(s, props) {
         const canvas = props.canvas as Mutable<Canvas3DProps>;
-        canvas.trackball.spin = s.spin.spin;
-        canvas.trackball.spinSpeed = s.spin.speed;
+        canvas.trackball.animate = s.animate;
         canvas.camera = s.camera;
         canvas.transparentBackground = s.background.transparent;
         canvas.renderer.backgroundColor = s.background.color;

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

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author David Sehnal <david.sehnal@gmail.com>
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
@@ -23,6 +23,7 @@ import { StateTransforms } from '../mol-plugin-state/transforms';
 import { BoxifyVolumeStreaming, CreateVolumeStreamingBehavior, InitVolumeStreaming } from '../mol-plugin/behavior/dynamic/volume-streaming/transformers';
 import { AnimateStateInterpolation } from '../mol-plugin-state/animation/built-in/state-interpolation';
 import { AnimateStructureSpin } from '../mol-plugin-state/animation/built-in/spin-structure';
+import { AnimateCameraRock } from '../mol-plugin-state/animation/built-in/camera-rock';
 
 export { PluginSpec };
 
@@ -131,6 +132,7 @@ export const DefaultPluginSpec = (): PluginSpec => ({
     animations: [
         AnimateModelIndex,
         AnimateCameraSpin,
+        AnimateCameraRock,
         AnimateStateSnapshots,
         AnimateAssemblyUnwind,
         AnimateStructureSpin,