commit beb325a0c0f7afd856d31e5c6f6e45aaf1b53450
Author: Leszek Koltunski <leszek@koltunski.pl>
Date:   Fri Feb 14 01:47:34 2020 +0000

    Major restructuring - separate the Manipulated Objects (i.e. at the time being - Cubes of various sizes) and the class holding knowledge how those Objects move ( RubikCubeMovement ) into a separate package; remove all knowledge of Objects and the way they move from the main package.

diff --git a/src/main/java/org/distorted/effect/BaseEffect.java b/src/main/java/org/distorted/effect/BaseEffect.java
index df9ac3f8..b71531a2 100644
--- a/src/main/java/org/distorted/effect/BaseEffect.java
+++ b/src/main/java/org/distorted/effect/BaseEffect.java
@@ -19,6 +19,7 @@
 
 package org.distorted.effect;
 
+import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
 import android.content.SharedPreferences;
 
@@ -195,60 +196,18 @@ public class BaseEffect
 
   ////////////////////////////////////////////////////////////////////////////////
 
-    public long startEffect(RubikRenderer renderer)
+    public long startEffect(RubikRenderer renderer) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException
       {
-      Method method1, method2;
-      BaseEffect baseEffect=null;
+      Method method1 = mClass.getDeclaredMethod("create", int.class);
 
-      try
-        {
-        method1 = mClass.getDeclaredMethod("create", int.class);
-        }
-      catch(NoSuchMethodException ex)
-        {
-        android.util.Log.e("BaseEffect", mClass.getSimpleName()+": 1 exception getting method: "+ex.getMessage());
-        return -1;
-        }
-
-      try
-        {
-        if( method1!=null )
-          {
-          Object value = method1.invoke(null,mCurrentType);
-          baseEffect = (BaseEffect)value;
-          }
-        }
-      catch(Exception ex)
-        {
-        android.util.Log.e("BaseEffect", mClass.getSimpleName()+": 1 exception invoking method: "+ex.getMessage());
-        return -2;
-        }
+      Object value1 = method1.invoke(null,mCurrentType);
+      BaseEffect baseEffect = (BaseEffect)value1;
 
-      try
-        {
-        method2 = mClass.getDeclaredMethod("start", int.class, RubikRenderer.class);
-        }
-      catch(NoSuchMethodException ex)
-        {
-        android.util.Log.e("BaseEffect", mClass.getSimpleName()+": 2 exception getting method: "+ex.getMessage());
-        return -3;
-        }
-
-      try
-        {
-        if( method2!=null )
-          {
-          Integer translated = translatePos(mCurrentPos)+1;
-          Object value = method2.invoke(baseEffect,translated,renderer);
-          return (Long)value;
-          }
-        }
-      catch(Exception ex)
-        {
-        android.util.Log.e("BaseEffect", mClass.getSimpleName()+": 2 exception invoking method: "+ex.getMessage());
-        }
+      Method method2 = mClass.getDeclaredMethod("start", int.class, RubikRenderer.class);
 
-      return -4;
+      Integer translated = translatePos(mCurrentPos)+1;
+      Object value2 = method2.invoke(baseEffect,translated,renderer);
+      return (Long)value2;
       }
 
   ////////////////////////////////////////////////////////////////////////////////
diff --git a/src/main/java/org/distorted/effect/scramble/ScrambleEffect.java b/src/main/java/org/distorted/effect/scramble/ScrambleEffect.java
index e90254dd..d80b570d 100644
--- a/src/main/java/org/distorted/effect/scramble/ScrambleEffect.java
+++ b/src/main/java/org/distorted/effect/scramble/ScrambleEffect.java
@@ -23,7 +23,7 @@ import org.distorted.effect.BaseEffect;
 import org.distorted.library.effect.Effect;
 import org.distorted.library.main.DistortedEffects;
 import org.distorted.library.message.EffectListener;
-import org.distorted.magic.RubikCube;
+import org.distorted.object.RubikCube;
 import org.distorted.magic.RubikRenderer;
 
 import java.lang.reflect.Method;
@@ -304,6 +304,8 @@ public abstract class ScrambleEffect extends BaseEffect implements EffectListene
     mCube     = renderer.getCube();
     mListener = renderer;
 
+    mCube.solve();
+
     int numScrambles = renderer.getNumScrambles();
     int dura = (int)(duration*Math.pow(numScrambles,0.6f));
     createBaseEffects(dura,numScrambles);
diff --git a/src/main/java/org/distorted/effect/sizechange/SizeChangeEffect.java b/src/main/java/org/distorted/effect/sizechange/SizeChangeEffect.java
index 2b6c8904..0b704956 100644
--- a/src/main/java/org/distorted/effect/sizechange/SizeChangeEffect.java
+++ b/src/main/java/org/distorted/effect/sizechange/SizeChangeEffect.java
@@ -24,7 +24,7 @@ import org.distorted.library.effect.Effect;
 import org.distorted.library.main.DistortedEffects;
 import org.distorted.library.main.DistortedScreen;
 import org.distorted.library.message.EffectListener;
-import org.distorted.magic.RubikCube;
+import org.distorted.object.RubikCube;
 import org.distorted.magic.RubikRenderer;
 
 import java.lang.reflect.Method;
diff --git a/src/main/java/org/distorted/effect/solve/SolveEffect.java b/src/main/java/org/distorted/effect/solve/SolveEffect.java
index 9a31ba0e..1d3dd709 100644
--- a/src/main/java/org/distorted/effect/solve/SolveEffect.java
+++ b/src/main/java/org/distorted/effect/solve/SolveEffect.java
@@ -24,7 +24,7 @@ import org.distorted.library.effect.Effect;
 import org.distorted.library.main.DistortedEffects;
 import org.distorted.library.main.DistortedScreen;
 import org.distorted.library.message.EffectListener;
-import org.distorted.magic.RubikCube;
+import org.distorted.object.RubikCube;
 import org.distorted.magic.RubikRenderer;
 
 import java.lang.reflect.Method;
diff --git a/src/main/java/org/distorted/effect/win/WinEffect.java b/src/main/java/org/distorted/effect/win/WinEffect.java
index 76e228cb..f5942e68 100644
--- a/src/main/java/org/distorted/effect/win/WinEffect.java
+++ b/src/main/java/org/distorted/effect/win/WinEffect.java
@@ -24,7 +24,7 @@ import org.distorted.library.effect.Effect;
 import org.distorted.library.main.DistortedEffects;
 import org.distorted.library.main.DistortedScreen;
 import org.distorted.library.message.EffectListener;
-import org.distorted.magic.RubikCube;
+import org.distorted.object.RubikCube;
 import org.distorted.magic.RubikRenderer;
 
 import java.lang.reflect.Method;
diff --git a/src/main/java/org/distorted/magic/RubikActivity.java b/src/main/java/org/distorted/magic/RubikActivity.java
index 2dd4375e..a827d3c2 100644
--- a/src/main/java/org/distorted/magic/RubikActivity.java
+++ b/src/main/java/org/distorted/magic/RubikActivity.java
@@ -20,18 +20,11 @@
 package org.distorted.magic;
 
 import android.content.SharedPreferences;
-import android.graphics.PorterDuff;
-import android.graphics.drawable.Drawable;
 import android.opengl.GLSurfaceView;
 import android.os.Bundle;
 import android.preference.PreferenceManager;
-import android.support.v4.content.ContextCompat;
 import android.support.v7.app.AppCompatActivity;
-import android.util.DisplayMetrics;
 import android.view.View;
-import android.view.ViewGroup;
-import android.widget.ImageButton;
-import android.widget.LinearLayout;
 
 import org.distorted.component.HorizontalNumberPicker;
 import org.distorted.library.main.DistortedLibrary;
@@ -45,30 +38,8 @@ public class RubikActivity extends AppCompatActivity implements View.OnClickList
     public static final int DEF_SCRAMBLE =  1;
     public static final int MAX_SCRAMBLE = 18;
 
-    private static int mButton = RubikSize.SIZE3.ordinal();
     private HorizontalNumberPicker mPicker;
 
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    private void markButton(int button)
-      {
-      mButton = button;
-
-      for(int b=0; b<RubikSize.LENGTH; b++)
-        {
-        Drawable d = findViewById(b).getBackground();
-
-        if( b==button )
-          {
-          d.setColorFilter(ContextCompat.getColor(this,R.color.red), PorterDuff.Mode.MULTIPLY);
-          }
-        else
-          {
-          d.clearColorFilter();
-          }
-        }
-      }
-
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
     private void savePreferences()
@@ -102,37 +73,6 @@ public class RubikActivity extends AppCompatActivity implements View.OnClickList
       mPicker.setValue(scramble);
       }
 
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    private void addSizeButtons()
-      {
-      LinearLayout layout = findViewById(R.id.sizeLayout);
-      DisplayMetrics metrics = getResources().getDisplayMetrics();
-      float scale = metrics.density;
-      int size = (int)(64*scale +0.5f);
-      int padding = (int)(3*scale + 0.5f);
-      ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(size,size);
-
-      for(int i=0; i<RubikSize.LENGTH; i++)
-        {
-        ImageButton button = new ImageButton(this);
-        button.setLayoutParams(params);
-        button.setId(i);
-        button.setPadding(padding,0,padding,0);
-        int iconID = RubikSize.getSize(i).getIconID();
-        button.setImageResource(iconID);
-        button.setOnClickListener(this);
-        layout.addView(button);
-        }
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    static int getRedButton()
-      {
-      return mButton;
-      }
-
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
     @Override
@@ -141,8 +81,9 @@ public class RubikActivity extends AppCompatActivity implements View.OnClickList
       super.onCreate(savedState);
       setTheme(R.style.CustomActivityThemeNoActionBar);
       setContentView(R.layout.main);
-      addSizeButtons();
-      markButton(mButton);
+      RubikSurfaceView view = findViewById(R.id.rubikSurfaceView);
+      view.addSizeButtons(this);
+      view.markButton(view.getRedButton());
 
       mPicker = findViewById(R.id.rubikNumberPicker);
       mPicker.setMin(MIN_SCRAMBLE);
@@ -199,7 +140,7 @@ public class RubikActivity extends AppCompatActivity implements View.OnClickList
 
         if( success )
           {
-          markButton(id);
+          view.markButton(id);
           }
         }
       }
@@ -218,8 +159,9 @@ public class RubikActivity extends AppCompatActivity implements View.OnClickList
 
     public void Scores(View v)
       {
+      RubikSurfaceView view = findViewById(R.id.rubikSurfaceView);
       Bundle bundle = new Bundle();
-      bundle.putInt("tab", mButton);
+      bundle.putInt("tab", view.getRedButton());
 
       RubikScores scores = new RubikScores();
       scores.setArguments(bundle);
@@ -242,6 +184,7 @@ public class RubikActivity extends AppCompatActivity implements View.OnClickList
 
       RubikSurfaceView view = findViewById(R.id.rubikSurfaceView);
       view.getRenderer().scrambleCube(scramble);
+      view.enterScrambleMode();
       }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
@@ -250,5 +193,6 @@ public class RubikActivity extends AppCompatActivity implements View.OnClickList
       {
       RubikSurfaceView view = findViewById(R.id.rubikSurfaceView);
       view.getRenderer().solveCube();
+      view.leaveScrambleMode();
       }
 }
diff --git a/src/main/java/org/distorted/magic/RubikCube.java b/src/main/java/org/distorted/magic/RubikCube.java
deleted file mode 100644
index 58f41501..00000000
--- a/src/main/java/org/distorted/magic/RubikCube.java
+++ /dev/null
@@ -1,690 +0,0 @@
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Copyright 2019 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.magic;
-
-import android.graphics.Bitmap;
-import android.graphics.Canvas;
-import android.graphics.Paint;
-
-import org.distorted.library.effect.Effect;
-import org.distorted.library.effect.MatrixEffectMove;
-import org.distorted.library.effect.MatrixEffectQuaternion;
-import org.distorted.library.effect.MatrixEffectRotate;
-import org.distorted.library.effect.MatrixEffectScale;
-import org.distorted.library.effect.VertexEffectSink;
-import org.distorted.library.main.DistortedEffects;
-import org.distorted.library.main.DistortedNode;
-import org.distorted.library.main.DistortedTexture;
-import org.distorted.library.mesh.MeshCubes;
-import org.distorted.library.mesh.MeshFlat;
-import org.distorted.library.message.EffectListener;
-import org.distorted.library.type.Dynamic1D;
-import org.distorted.library.type.Static1D;
-import org.distorted.library.type.Static3D;
-import org.distorted.library.type.Static4D;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-public class RubikCube extends DistortedNode
-{
-    private static final int POST_ROTATION_MILLISEC = 500;
-    private static final int TEXTURE_SIZE = 100;
-
-    private static final Static3D VectX = new Static3D(1,0,0);
-    private static final Static3D VectY = new Static3D(0,1,0);
-    private static final Static3D VectZ = new Static3D(0,0,1);
-
-    public static final int VECTX = 0;  //
-    public static final int VECTY = 1;  // don't change this
-    public static final int VECTZ = 2;  //
-
-    private DistortedNode[][][] mNodes;
-    private MeshCubes[][][] mCubes;
-    private DistortedEffects[][][] mEffects;
-    private Static4D[][][] mQuatScramble;
-    private Static3D[][][] mRotationAxis;
-    private Dynamic1D[][][] mRotationAngle;
-    private Static3D[][][] mCurrentPosition;
-    private MatrixEffectRotate[][][] mRotateEffect;
-    private Static1D mRotationAngleStatic, mRotationAngleMiddle, mRotationAngleFinal;
-    private Static3D mMove, mScale, mNodeMove, mNodeScale;
-    private Static4D mQuatAccumulated;
-    private DistortedTexture mTexture;
-
-    private int mRotAxis, mRotRow;
-    private int mSize;
-
-    private DistortedTexture mNodeTexture;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    RubikCube(int size, Static4D quatCur, Static4D quatAcc, DistortedTexture texture, MeshFlat mesh, DistortedEffects effects)
-      {
-      super(texture,effects,mesh);
-
-      mNodeTexture = texture;
-
-      mSize = size;
-
-      mRotationAngleStatic = new Static1D(0);
-      mRotationAngleMiddle = new Static1D(0);
-      mRotationAngleFinal  = new Static1D(0);
-
-      mMove     = new Static3D(0,0,0);
-      mScale    = new Static3D(1,1,1);
-      mNodeMove = new Static3D(0,0,0);
-      mNodeScale= new Static3D(1,1,1);
-
-      mQuatAccumulated = quatAcc;
-
-      mRotAxis = VECTX;
-      mTexture = new DistortedTexture(TEXTURE_SIZE,TEXTURE_SIZE);
-
-      mNodes          = new DistortedNode[mSize][mSize][mSize];
-      mCubes          = new MeshCubes[mSize][mSize][mSize];
-      mEffects        = new DistortedEffects[mSize][mSize][mSize];
-      mQuatScramble   = new Static4D[mSize][mSize][mSize];
-      mRotationAxis   = new Static3D[mSize][mSize][mSize];
-      mRotationAngle  = new Dynamic1D[mSize][mSize][mSize];
-      mCurrentPosition= new Static3D[mSize][mSize][mSize];
-      mRotateEffect   = new MatrixEffectRotate[mSize][mSize][mSize];
-
-      Static3D[][][] cubeVectors = new Static3D[mSize][mSize][mSize];
-
-      Static3D sinkCenter = new Static3D(TEXTURE_SIZE*0.5f, TEXTURE_SIZE*0.5f, TEXTURE_SIZE*0.5f);
-      Static3D matrCenter = new Static3D(0,0,0);
-      Static4D region = new Static4D(0,0,0, TEXTURE_SIZE*0.72f);
-
-      VertexEffectSink        sinkEffect = new VertexEffectSink( new Static1D(getSinkStrength()), sinkCenter, region );
-      MatrixEffectMove        moveEffect = new MatrixEffectMove(mMove);
-      MatrixEffectScale      scaleEffect = new MatrixEffectScale(mScale);
-      MatrixEffectQuaternion quatCEffect = new MatrixEffectQuaternion(quatCur, matrCenter);
-      MatrixEffectQuaternion quatAEffect = new MatrixEffectQuaternion(quatAcc, matrCenter);
-
-      MatrixEffectMove       nodeMoveEffect  = new MatrixEffectMove(mNodeMove);
-      MatrixEffectScale      nodeScaleEffect = new MatrixEffectScale(mNodeScale);
-
-      effects.apply(nodeScaleEffect);
-      effects.apply(nodeMoveEffect);
-
-      // 3x2 bitmap = 6 squares:
-      //
-      // RED     GREEN   BLUE
-      // YELLOW  WHITE   BROWN
-
-      final float ze = 0.0f;
-      final float ot = 1.0f/3.0f;
-      final float tt = 2.0f/3.0f;
-      final float oh = 1.0f/2.0f;
-      final float of = 1.0f/40.0f;
-
-      final Static4D mapFront = new Static4D(ze,oh, ze+ot,oh+oh);
-      final Static4D mapBack  = new Static4D(tt,ze, tt+ot,ze+oh);
-      final Static4D mapLeft  = new Static4D(ot,ze, ot+ot,ze+oh);
-      final Static4D mapRight = new Static4D(ze,ze, ze+ot,ze+oh);
-      final Static4D mapTop   = new Static4D(tt,oh, tt+ot,oh+oh);
-      final Static4D mapBottom= new Static4D(ot,oh, ot+ot,oh+oh);
-
-      final Static4D mapBlack = new Static4D(ze,ze, ze+of,ze+of);
-
-      Static4D tmpFront, tmpBack, tmpLeft, tmpRight, tmpTop, tmpBottom;
-      float nc = 0.5f*mSize;
-      int vertices = (int)(24.0f/mSize + 2.0f);
-
-      for(int x = 0; x< mSize; x++)
-        for(int y = 0; y< mSize; y++)
-          for(int z = 0; z< mSize; z++)
-            {
-            if( x==0 || x==mSize-1 || y==0 || y==mSize-1 || z==0 || z==mSize-1 ) // only the external walls
-              {
-              tmpLeft  = (x==       0 ? mapLeft  :mapBlack);
-              tmpRight = (x== mSize-1 ? mapRight :mapBlack);
-              tmpFront = (z== mSize-1 ? mapFront :mapBlack);
-              tmpBack  = (z==       0 ? mapBack  :mapBlack);
-              tmpTop   = (y== mSize-1 ? mapTop   :mapBlack);
-              tmpBottom= (y==       0 ? mapBottom:mapBlack);
-
-              mCubes[x][y][z]           = new MeshCubes(vertices,vertices,vertices, tmpFront, tmpBack, tmpLeft, tmpRight, tmpTop, tmpBottom);
-              cubeVectors[x][y][z]      = new Static3D( TEXTURE_SIZE*(x-nc), TEXTURE_SIZE*(y-nc), TEXTURE_SIZE*(z-nc) );
-              mQuatScramble[x][y][z]    = new Static4D(0,0,0,1);
-              mRotationAngle[x][y][z]   = new Dynamic1D();
-              mRotationAxis[x][y][z]    = new Static3D(1,0,0);
-              mCurrentPosition[x][y][z] = new Static3D(x,y,z);
-              mRotateEffect[x][y][z]    = new MatrixEffectRotate(mRotationAngle[x][y][z], mRotationAxis[x][y][z], matrCenter);
-
-              mEffects[x][y][z] = new DistortedEffects();
-              mEffects[x][y][z].apply(sinkEffect);
-              mEffects[x][y][z].apply( new MatrixEffectMove(cubeVectors[x][y][z]) );
-              mEffects[x][y][z].apply( new MatrixEffectQuaternion(mQuatScramble[x][y][z], matrCenter));
-              mEffects[x][y][z].apply(mRotateEffect[x][y][z]);
-              mEffects[x][y][z].apply(quatAEffect);
-              mEffects[x][y][z].apply(quatCEffect);
-              mEffects[x][y][z].apply(scaleEffect);
-              mEffects[x][y][z].apply(moveEffect);
-
-              mNodes[x][y][z] = new DistortedNode(mTexture,mEffects[x][y][z],mCubes[x][y][z]);
-
-              attach(mNodes[x][y][z]);
-              }
-            }
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    private int computeNearestAngle(float angle)
-      {
-      final int NEAREST = 90;
-
-      int tmp = (int)((angle+NEAREST/2)/NEAREST);
-      if( angle< -(NEAREST/2) ) tmp-=1;
-
-      return NEAREST*tmp;
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// All legal rotation quats must have all four of their components equal to either
-// 0, 1, -1, 0.5, -0.5 or +-sqrt(2)/2.
-//
-// Because of quatMultiplication, errors can accumulate - so to avoid this, we
-// correct the value of the 'scramble' quat to what it should be.
-//
-// We also have to remember that the group of unit quaternions is a double-cover of rotations
-// in 3D ( q represents the same rotation as -q ) - so invert if needed.
-
-    private static final float SQ2 = 0.5f*((float)Math.sqrt(2));
-    private static final float[] LEGAL = { 0.0f , 0.5f , -0.5f , 1.0f , -1.0f , SQ2 , -SQ2 };
-
-    private void normalizeScrambleQuat(int i, int j, int k)
-      {
-      Static4D quat = mQuatScramble[i][j][k];
-
-      float x = quat.get1();
-      float y = quat.get2();
-      float z = quat.get3();
-      float w = quat.get4();
-      float diff;
-
-      for(float legal: LEGAL)
-        {
-        diff = x-legal;
-        if( diff*diff<0.01f ) x = legal;
-        diff = y-legal;
-        if( diff*diff<0.01f ) y = legal;
-        diff = z-legal;
-        if( diff*diff<0.01f ) z = legal;
-        diff = w-legal;
-        if( diff*diff<0.01f ) w = legal;
-        }
-
-      if( w<0 )
-        {
-        w = -w;
-        z = -z;
-        y = -y;
-        x = -x;
-        }
-      else if( w==0 )
-        {
-        if( z<0 )
-          {
-          z = -z;
-          y = -y;
-          x = -x;
-          }
-        else if( z==0 )
-          {
-          if( y<0 )
-            {
-            y = -y;
-            x = -x;
-            }
-          else if( y==0 )
-            {
-            if( x<0 )
-              {
-              x = -x;
-              }
-            }
-          }
-        }
-
-      mQuatScramble[i][j][k].set(x,y,z,w);
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    private float getSinkStrength()
-      {
-      switch(mSize)
-        {
-        case 1 : return 1.1f;
-        case 2 : return 1.5f;
-        case 3 : return 1.8f;
-        case 4 : return 2.0f;
-        default: return 3.0f - 4.0f/mSize;
-        }
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    private boolean belongsToRotation(int x, int y, int z, int vector, int row)
-      {
-      switch(vector)
-        {
-        case VECTX: return mCurrentPosition[x][y][z].get1()==row;
-        case VECTY: return mCurrentPosition[x][y][z].get2()==row;
-        case VECTZ: return mCurrentPosition[x][y][z].get3()==row;
-        }
-
-      return false;
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    private void modifyCurrentPosition(int x, int y, int z, Static4D quat)
-      {
-      Static3D current = mCurrentPosition[x][y][z];
-      float diff = 0.5f*(mSize-1);
-      float cubitCenterX = current.get1() - diff;
-      float cubitCenterY = current.get2() - diff;
-      float cubitCenterZ = current.get3() - diff;
-
-      Static4D cubitCenter =  new Static4D(cubitCenterX, cubitCenterY, cubitCenterZ, 0);
-      Static4D rotatedCenter = RubikSurfaceView.rotateVectorByQuat( cubitCenter, quat);
-
-      float rotatedX = rotatedCenter.get1() + diff;
-      float rotatedY = rotatedCenter.get2() + diff;
-      float rotatedZ = rotatedCenter.get3() + diff;
-
-      int roundedX = (int)(rotatedX+0.1f);
-      int roundedY = (int)(rotatedY+0.1f);
-      int roundedZ = (int)(rotatedZ+0.1f);
-
-      mCurrentPosition[x][y][z].set1(roundedX);
-      mCurrentPosition[x][y][z].set2(roundedY);
-      mCurrentPosition[x][y][z].set3(roundedZ);
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    boolean isSolved()
-      {
-      Static4D q = mQuatScramble[0][0][0];
-
-      float x = q.get1();
-      float y = q.get2();
-      float z = q.get3();
-      float w = q.get4();
-
-      for(int i = 0; i< mSize; i++)
-        for(int j = 0; j< mSize; j++)
-          for(int k = 0; k< mSize; k++)
-            {
-            if( i==0 || i==mSize-1 || j==0 || j==mSize-1 || k==0 || k==mSize-1 )
-              {
-              q = mQuatScramble[i][j][k];
-
-              if( q.get1()!=x || q.get2()!=y || q.get3()!=z || q.get4()!=w )
-                {
-                return false;
-                }
-              }
-            }
-
-      return true;
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// all DistortedTextures, DistortedNodes, DistortedFramebuffers, DistortedScreens and all types of
-// Meshes HAVE TO be markedForDeletion when they are no longer needed- otherwise we have a major
-// memory leak.
-
-    void releaseResources()
-      {
-      mTexture.markForDeletion();
-
-      for(int x=0; x<mSize; x++)
-        for(int y=0; y<mSize; y++)
-          for(int z=0; z<mSize; z++)
-            {
-            if( x==0 || x==mSize-1 || y==0 || y==mSize-1 || z==0 || z==mSize-1 )
-              {
-              mCubes[x][y][z].markForDeletion();
-              mNodes[x][y][z].markForDeletion();
-              }
-            }
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    void addNewRotation(int vector, int row )
-      {
-      Static3D axis = VectX;
-
-      switch(vector)
-        {
-        case VECTX: axis = VectX; break;
-        case VECTY: axis = VectY; break;
-        case VECTZ: axis = VectZ; break;
-        }
-
-      mRotAxis = vector;
-      mRotRow  = row;
-
-      mRotationAngleStatic.set1(0.0f);
-
-      for(int x=0; x<mSize; x++)
-        for(int y=0; y<mSize; y++)
-          for(int z=0; z<mSize; z++)
-            if( x==0 || x==mSize-1 || y==0 || y==mSize-1 || z==0 || z==mSize-1 )
-              {
-              if( belongsToRotation(x,y,z,vector,mRotRow) )
-                {
-                mRotationAxis[x][y][z].set(axis);
-                mRotationAngle[x][y][z].add(mRotationAngleStatic);
-                }
-              }
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    long finishRotationNow(EffectListener listener)
-      {
-      boolean first = true;
-      long effectID=0;
-
-      for(int x=0; x<mSize; x++)
-        for(int y=0; y<mSize; y++)
-          for(int z=0; z<mSize; z++)
-            if( x==0 || x==mSize-1 || y==0 || y==mSize-1 || z==0 || z==mSize-1 )
-              {
-              if( belongsToRotation(x,y,z,mRotAxis,mRotRow) )
-                {
-                if( first )
-                  {
-                  first = false;
-                  mRotateEffect[x][y][z].notifyWhenFinished(listener);
-                  effectID = mRotateEffect[x][y][z].getID();
-                  int pointNum = mRotationAngle[x][y][z].getNumPoints();
-
-                  if( pointNum>=1 )
-                    {
-                    float startingAngle = mRotationAngle[x][y][z].getPoint(pointNum-1).get1();
-                    int nearestAngleInDegrees = computeNearestAngle(startingAngle);
-                    mRotationAngleStatic.set1(startingAngle);
-                    mRotationAngleFinal.set1(nearestAngleInDegrees);
-                    mRotationAngleMiddle.set1( nearestAngleInDegrees + (nearestAngleInDegrees-startingAngle)*0.2f );
-                    }
-                  else
-                    {
-                    android.util.Log.e("cube", "ERROR finishing rotation!");
-                    return 0;
-                    }
-                  }
-
-                mRotationAngle[x][y][z].setDuration(POST_ROTATION_MILLISEC);
-                mRotationAngle[x][y][z].resetToBeginning();
-                mRotationAngle[x][y][z].removeAll();
-                mRotationAngle[x][y][z].add(mRotationAngleStatic);
-                mRotationAngle[x][y][z].add(mRotationAngleMiddle);
-                mRotationAngle[x][y][z].add(mRotationAngleFinal);
-                }
-              }
-
-      return effectID;
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    void continueRotation(float angleInDegrees)
-      {
-      mRotationAngleStatic.set1(angleInDegrees);
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    void createTexture()
-      {
-      Bitmap bitmap;
-
-      final int S = 128;
-      final int W = 3*S;
-      final int H = 2*S;
-      final int R = S/10;
-      final int M = S/20;
-
-      Paint paint = new Paint();
-      bitmap = Bitmap.createBitmap(W,H, Bitmap.Config.ARGB_8888);
-      Canvas canvas = new Canvas(bitmap);
-
-      paint.setAntiAlias(true);
-      paint.setTextAlign(Paint.Align.CENTER);
-      paint.setStyle(Paint.Style.FILL);
-
-      // 3x2 bitmap = 6 squares:
-      //
-      // RED     GREEN   BLUE
-      // YELLOW  WHITE   BROWN
-
-      paint.setColor(0xff000000);                                  // BLACK BACKGROUND
-      canvas.drawRect(0, 0, W, H, paint);                          //
-
-      paint.setColor(0xffff0000);                                  // RED
-      canvas.drawRoundRect(    M,   M,   S-M,   S-M, R, R, paint); //
-      paint.setColor(0xff00ff00);                                  // GREEN
-      canvas.drawRoundRect(  S+M,   M, 2*S-M,   S-M, R, R, paint); //
-      paint.setColor(0xff0000ff);                                  // BLUE
-      canvas.drawRoundRect(2*S+M,   M, 3*S-M,   S-M, R, R, paint); //
-      paint.setColor(0xffffff00);                                  // YELLOW
-      canvas.drawRoundRect(    M, S+M,   S-M, 2*S-M, R, R, paint); //
-      paint.setColor(0xffffffff);                                  // WHITE
-      canvas.drawRoundRect(  S+M, S+M, 2*S-M, 2*S-M, R, R, paint); //
-      paint.setColor(0xffb5651d);                                  // BROWN
-      canvas.drawRoundRect(2*S+M, S+M, 3*S-M, 2*S-M, R, R, paint); //
-
-      mTexture.setTexture(bitmap);
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    void recomputeScaleFactor(int screenWidth, int screenHeight, float size)
-      {
-      int texW = mNodeTexture.getWidth();
-      int texH = mNodeTexture.getHeight();
-
-      if( (float)texH/texW > (float)screenHeight/screenWidth )
-        {
-        int w = (screenHeight*texW)/texH;
-        float factor = (float)screenHeight/texH;
-        mNodeMove.set((screenWidth-w)*0.5f ,0, 0);
-        mNodeScale.set(factor,factor,factor);
-        }
-      else
-        {
-        int h = (screenWidth*texH)/texW;
-        float factor = (float)screenWidth/texW;
-        mNodeMove.set(0,(screenHeight-h)*0.5f,0);
-        mNodeScale.set(factor,factor,factor);
-        }
-
-      float scaleFactor = (size/(TEXTURE_SIZE*mSize)) * (float)texW/(screenWidth>screenHeight ? screenHeight:screenWidth);
-
-      mMove.set( texW*0.5f , texH*0.5f , 0.0f );
-      mScale.set(scaleFactor,scaleFactor,scaleFactor);
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// PUBLIC API
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    public Static4D getRotationQuat()
-      {
-      return mQuatAccumulated;
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    public void apply(Effect effect, int position)
-      {
-      for(int x=0; x<mSize; x++)
-        for(int y=0; y<mSize; y++)
-          for(int z=0; z<mSize; z++)
-            {
-            if( x==0 || x==mSize-1 || y==0 || y==mSize-1 || z==0 || z==mSize-1 )
-              {
-              mEffects[x][y][z].apply(effect, position);
-              }
-            }
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    public void remove(long effectID)
-      {
-      for(int x=0; x<mSize; x++)
-        for(int y=0; y<mSize; y++)
-          for(int z=0; z<mSize; z++)
-            {
-            if( x==0 || x==mSize-1 || y==0 || y==mSize-1 || z==0 || z==mSize-1 )
-              {
-              mEffects[x][y][z].abortById(effectID);
-              }
-            }
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    public void solve()
-      {
-      for(int x=0; x<mSize; x++)
-        for(int y=0; y<mSize; y++)
-          for(int z=0; z<mSize; z++)
-            if( x==0 || x==mSize-1 || y==0 || y==mSize-1 || z==0 || z==mSize-1 )
-              {
-              mQuatScramble[x][y][z].set(0,0,0,1);
-              mCurrentPosition[x][y][z].set(x,y,z);
-              }
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    public int getSize()
-      {
-      return mSize;
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    public long addNewRotation(int vector, int row, int angle, long durationMillis, EffectListener listener )
-      {
-      Static3D axis = VectX;
-      long effectID=0;
-      boolean first = true;
-
-      switch(vector)
-        {
-        case VECTX: axis = VectX; break;
-        case VECTY: axis = VectY; break;
-        case VECTZ: axis = VectZ; break;
-        }
-
-      mRotAxis = vector;
-      mRotRow  = row;
-
-      mRotationAngleStatic.set1(0.0f);
-
-      for(int x=0; x<mSize; x++)
-        for(int y=0; y<mSize; y++)
-          for(int z=0; z<mSize; z++)
-            if( x==0 || x==mSize-1 || y==0 || y==mSize-1 || z==0 || z==mSize-1 )
-              {
-              if( belongsToRotation(x,y,z,vector,mRotRow) )
-                {
-                mRotationAxis[x][y][z].set(axis);
-                mRotationAngle[x][y][z].setDuration(durationMillis);
-                mRotationAngle[x][y][z].resetToBeginning();
-                mRotationAngle[x][y][z].add(new Static1D(0));
-                mRotationAngle[x][y][z].add(new Static1D(angle));
-
-                if( first )
-                  {
-                  first = false;
-                  effectID = mRotateEffect[x][y][z].getID();
-                  mRotateEffect[x][y][z].notifyWhenFinished(listener);
-                  }
-                }
-              }
-
-      return effectID;
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    public void removeRotationNow()
-      {
-      float qx=0,qy=0,qz=0;
-      boolean first = true;
-      Static4D quat = null;
-
-      switch(mRotAxis)
-        {
-        case VECTX: qx=1; break;
-        case VECTY: qy=1; break;
-        case VECTZ: qz=1; break;
-        }
-
-      for(int x=0; x<mSize; x++)
-        for(int y=0; y<mSize; y++)
-          for(int z=0; z<mSize; z++)
-            if( x==0 || x==mSize-1 || y==0 || y==mSize-1 || z==0 || z==mSize-1 )
-              {
-              if( belongsToRotation(x,y,z,mRotAxis,mRotRow) )
-                {
-                if( first )
-                  {
-                  first = false;
-                  int pointNum = mRotationAngle[x][y][z].getNumPoints();
-
-                  if( pointNum>=1 )
-                    {
-                    float startingAngle = mRotationAngle[x][y][z].getPoint(pointNum-1).get1();
-                    int nearestAngleInDegrees = computeNearestAngle(startingAngle);
-                    double nearestAngleInRadians = nearestAngleInDegrees*Math.PI/180;
-                    float sinA =-(float)Math.sin(nearestAngleInRadians*0.5);
-                    float cosA = (float)Math.cos(nearestAngleInRadians*0.5);
-                    quat = new Static4D(qx*sinA, qy*sinA, qz*sinA, cosA);
-                    }
-                  else
-                    {
-                    android.util.Log.e("cube", "ERROR removing rotation!");
-                    return;
-                    }
-                  }
-
-                mRotationAngle[x][y][z].removeAll();
-                mQuatScramble[x][y][z].set(RubikSurfaceView.quatMultiply(quat,mQuatScramble[x][y][z]));
-                normalizeScrambleQuat(x,y,z);
-                modifyCurrentPosition(x,y,z,quat);
-                }
-              }
-
-      mRotationAngleStatic.set1(0);
-      }
-}
diff --git a/src/main/java/org/distorted/magic/RubikRenderer.java b/src/main/java/org/distorted/magic/RubikRenderer.java
index 4e293f5e..a24bb36a 100644
--- a/src/main/java/org/distorted/magic/RubikRenderer.java
+++ b/src/main/java/org/distorted/magic/RubikRenderer.java
@@ -29,6 +29,8 @@ import org.distorted.library.main.DistortedScreen;
 import org.distorted.library.main.DistortedTexture;
 import org.distorted.library.mesh.MeshFlat;
 import org.distorted.library.message.EffectListener;
+import org.distorted.object.RubikCube;
+import org.distorted.object.RubikCubeMovement;
 
 import javax.microedition.khronos.egl.EGLConfig;
 import javax.microedition.khronos.opengles.GL10;
@@ -37,13 +39,11 @@ import javax.microedition.khronos.opengles.GL10;
 
 public class RubikRenderer implements GLSurfaceView.Renderer, EffectListener
 {
-    private static final float CUBE_SCREEN_RATIO = 0.5f;
     private static final float CAMERA_DISTANCE   = 0.6f;  // 0.6 of the length of max(scrHeight,scrWidth)
     public  static final int TEXTURE_SIZE = 600;
 
     private RubikSurfaceView mView;
     private DistortedScreen mScreen;
-    private float mCubeSizeInScreenSpace;
     private int mNextCubeSize, mScrambleCubeNum;
     private long mRotationFinishedID;
     private long[] mEffectID;
@@ -60,7 +60,6 @@ public class RubikRenderer implements GLSurfaceView.Renderer, EffectListener
     RubikRenderer(RubikSurfaceView v)
       {
       mView = v;
-
       mScreen = new DistortedScreen();
 
       mOldCube = null;
@@ -84,7 +83,7 @@ public class RubikRenderer implements GLSurfaceView.Renderer, EffectListener
       mEffectID = new long[BaseEffect.Type.LENGTH];
 
       mMesh= new MeshFlat(20,20);
-      mNextCubeSize = RubikSize.getSize(RubikActivity.getRedButton()).getCubeSize();
+      mNextCubeSize = RubikSize.getSize(mView.getRedButton()).getCubeSize();
       }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
@@ -95,18 +94,6 @@ public class RubikRenderer implements GLSurfaceView.Renderer, EffectListener
      return (float)(2*halfFOVInRadians*(180/Math.PI));
      }
 
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-   private void recomputeScaleFactor(int screenWidth, int screenHeight)
-     {
-     mCubeSizeInScreenSpace = CUBE_SCREEN_RATIO*(screenWidth>screenHeight ? screenHeight:screenWidth);
-
-     if( mNewCube!=null )
-       {
-       mNewCube.recomputeScaleFactor(screenWidth, screenHeight, mCubeSizeInScreenSpace);
-       }
-     }
-
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
    private void createCubeNow(int newSize)
@@ -122,9 +109,12 @@ public class RubikRenderer implements GLSurfaceView.Renderer, EffectListener
 
      if( mScreenWidth!=0 )
        {
-       recomputeScaleFactor(mScreenWidth,mScreenHeight);
+       mNewCube.recomputeScaleFactor(mScreenWidth, mScreenHeight);
        }
 
+     RubikCubeMovement movement = new RubikCubeMovement(mNewCube);
+     mView.setMovement(movement);
+
      mIsSolved = true;
      }
 
@@ -135,10 +125,14 @@ public class RubikRenderer implements GLSurfaceView.Renderer, EffectListener
      {
      int index = type.ordinal();
 
-     mEffectID[index] = type.startEffect(this);
-
-     if( mEffectID[index] < 0 )
+     try
+       {
+       mEffectID[index] = type.startEffect(this);
+       }
+     catch( Exception ex )
        {
+       android.util.Log.e("renderer", "exception starting effect: "+ex.getMessage());
+
        mCanUI     = true;
        mCanRotate = true;
        mCanDrag   = true;
@@ -188,13 +182,6 @@ public class RubikRenderer implements GLSurfaceView.Renderer, EffectListener
        }
      }
 
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-   float returnCubeSizeInScreenSpace()
-     {
-     return mCubeSizeInScreenSpace;
-     }
-
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
    boolean canRotate()
@@ -267,6 +254,7 @@ public class RubikRenderer implements GLSurfaceView.Renderer, EffectListener
          mCanRotate      = false;
          mCanUI          = false;
          doEffectNow( BaseEffect.Type.WIN );
+         mView.leaveScrambleModeNonUI();
          }
        else
          {
@@ -316,12 +304,16 @@ public class RubikRenderer implements GLSurfaceView.Renderer, EffectListener
       float cameraDistance = CAMERA_DISTANCE*(width>height ? width:height);
       float fovInDegrees   = computeFOV(cameraDistance,height);
 
-      mScreen.setProjection( fovInDegrees, 0.1f);
       mView.setScreenSize(width,height);
       mView.setCameraDist(cameraDistance);
+
+      mScreen.setProjection( fovInDegrees, 0.1f);
       mScreen.resize(width, height);
 
-      recomputeScaleFactor(width,height);
+      if( mNewCube!=null )
+        {
+        mNewCube.recomputeScaleFactor(width, height);
+        }
 
       mScreenHeight = height;
       mScreenWidth  = width;
diff --git a/src/main/java/org/distorted/magic/RubikScoresDownloader.java b/src/main/java/org/distorted/magic/RubikScoresDownloader.java
index 3687ef4b..57d20b35 100644
--- a/src/main/java/org/distorted/magic/RubikScoresDownloader.java
+++ b/src/main/java/org/distorted/magic/RubikScoresDownloader.java
@@ -211,7 +211,7 @@ class RubikScoresDownloader implements Runnable
         }
       catch( final Exception e)
         {
-        mReceiver.exception("biffed it getting HTTPResponse");
+        mReceiver.exception("Failed to get an answer from the High Scores server");
         return false;
         }
       }
diff --git a/src/main/java/org/distorted/magic/RubikScoresPagerAdapter.java b/src/main/java/org/distorted/magic/RubikScoresPagerAdapter.java
index c1b24c10..4e2313dd 100644
--- a/src/main/java/org/distorted/magic/RubikScoresPagerAdapter.java
+++ b/src/main/java/org/distorted/magic/RubikScoresPagerAdapter.java
@@ -42,13 +42,13 @@ class RubikScoresPagerAdapter extends PagerAdapter implements RubikScoresDownloa
 
     int c = mViewPager.getCurrentItem();
 
-    addPage(c,country[c],name[c],time[c]);
+    addPage(mViews[c],country[c],name[c],time[c]);
 
     for(int i=0; i<RubikSize.LENGTH; i++)
       {
       if( i==c ) continue;
 
-      addPage(i,country[i],name[i],time[i]);
+      addPage(mViews[i],country[i],name[i],time[i]);
       }
     }
 
@@ -79,7 +79,7 @@ class RubikScoresPagerAdapter extends PagerAdapter implements RubikScoresDownloa
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  private void addPage(final int page, final int[][] country, final String[][] name, final String[][] time)
+  private void addPage(final RubikScoresView view, final int[][] country, final String[][] name, final String[][] time)
     {
     for(int section=0; section<RubikActivity.MAX_SCRAMBLE; section++)
       {
@@ -93,7 +93,7 @@ class RubikScoresPagerAdapter extends PagerAdapter implements RubikScoresDownloa
         @Override
         public void run()
           {
-          mViews[page].addSection(mAct, sec, c, n, t);
+          view.addSection(mAct, sec, c, n, t);
           }
         });
 
diff --git a/src/main/java/org/distorted/magic/RubikScoresView.java b/src/main/java/org/distorted/magic/RubikScoresView.java
index 1fb02ff1..14a92fc9 100644
--- a/src/main/java/org/distorted/magic/RubikScoresView.java
+++ b/src/main/java/org/distorted/magic/RubikScoresView.java
@@ -26,7 +26,6 @@ import android.view.View;
 import android.widget.FrameLayout;
 import android.widget.ImageView;
 import android.widget.LinearLayout;
-import android.widget.ScrollView;
 import android.widget.TextView;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
diff --git a/src/main/java/org/distorted/magic/RubikSurfaceView.java b/src/main/java/org/distorted/magic/RubikSurfaceView.java
index 50347fec..cdd2afe6 100644
--- a/src/main/java/org/distorted/magic/RubikSurfaceView.java
+++ b/src/main/java/org/distorted/magic/RubikSurfaceView.java
@@ -22,45 +22,42 @@ package org.distorted.magic;
 import android.app.ActivityManager;
 import android.content.Context;
 import android.content.pm.ConfigurationInfo;
+import android.graphics.PorterDuff;
+import android.graphics.drawable.Drawable;
 import android.opengl.GLSurfaceView;
+import android.support.v4.content.ContextCompat;
 import android.util.AttributeSet;
+import android.util.DisplayMetrics;
 import android.view.MotionEvent;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.ImageButton;
+import android.widget.LinearLayout;
 
 import org.distorted.library.type.Static4D;
+import org.distorted.object.RubikCubeMovement;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-class RubikSurfaceView extends GLSurfaceView
+public class RubikSurfaceView extends GLSurfaceView
 {
-    // Moving the finger from the middle of the vertical screen to the right edge will rotate a
-    // given face by SWIPING_SENSITIVITY/2 degrees.
-    private final static int SWIPING_SENSITIVITY  = 240;
-
     // Moving the finger by 1/12 the distance of min(scrWidth,scrHeight) will start a Rotation.
     private final static int ROTATION_SENSITIVITY =  12;
 
     // Every 1/12 the distance of min(scrWidth,scrHeight) the direction of cube rotation will reset.
     private final static int DIRECTION_SENSITIVITY=  12;
 
-    private final static int NONE   =-1;
-    private final static int FRONT  = 0;  // has to be 6 consecutive ints
-    private final static int BACK   = 1;  // FRONT ... BOTTOM
-    private final static int LEFT   = 2;  //
-    private final static int RIGHT  = 3;  //
-    private final static int TOP    = 4;  //
-    private final static int BOTTOM = 5;  //
+    private RubikRenderer mRenderer;
+    private RubikCubeMovement mMovement;
 
-    private static final int[] VECT = {RubikCube.VECTX,RubikCube.VECTY,RubikCube.VECTZ};
+    private boolean mInScrambleMode;
+    private int mButton = RubikSize.SIZE3.ordinal();
 
     private boolean mDragging, mBeginningRotation, mContinuingRotation;
     private int mX, mY;
-    private int mRotationVect;
-    private RubikRenderer mRenderer;
-
-    private float[] mPoint, mCamera, mTouchPointCastOntoFace, mDiff, mTouchPoint; // all in screen space
     private int mLastTouchedFace;
-    private int mScreenWidth, mScreenHeight, mScreenMin;
     private float mCameraDistance;
+    private int mScreenWidth, mScreenHeight, mScreenMin;
 
     private static Static4D mQuatCurrent    = new Static4D(0,0,0,1);
     private static Static4D mQuatAccumulated= new Static4D(-0.25189602f,0.3546389f,0.009657208f,0.90038127f);
@@ -70,7 +67,7 @@ class RubikSurfaceView extends GLSurfaceView
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 // return quat1*quat2
 
-    static Static4D quatMultiply( Static4D quat1, Static4D quat2 )
+    public static Static4D quatMultiply( Static4D quat1, Static4D quat2 )
       {
       float qx = quat1.get1();
       float qy = quat1.get2();
@@ -91,9 +88,9 @@ class RubikSurfaceView extends GLSurfaceView
       }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
-// rotate 'vector' by quat^(-1)  ( i.e. return (quat^-1)*vector*quat )
+// rotate 'vector' by quat  ( i.e. return quat*vector*(quat^-1) )
 
-    static Static4D rotateVectorByInvertedQuat(Static4D vector, Static4D quat)
+    public static Static4D rotateVectorByQuat(Static4D vector, Static4D quat)
       {
       float qx = quat.get1();
       float qy = quat.get2();
@@ -101,15 +98,15 @@ class RubikSurfaceView extends GLSurfaceView
       float qw = quat.get4();
 
       Static4D quatInverted= new Static4D(-qx,-qy,-qz,qw);
-      Static4D tmp = quatMultiply(quatInverted,vector);
+      Static4D tmp = quatMultiply(quat,vector);
 
-      return quatMultiply(tmp,quat);
+      return quatMultiply(tmp,quatInverted);
       }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
-// rotate 'vector' by quat  ( i.e. return quat*vector*(quat^-1) )
+// rotate 'vector' by quat^(-1)  ( i.e. return (quat^-1)*vector*quat )
 
-    static Static4D rotateVectorByQuat(Static4D vector, Static4D quat)
+    public static Static4D rotateVectorByInvertedQuat(Static4D vector, Static4D quat)
       {
       float qx = quat.get1();
       float qy = quat.get2();
@@ -117,242 +114,154 @@ class RubikSurfaceView extends GLSurfaceView
       float qw = quat.get4();
 
       Static4D quatInverted= new Static4D(-qx,-qy,-qz,qw);
-      Static4D tmp = quatMultiply(quat,vector);
-
-      return quatMultiply(tmp,quatInverted);
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    private int faceTouched(int xTouch, int yTouch)
-      {
-      float cubeHalfSize= mRenderer.returnCubeSizeInScreenSpace()*0.5f;
-
-      convertTouchPointToScreenSpace(xTouch,yTouch);
-      convertCameraPointToScreenSpace();
-
-      for(int face=FRONT; face<=BOTTOM; face++)
-        {
-        if( faceIsVisible(face,cubeHalfSize) )
-          {
-          castTouchPointOntoFace(face,cubeHalfSize, mTouchPointCastOntoFace);
-
-          float qX= (mTouchPointCastOntoFace[0]+cubeHalfSize) / (2*cubeHalfSize);
-          float qY= (mTouchPointCastOntoFace[1]+cubeHalfSize) / (2*cubeHalfSize);
-          float qZ= (mTouchPointCastOntoFace[2]+cubeHalfSize) / (2*cubeHalfSize);
-
-          if( qX<=1 && qX>=0 && qY<=1 && qY>=0 && qZ<=1 && qZ>=0 ) return face;
-          }
-        }
+      Static4D tmp = quatMultiply(quatInverted,vector);
 
-      return NONE;
+      return quatMultiply(tmp,quat);
       }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-    private void addNewRotation(int x, int y)
+    void setScreenSize(int width, int height)
       {
-      float cubeHalfSize= mRenderer.returnCubeSizeInScreenSpace()*0.5f;
-
-      convertTouchPointToScreenSpace(x,y);
-      castTouchPointOntoFace(mLastTouchedFace,cubeHalfSize,mDiff);
-
-      mDiff[0] -= mTouchPointCastOntoFace[0];
-      mDiff[1] -= mTouchPointCastOntoFace[1];
-      mDiff[2] -= mTouchPointCastOntoFace[2];
-
-      int xAxis = retFaceXaxis(mLastTouchedFace);
-      int yAxis = retFaceYaxis(mLastTouchedFace);
-      mRotationVect = (isVertical( mDiff[xAxis], mDiff[yAxis]) ? VECT[xAxis]:VECT[yAxis]);
-      float offset= (mTouchPointCastOntoFace[mRotationVect]+cubeHalfSize)/(2*cubeHalfSize);
-
-      mTouchPoint[0] = mPoint[0];
-      mTouchPoint[1] = mPoint[1];
-      mTouchPoint[2] = mPoint[2];
+      mScreenWidth = width;
+      mScreenHeight= height;
 
-      RubikCube cube = mRenderer.getCube();
-      cube.addNewRotation(mRotationVect, (int)(cube.getSize()*offset) );
+      mScreenMin = width<height ? width:height;
       }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-    private boolean isVertical(float x, float y)
+    void setCameraDist(float distance)
       {
-      return (y>x) ? (y>=-x) : (y< -x);
+      mCameraDistance = distance;
       }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-    private void continueRotation(int x, int y)
+    RubikRenderer getRenderer()
       {
-      convertTouchPointToScreenSpace(x,y);
-
-      mDiff[0] = mPoint[0]-mTouchPoint[0];
-      mDiff[1] = mPoint[1]-mTouchPoint[1];
-      mDiff[2] = mPoint[2]-mTouchPoint[2];
-
-      int xAxis= retFaceXaxis(mLastTouchedFace);
-      int yAxis= retFaceYaxis(mLastTouchedFace);
-      int sign = retFaceRotationSign(mLastTouchedFace);
-      float angle = (mRotationVect==xAxis ? mDiff[yAxis] : -mDiff[xAxis]);
-
-      mRenderer.getCube().continueRotation(SWIPING_SENSITIVITY*sign*angle/mScreenMin);
+      return mRenderer;
       }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-    private void finishRotation()
+    int getRedButton()
       {
-      mRenderer.finishRotation();
+      return mButton;
       }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-    private Static4D quatFromDrag(float dragX, float dragY)
+    void markButton(int button)
       {
-      float axisX = dragY;  // inverted X and Y - rotation axis is perpendicular to (dragX,dragY)
-      float axisY = dragX;  // Why not (-dragY, dragX) ? because Y axis is also inverted!
-      float axisZ = 0;
-      float axisL = (float)Math.sqrt(axisX*axisX + axisY*axisY + axisZ*axisZ);
+      mButton = button;
+      RubikActivity act = (RubikActivity)getContext();
 
-      if( axisL>0 )
+      for(int b=0; b<RubikSize.LENGTH; b++)
         {
-        axisX /= axisL;
-        axisY /= axisL;
-        axisZ /= axisL;
+        Drawable d = act.findViewById(b).getBackground();
 
-        float ratio = axisL/mScreenMin;
-        ratio = ratio - (int)ratio;     // the cos() is only valid in (0,Pi)
-
-        float cosA = (float)Math.cos(Math.PI*ratio);
-        float sinA = (float)Math.sqrt(1-cosA*cosA);
-
-        return new Static4D(axisX*sinA, axisY*sinA, axisZ*sinA, cosA);
+        if( b==button )
+          {
+          d.setColorFilter(ContextCompat.getColor(act,R.color.red), PorterDuff.Mode.MULTIPLY);
+          }
+        else
+          {
+          d.clearColorFilter();
+          }
         }
-
-      return new Static4D(0f, 0f, 0f, 1f);
       }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-    private boolean faceIsVisible(int face, float cubeHalfSize)
+    void addSizeButtons(RubikActivity act)
       {
-      int sign = retFaceSign(face);
-      int zAxis= retFaceZaxis(face);
-
-      return sign*mCamera[zAxis] > cubeHalfSize;
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    private void convertTouchPointToScreenSpace(int x, int y)
-      {
-      float halfScrWidth  = mScreenWidth *0.5f;
-      float halfScrHeight = mScreenHeight*0.5f;
-      Static4D touchPoint = new Static4D(x-halfScrWidth, halfScrHeight-y, 0, 0);
-      Static4D rotatedTouchPoint= rotateVectorByInvertedQuat(touchPoint, mQuatAccumulated);
-
-      mPoint[0] = rotatedTouchPoint.get1();
-      mPoint[1] = rotatedTouchPoint.get2();
-      mPoint[2] = rotatedTouchPoint.get3();
+      LinearLayout layout = act.findViewById(R.id.sizeLayout);
+      DisplayMetrics metrics = getResources().getDisplayMetrics();
+      float scale = metrics.density;
+      int size = (int)(64*scale +0.5f);
+      int padding = (int)(3*scale + 0.5f);
+      ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(size,size);
+
+      for(int i=0; i<RubikSize.LENGTH; i++)
+        {
+        ImageButton button = new ImageButton(act);
+        button.setLayoutParams(params);
+        button.setId(i);
+        button.setPadding(padding,0,padding,0);
+        int iconID = RubikSize.getSize(i).getIconID();
+        button.setImageResource(iconID);
+        button.setOnClickListener(act);
+        layout.addView(button);
+        }
       }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-    private void convertCameraPointToScreenSpace()
+    void enterScrambleMode()
       {
-      Static4D cameraPoint = new Static4D(0, 0, mCameraDistance, 0);
-      Static4D rotatedCamera= rotateVectorByInvertedQuat(cameraPoint, mQuatAccumulated);
-
-      mCamera[0] = rotatedCamera.get1();
-      mCamera[1] = rotatedCamera.get2();
-      mCamera[2] = rotatedCamera.get3();
-      }
+      if( !mInScrambleMode )
+        {
+        mInScrambleMode = true;
 
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// given precomputed mCamera and mPoint, respectively camera and touch point positions in ScreenSpace,
-// cast this touch point onto the surface defined by the 'face' and write the cast coords to 'output'.
-// Center of the 'face' = (0,0), third coord always +- cubeHalfSize.
+        android.util.Log.e("view", "entering scramble mode");
 
-    private void castTouchPointOntoFace(int face, float cubeHalfSize, float[] output)
-      {
-      int sign = retFaceSign(face);
-      int zAxis= retFaceZaxis(face);
-      float diff = mPoint[zAxis]-mCamera[zAxis];
+        RubikActivity act = (RubikActivity)getContext();
 
-      float ratio =  diff!=0.0f ? (sign*cubeHalfSize-mCamera[zAxis])/diff : 0.0f;
+        Button scrambleButt = act.findViewById(R.id.rubikScramble);
 
-      output[0] = (mPoint[0]-mCamera[0])*ratio + mCamera[0];
-      output[1] = (mPoint[1]-mCamera[1])*ratio + mCamera[1];
-      output[2] = (mPoint[2]-mCamera[2])*ratio + mCamera[2];
+        if( scrambleButt!=null )
+          {
+          scrambleButt.setClickable(false);
+          scrambleButt.setVisibility(INVISIBLE);
+          }
+        else
+          {
+          android.util.Log.e("view", "button null!");
+          }
+        }
       }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-    private int retFaceSign(int face)
+    void leaveScrambleMode()
       {
-      return (face==FRONT || face==RIGHT || face==TOP) ? 1:-1;
-      }
+      if( mInScrambleMode )
+        {
+        mInScrambleMode = false;
 
-///////////////////////////////////////////////////////////////////////////////////////////////////
+        android.util.Log.e("view", "leaving scramble mode");
 
-    private int retFaceRotationSign(int face)
-      {
-      return (face==BACK || face==RIGHT || face==TOP) ? 1:-1;
-      }
+        RubikActivity act = (RubikActivity)getContext();
 
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// retFace{X,Y,Z}axis: 3 functions which return which real AXIS gets mapped to which when we look
-// directly at a given face. For example, when we look at the RIGHT face of the cube (with TOP still
-// in the top) then the 'real' X axis becomes the 'Z' axis, thus retFaceZaxis(RIGHT) = VECTX.
+        Button scrambleButt = act.findViewById(R.id.rubikScramble);
 
-    private int retFaceXaxis(int face)
-      {
-      switch(face)
-        {
-        case FRONT :
-        case BACK  : return RubikCube.VECTX;
-        case LEFT  :
-        case RIGHT : return RubikCube.VECTZ;
-        case TOP   :
-        case BOTTOM: return RubikCube.VECTX;
+        if( scrambleButt!=null )
+          {
+          scrambleButt.setClickable(true);
+          scrambleButt.setVisibility(VISIBLE);
+          }
+        else
+          {
+          android.util.Log.e("view", "button null!");
+          }
         }
-
-      return -1;
       }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-    private int retFaceYaxis(int face)
+    void leaveScrambleModeNonUI()
       {
-      switch(face)
-        {
-        case FRONT :
-        case BACK  : return RubikCube.VECTY;
-        case LEFT  :
-        case RIGHT : return RubikCube.VECTY;
-        case TOP   :
-        case BOTTOM: return RubikCube.VECTZ;
-        }
+      RubikActivity act = (RubikActivity)getContext();
 
-      return -1;
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    private int retFaceZaxis(int face)
-      {
-      switch(face)
+      act.runOnUiThread(new Runnable()
         {
-        case FRONT :
-        case BACK  : return RubikCube.VECTZ;
-        case LEFT  :
-        case RIGHT : return RubikCube.VECTX;
-        case TOP   :
-        case BOTTOM: return RubikCube.VECTY;
-        }
-
-      return -1;
+        @Override
+        public void run()
+          {
+          leaveScrambleMode();
+          }
+        });
       }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
@@ -385,26 +294,36 @@ class RubikSurfaceView extends GLSurfaceView
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-    RubikRenderer getRenderer()
+    private Static4D quatFromDrag(float dragX, float dragY)
       {
-      return mRenderer;
-      }
+      float axisX = dragY;  // inverted X and Y - rotation axis is perpendicular to (dragX,dragY)
+      float axisY = dragX;  // Why not (-dragY, dragX) ? because Y axis is also inverted!
+      float axisZ = 0;
+      float axisL = (float)Math.sqrt(axisX*axisX + axisY*axisY + axisZ*axisZ);
 
-///////////////////////////////////////////////////////////////////////////////////////////////////
+      if( axisL>0 )
+        {
+        axisX /= axisL;
+        axisY /= axisL;
+        axisZ /= axisL;
 
-    void setScreenSize(int width, int height)
-      {
-      mScreenWidth = width;
-      mScreenHeight= height;
+        float ratio = axisL/mScreenMin;
+        ratio = ratio - (int)ratio;     // the cos() is only valid in (0,Pi)
 
-      mScreenMin = width<height ? width:height;
+        float cosA = (float)Math.cos(Math.PI*ratio);
+        float sinA = (float)Math.sqrt(1-cosA*cosA);
+
+        return new Static4D(axisX*sinA, axisY*sinA, axisZ*sinA, cosA);
+        }
+
+      return new Static4D(0f, 0f, 0f, 1f);
       }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-    void setCameraDist(float distance)
+    void setMovement(RubikCubeMovement movement)
       {
-      mCameraDistance = distance;
+      mMovement = movement;
       }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
@@ -417,16 +336,7 @@ class RubikSurfaceView extends GLSurfaceView
 
       if(!isInEditMode())
         {
-        mRotationVect = VECT[0];
-
-        mPoint = new float[3];
-        mCamera= new float[3];
-        mDiff  = new float[3];
-        mTouchPoint = new float[3];
-        mTouchPointCastOntoFace = new float[3];
-
-        mScreenWidth = mScreenHeight = mScreenMin = 0;
-
+        mInScrambleMode = false;
         mRenderer = new RubikRenderer(this);
 
         final ActivityManager activityManager     = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
@@ -449,9 +359,9 @@ class RubikSurfaceView extends GLSurfaceView
          {
          case MotionEvent.ACTION_DOWN: mX = x;
                                        mY = y;
-                                       mLastTouchedFace = faceTouched(x,y);
+                                       mLastTouchedFace = mMovement.faceTouched(mQuatAccumulated,mCameraDistance, x,y, mScreenWidth, mScreenHeight);
 
-                                       if( mLastTouchedFace != NONE )
+                                       if( mLastTouchedFace != RubikCubeMovement.NONE )
                                          {
                                          mDragging           = false;
                                          mBeginningRotation  = mRenderer.canRotate();
@@ -487,14 +397,14 @@ class RubikSurfaceView extends GLSurfaceView
 
                                          if( (mX-x)*(mX-x)+(mY-y)*(mY-y) > minimumDistToStartRotating )
                                            {
-                                           addNewRotation(x,y);
+                                           mMovement.addNewRotation(mQuatAccumulated,mLastTouchedFace, x,y, mScreenWidth, mScreenHeight);
                                            mBeginningRotation = false;
                                            mContinuingRotation= true;
                                            }
                                          }
                                        else if( mContinuingRotation )
                                          {
-                                         continueRotation(x,y);
+                                         mMovement.continueRotation(mQuatAccumulated,mLastTouchedFace, x,y, mScreenWidth, mScreenHeight);
                                          }
                                        break;
          case MotionEvent.ACTION_UP  : if( mDragging )
@@ -507,9 +417,8 @@ class RubikSurfaceView extends GLSurfaceView
 
                                        if( mContinuingRotation )
                                          {
-                                         finishRotation();
+                                         mRenderer.finishRotation();
                                          }
-
                                        break;
          }
 
diff --git a/src/main/java/org/distorted/object/RubikCube.java b/src/main/java/org/distorted/object/RubikCube.java
new file mode 100644
index 00000000..78ad9747
--- /dev/null
+++ b/src/main/java/org/distorted/object/RubikCube.java
@@ -0,0 +1,702 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2019 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.object;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+
+import org.distorted.library.effect.Effect;
+import org.distorted.library.effect.MatrixEffectMove;
+import org.distorted.library.effect.MatrixEffectQuaternion;
+import org.distorted.library.effect.MatrixEffectRotate;
+import org.distorted.library.effect.MatrixEffectScale;
+import org.distorted.library.effect.VertexEffectSink;
+import org.distorted.library.main.DistortedEffects;
+import org.distorted.library.main.DistortedNode;
+import org.distorted.library.main.DistortedTexture;
+import org.distorted.library.mesh.MeshCubes;
+import org.distorted.library.mesh.MeshFlat;
+import org.distorted.library.message.EffectListener;
+import org.distorted.library.type.Dynamic1D;
+import org.distorted.library.type.Static1D;
+import org.distorted.library.type.Static3D;
+import org.distorted.library.type.Static4D;
+import org.distorted.magic.RubikSurfaceView;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+public class RubikCube extends DistortedNode
+{
+    private static final float CUBE_SCREEN_RATIO = 0.5f;
+    private static final int POST_ROTATION_MILLISEC = 500;
+    private static final int TEXTURE_SIZE = 100;
+
+    private static final Static3D VectX = new Static3D(1,0,0);
+    private static final Static3D VectY = new Static3D(0,1,0);
+    private static final Static3D VectZ = new Static3D(0,0,1);
+
+    public static final int VECTX = 0;  //
+    public static final int VECTY = 1;  // don't change this
+    public static final int VECTZ = 2;  //
+
+    private DistortedNode[][][] mNodes;
+    private MeshCubes[][][] mCubes;
+    private DistortedEffects[][][] mEffects;
+    private Static4D[][][] mQuatScramble;
+    private Static3D[][][] mRotationAxis;
+    private Dynamic1D[][][] mRotationAngle;
+    private Static3D[][][] mCurrentPosition;
+    private MatrixEffectRotate[][][] mRotateEffect;
+    private Static1D mRotationAngleStatic, mRotationAngleMiddle, mRotationAngleFinal;
+    private Static3D mMove, mScale, mNodeMove, mNodeScale;
+    private Static4D mQuatAccumulated;
+    private DistortedTexture mTexture;
+
+    private int mRotAxis, mRotRow;
+    private int mSize;
+
+    private DistortedTexture mNodeTexture;
+    private float mCubeSizeInScreenSpace;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    private int computeNearestAngle(float angle)
+      {
+      final int NEAREST = 90;
+
+      int tmp = (int)((angle+NEAREST/2)/NEAREST);
+      if( angle< -(NEAREST*0.5) ) tmp-=1;
+
+      return NEAREST*tmp;
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// All legal rotation quats must have all four of their components equal to either
+// 0, 1, -1, 0.5, -0.5 or +-sqrt(2)/2.
+//
+// Because of quatMultiplication, errors can accumulate - so to avoid this, we
+// correct the value of the 'scramble' quat to what it should be.
+//
+// We also have to remember that the group of unit quaternions is a double-cover of rotations
+// in 3D ( q represents the same rotation as -q ) - so invert if needed.
+
+    private static final float SQ2 = 0.5f*((float)Math.sqrt(2));
+    private static final float[] LEGAL = { 0.0f , 0.5f , -0.5f , 1.0f , -1.0f , SQ2 , -SQ2 };
+
+    private void normalizeScrambleQuat(int i, int j, int k)
+      {
+      Static4D quat = mQuatScramble[i][j][k];
+
+      float x = quat.get1();
+      float y = quat.get2();
+      float z = quat.get3();
+      float w = quat.get4();
+      float diff;
+
+      for(float legal: LEGAL)
+        {
+        diff = x-legal;
+        if( diff*diff<0.01f ) x = legal;
+        diff = y-legal;
+        if( diff*diff<0.01f ) y = legal;
+        diff = z-legal;
+        if( diff*diff<0.01f ) z = legal;
+        diff = w-legal;
+        if( diff*diff<0.01f ) w = legal;
+        }
+
+      if( w<0 )
+        {
+        w = -w;
+        z = -z;
+        y = -y;
+        x = -x;
+        }
+      else if( w==0 )
+        {
+        if( z<0 )
+          {
+          z = -z;
+          y = -y;
+          x = -x;
+          }
+        else if( z==0 )
+          {
+          if( y<0 )
+            {
+            y = -y;
+            x = -x;
+            }
+          else if( y==0 )
+            {
+            if( x<0 )
+              {
+              x = -x;
+              }
+            }
+          }
+        }
+
+      mQuatScramble[i][j][k].set(x,y,z,w);
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    private float getSinkStrength()
+      {
+      switch(mSize)
+        {
+        case 1 : return 1.1f;
+        case 2 : return 1.5f;
+        case 3 : return 1.8f;
+        case 4 : return 2.0f;
+        default: return 3.0f - 4.0f/mSize;
+        }
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    private boolean belongsToRotation(int x, int y, int z, int vector, int row)
+      {
+      switch(vector)
+        {
+        case VECTX: return mCurrentPosition[x][y][z].get1()==row;
+        case VECTY: return mCurrentPosition[x][y][z].get2()==row;
+        case VECTZ: return mCurrentPosition[x][y][z].get3()==row;
+        }
+
+      return false;
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    private void modifyCurrentPosition(int x, int y, int z, Static4D quat)
+      {
+      Static3D current = mCurrentPosition[x][y][z];
+      float diff = 0.5f*(mSize-1);
+      float cubitCenterX = current.get1() - diff;
+      float cubitCenterY = current.get2() - diff;
+      float cubitCenterZ = current.get3() - diff;
+
+      Static4D cubitCenter =  new Static4D(cubitCenterX, cubitCenterY, cubitCenterZ, 0);
+      Static4D rotatedCenter = RubikSurfaceView.rotateVectorByQuat( cubitCenter, quat);
+
+      float rotatedX = rotatedCenter.get1() + diff;
+      float rotatedY = rotatedCenter.get2() + diff;
+      float rotatedZ = rotatedCenter.get3() + diff;
+
+      int roundedX = (int)(rotatedX+0.1f);
+      int roundedY = (int)(rotatedY+0.1f);
+      int roundedZ = (int)(rotatedZ+0.1f);
+
+      mCurrentPosition[x][y][z].set1(roundedX);
+      mCurrentPosition[x][y][z].set2(roundedY);
+      mCurrentPosition[x][y][z].set3(roundedZ);
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    void addNewRotation(int vector, int row )
+      {
+      Static3D axis = VectX;
+
+      switch(vector)
+        {
+        case VECTX: axis = VectX; break;
+        case VECTY: axis = VectY; break;
+        case VECTZ: axis = VectZ; break;
+        }
+
+      mRotAxis = vector;
+      mRotRow  = row;
+
+      mRotationAngleStatic.set1(0.0f);
+
+      for(int x=0; x<mSize; x++)
+        for(int y=0; y<mSize; y++)
+          for(int z=0; z<mSize; z++)
+            if( x==0 || x==mSize-1 || y==0 || y==mSize-1 || z==0 || z==mSize-1 )
+              {
+              if( belongsToRotation(x,y,z,vector,mRotRow) )
+                {
+                mRotationAxis[x][y][z].set(axis);
+                mRotationAngle[x][y][z].add(mRotationAngleStatic);
+                }
+              }
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    void continueRotation(float angleInDegrees)
+      {
+      mRotationAngleStatic.set1(angleInDegrees);
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    float returnCubeSizeInScreenSpace()
+      {
+      return mCubeSizeInScreenSpace;
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// PUBLIC API
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public RubikCube(int size, Static4D quatCur, Static4D quatAcc, DistortedTexture texture, MeshFlat mesh, DistortedEffects effects)
+      {
+      super(texture,effects,mesh);
+
+      mNodeTexture = texture;
+
+      mSize = size;
+
+      mRotationAngleStatic = new Static1D(0);
+      mRotationAngleMiddle = new Static1D(0);
+      mRotationAngleFinal  = new Static1D(0);
+
+      mMove     = new Static3D(0,0,0);
+      mScale    = new Static3D(1,1,1);
+      mNodeMove = new Static3D(0,0,0);
+      mNodeScale= new Static3D(1,1,1);
+
+      mQuatAccumulated = quatAcc;
+
+      mRotAxis = VECTX;
+      mTexture = new DistortedTexture(TEXTURE_SIZE,TEXTURE_SIZE);
+
+      mNodes          = new DistortedNode[mSize][mSize][mSize];
+      mCubes          = new MeshCubes[mSize][mSize][mSize];
+      mEffects        = new DistortedEffects[mSize][mSize][mSize];
+      mQuatScramble   = new Static4D[mSize][mSize][mSize];
+      mRotationAxis   = new Static3D[mSize][mSize][mSize];
+      mRotationAngle  = new Dynamic1D[mSize][mSize][mSize];
+      mCurrentPosition= new Static3D[mSize][mSize][mSize];
+      mRotateEffect   = new MatrixEffectRotate[mSize][mSize][mSize];
+
+      Static3D[][][] cubeVectors = new Static3D[mSize][mSize][mSize];
+
+      Static3D sinkCenter = new Static3D(TEXTURE_SIZE*0.5f, TEXTURE_SIZE*0.5f, TEXTURE_SIZE*0.5f);
+      Static3D matrCenter = new Static3D(0,0,0);
+      Static4D region = new Static4D(0,0,0, TEXTURE_SIZE*0.72f);
+
+      VertexEffectSink        sinkEffect = new VertexEffectSink( new Static1D(getSinkStrength()), sinkCenter, region );
+      MatrixEffectMove        moveEffect = new MatrixEffectMove(mMove);
+      MatrixEffectScale      scaleEffect = new MatrixEffectScale(mScale);
+      MatrixEffectQuaternion quatCEffect = new MatrixEffectQuaternion(quatCur, matrCenter);
+      MatrixEffectQuaternion quatAEffect = new MatrixEffectQuaternion(quatAcc, matrCenter);
+
+      MatrixEffectMove       nodeMoveEffect  = new MatrixEffectMove(mNodeMove);
+      MatrixEffectScale      nodeScaleEffect = new MatrixEffectScale(mNodeScale);
+
+      effects.apply(nodeScaleEffect);
+      effects.apply(nodeMoveEffect);
+
+      // 3x2 bitmap = 6 squares:
+      //
+      // RED     GREEN   BLUE
+      // YELLOW  WHITE   BROWN
+
+      final float ze = 0.0f;
+      final float ot = 1.0f/3.0f;
+      final float tt = 2.0f/3.0f;
+      final float oh = 1.0f/2.0f;
+      final float of = 1.0f/40.0f;
+
+      final Static4D mapFront = new Static4D(ze,oh, ze+ot,oh+oh);
+      final Static4D mapBack  = new Static4D(tt,ze, tt+ot,ze+oh);
+      final Static4D mapLeft  = new Static4D(ot,ze, ot+ot,ze+oh);
+      final Static4D mapRight = new Static4D(ze,ze, ze+ot,ze+oh);
+      final Static4D mapTop   = new Static4D(tt,oh, tt+ot,oh+oh);
+      final Static4D mapBottom= new Static4D(ot,oh, ot+ot,oh+oh);
+
+      final Static4D mapBlack = new Static4D(ze,ze, ze+of,ze+of);
+
+      Static4D tmpFront, tmpBack, tmpLeft, tmpRight, tmpTop, tmpBottom;
+      float nc = 0.5f*mSize;
+      int vertices = (int)(24.0f/mSize + 2.0f);
+
+      for(int x = 0; x< mSize; x++)
+        for(int y = 0; y< mSize; y++)
+          for(int z = 0; z< mSize; z++)
+            {
+            if( x==0 || x==mSize-1 || y==0 || y==mSize-1 || z==0 || z==mSize-1 ) // only the external walls
+              {
+              tmpLeft  = (x==       0 ? mapLeft  :mapBlack);
+              tmpRight = (x== mSize-1 ? mapRight :mapBlack);
+              tmpFront = (z== mSize-1 ? mapFront :mapBlack);
+              tmpBack  = (z==       0 ? mapBack  :mapBlack);
+              tmpTop   = (y== mSize-1 ? mapTop   :mapBlack);
+              tmpBottom= (y==       0 ? mapBottom:mapBlack);
+
+              mCubes[x][y][z]           = new MeshCubes(vertices,vertices,vertices, tmpFront, tmpBack, tmpLeft, tmpRight, tmpTop, tmpBottom);
+              cubeVectors[x][y][z]      = new Static3D( TEXTURE_SIZE*(x-nc), TEXTURE_SIZE*(y-nc), TEXTURE_SIZE*(z-nc) );
+              mQuatScramble[x][y][z]    = new Static4D(0,0,0,1);
+              mRotationAngle[x][y][z]   = new Dynamic1D();
+              mRotationAxis[x][y][z]    = new Static3D(1,0,0);
+              mCurrentPosition[x][y][z] = new Static3D(x,y,z);
+              mRotateEffect[x][y][z]    = new MatrixEffectRotate(mRotationAngle[x][y][z], mRotationAxis[x][y][z], matrCenter);
+
+              mEffects[x][y][z] = new DistortedEffects();
+              mEffects[x][y][z].apply(sinkEffect);
+              mEffects[x][y][z].apply( new MatrixEffectMove(cubeVectors[x][y][z]) );
+              mEffects[x][y][z].apply( new MatrixEffectQuaternion(mQuatScramble[x][y][z], matrCenter));
+              mEffects[x][y][z].apply(mRotateEffect[x][y][z]);
+              mEffects[x][y][z].apply(quatAEffect);
+              mEffects[x][y][z].apply(quatCEffect);
+              mEffects[x][y][z].apply(scaleEffect);
+              mEffects[x][y][z].apply(moveEffect);
+
+              mNodes[x][y][z] = new DistortedNode(mTexture,mEffects[x][y][z],mCubes[x][y][z]);
+
+              attach(mNodes[x][y][z]);
+              }
+            }
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public Static4D getRotationQuat()
+      {
+      return mQuatAccumulated;
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public long finishRotationNow(EffectListener listener)
+      {
+      boolean first = true;
+      long effectID=0;
+
+      for(int x=0; x<mSize; x++)
+        for(int y=0; y<mSize; y++)
+          for(int z=0; z<mSize; z++)
+            if( x==0 || x==mSize-1 || y==0 || y==mSize-1 || z==0 || z==mSize-1 )
+              {
+              if( belongsToRotation(x,y,z,mRotAxis,mRotRow) )
+                {
+                if( first )
+                  {
+                  first = false;
+                  mRotateEffect[x][y][z].notifyWhenFinished(listener);
+                  effectID = mRotateEffect[x][y][z].getID();
+                  int pointNum = mRotationAngle[x][y][z].getNumPoints();
+
+                  if( pointNum>=1 )
+                    {
+                    float startingAngle = mRotationAngle[x][y][z].getPoint(pointNum-1).get1();
+                    int nearestAngleInDegrees = computeNearestAngle(startingAngle);
+                    mRotationAngleStatic.set1(startingAngle);
+                    mRotationAngleFinal.set1(nearestAngleInDegrees);
+                    mRotationAngleMiddle.set1( nearestAngleInDegrees + (nearestAngleInDegrees-startingAngle)*0.2f );
+                    }
+                  else
+                    {
+                    android.util.Log.e("cube", "ERROR finishing rotation!");
+                    return 0;
+                    }
+                  }
+
+                mRotationAngle[x][y][z].setDuration(POST_ROTATION_MILLISEC);
+                mRotationAngle[x][y][z].resetToBeginning();
+                mRotationAngle[x][y][z].removeAll();
+                mRotationAngle[x][y][z].add(mRotationAngleStatic);
+                mRotationAngle[x][y][z].add(mRotationAngleMiddle);
+                mRotationAngle[x][y][z].add(mRotationAngleFinal);
+                }
+              }
+
+      return effectID;
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// all DistortedTextures, DistortedNodes, DistortedFramebuffers, DistortedScreens and all types of
+// Meshes HAVE TO be markedForDeletion when they are no longer needed- otherwise we have a major
+// memory leak.
+
+    public void releaseResources()
+      {
+      mTexture.markForDeletion();
+
+      for(int x=0; x<mSize; x++)
+        for(int y=0; y<mSize; y++)
+          for(int z=0; z<mSize; z++)
+            {
+            if( x==0 || x==mSize-1 || y==0 || y==mSize-1 || z==0 || z==mSize-1 )
+              {
+              mCubes[x][y][z].markForDeletion();
+              mNodes[x][y][z].markForDeletion();
+              }
+            }
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public void createTexture()
+      {
+      Bitmap bitmap;
+
+      final int S = 128;
+      final int W = 3*S;
+      final int H = 2*S;
+      final int R = S/10;
+      final int M = S/20;
+
+      Paint paint = new Paint();
+      bitmap = Bitmap.createBitmap(W,H, Bitmap.Config.ARGB_8888);
+      Canvas canvas = new Canvas(bitmap);
+
+      paint.setAntiAlias(true);
+      paint.setTextAlign(Paint.Align.CENTER);
+      paint.setStyle(Paint.Style.FILL);
+
+      // 3x2 bitmap = 6 squares:
+      //
+      // RED     GREEN   BLUE
+      // YELLOW  WHITE   BROWN
+
+      paint.setColor(0xff000000);                                  // BLACK BACKGROUND
+      canvas.drawRect(0, 0, W, H, paint);                          //
+
+      paint.setColor(0xffff0000);                                  // RED
+      canvas.drawRoundRect(    M,   M,   S-M,   S-M, R, R, paint); //
+      paint.setColor(0xff00ff00);                                  // GREEN
+      canvas.drawRoundRect(  S+M,   M, 2*S-M,   S-M, R, R, paint); //
+      paint.setColor(0xff0000ff);                                  // BLUE
+      canvas.drawRoundRect(2*S+M,   M, 3*S-M,   S-M, R, R, paint); //
+      paint.setColor(0xffffff00);                                  // YELLOW
+      canvas.drawRoundRect(    M, S+M,   S-M, 2*S-M, R, R, paint); //
+      paint.setColor(0xffffffff);                                  // WHITE
+      canvas.drawRoundRect(  S+M, S+M, 2*S-M, 2*S-M, R, R, paint); //
+      paint.setColor(0xffb5651d);                                  // BROWN
+      canvas.drawRoundRect(2*S+M, S+M, 3*S-M, 2*S-M, R, R, paint); //
+
+      mTexture.setTexture(bitmap);
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public void recomputeScaleFactor(int screenWidth, int screenHeight)
+      {
+      mCubeSizeInScreenSpace = CUBE_SCREEN_RATIO*(screenWidth>screenHeight ? screenHeight:screenWidth);
+
+      int texW = mNodeTexture.getWidth();
+      int texH = mNodeTexture.getHeight();
+
+      if( (float)texH/texW > (float)screenHeight/screenWidth )
+        {
+        int w = (screenHeight*texW)/texH;
+        float factor = (float)screenHeight/texH;
+        mNodeMove.set((screenWidth-w)*0.5f ,0, 0);
+        mNodeScale.set(factor,factor,factor);
+        }
+      else
+        {
+        int h = (screenWidth*texH)/texW;
+        float factor = (float)screenWidth/texW;
+        mNodeMove.set(0,(screenHeight-h)*0.5f,0);
+        mNodeScale.set(factor,factor,factor);
+        }
+
+      float scaleFactor = (mCubeSizeInScreenSpace/(TEXTURE_SIZE*mSize)) * (float)texW/(screenWidth>screenHeight ? screenHeight:screenWidth);
+
+      mMove.set( texW*0.5f , texH*0.5f , 0.0f );
+      mScale.set(scaleFactor,scaleFactor,scaleFactor);
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public void apply(Effect effect, int position)
+      {
+      for(int x=0; x<mSize; x++)
+        for(int y=0; y<mSize; y++)
+          for(int z=0; z<mSize; z++)
+            {
+            if( x==0 || x==mSize-1 || y==0 || y==mSize-1 || z==0 || z==mSize-1 )
+              {
+              mEffects[x][y][z].apply(effect, position);
+              }
+            }
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public void remove(long effectID)
+      {
+      for(int x=0; x<mSize; x++)
+        for(int y=0; y<mSize; y++)
+          for(int z=0; z<mSize; z++)
+            {
+            if( x==0 || x==mSize-1 || y==0 || y==mSize-1 || z==0 || z==mSize-1 )
+              {
+              mEffects[x][y][z].abortById(effectID);
+              }
+            }
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public void solve()
+      {
+      for(int x=0; x<mSize; x++)
+        for(int y=0; y<mSize; y++)
+          for(int z=0; z<mSize; z++)
+            if( x==0 || x==mSize-1 || y==0 || y==mSize-1 || z==0 || z==mSize-1 )
+              {
+              mQuatScramble[x][y][z].set(0,0,0,1);
+              mCurrentPosition[x][y][z].set(x,y,z);
+              }
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public boolean isSolved()
+      {
+      Static4D q = mQuatScramble[0][0][0];
+
+      float x = q.get1();
+      float y = q.get2();
+      float z = q.get3();
+      float w = q.get4();
+
+      for(int i = 0; i< mSize; i++)
+        for(int j = 0; j< mSize; j++)
+          for(int k = 0; k< mSize; k++)
+            {
+            if( i==0 || i==mSize-1 || j==0 || j==mSize-1 || k==0 || k==mSize-1 )
+              {
+              q = mQuatScramble[i][j][k];
+
+              if( q.get1()!=x || q.get2()!=y || q.get3()!=z || q.get4()!=w )
+                {
+                return false;
+                }
+              }
+            }
+
+      return true;
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public int getSize()
+      {
+      return mSize;
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public long addNewRotation(int vector, int row, int angle, long durationMillis, EffectListener listener )
+      {
+      Static3D axis = VectX;
+      long effectID=0;
+      boolean first = true;
+
+      switch(vector)
+        {
+        case VECTX: axis = VectX; break;
+        case VECTY: axis = VectY; break;
+        case VECTZ: axis = VectZ; break;
+        }
+
+      mRotAxis = vector;
+      mRotRow  = row;
+
+      mRotationAngleStatic.set1(0.0f);
+
+      for(int x=0; x<mSize; x++)
+        for(int y=0; y<mSize; y++)
+          for(int z=0; z<mSize; z++)
+            if( x==0 || x==mSize-1 || y==0 || y==mSize-1 || z==0 || z==mSize-1 )
+              {
+              if( belongsToRotation(x,y,z,vector,mRotRow) )
+                {
+                mRotationAxis[x][y][z].set(axis);
+                mRotationAngle[x][y][z].setDuration(durationMillis);
+                mRotationAngle[x][y][z].resetToBeginning();
+                mRotationAngle[x][y][z].add(new Static1D(0));
+                mRotationAngle[x][y][z].add(new Static1D(angle));
+
+                if( first )
+                  {
+                  first = false;
+                  effectID = mRotateEffect[x][y][z].getID();
+                  mRotateEffect[x][y][z].notifyWhenFinished(listener);
+                  }
+                }
+              }
+
+      return effectID;
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public void removeRotationNow()
+      {
+      float qx=0,qy=0,qz=0;
+      boolean first = true;
+      Static4D quat = null;
+
+      switch(mRotAxis)
+        {
+        case VECTX: qx=1; break;
+        case VECTY: qy=1; break;
+        case VECTZ: qz=1; break;
+        }
+
+      for(int x=0; x<mSize; x++)
+        for(int y=0; y<mSize; y++)
+          for(int z=0; z<mSize; z++)
+            if( x==0 || x==mSize-1 || y==0 || y==mSize-1 || z==0 || z==mSize-1 )
+              {
+              if( belongsToRotation(x,y,z,mRotAxis,mRotRow) )
+                {
+                if( first )
+                  {
+                  first = false;
+                  int pointNum = mRotationAngle[x][y][z].getNumPoints();
+
+                  if( pointNum>=1 )
+                    {
+                    float startingAngle = mRotationAngle[x][y][z].getPoint(pointNum-1).get1();
+                    int nearestAngleInDegrees = computeNearestAngle(startingAngle);
+                    double nearestAngleInRadians = nearestAngleInDegrees*Math.PI/180;
+                    float sinA =-(float)Math.sin(nearestAngleInRadians*0.5);
+                    float cosA = (float)Math.cos(nearestAngleInRadians*0.5);
+                    quat = new Static4D(qx*sinA, qy*sinA, qz*sinA, cosA);
+                    }
+                  else
+                    {
+                    android.util.Log.e("cube", "ERROR removing rotation!");
+                    return;
+                    }
+                  }
+
+                mRotationAngle[x][y][z].removeAll();
+                mQuatScramble[x][y][z].set(RubikSurfaceView.quatMultiply(quat,mQuatScramble[x][y][z]));
+                normalizeScrambleQuat(x,y,z);
+                modifyCurrentPosition(x,y,z,quat);
+                }
+              }
+
+      mRotationAngleStatic.set1(0);
+      }
+}
diff --git a/src/main/java/org/distorted/object/RubikCubeMovement.java b/src/main/java/org/distorted/object/RubikCubeMovement.java
new file mode 100644
index 00000000..52488855
--- /dev/null
+++ b/src/main/java/org/distorted/object/RubikCubeMovement.java
@@ -0,0 +1,263 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2020 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.object;
+
+import org.distorted.library.type.Static4D;
+import org.distorted.magic.RubikSurfaceView;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+public class RubikCubeMovement
+{
+    public  final static int NONE   =-1;
+    private final static int FRONT  = 0;  // has to be 6 consecutive ints
+    private final static int BACK   = 1;  // FRONT ... BOTTOM
+    private final static int LEFT   = 2;  //
+    private final static int RIGHT  = 3;  //
+    private final static int TOP    = 4;  //
+    private final static int BOTTOM = 5;  //
+
+    private static final int[] VECT = {RubikCube.VECTX,RubikCube.VECTY,RubikCube.VECTZ};
+
+    // Moving the finger from the middle of the vertical screen to the right edge will rotate a
+    // given face by SWIPING_SENSITIVITY/2 degrees.
+    private final static int SWIPING_SENSITIVITY  = 240;
+
+    private RubikCube mCube;
+    private float[] mPoint, mCamera, mTouchPointCastOntoFace, mDiff, mTouchPoint; // all in screen space
+    private int mRotationVect;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    private boolean isVertical(float x, float y)
+      {
+      return (y>x) ? (y>=-x) : (y< -x);
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    private boolean faceIsVisible(int face, float cubeHalfSize)
+      {
+      int sign = retFaceSign(face);
+      int zAxis= retFaceZaxis(face);
+
+      return sign*mCamera[zAxis] > cubeHalfSize;
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    private void convertTouchPointToScreenSpace(Static4D accumulated, int x, int y, int scrW, int scrH)
+      {
+      float halfScrWidth  = scrW*0.5f;
+      float halfScrHeight = scrH*0.5f;
+      Static4D touchPoint = new Static4D(x-halfScrWidth, halfScrHeight-y, 0, 0);
+      Static4D rotatedTouchPoint= RubikSurfaceView.rotateVectorByInvertedQuat(touchPoint, accumulated);
+
+      mPoint[0] = rotatedTouchPoint.get1();
+      mPoint[1] = rotatedTouchPoint.get2();
+      mPoint[2] = rotatedTouchPoint.get3();
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    private void convertCameraPointToScreenSpace(Static4D accumulated, float cameraDistance)
+      {
+      Static4D cameraPoint = new Static4D(0, 0, cameraDistance, 0);
+      Static4D rotatedCamera= RubikSurfaceView.rotateVectorByInvertedQuat(cameraPoint, accumulated);
+
+      mCamera[0] = rotatedCamera.get1();
+      mCamera[1] = rotatedCamera.get2();
+      mCamera[2] = rotatedCamera.get3();
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// given precomputed mCamera and mPoint, respectively camera and touch point positions in ScreenSpace,
+// cast this touch point onto the surface defined by the 'face' and write the cast coords to 'output'.
+// Center of the 'face' = (0,0), third coord always +- cubeHalfSize.
+
+    private void castTouchPointOntoFace(int face, float cubeHalfSize, float[] output)
+      {
+      int sign = retFaceSign(face);
+      int zAxis= retFaceZaxis(face);
+      float diff = mPoint[zAxis]-mCamera[zAxis];
+
+      float ratio =  diff!=0.0f ? (sign*cubeHalfSize-mCamera[zAxis])/diff : 0.0f;
+
+      output[0] = (mPoint[0]-mCamera[0])*ratio + mCamera[0];
+      output[1] = (mPoint[1]-mCamera[1])*ratio + mCamera[1];
+      output[2] = (mPoint[2]-mCamera[2])*ratio + mCamera[2];
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    private int retFaceSign(int face)
+      {
+      return (face==FRONT || face==RIGHT || face==TOP) ? 1:-1;
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    private int retFaceRotationSign(int face)
+      {
+      return (face==BACK || face==RIGHT || face==TOP) ? 1:-1;
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// retFace{X,Y,Z}axis: 3 functions which return which real AXIS gets mapped to which when we look
+// directly at a given face. For example, when we look at the RIGHT face of the cube (with TOP still
+// in the top) then the 'real' X axis becomes the 'Z' axis, thus retFaceZaxis(RIGHT) = VECTX.
+
+    private int retFaceXaxis(int face)
+      {
+      switch(face)
+        {
+        case FRONT :
+        case BACK  : return RubikCube.VECTX;
+        case LEFT  :
+        case RIGHT : return RubikCube.VECTZ;
+        case TOP   :
+        case BOTTOM: return RubikCube.VECTX;
+        }
+
+      return -1;
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    private int retFaceYaxis(int face)
+      {
+      switch(face)
+        {
+        case FRONT :
+        case BACK  : return RubikCube.VECTY;
+        case LEFT  :
+        case RIGHT : return RubikCube.VECTY;
+        case TOP   :
+        case BOTTOM: return RubikCube.VECTZ;
+        }
+
+      return -1;
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    private int retFaceZaxis(int face)
+      {
+      switch(face)
+        {
+        case FRONT :
+        case BACK  : return RubikCube.VECTZ;
+        case LEFT  :
+        case RIGHT : return RubikCube.VECTX;
+        case TOP   :
+        case BOTTOM: return RubikCube.VECTY;
+        }
+
+      return -1;
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// PUBLIC API
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public RubikCubeMovement(RubikCube cube)
+      {
+      mCube = cube;
+
+      mRotationVect = VECT[0];
+
+      mPoint = new float[3];
+      mCamera= new float[3];
+      mDiff  = new float[3];
+      mTouchPoint = new float[3];
+      mTouchPointCastOntoFace = new float[3];
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public int faceTouched(Static4D accumulated, float cameraDistance, int x, int y, int scrW, int scrH)
+      {
+      float cubeHalfSize= mCube.returnCubeSizeInScreenSpace()*0.5f;
+
+      convertTouchPointToScreenSpace(accumulated,x,y, scrW, scrH);
+      convertCameraPointToScreenSpace(accumulated, cameraDistance);
+
+      for(int face=FRONT; face<=BOTTOM; face++)
+        {
+        if( faceIsVisible(face,cubeHalfSize) )
+          {
+          castTouchPointOntoFace(face,cubeHalfSize, mTouchPointCastOntoFace);
+
+          float qX= (mTouchPointCastOntoFace[0]+cubeHalfSize) / (2*cubeHalfSize);
+          float qY= (mTouchPointCastOntoFace[1]+cubeHalfSize) / (2*cubeHalfSize);
+          float qZ= (mTouchPointCastOntoFace[2]+cubeHalfSize) / (2*cubeHalfSize);
+
+          if( qX<=1 && qX>=0 && qY<=1 && qY>=0 && qZ<=1 && qZ>=0 ) return face;
+          }
+        }
+
+      return NONE;
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public void addNewRotation(Static4D accumulated, int lastTouchedFace, int x, int y, int scrW, int scrH)
+      {
+      float cubeHalfSize= mCube.returnCubeSizeInScreenSpace()*0.5f;
+
+      convertTouchPointToScreenSpace(accumulated, x,y, scrW, scrH);
+      castTouchPointOntoFace(lastTouchedFace,cubeHalfSize,mDiff);
+
+      mDiff[0] -= mTouchPointCastOntoFace[0];
+      mDiff[1] -= mTouchPointCastOntoFace[1];
+      mDiff[2] -= mTouchPointCastOntoFace[2];
+
+      int xAxis = retFaceXaxis(lastTouchedFace);
+      int yAxis = retFaceYaxis(lastTouchedFace);
+      mRotationVect = (isVertical( mDiff[xAxis], mDiff[yAxis]) ? VECT[xAxis]:VECT[yAxis]);
+      float offset= (mTouchPointCastOntoFace[mRotationVect]+cubeHalfSize)/(2*cubeHalfSize);
+
+      mTouchPoint[0] = mPoint[0];
+      mTouchPoint[1] = mPoint[1];
+      mTouchPoint[2] = mPoint[2];
+
+      mCube.addNewRotation(mRotationVect, (int)(mCube.getSize()*offset) );
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public void continueRotation(Static4D accumulated, int lastTouchedFace, int x, int y, int scrW, int scrH)
+      {
+      convertTouchPointToScreenSpace(accumulated, x,y, scrW, scrH);
+
+      mDiff[0] = mPoint[0]-mTouchPoint[0];
+      mDiff[1] = mPoint[1]-mTouchPoint[1];
+      mDiff[2] = mPoint[2]-mTouchPoint[2];
+
+      int xAxis= retFaceXaxis(lastTouchedFace);
+      int yAxis= retFaceYaxis(lastTouchedFace);
+      int sign = retFaceRotationSign(lastTouchedFace);
+      float angle = (mRotationVect==xAxis ? mDiff[yAxis] : -mDiff[xAxis]);
+
+      int scrMin = scrW<scrH ? scrW:scrH;
+
+      mCube.continueRotation(SWIPING_SENSITIVITY*sign*angle/scrMin);
+      }
+}
