commit 9276dcedf463e3b724e897bd6d8864c42810089c
Author: Leszek Koltunski <leszek@koltunski.pl>
Date:   Tue Oct 5 11:58:06 2021 +0200

    Move RubikControl to objectlib (as 'ObjectAutomator')
    Hide ObjectPreRender inside objectlib and move its APi to ObjectControl.

diff --git a/src/main/java/org/distorted/objectlib/automator/ObjectAutomator.java b/src/main/java/org/distorted/objectlib/automator/ObjectAutomator.java
new file mode 100644
index 00000000..e0b6fdb2
--- /dev/null
+++ b/src/main/java/org/distorted/objectlib/automator/ObjectAutomator.java
@@ -0,0 +1,239 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2021 Leszek Koltunski                                                               //
+//                                                                                               //
+// This file is part of Magic Cube.                                                              //
+//                                                                                               //
+// Magic Cube is free software: you can redistribute it and/or modify                            //
+// it under the terms of the GNU General Public License as published by                          //
+// the Free Software Foundation, either version 2 of the License, or                             //
+// (at your option) any later version.                                                           //
+//                                                                                               //
+// Magic Cube is distributed in the hope that it will be useful,                                 //
+// but WITHOUT ANY WARRANTY; without even the implied warranty of                                //
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the                                 //
+// GNU General Public License for more details.                                                  //
+//                                                                                               //
+// You should have received a copy of the GNU General Public License                             //
+// along with Magic Cube.  If not, see <http://www.gnu.org/licenses/>.                           //
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+package org.distorted.objectlib.automator;
+
+import org.distorted.library.main.DistortedNode;
+import org.distorted.library.main.DistortedScreen;
+import org.distorted.library.message.EffectListener;
+
+import org.distorted.library.type.Static4D;
+import org.distorted.objectlib.main.ObjectControl;
+import org.distorted.objectlib.main.ObjectPreRender;
+import org.distorted.objectlib.helpers.BlockController;
+import org.distorted.objectlib.helpers.TwistyActivity;
+import org.distorted.objectlib.main.TwistyObject;
+
+import java.lang.ref.WeakReference;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+public class ObjectAutomator implements EffectListener
+  {
+  private static ObjectAutomator mControl;
+  private ObjectControl mObjectControl;
+
+  WeakReference<TwistyActivity> mRefAct;
+  boolean mWholeReturned, mRotateReturned;
+
+  ObjectAutomatorWhole mWhole;
+  ObjectAutomatorRotate mRotate;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  DistortedScreen getScreen()
+    {
+    TwistyActivity act = mRefAct.get();
+    return act!=null ? act.getScreen() : null;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  ObjectControl getObjectControl()
+    {
+    return mObjectControl;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  Static4D getQuat()
+    {
+    ObjectControl control = getObjectControl();
+    return control.getQuat();
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  TwistyObject getObject()
+    {
+    ObjectControl control = getObjectControl();
+    ObjectPreRender pre =  control.getPreRender();
+    return pre.getObject();
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void addWholeObjects()
+    {
+    DistortedScreen screen = getScreen();
+    DistortedNode[] nodes = mWhole.getNodes();
+
+    if( screen!=null && nodes!=null )
+      {
+      for (DistortedNode node : nodes) screen.attach(node);
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void removeWholeObjects()
+    {
+    DistortedScreen screen = getScreen();
+    DistortedNode[] nodes = mWhole.returnNodes();
+
+    if( screen!=null && nodes!=null )
+      {
+      for (DistortedNode node : nodes) screen.detach(node);
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void addRotateObjects()
+    {
+    DistortedScreen screen = getScreen();
+    DistortedNode[] screenNodes = mRotate.getScreenNodes();
+
+    if( screen!=null && screenNodes!=null )
+      {
+      for (DistortedNode node : screenNodes) screen.attach(node);
+      }
+
+    ObjectControl control = getObjectControl();
+    ObjectPreRender pre =  control.getPreRender();
+    DistortedNode object = pre.getObject();
+    DistortedNode[] objectNodes = mRotate.getObjectNodes();
+
+    if( object!=null && objectNodes!=null )
+      {
+      for (DistortedNode node : objectNodes) object.attach(node);
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void removeRotateObjects()
+    {
+    DistortedScreen screen = getScreen();
+    DistortedNode[] screenNodes = mRotate.returnScreenNodes();
+
+    if( screen!=null && screenNodes!=null )
+      {
+      for (DistortedNode node : screenNodes) screen.detach(node);
+      }
+
+    ObjectControl control = getObjectControl();
+    ObjectPreRender pre =  control.getPreRender();
+    DistortedNode object = pre.getObject();
+    DistortedNode[] objectNodes = mRotate.returnObjectNodes();
+
+    if( object!=null && objectNodes!=null )
+      {
+      for (DistortedNode node : objectNodes) object.detach(node);
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void finishWhole()
+    {
+    removeWholeObjects();
+
+    mWholeReturned = true;
+
+    if( !mRotateReturned ) addRotateObjects();
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void finishRotate()
+    {
+    removeRotateObjects();
+    mRotateReturned= true;
+    mObjectControl.getPreRender().unblockEverything();
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  TwistyActivity getActivity()
+    {
+    return mRefAct.get();
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private ObjectAutomator(ObjectControl control)
+    {
+    mObjectControl = control;
+
+    mWhole = new ObjectAutomatorWhole(this);
+    mRotate= new ObjectAutomatorRotate(this);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// PUBLIC
+
+  public static ObjectAutomator getInstance(ObjectControl control)
+    {
+    if( mControl==null ) mControl = new ObjectAutomator(control);
+
+    return mControl;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void postDrawFrame(long time)
+    {
+    mWhole.postDrawFrame(time);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void effectFinished(long effectID)
+    {
+    if( effectID==mWhole.getEffectID()  ) finishWhole();
+    if( effectID==mRotate.getEffectID() ) finishRotate();
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void animateAll(TwistyActivity act)
+    {
+    mObjectControl.getPreRender().blockEverything(BlockController.CONTROL_PLACE_0);
+    mRefAct = new WeakReference<>(act);
+
+    mWholeReturned = false;
+    mRotateReturned= false;
+
+    addWholeObjects();
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void animateRotate(TwistyActivity act)
+    {
+    mObjectControl.getPreRender().blockEverything(BlockController.CONTROL_PLACE_1);
+    mRefAct = new WeakReference<>(act);
+
+    mWholeReturned = true;
+    mRotateReturned= false;
+
+    addRotateObjects();
+    }
+  }
diff --git a/src/main/java/org/distorted/objectlib/automator/ObjectAutomatorRotate.java b/src/main/java/org/distorted/objectlib/automator/ObjectAutomatorRotate.java
new file mode 100644
index 00000000..90d284f7
--- /dev/null
+++ b/src/main/java/org/distorted/objectlib/automator/ObjectAutomatorRotate.java
@@ -0,0 +1,371 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2021 Leszek Koltunski                                                               //
+//                                                                                               //
+// This file is part of Magic Cube.                                                              //
+//                                                                                               //
+// Magic Cube is free software: you can redistribute it and/or modify                            //
+// it under the terms of the GNU General Public License as published by                          //
+// the Free Software Foundation, either version 2 of the License, or                             //
+// (at your option) any later version.                                                           //
+//                                                                                               //
+// Magic Cube is distributed in the hope that it will be useful,                                 //
+// but WITHOUT ANY WARRANTY; without even the implied warranty of                                //
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the                                 //
+// GNU General Public License for more details.                                                  //
+//                                                                                               //
+// You should have received a copy of the GNU General Public License                             //
+// along with Magic Cube.  If not, see <http://www.gnu.org/licenses/>.                           //
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+package org.distorted.objectlib.automator;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+
+import org.distorted.library.effect.MatrixEffectQuaternion;
+import org.distorted.library.effect.MatrixEffectScale;
+import org.distorted.library.main.DistortedEffects;
+import org.distorted.library.main.DistortedNode;
+import org.distorted.library.main.DistortedScreen;
+import org.distorted.library.main.DistortedTexture;
+import org.distorted.library.mesh.MeshQuad;
+import org.distorted.library.message.EffectListener;
+import org.distorted.library.type.Dynamic;
+import org.distorted.library.type.Dynamic3D;
+import org.distorted.library.type.Dynamic4D;
+import org.distorted.library.type.Static3D;
+import org.distorted.library.type.Static4D;
+import org.distorted.library.main.QuatHelper;
+
+import org.distorted.objectlib.R;
+import org.distorted.objectlib.helpers.TwistyActivity;
+import org.distorted.objectlib.main.TwistyObject;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+class ObjectAutomatorRotate implements EffectListener
+  {
+  private static final int NUM_SCREEN = 0;
+  private static final int NUM_OBJECT = 1;
+
+  private static Static4D INIT_QUAT;
+
+  private final ObjectAutomator mControl;
+  private DistortedEffects[] mScreenEffects, mObjectEffects;
+  private DistortedNode[] mScreenNodes, mObjectNodes;
+  private long mScreenEffectID, mObjectEffectID, mCubeEffectID;
+  private Static4D mObjRotQuat;
+
+  private Dynamic3D mDynamic3;
+  private Dynamic4D mDynamic4;
+  private MatrixEffectScale mScale;
+  private MatrixEffectQuaternion mQuaternion;
+  private MeshQuad mScreenQuad, mObjectQuad;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+   private Bitmap openBitmap(TwistyActivity act, int resource)
+     {
+     try( InputStream is = act.getResources().openRawResource(resource) )
+       {
+       return BitmapFactory.decodeStream(is);
+       }
+     catch( IOException e )
+       {
+       // ignore
+       }
+
+     return null;
+     }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void computeInitQuat()
+    {
+    double alphaZ = 0;//-Math.PI* 0.1250f;
+    double alphaY = 0;//-Math.PI* 0.0625f;
+
+    alphaY /= 2;
+    alphaZ /= 2;
+
+    float sinZ = (float)Math.sin(alphaZ);
+    float cosZ = (float)Math.cos(alphaZ);
+    float sinY = (float)Math.sin(alphaY);
+    float cosY = (float)Math.cos(alphaY);
+
+    Static4D qZ = new Static4D(0,0,sinZ,cosZ);
+    Static4D qY = new Static4D(0,sinY,0,cosY);
+
+    INIT_QUAT = QuatHelper.quatMultiply(qY,qZ);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Take 3D vector 'ax', rotate it by quaternion 'objQuat' to get vector V1.
+// Take 3D vector (1,0,0) and rotate it by INIT_QUAT to get vector V2.
+// Return a quaternion Q such that if we rotate V1 by Q, we get V2.
+
+  private Static4D computeQuat(Static3D ax, Static4D objQuat, float x, float y, float z)
+    {
+    Static4D ax4D = new Static4D( ax.get0(), ax.get1(), ax.get2(), 0);
+    Static4D axRo = QuatHelper.rotateVectorByQuat(ax4D,objQuat);
+    return QuatHelper.retRotationQuat(axRo.get0(),axRo.get1(),axRo.get2(),x,y,z);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void computeRotQuat()
+    {
+    TwistyObject object = mControl.getObject();
+    Static3D[] axis = object.getRotationAxis();
+    int chosen=-1,numAxis = axis.length;
+    float cos,maxCos = -1.0f;
+    Static4D quat;
+
+    Static4D objCurrQuat = mControl.getQuat();
+    Static4D axisX = new Static4D(1,0,0,0);
+    Static4D rotAxisX = QuatHelper.rotateVectorByQuat(axisX,INIT_QUAT);
+
+    float axX = rotAxisX.get0();
+    float axY = rotAxisX.get1();
+    float axZ = rotAxisX.get2();
+
+    for (int a=0; a<numAxis; a++)
+      {
+      quat = computeQuat(axis[a],objCurrQuat,axX,axY,axZ);
+      cos = quat.get3();
+
+android.util.Log.e("D", "axis="+a+" "+quat.get0()+" "+quat.get1()+" "+quat.get2()+" "+quat.get3() );
+
+      if (cos > maxCos)
+        {
+        maxCos = cos;
+        chosen = a;
+        if( mObjRotQuat==null ) mObjRotQuat = new Static4D(quat);
+        else mObjRotQuat.set(quat);
+        }
+      }
+
+    android.util.Log.e("D", "axis chosen: "+chosen);
+
+
+android.util.Log.e("D", mObjRotQuat.get0()+" "+mObjRotQuat.get1()+" "+mObjRotQuat.get2()+" "+mObjRotQuat.get3() );
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void setScreenEffectsStage1()
+    {
+    mDynamic3.resetToBeginning();
+    mScale.notifyWhenFinished(mControl);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// the whole cube rotates so that its axis is alignled according to the Euler angles defined in
+// computeInitQuat()
+
+  private void setObjectEffectsStage1()
+    {
+    TwistyObject obj = mControl.getObject();
+    obj.apply(mQuaternion,0);
+    mDynamic4.resetToBeginning();
+    mQuaternion.notifyWhenFinished(this);
+
+
+Static4D d = mDynamic4.getPoint(1);
+
+android.util.Log.e("D", "set: "+d.get0()+" "+d.get1()+" "+d.get2()+" "+d.get3() );
+
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void setObjectEffectsStage2()
+    {
+
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void createScreenEffects()
+    {
+    mScreenEffects   = new DistortedEffects[NUM_SCREEN];
+    mScreenEffects[0]= new DistortedEffects();
+
+    DistortedScreen screen = mControl.getScreen();
+    int wid = screen.getWidth();
+
+    Static3D scaleStart= new Static3D(1,1,1);
+    Static3D scaleEnd  = new Static3D(wid,wid,wid);
+
+    mDynamic3 = new Dynamic3D(10000,0.5f);
+    mDynamic3.add(scaleStart);
+    mDynamic3.add(scaleEnd  );
+    mDynamic3.add(scaleStart);
+    mDynamic3.setMode(Dynamic.MODE_PATH);
+
+    mScale = new MatrixEffectScale(mDynamic3);
+    mScreenEffectID = mScale.getID();
+    mScreenEffects[0].apply(mScale);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void createObjectEffects()
+    {
+    mObjectEffects   = new DistortedEffects[NUM_OBJECT];
+    mObjectEffects[0]= new DistortedEffects();
+
+    mDynamic4 = new Dynamic4D(5000,0.5f);
+    mDynamic4.add(new Static4D(0,0,0,1));
+    mDynamic4.add(mObjRotQuat);
+    mDynamic4.add(mObjRotQuat);
+    mDynamic4.add(mObjRotQuat);
+    mDynamic4.setMode(Dynamic.MODE_PATH);
+
+android.util.Log.e("D", "create: "+mObjRotQuat.get0()+" "+mObjRotQuat.get1()+" "+mObjRotQuat.get2()+" "+mObjRotQuat.get3() );
+
+TwistyObject obj = mControl.getObject();
+Static3D ax = obj.getRotationAxis()[0];
+Static4D axis = new Static4D(ax.get0(), ax.get1(), ax.get2(), 0);
+
+Static4D v1 = QuatHelper.rotateVectorByQuat( new Static4D(1,0,0,0), INIT_QUAT);
+Static4D v2 = QuatHelper.rotateVectorByQuat( axis                 , mObjRotQuat);
+
+android.util.Log.e("D", "v1: "+v1.get0()+" "+v1.get1()+" "+v1.get2()+" "+v1.get3());
+android.util.Log.e("D", "v2: "+v2.get0()+" "+v2.get1()+" "+v2.get2()+" "+v2.get3());
+android.util.Log.e("D", "ax: "+ax.get0()+" "+ax.get1()+" "+ax.get2());
+
+
+    Static3D center = new Static3D(0,0,0);
+    mQuaternion = new MatrixEffectQuaternion(mDynamic4, center);
+    mCubeEffectID = mQuaternion.getID();
+
+    DistortedScreen screen = mControl.getScreen();
+    int wid = screen.getWidth();
+    Static3D scaleFactor = new Static3D(wid,wid*0.1f,10);
+    MatrixEffectScale scale = new MatrixEffectScale(scaleFactor);
+    MatrixEffectQuaternion quat = new MatrixEffectQuaternion(INIT_QUAT,center);
+
+    mObjectEffects[0].apply(scale);
+    mObjectEffects[0].apply(quat);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void createScreenNodes()
+    {
+    if( mScreenNodes==null )
+      {
+      mScreenNodes= new DistortedNode[NUM_SCREEN];
+      mScreenQuad = new MeshQuad();
+      }
+
+    DistortedTexture texture = new DistortedTexture();
+    texture.setColorARGB(0xff00ff00);
+    mScreenNodes[0] = new DistortedNode(texture, mScreenEffects[0], mScreenQuad);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void createObjectNodes()
+    {
+    if( mObjectNodes==null )
+      {
+      mObjectNodes= new DistortedNode[NUM_OBJECT];
+      mObjectQuad = new MeshQuad();
+      }
+
+    TwistyActivity act = mControl.getActivity();
+
+    if( act!=null )
+      {
+      Bitmap bmpArrow = openBitmap(act, R.drawable.ui_axis_arrow);
+      DistortedTexture textureArrow = new DistortedTexture();
+
+      if( bmpArrow!=null ) textureArrow.setTexture(bmpArrow);
+
+      mObjectNodes[0] = new DistortedNode(textureArrow, mObjectEffects[0], mObjectQuad);
+      }
+    else
+      {
+      android.util.Log.e("D", "Activity NULL!!");
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  long getEffectID()
+    {
+    return mObjectEffectID;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  DistortedNode[] getScreenNodes()
+    {
+    if( NUM_SCREEN>0 )
+      {
+      if( mScreenEffects==null ) createScreenEffects();
+      createScreenNodes();
+      setScreenEffectsStage1();
+      }
+
+    return mScreenNodes;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  DistortedNode[] returnScreenNodes()
+    {
+    return mScreenNodes;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+   DistortedNode[] getObjectNodes()
+    {
+    if( NUM_OBJECT>0 )
+      {
+      if( INIT_QUAT==null ) computeInitQuat();
+      computeRotQuat();
+      if( mObjectEffects==null ) createObjectEffects();
+      createObjectNodes();
+      setObjectEffectsStage1();
+      }
+
+    return mObjectNodes;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+   DistortedNode[] returnObjectNodes()
+    {
+    return mObjectNodes;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  ObjectAutomatorRotate(ObjectAutomator control)
+    {
+    mControl = control;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  @Override
+  public void effectFinished(long effectID)
+    {
+    if( effectID==mCubeEffectID)
+      {
+      setObjectEffectsStage2();
+
+      TwistyObject obj = mControl.getObject();
+      obj.remove(mCubeEffectID);
+      mObjectEffectID = -1;
+      mControl.effectFinished(mObjectEffectID);
+      }
+    }
+  }
diff --git a/src/main/java/org/distorted/objectlib/automator/ObjectAutomatorWhole.java b/src/main/java/org/distorted/objectlib/automator/ObjectAutomatorWhole.java
new file mode 100644
index 00000000..c2671f14
--- /dev/null
+++ b/src/main/java/org/distorted/objectlib/automator/ObjectAutomatorWhole.java
@@ -0,0 +1,986 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2021 Leszek Koltunski                                                               //
+//                                                                                               //
+// This file is part of Magic Cube.                                                              //
+//                                                                                               //
+// Magic Cube is free software: you can redistribute it and/or modify                            //
+// it under the terms of the GNU General Public License as published by                          //
+// the Free Software Foundation, either version 2 of the License, or                             //
+// (at your option) any later version.                                                           //
+//                                                                                               //
+// Magic Cube is distributed in the hope that it will be useful,                                 //
+// but WITHOUT ANY WARRANTY; without even the implied warranty of                                //
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the                                 //
+// GNU General Public License for more details.                                                  //
+//                                                                                               //
+// You should have received a copy of the GNU General Public License                             //
+// along with Magic Cube.  If not, see <http://www.gnu.org/licenses/>.                           //
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+package org.distorted.objectlib.automator;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+
+import org.distorted.library.effect.MatrixEffectMove;
+import org.distorted.library.effect.MatrixEffectScale;
+import org.distorted.library.main.DistortedEffects;
+import org.distorted.library.main.DistortedNode;
+import org.distorted.library.main.DistortedScreen;
+import org.distorted.library.main.DistortedTexture;
+import org.distorted.library.mesh.MeshQuad;
+import org.distorted.library.type.Dynamic;
+import org.distorted.library.type.Dynamic3D;
+import org.distorted.library.type.Static3D;
+import org.distorted.objectlib.R;
+import org.distorted.objectlib.helpers.TwistyActivity;
+import org.distorted.objectlib.main.ObjectControl;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+class ObjectAutomatorWhole
+  {
+  private static final int NUM_NODE = 4;
+  private static final int NUM_EFFE = 4;
+
+  private static final int D1 = 2600; // time it takes for the finger to appear
+  private static final int D2 =  250; // finger press
+  private static final int D3 =10000; // finger triangle
+  private static final int D4 = 3000; // fingers approach
+  private static final int D5 = 8000; // fingers circle
+
+  private static final int[] DUR = { D1, D2, D3, D2, D1/4, 3*D1/4, D1/4, D2, D4, D5, D4, D2, D1 };
+  private static final int[] DYN = { 2, 1, 1, 1, 2, 2, 4, 2, 2, 2, 2, 2, 4};
+
+  private float X0, X1, X2, Y1, D, s001, s014, s033, F;
+  private int mWidth, mHeight;
+
+  private final ObjectAutomator mControl;
+  private DistortedEffects[] mEffects;
+  private DistortedNode[] mNodes;
+  private long mEffectID;
+  private int mCurrentStage;
+
+  private MeshQuad mQuad;
+  private DistortedTexture mTextureShad, mTextureCirc;
+  private Dynamic3D mDynMoveHand1, mDynMoveShad1;
+  private Dynamic3D mDynScaleHand1, mDynScaleShad1;
+  private Dynamic3D mDynMoveHand2, mDynMoveShad2;
+  private Dynamic3D mDynScaleHand2, mDynScaleShad2;
+  private Dynamic3D mDyn1, mDyn2, mDyn3, mDyn4;
+  private Static3D mPosition1, mPosition2, mPosition3, mPosition4;
+  private float[] tmpBuffer;
+  private long mLastTime, mDiffTime;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+   private void setPostFrame(boolean on)
+     {
+     TwistyActivity act = mControl.getActivity();
+  //   act.setControlState(on);
+     mLastTime = -1;
+     }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+   private Bitmap openBitmap(TwistyActivity act, int resource)
+     {
+     try( InputStream is = act.getResources().openRawResource(resource) )
+       {
+       return BitmapFactory.decodeStream(is);
+       }
+     catch( IOException e )
+       {
+       // ignore
+       }
+
+     return null;
+     }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void resetDynamics1(int stage)
+    {
+    int dur = DUR[stage-1];
+
+    mDynMoveHand1.removeAll();
+    mDynMoveShad1.removeAll();
+    mDynScaleHand1.removeAll();
+    mDynScaleShad1.removeAll();
+
+    mDynMoveHand1.setDuration(dur);
+    mDynMoveShad1.setDuration(dur);
+    mDynScaleHand1.setDuration(dur);
+    mDynScaleShad1.setDuration(dur);
+    mDynMoveHand1.resetToBeginning();
+    mDynMoveShad1.resetToBeginning();
+    mDynScaleHand1.resetToBeginning();
+    mDynScaleShad1.resetToBeginning();
+
+    mCurrentStage = stage;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void resetDynamics2(int stage)
+    {
+    int dur = DUR[stage-1];
+
+    mDynMoveHand2.removeAll();
+    mDynMoveShad2.removeAll();
+    mDynScaleHand2.removeAll();
+    mDynScaleShad2.removeAll();
+
+    mDynMoveHand2.setDuration(dur);
+    mDynMoveShad2.setDuration(dur);
+    mDynScaleHand2.setDuration(dur);
+    mDynScaleShad2.setDuration(dur);
+    mDynMoveHand2.resetToBeginning();
+    mDynMoveShad2.resetToBeginning();
+    mDynScaleHand2.resetToBeginning();
+    mDynScaleShad2.resetToBeginning();
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void resetDynamics3(int stage)
+    {
+    int dur = DUR[stage-1];
+
+    mDyn1.removeAll();
+    mDyn2.removeAll();
+    mDyn1.setDuration(dur);
+    mDyn2.setDuration(dur);
+    mDyn1.resetToBeginning();
+    mDyn2.resetToBeginning();
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void resetDynamics4(int stage)
+    {
+    int dur = DUR[stage-1];
+
+    mDyn3.removeAll();
+    mDyn4.removeAll();
+    mDyn3.setDuration(dur);
+    mDyn4.setDuration(dur);
+    mDyn3.resetToBeginning();
+    mDyn4.resetToBeginning();
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// first finger appears and approaches the screen
+
+  private void setEffectsStage1()
+    {
+    resetDynamics1(1);
+    resetDynamics2(1);
+    resetDynamics3(1);
+
+    Static3D point0h = new Static3D(-X0    ,-Y1    , 0);
+    Static3D point1h = new Static3D(-X1    ,-Y1    , 0);
+    Static3D point2h = new Static3D(-X2    ,-Y1    , 0);
+    Static3D point3h = new Static3D(-X2  +D,-Y1  +D, 0);
+    Static3D point0s = new Static3D(-X0+2*D,-Y1+2*D, 0);
+    Static3D point1s = new Static3D(-X1+2*D,-Y1+2*D, 0);
+    Static3D point2s = new Static3D(-X2+2*D,-Y1+2*D, 0);
+    Static3D point3s = new Static3D(-X2  +D,-Y1  +D, 0);
+
+    Static3D pointSc = new Static3D(s033,s033,s033);
+
+    mDyn1.add(point0h);
+    mDyn1.add(point1h);
+    mDyn1.add(point2h);
+    mDyn1.add(point2h);
+    mDyn1.add(point3h);
+    mDyn2.add(point0s);
+    mDyn2.add(point1s);
+    mDyn2.add(point2s);
+    mDyn2.add(point2s);
+    mDyn2.add(point3s);
+
+    mPosition1.set(point0h);
+    mPosition2.set(point0s);
+
+    mDynMoveHand1.add(mPosition1);
+    mDynMoveShad1.add(mPosition2);
+
+    mDynScaleHand1.add(pointSc);
+    mDynScaleShad1.add(pointSc);
+
+    setPostFrame(true);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// first finger touches the screen
+
+  private void setEffectsStage2()
+    {
+    resetDynamics1(2);
+    resetDynamics3(2);
+
+    Static3D point3h = new Static3D(-X2  +D,-Y1  +D, 0);
+    Static3D scaleS  = new Static3D(s001,s001,s001);
+    Static3D scaleF  = new Static3D(s014,s014,s014);
+    Static3D pointH  = new Static3D(s033,s033,s033);
+
+    mPosition1.set(scaleS);
+    mDyn1.add(scaleS);
+    mDyn1.add(scaleF);
+
+    mDynMoveHand1.add(point3h);
+    mDynMoveShad1.add(point3h);
+    mDynScaleHand1.add(pointH);
+    mDynScaleShad1.add(mPosition1);
+
+    mNodes[0].changeInputSurface(mTextureCirc);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// first finger moves across the screen in a triangular fashion
+
+  private void setEffectsStage3()
+    {
+    resetDynamics1(3);
+    resetDynamics3(3);
+
+    Static3D scaleS = new Static3D(s014,s014,s014);
+    Static3D pointH = new Static3D(s033,s033,s033);
+    Static3D point1 = new Static3D(-X2  +D,-Y1  +D, 0);
+    Static3D point2 = new Static3D(     +D,-Y1  +D, 0);
+    Static3D point3 = new Static3D(+X2  +D,-Y1  +D, 0);
+    Static3D point4 = new Static3D(+X2  +D,     +D, 0);
+    Static3D point5 = new Static3D(+X2  +D,+Y1  +D, 0);
+    Static3D point6 = new Static3D(     +D,     +D, 0);
+
+    mDynScaleHand1.add(pointH);
+    mDynScaleShad1.add(scaleS);
+
+    mDyn1.add(point1);
+    mDyn1.add(point1);
+    mDyn1.add(point2);
+    mDyn1.add(point3);
+    mDyn1.add(point3);
+    mDyn1.add(point4);
+    mDyn1.add(point5);
+    mDyn1.add(point5);
+    mDyn1.add(point6);
+    mDyn1.add(point1);
+    mDyn1.add(point1);
+
+    mPosition1.set(point1);
+
+    mDynMoveHand1.add(mPosition1);
+    mDynMoveShad1.add(mPosition1);
+
+    ObjectControl control = mControl.getObjectControl();
+    float x = point1.get0() + mWidth*0.5f;
+    float y = mHeight*0.5f - point1.get1();
+
+    /*
+    view.prepareDown();
+    view.actionDown(x,y);
+
+     */
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// first finger un-touches the screen
+
+  private void setEffectsStage4()
+    {
+    resetDynamics1(4);
+    resetDynamics3(4);
+
+    Static3D point3h = new Static3D(-X2+D,-Y1+D, 0);
+    Static3D scaleS  = new Static3D(s014,s014,s014);
+    Static3D scaleF  = new Static3D(s001,s001,s001);
+    Static3D pointH  = new Static3D(s033,s033,s033);
+
+    mDyn1.add(scaleS);
+    mDyn1.add(scaleF);
+    mPosition1.set(scaleS);
+
+    mDynMoveHand1.add(point3h);
+    mDynMoveShad1.add(point3h);
+    mDynScaleHand1.add(pointH);
+    mDynScaleShad1.add(mPosition1);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// first finger un-touches the screen (part2)
+
+  private void setEffectsStage5()
+    {
+    resetDynamics1(5);
+    resetDynamics3(5);
+
+    Static3D pointH = new Static3D(-X2    ,-Y1    , 0);
+    Static3D point0 = new Static3D(-X2  +D,-Y1  +D, 0);
+    Static3D pointS = new Static3D(-X2+2*D,-Y1+2*D, 0);
+    Static3D pointSc = new Static3D(s033,s033,s033);
+
+    mPosition1.set(point0);
+    mPosition2.set(point0);
+
+    mDyn1.add(point0);
+    mDyn1.add(pointH);
+    mDyn2.add(point0);
+    mDyn2.add(pointS);
+
+    mDynScaleHand1.add(pointSc);
+    mDynScaleShad1.add(pointSc);
+    mDynMoveHand1.add(mPosition1);
+    mDynMoveShad1.add(mPosition2);
+
+    mNodes[0].changeInputSurface(mTextureShad);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// second finger appears
+
+  private void setEffectsStage6()
+    {
+    resetDynamics1(6);
+    resetDynamics2(6);
+    resetDynamics3(6);
+
+    Static3D pointH = new Static3D(-X2    ,-Y1    , 0);
+    Static3D pointS = new Static3D(-X2+2*D,-Y1+2*D, 0);
+    Static3D pointSc= new Static3D(s033,s033,s033);
+
+    mDynScaleHand1.add(pointSc);
+    mDynScaleShad1.add(pointSc);
+    mDynMoveHand1.add(pointH);
+    mDynMoveShad1.add(pointS);
+
+    Static3D point0h = new Static3D( X0    , Y1    , 0);
+    Static3D point1h = new Static3D( X1    , Y1    , 0);
+    Static3D point2h = new Static3D( X2    , Y1    , 0);
+    Static3D point0s = new Static3D( X0+2*D, Y1+2*D, 0);
+    Static3D point1s = new Static3D( X1+2*D, Y1+2*D, 0);
+    Static3D point2s = new Static3D( X2+2*D, Y1+2*D, 0);
+    Static3D pointSm = new Static3D(-s033,s033,s033);
+
+    mPosition1.set(point0h);
+    mPosition2.set(point0s);
+
+    mDyn1.add(point0h);
+    mDyn1.add(point1h);
+    mDyn1.add(point2h);
+    mDyn1.add(point2h);
+    mDyn2.add(point0s);
+    mDyn2.add(point1s);
+    mDyn2.add(point2s);
+    mDyn2.add(point2s);
+
+    mDynScaleHand2.add(pointSm);
+    mDynScaleShad2.add(pointSm);
+    mDynMoveHand2.add(mPosition1);
+    mDynMoveShad2.add(mPosition2);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// both fingers touch the screen (part1)
+
+  private void setEffectsStage7()
+    {
+    resetDynamics1(7);
+    resetDynamics2(7);
+    resetDynamics3(7);
+    resetDynamics4(7);
+
+    Static3D point1Sc= new Static3D( s033,s033,s033);
+    Static3D point2Sc= new Static3D(-s033,s033,s033);
+    mDynScaleHand1.add(point1Sc);
+    mDynScaleShad1.add(point1Sc);
+    mDynScaleHand2.add(point2Sc);
+    mDynScaleShad2.add(point2Sc);
+
+    Static3D point1H = new Static3D(-X2    ,-Y1    , 0);
+    Static3D point1F = new Static3D(-X2  +D,-Y1  +D, 0);
+    Static3D point1S = new Static3D(-X2+2*D,-Y1+2*D, 0);
+
+    mDyn1.add(point1H);
+    mDyn1.add(point1F);
+    mDyn2.add(point1S);
+    mDyn2.add(point1F);
+
+    mPosition1.set(point1H);
+    mPosition2.set(point1S);
+    mDynMoveHand1.add(mPosition1);
+    mDynMoveShad1.add(mPosition2);
+
+    Static3D point2H = new Static3D( X2    , Y1    , 0);
+    Static3D point2F = new Static3D( X2  +D, Y1  +D, 0);
+    Static3D point2S = new Static3D( X2+2*D, Y1+2*D, 0);
+
+    mDyn3.add(point2H);
+    mDyn3.add(point2F);
+    mDyn4.add(point2S);
+    mDyn4.add(point2F);
+
+    mPosition3.set(point2H);
+    mPosition4.set(point2S);
+    mDynMoveHand2.add(mPosition3);
+    mDynMoveShad2.add(mPosition4);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// both fingers touch the screen (part2)
+
+  private void setEffectsStage8()
+    {
+    resetDynamics1(8);
+    resetDynamics2(8);
+    resetDynamics3(8);
+
+    Static3D point1h= new Static3D(-X2  +D,-Y1  +D, 0);
+    Static3D point2h= new Static3D( X2  +D, Y1  +D, 0);
+    Static3D scale1S = new Static3D( s001,s001,s001);
+    Static3D scale1F = new Static3D( s014,s014,s014);
+    Static3D point1H = new Static3D( s033,s033,s033);
+    Static3D scale2S = new Static3D(-s001,s001,s001);
+    Static3D scale2F = new Static3D(-s014,s014,s014);
+    Static3D point2H = new Static3D(-s033,s033,s033);
+
+    mPosition1.set(scale1S);
+    mDyn1.add(scale1S);
+    mDyn1.add(scale1F);
+
+    mDynMoveHand1.add(point1h);
+    mDynMoveShad1.add(point1h);
+    mDynScaleHand1.add(point1H);
+    mDynScaleShad1.add(mPosition1);
+
+    mPosition2.set(scale2S);
+    mDyn2.add(scale2S);
+    mDyn2.add(scale2F);
+
+    mDynMoveHand2.add(point2h);
+    mDynMoveShad2.add(point2h);
+    mDynScaleHand2.add(point2H);
+    mDynScaleShad2.add(mPosition2);
+
+    mNodes[0].changeInputSurface(mTextureCirc);
+    mNodes[1].changeInputSurface(mTextureCirc);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// both fingers approach each other
+
+  private void setEffectsStage9()
+    {
+    resetDynamics1(9);
+    resetDynamics2(9);
+    resetDynamics3(9);
+
+    Static3D point1s = new Static3D(-X2+D,-Y1+D, 0);
+    Static3D point2s = new Static3D( X2+D, Y1+D, 0);
+    Static3D point1f = new Static3D(-Y1*F,-Y1*F, 0);
+    Static3D point2f = new Static3D( Y1*F, Y1*F, 0);
+    Static3D scale1F = new Static3D( s014,s014,s014);
+    Static3D point1H = new Static3D( s033,s033,s033);
+    Static3D scale2F = new Static3D(-s014,s014,s014);
+    Static3D point2H = new Static3D(-s033,s033,s033);
+
+    mDynScaleHand1.add(point1H);
+    mDynScaleShad1.add(scale1F);
+    mDynScaleHand2.add(point2H);
+    mDynScaleShad2.add(scale2F);
+
+    mDyn1.add(point1s);
+    mDyn1.add(point1f);
+    mDyn2.add(point2s);
+    mDyn2.add(point2f);
+
+    mPosition1.set(point1s);
+    mPosition2.set(point2s);
+
+    mDynMoveHand1.add(mPosition1);
+    mDynMoveShad1.add(mPosition1);
+    mDynMoveHand2.add(mPosition2);
+    mDynMoveShad2.add(mPosition2);
+
+    float x1 = point1s.get0() + mWidth*0.5f;
+    float y1 = mHeight*0.5f - point1s.get1();
+    float x2 = point2s.get0() + mWidth*0.5f;
+    float y2 = mHeight*0.5f - point2s.get1();
+
+    ObjectControl control = mControl.getObjectControl();
+
+    /*
+    view.prepareDown();
+    view.prepareDown2();
+    view.actionDown(x1,y1);
+    view.actionDown2(x1,y1,x2,y2);
+
+     */
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// both fingers go around
+
+  private void setEffectsStage10()
+    {
+    resetDynamics1(10);
+    resetDynamics2(10);
+    resetDynamics3(10);
+
+    Static3D scale1F = new Static3D( s014,s014,s014);
+    Static3D point1H = new Static3D( s033,s033,s033);
+    Static3D scale2F = new Static3D(-s014,s014,s014);
+    Static3D point2H = new Static3D(-s033,s033,s033);
+
+    Static3D point0= new Static3D(-Y1*F,-Y1*F, 0);
+    Static3D point1= new Static3D(-Y1*F, Y1*F, 0);
+    Static3D point2= new Static3D( Y1*F, Y1*F, 0);
+    Static3D point3= new Static3D( Y1*F,-Y1*F, 0);
+
+    mDynScaleHand1.add(point1H);
+    mDynScaleShad1.add(scale1F);
+    mDynScaleHand2.add(point2H);
+    mDynScaleShad2.add(scale2F);
+
+    mDyn1.add(point0);
+    mDyn1.add(point1);
+    mDyn1.add(point2);
+    mDyn1.add(point3);
+    mDyn1.add(point0);
+
+    mDyn2.add(point2);
+    mDyn2.add(point3);
+    mDyn2.add(point0);
+    mDyn2.add(point1);
+    mDyn2.add(point2);
+
+    mDyn1.setConvexity(1.0f);
+    mDyn2.setConvexity(1.0f);
+
+    mDyn1.setSpeedMode(Dynamic.SPEED_MODE_SEGMENT_CONSTANT);
+    mDyn2.setSpeedMode(Dynamic.SPEED_MODE_SEGMENT_CONSTANT);
+
+    mPosition1.set(point0);
+    mPosition2.set(point2);
+
+    mDynMoveHand1.add(mPosition1);
+    mDynMoveShad1.add(mPosition1);
+    mDynMoveHand2.add(mPosition2);
+    mDynMoveShad2.add(mPosition2);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// both fingers move away from each other
+
+  private void setEffectsStage11()
+    {
+    resetDynamics1(11);
+    resetDynamics2(11);
+    resetDynamics3(11);
+
+    Static3D point1s= new Static3D(-X2+D,-Y1+D, 0);
+    Static3D point2s= new Static3D( X2+D, Y1+D, 0);
+    Static3D point1f= new Static3D(-Y1*F,-Y1*F, 0);
+    Static3D point2f= new Static3D( Y1*F, Y1*F, 0);
+    Static3D scale1F= new Static3D( s014,s014,s014);
+    Static3D point1H= new Static3D( s033,s033,s033);
+    Static3D scale2F= new Static3D(-s014,s014,s014);
+    Static3D point2H= new Static3D(-s033,s033,s033);
+
+    mDynScaleHand1.add(point1H);
+    mDynScaleShad1.add(scale1F);
+    mDynScaleHand2.add(point2H);
+    mDynScaleShad2.add(scale2F);
+
+    mDyn1.add(point1f);
+    mDyn1.add(point1s);
+    mDyn2.add(point2f);
+    mDyn2.add(point2s);
+
+    mDyn1.setConvexity(0.0f);
+    mDyn2.setConvexity(0.0f);
+
+    mDyn1.setSpeedMode(Dynamic.SPEED_MODE_SMOOTH);
+    mDyn2.setSpeedMode(Dynamic.SPEED_MODE_SMOOTH);
+
+    mPosition1.set(point1f);
+    mPosition2.set(point2f);
+
+    mDynMoveHand1.add(mPosition1);
+    mDynMoveShad1.add(mPosition1);
+    mDynMoveHand2.add(mPosition2);
+    mDynMoveShad2.add(mPosition2);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// both fingers un-touch the screen (part1)
+
+  private void setEffectsStage12()
+    {
+    resetDynamics1(12);
+    resetDynamics2(12);
+    resetDynamics3(12);
+
+    Static3D point1h = new Static3D(-X2+D,-Y1+D, 0);
+    Static3D point2h = new Static3D( X2+D, Y1+D, 0);
+    Static3D scale1S = new Static3D( s014,s014,s014);
+    Static3D scale1F = new Static3D( s001,s001,s001);
+    Static3D point1H = new Static3D( s033,s033,s033);
+    Static3D scale2S = new Static3D(-s014,s014,s014);
+    Static3D scale2F = new Static3D(-s001,s001,s001);
+    Static3D point2H = new Static3D(-s033,s033,s033);
+
+    mPosition1.set(scale1S);
+    mPosition2.set(scale2S);
+
+    mDyn1.add(scale1S);
+    mDyn1.add(scale1F);
+    mDyn2.add(scale2S);
+    mDyn2.add(scale2F);
+
+    mDynMoveHand1.add(point1h);
+    mDynMoveShad1.add(point1h);
+    mDynScaleHand1.add(point1H);
+    mDynScaleShad1.add(mPosition1);
+
+    mDynMoveHand2.add(point2h);
+    mDynMoveShad2.add(point2h);
+    mDynScaleHand2.add(point2H);
+    mDynScaleShad2.add(mPosition2);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// both fingers un-touch the screen (part2)
+
+  private void setEffectsStage13()
+    {
+    resetDynamics1(13);
+    resetDynamics2(13);
+    resetDynamics3(13);
+    resetDynamics4(13);
+
+    Static3D point1_0 = new Static3D(-X2  +D,-Y1  +D, 0);
+    Static3D point11H = new Static3D(-X2    ,-Y1    , 0);
+    Static3D point12H = new Static3D(-X1    ,-Y1    , 0);
+    Static3D point13H = new Static3D(-X0    ,-Y1    , 0);
+    Static3D point11S = new Static3D(-X2+2*D,-Y1+2*D, 0);
+    Static3D point12S = new Static3D(-X1+2*D,-Y1+2*D, 0);
+    Static3D point13S = new Static3D(-X0+2*D,-Y1+2*D, 0);
+    Static3D point1Sc = new Static3D( s033,s033,s033);
+
+    mPosition1.set(point1_0);
+    mDynMoveHand1.add(mPosition1);
+    mPosition2.set(point1_0);
+    mDynMoveShad1.add(mPosition2);
+
+    mDynScaleHand1.add(point1Sc);
+    mDynScaleShad1.add(point1Sc);
+
+    mDyn1.add(point1_0);
+    mDyn1.add(point11H);
+    mDyn1.add(point11H);
+    mDyn1.add(point12H);
+    mDyn1.add(point13H);
+
+    mDyn2.add(point1_0);
+    mDyn2.add(point11S);
+    mDyn2.add(point11S);
+    mDyn2.add(point12S);
+    mDyn2.add(point13S);
+
+    Static3D point2_0 = new Static3D( X2  +D, Y1  +D, 0);
+    Static3D point21H = new Static3D( X2    , Y1    , 0);
+    Static3D point22H = new Static3D( X1    , Y1    , 0);
+    Static3D point23H = new Static3D( X0    , Y1    , 0);
+    Static3D point21S = new Static3D( X2+2*D, Y1+2*D, 0);
+    Static3D point22S = new Static3D( X1+2*D, Y1+2*D, 0);
+    Static3D point23S = new Static3D( X0+2*D, Y1+2*D, 0);
+    Static3D point2Sc= new Static3D(-s033,s033,s033);
+
+    mPosition3.set(point2_0);
+    mDynMoveHand2.add(mPosition3);
+    mPosition4.set(point2_0);
+    mDynMoveShad2.add(mPosition4);
+
+    mDynScaleHand2.add(point2Sc);
+    mDynScaleShad2.add(point2Sc);
+
+    mDyn3.add(point2_0);
+    mDyn3.add(point21H);
+    mDyn3.add(point21H);
+    mDyn3.add(point22H);
+    mDyn3.add(point23H);
+
+    mDyn4.add(point2_0);
+    mDyn4.add(point21S);
+    mDyn4.add(point21S);
+    mDyn4.add(point22S);
+    mDyn4.add(point23S);
+
+    mNodes[0].changeInputSurface(mTextureShad);
+    mNodes[1].changeInputSurface(mTextureShad);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void createEffects()
+    {
+    mEffects = new DistortedEffects[NUM_EFFE];
+    for(int i=0; i<NUM_EFFE; i++) mEffects[i]= new DistortedEffects();
+
+    int time = DUR[0];
+
+    mDyn1 = new Dynamic3D(time,0.5f);
+    mDyn1.setMode(Dynamic.MODE_PATH);
+    mDyn1.setConvexity(0.0f);
+    mDyn2 = new Dynamic3D(time,0.5f);
+    mDyn2.setMode(Dynamic.MODE_PATH);
+    mDyn2.setConvexity(0.0f);
+    mDyn3 = new Dynamic3D(time,0.5f);
+    mDyn3.setMode(Dynamic.MODE_PATH);
+    mDyn3.setConvexity(0.0f);
+    mDyn4 = new Dynamic3D(time,0.5f);
+    mDyn4.setMode(Dynamic.MODE_PATH);
+    mDyn4.setConvexity(0.0f);
+
+    mPosition1 = new Static3D(0,0,0);
+    mPosition2 = new Static3D(0,0,0);
+    mPosition3 = new Static3D(0,0,0);
+    mPosition4 = new Static3D(0,0,0);
+
+    tmpBuffer = new float[12];
+
+    mDynMoveHand1 = new Dynamic3D(time,0.5f);
+    mDynMoveHand1.setMode(Dynamic.MODE_PATH);
+    mDynMoveHand1.setConvexity(0.0f);
+    mDynMoveShad1 = new Dynamic3D(time,0.5f);
+    mDynMoveShad1.setMode(Dynamic.MODE_PATH);
+    mDynMoveShad1.setConvexity(0.0f);
+    mDynScaleHand1 = new Dynamic3D(time,0.5f);
+    mDynScaleHand1.setMode(Dynamic.MODE_PATH);
+    mDynScaleHand1.setConvexity(0.0f);
+    mDynScaleShad1 = new Dynamic3D(time,0.5f);
+    mDynScaleShad1.setMode(Dynamic.MODE_PATH);
+    mDynScaleShad1.setConvexity(0.0f);
+
+    MatrixEffectMove  moveHand1 = new MatrixEffectMove(mDynMoveHand1);
+    MatrixEffectMove  moveShad1 = new MatrixEffectMove(mDynMoveShad1);
+    MatrixEffectScale scaleHand1= new MatrixEffectScale(mDynScaleHand1);
+    MatrixEffectScale scaleShad1= new MatrixEffectScale(mDynScaleShad1);
+
+    mEffects[0].apply(scaleShad1);
+    mEffects[0].apply(moveShad1);
+    mEffects[2].apply(scaleHand1);
+    mEffects[2].apply(moveHand1);
+
+    mDynMoveHand2 = new Dynamic3D(time,0.5f);
+    mDynMoveHand2.setMode(Dynamic.MODE_PATH);
+    mDynMoveHand2.setConvexity(0.0f);
+    mDynMoveShad2 = new Dynamic3D(time,0.5f);
+    mDynMoveShad2.setMode(Dynamic.MODE_PATH);
+    mDynMoveShad2.setConvexity(0.0f);
+    mDynScaleHand2 = new Dynamic3D(time,0.5f);
+    mDynScaleHand2.setMode(Dynamic.MODE_PATH);
+    mDynScaleHand2.setConvexity(0.0f);
+    mDynScaleShad2 = new Dynamic3D(time,0.5f);
+    mDynScaleShad2.setMode(Dynamic.MODE_PATH);
+    mDynScaleShad2.setConvexity(0.0f);
+
+    MatrixEffectMove  moveHand2 = new MatrixEffectMove(mDynMoveHand2);
+    MatrixEffectMove  moveShad2 = new MatrixEffectMove(mDynMoveShad2);
+    MatrixEffectScale scaleHand2= new MatrixEffectScale(mDynScaleHand2);
+    MatrixEffectScale scaleShad2= new MatrixEffectScale(mDynScaleShad2);
+
+    mEffects[1].apply(scaleShad2);
+    mEffects[1].apply(moveShad2);
+    mEffects[3].apply(scaleHand2);
+    mEffects[3].apply(moveHand2);
+
+    DistortedScreen screen = mControl.getScreen();
+    mWidth = screen.getWidth();
+    mHeight= screen.getHeight();
+
+    X0   = mWidth*0.65f;
+    X1   = mWidth*0.50f;
+    X2   = mWidth*0.35f;
+    Y1   = mHeight*0.28f;
+    D    = mWidth*0.01f;
+    s001 = mWidth*0.0001f;
+    s014 = mWidth*0.14f;
+    s033 = mWidth*0.33f;
+    F    = 0.50f;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void createNodes()
+    {
+    if( mNodes==null )
+      {
+      mNodes = new DistortedNode[NUM_NODE];
+      mQuad  = new MeshQuad();
+      }
+
+    TwistyActivity act = mControl.getActivity();
+
+    if( act!=null )
+      {
+      Bitmap bmpCirc = openBitmap(act, R.drawable.ui_fading_circle);
+      Bitmap bmpShad = openBitmap(act, R.drawable.ui_hand_shadow);
+      Bitmap bmpHand = openBitmap(act, R.drawable.ui_hand_pointer);
+
+      mTextureCirc = new DistortedTexture();
+      mTextureShad = new DistortedTexture();
+      DistortedTexture textureHand = new DistortedTexture();
+
+      if( bmpCirc!=null ) mTextureCirc.setTexture(bmpCirc);
+      if( bmpShad!=null ) mTextureShad.setTexture(bmpShad);
+      if( bmpHand!=null ) textureHand.setTexture(bmpHand);
+
+      mNodes[0]= new DistortedNode(mTextureShad,mEffects[0],mQuad);
+      mNodes[1]= new DistortedNode(mTextureShad,mEffects[1],mQuad);
+      mNodes[2]= new DistortedNode( textureHand,mEffects[2],mQuad);
+      mNodes[3]= new DistortedNode( textureHand,mEffects[3],mQuad);
+      }
+    else
+      {
+      android.util.Log.e("D", "Activity NULL!!");
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  long getEffectID()
+    {
+    return mEffectID;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  DistortedNode[] getNodes()
+    {
+    if( mEffects==null ) createEffects();
+    createNodes();
+    setEffectsStage1();
+
+    return mNodes;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  DistortedNode[] returnNodes()
+    {
+    return mNodes;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  ObjectAutomatorWhole(ObjectAutomator control)
+    {
+    mControl = control;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void stageFinished(int stage)
+    {
+    switch(stage)
+      {
+      case  1: setEffectsStage2() ; break;
+      case  2: setEffectsStage3() ; break;
+      case  3: setEffectsStage4() ; break;
+      case  4: setEffectsStage5() ; break;
+      case  5: setEffectsStage6() ; break;
+      case  6: setEffectsStage7() ; break;
+      case  7: setEffectsStage8() ; break;
+      case  8: setEffectsStage9() ; break;
+      case  9: setEffectsStage10(); break;
+      case 10: setEffectsStage11(); break;
+      case 11: setEffectsStage12(); break;
+      case 12: setEffectsStage13(); break;
+      default: setPostFrame(false);
+               mEffectID = -1;
+               mControl.effectFinished(mEffectID);
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// PUBLIC
+
+  public void postDrawFrame(long time)
+    {
+    if( mLastTime<0 ) mLastTime = time;
+    else mDiffTime = time - mLastTime;
+    mLastTime = time;
+
+    switch(DYN[mCurrentStage-1])
+      {
+      case 1: boolean finished1 = mDyn1.get( tmpBuffer,0, time, mDiffTime);
+              mPosition1.set(tmpBuffer[0], tmpBuffer[1], tmpBuffer[2]);
+
+              if( mCurrentStage==3 )
+                {
+                ObjectControl control = mControl.getObjectControl();
+                float x1 = tmpBuffer[0]+mWidth*0.5f;
+                float y1 = mHeight*0.5f-tmpBuffer[1];
+                //view.actionMove(x1,y1,0,0);
+                }
+
+              if( finished1 )
+                {
+                if( mCurrentStage==3 )
+                  {
+                  ObjectControl control = mControl.getObjectControl();
+                  //view.prepareUp();
+                  //view.actionUp();
+                  }
+
+                stageFinished(mCurrentStage);
+                }
+              break;
+      case 2: boolean finished2_1 = mDyn1.get( tmpBuffer,0, time, mDiffTime);
+              boolean finished2_2 = mDyn2.get( tmpBuffer,3, time, mDiffTime);
+              mPosition1.set(tmpBuffer[0], tmpBuffer[1], tmpBuffer[2]);
+              mPosition2.set(tmpBuffer[3], tmpBuffer[4], tmpBuffer[5]);
+
+              if( mCurrentStage==9 || mCurrentStage==10 || mCurrentStage==11 )
+                {
+                ObjectControl control = mControl.getObjectControl();
+                float x1 = tmpBuffer[0]+mWidth*0.5f;
+                float y1 = mHeight*0.5f-tmpBuffer[1];
+                float x2 = tmpBuffer[3]+mWidth*0.5f;
+                float y2 = mHeight*0.5f-tmpBuffer[4];
+                //view.prepareMove(x1,y1,x2,y2);
+                //view.actionMove(x1,y1,x2,y2);
+                }
+
+              if( finished2_1 && finished2_2 )
+                {
+                 if( mCurrentStage==11 )
+                  {
+                  ObjectControl control = mControl.getObjectControl();
+                  //view.prepareUp();
+                  //view.actionUp2(true,0,0,false,0,0);
+                  //view.actionUp();
+                  }
+
+                stageFinished(mCurrentStage);
+                }
+              break;
+      case 4: boolean finished4_1 = mDyn1.get( tmpBuffer,0, time, mDiffTime);
+              boolean finished4_2 = mDyn2.get( tmpBuffer,3, time, mDiffTime);
+              boolean finished4_3 = mDyn3.get( tmpBuffer,6, time, mDiffTime);
+              boolean finished4_4 = mDyn4.get( tmpBuffer,9, time, mDiffTime);
+              mPosition1.set(tmpBuffer[0], tmpBuffer[ 1], tmpBuffer[ 2]);
+              mPosition2.set(tmpBuffer[3], tmpBuffer[ 4], tmpBuffer[ 5]);
+              mPosition3.set(tmpBuffer[6], tmpBuffer[ 7], tmpBuffer[ 8]);
+              mPosition4.set(tmpBuffer[9], tmpBuffer[10], tmpBuffer[11]);
+              if( finished4_1 && finished4_2 && finished4_3 && finished4_4 ) stageFinished(mCurrentStage);
+              break;
+      }
+    }
+  }
diff --git a/src/main/java/org/distorted/objectlib/helpers/BlockController.java b/src/main/java/org/distorted/objectlib/helpers/BlockController.java
index 5e4a3ec3..bc2bfb49 100644
--- a/src/main/java/org/distorted/objectlib/helpers/BlockController.java
+++ b/src/main/java/org/distorted/objectlib/helpers/BlockController.java
@@ -21,7 +21,6 @@ package org.distorted.objectlib.helpers;
 
 import com.google.firebase.crashlytics.FirebaseCrashlytics;
 
-import java.lang.ref.WeakReference;
 import java.util.Timer;
 import java.util.TimerTask;
 
@@ -52,7 +51,7 @@ public class BlockController
   private long mTouchBlockTime, mUIBlockTime;
   private int mLastTouchPlace, mLastUIPlace;
 
-  private final WeakReference<TwistyActivity> mAct;
+  private final ObjectPreRender mPre;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
@@ -70,9 +69,9 @@ public class BlockController
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  public BlockController(TwistyActivity act)
+  public BlockController(TwistyActivity act, ObjectPreRender pre)
     {
-    mAct = new WeakReference<>(act);
+    mPre = pre;
 
     Timer timer = new Timer();
 
@@ -129,19 +128,8 @@ public class BlockController
         }
       else
         {
-        TwistyActivity act = mAct.get();
-        boolean reallyBlocked = true;
-
-        if( act!=null )
-          {
-          ObjectPreRender pre = act.getPreRender();
-          if( pre!=null )
-            {
-            reallyBlocked = pre.isTouchBlocked();
-            pre.unblockTouch();
-            }
-          }
-
+        boolean reallyBlocked = mPre.isTouchBlocked();
+        mPre.unblockTouch();
         reportTouchProblem(touchBlocked, reallyBlocked);
         }
       }
@@ -160,19 +148,8 @@ public class BlockController
         }
       else
         {
-        TwistyActivity act = mAct.get();
-        boolean reallyBlocked = true;
-
-        if( act!=null )
-          {
-          ObjectPreRender pre = act.getPreRender();
-          if( pre!=null )
-            {
-            reallyBlocked = !pre.isUINotBlocked();
-            pre.unblockUI();
-            }
-          }
-
+        boolean reallyBlocked =  !mPre.isUINotBlocked();
+        mPre.unblockUI();
         reportUIProblem(uiBlocked, reallyBlocked);
         }
       }
diff --git a/src/main/java/org/distorted/objectlib/helpers/TwistyActivity.java b/src/main/java/org/distorted/objectlib/helpers/TwistyActivity.java
index db20bda1..431caeba 100644
--- a/src/main/java/org/distorted/objectlib/helpers/TwistyActivity.java
+++ b/src/main/java/org/distorted/objectlib/helpers/TwistyActivity.java
@@ -22,7 +22,7 @@ package org.distorted.objectlib.helpers;
 import androidx.appcompat.app.AppCompatActivity;
 
 import org.distorted.library.main.DistortedScreen;
-import org.distorted.objectlib.main.ObjectPreRender;
+import org.distorted.objectlib.main.ObjectControl;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
@@ -32,7 +32,7 @@ abstract public class TwistyActivity extends AppCompatActivity
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  public abstract ObjectPreRender getPreRender();
+  public abstract ObjectControl getControl();
   public abstract boolean isLocked();
   public abstract DistortedScreen getScreen();
 
diff --git a/src/main/java/org/distorted/objectlib/main/ObjectControl.java b/src/main/java/org/distorted/objectlib/main/ObjectControl.java
index e0dccae8..447e0041 100644
--- a/src/main/java/org/distorted/objectlib/main/ObjectControl.java
+++ b/src/main/java/org/distorted/objectlib/main/ObjectControl.java
@@ -21,6 +21,7 @@ package org.distorted.objectlib.main;
 
 import java.lang.ref.WeakReference;
 
+import android.content.SharedPreferences;
 import android.util.DisplayMetrics;
 import android.view.MotionEvent;
 
@@ -28,6 +29,7 @@ import org.distorted.library.main.QuatHelper;
 import org.distorted.library.type.Static2D;
 import org.distorted.library.type.Static4D;
 
+import org.distorted.objectlib.helpers.MovesFinished;
 import org.distorted.objectlib.helpers.ObjectStateActioner;
 import org.distorted.objectlib.helpers.TwistyActivity;
 
@@ -486,6 +488,16 @@ public class ObjectControl
         }
       }
 
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// INTERNAL API (for AutomaticControl)
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public ObjectPreRender getPreRender()
+      {
+      return mPreRender;
+      }
+
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 // PUBLIC API
 ///////////////////////////////////////////////////////////////////////////////////////////////////
@@ -519,8 +531,8 @@ public class ObjectControl
       {
       mScreenWidth = width;
       mScreenHeight= height;
-
       mScreenMin = Math.min(width, height);
+      mPreRender.setScreenSize(width);
       }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
@@ -554,9 +566,128 @@ public class ObjectControl
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-    public ObjectPreRender getPreRender()
+    public void preRender()
       {
-      return mPreRender;
+      mPreRender.preRender();
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public void blockEverything(int place)
+      {
+      mPreRender.blockEverything(place);
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public void blockTouch(int place)
+      {
+      mPreRender.blockTouch(place);
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public void unblockEverything()
+      {
+      mPreRender.unblockEverything();
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public void unblockTouch()
+      {
+      mPreRender.unblockTouch();
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public void unblockUI()
+      {
+      mPreRender.unblockUI();
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public boolean isTouchBlocked()
+      {
+      return mPreRender.isTouchBlocked();
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public boolean isUINotBlocked()
+      {
+      return mPreRender.isUINotBlocked();
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public void initializeObject(int[][] moves)
+      {
+      mPreRender.initializeObject(moves);
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public void changeObject(ObjectType object)
+      {
+      mPreRender.changeObject(object);
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public void setupObject(ObjectType object, int[][] moves)
+      {
+      mPreRender.setupObject(object,moves);
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public void scrambleObject(int num)
+      {
+      mPreRender.scrambleObject(num);
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public void solveObject()
+      {
+      mPreRender.solveObject();
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public void addRotation(MovesFinished listener, int axis, int rowBitmap, int angle, long duration)
+      {
+      mPreRender.addRotation(listener,axis,rowBitmap,angle,duration);
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public void resetAllTextureMaps()
+      {
+      mPreRender.resetAllTextureMaps();
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public TwistyObject getObject()
+      {
+      return mPreRender.getObject();
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public void savePreferences(SharedPreferences.Editor editor)
+      {
+      mPreRender.savePreferences(editor);
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public void restorePreferences(SharedPreferences preferences)
+      {
+      mPreRender.restorePreferences(preferences);
       }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
diff --git a/src/main/java/org/distorted/objectlib/main/ObjectPreRender.java b/src/main/java/org/distorted/objectlib/main/ObjectPreRender.java
index 97a99fea..6958ff08 100644
--- a/src/main/java/org/distorted/objectlib/main/ObjectPreRender.java
+++ b/src/main/java/org/distorted/objectlib/main/ObjectPreRender.java
@@ -91,7 +91,7 @@ public class ObjectPreRender implements EffectController
 
     mEffectID = new long[BaseEffect.Type.LENGTH];
 
-    mBlockController = new BlockController(act);
+    mBlockController = new BlockController(act,this);
     unblockEverything();
     }
 
diff --git a/src/main/res/drawable-nodpi/ui_axis_arrow.png b/src/main/res/drawable-nodpi/ui_axis_arrow.png
new file mode 100644
index 00000000..3ba666ba
Binary files /dev/null and b/src/main/res/drawable-nodpi/ui_axis_arrow.png differ
diff --git a/src/main/res/drawable-nodpi/ui_fading_circle.png b/src/main/res/drawable-nodpi/ui_fading_circle.png
new file mode 100644
index 00000000..a1c9ae21
Binary files /dev/null and b/src/main/res/drawable-nodpi/ui_fading_circle.png differ
diff --git a/src/main/res/drawable-nodpi/ui_hand_pointer.png b/src/main/res/drawable-nodpi/ui_hand_pointer.png
new file mode 100644
index 00000000..78cc84e6
Binary files /dev/null and b/src/main/res/drawable-nodpi/ui_hand_pointer.png differ
diff --git a/src/main/res/drawable-nodpi/ui_hand_shadow.png b/src/main/res/drawable-nodpi/ui_hand_shadow.png
new file mode 100644
index 00000000..58e737b8
Binary files /dev/null and b/src/main/res/drawable-nodpi/ui_hand_shadow.png differ
