Add axis param to camera spin/rock animation

This commit is contained in:
Alexander Rose
2026-05-16 22:25:55 -07:00
parent 1b8117d3f1
commit f806ac1444
3 changed files with 41 additions and 10 deletions

View File

@@ -13,6 +13,7 @@ Note that since we don't clearly distinguish between a public and private interf
- Add presets option to `ObjectList` param definition
- Fix memory leak in `State.dispose()` not invoking transformer `dispose` callbacks for live cells
- Fix `Volume` and `Isosurface` getBoundingSphere ignoring instances
- Add axis param to camera spin/rock animation
## [v5.9.0] - 2026-05-03
- Fix edge case when `PluginSpec.animations` is empty

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2022-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
@@ -12,7 +12,7 @@ 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();
const _dir = Vec3(), _axis = Vec3(), _rot = Quat(), _up = Vec3(), _side = Vec3();
type State = { snapshot: Camera.Snapshot };
@@ -24,6 +24,7 @@ export const AnimateCameraRock = PluginStateAnimation.create({
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.' }),
axis: PD.Vec3(Vec3.create(0, -1, 0), {}, { description: 'Axis of rotation in camera space' }),
}),
initialState: (p, ctx) => ({ snapshot: ctx.canvas3d!.camera.getSnapshot() }) as State,
getDuration: p => ({ kind: 'fixed', durationMs: p.durationInMs }),
@@ -47,11 +48,25 @@ export const AnimateCameraRock = PluginStateAnimation.create({
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);
// Transform axis from camera space to world space
Vec3.normalize(_axis, _dir); // Z = view direction
Vec3.normalize(_up, snapshot.up); // Y = up
Vec3.cross(_side, _up, _axis); // X = right
Vec3.normalize(_side, _side);
const a = ctx.params.axis;
Vec3.set(_axis,
a[0] * _side[0] + a[1] * _up[0] + a[2] * _axis[0],
a[0] * _side[1] + a[1] * _up[1] + a[2] * _axis[1],
a[0] * _side[2] + a[1] * _up[2] + a[2] * _axis[2]
);
Vec3.normalize(_axis, _axis);
Quat.setAxisAngle(_rot, _axis, angle);
Vec3.transformQuat(_dir, _dir, _rot);
Vec3.transformQuat(_up, snapshot.up, _rot);
const position = Vec3.add(Vec3(), snapshot.target, _dir);
ctx.plugin.canvas3d?.requestCameraReset({ snapshot: { ...snapshot, position }, durationMs: 0 });
ctx.plugin.canvas3d?.requestCameraReset({ snapshot: { ...snapshot, position, up: _up }, durationMs: 0 });
if (phase >= 0.99999) {
return { kind: 'finished' };

View File

@@ -1,7 +1,8 @@
/**
* Copyright (c) 2020-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2020-2026 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>
*/
import { Camera } from '../../../mol-canvas3d/camera';
@@ -11,7 +12,7 @@ import { Vec3 } from '../../../mol-math/linear-algebra/3d/vec3';
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
import { PluginStateAnimation } from '../model';
const _dir = Vec3(), _axis = Vec3(), _rot = Quat();
const _dir = Vec3(), _axis = Vec3(), _rot = Quat(), _up = Vec3(), _side = Vec3();
type State = { snapshot: Camera.Snapshot };
@@ -22,7 +23,7 @@ export const AnimateCameraSpin = PluginStateAnimation.create({
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 duration.' }),
direction: PD.Select<'cw' | 'ccw'>('cw', [['cw', 'Clockwise'], ['ccw', 'Counter Clockwise']], { cycle: true })
axis: PD.Vec3(Vec3.create(0, -1, 0), {}, { description: 'Axis of rotation in camera space' }),
}),
initialState: (_, ctx) => ({ snapshot: ctx.canvas3d?.camera.getSnapshot()! }) as State,
getDuration: p => ({ kind: 'fixed', durationMs: p.durationInMs }),
@@ -42,14 +43,28 @@ export const AnimateCameraSpin = PluginStateAnimation.create({
const phase = t.animation
? t.animation?.currentFrame / (t.animation.frameCount + 1)
: clamp(t.current / ctx.params.durationInMs, 0, 1);
const angle = 2 * Math.PI * phase * ctx.params.speed * (ctx.params.direction === 'ccw' ? -1 : 1);
const angle = 2 * Math.PI * phase * ctx.params.speed;
Vec3.sub(_dir, snapshot.position, snapshot.target);
Vec3.normalize(_axis, snapshot.up);
// Transform axis from camera space to world space
Vec3.normalize(_axis, _dir); // Z = view direction
Vec3.normalize(_up, snapshot.up); // Y = up
Vec3.cross(_side, _up, _axis); // X = right
Vec3.normalize(_side, _side);
const a = ctx.params.axis;
Vec3.set(_axis,
a[0] * _side[0] + a[1] * _up[0] + a[2] * _axis[0],
a[0] * _side[1] + a[1] * _up[1] + a[2] * _axis[1],
a[0] * _side[2] + a[1] * _up[2] + a[2] * _axis[2]
);
Vec3.normalize(_axis, _axis);
Quat.setAxisAngle(_rot, _axis, angle);
Vec3.transformQuat(_dir, _dir, _rot);
Vec3.transformQuat(_up, snapshot.up, _rot);
const position = Vec3.add(Vec3(), snapshot.target, _dir);
ctx.plugin.canvas3d?.requestCameraReset({ snapshot: { ...snapshot, position }, durationMs: 0 });
ctx.plugin.canvas3d?.requestCameraReset({ snapshot: { ...snapshot, position, up: _up }, durationMs: 0 });
if (phase >= 0.99999) {
return { kind: 'finished' };