diff --git a/src/mol-plugin/behavior/dynamic/animation.ts b/src/mol-plugin/behavior/dynamic/animation.ts
index 9216f827a91a048cdf036c13a75b6c0ac203cdf5..62a77c0dc6e6b0ccf85eedf6f8f7f5b0faec210d 100644
--- a/src/mol-plugin/behavior/dynamic/animation.ts
+++ b/src/mol-plugin/behavior/dynamic/animation.ts
@@ -15,7 +15,9 @@ import { StateObjectCell, State } from 'mol-state';
 
 const StructureAnimationParams = {
     rotate: PD.Boolean(false),
-    explode: PD.Boolean(false)
+    rotateValue: PD.Numeric(0, { min: 0, max: 360, step: 0.1 }),
+    explode: PD.Boolean(false),
+    explodeValue: PD.Numeric(0, { min: 0, max: 100, step: 0.1 }),
 }
 type StructureAnimationProps = PD.Values<typeof StructureAnimationParams>
 
@@ -38,7 +40,11 @@ function getRootStructure(root: StateObjectCell, state: State) {
     return parent && parent.obj ? parent.obj as PluginStateObject.Molecule.Structure : undefined
 }
 
-// TODO this is just for testing purposes
+/**
+ * TODO
+ * - animation class is just for testing purposes, needs better API
+ * - allow per-unit transform `unitTransform: { [unitId: number]: Mat4 }`
+ */
 export const StructureAnimation = PluginBehavior.create<StructureAnimationProps>({
     name: 'structure-animation',
     display: { name: 'Structure Animation', group: 'Animation' },
@@ -58,62 +64,68 @@ export const StructureAnimation = PluginBehavior.create<StructureAnimationProps>
             this.update(params)
         }
 
-        rotate(play: boolean) {
+        rotate(rad: number) {
+            const state = this.ctx.state.dataState
+            const reprs = state.select(q => q.rootsOfType(PluginStateObject.Molecule.Representation3D));
+            Mat4.rotate(this.rotMat, this.tmpMat, rad, this.rotVec)
+            for (const r of reprs) {
+                if (!SO.isRepresentation3D(r.obj)) return
+                const structure = getRootStructure(r, state)
+                if (!structure) continue
+
+                Vec3.negate(this.transVec, Vec3.copy(this.transVec, structure.data.boundary.sphere.center))
+                Mat4.fromTranslation(this.transMat, this.transVec)
+                Mat4.mul(this.animMat, this.rotMat, this.transMat)
+
+                Vec3.copy(this.transVec, structure.data.boundary.sphere.center)
+                Mat4.fromTranslation(this.transMat, this.transVec)
+                Mat4.mul(this.animMat, this.transMat, this.animMat)
+
+                r.obj.data.setState({ transform: this.animMat })
+                this.ctx.canvas3d.add(r.obj.data)
+                this.ctx.canvas3d.requestDraw(true)
+            }
+        }
+
+        animateRotate(play: boolean) {
             if (play) {
-                const state = this.ctx.state.dataState
-                const reprs = state.select(q => q.rootsOfType(PluginStateObject.Molecule.Representation3D));
-                const rotate = (t: number) => {
-                    const rad = degToRad((t / 10) % 360)
-                    Mat4.rotate(this.rotMat, this.tmpMat, rad, this.rotVec)
-                    for (const r of reprs) {
-                        if (!SO.isRepresentation3D(r.obj)) return
-                        const structure = getRootStructure(r, state)
-                        if (!structure) continue
-
-                        Vec3.negate(this.transVec, Vec3.copy(this.transVec, structure.data.boundary.sphere.center))
-                        Mat4.fromTranslation(this.transMat, this.transVec)
-                        Mat4.mul(this.animMat, this.rotMat, this.transMat)
-
-                        Vec3.copy(this.transVec, structure.data.boundary.sphere.center)
-                        Mat4.fromTranslation(this.transMat, this.transVec)
-                        Mat4.mul(this.animMat, this.transMat, this.animMat)
-
-                        r.obj.data.setState({ transform: this.animMat })
-                        this.ctx.canvas3d.add(r.obj.data)
-                        this.ctx.canvas3d.requestDraw(true)
-                    }
-                    this.rotateAnimHandle = requestAnimationFrame(rotate)
+                const animateRotate = (t: number) => {
+                    this.rotate(degToRad((t / 10) % 360))
+                    this.rotateAnimHandle = requestAnimationFrame(animateRotate)
                 }
-                this.rotateAnimHandle = requestAnimationFrame(rotate)
+                this.rotateAnimHandle = requestAnimationFrame(animateRotate)
             } else {
                 cancelAnimationFrame(this.rotateAnimHandle)
             }
         }
 
-        explode(play: boolean) {
+        explode(d: number) {
+            const state = this.ctx.state.dataState
+            const reprs = state.select(q => q.rootsOfType(PluginStateObject.Molecule.Representation3D));
+            for (const r of reprs) {
+                if (!SO.isRepresentation3D(r.obj)) return
+                const structure = getStructure(r, state)
+                if (!structure) continue
+                const rootStructure = getRootStructure(r, state)
+                if (!rootStructure) continue
+
+                Vec3.sub(this.transVec, structure.data.boundary.sphere.center, rootStructure.data.boundary.sphere.center)
+                Vec3.setMagnitude(this.transVec, this.transVec, d)
+                Mat4.fromTranslation(this.animMat, this.transVec)
+
+                r.obj.data.setState({ transform: this.animMat })
+                this.ctx.canvas3d.add(r.obj.data)
+                this.ctx.canvas3d.requestDraw(true)
+            }
+        }
+
+        animateExplode(play: boolean) {
             if (play) {
-                const state = this.ctx.state.dataState
-                const reprs = state.select(q => q.rootsOfType(PluginStateObject.Molecule.Representation3D));
-                const explode = (t: number) => {
-                    const d = (Math.sin(t * 0.001) + 1) * 5
-                    for (const r of reprs) {
-                        if (!SO.isRepresentation3D(r.obj)) return
-                        const structure = getStructure(r, state)
-                        if (!structure) continue
-                        const rootStructure = getRootStructure(r, state)
-                        if (!rootStructure) continue
-
-                        Vec3.sub(this.transVec, structure.data.boundary.sphere.center, rootStructure.data.boundary.sphere.center)
-                        Vec3.setMagnitude(this.transVec, this.transVec, d)
-                        Mat4.fromTranslation(this.animMat, this.transVec)
-
-                        r.obj.data.setState({ transform: this.animMat })
-                        this.ctx.canvas3d.add(r.obj.data)
-                        this.ctx.canvas3d.requestDraw(true)
-                    }
-                    this.explodeAnimHandle = requestAnimationFrame(explode)
+                const animateExplode = (t: number) => {
+                    this.explode((Math.sin(t * 0.001) + 1) * 5)
+                    this.explodeAnimHandle = requestAnimationFrame(animateExplode)
                 }
-                this.explodeAnimHandle = requestAnimationFrame(explode)
+                this.explodeAnimHandle = requestAnimationFrame(animateExplode)
             } else {
                 cancelAnimationFrame(this.explodeAnimHandle)
             }
@@ -125,11 +137,19 @@ export const StructureAnimation = PluginBehavior.create<StructureAnimationProps>
             let updated = PD.areEqual(StructureAnimationParams, this.params, p)
             if (this.params.rotate !== p.rotate) {
                 this.params.rotate = p.rotate
-                this.rotate(this.params.rotate)
+                this.animateRotate(this.params.rotate)
             }
             if (this.params.explode !== p.explode) {
                 this.params.explode = p.explode
-                this.explode(this.params.explode)
+                this.animateExplode(this.params.explode)
+            }
+            if (this.params.rotateValue !== p.rotateValue) {
+                this.params.rotateValue = p.rotateValue
+                this.rotate(degToRad(this.params.rotateValue))
+            }
+            if (this.params.explodeValue !== p.explodeValue) {
+                this.params.explodeValue = p.explodeValue
+                this.explode(this.params.explodeValue)
             }
             return updated;
         }
diff --git a/src/mol-plugin/index.ts b/src/mol-plugin/index.ts
index e0872207471cf72d6fd175c79f7c234965b5e5ec..ad6c9ebc6c105583ba435c90d6ac77d13a6fa063 100644
--- a/src/mol-plugin/index.ts
+++ b/src/mol-plugin/index.ts
@@ -37,7 +37,7 @@ const DefaultSpec: PluginSpec = {
         PluginSpec.Behavior(PluginBehaviors.Representation.SelectLoci),
         PluginSpec.Behavior(PluginBehaviors.Representation.DefaultLociLabelProvider),
         PluginSpec.Behavior(PluginBehaviors.Camera.FocusLociOnSelect, { minRadius: 20, extraRadius: 4 }),
-        PluginSpec.Behavior(PluginBehaviors.Animation.StructureAnimation, { rotate: false, explode: false }),
+        PluginSpec.Behavior(PluginBehaviors.Animation.StructureAnimation, { rotate: false, rotateValue: 0, explode: false, explodeValue: 0 }),
         PluginSpec.Behavior(PluginBehaviors.CustomProps.PDBeStructureQualityReport, { autoAttach: true }),
         PluginSpec.Behavior(PluginBehaviors.CustomProps.RCSBAssemblySymmetry, { autoAttach: true }),
     ]