commit eaf87d1d919d1cbef32c8a140c2f837e66579e12
Author: Leszek Koltunski <leszek@koltunski.pl>
Date:   Wed Apr 14 09:51:09 2021 +0200

    Rename packages

diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml
index 5602d235..a48670ba 100644
--- a/src/main/AndroidManifest.xml
+++ b/src/main/AndroidManifest.xml
@@ -31,6 +31,6 @@
             </intent-filter>
         </activity>
 
-        <activity android:name="org.distorted.tutorial.TutorialActivity" android:screenOrientation="portrait"/>
+        <activity android:name="org.distorted.tutorials.TutorialActivity" android:screenOrientation="portrait"/>
     </application>
 </manifest>
diff --git a/src/main/java/org/distorted/bandaged/BandagedState.java b/src/main/java/org/distorted/bandaged/BandagedState.java
deleted file mode 100644
index 64838182..00000000
--- a/src/main/java/org/distorted/bandaged/BandagedState.java
+++ /dev/null
@@ -1,116 +0,0 @@
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Copyright 2021 Leszek Koltunski                                                               //
-//                                                                                               //
-// This file is part of Magic Cube.                                                              //
-//                                                                                               //
-// Magic Cube is free software: you can redistribute it and/or modify                            //
-// it under the terms of the GNU General Public License as published by                          //
-// the Free Software Foundation, either version 2 of the License, or                             //
-// (at your option) any later version.                                                           //
-//                                                                                               //
-// Magic Cube is distributed in the hope that it will be useful,                                 //
-// but WITHOUT ANY WARRANTY; without even the implied warranty of                                //
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the                                 //
-// GNU General Public License for more details.                                                  //
-//                                                                                               //
-// You should have received a copy of the GNU General Public License                             //
-// along with Magic Cube.  If not, see <http://www.gnu.org/licenses/>.                           //
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-package org.distorted.bandaged;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-public class BandagedState
-{
-  private final int mNumX, mNumY, mNumZ;
-  private final int[] mInfo;
-  private final int[] mTmp;
-  private final int LEN = 4;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public BandagedState(int[] x, int[] y, int[] z)
-    {
-    mTmp = new int[LEN];
-
-    mNumX = x==null ? 0 : x.length/(LEN-1);
-    mNumY = y==null ? 0 : y.length/(LEN-1);
-    mNumZ = z==null ? 0 : z.length/(LEN-1);
-
-    mInfo = new int[LEN*(mNumX+mNumY+mNumZ)];
-    int start = 0;
-
-    for(int i=0; i<mNumX; i++)
-      {
-      mInfo[LEN*i   + start] = 0;
-      mInfo[LEN*i+1 + start] = x[(LEN-1)*i  ];
-      mInfo[LEN*i+2 + start] = x[(LEN-1)*i+1];
-      mInfo[LEN*i+3 + start] = x[(LEN-1)*i+2];
-      }
-
-    start = LEN*mNumX;
-
-    for(int i=0; i<mNumY; i++)
-      {
-      mInfo[LEN*i   + start] = 1;
-      mInfo[LEN*i+1 + start] = y[(LEN-1)*i  ];
-      mInfo[LEN*i+2 + start] = y[(LEN-1)*i+1];
-      mInfo[LEN*i+3 + start] = y[(LEN-1)*i+2];
-      }
-
-    start = LEN*(mNumX+mNumY);
-
-    for(int i=0; i<mNumZ; i++)
-      {
-      mInfo[LEN*i   + start] = 2;
-      mInfo[LEN*i+1 + start] = z[(LEN-1)*i  ];
-      mInfo[LEN*i+2 + start] = z[(LEN-1)*i+1];
-      mInfo[LEN*i+3 + start] = z[(LEN-1)*i+2];
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private int getIndex(int num, boolean useX, boolean useY, boolean useZ)
-    {
-    int current= -1, total= mNumX + mNumY + mNumZ;
-
-    for(int i=0; i<total; i++)
-      {
-      if( (mInfo[LEN*i]==0 && useX) || (mInfo[LEN*i]==1 && useY) || (mInfo[LEN*i]==2 && useZ) )
-        {
-        if( ++current==num ) return i;
-        }
-      }
-
-    return -1;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public int getTotal(boolean useX, boolean useY, boolean useZ)
-    {
-    int total = 0;
-
-    if( useX ) total += mNumX;
-    if( useY ) total += mNumY;
-    if( useZ ) total += mNumZ;
-
-    return total;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public int[] getInfo(int num, boolean useX, boolean useY, boolean useZ)
-    {
-    int index = getIndex(num,useX,useY,useZ);
-
-    mTmp[0] = mInfo[LEN*index  ];   // axis
-    mTmp[1] = mInfo[LEN*index+1];   // row
-    mTmp[2] = mInfo[LEN*index+2];   // angle
-    mTmp[3] = mInfo[LEN*index+3];   // next state
-
-    return mTmp;
-    }
-}
\ No newline at end of file
diff --git a/src/main/java/org/distorted/bandaged/BandagedStateGraph.java b/src/main/java/org/distorted/bandaged/BandagedStateGraph.java
deleted file mode 100644
index 8033acb9..00000000
--- a/src/main/java/org/distorted/bandaged/BandagedStateGraph.java
+++ /dev/null
@@ -1,665 +0,0 @@
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Copyright 2021 Leszek Koltunski                                                               //
-//                                                                                               //
-// This file is part of Magic Cube.                                                              //
-//                                                                                               //
-// Magic Cube is free software: you can redistribute it and/or modify                            //
-// it under the terms of the GNU General Public License as published by                          //
-// the Free Software Foundation, either version 2 of the License, or                             //
-// (at your option) any later version.                                                           //
-//                                                                                               //
-// Magic Cube is distributed in the hope that it will be useful,                                 //
-// but WITHOUT ANY WARRANTY; without even the implied warranty of                                //
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the                                 //
-// GNU General Public License for more details.                                                  //
-//                                                                                               //
-// You should have received a copy of the GNU General Public License                             //
-// along with Magic Cube.  If not, see <http://www.gnu.org/licenses/>.                           //
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-package org.distorted.bandaged;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-import java.util.ArrayList;
-
-public class BandagedStateGraph
-{
-  private static final int CORNER_S = 0;
-  private static final int CORNER_X = 1;
-  private static final int CORNER_Y = 2;
-  private static final int CORNER_Z = 3;
-
-  private static final int CENTER_0 = 0;
-  private static final int CENTER_1 = 1;
-  private static final int CENTER_2 = 2;
-  private static final int CENTER_3 = 3;
-
-  private int mID;
-  private final int[] mMoves;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public BandagedStateGraph(int id)
-    {
-    mID = id;
-    mMoves = createMoves(mID);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public static void computeGraph()
-    {
-    ArrayList<BandagedStateGraph> graph;
-
-    int id = 0;
-    int id1 = setCenter(id  , CENTER_2, 0);
-    int id2 = setCenter(id1 , CENTER_2, 1);
-    int id3 = setCenter(id2 , CENTER_3, 2);
-    int id4 = setCenter(id3 , CENTER_2, 3);
-
-    int id5 = setCorner(id4 , CORNER_X, 0);
-    int id6 = setCorner(id5 , CORNER_Y, 1);
-    int id7 = setCorner(id6 , CORNER_X, 2);
-    int id8 = setCorner(id7 , CORNER_Z, 3);
-    int id9 = setCorner(id8 , CORNER_Y, 4);
-    int id10= setCorner(id9 , CORNER_Y, 5);
-    int id11= setCorner(id10, CORNER_S, 6);
-    int id12= setCorner(id11, CORNER_Z, 7);
-
-    BandagedStateGraph bsg = new BandagedStateGraph(id12);
-    graph = new ArrayList<>();
-    graph.add(bsg);
-
-    insertChildren(graph,id12);
-    pruneGraph(graph);
-    remapGraph(graph);
-
-    int num = graph.size();
-    android.util.Log.e("D", "\n"+num+" states\n");
-
-    for(int i=0; i<num; i++)
-      {
-      bsg = graph.get(i);
-      android.util.Log.e("D", formatMoves(bsg));
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private static void insertChildren(ArrayList<BandagedStateGraph> list, int id)
-    {
-    BandagedStateGraph bsg = findState(list,id);
-
-    if( bsg==null )
-      {
-      android.util.Log.e("D", "error: "+id+" doesn't exist");
-      return;
-      }
-
-    for(int i=0; i<12; i++)
-      {
-      int move = bsg.getMove(i);
-
-      if( move!=0 )
-        {
-        BandagedStateGraph tmp = findState(list,move);
-
-        if( tmp==null )
-          {
-          tmp = new BandagedStateGraph(move);
-          list.add(tmp);
-          insertChildren(list,move);
-          }
-        }
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private static void pruneGraph(ArrayList<BandagedStateGraph> list)
-    {
-    int num = list.size(), numAxis;
-    boolean pruned = false;
-    BandagedStateGraph bsg;
-
-    for(int i=0; i<num; i++)
-      {
-      bsg = list.get(i);
-      numAxis = bsg.numAxis();
-
-      if( numAxis<2 )
-        {
-        list.remove(i);
-        int id = bsg.getID();
-        pruned = true;
-        remapID(list,id,0);
-        break;
-        }
-      }
-
-    if( pruned ) pruneGraph(list);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private static void remapGraph(ArrayList<BandagedStateGraph> list)
-    {
-    int id, num = list.size();
-    BandagedStateGraph bsg;
-
-    for(int i=0; i<num; i++ )
-      {
-      bsg = list.get(i);
-      id = bsg.getID();
-      bsg.setID(i);
-      remapID(list,id,i);
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private static void remapID(ArrayList<BandagedStateGraph> list, int id, int newId)
-    {
-    BandagedStateGraph bsg;
-    int size = list.size();
-
-    for(int i=0; i<size; i++)
-      {
-      bsg = list.get(i);
-
-      for(int j=0; j<12; j++)
-        {
-        if( bsg.getMove(j)==id ) bsg.setMove(j,newId);
-        }
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private static BandagedStateGraph findState(ArrayList<BandagedStateGraph> list, int id)
-    {
-    BandagedStateGraph bsg;
-    int num = list.size();
-
-    for(int i=0; i<num; i++)
-      {
-      bsg= list.get(i);
-      if( bsg.getID() == id ) return bsg;
-      }
-
-    return null;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private static String formatMoves(BandagedStateGraph bsg)
-    {
-    String x = getTable(bsg,0);
-    String y = getTable(bsg,3);
-    String z = getTable(bsg,6);
-
-    return "    new BandagedState( "+x+", "+y+", "+z+" ),";
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private static String getTable(BandagedStateGraph sc, int index)
-    {
-    String ret = "";
-
-    if( index==0 || index==3 )
-      {
-      int m0 = sc.getMove(index  );
-      int m1 = sc.getMove(index+1);
-      int m2 = sc.getMove(index+2);
-
-      if( m0==0 && m1==0 && m2==0 ) return formatL("null");
-
-      if( m0!=0 ) ret += formatRet(ret,2, 1,m0);
-      if( m1!=0 ) ret += formatRet(ret,2, 2,m1);
-      if( m2!=0 ) ret += formatRet(ret,2,-1,m2);
-      }
-    else
-      {
-      int m0 = sc.getMove(index  );
-      int m1 = sc.getMove(index+1);
-      int m2 = sc.getMove(index+2);
-      int m3 = sc.getMove(index+3);
-      int m4 = sc.getMove(index+4);
-      int m5 = sc.getMove(index+5);
-
-      if( m0==0 && m1==0 && m2==0 && m3==0 && m4==0 && m5==0 )
-        {
-        return formatL("null");
-        }
-
-      if( m0!=0 ) ret += formatRet(ret,0, 1,m0);
-      if( m1!=0 ) ret += formatRet(ret,0, 2,m1);
-      if( m2!=0 ) ret += formatRet(ret,0,-1,m2);
-      if( m3!=0 ) ret += formatRet(ret,2, 1,m3);
-      if( m4!=0 ) ret += formatRet(ret,2, 2,m4);
-      if( m5!=0 ) ret += formatRet(ret,2,-1,m5);
-      }
-
-    return formatL("new int[] {" + ret + "}");
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private static String formatRet(String str, int row, int angle, int id)
-    {
-    String ret = str.length()!=0 ? ",":"";
-
-    ret += row;
-    ret += angle<0 ? "," : ", ";
-    ret += angle;
-
-         if( id< 10 ) ret += (",  "+id);
-    else if( id<100 ) ret += (", " +id);
-    else              ret += (","  +id);
-
-    return ret;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private static final int LENGTH = 38;
-
-  private static String formatL(String input)
-    {
-    int len = input.length();
-    String ret = input;
-    for(int i=0 ;i<LENGTH-len; i++) ret += " ";
-    return ret;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private int getID()
-    {
-    return mID;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void setID(int id)
-    {
-    mID = id;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private int getMove(int index)
-    {
-    return (index>=0 && index<12) ? mMoves[index] : -1;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private int numAxis()
-    {
-    int num = 0;
-
-    if( mMoves[ 0]!=0 || mMoves[ 1]!=0 || mMoves[ 2]!=0 ) num++;
-    if( mMoves[ 3]!=0 || mMoves[ 4]!=0 || mMoves[ 5]!=0 ) num++;
-    if( mMoves[ 6]!=0 || mMoves[ 7]!=0 || mMoves[ 8]!=0 ) num++;
-    if( mMoves[ 9]!=0 || mMoves[10]!=0 || mMoves[11]!=0 ) num++;
-
-    return num;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void setMove(int index, int newMove)
-    {
-    if( index>=0 && index<12 ) mMoves[index] = newMove;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private static String debug(int id)
-    {
-    int ce0 = getCenter(id,0);
-    int ce1 = getCenter(id,1);
-    int ce2 = getCenter(id,2);
-    int ce3 = getCenter(id,3);
-
-    int co0 = getCorner(id,0);
-    int co1 = getCorner(id,1);
-    int co2 = getCorner(id,2);
-    int co3 = getCorner(id,3);
-    int co4 = getCorner(id,4);
-    int co5 = getCorner(id,5);
-    int co6 = getCorner(id,6);
-    int co7 = getCorner(id,7);
-
-    String center = centerString(ce0) + centerString(ce1) + centerString(ce2) + centerString(ce3);
-    String corner = cornerString(co0) + cornerString(co1) + cornerString(co2) + cornerString(co3) +
-                    cornerString(co4) + cornerString(co5) + cornerString(co6) + cornerString(co7);
-
-    return center + " -" + corner;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private static String centerString(int center)
-    {
-    return " "+center;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private static String cornerString(int corner)
-    {
-    switch(corner)
-      {
-      case CORNER_S: return " S";
-      case CORNER_X: return " X";
-      case CORNER_Y: return " Y";
-      case CORNER_Z: return " Z";
-      }
-
-    return "?";
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private static int[] createMoves(int id)
-    {
-    int[] ret = new int[12];
-
-    boolean moveX  = xPossible(id);
-    boolean moveY  = yPossible(id);
-    boolean moveZ0 = z0Possible(id);
-    boolean moveZ2 = z2Possible(id);
-
-    if( moveX ) createXmoves(id,ret);
-    else        { ret[ 0] = 0; ret[ 1] = 0; ret[ 2] = 0; }
-    if( moveY ) createYmoves(id,ret);
-    else        { ret[ 3] = 0; ret[ 4] = 0; ret[ 5] = 0; }
-    if( moveZ0) createZ0moves(id,ret);
-    else        { ret[ 6] = 0; ret[ 7] = 0; ret[ 8] = 0; }
-    if( moveZ2) createZ2moves(id,ret);
-    else        { ret[ 9] = 0; ret[10] = 0; ret[11] = 0; }
-
-    return ret;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private static boolean xPossible(int id)
-    {
-    if( getCorner(id,4)==CORNER_X ) return false;
-    if( getCorner(id,5)==CORNER_X ) return false;
-    if( getCorner(id,6)==CORNER_X ) return false;
-    if( getCorner(id,7)==CORNER_X ) return false;
-
-    if( getCenter(id,1)==CENTER_1 ) return false;
-    if( getCenter(id,2)==CENTER_1 ) return false;
-    if( getCenter(id,3)==CENTER_1 ) return false;
-
-    return true;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private static boolean yPossible(int id)
-    {
-    if( getCorner(id,2)==CORNER_Y ) return false;
-    if( getCorner(id,3)==CORNER_Y ) return false;
-    if( getCorner(id,6)==CORNER_Y ) return false;
-    if( getCorner(id,7)==CORNER_Y ) return false;
-
-    if( getCenter(id,0)==CENTER_0 ) return false;
-    if( getCenter(id,2)==CENTER_0 ) return false;
-    if( getCenter(id,3)==CENTER_0 ) return false;
-
-    return true;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private static boolean z0Possible(int id)
-    {
-    if( getCorner(id,0)==CORNER_Z ) return false;
-    if( getCorner(id,2)==CORNER_Z ) return false;
-    if( getCorner(id,4)==CORNER_Z ) return false;
-    if( getCorner(id,6)==CORNER_Z ) return false;
-
-    if( getCenter(id,0)==CENTER_1 ) return false;
-    if( getCenter(id,1)==CENTER_0 ) return false;
-
-    return true;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private static boolean z2Possible(int id)
-    {
-    if( getCorner(id,1)==CORNER_Z ) return false;
-    if( getCorner(id,3)==CORNER_Z ) return false;
-    if( getCorner(id,5)==CORNER_Z ) return false;
-    if( getCorner(id,7)==CORNER_Z ) return false;
-
-    if( getCenter(id,0)==CENTER_3 ) return false;
-    if( getCenter(id,1)==CENTER_2 ) return false;
-
-    return true;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private static int getCorner(int id, int index)
-    {
-    return (id>>(14-2*index))&3;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private static int getCenter(int id, int index)
-    {
-    return (id>>(22-2*index))&3;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private static int setCorner(int id, int corner, int index)
-    {
-    return id + ((corner-getCorner(id,index))<<(14-2*index));
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private static int setCenter(int id, int center, int index)
-    {
-    return id + ((center-getCenter(id,index))<<(22-2*index));
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private static void createXmoves(int id, int[] moves)
-    {
-    int id1 = rotateX(id);
-    moves[0] = id1;
-    int id2 = rotateX(id1);
-    moves[1] = id2;
-    int id3 = rotateX(id2);
-    moves[2] = id3;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private static void createYmoves(int id, int[] moves)
-    {
-    int id1 = rotateY(id);
-    moves[3] = id1;
-    int id2 = rotateY(id1);
-    moves[4] = id2;
-    int id3 = rotateY(id2);
-    moves[5] = id3;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private static void createZ0moves(int id, int[] moves)
-    {
-    int id1 = rotateZ0(id);
-    moves[6] = id1;
-    int id2 = rotateZ0(id1);
-    moves[7] = id2;
-    int id3 = rotateZ0(id2);
-    moves[8] = id3;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private static void createZ2moves(int id, int[] moves)
-    {
-    int id1 = rotateZ2(id);
-    moves[ 9] = id1;
-    int id2 = rotateZ2(id1);
-    moves[10] = id2;
-    int id3 = rotateZ2(id2);
-    moves[11] = id3;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private static int rotateX(int id)
-    {
-    int newCorner4 = rotCornerX(getCorner(id,5));
-    int newCorner5 = rotCornerX(getCorner(id,7));
-    int newCorner6 = rotCornerX(getCorner(id,4));
-    int newCorner7 = rotCornerX(getCorner(id,6));
-    int newCenter  = rotCenter (getCenter(id,0));
-
-    int id1 = setCorner(id ,newCorner4,4);
-    int id2 = setCorner(id1,newCorner5,5);
-    int id3 = setCorner(id2,newCorner6,6);
-    int id4 = setCorner(id3,newCorner7,7);
-    int id5 = setCenter(id4,newCenter ,0);
-
-    return id5;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private static int rotateY(int id)
-    {
-    int newCorner2 = rotCornerY(getCorner(id,6));
-    int newCorner3 = rotCornerY(getCorner(id,2));
-    int newCorner6 = rotCornerY(getCorner(id,7));
-    int newCorner7 = rotCornerY(getCorner(id,3));
-    int newCenter  = rotCenter (getCenter(id,1));
-
-    int id1 = setCorner(id ,newCorner2,2);
-    int id2 = setCorner(id1,newCorner3,3);
-    int id3 = setCorner(id2,newCorner6,6);
-    int id4 = setCorner(id3,newCorner7,7);
-    int id5 = setCenter(id4,newCenter ,1);
-
-    return id5;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private static int rotateZ0(int id)
-    {
-    int newCorner0 = rotCornerZ(getCorner(id,2));
-    int newCorner2 = rotCornerZ(getCorner(id,6));
-    int newCorner4 = rotCornerZ(getCorner(id,0));
-    int newCorner6 = rotCornerZ(getCorner(id,4));
-    int newCenter  = rotCenter (getCenter(id,2));
-
-    int id1 = setCorner(id ,newCorner0,0);
-    int id2 = setCorner(id1,newCorner2,2);
-    int id3 = setCorner(id2,newCorner4,4);
-    int id4 = setCorner(id3,newCorner6,6);
-    int id5 = setCenter(id4,newCenter ,2);
-
-    return id5;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private static int rotateZ2(int id)
-    {
-    int newCorner1 = rotCornerZ(getCorner(id,3));
-    int newCorner3 = rotCornerZ(getCorner(id,7));
-    int newCorner5 = rotCornerZ(getCorner(id,1));
-    int newCorner7 = rotCornerZ(getCorner(id,5));
-    int newCenter  = rotCenter (getCenter(id,3));
-
-    int id1 = setCorner(id ,newCorner1,1);
-    int id2 = setCorner(id1,newCorner3,3);
-    int id3 = setCorner(id2,newCorner5,5);
-    int id4 = setCorner(id3,newCorner7,7);
-    int id5 = setCenter(id4,newCenter ,3);
-
-    return id5;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private static int rotCornerX(int corner)
-    {
-    switch(corner)
-      {
-      case CORNER_S: return CORNER_S;
-      case CORNER_X: android.util.Log.e("DIST", "rotateX: ERROR");
-                     return CORNER_S;
-      case CORNER_Y: return CORNER_Z;
-      case CORNER_Z: return CORNER_Y;
-      }
-
-    return CORNER_S;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private static int rotCornerY(int corner)
-    {
-    switch(corner)
-      {
-      case CORNER_S: return CORNER_S;
-      case CORNER_X: return CORNER_Z;
-      case CORNER_Y: android.util.Log.e("DIST", "rotateY: ERROR");
-                     return CORNER_S;
-      case CORNER_Z: return CORNER_X;
-      }
-
-    return CORNER_S;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private static int rotCornerZ(int corner)
-    {
-    switch(corner)
-      {
-      case CORNER_S: return CORNER_S;
-      case CORNER_X: return CORNER_Y;
-      case CORNER_Y: return CORNER_X;
-      case CORNER_Z: android.util.Log.e("DIST", "rotateZ: ERROR");
-                     return CORNER_S;
-      }
-
-    return CORNER_S;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private static int rotCenter(int center)
-    {
-    switch(center)
-      {
-      case CENTER_0: return CENTER_3;
-      case CENTER_1: return CENTER_0;
-      case CENTER_2: return CENTER_1;
-      case CENTER_3: return CENTER_2;
-      }
-
-    return CENTER_0;
-    }
-}
diff --git a/src/main/java/org/distorted/dialogs/RubikDialogTutorial.java b/src/main/java/org/distorted/dialogs/RubikDialogTutorial.java
index 5a4b3afe..a15d34e2 100644
--- a/src/main/java/org/distorted/dialogs/RubikDialogTutorial.java
+++ b/src/main/java/org/distorted/dialogs/RubikDialogTutorial.java
@@ -43,7 +43,7 @@ import com.google.android.material.tabs.TabLayout;
 
 import org.distorted.main.R;
 import org.distorted.main.RubikActivity;
-import org.distorted.tutorial.TutorialList;
+import org.distorted.tutorials.TutorialList;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
diff --git a/src/main/java/org/distorted/dialogs/RubikDialogTutorialPagerAdapter.java b/src/main/java/org/distorted/dialogs/RubikDialogTutorialPagerAdapter.java
index 0eaf10a6..4563da8b 100644
--- a/src/main/java/org/distorted/dialogs/RubikDialogTutorialPagerAdapter.java
+++ b/src/main/java/org/distorted/dialogs/RubikDialogTutorialPagerAdapter.java
@@ -28,7 +28,7 @@ import androidx.fragment.app.FragmentActivity;
 import androidx.viewpager.widget.PagerAdapter;
 import androidx.viewpager.widget.ViewPager;
 
-import org.distorted.tutorial.TutorialList;
+import org.distorted.tutorials.TutorialList;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
diff --git a/src/main/java/org/distorted/dialogs/RubikDialogTutorialView.java b/src/main/java/org/distorted/dialogs/RubikDialogTutorialView.java
index b1429714..33ea6fb9 100644
--- a/src/main/java/org/distorted/dialogs/RubikDialogTutorialView.java
+++ b/src/main/java/org/distorted/dialogs/RubikDialogTutorialView.java
@@ -39,7 +39,7 @@ import org.distorted.main.BuildConfig;
 import org.distorted.main.R;
 import org.distorted.main.RubikActivity;
 import org.distorted.objects.ObjectList;
-import org.distorted.tutorial.TutorialList;
+import org.distorted.tutorials.TutorialList;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
diff --git a/src/main/java/org/distorted/helpers/BandagedState.java b/src/main/java/org/distorted/helpers/BandagedState.java
new file mode 100644
index 00000000..b0c02e58
--- /dev/null
+++ b/src/main/java/org/distorted/helpers/BandagedState.java
@@ -0,0 +1,116 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2021 Leszek Koltunski                                                               //
+//                                                                                               //
+// This file is part of Magic Cube.                                                              //
+//                                                                                               //
+// Magic Cube is free software: you can redistribute it and/or modify                            //
+// it under the terms of the GNU General Public License as published by                          //
+// the Free Software Foundation, either version 2 of the License, or                             //
+// (at your option) any later version.                                                           //
+//                                                                                               //
+// Magic Cube is distributed in the hope that it will be useful,                                 //
+// but WITHOUT ANY WARRANTY; without even the implied warranty of                                //
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the                                 //
+// GNU General Public License for more details.                                                  //
+//                                                                                               //
+// You should have received a copy of the GNU General Public License                             //
+// along with Magic Cube.  If not, see <http://www.gnu.org/licenses/>.                           //
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+package org.distorted.helpers;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+public class BandagedState
+{
+  private final int mNumX, mNumY, mNumZ;
+  private final int[] mInfo;
+  private final int[] mTmp;
+  private final int LEN = 4;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public BandagedState(int[] x, int[] y, int[] z)
+    {
+    mTmp = new int[LEN];
+
+    mNumX = x==null ? 0 : x.length/(LEN-1);
+    mNumY = y==null ? 0 : y.length/(LEN-1);
+    mNumZ = z==null ? 0 : z.length/(LEN-1);
+
+    mInfo = new int[LEN*(mNumX+mNumY+mNumZ)];
+    int start = 0;
+
+    for(int i=0; i<mNumX; i++)
+      {
+      mInfo[LEN*i   + start] = 0;
+      mInfo[LEN*i+1 + start] = x[(LEN-1)*i  ];
+      mInfo[LEN*i+2 + start] = x[(LEN-1)*i+1];
+      mInfo[LEN*i+3 + start] = x[(LEN-1)*i+2];
+      }
+
+    start = LEN*mNumX;
+
+    for(int i=0; i<mNumY; i++)
+      {
+      mInfo[LEN*i   + start] = 1;
+      mInfo[LEN*i+1 + start] = y[(LEN-1)*i  ];
+      mInfo[LEN*i+2 + start] = y[(LEN-1)*i+1];
+      mInfo[LEN*i+3 + start] = y[(LEN-1)*i+2];
+      }
+
+    start = LEN*(mNumX+mNumY);
+
+    for(int i=0; i<mNumZ; i++)
+      {
+      mInfo[LEN*i   + start] = 2;
+      mInfo[LEN*i+1 + start] = z[(LEN-1)*i  ];
+      mInfo[LEN*i+2 + start] = z[(LEN-1)*i+1];
+      mInfo[LEN*i+3 + start] = z[(LEN-1)*i+2];
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private int getIndex(int num, boolean useX, boolean useY, boolean useZ)
+    {
+    int current= -1, total= mNumX + mNumY + mNumZ;
+
+    for(int i=0; i<total; i++)
+      {
+      if( (mInfo[LEN*i]==0 && useX) || (mInfo[LEN*i]==1 && useY) || (mInfo[LEN*i]==2 && useZ) )
+        {
+        if( ++current==num ) return i;
+        }
+      }
+
+    return -1;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public int getTotal(boolean useX, boolean useY, boolean useZ)
+    {
+    int total = 0;
+
+    if( useX ) total += mNumX;
+    if( useY ) total += mNumY;
+    if( useZ ) total += mNumZ;
+
+    return total;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public int[] getInfo(int num, boolean useX, boolean useY, boolean useZ)
+    {
+    int index = getIndex(num,useX,useY,useZ);
+
+    mTmp[0] = mInfo[LEN*index  ];   // axis
+    mTmp[1] = mInfo[LEN*index+1];   // row
+    mTmp[2] = mInfo[LEN*index+2];   // angle
+    mTmp[3] = mInfo[LEN*index+3];   // next state
+
+    return mTmp;
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/org/distorted/helpers/BandagedStateGraph.java b/src/main/java/org/distorted/helpers/BandagedStateGraph.java
new file mode 100644
index 00000000..df66f000
--- /dev/null
+++ b/src/main/java/org/distorted/helpers/BandagedStateGraph.java
@@ -0,0 +1,665 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2021 Leszek Koltunski                                                               //
+//                                                                                               //
+// This file is part of Magic Cube.                                                              //
+//                                                                                               //
+// Magic Cube is free software: you can redistribute it and/or modify                            //
+// it under the terms of the GNU General Public License as published by                          //
+// the Free Software Foundation, either version 2 of the License, or                             //
+// (at your option) any later version.                                                           //
+//                                                                                               //
+// Magic Cube is distributed in the hope that it will be useful,                                 //
+// but WITHOUT ANY WARRANTY; without even the implied warranty of                                //
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the                                 //
+// GNU General Public License for more details.                                                  //
+//                                                                                               //
+// You should have received a copy of the GNU General Public License                             //
+// along with Magic Cube.  If not, see <http://www.gnu.org/licenses/>.                           //
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+package org.distorted.helpers;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+import java.util.ArrayList;
+
+public class BandagedStateGraph
+{
+  private static final int CORNER_S = 0;
+  private static final int CORNER_X = 1;
+  private static final int CORNER_Y = 2;
+  private static final int CORNER_Z = 3;
+
+  private static final int CENTER_0 = 0;
+  private static final int CENTER_1 = 1;
+  private static final int CENTER_2 = 2;
+  private static final int CENTER_3 = 3;
+
+  private int mID;
+  private final int[] mMoves;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public BandagedStateGraph(int id)
+    {
+    mID = id;
+    mMoves = createMoves(mID);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public static void computeGraph()
+    {
+    ArrayList<BandagedStateGraph> graph;
+
+    int id = 0;
+    int id1 = setCenter(id  , CENTER_2, 0);
+    int id2 = setCenter(id1 , CENTER_2, 1);
+    int id3 = setCenter(id2 , CENTER_3, 2);
+    int id4 = setCenter(id3 , CENTER_2, 3);
+
+    int id5 = setCorner(id4 , CORNER_X, 0);
+    int id6 = setCorner(id5 , CORNER_Y, 1);
+    int id7 = setCorner(id6 , CORNER_X, 2);
+    int id8 = setCorner(id7 , CORNER_Z, 3);
+    int id9 = setCorner(id8 , CORNER_Y, 4);
+    int id10= setCorner(id9 , CORNER_Y, 5);
+    int id11= setCorner(id10, CORNER_S, 6);
+    int id12= setCorner(id11, CORNER_Z, 7);
+
+    BandagedStateGraph bsg = new BandagedStateGraph(id12);
+    graph = new ArrayList<>();
+    graph.add(bsg);
+
+    insertChildren(graph,id12);
+    pruneGraph(graph);
+    remapGraph(graph);
+
+    int num = graph.size();
+    android.util.Log.e("D", "\n"+num+" states\n");
+
+    for(int i=0; i<num; i++)
+      {
+      bsg = graph.get(i);
+      android.util.Log.e("D", formatMoves(bsg));
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private static void insertChildren(ArrayList<BandagedStateGraph> list, int id)
+    {
+    BandagedStateGraph bsg = findState(list,id);
+
+    if( bsg==null )
+      {
+      android.util.Log.e("D", "error: "+id+" doesn't exist");
+      return;
+      }
+
+    for(int i=0; i<12; i++)
+      {
+      int move = bsg.getMove(i);
+
+      if( move!=0 )
+        {
+        BandagedStateGraph tmp = findState(list,move);
+
+        if( tmp==null )
+          {
+          tmp = new BandagedStateGraph(move);
+          list.add(tmp);
+          insertChildren(list,move);
+          }
+        }
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private static void pruneGraph(ArrayList<BandagedStateGraph> list)
+    {
+    int num = list.size(), numAxis;
+    boolean pruned = false;
+    BandagedStateGraph bsg;
+
+    for(int i=0; i<num; i++)
+      {
+      bsg = list.get(i);
+      numAxis = bsg.numAxis();
+
+      if( numAxis<2 )
+        {
+        list.remove(i);
+        int id = bsg.getID();
+        pruned = true;
+        remapID(list,id,0);
+        break;
+        }
+      }
+
+    if( pruned ) pruneGraph(list);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private static void remapGraph(ArrayList<BandagedStateGraph> list)
+    {
+    int id, num = list.size();
+    BandagedStateGraph bsg;
+
+    for(int i=0; i<num; i++ )
+      {
+      bsg = list.get(i);
+      id = bsg.getID();
+      bsg.setID(i);
+      remapID(list,id,i);
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private static void remapID(ArrayList<BandagedStateGraph> list, int id, int newId)
+    {
+    BandagedStateGraph bsg;
+    int size = list.size();
+
+    for(int i=0; i<size; i++)
+      {
+      bsg = list.get(i);
+
+      for(int j=0; j<12; j++)
+        {
+        if( bsg.getMove(j)==id ) bsg.setMove(j,newId);
+        }
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private static BandagedStateGraph findState(ArrayList<BandagedStateGraph> list, int id)
+    {
+    BandagedStateGraph bsg;
+    int num = list.size();
+
+    for(int i=0; i<num; i++)
+      {
+      bsg= list.get(i);
+      if( bsg.getID() == id ) return bsg;
+      }
+
+    return null;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private static String formatMoves(BandagedStateGraph bsg)
+    {
+    String x = getTable(bsg,0);
+    String y = getTable(bsg,3);
+    String z = getTable(bsg,6);
+
+    return "    new BandagedState( "+x+", "+y+", "+z+" ),";
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private static String getTable(BandagedStateGraph sc, int index)
+    {
+    String ret = "";
+
+    if( index==0 || index==3 )
+      {
+      int m0 = sc.getMove(index  );
+      int m1 = sc.getMove(index+1);
+      int m2 = sc.getMove(index+2);
+
+      if( m0==0 && m1==0 && m2==0 ) return formatL("null");
+
+      if( m0!=0 ) ret += formatRet(ret,2, 1,m0);
+      if( m1!=0 ) ret += formatRet(ret,2, 2,m1);
+      if( m2!=0 ) ret += formatRet(ret,2,-1,m2);
+      }
+    else
+      {
+      int m0 = sc.getMove(index  );
+      int m1 = sc.getMove(index+1);
+      int m2 = sc.getMove(index+2);
+      int m3 = sc.getMove(index+3);
+      int m4 = sc.getMove(index+4);
+      int m5 = sc.getMove(index+5);
+
+      if( m0==0 && m1==0 && m2==0 && m3==0 && m4==0 && m5==0 )
+        {
+        return formatL("null");
+        }
+
+      if( m0!=0 ) ret += formatRet(ret,0, 1,m0);
+      if( m1!=0 ) ret += formatRet(ret,0, 2,m1);
+      if( m2!=0 ) ret += formatRet(ret,0,-1,m2);
+      if( m3!=0 ) ret += formatRet(ret,2, 1,m3);
+      if( m4!=0 ) ret += formatRet(ret,2, 2,m4);
+      if( m5!=0 ) ret += formatRet(ret,2,-1,m5);
+      }
+
+    return formatL("new int[] {" + ret + "}");
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private static String formatRet(String str, int row, int angle, int id)
+    {
+    String ret = str.length()!=0 ? ",":"";
+
+    ret += row;
+    ret += angle<0 ? "," : ", ";
+    ret += angle;
+
+         if( id< 10 ) ret += (",  "+id);
+    else if( id<100 ) ret += (", " +id);
+    else              ret += (","  +id);
+
+    return ret;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private static final int LENGTH = 38;
+
+  private static String formatL(String input)
+    {
+    int len = input.length();
+    String ret = input;
+    for(int i=0 ;i<LENGTH-len; i++) ret += " ";
+    return ret;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private int getID()
+    {
+    return mID;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void setID(int id)
+    {
+    mID = id;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private int getMove(int index)
+    {
+    return (index>=0 && index<12) ? mMoves[index] : -1;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private int numAxis()
+    {
+    int num = 0;
+
+    if( mMoves[ 0]!=0 || mMoves[ 1]!=0 || mMoves[ 2]!=0 ) num++;
+    if( mMoves[ 3]!=0 || mMoves[ 4]!=0 || mMoves[ 5]!=0 ) num++;
+    if( mMoves[ 6]!=0 || mMoves[ 7]!=0 || mMoves[ 8]!=0 ) num++;
+    if( mMoves[ 9]!=0 || mMoves[10]!=0 || mMoves[11]!=0 ) num++;
+
+    return num;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void setMove(int index, int newMove)
+    {
+    if( index>=0 && index<12 ) mMoves[index] = newMove;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private static String debug(int id)
+    {
+    int ce0 = getCenter(id,0);
+    int ce1 = getCenter(id,1);
+    int ce2 = getCenter(id,2);
+    int ce3 = getCenter(id,3);
+
+    int co0 = getCorner(id,0);
+    int co1 = getCorner(id,1);
+    int co2 = getCorner(id,2);
+    int co3 = getCorner(id,3);
+    int co4 = getCorner(id,4);
+    int co5 = getCorner(id,5);
+    int co6 = getCorner(id,6);
+    int co7 = getCorner(id,7);
+
+    String center = centerString(ce0) + centerString(ce1) + centerString(ce2) + centerString(ce3);
+    String corner = cornerString(co0) + cornerString(co1) + cornerString(co2) + cornerString(co3) +
+                    cornerString(co4) + cornerString(co5) + cornerString(co6) + cornerString(co7);
+
+    return center + " -" + corner;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private static String centerString(int center)
+    {
+    return " "+center;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private static String cornerString(int corner)
+    {
+    switch(corner)
+      {
+      case CORNER_S: return " S";
+      case CORNER_X: return " X";
+      case CORNER_Y: return " Y";
+      case CORNER_Z: return " Z";
+      }
+
+    return "?";
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private static int[] createMoves(int id)
+    {
+    int[] ret = new int[12];
+
+    boolean moveX  = xPossible(id);
+    boolean moveY  = yPossible(id);
+    boolean moveZ0 = z0Possible(id);
+    boolean moveZ2 = z2Possible(id);
+
+    if( moveX ) createXmoves(id,ret);
+    else        { ret[ 0] = 0; ret[ 1] = 0; ret[ 2] = 0; }
+    if( moveY ) createYmoves(id,ret);
+    else        { ret[ 3] = 0; ret[ 4] = 0; ret[ 5] = 0; }
+    if( moveZ0) createZ0moves(id,ret);
+    else        { ret[ 6] = 0; ret[ 7] = 0; ret[ 8] = 0; }
+    if( moveZ2) createZ2moves(id,ret);
+    else        { ret[ 9] = 0; ret[10] = 0; ret[11] = 0; }
+
+    return ret;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private static boolean xPossible(int id)
+    {
+    if( getCorner(id,4)==CORNER_X ) return false;
+    if( getCorner(id,5)==CORNER_X ) return false;
+    if( getCorner(id,6)==CORNER_X ) return false;
+    if( getCorner(id,7)==CORNER_X ) return false;
+
+    if( getCenter(id,1)==CENTER_1 ) return false;
+    if( getCenter(id,2)==CENTER_1 ) return false;
+    if( getCenter(id,3)==CENTER_1 ) return false;
+
+    return true;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private static boolean yPossible(int id)
+    {
+    if( getCorner(id,2)==CORNER_Y ) return false;
+    if( getCorner(id,3)==CORNER_Y ) return false;
+    if( getCorner(id,6)==CORNER_Y ) return false;
+    if( getCorner(id,7)==CORNER_Y ) return false;
+
+    if( getCenter(id,0)==CENTER_0 ) return false;
+    if( getCenter(id,2)==CENTER_0 ) return false;
+    if( getCenter(id,3)==CENTER_0 ) return false;
+
+    return true;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private static boolean z0Possible(int id)
+    {
+    if( getCorner(id,0)==CORNER_Z ) return false;
+    if( getCorner(id,2)==CORNER_Z ) return false;
+    if( getCorner(id,4)==CORNER_Z ) return false;
+    if( getCorner(id,6)==CORNER_Z ) return false;
+
+    if( getCenter(id,0)==CENTER_1 ) return false;
+    if( getCenter(id,1)==CENTER_0 ) return false;
+
+    return true;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private static boolean z2Possible(int id)
+    {
+    if( getCorner(id,1)==CORNER_Z ) return false;
+    if( getCorner(id,3)==CORNER_Z ) return false;
+    if( getCorner(id,5)==CORNER_Z ) return false;
+    if( getCorner(id,7)==CORNER_Z ) return false;
+
+    if( getCenter(id,0)==CENTER_3 ) return false;
+    if( getCenter(id,1)==CENTER_2 ) return false;
+
+    return true;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private static int getCorner(int id, int index)
+    {
+    return (id>>(14-2*index))&3;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private static int getCenter(int id, int index)
+    {
+    return (id>>(22-2*index))&3;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private static int setCorner(int id, int corner, int index)
+    {
+    return id + ((corner-getCorner(id,index))<<(14-2*index));
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private static int setCenter(int id, int center, int index)
+    {
+    return id + ((center-getCenter(id,index))<<(22-2*index));
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private static void createXmoves(int id, int[] moves)
+    {
+    int id1 = rotateX(id);
+    moves[0] = id1;
+    int id2 = rotateX(id1);
+    moves[1] = id2;
+    int id3 = rotateX(id2);
+    moves[2] = id3;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private static void createYmoves(int id, int[] moves)
+    {
+    int id1 = rotateY(id);
+    moves[3] = id1;
+    int id2 = rotateY(id1);
+    moves[4] = id2;
+    int id3 = rotateY(id2);
+    moves[5] = id3;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private static void createZ0moves(int id, int[] moves)
+    {
+    int id1 = rotateZ0(id);
+    moves[6] = id1;
+    int id2 = rotateZ0(id1);
+    moves[7] = id2;
+    int id3 = rotateZ0(id2);
+    moves[8] = id3;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private static void createZ2moves(int id, int[] moves)
+    {
+    int id1 = rotateZ2(id);
+    moves[ 9] = id1;
+    int id2 = rotateZ2(id1);
+    moves[10] = id2;
+    int id3 = rotateZ2(id2);
+    moves[11] = id3;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private static int rotateX(int id)
+    {
+    int newCorner4 = rotCornerX(getCorner(id,5));
+    int newCorner5 = rotCornerX(getCorner(id,7));
+    int newCorner6 = rotCornerX(getCorner(id,4));
+    int newCorner7 = rotCornerX(getCorner(id,6));
+    int newCenter  = rotCenter (getCenter(id,0));
+
+    int id1 = setCorner(id ,newCorner4,4);
+    int id2 = setCorner(id1,newCorner5,5);
+    int id3 = setCorner(id2,newCorner6,6);
+    int id4 = setCorner(id3,newCorner7,7);
+    int id5 = setCenter(id4,newCenter ,0);
+
+    return id5;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private static int rotateY(int id)
+    {
+    int newCorner2 = rotCornerY(getCorner(id,6));
+    int newCorner3 = rotCornerY(getCorner(id,2));
+    int newCorner6 = rotCornerY(getCorner(id,7));
+    int newCorner7 = rotCornerY(getCorner(id,3));
+    int newCenter  = rotCenter (getCenter(id,1));
+
+    int id1 = setCorner(id ,newCorner2,2);
+    int id2 = setCorner(id1,newCorner3,3);
+    int id3 = setCorner(id2,newCorner6,6);
+    int id4 = setCorner(id3,newCorner7,7);
+    int id5 = setCenter(id4,newCenter ,1);
+
+    return id5;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private static int rotateZ0(int id)
+    {
+    int newCorner0 = rotCornerZ(getCorner(id,2));
+    int newCorner2 = rotCornerZ(getCorner(id,6));
+    int newCorner4 = rotCornerZ(getCorner(id,0));
+    int newCorner6 = rotCornerZ(getCorner(id,4));
+    int newCenter  = rotCenter (getCenter(id,2));
+
+    int id1 = setCorner(id ,newCorner0,0);
+    int id2 = setCorner(id1,newCorner2,2);
+    int id3 = setCorner(id2,newCorner4,4);
+    int id4 = setCorner(id3,newCorner6,6);
+    int id5 = setCenter(id4,newCenter ,2);
+
+    return id5;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private static int rotateZ2(int id)
+    {
+    int newCorner1 = rotCornerZ(getCorner(id,3));
+    int newCorner3 = rotCornerZ(getCorner(id,7));
+    int newCorner5 = rotCornerZ(getCorner(id,1));
+    int newCorner7 = rotCornerZ(getCorner(id,5));
+    int newCenter  = rotCenter (getCenter(id,3));
+
+    int id1 = setCorner(id ,newCorner1,1);
+    int id2 = setCorner(id1,newCorner3,3);
+    int id3 = setCorner(id2,newCorner5,5);
+    int id4 = setCorner(id3,newCorner7,7);
+    int id5 = setCenter(id4,newCenter ,3);
+
+    return id5;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private static int rotCornerX(int corner)
+    {
+    switch(corner)
+      {
+      case CORNER_S: return CORNER_S;
+      case CORNER_X: android.util.Log.e("DIST", "rotateX: ERROR");
+                     return CORNER_S;
+      case CORNER_Y: return CORNER_Z;
+      case CORNER_Z: return CORNER_Y;
+      }
+
+    return CORNER_S;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private static int rotCornerY(int corner)
+    {
+    switch(corner)
+      {
+      case CORNER_S: return CORNER_S;
+      case CORNER_X: return CORNER_Z;
+      case CORNER_Y: android.util.Log.e("DIST", "rotateY: ERROR");
+                     return CORNER_S;
+      case CORNER_Z: return CORNER_X;
+      }
+
+    return CORNER_S;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private static int rotCornerZ(int corner)
+    {
+    switch(corner)
+      {
+      case CORNER_S: return CORNER_S;
+      case CORNER_X: return CORNER_Y;
+      case CORNER_Y: return CORNER_X;
+      case CORNER_Z: android.util.Log.e("DIST", "rotateZ: ERROR");
+                     return CORNER_S;
+      }
+
+    return CORNER_S;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private static int rotCenter(int center)
+    {
+    switch(center)
+      {
+      case CENTER_0: return CENTER_3;
+      case CENTER_1: return CENTER_0;
+      case CENTER_2: return CENTER_1;
+      case CENTER_3: return CENTER_2;
+      }
+
+    return CENTER_0;
+    }
+}
diff --git a/src/main/java/org/distorted/main/RubikActivity.java b/src/main/java/org/distorted/main/RubikActivity.java
index f06bc1c8..f32fbd61 100644
--- a/src/main/java/org/distorted/main/RubikActivity.java
+++ b/src/main/java/org/distorted/main/RubikActivity.java
@@ -47,7 +47,7 @@ import org.distorted.scores.RubikScoresDownloader;
 import org.distorted.objects.ObjectList;
 import org.distorted.states.StateList;
 import org.distorted.states.RubikStatePlay;
-import org.distorted.tutorial.TutorialActivity;
+import org.distorted.tutorials.TutorialActivity;
 
 import java.util.Locale;
 
diff --git a/src/main/java/org/distorted/objects/TwistyBandaged3Plate.java b/src/main/java/org/distorted/objects/TwistyBandaged3Plate.java
index fde5f544..86935225 100644
--- a/src/main/java/org/distorted/objects/TwistyBandaged3Plate.java
+++ b/src/main/java/org/distorted/objects/TwistyBandaged3Plate.java
@@ -25,7 +25,7 @@ import org.distorted.library.main.DistortedEffects;
 import org.distorted.library.main.DistortedTexture;
 import org.distorted.library.mesh.MeshSquare;
 import org.distorted.library.type.Static4D;
-import org.distorted.bandaged.BandagedState;
+import org.distorted.helpers.BandagedState;
 import org.distorted.main.R;
 
 import java.util.Random;
diff --git a/src/main/java/org/distorted/objects/TwistyBandagedEvil.java b/src/main/java/org/distorted/objects/TwistyBandagedEvil.java
index 0db3bdda..15297e3e 100644
--- a/src/main/java/org/distorted/objects/TwistyBandagedEvil.java
+++ b/src/main/java/org/distorted/objects/TwistyBandagedEvil.java
@@ -21,7 +21,7 @@ package org.distorted.objects;
 
 import android.content.res.Resources;
 
-import org.distorted.bandaged.BandagedState;
+import org.distorted.helpers.BandagedState;
 import org.distorted.library.main.DistortedEffects;
 import org.distorted.library.main.DistortedTexture;
 import org.distorted.library.mesh.MeshSquare;
diff --git a/src/main/java/org/distorted/states/RubikStateAbstract.java b/src/main/java/org/distorted/states/RubikStateAbstract.java
index 99bd981c..c14bcf25 100644
--- a/src/main/java/org/distorted/states/RubikStateAbstract.java
+++ b/src/main/java/org/distorted/states/RubikStateAbstract.java
@@ -24,7 +24,7 @@ import android.content.SharedPreferences;
 import org.distorted.main.RubikActivity;
 import org.distorted.objects.ObjectList;
 import org.distorted.patterns.RubikPatternList;
-import org.distorted.tutorial.TutorialList;
+import org.distorted.tutorials.TutorialList;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
diff --git a/src/main/java/org/distorted/tutorial/TutorialActivity.java b/src/main/java/org/distorted/tutorial/TutorialActivity.java
deleted file mode 100644
index 592f8841..00000000
--- a/src/main/java/org/distorted/tutorial/TutorialActivity.java
+++ /dev/null
@@ -1,358 +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.tutorial;
-
-import android.os.Build;
-import android.os.Bundle;
-import android.util.DisplayMetrics;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.WindowManager;
-import android.webkit.WebView;
-import android.widget.LinearLayout;
-
-import androidx.appcompat.app.AppCompatActivity;
-
-import com.google.firebase.analytics.FirebaseAnalytics;
-
-import org.distorted.dialogs.RubikDialogError;
-import org.distorted.library.main.DistortedLibrary;
-import org.distorted.main.R;
-import org.distorted.objects.ObjectList;
-import org.distorted.objects.TwistyObject;
-import org.distorted.states.StateList;
-
-import static org.distorted.main.RubikRenderer.BRIGHTNESS;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-public class TutorialActivity extends AppCompatActivity
-{
-    private static final String URL = "https://www.youtube.com/embed/";
-
-    public static final float DIALOG_BUTTON_SIZE  = 0.06f;
-    public static final float MENU_BIG_TEXT_SIZE  = 0.05f;
-
-    public static final int FLAGS =  View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
-                                   | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
-                                   | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
-                                   | View.SYSTEM_UI_FLAG_FULLSCREEN
-                                   | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
-
-    public static final int FLAGS2=  View.SYSTEM_UI_FLAG_LAYOUT_STABLE
-                                   | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
-
-    private boolean mIsLocked;
-    private FirebaseAnalytics mFirebaseAnalytics;
-    private static int mScreenWidth, mScreenHeight;
-    private int mCurrentApiVersion;
-    private TutorialState mState;
-    private String mURL;
-    private int mObjectOrdinal, mObjectSize;
-    private TutorialWebView mWebView;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    @Override
-    protected void onCreate(Bundle savedState)
-      {
-      super.onCreate(savedState);
-      DistortedLibrary.onCreate(1);
-      setTheme(R.style.CustomActivityThemeNoActionBar);
-      setContentView(R.layout.tutorial);
-
-      Bundle b = getIntent().getExtras();
-
-      if(b != null)
-        {
-        mURL           = b.getString("url");
-        mObjectOrdinal = b.getInt("obj");
-        mObjectSize    = b.getInt("siz");
-        }
-
-      mIsLocked = false;
-      mFirebaseAnalytics = FirebaseAnalytics.getInstance(this);
-
-      DisplayMetrics displaymetrics = new DisplayMetrics();
-      getWindowManager().getDefaultDisplay().getMetrics(displaymetrics);
-      mScreenWidth =displaymetrics.widthPixels;
-      mScreenHeight=displaymetrics.heightPixels;
-
-      hideNavigationBar();
-      cutoutHack();
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    private void hideNavigationBar()
-      {
-      mCurrentApiVersion = Build.VERSION.SDK_INT;
-
-      if(mCurrentApiVersion >= Build.VERSION_CODES.KITKAT)
-        {
-        final View decorView = getWindow().getDecorView();
-
-        decorView.setSystemUiVisibility(FLAGS);
-
-        decorView.setOnSystemUiVisibilityChangeListener(new View.OnSystemUiVisibilityChangeListener()
-          {
-          @Override
-          public void onSystemUiVisibilityChange(int visibility)
-            {
-            if((visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0)
-              {
-              decorView.setSystemUiVisibility(FLAGS);
-              }
-            }
-          });
-        }
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    @Override
-    public void onAttachedToWindow()
-      {
-      super.onAttachedToWindow();
-
-      final float RATIO = 0.15f;
-      float width = getScreenWidthInPixels();
-
-      TutorialSurfaceView viewL = findViewById(R.id.tutorialSurfaceView);
-      ViewGroup.LayoutParams paramsL = viewL.getLayoutParams();
-      paramsL.width = (int)(width*(1.0f-RATIO));
-      viewL.setLayoutParams(paramsL);
-
-      LinearLayout viewR = findViewById(R.id.tutorialRightBar);
-      ViewGroup.LayoutParams paramsR = viewR.getLayoutParams();
-      paramsR.width = (int)(width*RATIO);
-      viewR.setLayoutParams(paramsR);
-
-      final int color = (int)(BRIGHTNESS*255);
-      viewR.setBackgroundColor( (0xFF<<24)+(color<<16)+(color<<8)+color);
-
-      if( mState==null ) mState = new TutorialState();
-
-      mState.createRightPane(this,width);
-
-      WebView videoView = findViewById(R.id.tutorialVideoView);
-      mWebView = new TutorialWebView(this,videoView);
-      mWebView.load(URL+mURL);
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// do not avoid cutouts
-
-    private void cutoutHack()
-      {
-      if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P)
-        {
-        getWindow().getAttributes().layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
-        }
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    @Override
-    public void onWindowFocusChanged(boolean hasFocus)
-      {
-      super.onWindowFocusChanged(hasFocus);
-
-      if(mCurrentApiVersion >= Build.VERSION_CODES.KITKAT && hasFocus)
-        {
-        getWindow().getDecorView().setSystemUiVisibility(FLAGS);
-        }
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-    
-    @Override
-    protected void onPause() 
-      {
-      super.onPause();
-      TutorialSurfaceView view = findViewById(R.id.tutorialSurfaceView);
-      view.onPause();
-
-      if( mWebView!=null ) mWebView.onPause();
-
-      DistortedLibrary.onPause(1);
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-    
-    @Override
-    protected void onResume() 
-      {
-      super.onResume();
-      DistortedLibrary.onResume(1);
-      TutorialSurfaceView view = findViewById(R.id.tutorialSurfaceView);
-      view.onResume();
-      view.initialize();
-
-      if( mWebView!=null ) mWebView.onResume();
-
-      if( mObjectOrdinal>=0 && mObjectOrdinal< ObjectList.NUM_OBJECTS )
-        {
-        ObjectList obj = ObjectList.getObject(mObjectOrdinal);
-        int[] sizes = obj.getSizes();
-        int sizeIndex = ObjectList.getSizeIndex(mObjectOrdinal,mObjectSize);
-
-        if( sizeIndex>=0 && sizeIndex<sizes.length )
-          {
-          view.getPreRender().changeObject(obj,mObjectSize);
-          }
-        }
-      }
-    
-///////////////////////////////////////////////////////////////////////////////////////////////////
-    
-    @Override
-    protected void onDestroy() 
-      {
-      super.onDestroy();
-      DistortedLibrary.onDestroy(1);
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    void OpenGLError()
-      {
-      RubikDialogError errDiag = new RubikDialogError();
-      errDiag.show(getSupportFragmentManager(), null);
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    TutorialState getState()
-      {
-      return mState;
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// PUBLIC API
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    public FirebaseAnalytics getAnalytics()
-      {
-      return mFirebaseAnalytics;
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    public TwistyObject getObject()
-      {
-      TutorialSurfaceView view = findViewById(R.id.rubikSurfaceView);
-      TutorialPreRender pre = view.getPreRender();
-      return pre.getObject();
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    public int getScreenWidthInPixels()
-      {
-      return mScreenWidth;
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    public int getScreenHeightInPixels()
-      {
-      return mScreenHeight;
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    public TutorialPreRender getPreRender()
-      {
-      TutorialSurfaceView view = findViewById(R.id.tutorialSurfaceView);
-      return view.getPreRender();
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    public static int getDrawableSize()
-      {
-      if( mScreenHeight<1000 )
-        {
-        return 0;
-        }
-      if( mScreenHeight<1600 )
-        {
-        return 1;
-        }
-      if( mScreenHeight<1900 )
-        {
-        return 2;
-        }
-
-      return 3;
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    public static int getDrawable(int small, int medium, int big, int huge)
-      {
-      int size = getDrawableSize();
-
-      switch(size)
-        {
-        case 0 : return small;
-        case 1 : return medium;
-        case 2 : return big;
-        default: return huge;
-        }
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    public boolean isVertical()
-      {
-      TutorialSurfaceView view = findViewById(R.id.tutorialSurfaceView);
-      return view.isVertical();
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    public void toggleLock()
-      {
-      mIsLocked = !mIsLocked;
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    public boolean isLocked()
-      {
-      StateList state = StateList.getCurrentState();
-
-      if( state== StateList.PLAY || state== StateList.READ || state== StateList.SOLV )
-        {
-        return mIsLocked;
-        }
-
-      return false;
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    public boolean retLocked()
-      {
-      return mIsLocked;
-      }
-}
diff --git a/src/main/java/org/distorted/tutorial/TutorialList.java b/src/main/java/org/distorted/tutorial/TutorialList.java
deleted file mode 100644
index f7f03887..00000000
--- a/src/main/java/org/distorted/tutorial/TutorialList.java
+++ /dev/null
@@ -1,429 +0,0 @@
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// 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.tutorial;
-
-import org.distorted.objects.ObjectList;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-public enum TutorialList
-{
-  CUBE2 ( ObjectList.CUBE, 2,
-          new String[][] {
-                          {"gb","rJlh5p2wAKA","How to Solve a 2x2 Rubik's Cube","Z3"},
-                          {"es","f85wqJTIDlw","Resolver cubo de Rubik 2X2","Cuby"},
-                          {"ru","azC6P3VYFkc","Как собрать кубик 2Х2","Е Бондаренко"},
-                          {"fr","V1XS993AUuw","Résoudre le cube 2x2","Rachma Nikov"},
-                          {"de","d8tKa8SRkXw","2x2 Zauberwürfel lösen","Pezcraft"},
-                          {"pl","haNWdAYWGsY","Jak ułożyć kostkę Rubika 2x2","DżoDżo"},
-                          {"kr","wTMsdWKq6No","2x2 큐브 공식을 이해하는 해법","듀나메스 큐브 해법연구소"},
-                     //   {"tw","CfOCXxhCb8U","2x2魔術方塊復原","1hrBLD"},
-                         }
-        ),
-
-  CUBE3 ( ObjectList.CUBE, 3,
-          new String[][] {
-                          {"gb","-8ohoCKN0Zw","How to Solve a Rubik's Cube","Z3"},
-                          {"es","GyY0OxDk5lI","Resolver cubo de Rubik 3x3","Cuby"},
-                          {"ru","5S2eq81FRzI","Как собрать кубик рубика","Е Бондаренко"},
-                          {"fr","T-ASx2wbHVY","Comment résoudre un Rubik's Cube","Le Cube"},
-                          {"de","epdcq0L3bDE","3x3 Zauberwürfel lösen","Pezcraft"},
-                          {"pl","cBU9Y729nQM","Jak ułożyć kostkę Rubika 3x3","DżoDżo"},
-                          {"kr","PMLG4__npcY","3x3 큐브 기초해법 (파트1)","듀나메스 큐브 해법연구소"},
-                          {"kr","vbvyjs4Vmoc","3x3 큐브 기초해법 (파트2)","듀나메스 큐브 해법연구소"},
-                          {"kr","V5eeKu9abCc","3x3 큐브 기초해법 (파트3)","듀나메스 큐브 해법연구소"},
-                     //   {"tw","76NmRQx5CLA","魔術方塊教學","1hrBLD"},
-                         }
-        ),
-
-  CUBE4 ( ObjectList.CUBE, 4,
-          new String[][] {
-                          {"gb","RR77Md71Ymc","How to Solve the 4x4 Rubik's Cube","Z3"},
-                          {"es","d_4xk1r9hxU","Resolver cubo de Rubik 4x4","Cuby"},
-                          {"ru","v5ytiOyTFSA","Как собрать кубик 4х4","Алексей Ярыгин"},
-                          {"fr","C83gYXn-zpI","Comment résoudre un Rubik's Cube 4x4","Le Cube"},
-                          {"de","Z7EmIp-TLN0","4x4 Zauberwürfel lösen","JamesKnopf"},
-                          {"pl","LiUxNsowXiI","Jak ułożyć kostkę 4x4","DżoDżo"},
-                          {"kr","5g4QORteCsk","원리로 이해하는 444 큐브 기초 해법","듀나메스 큐브 해법연구소"},
-                      //    {"tw","HuyaNIUaSqo","4x4魔術方塊復原#1","1hrBLD"},
-                      //    {"tw","gHho4gJQMXw","4x4魔術方塊復原#2","1hrBLD"},
-                      //    {"tw","7pbNgXMQxCE","4x4魔術方塊復原#3","1hrBLD"},
-                      //    {"tw","PZvc5XJ2bLY","4x4魔術方塊復原#4","1hrBLD"},
-                      //    {"tw","97vDE29lu2o","4x4魔術方塊復原#5","1hrBLD"},
-                         }
-        ),
-
-  CUBE5 ( ObjectList.CUBE, 5,
-          new String[][] {
-                          {"gb","zMkNkXHzQts","How to Solve the 5x5 Rubik's Cube","Z3"},
-                          {"es","6uaq-xfFs98","Resolver cubo de Rubik 5x5","Cuby"},
-                          {"ru","UtKsyLk45uA","Как собрать кубик 5x5","Кубмаркет"},
-                          {"fr","sq14CsrSkbo","Comment résoudre un Rubik's Cube 5x5","Le Cube"},
-                          {"de","luLwvHDPnrA","5x5 Zauberwürfel lösen","Pezcraft"},
-                          {"pl","ERsPyWOF7mg","Jak ułożyć kostkę 5x5x5","DżoDżo"},
-                          {"kr","D46qDaBFWNM","[555큐브]믿고보는영상!","Playon U온돌차"},
-                         }
-        ),
-
-  PYRA3 ( ObjectList.PYRA, 3,
-          new String[][] {
-                          {"gb","xIQtn2qazvg","Pyraminx Layer By Layer","Z3"},
-                          {"es","4cJJe9RAzAU","Resolver Pyraminx","Cuby"},
-                          {"ru","F4_bhfWyVRQ","Как собрать ПИРАМИДКУ","Е Бондаренко"},
-                          {"fr","Z2h1YI6jPes","Comment résoudre le Pyraminx","ValentinoCube"},
-                          {"de","x_DMA8htJpY","Pyraminx lösen","Pezcraft"},
-                          {"pl","uNpKpJfAa5I","Jak ułożyć: Pyraminx","DżoDżo"},
-                          {"kr","mO3excjvvoA","피라밍크스 맞추는 방법","iamzoone"},
-                    //    {"tw","YS3cDcP6Aro","金字塔方塊解法","1hrBLD"},
-                         }
-        ),
-
-  PYRA4 ( ObjectList.PYRA, 4,
-          new String[][] {
-                          {"gb","tGQDqDcSa6U","How to Solve the Master Pyraminx","Z3"},
-                          {"es","74PIPm9-uPg","Resolver Master Pyraminx 4x4","Cuby"},
-                          {"ru","-F_xJAwkobU","Как собрать Мастер Пираминкс"," Алексей Ярыгин"},
-                          {"fr","F3gzBs7uvmw","Tuto: résoudre le Master Pyraminx","Spaghetti Cubing"},
-                          {"de","3Q_bO7_FfAI","Master Pyraminx lösen","CubaroCubing"},
-                          {"pl","EamwvhmHC7Q","4x4 (Master) Pyraminx PL","MrUk"},
-                          {"kr","JlmBKaHESyY","마스터 피라밍크스 해법","주누후누"},
-                         }
-        ),
-
-  PYRA5 ( ObjectList.PYRA, 5,
-          new String[][] {
-                          {"gb","2nsPEECDdN0","Professor Pyraminx Solve","RedKB"},
-                          {"es","cSDj8OQK3TU","Tutorial del Professor Pyraminx","QBAndo"},
-                          {"ru","gMp1tbDyDWg","Как собрать Professor Pyraminx","RBcuber"},
-                          {"de","pCHx9bVMSgI","Professor Pyraminx Teil 1","Arvid Bollmann"},
-                          {"de","iiNXJMVNmCM","Professor Pyraminx Teil 2","Arvid Bollmann"},
-                         }
-        ),
-
-  DIAM2 ( ObjectList.DIAM, 2,
-          new String[][] {
-                          {"gb","R2wrbJJ3izM","How to Solve a Skewb Diamond","Dr. Penguin^3"},
-                          {"es","2RCusYQdYYE","Como resolver Skewb Diamond","Tutoriales Rubik"},
-                          {"ru","k8B6RFcNoGw","Как собрать Skewb Diamond","Алексей Ярыгин"},
-                          {"fr","tqbkgwNcZCE","Comment résoudre le Skewb Diamond","Valentino Cube"},
-                          {"de","6ewzrCOnZfg","Octagon lösen","JamesKnopf"},
-                          {"pl","61_Z4TpLMBc","Diamond Skewb TUTORIAL PL","MrUk"},
-                          {"kr","hVBSlfHVTME","공식 하나만 사용 - 다이아몬드 스큐브","Denzel Washington"},
-                         }
-        ),
-
-  DINO3 ( ObjectList.DINO, 3,
-          new String[][] {
-                          {"gb","puTJZqFBQwo","Dino Skewb Cube Tutorial","Bearded Cubing"},
-                          {"es","6o1Yo5iCxvI","Resolver Cubo Dino","Cuby"},
-                          {"ru","tWDrCtIv1_U","Как собрать Дино Куб","Алексей Ярыгин"},
-                          {"fr","hNkpte7Mesc","Comment résoudre le Dino Cube","Valentino Cube"},
-                          {"de","RqJLI6_C9JA","Dino Cube Tutorial","GerCubing"},
-                          {"pl","o05DYu8UMio","Dino Cube TUTORIAL PL","MrUk"},
-                          {"kr","imbrqGPSXWQ","(엑스큐브)완전 정복하기!","초등취미생활"},
-                         }
-        ),
-
-  REDI3 ( ObjectList.REDI, 3,
-          new String[][] {
-                          {"gb","Qn7TJED6O-4","How to Solve the MoYu Redi Cube","Z3"},
-                          {"es","g0M38Aotgac","Resolver Redi Cube","Cuby"},
-                          {"ru","ip2wYwc0DMI","Как собрать Реди Куб?","Кубмаркет"},
-                          {"fr","zw7UZcqqsgA","Comment résoudre le Redi Cube","ValentinoCube"},
-                          {"de","YU8riouyC2w","Redi Cube Solve","CubaroCubing"},
-                          {"pl","vxo3lXMsWQI","Jak ułożyć Redi Cube?","DJ rubiks"},
-                          {"kr","a5CzDMbRzbY","레디큐브를 배우기","vincentcube"},
-                         }
-        ),
-
-  HELI3 ( ObjectList.HELI, 3,
-          new String[][] {
-                          {"gb","-suwJpd_PO8","Helicopter Cube Tutorial","Bearded Cubing"},
-                          {"es","DWG9n_YyGPA","Resolver Helicopter Cube","Cuby"},
-                          {"ru","V4lJ3pg7Hio","Как собрать Куб Вертолет","Алексей Ярыгин"},
-                          {"fr","Zk8zWBWD2Ow","Comment résoudre le Helicopter Cube","Julien"},
-                          {"de","6VUH_FkBTlw","Helicopter Cube Tutorial","GerCubing"},
-                          {"pl","zoBZame4gFo","Helicopter Cube TUTORIAL PL","MrUk"},
-                          {"kr","xsXQSrEbgag","헬리콥터 큐브를 맞추는 법","연서큐브박"},
-                         }
-        ),
-
-  SKEW2 ( ObjectList.SKEW, 2,
-          new String[][] {
-                          {"gb","I6132yshkeU","How to Solve the Skewb","Z3"},
-                          {"es","wxQX3HhPgds","Resolver Skewb (Principiantes)","Cuby"},
-                          {"ru","_HSKZLC17w4","Как собрать Скьюб?","Кубмаркет"},
-                          {"fr","lR-GuIroh4k","Comment réussir le skewb","Rachma Nikov"},
-                          {"de","7RX6D5pznOk","Skewb lösen","Pezcraft"},
-                          {"pl","ofRu1fByNpk","Jak ułożyć: Skewb","DżoDżo"},
-                          {"kr","5R3sU-_bMAI","SKEWB 초보 공식","iamzoone"},
-                     //   {"tw","8srf9xhsS9k","Skewb斜轉方塊解法","1hrBLD"},
-                         }
-        ),
-
-  SKEW3 ( ObjectList.SKEW, 3,
-          new String[][] {
-                          {"gb","Jiuf7zQyPYI","Master Skewb Cube Tutorial","Bearded Cubing"},
-                          {"es","8TP6p63KQCA","Master Skewb en Español","jorlozCubes"},
-                          {"ru","7155QSp3T74","часть 1: Как собрать мастер Скьюб","Иван Циков"},
-                          {"ru","14ey-RihjgY","часть 2: Как собрать мастер Скьюб","Иван Циков"},
-                          {"ru","watq6TLa5_E","часть 2.5: Как собрать мастер Скьюб","Иван Циков"},
-                          {"ru","UnsvseFBXmo","часть 3: Как собрать мастер Скьюб","Иван Циков"},
-                          {"fr","tYMoY4EOHVA","Résolution du Master Skewb","Asthalis"},
-                          {"de","LSErzqGNElI","Master Skewb lösen","JamesKnopf"},
-                          {"pl","Y7l3AYFvDJI","Master Skewb TUTORIAL PL","MrUk"},
-                          {"kr","ycR-WmXJCG0","마스터 스큐브 3번째 해법","듀나메스 큐브 해법연구소"},
-                         }
-        ),
-
-  IVY2 ( ObjectList.IVY, 2,
-          new String[][] {
-                          {"gb","QMzeJobSu1M","How to Solve the Ivy Cube","Z3"},
-                          {"es","2-Gf2cmEJDs","Resolver Ivy Cube","Cuby"},
-                          {"ru","pbkfOCnnfsA","Как собрать Иви куб","Алексей Ярыгин"},
-                          {"fr","mn7YTnYu3Uc","Comment résoudre le Ivy Cube","ValentinoCube"},
-                          {"de","vaW5fSUG_O8","Ivy Cube ","ThomasStadler"},
-                          {"pl","8s_0VxNvFA8","Jak ułożyć Ivy Cube","DubiCube"},
-                          {"kr","TmSPgjtSFac","15분만에 아이비큐브 완전정복하기!","초등취미생활"},
-                         }
-        ),
-
-  REX3 ( ObjectList.REX, 3,
-          new String[][] {
-                          {"gb","noAQfWqlMbk","Rex Cube Tutorial","CrazyBadCuber"},
-                          {"es","Q90x9rjLJzw","Resolver Cubo Rex","Cuby"},
-                          {"ru","Dr9CLM6A3fU","Как собрать Рекс Куб","Алексей Ярыгин"},
-                          {"fr","SvK1kf6c43c","Résolution du Rex Cube","Asthalis"},
-                          {"de","AI4vtwpRkEQ","Rex Cube - Tutorial","GerCubing"},
-                          {"pl","ffbFRnHglWY","Rex Cube TUTORIAL PL","MrUk"},
-                          {"kr","B3ftZzHRQyU","렉스 큐브 해법","듀나메스 큐브 해법연구소"},
-                         }
-        ),
-
-  KILO3( ObjectList.KILO, 3,
-          new String[][] {
-                          {"gb","grgGgUSxiQg","How to Solve the Kilominx","Z3"},
-                          {"es","g6WMYjkCLok","Resolver Kilominx","Cuby"},
-                          {"ru","gjaknjuZXPs","Киломинкс как собрать","CUBES WORLD"},
-                          {"fr","F7z6LztN-7A","Résoudre le Kilominx","Twins Cuber"},
-                          {"de","fcmJdpLfZwk","Megaminx 2x2 lösen","JamesKnopf"},
-                          {"pl","tdWh8f8qpq4","Kilominx TUTORIAL PL","MrUK"},
-                          {"kr","8-X4GhQnE5I","2X2 킬로밍크스 TUTORIAL","큐브놀이터"},
-                         }
-       ),
-
-  KILO5( ObjectList.KILO, 5,
-          new String[][] {
-                          {"gb","VAnzC2SYVc4","How To Solve A Master Kilominx","Grizz Media"},
-                          {"es","ozINTg-61Fs","Tutorial Master Kilominx","RubikArt"},
-                          {"ru","0aemQayCZRc","Как собрать Мастер Киломинкс ч.1","Артем Мартиросов"},
-                          {"ru","ohOUFTx-oQI","Как собрать Мастер Киломинкс ч.2","Артем Мартиросов"},
-                          {"ru","YRXRdT2jCn8","Как собрать Мастер Киломинкс ч.3","Артем Мартиросов"},
-                          {"fr","usMiWt44aqo","Résolution du Master Kilominx","Asthalis"},
-                          {"pl","rdln0IG86_s","Master Kilominx TUTORIAL PL","MrUK"},
-                          {"kr","dvy-GxCjm5c","마스터 킬로밍크스 배우기 1","vincentcube"},
-                          {"kr","Jm0B12vNxsE","마스터 킬로밍크스 배우기 2","vincentcube"},
-                          {"kr","H1I18FVpr6g","마스터 킬로밍크스 배우기 3","vincentcube"},
-                         }
-       ),
-
-  MEGA3( ObjectList.MEGA, 3,
-          new String[][] {
-                          {"gb","j4x61L5Onzk","How to Solve the Megaminx","Z3"},
-                          {"es","xuKbT6Il0Ko","Resolver Megaminx","Cuby"},
-                          {"ru","WgoguOY3tKI","Как собрать Мегаминкс","Алексей Ярыгин"},
-                          {"fr","Ln1vl85puKo","Résoudre le Megaminx","Victor Colin"},
-                          {"de","d-GQD6CBdB8","Megaminx lösen","Pezcraft"},
-                          {"pl","BZTW6ApeRZE","Jak ułożyć: Megaminx","DżoDżo"},
-                          {"kr","2NUsMclrD-0","메가밍크스 예시솔빙","iamzoone"},
-                         }
-       ),
-
-  MEGA5( ObjectList.MEGA, 5,
-          new String[][] {
-                          {"gb","MNBMm8BnHtQ","Solve the Gigaminx Part 1","BeardedCubing"},
-                          {"gb","QrrP4GwqVMw","Solve the Gigaminx Part 2","BeardedCubing"},
-                          {"es","ex5EQMBxV1U","Tutorial Gigaminx","RubikArt"},
-                          {"ru","UJYK3SHjSGg","Как собрать Гигаминкс ч.1","Артем Мартиросов"},
-                          {"ru","-iBCpr4Gwsw","Как собрать Гигаминкс ч.2","Артем Мартиросов"},
-                          {"ru","4-dI7NCW8n8","Как собрать Гигаминкс ч.3","Артем Мартиросов"},
-                          {"fr","e485fh0V1dg","Résolution du Gigaminx","Asthalis"},
-                          {"de","APSAj4UtOAg","Megaminx 5x5 lösen","JamesKnopf"},
-                          {"pl","qbKLMCX1wKg","Jak ułożyć Gigaminxa cz.1","chomik19751"},
-                          {"pl","JQOXD3qleH4","Jak ułożyć Gigaminxa cz.2","chomik19751"},
-                          {"pl","WF2katJ22FA","Jak ułożyć Gigaminxa cz.3","chomik19751"},
-                          {"pl","jlyRrJjH4qQ","Jak ułożyć Gigaminxa cz.4","chomik19751"},
-                          {"kr","HfPFrWuz6z4","기가밍크스 gigaminx","큐브놀이터"},
-                         }
-       ),
-
-  BAN1( ObjectList.BAN1, 3,
-          new String[][] {
-                          {"gb","F_iJk_IvpVo","Bandaged Cube","CanChrisSolve"},
-                          {"es","_lTgw5aEFOg","Tutorial 3x3 Fuse Cube","QBAndo"},
-                          {"ru","raYDwFEXIq4","Как собрать Fused Cube","Алексей Ярыгин"},
-                          {"fr","9Cfi4rhKzIw","Tutoriel: résolution du Fused Cube","Skieur Cubb"},
-                          {"pl","0PcUoGxQa6s","Bandaged 3x3 v.A cube","MrUK"},
-                          {"kr","1RePOLrzJNE","밴디지 타입 A 해법","듀나메스 큐브 해법연구소"},
-                         }
-       ),
-
-  BAN2( ObjectList.BAN2, 3,
-          new String[][] {
-                          {"ru","lS_EK0PMWI8","Как собрать 2-bar Cube","Алексей Ярыгин"},
-                          {"pl","tX8ubTLh6p8","Bandaged 3x3 (Two bar)","MrUK"},
-                          {"kr","NE6XuC1r8xw","밴디지 큐브","Denzel Washington"},
-                         }
-       ),
-
-  BAN3( ObjectList.BAN3, 3,
-          new String[][] {
-                          {"gb","7UiCVGygUT4","Bandage Cube C Tutorial","PolyakB"},
-                          {"ru","gXenRA92Wdc","Как собрать Bandaged 3x3 Type C","YG Cuber"},
-                          {"pl","sKfdFLm79Zs","Bandaged 3x3 v.C cube","MrUK"},
-                          {"kr","BcCFgeFy6Ec","밴디지 타입 C 해법","듀나메스 큐브 해법연구소"},
-                         }
-       ),
-
-  BAN4( ObjectList.BAN4, 3,
-          new String[][] {
-                          {"gb","AnpdIKICBpM","Trying to Solve a Bandaged Cube","RedKB"},
-                          {"es","cUyo5fycrvI","Tutorial Bandaged Cube en español","Rafa Garcia Benacazon"},
-                          {"ru","-MTzeEJptsg","Как собрать bandaged Cube B","стратегия знаний"},
-                          {"fr","3rsfIJ3roT0","Tutoriel: résolution du Bicube","Skieur Cubb"},
-                          {"de","sqWVRwkXX9w","Bandaged Cube - Tutorial","GerCubing"},
-                          {"pl","XcHzTvVR6Po","Bandaged 3x3 v.B cube","MrUK"},
-                          {"kr","1gsoijF_5q0","BiCube Tutorial (해법)","듀나메스 큐브 해법연구소"},
-                         }
-       );
-
-  public static final int NUM_OBJECTS = values().length;
-  private final ObjectList mObject;
-  private final int mSize;
-  private final String[][] mTutorials;
-  private final int mNumTutorials;
-
-  private static final TutorialList[] objects;
-
-  static
-    {
-    objects = new TutorialList[NUM_OBJECTS];
-    int i=0;
-
-    for(TutorialList object: TutorialList.values())
-      {
-      objects[i++] = object;
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  TutorialList(ObjectList object, int size, String[][] tutorials)
-    {
-    mObject       = object;
-    mSize         = size;
-    mTutorials    = tutorials;
-    mNumTutorials = mTutorials.length;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public static TutorialList getObject(int ordinal)
-    {
-    return ordinal>=0 && ordinal<NUM_OBJECTS ? objects[ordinal] : CUBE3;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public static int getOrdinal(int objectOrdinal, int size)
-    {
-    if( objectOrdinal==ObjectList.DIN4.ordinal() )
-      {
-      objectOrdinal= ObjectList.DINO.ordinal();
-      }
-
-    for(int i=0; i<NUM_OBJECTS; i++)
-      {
-      if( objects[i].mObject.ordinal() == objectOrdinal && objects[i].mSize == size )
-        {
-        return i;
-        }
-      }
-
-    return -1;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public ObjectList getObjectList()
-    {
-    return mObject;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public int getSize()
-    {
-    return mSize;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public int getNumTutorials()
-    {
-    return mNumTutorials;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public int getIconID()
-    {
-    int sizeIndex = ObjectList.getSizeIndex(mObject.ordinal(),mSize);
-    return mObject.getIconIDs()[sizeIndex];
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public String getTutorialLanguage(int index)
-    {
-    return ( index>=0 && index<mNumTutorials ) ? mTutorials[index][0] : null;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public String getTutorialURL(int index)
-    {
-    return ( index>=0 && index<mNumTutorials ) ? mTutorials[index][1] : null;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public String getTutorialDescription(int index)
-    {
-    return ( index>=0 && index<mNumTutorials ) ? mTutorials[index][2] : null;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public String getTutorialAuthor(int index)
-    {
-    return ( index>=0 && index<mNumTutorials ) ? mTutorials[index][3] : null;
-    }
-}
diff --git a/src/main/java/org/distorted/tutorial/TutorialPreRender.java b/src/main/java/org/distorted/tutorial/TutorialPreRender.java
deleted file mode 100644
index cf032870..00000000
--- a/src/main/java/org/distorted/tutorial/TutorialPreRender.java
+++ /dev/null
@@ -1,420 +0,0 @@
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// 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.tutorial;
-
-import android.content.Context;
-import android.content.res.Resources;
-
-import org.distorted.effects.BaseEffect;
-import org.distorted.effects.EffectController;
-import org.distorted.objects.ObjectList;
-import org.distorted.objects.TwistyObject;
-import org.distorted.main.RubikPreRender.ActionFinishedListener;
-import org.distorted.scores.RubikScores;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-public class TutorialPreRender implements EffectController
-  {
-  private ActionFinishedListener mAddActionListener;
-  private TutorialSurfaceView mView;
-  private boolean mFinishRotation, mRemoveRotation, mAddRotation,
-                  mSetQuat, mChangeObject, mSetupObject, mSolveObject, mScrambleObject,
-                  mInitializeObject, mResetAllTextureMaps, mRemovePatternRotation;
-  private boolean mCanPlay;
-  private boolean mIsSolved;
-  private ObjectList mNextObject;
-  private int mNextSize;
-  private long mRotationFinishedID;
-  private int mScreenWidth;
-  private int[][] mNextMoves;
-  private TwistyObject mOldObject, mNewObject;
-  private int mAddRotationAxis, mAddRotationRowBitmap, mAddRotationAngle;
-  private long mAddRotationDuration;
-  private long mAddRotationID, mRemoveRotationID;
-  private int mNearestAngle;
-  private int mScrambleObjectNum;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  TutorialPreRender(TutorialSurfaceView view)
-    {
-    mView = view;
-
-    mFinishRotation = false;
-    mRemoveRotation = false;
-    mAddRotation    = false;
-    mSetQuat        = false;
-    mChangeObject   = false;
-    mSetupObject    = false;
-    mSolveObject    = false;
-    mScrambleObject = false;
-
-    mCanPlay        = true;
-    mOldObject      = null;
-    mNewObject      = null;
-
-    mScreenWidth       = 0;
-    mScrambleObjectNum = 0;
-
-    mRemovePatternRotation= false;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void createObjectNow(ObjectList object, int size, int[][] moves)
-    {
-    if( mOldObject!=null ) mOldObject.releaseResources();
-    mOldObject = mNewObject;
-
-    Context con = mView.getContext();
-    Resources res = con.getResources();
-
-    mNewObject = object.create(size, mView.getQuat(), moves, res, mScreenWidth);
-
-    if( mNewObject!=null )
-      {
-      mNewObject.createTexture();
-      mView.setMovement(object.getObjectMovementClass());
-
-      if( mScreenWidth!=0 )
-        {
-        mNewObject.recomputeScaleFactor(mScreenWidth);
-        }
-
-      mIsSolved = mNewObject.isSolved();
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void doEffectNow(BaseEffect.Type type)
-    {
-    try
-      {
-      type.startEffect(mView.getRenderer().getScreen(),this);
-      }
-    catch( Exception ex )
-      {
-      mCanPlay= true;
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void removePatternRotation()
-    {
-    mRemovePatternRotation = true;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void removePatternRotationNow()
-    {
-    mRemovePatternRotation=false;
-    mNewObject.removeRotationNow();
-    mAddActionListener.onActionFinished(mRemoveRotationID);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void removeRotationNow()
-    {
-    mRemoveRotation=false;
-    mNewObject.removeRotationNow();
-
-    boolean solved = mNewObject.isSolved();
-
-    if( solved && !mIsSolved )
-      {
-      doEffectNow( BaseEffect.Type.WIN );
-      }
-    else
-      {
-      mCanPlay = true;
-      }
-
-    mIsSolved = solved;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void removeRotation()
-    {
-    mRemoveRotation = true;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void addRotationNow()
-    {
-    mAddRotation = false;
-    mAddRotationID = mNewObject.addNewRotation( mAddRotationAxis, mAddRotationRowBitmap,
-                                                mAddRotationAngle, mAddRotationDuration, this);
-
-    if( mAddRotationID==0 ) // failed to add effect - should never happen
-      {
-      mCanPlay = true;
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void finishRotationNow()
-    {
-    mFinishRotation = false;
-    mCanPlay        = false;
-    mRotationFinishedID = mNewObject.finishRotationNow(this, mNearestAngle);
-
-    if( mRotationFinishedID==0 ) // failed to add effect - should never happen
-      {
-      mCanPlay = true;
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void changeObjectNow()
-    {
-    mChangeObject = false;
-
-    if ( mNewObject==null || mNewObject.getObjectList()!=mNextObject || mNewObject.getNumLayers()!=mNextSize)
-      {
-      mCanPlay  = false;
-      createObjectNow(mNextObject, mNextSize, null);
-      doEffectNow( BaseEffect.Type.SIZECHANGE );
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void setupObjectNow()
-    {
-    mSetupObject = false;
-
-    if ( mNewObject==null || mNewObject.getObjectList()!=mNextObject || mNewObject.getNumLayers()!=mNextSize)
-      {
-      mCanPlay  = false;
-      createObjectNow(mNextObject, mNextSize, mNextMoves);
-      doEffectNow( BaseEffect.Type.SIZECHANGE );
-      }
-    else
-      {
-      mNewObject.initializeObject(mNextMoves);
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void solveObjectNow()
-    {
-    mSolveObject = false;
-    mCanPlay     = false;
-    doEffectNow( BaseEffect.Type.SOLVE );
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void scrambleObjectNow()
-    {
-    mScrambleObject = false;
-    mCanPlay        = false;
-    mIsSolved       = false;
-    RubikScores.getInstance().incrementNumPlays();
-    doEffectNow( BaseEffect.Type.SCRAMBLE );
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void initializeObjectNow()
-    {
-    mInitializeObject = false;
-    mNewObject.initializeObject(mNextMoves);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void resetAllTextureMapsNow()
-    {
-    mResetAllTextureMaps = false;
-
-    if( mNewObject!=null ) mNewObject.resetAllTextureMaps();
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void setQuatNow()
-    {
-    mSetQuat = false;
-    mView.setQuat();
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-//
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  void setScreenSize(int width)
-    {
-    if( mNewObject!=null )
-      {
-      mNewObject.createTexture();
-      mNewObject.recomputeScaleFactor(width);
-      }
-
-    mScreenWidth  = width;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  void finishRotation(int nearestAngle)
-    {
-    mNearestAngle   = nearestAngle;
-    mFinishRotation = true;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  void changeObject(ObjectList object, int size)
-    {
-    if( size>0 )
-      {
-      mChangeObject = true;
-      mNextObject = object;
-      mNextSize   = size;
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  void setQuatOnNextRender()
-    {
-    mSetQuat = true;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  void preRender()
-    {
-    if( mSetQuat               ) setQuatNow();
-    if( mFinishRotation        ) finishRotationNow();
-    if( mRemoveRotation        ) removeRotationNow();
-    if( mChangeObject          ) changeObjectNow();
-    if( mSetupObject           ) setupObjectNow();
-    if( mSolveObject           ) solveObjectNow();
-    if( mScrambleObject        ) scrambleObjectNow();
-    if( mAddRotation           ) addRotationNow();
-    if( mInitializeObject      ) initializeObjectNow();
-    if( mResetAllTextureMaps   ) resetAllTextureMapsNow();
-    if( mRemovePatternRotation ) removePatternRotationNow();
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// PUBLIC API
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void addRotation(ActionFinishedListener listener, int axis, int rowBitmap, int angle, long duration)
-    {
-    mAddRotation = true;
-
-    mAddActionListener    = listener;
-    mAddRotationAxis      = axis;
-    mAddRotationRowBitmap = rowBitmap;
-    mAddRotationAngle     = angle;
-    mAddRotationDuration  = duration;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void initializeObject(int[][] moves)
-    {
-    mInitializeObject = true;
-    mNextMoves = moves;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public int getNumScrambles()
-    {
-    return mScrambleObjectNum;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void solveObject()
-    {
-    if( mCanPlay )
-      {
-      mSolveObject = true;
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void scrambleObject(int num)
-    {
-    if( mCanPlay )
-      {
-      mScrambleObject = true;
-      mScrambleObjectNum = num;
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void resetAllTextureMaps()
-    {
-    mResetAllTextureMaps = true;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public TwistyObject getObject()
-    {
-    return mNewObject;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public TwistyObject getOldObject()
-    {
-    return null;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void effectFinished(final long effectID)
-    {
-    if( effectID == mRotationFinishedID )
-      {
-      mRotationFinishedID = 0;
-      removeRotation();
-      }
-    else if( effectID == mAddRotationID )
-      {
-      mAddRotationID = 0;
-      mRemoveRotationID = effectID;
-      removePatternRotation();
-      }
-    else
-      {
-      mCanPlay   = true;
-      }
-    }
-  }
diff --git a/src/main/java/org/distorted/tutorial/TutorialRenderer.java b/src/main/java/org/distorted/tutorial/TutorialRenderer.java
deleted file mode 100644
index 2851b872..00000000
--- a/src/main/java/org/distorted/tutorial/TutorialRenderer.java
+++ /dev/null
@@ -1,98 +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.tutorial;
-
-import android.opengl.GLSurfaceView;
-
-import org.distorted.effects.BaseEffect;
-import org.distorted.library.effect.EffectType;
-import org.distorted.library.effect.VertexEffectQuaternion;
-import org.distorted.library.effect.VertexEffectRotate;
-import org.distorted.library.main.DistortedLibrary;
-import org.distorted.library.main.DistortedScreen;
-
-import javax.microedition.khronos.egl.EGLConfig;
-import javax.microedition.khronos.opengles.GL10;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-public class TutorialRenderer implements GLSurfaceView.Renderer, DistortedLibrary.ExceptionListener
-{
-   private final TutorialSurfaceView mView;
-   private final DistortedScreen mScreen;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-   TutorialRenderer(TutorialSurfaceView v)
-     {
-     final float BRIGHTNESS = 0.30f;
-
-     mView = v;
-     mScreen = new DistortedScreen();
-     mScreen.glClearColor(BRIGHTNESS, BRIGHTNESS, BRIGHTNESS, 1.0f);
-     }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-   @Override
-   public void onDrawFrame(GL10 glUnused)
-     {
-     long time = System.currentTimeMillis();
-     mView.getPreRender().preRender();
-     mScreen.render(time);
-     }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-   @Override
-   public void onSurfaceChanged(GL10 glUnused, int width, int height)
-      {
-      mScreen.resize(width,height);
-      mView.setScreenSize(width,height);
-      mView.getPreRender().setScreenSize(width);
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-   @Override
-   public void onSurfaceCreated(GL10 glUnused, EGLConfig config)
-      {
-      DistortedLibrary.setMax(EffectType.VERTEX,61);    // 60 Minx quaternions + rotate
-      VertexEffectRotate.enable();
-      VertexEffectQuaternion.enable();
-      BaseEffect.Type.enableEffects();
-
-      DistortedLibrary.onSurfaceCreated(mView.getContext(),this,1);
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-   public void distortedException(Exception ex)
-     {
-     android.util.Log.e("TUTORIAL", "unexpected exception: "+ex.getMessage() );
-     }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-   DistortedScreen getScreen()
-     {
-     return mScreen;
-     }
-}
diff --git a/src/main/java/org/distorted/tutorial/TutorialState.java b/src/main/java/org/distorted/tutorial/TutorialState.java
deleted file mode 100644
index 189938b9..00000000
--- a/src/main/java/org/distorted/tutorial/TutorialState.java
+++ /dev/null
@@ -1,245 +0,0 @@
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// 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.tutorial;
-
-import android.view.View;
-import android.widget.ImageButton;
-import android.widget.LinearLayout;
-
-import org.distorted.main.R;
-import org.distorted.main.RubikActivity;
-import org.distorted.main.RubikPreRender;
-import org.distorted.objects.ObjectList;
-import org.distorted.objects.TwistyObject;
-import org.distorted.states.RubikStatePlay;
-import org.distorted.states.StateList;
-import org.distorted.states.TransparentImageButton;
-
-import java.util.ArrayList;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-public class TutorialState implements RubikPreRender.ActionFinishedListener
-{
-  private static final int DURATION_MILLIS = 750;
-
-  private ImageButton mPrevButton, mLockButton, mSolveButton, mScrambleButton, mBackButton;
-
-  private boolean mCanPrevMove;
-
-  private static class Move
-    {
-    private int mAxis, mRow, mAngle;
-
-    Move(int axis, int row, int angle)
-      {
-      mAxis = axis;
-      mRow  = row;
-      mAngle= angle;
-      }
-    }
-
-  ArrayList<Move> mMoves;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void backMove(TutorialPreRender pre)
-    {
-    if( mCanPrevMove )
-      {
-      int numMoves = mMoves.size();
-
-      if( numMoves>0 )
-        {
-        Move move = mMoves.remove(numMoves-1);
-        TwistyObject object = pre.getObject();
-
-        int axis  = move.mAxis;
-        int row   = (1<<move.mRow);
-        int angle = move.mAngle;
-        int numRot= Math.abs(angle*object.getBasicAngle()/360);
-
-        if( angle!=0 )
-          {
-          mCanPrevMove = false;
-          pre.addRotation(this, axis, row, -angle, numRot*DURATION_MILLIS);
-          }
-        else
-          {
-          android.util.Log.e("solution", "error: trying to back move of angle 0");
-          }
-        }
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void toggleLock(TutorialActivity act)
-    {
-    act.toggleLock();
-    mLockButton.setImageResource(getLockIcon(act));
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private int getLockIcon(TutorialActivity act)
-    {
-    if( act.retLocked() )
-      {
-      return RubikActivity.getDrawable(R.drawable.ui_small_locked,R.drawable.ui_medium_locked, R.drawable.ui_big_locked, R.drawable.ui_huge_locked);
-      }
-    else
-      {
-      return RubikActivity.getDrawable(R.drawable.ui_small_unlocked,R.drawable.ui_medium_unlocked, R.drawable.ui_big_unlocked, R.drawable.ui_huge_unlocked);
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void setupSolveButton(final TutorialActivity act, final float width)
-    {
-    int icon = RubikActivity.getDrawable(R.drawable.ui_small_cube_solve,R.drawable.ui_medium_cube_solve, R.drawable.ui_big_cube_solve, R.drawable.ui_huge_cube_solve);
-    mSolveButton = new TransparentImageButton(act, icon, width, LinearLayout.LayoutParams.MATCH_PARENT);
-
-    mSolveButton.setOnClickListener( new View.OnClickListener()
-      {
-      @Override
-      public void onClick(View v)
-        {
-        act.getPreRender().solveObject();
-        mMoves.clear();
-        }
-      });
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void setupLockButton(final TutorialActivity act, final float width)
-    {
-    final int icon = getLockIcon(act);
-    mLockButton = new TransparentImageButton(act, icon, width,LinearLayout.LayoutParams.MATCH_PARENT);
-
-    mLockButton.setOnClickListener( new View.OnClickListener()
-      {
-      @Override
-      public void onClick(View v)
-        {
-        toggleLock(act);
-        }
-      });
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void setupPrevButton(final TutorialActivity act, final float width)
-    {
-    int icon = RubikActivity.getDrawable(R.drawable.ui_small_cube_back,R.drawable.ui_medium_cube_back, R.drawable.ui_big_cube_back, R.drawable.ui_huge_cube_back);
-    mPrevButton = new TransparentImageButton(act, icon, width,LinearLayout.LayoutParams.MATCH_PARENT);
-
-    mPrevButton.setOnClickListener( new View.OnClickListener()
-      {
-      @Override
-      public void onClick(View v)
-        {
-        TutorialPreRender pre = act.getPreRender();
-        backMove(pre);
-        }
-      });
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void setupScrambleButton(final TutorialActivity act, final float width)
-    {
-    int icon = RubikActivity.getDrawable(R.drawable.ui_small_cube_scramble,R.drawable.ui_medium_cube_scramble, R.drawable.ui_big_cube_scramble, R.drawable.ui_huge_cube_scramble);
-    mScrambleButton = new TransparentImageButton(act, icon, width,LinearLayout.LayoutParams.MATCH_PARENT);
-
-    mScrambleButton.setOnClickListener( new View.OnClickListener()
-      {
-      @Override
-      public void onClick(View v)
-        {
-        RubikStatePlay play = (RubikStatePlay) StateList.PLAY.getStateClass();
-        int size = play.getSize();
-        int object= play.getObject();
-        int sizeIndex = ObjectList.getSizeIndex(object,size);
-        int maxLevel = ObjectList.getMaxLevel(object, sizeIndex);
-
-        act.getPreRender().scrambleObject(maxLevel);
-        }
-      });
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void setupBackButton(final TutorialActivity act, final float width)
-    {
-    int icon = RubikActivity.getDrawable(R.drawable.ui_small_smallback,R.drawable.ui_medium_smallback, R.drawable.ui_big_smallback, R.drawable.ui_huge_smallback);
-    mBackButton = new TransparentImageButton(act, icon, width, LinearLayout.LayoutParams.MATCH_PARENT);
-
-    mBackButton.setOnClickListener( new View.OnClickListener()
-      {
-      @Override
-      public void onClick(View v)
-        {
-        act.finish();
-        }
-      });
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  void createRightPane(final TutorialActivity act, float width)
-    {
-    mCanPrevMove = true;
-
-    if( mMoves==null ) mMoves = new ArrayList<>();
-    else               mMoves.clear();
-
-    LinearLayout layout = act.findViewById(R.id.tutorialRightBar);
-    layout.removeAllViews();
-
-    setupPrevButton(act,width);
-    setupLockButton(act,width);
-    setupSolveButton(act,width);
-    setupScrambleButton(act,width);
-    setupBackButton(act,width);
-
-    layout.addView(mSolveButton);
-    layout.addView(mPrevButton);
-    layout.addView(mScrambleButton);
-    layout.addView(mLockButton);
-    layout.addView(mBackButton);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void addMove(int axis, int row, int angle)
-    {
-    mMoves.add(new Move(axis,row,angle));
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void onActionFinished(final long effectID)
-    {
-    mCanPrevMove = true;
-    }
-}
diff --git a/src/main/java/org/distorted/tutorial/TutorialSurfaceView.java b/src/main/java/org/distorted/tutorial/TutorialSurfaceView.java
deleted file mode 100644
index 178a675f..00000000
--- a/src/main/java/org/distorted/tutorial/TutorialSurfaceView.java
+++ /dev/null
@@ -1,703 +0,0 @@
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// 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.tutorial;
-
-import android.app.ActivityManager;
-import android.content.Context;
-import android.content.pm.ConfigurationInfo;
-import android.opengl.GLES30;
-import android.opengl.GLSurfaceView;
-import android.util.AttributeSet;
-import android.util.DisplayMetrics;
-import android.view.MotionEvent;
-
-import com.google.firebase.crashlytics.FirebaseCrashlytics;
-
-import org.distorted.library.type.Static2D;
-import org.distorted.library.type.Static4D;
-import org.distorted.objects.Movement;
-import org.distorted.objects.TwistyObject;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-public class TutorialSurfaceView extends GLSurfaceView
-{
-    private static final int NUM_SPEED_PROBES = 10;
-    private static final int INVALID_POINTER_ID = -1;
-
-    // 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 0.3 of an inch will start a Rotation.
-    private final static float ROTATION_SENSITIVITY = 0.3f;
-
-    private final Static4D CAMERA_POINT = new Static4D(0, 0, 1, 0);
-
-    private TutorialRenderer mRenderer;
-    private TutorialPreRender mPreRender;
-    private Movement mMovement;
-    private boolean mDragging, mBeginningRotation, mContinuingRotation;
-    private int mScreenWidth, mScreenHeight, mScreenMin;
-
-    private float mRotAngle, mInitDistance;
-    private int mPtrID1, mPtrID2;
-    private float mX, mY;
-    private float mStartRotX, mStartRotY;
-    private float mAxisX, mAxisY;
-    private float mRotationFactor;
-    private int mCurrentAxis, mCurrentRow;
-    private float mCurrentAngle, mCurrRotSpeed;
-    private float[] mLastX;
-    private float[] mLastY;
-    private long[] mLastT;
-    private int mFirstIndex, mLastIndex;
-    private int mDensity;
-
-    private static Static4D mQuat= new Static4D(-0.25189602f,0.3546389f,0.009657208f,0.90038127f);
-    private static Static4D mTemp= new Static4D(0,0,0,1);
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    void setScreenSize(int width, int height)
-      {
-      mScreenWidth = width;
-      mScreenHeight= height;
-
-      mScreenMin = Math.min(width, height);
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    boolean isVertical()
-      {
-      return mScreenHeight>mScreenWidth;
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    TutorialRenderer getRenderer()
-      {
-      return mRenderer;
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    TutorialPreRender getPreRender()
-      {
-      return mPreRender;
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    void setQuat()
-      {
-      mQuat.set(mTemp);
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    Static4D getQuat()
-      {
-      return mQuat;
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    void setMovement(Movement movement)
-      {
-      mMovement = movement;
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    private Static4D quatFromDrag(float dragX, float dragY)
-      {
-      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;
-
-        float ratio = axisL;
-        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);
-        }
-
-      return new Static4D(0f, 0f, 0f, 1f);
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// cast the 3D axis we are currently rotating along (which is already casted to the surface of the
-// currently touched face AND converted into a 4D vector - fourth 0) to a 2D in-screen-surface axis
-
-    private void computeCurrentAxis(Static4D axis)
-      {
-      Static4D result = rotateVectorByQuat(axis, mQuat);
-
-      mAxisX =result.get0();
-      mAxisY =result.get1();
-
-      float len = (float)Math.sqrt(mAxisX*mAxisX + mAxisY*mAxisY);
-      mAxisX /= len;
-      mAxisY /= len;
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// return quat1*quat2
-
-    public static Static4D quatMultiply( Static4D quat1, Static4D quat2 )
-      {
-      float qx = quat1.get0();
-      float qy = quat1.get1();
-      float qz = quat1.get2();
-      float qw = quat1.get3();
-
-      float rx = quat2.get0();
-      float ry = quat2.get1();
-      float rz = quat2.get2();
-      float rw = quat2.get3();
-
-      float tx = rw*qx - rz*qy + ry*qz + rx*qw;
-      float ty = rw*qy + rz*qx + ry*qw - rx*qz;
-      float tz = rw*qz + rz*qw - ry*qx + rx*qy;
-      float tw = rw*qw - rz*qz - ry*qy - rx*qx;
-
-      return new Static4D(tx,ty,tz,tw);
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// rotate 'vector' by quat  ( i.e. return quat*vector*(quat^-1) )
-
-    public static Static4D rotateVectorByQuat(Static4D vector, Static4D quat)
-      {
-      float qx = quat.get0();
-      float qy = quat.get1();
-      float qz = quat.get2();
-      float qw = quat.get3();
-
-      Static4D quatInverted= new Static4D(-qx,-qy,-qz,qw);
-      Static4D tmp = quatMultiply(quat,vector);
-
-      return quatMultiply(tmp,quatInverted);
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// rotate 'vector' by quat^(-1)  ( i.e. return (quat^-1)*vector*quat )
-
-    public static Static4D rotateVectorByInvertedQuat(Static4D vector, Static4D quat)
-      {
-      float qx = quat.get0();
-      float qy = quat.get1();
-      float qz = quat.get2();
-      float qw = quat.get3();
-
-      Static4D quatInverted= new Static4D(-qx,-qy,-qz,qw);
-      Static4D tmp = quatMultiply(quatInverted,vector);
-
-      return quatMultiply(tmp,quat);
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    private void addSpeedProbe(float x, float y)
-      {
-      long currTime = System.currentTimeMillis();
-      boolean theSame = mLastIndex==mFirstIndex;
-
-      mLastIndex++;
-      if( mLastIndex>=NUM_SPEED_PROBES ) mLastIndex=0;
-
-      mLastT[mLastIndex] = currTime;
-      mLastX[mLastIndex] = x;
-      mLastY[mLastIndex] = y;
-
-      if( mLastIndex==mFirstIndex)
-        {
-        mFirstIndex++;
-        if( mFirstIndex>=NUM_SPEED_PROBES ) mFirstIndex=0;
-        }
-
-      if( theSame )
-        {
-        mLastT[mFirstIndex] = currTime;
-        mLastX[mFirstIndex] = x;
-        mLastY[mFirstIndex] = y;
-        }
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    private void computeCurrentSpeedInInchesPerSecond()
-      {
-      long firstTime = mLastT[mFirstIndex];
-      long lastTime  = mLastT[mLastIndex];
-      float fX = mLastX[mFirstIndex];
-      float fY = mLastY[mFirstIndex];
-      float lX = mLastX[mLastIndex];
-      float lY = mLastY[mLastIndex];
-
-      long timeDiff = lastTime-firstTime;
-
-      mLastIndex = 0;
-      mFirstIndex= 0;
-
-      mCurrRotSpeed = timeDiff>0 ? 1000*retFingerDragDistanceInInches(fX,fY,lX,lY)/timeDiff : 0;
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    private float retFingerDragDistanceInInches(float xFrom, float yFrom, float xTo, float yTo)
-      {
-      float xDist = mScreenWidth*(xFrom-xTo);
-      float yDist = mScreenHeight*(yFrom-yTo);
-      float distInPixels = (float)Math.sqrt(xDist*xDist + yDist*yDist);
-
-      return distInPixels/mDensity;
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    private void setUpDragOrRotate(float x, float y)
-      {
-        Static4D touchPoint = new Static4D(x, y, 0, 0);
-        Static4D rotatedTouchPoint= rotateVectorByInvertedQuat(touchPoint, mQuat);
-        Static4D rotatedCamera= rotateVectorByInvertedQuat(CAMERA_POINT, mQuat);
-
-        if( mMovement!=null && mMovement.faceTouched(rotatedTouchPoint,rotatedCamera) )
-          {
-          mDragging           = false;
-          mContinuingRotation = false;
-          mBeginningRotation  = true;
-          }
-        else
-          {
-          final TutorialActivity act = (TutorialActivity)getContext();
-          mDragging           = !act.isLocked();
-          mContinuingRotation = false;
-          mBeginningRotation  = false;
-          }
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    private void drag(MotionEvent event, float x, float y)
-      {
-      if( mPtrID1!=INVALID_POINTER_ID && mPtrID2!=INVALID_POINTER_ID)
-        {
-        int pointer = event.findPointerIndex(mPtrID2);
-        float pX,pY;
-
-        try
-          {
-          pX = event.getX(pointer);
-          pY = event.getY(pointer);
-          }
-        catch(IllegalArgumentException ex)
-          {
-          mPtrID1=INVALID_POINTER_ID;
-          mPtrID2=INVALID_POINTER_ID;
-
-          FirebaseCrashlytics crashlytics = FirebaseCrashlytics.getInstance();
-          crashlytics.setCustomKey("DragError", "pointer="+pointer );
-          crashlytics.recordException(ex);
-
-          return;
-          }
-
-        float x2 = (pX - mScreenWidth*0.5f)/mScreenMin;
-        float y2 = (mScreenHeight*0.5f -pY)/mScreenMin;
-
-        float angleNow = getAngle(x,y,x2,y2);
-        float angleDiff = angleNow-mRotAngle;
-        float sinA =-(float)Math.sin(angleDiff);
-        float cosA = (float)Math.cos(angleDiff);
-
-        Static4D dragQuat = quatMultiply(new Static4D(0,0,sinA,cosA), mQuat);
-        mTemp.set(dragQuat);
-
-        mRotAngle = angleNow;
-
-        float distNow  = (float)Math.sqrt( (x-x2)*(x-x2) + (y-y2)*(y-y2) );
-        float distQuot = mInitDistance<0 ? 1.0f : distNow/ mInitDistance;
-        mInitDistance = distNow;
-
-        TwistyObject object = mPreRender.getObject();
-        if( object!=null ) object.setObjectRatio(distQuot);
-        }
-      else
-        {
-        Static4D dragQuat = quatMultiply(quatFromDrag(mX-x,y-mY), mQuat);
-        mTemp.set(dragQuat);
-        }
-
-      mPreRender.setQuatOnNextRender();
-      mX = x;
-      mY = y;
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    private void finishRotation()
-      {
-      computeCurrentSpeedInInchesPerSecond();
-      int angle = mPreRender.getObject().computeNearestAngle(mCurrentAngle, mCurrRotSpeed);
-      mPreRender.finishRotation(angle);
-
-      if( angle!=0 )
-        {
-        final TutorialActivity act = (TutorialActivity)getContext();
-        TutorialState state = act.getState();
-        state.addMove(mCurrentAxis, mCurrentRow, angle);
-        }
-
-      mContinuingRotation = false;
-      mBeginningRotation  = false;
-      mDragging           = true;
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    private void continueRotation(float x, float y)
-      {
-      float dx = x-mStartRotX;
-      float dy = y-mStartRotY;
-      float alpha = dx*mAxisX + dy*mAxisY;
-      float x2 = dx - alpha*mAxisX;
-      float y2 = dy - alpha*mAxisY;
-
-      float len = (float)Math.sqrt(x2*x2 + y2*y2);
-
-      // we have the length of 1D vector 'angle', now the direction:
-      float tmp = mAxisY==0 ? -mAxisX*y2 : mAxisY*x2;
-
-      float angle = (tmp>0 ? 1:-1)*len*mRotationFactor;
-      mCurrentAngle = SWIPING_SENSITIVITY*angle;
-      mPreRender.getObject().continueRotation(mCurrentAngle);
-
-      addSpeedProbe(x2,y2);
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    private void beginRotation(float x, float y)
-      {
-      mStartRotX = x;
-      mStartRotY = y;
-
-      TwistyObject object = mPreRender.getObject();
-      int numLayers = object.getNumLayers();
-
-      Static4D touchPoint2 = new Static4D(x, y, 0, 0);
-      Static4D rotatedTouchPoint2= rotateVectorByInvertedQuat(touchPoint2, mQuat);
-      Static2D res = mMovement.newRotation(numLayers,rotatedTouchPoint2);
-
-      mCurrentAxis = (int)res.get0();
-      mCurrentRow  = (int)res.get1();
-
-      computeCurrentAxis( mMovement.getCastedRotAxis(mCurrentAxis) );
-      mRotationFactor = mMovement.returnRotationFactor(numLayers,mCurrentRow);
-
-      object.beginNewRotation( mCurrentAxis, mCurrentRow );
-
-      addSpeedProbe(x,y);
-
-      mBeginningRotation = false;
-      mContinuingRotation= true;
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    private float getAngle(float x1, float y1, float x2, float y2)
-      {
-      return (float) Math.atan2(y1-y2, x1-x2);
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    private void actionMove(MotionEvent event)
-      {
-      int pointer = event.findPointerIndex(mPtrID1 != INVALID_POINTER_ID ? mPtrID1:mPtrID2);
-
-      if( pointer<0 ) return;
-
-      float pX = event.getX(pointer);
-      float pY = event.getY(pointer);
-
-      float x = (pX - mScreenWidth*0.5f)/mScreenMin;
-      float y = (mScreenHeight*0.5f -pY)/mScreenMin;
-
-      if( mBeginningRotation )
-        {
-        if( retFingerDragDistanceInInches(mX,mY,x,y) > ROTATION_SENSITIVITY )
-          {
-          beginRotation(x,y);
-          }
-        }
-      else if( mContinuingRotation )
-        {
-        continueRotation(x,y);
-        }
-      else if( mDragging )
-        {
-        drag(event,x,y);
-        }
-      else
-        {
-        setUpDragOrRotate(x,y);
-        }
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    private void actionDown(MotionEvent event)
-      {
-      mPtrID1 = event.getPointerId(0);
-
-      float x = event.getX();
-      float y = event.getY();
-
-      mX = (x - mScreenWidth*0.5f)/mScreenMin;
-      mY = (mScreenHeight*0.5f -y)/mScreenMin;
-
-      setUpDragOrRotate(mX,mY);
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    private void actionUp(MotionEvent event)
-      {
-      mPtrID1 = INVALID_POINTER_ID;
-      mPtrID2 = INVALID_POINTER_ID;
-
-      if( mContinuingRotation )
-        {
-        finishRotation();
-        }
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    private void actionDown2(MotionEvent event)
-      {
-      int index = event.getActionIndex();
-
-      if( mPtrID1==INVALID_POINTER_ID )
-        {
-        mPtrID1 = event.getPointerId(index);
-        float x = event.getX();
-        float y = event.getY();
-
-        if( mPtrID2 != INVALID_POINTER_ID )
-          {
-          int pointer = event.findPointerIndex(mPtrID2);
-
-          try
-            {
-            float x2 = event.getX(pointer);
-            float y2 = event.getY(pointer);
-
-            mRotAngle = getAngle(x,-y,x2,-y2);
-            mInitDistance = -1;
-            }
-          catch(IllegalArgumentException ex)
-            {
-            mPtrID1=INVALID_POINTER_ID;
-            mPtrID2=INVALID_POINTER_ID;
-
-            FirebaseCrashlytics crashlytics = FirebaseCrashlytics.getInstance();
-            crashlytics.setCustomKey("DragError", "pointer="+pointer );
-            crashlytics.recordException(ex);
-
-            return;
-            }
-          }
-
-        mX = (x - mScreenWidth*0.5f)/mScreenMin;
-        mY = (mScreenHeight*0.5f -y)/mScreenMin;
-        }
-      else if( mPtrID2==INVALID_POINTER_ID )
-        {
-        mPtrID2 = event.getPointerId(index);
-
-        float x = event.getX();
-        float y = event.getY();
-
-        if( mPtrID2 != INVALID_POINTER_ID )
-          {
-          int pointer = event.findPointerIndex(mPtrID2);
-
-          try
-            {
-            float x2 = event.getX(pointer);
-            float y2 = event.getY(pointer);
-
-            mRotAngle = getAngle(x,-y,x2,-y2);
-            mInitDistance = -1;
-            }
-          catch(IllegalArgumentException ex)
-            {
-            mPtrID1=INVALID_POINTER_ID;
-            mPtrID2=INVALID_POINTER_ID;
-
-            FirebaseCrashlytics crashlytics = FirebaseCrashlytics.getInstance();
-            crashlytics.setCustomKey("DragError", "pointer="+pointer );
-            crashlytics.recordException(ex);
-
-            return;
-            }
-          }
-
-        if( mBeginningRotation || mContinuingRotation )
-          {
-          mX = (x - mScreenWidth*0.5f)/mScreenMin;
-          mY = (mScreenHeight*0.5f -y)/mScreenMin;
-          }
-        }
-
-      if( mBeginningRotation )
-        {
-        mContinuingRotation = false;
-        mBeginningRotation  = false;
-        mDragging           = true;
-        }
-      else if( mContinuingRotation )
-        {
-        finishRotation();
-        }
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    private void actionUp2(MotionEvent event)
-      {
-      int index = event.getActionIndex();
-
-      if( index==event.findPointerIndex(mPtrID1) )
-        {
-        mPtrID1 = INVALID_POINTER_ID;
-        int pointer = event.findPointerIndex(mPtrID2);
-
-        if( pointer>=0 )
-          {
-          float x1 = event.getX(pointer);
-          float y1 = event.getY(pointer);
-
-          mX = (x1 - mScreenWidth*0.5f)/mScreenMin;
-          mY = (mScreenHeight*0.5f -y1)/mScreenMin;
-          }
-        }
-      else if( index==event.findPointerIndex(mPtrID2) )
-        {
-        mPtrID2 = INVALID_POINTER_ID;
-        }
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    void initialize()
-      {
-      mPtrID1 = INVALID_POINTER_ID;
-      mPtrID2 = INVALID_POINTER_ID;
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// PUBLIC API
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    public TutorialSurfaceView(Context context, AttributeSet attrs)
-      {
-      super(context,attrs);
-
-      if(!isInEditMode())
-        {
-        mCurrRotSpeed= 0.0f;
-
-        mLastX = new float[NUM_SPEED_PROBES];
-        mLastY = new float[NUM_SPEED_PROBES];
-        mLastT = new long[NUM_SPEED_PROBES];
-        mFirstIndex =0;
-        mLastIndex  =0;
-
-        mRenderer  = new TutorialRenderer(this);
-        mPreRender = new TutorialPreRender(this);
-
-        TutorialActivity act = (TutorialActivity)context;
-        DisplayMetrics dm = new DisplayMetrics();
-        act.getWindowManager().getDefaultDisplay().getMetrics(dm);
-
-        mDensity = dm.densityDpi;
-
-        final ActivityManager activityManager= (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
-
-        try
-          {
-          final ConfigurationInfo configurationInfo = activityManager.getDeviceConfigurationInfo();
-          int esVersion = configurationInfo.reqGlEsVersion>>16;
-          setEGLContextClientVersion(esVersion);
-          setRenderer(mRenderer);
-          }
-        catch(Exception ex)
-          {
-          act.OpenGLError();
-
-          String shading = GLES30.glGetString(GLES30.GL_SHADING_LANGUAGE_VERSION);
-          String version = GLES30.glGetString(GLES30.GL_VERSION);
-          String vendor  = GLES30.glGetString(GLES30.GL_VENDOR);
-          String renderer= GLES30.glGetString(GLES30.GL_RENDERER);
-
-          FirebaseCrashlytics crashlytics = FirebaseCrashlytics.getInstance();
-          crashlytics.setCustomKey("GLSL Version"  , shading );
-          crashlytics.setCustomKey("GLversion"     , version );
-          crashlytics.setCustomKey("GL Vendor "    , vendor  );
-          crashlytics.setCustomKey("GLSLrenderer"  , renderer);
-          crashlytics.recordException(ex);
-          }
-        }
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    @Override
-    public boolean onTouchEvent(MotionEvent event)
-      {
-      int action = event.getActionMasked();
-
-      switch(action)
-         {
-         case MotionEvent.ACTION_DOWN        : actionDown(event) ; break;
-         case MotionEvent.ACTION_MOVE        : actionMove(event) ; break;
-         case MotionEvent.ACTION_UP          : actionUp(event)   ; break;
-         case MotionEvent.ACTION_POINTER_DOWN: actionDown2(event); break;
-         case MotionEvent.ACTION_POINTER_UP  : actionUp2(event)  ; break;
-         }
-
-      return true;
-      }
-}
-
diff --git a/src/main/java/org/distorted/tutorial/TutorialWebView.java b/src/main/java/org/distorted/tutorial/TutorialWebView.java
deleted file mode 100644
index e1b4ed50..00000000
--- a/src/main/java/org/distorted/tutorial/TutorialWebView.java
+++ /dev/null
@@ -1,97 +0,0 @@
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// 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.tutorial;
-
-import android.annotation.SuppressLint;
-import android.content.Context;
-import android.content.res.Resources;
-import android.webkit.WebView;
-import android.webkit.WebViewClient;
-
-import org.distorted.main.R;
-
-import java.io.BufferedReader;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-public class TutorialWebView
-{
-    private String  mUrl;
-    private Context mContext;
-    private WebView mWebView;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    @SuppressLint("SetJavaScriptEnabled")
-    public TutorialWebView(Context context, WebView webview)
-      {
-      mWebView = webview;
-      mContext = context;
-      mWebView.setBackgroundColor(0);
-      mWebView.getSettings().setJavaScriptEnabled(true);
-
-      mWebView.setWebViewClient(new WebViewClient()
-        {
-        @Override
-        public boolean shouldOverrideUrlLoading(WebView view, String url)
-          {
-          return false;
-          }
-        });
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    public void load(String url)
-      {
-      mUrl = url;
-
-      String data1 = "<html><body><iframe width=\"100%\" height=\"100%\" src=\"";
-      String data2 = "\" frameborder=\"0\" allowfullscreen></iframe></body></html>";
-
-      mWebView.loadData(data1+url+data2, "text/html", "UTF-8");
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    public void onPause()
-      {
-      mWebView.onPause();
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    public void onResume()
-      {
-      mWebView.onResume();
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    public void reload()
-      {
-      if (mUrl!=null)
-        {
-        load(mUrl);
-        }
-      }
-}
diff --git a/src/main/java/org/distorted/tutorials/TutorialActivity.java b/src/main/java/org/distorted/tutorials/TutorialActivity.java
new file mode 100644
index 00000000..f4011f95
--- /dev/null
+++ b/src/main/java/org/distorted/tutorials/TutorialActivity.java
@@ -0,0 +1,358 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// 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.tutorials;
+
+import android.os.Build;
+import android.os.Bundle;
+import android.util.DisplayMetrics;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+import android.webkit.WebView;
+import android.widget.LinearLayout;
+
+import androidx.appcompat.app.AppCompatActivity;
+
+import com.google.firebase.analytics.FirebaseAnalytics;
+
+import org.distorted.dialogs.RubikDialogError;
+import org.distorted.library.main.DistortedLibrary;
+import org.distorted.main.R;
+import org.distorted.objects.ObjectList;
+import org.distorted.objects.TwistyObject;
+import org.distorted.states.StateList;
+
+import static org.distorted.main.RubikRenderer.BRIGHTNESS;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+public class TutorialActivity extends AppCompatActivity
+{
+    private static final String URL = "https://www.youtube.com/embed/";
+
+    public static final float DIALOG_BUTTON_SIZE  = 0.06f;
+    public static final float MENU_BIG_TEXT_SIZE  = 0.05f;
+
+    public static final int FLAGS =  View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
+                                   | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
+                                   | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
+                                   | View.SYSTEM_UI_FLAG_FULLSCREEN
+                                   | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
+
+    public static final int FLAGS2=  View.SYSTEM_UI_FLAG_LAYOUT_STABLE
+                                   | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
+
+    private boolean mIsLocked;
+    private FirebaseAnalytics mFirebaseAnalytics;
+    private static int mScreenWidth, mScreenHeight;
+    private int mCurrentApiVersion;
+    private TutorialState mState;
+    private String mURL;
+    private int mObjectOrdinal, mObjectSize;
+    private TutorialWebView mWebView;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    @Override
+    protected void onCreate(Bundle savedState)
+      {
+      super.onCreate(savedState);
+      DistortedLibrary.onCreate(1);
+      setTheme(R.style.CustomActivityThemeNoActionBar);
+      setContentView(R.layout.tutorial);
+
+      Bundle b = getIntent().getExtras();
+
+      if(b != null)
+        {
+        mURL           = b.getString("url");
+        mObjectOrdinal = b.getInt("obj");
+        mObjectSize    = b.getInt("siz");
+        }
+
+      mIsLocked = false;
+      mFirebaseAnalytics = FirebaseAnalytics.getInstance(this);
+
+      DisplayMetrics displaymetrics = new DisplayMetrics();
+      getWindowManager().getDefaultDisplay().getMetrics(displaymetrics);
+      mScreenWidth =displaymetrics.widthPixels;
+      mScreenHeight=displaymetrics.heightPixels;
+
+      hideNavigationBar();
+      cutoutHack();
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    private void hideNavigationBar()
+      {
+      mCurrentApiVersion = Build.VERSION.SDK_INT;
+
+      if(mCurrentApiVersion >= Build.VERSION_CODES.KITKAT)
+        {
+        final View decorView = getWindow().getDecorView();
+
+        decorView.setSystemUiVisibility(FLAGS);
+
+        decorView.setOnSystemUiVisibilityChangeListener(new View.OnSystemUiVisibilityChangeListener()
+          {
+          @Override
+          public void onSystemUiVisibilityChange(int visibility)
+            {
+            if((visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0)
+              {
+              decorView.setSystemUiVisibility(FLAGS);
+              }
+            }
+          });
+        }
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    @Override
+    public void onAttachedToWindow()
+      {
+      super.onAttachedToWindow();
+
+      final float RATIO = 0.15f;
+      float width = getScreenWidthInPixels();
+
+      TutorialSurfaceView viewL = findViewById(R.id.tutorialSurfaceView);
+      ViewGroup.LayoutParams paramsL = viewL.getLayoutParams();
+      paramsL.width = (int)(width*(1.0f-RATIO));
+      viewL.setLayoutParams(paramsL);
+
+      LinearLayout viewR = findViewById(R.id.tutorialRightBar);
+      ViewGroup.LayoutParams paramsR = viewR.getLayoutParams();
+      paramsR.width = (int)(width*RATIO);
+      viewR.setLayoutParams(paramsR);
+
+      final int color = (int)(BRIGHTNESS*255);
+      viewR.setBackgroundColor( (0xFF<<24)+(color<<16)+(color<<8)+color);
+
+      if( mState==null ) mState = new TutorialState();
+
+      mState.createRightPane(this,width);
+
+      WebView videoView = findViewById(R.id.tutorialVideoView);
+      mWebView = new TutorialWebView(this,videoView);
+      mWebView.load(URL+mURL);
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// do not avoid cutouts
+
+    private void cutoutHack()
+      {
+      if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P)
+        {
+        getWindow().getAttributes().layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
+        }
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    @Override
+    public void onWindowFocusChanged(boolean hasFocus)
+      {
+      super.onWindowFocusChanged(hasFocus);
+
+      if(mCurrentApiVersion >= Build.VERSION_CODES.KITKAT && hasFocus)
+        {
+        getWindow().getDecorView().setSystemUiVisibility(FLAGS);
+        }
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+    
+    @Override
+    protected void onPause() 
+      {
+      super.onPause();
+      TutorialSurfaceView view = findViewById(R.id.tutorialSurfaceView);
+      view.onPause();
+
+      if( mWebView!=null ) mWebView.onPause();
+
+      DistortedLibrary.onPause(1);
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+    
+    @Override
+    protected void onResume() 
+      {
+      super.onResume();
+      DistortedLibrary.onResume(1);
+      TutorialSurfaceView view = findViewById(R.id.tutorialSurfaceView);
+      view.onResume();
+      view.initialize();
+
+      if( mWebView!=null ) mWebView.onResume();
+
+      if( mObjectOrdinal>=0 && mObjectOrdinal< ObjectList.NUM_OBJECTS )
+        {
+        ObjectList obj = ObjectList.getObject(mObjectOrdinal);
+        int[] sizes = obj.getSizes();
+        int sizeIndex = ObjectList.getSizeIndex(mObjectOrdinal,mObjectSize);
+
+        if( sizeIndex>=0 && sizeIndex<sizes.length )
+          {
+          view.getPreRender().changeObject(obj,mObjectSize);
+          }
+        }
+      }
+    
+///////////////////////////////////////////////////////////////////////////////////////////////////
+    
+    @Override
+    protected void onDestroy() 
+      {
+      super.onDestroy();
+      DistortedLibrary.onDestroy(1);
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    void OpenGLError()
+      {
+      RubikDialogError errDiag = new RubikDialogError();
+      errDiag.show(getSupportFragmentManager(), null);
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    TutorialState getState()
+      {
+      return mState;
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// PUBLIC API
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public FirebaseAnalytics getAnalytics()
+      {
+      return mFirebaseAnalytics;
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public TwistyObject getObject()
+      {
+      TutorialSurfaceView view = findViewById(R.id.rubikSurfaceView);
+      TutorialPreRender pre = view.getPreRender();
+      return pre.getObject();
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public int getScreenWidthInPixels()
+      {
+      return mScreenWidth;
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public int getScreenHeightInPixels()
+      {
+      return mScreenHeight;
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public TutorialPreRender getPreRender()
+      {
+      TutorialSurfaceView view = findViewById(R.id.tutorialSurfaceView);
+      return view.getPreRender();
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public static int getDrawableSize()
+      {
+      if( mScreenHeight<1000 )
+        {
+        return 0;
+        }
+      if( mScreenHeight<1600 )
+        {
+        return 1;
+        }
+      if( mScreenHeight<1900 )
+        {
+        return 2;
+        }
+
+      return 3;
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public static int getDrawable(int small, int medium, int big, int huge)
+      {
+      int size = getDrawableSize();
+
+      switch(size)
+        {
+        case 0 : return small;
+        case 1 : return medium;
+        case 2 : return big;
+        default: return huge;
+        }
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public boolean isVertical()
+      {
+      TutorialSurfaceView view = findViewById(R.id.tutorialSurfaceView);
+      return view.isVertical();
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public void toggleLock()
+      {
+      mIsLocked = !mIsLocked;
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public boolean isLocked()
+      {
+      StateList state = StateList.getCurrentState();
+
+      if( state== StateList.PLAY || state== StateList.READ || state== StateList.SOLV )
+        {
+        return mIsLocked;
+        }
+
+      return false;
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public boolean retLocked()
+      {
+      return mIsLocked;
+      }
+}
diff --git a/src/main/java/org/distorted/tutorials/TutorialList.java b/src/main/java/org/distorted/tutorials/TutorialList.java
new file mode 100644
index 00000000..4ce6a597
--- /dev/null
+++ b/src/main/java/org/distorted/tutorials/TutorialList.java
@@ -0,0 +1,429 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// 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.tutorials;
+
+import org.distorted.objects.ObjectList;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+public enum TutorialList
+{
+  CUBE2 ( ObjectList.CUBE, 2,
+          new String[][] {
+                          {"gb","rJlh5p2wAKA","How to Solve a 2x2 Rubik's Cube","Z3"},
+                          {"es","f85wqJTIDlw","Resolver cubo de Rubik 2X2","Cuby"},
+                          {"ru","azC6P3VYFkc","Как собрать кубик 2Х2","Е Бондаренко"},
+                          {"fr","V1XS993AUuw","Résoudre le cube 2x2","Rachma Nikov"},
+                          {"de","d8tKa8SRkXw","2x2 Zauberwürfel lösen","Pezcraft"},
+                          {"pl","haNWdAYWGsY","Jak ułożyć kostkę Rubika 2x2","DżoDżo"},
+                          {"kr","wTMsdWKq6No","2x2 큐브 공식을 이해하는 해법","듀나메스 큐브 해법연구소"},
+                     //   {"tw","CfOCXxhCb8U","2x2魔術方塊復原","1hrBLD"},
+                         }
+        ),
+
+  CUBE3 ( ObjectList.CUBE, 3,
+          new String[][] {
+                          {"gb","-8ohoCKN0Zw","How to Solve a Rubik's Cube","Z3"},
+                          {"es","GyY0OxDk5lI","Resolver cubo de Rubik 3x3","Cuby"},
+                          {"ru","5S2eq81FRzI","Как собрать кубик рубика","Е Бондаренко"},
+                          {"fr","T-ASx2wbHVY","Comment résoudre un Rubik's Cube","Le Cube"},
+                          {"de","epdcq0L3bDE","3x3 Zauberwürfel lösen","Pezcraft"},
+                          {"pl","cBU9Y729nQM","Jak ułożyć kostkę Rubika 3x3","DżoDżo"},
+                          {"kr","PMLG4__npcY","3x3 큐브 기초해법 (파트1)","듀나메스 큐브 해법연구소"},
+                          {"kr","vbvyjs4Vmoc","3x3 큐브 기초해법 (파트2)","듀나메스 큐브 해법연구소"},
+                          {"kr","V5eeKu9abCc","3x3 큐브 기초해법 (파트3)","듀나메스 큐브 해법연구소"},
+                     //   {"tw","76NmRQx5CLA","魔術方塊教學","1hrBLD"},
+                         }
+        ),
+
+  CUBE4 ( ObjectList.CUBE, 4,
+          new String[][] {
+                          {"gb","RR77Md71Ymc","How to Solve the 4x4 Rubik's Cube","Z3"},
+                          {"es","d_4xk1r9hxU","Resolver cubo de Rubik 4x4","Cuby"},
+                          {"ru","v5ytiOyTFSA","Как собрать кубик 4х4","Алексей Ярыгин"},
+                          {"fr","C83gYXn-zpI","Comment résoudre un Rubik's Cube 4x4","Le Cube"},
+                          {"de","Z7EmIp-TLN0","4x4 Zauberwürfel lösen","JamesKnopf"},
+                          {"pl","LiUxNsowXiI","Jak ułożyć kostkę 4x4","DżoDżo"},
+                          {"kr","5g4QORteCsk","원리로 이해하는 444 큐브 기초 해법","듀나메스 큐브 해법연구소"},
+                      //    {"tw","HuyaNIUaSqo","4x4魔術方塊復原#1","1hrBLD"},
+                      //    {"tw","gHho4gJQMXw","4x4魔術方塊復原#2","1hrBLD"},
+                      //    {"tw","7pbNgXMQxCE","4x4魔術方塊復原#3","1hrBLD"},
+                      //    {"tw","PZvc5XJ2bLY","4x4魔術方塊復原#4","1hrBLD"},
+                      //    {"tw","97vDE29lu2o","4x4魔術方塊復原#5","1hrBLD"},
+                         }
+        ),
+
+  CUBE5 ( ObjectList.CUBE, 5,
+          new String[][] {
+                          {"gb","zMkNkXHzQts","How to Solve the 5x5 Rubik's Cube","Z3"},
+                          {"es","6uaq-xfFs98","Resolver cubo de Rubik 5x5","Cuby"},
+                          {"ru","UtKsyLk45uA","Как собрать кубик 5x5","Кубмаркет"},
+                          {"fr","sq14CsrSkbo","Comment résoudre un Rubik's Cube 5x5","Le Cube"},
+                          {"de","luLwvHDPnrA","5x5 Zauberwürfel lösen","Pezcraft"},
+                          {"pl","ERsPyWOF7mg","Jak ułożyć kostkę 5x5x5","DżoDżo"},
+                          {"kr","D46qDaBFWNM","[555큐브]믿고보는영상!","Playon U온돌차"},
+                         }
+        ),
+
+  PYRA3 ( ObjectList.PYRA, 3,
+          new String[][] {
+                          {"gb","xIQtn2qazvg","Pyraminx Layer By Layer","Z3"},
+                          {"es","4cJJe9RAzAU","Resolver Pyraminx","Cuby"},
+                          {"ru","F4_bhfWyVRQ","Как собрать ПИРАМИДКУ","Е Бондаренко"},
+                          {"fr","Z2h1YI6jPes","Comment résoudre le Pyraminx","ValentinoCube"},
+                          {"de","x_DMA8htJpY","Pyraminx lösen","Pezcraft"},
+                          {"pl","uNpKpJfAa5I","Jak ułożyć: Pyraminx","DżoDżo"},
+                          {"kr","mO3excjvvoA","피라밍크스 맞추는 방법","iamzoone"},
+                    //    {"tw","YS3cDcP6Aro","金字塔方塊解法","1hrBLD"},
+                         }
+        ),
+
+  PYRA4 ( ObjectList.PYRA, 4,
+          new String[][] {
+                          {"gb","tGQDqDcSa6U","How to Solve the Master Pyraminx","Z3"},
+                          {"es","74PIPm9-uPg","Resolver Master Pyraminx 4x4","Cuby"},
+                          {"ru","-F_xJAwkobU","Как собрать Мастер Пираминкс"," Алексей Ярыгин"},
+                          {"fr","F3gzBs7uvmw","Tuto: résoudre le Master Pyraminx","Spaghetti Cubing"},
+                          {"de","3Q_bO7_FfAI","Master Pyraminx lösen","CubaroCubing"},
+                          {"pl","EamwvhmHC7Q","4x4 (Master) Pyraminx PL","MrUk"},
+                          {"kr","JlmBKaHESyY","마스터 피라밍크스 해법","주누후누"},
+                         }
+        ),
+
+  PYRA5 ( ObjectList.PYRA, 5,
+          new String[][] {
+                          {"gb","2nsPEECDdN0","Professor Pyraminx Solve","RedKB"},
+                          {"es","cSDj8OQK3TU","Tutorial del Professor Pyraminx","QBAndo"},
+                          {"ru","gMp1tbDyDWg","Как собрать Professor Pyraminx","RBcuber"},
+                          {"de","pCHx9bVMSgI","Professor Pyraminx Teil 1","Arvid Bollmann"},
+                          {"de","iiNXJMVNmCM","Professor Pyraminx Teil 2","Arvid Bollmann"},
+                         }
+        ),
+
+  DIAM2 ( ObjectList.DIAM, 2,
+          new String[][] {
+                          {"gb","R2wrbJJ3izM","How to Solve a Skewb Diamond","Dr. Penguin^3"},
+                          {"es","2RCusYQdYYE","Como resolver Skewb Diamond","Tutoriales Rubik"},
+                          {"ru","k8B6RFcNoGw","Как собрать Skewb Diamond","Алексей Ярыгин"},
+                          {"fr","tqbkgwNcZCE","Comment résoudre le Skewb Diamond","Valentino Cube"},
+                          {"de","6ewzrCOnZfg","Octagon lösen","JamesKnopf"},
+                          {"pl","61_Z4TpLMBc","Diamond Skewb TUTORIAL PL","MrUk"},
+                          {"kr","hVBSlfHVTME","공식 하나만 사용 - 다이아몬드 스큐브","Denzel Washington"},
+                         }
+        ),
+
+  DINO3 ( ObjectList.DINO, 3,
+          new String[][] {
+                          {"gb","puTJZqFBQwo","Dino Skewb Cube Tutorial","Bearded Cubing"},
+                          {"es","6o1Yo5iCxvI","Resolver Cubo Dino","Cuby"},
+                          {"ru","tWDrCtIv1_U","Как собрать Дино Куб","Алексей Ярыгин"},
+                          {"fr","hNkpte7Mesc","Comment résoudre le Dino Cube","Valentino Cube"},
+                          {"de","RqJLI6_C9JA","Dino Cube Tutorial","GerCubing"},
+                          {"pl","o05DYu8UMio","Dino Cube TUTORIAL PL","MrUk"},
+                          {"kr","imbrqGPSXWQ","(엑스큐브)완전 정복하기!","초등취미생활"},
+                         }
+        ),
+
+  REDI3 ( ObjectList.REDI, 3,
+          new String[][] {
+                          {"gb","Qn7TJED6O-4","How to Solve the MoYu Redi Cube","Z3"},
+                          {"es","g0M38Aotgac","Resolver Redi Cube","Cuby"},
+                          {"ru","ip2wYwc0DMI","Как собрать Реди Куб?","Кубмаркет"},
+                          {"fr","zw7UZcqqsgA","Comment résoudre le Redi Cube","ValentinoCube"},
+                          {"de","YU8riouyC2w","Redi Cube Solve","CubaroCubing"},
+                          {"pl","vxo3lXMsWQI","Jak ułożyć Redi Cube?","DJ rubiks"},
+                          {"kr","a5CzDMbRzbY","레디큐브를 배우기","vincentcube"},
+                         }
+        ),
+
+  HELI3 ( ObjectList.HELI, 3,
+          new String[][] {
+                          {"gb","-suwJpd_PO8","Helicopter Cube Tutorial","Bearded Cubing"},
+                          {"es","DWG9n_YyGPA","Resolver Helicopter Cube","Cuby"},
+                          {"ru","V4lJ3pg7Hio","Как собрать Куб Вертолет","Алексей Ярыгин"},
+                          {"fr","Zk8zWBWD2Ow","Comment résoudre le Helicopter Cube","Julien"},
+                          {"de","6VUH_FkBTlw","Helicopter Cube Tutorial","GerCubing"},
+                          {"pl","zoBZame4gFo","Helicopter Cube TUTORIAL PL","MrUk"},
+                          {"kr","xsXQSrEbgag","헬리콥터 큐브를 맞추는 법","연서큐브박"},
+                         }
+        ),
+
+  SKEW2 ( ObjectList.SKEW, 2,
+          new String[][] {
+                          {"gb","I6132yshkeU","How to Solve the Skewb","Z3"},
+                          {"es","wxQX3HhPgds","Resolver Skewb (Principiantes)","Cuby"},
+                          {"ru","_HSKZLC17w4","Как собрать Скьюб?","Кубмаркет"},
+                          {"fr","lR-GuIroh4k","Comment réussir le skewb","Rachma Nikov"},
+                          {"de","7RX6D5pznOk","Skewb lösen","Pezcraft"},
+                          {"pl","ofRu1fByNpk","Jak ułożyć: Skewb","DżoDżo"},
+                          {"kr","5R3sU-_bMAI","SKEWB 초보 공식","iamzoone"},
+                     //   {"tw","8srf9xhsS9k","Skewb斜轉方塊解法","1hrBLD"},
+                         }
+        ),
+
+  SKEW3 ( ObjectList.SKEW, 3,
+          new String[][] {
+                          {"gb","Jiuf7zQyPYI","Master Skewb Cube Tutorial","Bearded Cubing"},
+                          {"es","8TP6p63KQCA","Master Skewb en Español","jorlozCubes"},
+                          {"ru","7155QSp3T74","часть 1: Как собрать мастер Скьюб","Иван Циков"},
+                          {"ru","14ey-RihjgY","часть 2: Как собрать мастер Скьюб","Иван Циков"},
+                          {"ru","watq6TLa5_E","часть 2.5: Как собрать мастер Скьюб","Иван Циков"},
+                          {"ru","UnsvseFBXmo","часть 3: Как собрать мастер Скьюб","Иван Циков"},
+                          {"fr","tYMoY4EOHVA","Résolution du Master Skewb","Asthalis"},
+                          {"de","LSErzqGNElI","Master Skewb lösen","JamesKnopf"},
+                          {"pl","Y7l3AYFvDJI","Master Skewb TUTORIAL PL","MrUk"},
+                          {"kr","ycR-WmXJCG0","마스터 스큐브 3번째 해법","듀나메스 큐브 해법연구소"},
+                         }
+        ),
+
+  IVY2 ( ObjectList.IVY, 2,
+          new String[][] {
+                          {"gb","QMzeJobSu1M","How to Solve the Ivy Cube","Z3"},
+                          {"es","2-Gf2cmEJDs","Resolver Ivy Cube","Cuby"},
+                          {"ru","pbkfOCnnfsA","Как собрать Иви куб","Алексей Ярыгин"},
+                          {"fr","mn7YTnYu3Uc","Comment résoudre le Ivy Cube","ValentinoCube"},
+                          {"de","vaW5fSUG_O8","Ivy Cube ","ThomasStadler"},
+                          {"pl","8s_0VxNvFA8","Jak ułożyć Ivy Cube","DubiCube"},
+                          {"kr","TmSPgjtSFac","15분만에 아이비큐브 완전정복하기!","초등취미생활"},
+                         }
+        ),
+
+  REX3 ( ObjectList.REX, 3,
+          new String[][] {
+                          {"gb","noAQfWqlMbk","Rex Cube Tutorial","CrazyBadCuber"},
+                          {"es","Q90x9rjLJzw","Resolver Cubo Rex","Cuby"},
+                          {"ru","Dr9CLM6A3fU","Как собрать Рекс Куб","Алексей Ярыгин"},
+                          {"fr","SvK1kf6c43c","Résolution du Rex Cube","Asthalis"},
+                          {"de","AI4vtwpRkEQ","Rex Cube - Tutorial","GerCubing"},
+                          {"pl","ffbFRnHglWY","Rex Cube TUTORIAL PL","MrUk"},
+                          {"kr","B3ftZzHRQyU","렉스 큐브 해법","듀나메스 큐브 해법연구소"},
+                         }
+        ),
+
+  KILO3( ObjectList.KILO, 3,
+          new String[][] {
+                          {"gb","grgGgUSxiQg","How to Solve the Kilominx","Z3"},
+                          {"es","g6WMYjkCLok","Resolver Kilominx","Cuby"},
+                          {"ru","gjaknjuZXPs","Киломинкс как собрать","CUBES WORLD"},
+                          {"fr","F7z6LztN-7A","Résoudre le Kilominx","Twins Cuber"},
+                          {"de","fcmJdpLfZwk","Megaminx 2x2 lösen","JamesKnopf"},
+                          {"pl","tdWh8f8qpq4","Kilominx TUTORIAL PL","MrUK"},
+                          {"kr","8-X4GhQnE5I","2X2 킬로밍크스 TUTORIAL","큐브놀이터"},
+                         }
+       ),
+
+  KILO5( ObjectList.KILO, 5,
+          new String[][] {
+                          {"gb","VAnzC2SYVc4","How To Solve A Master Kilominx","Grizz Media"},
+                          {"es","ozINTg-61Fs","Tutorial Master Kilominx","RubikArt"},
+                          {"ru","0aemQayCZRc","Как собрать Мастер Киломинкс ч.1","Артем Мартиросов"},
+                          {"ru","ohOUFTx-oQI","Как собрать Мастер Киломинкс ч.2","Артем Мартиросов"},
+                          {"ru","YRXRdT2jCn8","Как собрать Мастер Киломинкс ч.3","Артем Мартиросов"},
+                          {"fr","usMiWt44aqo","Résolution du Master Kilominx","Asthalis"},
+                          {"pl","rdln0IG86_s","Master Kilominx TUTORIAL PL","MrUK"},
+                          {"kr","dvy-GxCjm5c","마스터 킬로밍크스 배우기 1","vincentcube"},
+                          {"kr","Jm0B12vNxsE","마스터 킬로밍크스 배우기 2","vincentcube"},
+                          {"kr","H1I18FVpr6g","마스터 킬로밍크스 배우기 3","vincentcube"},
+                         }
+       ),
+
+  MEGA3( ObjectList.MEGA, 3,
+          new String[][] {
+                          {"gb","j4x61L5Onzk","How to Solve the Megaminx","Z3"},
+                          {"es","xuKbT6Il0Ko","Resolver Megaminx","Cuby"},
+                          {"ru","WgoguOY3tKI","Как собрать Мегаминкс","Алексей Ярыгин"},
+                          {"fr","Ln1vl85puKo","Résoudre le Megaminx","Victor Colin"},
+                          {"de","d-GQD6CBdB8","Megaminx lösen","Pezcraft"},
+                          {"pl","BZTW6ApeRZE","Jak ułożyć: Megaminx","DżoDżo"},
+                          {"kr","2NUsMclrD-0","메가밍크스 예시솔빙","iamzoone"},
+                         }
+       ),
+
+  MEGA5( ObjectList.MEGA, 5,
+          new String[][] {
+                          {"gb","MNBMm8BnHtQ","Solve the Gigaminx Part 1","BeardedCubing"},
+                          {"gb","QrrP4GwqVMw","Solve the Gigaminx Part 2","BeardedCubing"},
+                          {"es","ex5EQMBxV1U","Tutorial Gigaminx","RubikArt"},
+                          {"ru","UJYK3SHjSGg","Как собрать Гигаминкс ч.1","Артем Мартиросов"},
+                          {"ru","-iBCpr4Gwsw","Как собрать Гигаминкс ч.2","Артем Мартиросов"},
+                          {"ru","4-dI7NCW8n8","Как собрать Гигаминкс ч.3","Артем Мартиросов"},
+                          {"fr","e485fh0V1dg","Résolution du Gigaminx","Asthalis"},
+                          {"de","APSAj4UtOAg","Megaminx 5x5 lösen","JamesKnopf"},
+                          {"pl","qbKLMCX1wKg","Jak ułożyć Gigaminxa cz.1","chomik19751"},
+                          {"pl","JQOXD3qleH4","Jak ułożyć Gigaminxa cz.2","chomik19751"},
+                          {"pl","WF2katJ22FA","Jak ułożyć Gigaminxa cz.3","chomik19751"},
+                          {"pl","jlyRrJjH4qQ","Jak ułożyć Gigaminxa cz.4","chomik19751"},
+                          {"kr","HfPFrWuz6z4","기가밍크스 gigaminx","큐브놀이터"},
+                         }
+       ),
+
+  BAN1( ObjectList.BAN1, 3,
+          new String[][] {
+                          {"gb","F_iJk_IvpVo","Bandaged Cube","CanChrisSolve"},
+                          {"es","_lTgw5aEFOg","Tutorial 3x3 Fuse Cube","QBAndo"},
+                          {"ru","raYDwFEXIq4","Как собрать Fused Cube","Алексей Ярыгин"},
+                          {"fr","9Cfi4rhKzIw","Tutoriel: résolution du Fused Cube","Skieur Cubb"},
+                          {"pl","0PcUoGxQa6s","Bandaged 3x3 v.A cube","MrUK"},
+                          {"kr","1RePOLrzJNE","밴디지 타입 A 해법","듀나메스 큐브 해법연구소"},
+                         }
+       ),
+
+  BAN2( ObjectList.BAN2, 3,
+          new String[][] {
+                          {"ru","lS_EK0PMWI8","Как собрать 2-bar Cube","Алексей Ярыгин"},
+                          {"pl","tX8ubTLh6p8","Bandaged 3x3 (Two bar)","MrUK"},
+                          {"kr","NE6XuC1r8xw","밴디지 큐브","Denzel Washington"},
+                         }
+       ),
+
+  BAN3( ObjectList.BAN3, 3,
+          new String[][] {
+                          {"gb","7UiCVGygUT4","Bandage Cube C Tutorial","PolyakB"},
+                          {"ru","gXenRA92Wdc","Как собрать Bandaged 3x3 Type C","YG Cuber"},
+                          {"pl","sKfdFLm79Zs","Bandaged 3x3 v.C cube","MrUK"},
+                          {"kr","BcCFgeFy6Ec","밴디지 타입 C 해법","듀나메스 큐브 해법연구소"},
+                         }
+       ),
+
+  BAN4( ObjectList.BAN4, 3,
+          new String[][] {
+                          {"gb","AnpdIKICBpM","Trying to Solve a Bandaged Cube","RedKB"},
+                          {"es","cUyo5fycrvI","Tutorial Bandaged Cube en español","Rafa Garcia Benacazon"},
+                          {"ru","-MTzeEJptsg","Как собрать bandaged Cube B","стратегия знаний"},
+                          {"fr","3rsfIJ3roT0","Tutoriel: résolution du Bicube","Skieur Cubb"},
+                          {"de","sqWVRwkXX9w","Bandaged Cube - Tutorial","GerCubing"},
+                          {"pl","XcHzTvVR6Po","Bandaged 3x3 v.B cube","MrUK"},
+                          {"kr","1gsoijF_5q0","BiCube Tutorial (해법)","듀나메스 큐브 해법연구소"},
+                         }
+       );
+
+  public static final int NUM_OBJECTS = values().length;
+  private final ObjectList mObject;
+  private final int mSize;
+  private final String[][] mTutorials;
+  private final int mNumTutorials;
+
+  private static final TutorialList[] objects;
+
+  static
+    {
+    objects = new TutorialList[NUM_OBJECTS];
+    int i=0;
+
+    for(TutorialList object: TutorialList.values())
+      {
+      objects[i++] = object;
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  TutorialList(ObjectList object, int size, String[][] tutorials)
+    {
+    mObject       = object;
+    mSize         = size;
+    mTutorials    = tutorials;
+    mNumTutorials = mTutorials.length;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public static TutorialList getObject(int ordinal)
+    {
+    return ordinal>=0 && ordinal<NUM_OBJECTS ? objects[ordinal] : CUBE3;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public static int getOrdinal(int objectOrdinal, int size)
+    {
+    if( objectOrdinal==ObjectList.DIN4.ordinal() )
+      {
+      objectOrdinal= ObjectList.DINO.ordinal();
+      }
+
+    for(int i=0; i<NUM_OBJECTS; i++)
+      {
+      if( objects[i].mObject.ordinal() == objectOrdinal && objects[i].mSize == size )
+        {
+        return i;
+        }
+      }
+
+    return -1;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public ObjectList getObjectList()
+    {
+    return mObject;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public int getSize()
+    {
+    return mSize;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public int getNumTutorials()
+    {
+    return mNumTutorials;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public int getIconID()
+    {
+    int sizeIndex = ObjectList.getSizeIndex(mObject.ordinal(),mSize);
+    return mObject.getIconIDs()[sizeIndex];
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public String getTutorialLanguage(int index)
+    {
+    return ( index>=0 && index<mNumTutorials ) ? mTutorials[index][0] : null;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public String getTutorialURL(int index)
+    {
+    return ( index>=0 && index<mNumTutorials ) ? mTutorials[index][1] : null;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public String getTutorialDescription(int index)
+    {
+    return ( index>=0 && index<mNumTutorials ) ? mTutorials[index][2] : null;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public String getTutorialAuthor(int index)
+    {
+    return ( index>=0 && index<mNumTutorials ) ? mTutorials[index][3] : null;
+    }
+}
diff --git a/src/main/java/org/distorted/tutorials/TutorialPreRender.java b/src/main/java/org/distorted/tutorials/TutorialPreRender.java
new file mode 100644
index 00000000..f8af705d
--- /dev/null
+++ b/src/main/java/org/distorted/tutorials/TutorialPreRender.java
@@ -0,0 +1,420 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// 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.tutorials;
+
+import android.content.Context;
+import android.content.res.Resources;
+
+import org.distorted.effects.BaseEffect;
+import org.distorted.effects.EffectController;
+import org.distorted.objects.ObjectList;
+import org.distorted.objects.TwistyObject;
+import org.distorted.main.RubikPreRender.ActionFinishedListener;
+import org.distorted.scores.RubikScores;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+public class TutorialPreRender implements EffectController
+  {
+  private ActionFinishedListener mAddActionListener;
+  private TutorialSurfaceView mView;
+  private boolean mFinishRotation, mRemoveRotation, mAddRotation,
+                  mSetQuat, mChangeObject, mSetupObject, mSolveObject, mScrambleObject,
+                  mInitializeObject, mResetAllTextureMaps, mRemovePatternRotation;
+  private boolean mCanPlay;
+  private boolean mIsSolved;
+  private ObjectList mNextObject;
+  private int mNextSize;
+  private long mRotationFinishedID;
+  private int mScreenWidth;
+  private int[][] mNextMoves;
+  private TwistyObject mOldObject, mNewObject;
+  private int mAddRotationAxis, mAddRotationRowBitmap, mAddRotationAngle;
+  private long mAddRotationDuration;
+  private long mAddRotationID, mRemoveRotationID;
+  private int mNearestAngle;
+  private int mScrambleObjectNum;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  TutorialPreRender(TutorialSurfaceView view)
+    {
+    mView = view;
+
+    mFinishRotation = false;
+    mRemoveRotation = false;
+    mAddRotation    = false;
+    mSetQuat        = false;
+    mChangeObject   = false;
+    mSetupObject    = false;
+    mSolveObject    = false;
+    mScrambleObject = false;
+
+    mCanPlay        = true;
+    mOldObject      = null;
+    mNewObject      = null;
+
+    mScreenWidth       = 0;
+    mScrambleObjectNum = 0;
+
+    mRemovePatternRotation= false;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void createObjectNow(ObjectList object, int size, int[][] moves)
+    {
+    if( mOldObject!=null ) mOldObject.releaseResources();
+    mOldObject = mNewObject;
+
+    Context con = mView.getContext();
+    Resources res = con.getResources();
+
+    mNewObject = object.create(size, mView.getQuat(), moves, res, mScreenWidth);
+
+    if( mNewObject!=null )
+      {
+      mNewObject.createTexture();
+      mView.setMovement(object.getObjectMovementClass());
+
+      if( mScreenWidth!=0 )
+        {
+        mNewObject.recomputeScaleFactor(mScreenWidth);
+        }
+
+      mIsSolved = mNewObject.isSolved();
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void doEffectNow(BaseEffect.Type type)
+    {
+    try
+      {
+      type.startEffect(mView.getRenderer().getScreen(),this);
+      }
+    catch( Exception ex )
+      {
+      mCanPlay= true;
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void removePatternRotation()
+    {
+    mRemovePatternRotation = true;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void removePatternRotationNow()
+    {
+    mRemovePatternRotation=false;
+    mNewObject.removeRotationNow();
+    mAddActionListener.onActionFinished(mRemoveRotationID);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void removeRotationNow()
+    {
+    mRemoveRotation=false;
+    mNewObject.removeRotationNow();
+
+    boolean solved = mNewObject.isSolved();
+
+    if( solved && !mIsSolved )
+      {
+      doEffectNow( BaseEffect.Type.WIN );
+      }
+    else
+      {
+      mCanPlay = true;
+      }
+
+    mIsSolved = solved;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void removeRotation()
+    {
+    mRemoveRotation = true;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void addRotationNow()
+    {
+    mAddRotation = false;
+    mAddRotationID = mNewObject.addNewRotation( mAddRotationAxis, mAddRotationRowBitmap,
+                                                mAddRotationAngle, mAddRotationDuration, this);
+
+    if( mAddRotationID==0 ) // failed to add effect - should never happen
+      {
+      mCanPlay = true;
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void finishRotationNow()
+    {
+    mFinishRotation = false;
+    mCanPlay        = false;
+    mRotationFinishedID = mNewObject.finishRotationNow(this, mNearestAngle);
+
+    if( mRotationFinishedID==0 ) // failed to add effect - should never happen
+      {
+      mCanPlay = true;
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void changeObjectNow()
+    {
+    mChangeObject = false;
+
+    if ( mNewObject==null || mNewObject.getObjectList()!=mNextObject || mNewObject.getNumLayers()!=mNextSize)
+      {
+      mCanPlay  = false;
+      createObjectNow(mNextObject, mNextSize, null);
+      doEffectNow( BaseEffect.Type.SIZECHANGE );
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void setupObjectNow()
+    {
+    mSetupObject = false;
+
+    if ( mNewObject==null || mNewObject.getObjectList()!=mNextObject || mNewObject.getNumLayers()!=mNextSize)
+      {
+      mCanPlay  = false;
+      createObjectNow(mNextObject, mNextSize, mNextMoves);
+      doEffectNow( BaseEffect.Type.SIZECHANGE );
+      }
+    else
+      {
+      mNewObject.initializeObject(mNextMoves);
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void solveObjectNow()
+    {
+    mSolveObject = false;
+    mCanPlay     = false;
+    doEffectNow( BaseEffect.Type.SOLVE );
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void scrambleObjectNow()
+    {
+    mScrambleObject = false;
+    mCanPlay        = false;
+    mIsSolved       = false;
+    RubikScores.getInstance().incrementNumPlays();
+    doEffectNow( BaseEffect.Type.SCRAMBLE );
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void initializeObjectNow()
+    {
+    mInitializeObject = false;
+    mNewObject.initializeObject(mNextMoves);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void resetAllTextureMapsNow()
+    {
+    mResetAllTextureMaps = false;
+
+    if( mNewObject!=null ) mNewObject.resetAllTextureMaps();
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void setQuatNow()
+    {
+    mSetQuat = false;
+    mView.setQuat();
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+//
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void setScreenSize(int width)
+    {
+    if( mNewObject!=null )
+      {
+      mNewObject.createTexture();
+      mNewObject.recomputeScaleFactor(width);
+      }
+
+    mScreenWidth  = width;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void finishRotation(int nearestAngle)
+    {
+    mNearestAngle   = nearestAngle;
+    mFinishRotation = true;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void changeObject(ObjectList object, int size)
+    {
+    if( size>0 )
+      {
+      mChangeObject = true;
+      mNextObject = object;
+      mNextSize   = size;
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void setQuatOnNextRender()
+    {
+    mSetQuat = true;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void preRender()
+    {
+    if( mSetQuat               ) setQuatNow();
+    if( mFinishRotation        ) finishRotationNow();
+    if( mRemoveRotation        ) removeRotationNow();
+    if( mChangeObject          ) changeObjectNow();
+    if( mSetupObject           ) setupObjectNow();
+    if( mSolveObject           ) solveObjectNow();
+    if( mScrambleObject        ) scrambleObjectNow();
+    if( mAddRotation           ) addRotationNow();
+    if( mInitializeObject      ) initializeObjectNow();
+    if( mResetAllTextureMaps   ) resetAllTextureMapsNow();
+    if( mRemovePatternRotation ) removePatternRotationNow();
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// PUBLIC API
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void addRotation(ActionFinishedListener listener, int axis, int rowBitmap, int angle, long duration)
+    {
+    mAddRotation = true;
+
+    mAddActionListener    = listener;
+    mAddRotationAxis      = axis;
+    mAddRotationRowBitmap = rowBitmap;
+    mAddRotationAngle     = angle;
+    mAddRotationDuration  = duration;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void initializeObject(int[][] moves)
+    {
+    mInitializeObject = true;
+    mNextMoves = moves;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public int getNumScrambles()
+    {
+    return mScrambleObjectNum;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void solveObject()
+    {
+    if( mCanPlay )
+      {
+      mSolveObject = true;
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void scrambleObject(int num)
+    {
+    if( mCanPlay )
+      {
+      mScrambleObject = true;
+      mScrambleObjectNum = num;
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void resetAllTextureMaps()
+    {
+    mResetAllTextureMaps = true;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public TwistyObject getObject()
+    {
+    return mNewObject;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public TwistyObject getOldObject()
+    {
+    return null;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void effectFinished(final long effectID)
+    {
+    if( effectID == mRotationFinishedID )
+      {
+      mRotationFinishedID = 0;
+      removeRotation();
+      }
+    else if( effectID == mAddRotationID )
+      {
+      mAddRotationID = 0;
+      mRemoveRotationID = effectID;
+      removePatternRotation();
+      }
+    else
+      {
+      mCanPlay   = true;
+      }
+    }
+  }
diff --git a/src/main/java/org/distorted/tutorials/TutorialRenderer.java b/src/main/java/org/distorted/tutorials/TutorialRenderer.java
new file mode 100644
index 00000000..48a8d850
--- /dev/null
+++ b/src/main/java/org/distorted/tutorials/TutorialRenderer.java
@@ -0,0 +1,98 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// 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.tutorials;
+
+import android.opengl.GLSurfaceView;
+
+import org.distorted.effects.BaseEffect;
+import org.distorted.library.effect.EffectType;
+import org.distorted.library.effect.VertexEffectQuaternion;
+import org.distorted.library.effect.VertexEffectRotate;
+import org.distorted.library.main.DistortedLibrary;
+import org.distorted.library.main.DistortedScreen;
+
+import javax.microedition.khronos.egl.EGLConfig;
+import javax.microedition.khronos.opengles.GL10;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+public class TutorialRenderer implements GLSurfaceView.Renderer, DistortedLibrary.ExceptionListener
+{
+   private final TutorialSurfaceView mView;
+   private final DistortedScreen mScreen;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+   TutorialRenderer(TutorialSurfaceView v)
+     {
+     final float BRIGHTNESS = 0.30f;
+
+     mView = v;
+     mScreen = new DistortedScreen();
+     mScreen.glClearColor(BRIGHTNESS, BRIGHTNESS, BRIGHTNESS, 1.0f);
+     }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+   @Override
+   public void onDrawFrame(GL10 glUnused)
+     {
+     long time = System.currentTimeMillis();
+     mView.getPreRender().preRender();
+     mScreen.render(time);
+     }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+   @Override
+   public void onSurfaceChanged(GL10 glUnused, int width, int height)
+      {
+      mScreen.resize(width,height);
+      mView.setScreenSize(width,height);
+      mView.getPreRender().setScreenSize(width);
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+   @Override
+   public void onSurfaceCreated(GL10 glUnused, EGLConfig config)
+      {
+      DistortedLibrary.setMax(EffectType.VERTEX,61);    // 60 Minx quaternions + rotate
+      VertexEffectRotate.enable();
+      VertexEffectQuaternion.enable();
+      BaseEffect.Type.enableEffects();
+
+      DistortedLibrary.onSurfaceCreated(mView.getContext(),this,1);
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+   public void distortedException(Exception ex)
+     {
+     android.util.Log.e("TUTORIAL", "unexpected exception: "+ex.getMessage() );
+     }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+   DistortedScreen getScreen()
+     {
+     return mScreen;
+     }
+}
diff --git a/src/main/java/org/distorted/tutorials/TutorialState.java b/src/main/java/org/distorted/tutorials/TutorialState.java
new file mode 100644
index 00000000..2ff848ce
--- /dev/null
+++ b/src/main/java/org/distorted/tutorials/TutorialState.java
@@ -0,0 +1,245 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// 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.tutorials;
+
+import android.view.View;
+import android.widget.ImageButton;
+import android.widget.LinearLayout;
+
+import org.distorted.main.R;
+import org.distorted.main.RubikActivity;
+import org.distorted.main.RubikPreRender;
+import org.distorted.objects.ObjectList;
+import org.distorted.objects.TwistyObject;
+import org.distorted.states.RubikStatePlay;
+import org.distorted.states.StateList;
+import org.distorted.states.TransparentImageButton;
+
+import java.util.ArrayList;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+public class TutorialState implements RubikPreRender.ActionFinishedListener
+{
+  private static final int DURATION_MILLIS = 750;
+
+  private ImageButton mPrevButton, mLockButton, mSolveButton, mScrambleButton, mBackButton;
+
+  private boolean mCanPrevMove;
+
+  private static class Move
+    {
+    private int mAxis, mRow, mAngle;
+
+    Move(int axis, int row, int angle)
+      {
+      mAxis = axis;
+      mRow  = row;
+      mAngle= angle;
+      }
+    }
+
+  ArrayList<Move> mMoves;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void backMove(TutorialPreRender pre)
+    {
+    if( mCanPrevMove )
+      {
+      int numMoves = mMoves.size();
+
+      if( numMoves>0 )
+        {
+        Move move = mMoves.remove(numMoves-1);
+        TwistyObject object = pre.getObject();
+
+        int axis  = move.mAxis;
+        int row   = (1<<move.mRow);
+        int angle = move.mAngle;
+        int numRot= Math.abs(angle*object.getBasicAngle()/360);
+
+        if( angle!=0 )
+          {
+          mCanPrevMove = false;
+          pre.addRotation(this, axis, row, -angle, numRot*DURATION_MILLIS);
+          }
+        else
+          {
+          android.util.Log.e("solution", "error: trying to back move of angle 0");
+          }
+        }
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void toggleLock(TutorialActivity act)
+    {
+    act.toggleLock();
+    mLockButton.setImageResource(getLockIcon(act));
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private int getLockIcon(TutorialActivity act)
+    {
+    if( act.retLocked() )
+      {
+      return RubikActivity.getDrawable(R.drawable.ui_small_locked,R.drawable.ui_medium_locked, R.drawable.ui_big_locked, R.drawable.ui_huge_locked);
+      }
+    else
+      {
+      return RubikActivity.getDrawable(R.drawable.ui_small_unlocked,R.drawable.ui_medium_unlocked, R.drawable.ui_big_unlocked, R.drawable.ui_huge_unlocked);
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void setupSolveButton(final TutorialActivity act, final float width)
+    {
+    int icon = RubikActivity.getDrawable(R.drawable.ui_small_cube_solve,R.drawable.ui_medium_cube_solve, R.drawable.ui_big_cube_solve, R.drawable.ui_huge_cube_solve);
+    mSolveButton = new TransparentImageButton(act, icon, width, LinearLayout.LayoutParams.MATCH_PARENT);
+
+    mSolveButton.setOnClickListener( new View.OnClickListener()
+      {
+      @Override
+      public void onClick(View v)
+        {
+        act.getPreRender().solveObject();
+        mMoves.clear();
+        }
+      });
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void setupLockButton(final TutorialActivity act, final float width)
+    {
+    final int icon = getLockIcon(act);
+    mLockButton = new TransparentImageButton(act, icon, width,LinearLayout.LayoutParams.MATCH_PARENT);
+
+    mLockButton.setOnClickListener( new View.OnClickListener()
+      {
+      @Override
+      public void onClick(View v)
+        {
+        toggleLock(act);
+        }
+      });
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void setupPrevButton(final TutorialActivity act, final float width)
+    {
+    int icon = RubikActivity.getDrawable(R.drawable.ui_small_cube_back,R.drawable.ui_medium_cube_back, R.drawable.ui_big_cube_back, R.drawable.ui_huge_cube_back);
+    mPrevButton = new TransparentImageButton(act, icon, width,LinearLayout.LayoutParams.MATCH_PARENT);
+
+    mPrevButton.setOnClickListener( new View.OnClickListener()
+      {
+      @Override
+      public void onClick(View v)
+        {
+        TutorialPreRender pre = act.getPreRender();
+        backMove(pre);
+        }
+      });
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void setupScrambleButton(final TutorialActivity act, final float width)
+    {
+    int icon = RubikActivity.getDrawable(R.drawable.ui_small_cube_scramble,R.drawable.ui_medium_cube_scramble, R.drawable.ui_big_cube_scramble, R.drawable.ui_huge_cube_scramble);
+    mScrambleButton = new TransparentImageButton(act, icon, width,LinearLayout.LayoutParams.MATCH_PARENT);
+
+    mScrambleButton.setOnClickListener( new View.OnClickListener()
+      {
+      @Override
+      public void onClick(View v)
+        {
+        RubikStatePlay play = (RubikStatePlay) StateList.PLAY.getStateClass();
+        int size = play.getSize();
+        int object= play.getObject();
+        int sizeIndex = ObjectList.getSizeIndex(object,size);
+        int maxLevel = ObjectList.getMaxLevel(object, sizeIndex);
+
+        act.getPreRender().scrambleObject(maxLevel);
+        }
+      });
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void setupBackButton(final TutorialActivity act, final float width)
+    {
+    int icon = RubikActivity.getDrawable(R.drawable.ui_small_smallback,R.drawable.ui_medium_smallback, R.drawable.ui_big_smallback, R.drawable.ui_huge_smallback);
+    mBackButton = new TransparentImageButton(act, icon, width, LinearLayout.LayoutParams.MATCH_PARENT);
+
+    mBackButton.setOnClickListener( new View.OnClickListener()
+      {
+      @Override
+      public void onClick(View v)
+        {
+        act.finish();
+        }
+      });
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void createRightPane(final TutorialActivity act, float width)
+    {
+    mCanPrevMove = true;
+
+    if( mMoves==null ) mMoves = new ArrayList<>();
+    else               mMoves.clear();
+
+    LinearLayout layout = act.findViewById(R.id.tutorialRightBar);
+    layout.removeAllViews();
+
+    setupPrevButton(act,width);
+    setupLockButton(act,width);
+    setupSolveButton(act,width);
+    setupScrambleButton(act,width);
+    setupBackButton(act,width);
+
+    layout.addView(mSolveButton);
+    layout.addView(mPrevButton);
+    layout.addView(mScrambleButton);
+    layout.addView(mLockButton);
+    layout.addView(mBackButton);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void addMove(int axis, int row, int angle)
+    {
+    mMoves.add(new Move(axis,row,angle));
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void onActionFinished(final long effectID)
+    {
+    mCanPrevMove = true;
+    }
+}
diff --git a/src/main/java/org/distorted/tutorials/TutorialSurfaceView.java b/src/main/java/org/distorted/tutorials/TutorialSurfaceView.java
new file mode 100644
index 00000000..53080bc0
--- /dev/null
+++ b/src/main/java/org/distorted/tutorials/TutorialSurfaceView.java
@@ -0,0 +1,703 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// 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.tutorials;
+
+import android.app.ActivityManager;
+import android.content.Context;
+import android.content.pm.ConfigurationInfo;
+import android.opengl.GLES30;
+import android.opengl.GLSurfaceView;
+import android.util.AttributeSet;
+import android.util.DisplayMetrics;
+import android.view.MotionEvent;
+
+import com.google.firebase.crashlytics.FirebaseCrashlytics;
+
+import org.distorted.library.type.Static2D;
+import org.distorted.library.type.Static4D;
+import org.distorted.objects.Movement;
+import org.distorted.objects.TwistyObject;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+public class TutorialSurfaceView extends GLSurfaceView
+{
+    private static final int NUM_SPEED_PROBES = 10;
+    private static final int INVALID_POINTER_ID = -1;
+
+    // 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 0.3 of an inch will start a Rotation.
+    private final static float ROTATION_SENSITIVITY = 0.3f;
+
+    private final Static4D CAMERA_POINT = new Static4D(0, 0, 1, 0);
+
+    private TutorialRenderer mRenderer;
+    private TutorialPreRender mPreRender;
+    private Movement mMovement;
+    private boolean mDragging, mBeginningRotation, mContinuingRotation;
+    private int mScreenWidth, mScreenHeight, mScreenMin;
+
+    private float mRotAngle, mInitDistance;
+    private int mPtrID1, mPtrID2;
+    private float mX, mY;
+    private float mStartRotX, mStartRotY;
+    private float mAxisX, mAxisY;
+    private float mRotationFactor;
+    private int mCurrentAxis, mCurrentRow;
+    private float mCurrentAngle, mCurrRotSpeed;
+    private float[] mLastX;
+    private float[] mLastY;
+    private long[] mLastT;
+    private int mFirstIndex, mLastIndex;
+    private int mDensity;
+
+    private static Static4D mQuat= new Static4D(-0.25189602f,0.3546389f,0.009657208f,0.90038127f);
+    private static Static4D mTemp= new Static4D(0,0,0,1);
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    void setScreenSize(int width, int height)
+      {
+      mScreenWidth = width;
+      mScreenHeight= height;
+
+      mScreenMin = Math.min(width, height);
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    boolean isVertical()
+      {
+      return mScreenHeight>mScreenWidth;
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    TutorialRenderer getRenderer()
+      {
+      return mRenderer;
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    TutorialPreRender getPreRender()
+      {
+      return mPreRender;
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    void setQuat()
+      {
+      mQuat.set(mTemp);
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    Static4D getQuat()
+      {
+      return mQuat;
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    void setMovement(Movement movement)
+      {
+      mMovement = movement;
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    private Static4D quatFromDrag(float dragX, float dragY)
+      {
+      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;
+
+        float ratio = axisL;
+        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);
+        }
+
+      return new Static4D(0f, 0f, 0f, 1f);
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// cast the 3D axis we are currently rotating along (which is already casted to the surface of the
+// currently touched face AND converted into a 4D vector - fourth 0) to a 2D in-screen-surface axis
+
+    private void computeCurrentAxis(Static4D axis)
+      {
+      Static4D result = rotateVectorByQuat(axis, mQuat);
+
+      mAxisX =result.get0();
+      mAxisY =result.get1();
+
+      float len = (float)Math.sqrt(mAxisX*mAxisX + mAxisY*mAxisY);
+      mAxisX /= len;
+      mAxisY /= len;
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// return quat1*quat2
+
+    public static Static4D quatMultiply( Static4D quat1, Static4D quat2 )
+      {
+      float qx = quat1.get0();
+      float qy = quat1.get1();
+      float qz = quat1.get2();
+      float qw = quat1.get3();
+
+      float rx = quat2.get0();
+      float ry = quat2.get1();
+      float rz = quat2.get2();
+      float rw = quat2.get3();
+
+      float tx = rw*qx - rz*qy + ry*qz + rx*qw;
+      float ty = rw*qy + rz*qx + ry*qw - rx*qz;
+      float tz = rw*qz + rz*qw - ry*qx + rx*qy;
+      float tw = rw*qw - rz*qz - ry*qy - rx*qx;
+
+      return new Static4D(tx,ty,tz,tw);
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// rotate 'vector' by quat  ( i.e. return quat*vector*(quat^-1) )
+
+    public static Static4D rotateVectorByQuat(Static4D vector, Static4D quat)
+      {
+      float qx = quat.get0();
+      float qy = quat.get1();
+      float qz = quat.get2();
+      float qw = quat.get3();
+
+      Static4D quatInverted= new Static4D(-qx,-qy,-qz,qw);
+      Static4D tmp = quatMultiply(quat,vector);
+
+      return quatMultiply(tmp,quatInverted);
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// rotate 'vector' by quat^(-1)  ( i.e. return (quat^-1)*vector*quat )
+
+    public static Static4D rotateVectorByInvertedQuat(Static4D vector, Static4D quat)
+      {
+      float qx = quat.get0();
+      float qy = quat.get1();
+      float qz = quat.get2();
+      float qw = quat.get3();
+
+      Static4D quatInverted= new Static4D(-qx,-qy,-qz,qw);
+      Static4D tmp = quatMultiply(quatInverted,vector);
+
+      return quatMultiply(tmp,quat);
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    private void addSpeedProbe(float x, float y)
+      {
+      long currTime = System.currentTimeMillis();
+      boolean theSame = mLastIndex==mFirstIndex;
+
+      mLastIndex++;
+      if( mLastIndex>=NUM_SPEED_PROBES ) mLastIndex=0;
+
+      mLastT[mLastIndex] = currTime;
+      mLastX[mLastIndex] = x;
+      mLastY[mLastIndex] = y;
+
+      if( mLastIndex==mFirstIndex)
+        {
+        mFirstIndex++;
+        if( mFirstIndex>=NUM_SPEED_PROBES ) mFirstIndex=0;
+        }
+
+      if( theSame )
+        {
+        mLastT[mFirstIndex] = currTime;
+        mLastX[mFirstIndex] = x;
+        mLastY[mFirstIndex] = y;
+        }
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    private void computeCurrentSpeedInInchesPerSecond()
+      {
+      long firstTime = mLastT[mFirstIndex];
+      long lastTime  = mLastT[mLastIndex];
+      float fX = mLastX[mFirstIndex];
+      float fY = mLastY[mFirstIndex];
+      float lX = mLastX[mLastIndex];
+      float lY = mLastY[mLastIndex];
+
+      long timeDiff = lastTime-firstTime;
+
+      mLastIndex = 0;
+      mFirstIndex= 0;
+
+      mCurrRotSpeed = timeDiff>0 ? 1000*retFingerDragDistanceInInches(fX,fY,lX,lY)/timeDiff : 0;
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    private float retFingerDragDistanceInInches(float xFrom, float yFrom, float xTo, float yTo)
+      {
+      float xDist = mScreenWidth*(xFrom-xTo);
+      float yDist = mScreenHeight*(yFrom-yTo);
+      float distInPixels = (float)Math.sqrt(xDist*xDist + yDist*yDist);
+
+      return distInPixels/mDensity;
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    private void setUpDragOrRotate(float x, float y)
+      {
+        Static4D touchPoint = new Static4D(x, y, 0, 0);
+        Static4D rotatedTouchPoint= rotateVectorByInvertedQuat(touchPoint, mQuat);
+        Static4D rotatedCamera= rotateVectorByInvertedQuat(CAMERA_POINT, mQuat);
+
+        if( mMovement!=null && mMovement.faceTouched(rotatedTouchPoint,rotatedCamera) )
+          {
+          mDragging           = false;
+          mContinuingRotation = false;
+          mBeginningRotation  = true;
+          }
+        else
+          {
+          final TutorialActivity act = (TutorialActivity)getContext();
+          mDragging           = !act.isLocked();
+          mContinuingRotation = false;
+          mBeginningRotation  = false;
+          }
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    private void drag(MotionEvent event, float x, float y)
+      {
+      if( mPtrID1!=INVALID_POINTER_ID && mPtrID2!=INVALID_POINTER_ID)
+        {
+        int pointer = event.findPointerIndex(mPtrID2);
+        float pX,pY;
+
+        try
+          {
+          pX = event.getX(pointer);
+          pY = event.getY(pointer);
+          }
+        catch(IllegalArgumentException ex)
+          {
+          mPtrID1=INVALID_POINTER_ID;
+          mPtrID2=INVALID_POINTER_ID;
+
+          FirebaseCrashlytics crashlytics = FirebaseCrashlytics.getInstance();
+          crashlytics.setCustomKey("DragError", "pointer="+pointer );
+          crashlytics.recordException(ex);
+
+          return;
+          }
+
+        float x2 = (pX - mScreenWidth*0.5f)/mScreenMin;
+        float y2 = (mScreenHeight*0.5f -pY)/mScreenMin;
+
+        float angleNow = getAngle(x,y,x2,y2);
+        float angleDiff = angleNow-mRotAngle;
+        float sinA =-(float)Math.sin(angleDiff);
+        float cosA = (float)Math.cos(angleDiff);
+
+        Static4D dragQuat = quatMultiply(new Static4D(0,0,sinA,cosA), mQuat);
+        mTemp.set(dragQuat);
+
+        mRotAngle = angleNow;
+
+        float distNow  = (float)Math.sqrt( (x-x2)*(x-x2) + (y-y2)*(y-y2) );
+        float distQuot = mInitDistance<0 ? 1.0f : distNow/ mInitDistance;
+        mInitDistance = distNow;
+
+        TwistyObject object = mPreRender.getObject();
+        if( object!=null ) object.setObjectRatio(distQuot);
+        }
+      else
+        {
+        Static4D dragQuat = quatMultiply(quatFromDrag(mX-x,y-mY), mQuat);
+        mTemp.set(dragQuat);
+        }
+
+      mPreRender.setQuatOnNextRender();
+      mX = x;
+      mY = y;
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    private void finishRotation()
+      {
+      computeCurrentSpeedInInchesPerSecond();
+      int angle = mPreRender.getObject().computeNearestAngle(mCurrentAngle, mCurrRotSpeed);
+      mPreRender.finishRotation(angle);
+
+      if( angle!=0 )
+        {
+        final TutorialActivity act = (TutorialActivity)getContext();
+        TutorialState state = act.getState();
+        state.addMove(mCurrentAxis, mCurrentRow, angle);
+        }
+
+      mContinuingRotation = false;
+      mBeginningRotation  = false;
+      mDragging           = true;
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    private void continueRotation(float x, float y)
+      {
+      float dx = x-mStartRotX;
+      float dy = y-mStartRotY;
+      float alpha = dx*mAxisX + dy*mAxisY;
+      float x2 = dx - alpha*mAxisX;
+      float y2 = dy - alpha*mAxisY;
+
+      float len = (float)Math.sqrt(x2*x2 + y2*y2);
+
+      // we have the length of 1D vector 'angle', now the direction:
+      float tmp = mAxisY==0 ? -mAxisX*y2 : mAxisY*x2;
+
+      float angle = (tmp>0 ? 1:-1)*len*mRotationFactor;
+      mCurrentAngle = SWIPING_SENSITIVITY*angle;
+      mPreRender.getObject().continueRotation(mCurrentAngle);
+
+      addSpeedProbe(x2,y2);
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    private void beginRotation(float x, float y)
+      {
+      mStartRotX = x;
+      mStartRotY = y;
+
+      TwistyObject object = mPreRender.getObject();
+      int numLayers = object.getNumLayers();
+
+      Static4D touchPoint2 = new Static4D(x, y, 0, 0);
+      Static4D rotatedTouchPoint2= rotateVectorByInvertedQuat(touchPoint2, mQuat);
+      Static2D res = mMovement.newRotation(numLayers,rotatedTouchPoint2);
+
+      mCurrentAxis = (int)res.get0();
+      mCurrentRow  = (int)res.get1();
+
+      computeCurrentAxis( mMovement.getCastedRotAxis(mCurrentAxis) );
+      mRotationFactor = mMovement.returnRotationFactor(numLayers,mCurrentRow);
+
+      object.beginNewRotation( mCurrentAxis, mCurrentRow );
+
+      addSpeedProbe(x,y);
+
+      mBeginningRotation = false;
+      mContinuingRotation= true;
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    private float getAngle(float x1, float y1, float x2, float y2)
+      {
+      return (float) Math.atan2(y1-y2, x1-x2);
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    private void actionMove(MotionEvent event)
+      {
+      int pointer = event.findPointerIndex(mPtrID1 != INVALID_POINTER_ID ? mPtrID1:mPtrID2);
+
+      if( pointer<0 ) return;
+
+      float pX = event.getX(pointer);
+      float pY = event.getY(pointer);
+
+      float x = (pX - mScreenWidth*0.5f)/mScreenMin;
+      float y = (mScreenHeight*0.5f -pY)/mScreenMin;
+
+      if( mBeginningRotation )
+        {
+        if( retFingerDragDistanceInInches(mX,mY,x,y) > ROTATION_SENSITIVITY )
+          {
+          beginRotation(x,y);
+          }
+        }
+      else if( mContinuingRotation )
+        {
+        continueRotation(x,y);
+        }
+      else if( mDragging )
+        {
+        drag(event,x,y);
+        }
+      else
+        {
+        setUpDragOrRotate(x,y);
+        }
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    private void actionDown(MotionEvent event)
+      {
+      mPtrID1 = event.getPointerId(0);
+
+      float x = event.getX();
+      float y = event.getY();
+
+      mX = (x - mScreenWidth*0.5f)/mScreenMin;
+      mY = (mScreenHeight*0.5f -y)/mScreenMin;
+
+      setUpDragOrRotate(mX,mY);
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    private void actionUp(MotionEvent event)
+      {
+      mPtrID1 = INVALID_POINTER_ID;
+      mPtrID2 = INVALID_POINTER_ID;
+
+      if( mContinuingRotation )
+        {
+        finishRotation();
+        }
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    private void actionDown2(MotionEvent event)
+      {
+      int index = event.getActionIndex();
+
+      if( mPtrID1==INVALID_POINTER_ID )
+        {
+        mPtrID1 = event.getPointerId(index);
+        float x = event.getX();
+        float y = event.getY();
+
+        if( mPtrID2 != INVALID_POINTER_ID )
+          {
+          int pointer = event.findPointerIndex(mPtrID2);
+
+          try
+            {
+            float x2 = event.getX(pointer);
+            float y2 = event.getY(pointer);
+
+            mRotAngle = getAngle(x,-y,x2,-y2);
+            mInitDistance = -1;
+            }
+          catch(IllegalArgumentException ex)
+            {
+            mPtrID1=INVALID_POINTER_ID;
+            mPtrID2=INVALID_POINTER_ID;
+
+            FirebaseCrashlytics crashlytics = FirebaseCrashlytics.getInstance();
+            crashlytics.setCustomKey("DragError", "pointer="+pointer );
+            crashlytics.recordException(ex);
+
+            return;
+            }
+          }
+
+        mX = (x - mScreenWidth*0.5f)/mScreenMin;
+        mY = (mScreenHeight*0.5f -y)/mScreenMin;
+        }
+      else if( mPtrID2==INVALID_POINTER_ID )
+        {
+        mPtrID2 = event.getPointerId(index);
+
+        float x = event.getX();
+        float y = event.getY();
+
+        if( mPtrID2 != INVALID_POINTER_ID )
+          {
+          int pointer = event.findPointerIndex(mPtrID2);
+
+          try
+            {
+            float x2 = event.getX(pointer);
+            float y2 = event.getY(pointer);
+
+            mRotAngle = getAngle(x,-y,x2,-y2);
+            mInitDistance = -1;
+            }
+          catch(IllegalArgumentException ex)
+            {
+            mPtrID1=INVALID_POINTER_ID;
+            mPtrID2=INVALID_POINTER_ID;
+
+            FirebaseCrashlytics crashlytics = FirebaseCrashlytics.getInstance();
+            crashlytics.setCustomKey("DragError", "pointer="+pointer );
+            crashlytics.recordException(ex);
+
+            return;
+            }
+          }
+
+        if( mBeginningRotation || mContinuingRotation )
+          {
+          mX = (x - mScreenWidth*0.5f)/mScreenMin;
+          mY = (mScreenHeight*0.5f -y)/mScreenMin;
+          }
+        }
+
+      if( mBeginningRotation )
+        {
+        mContinuingRotation = false;
+        mBeginningRotation  = false;
+        mDragging           = true;
+        }
+      else if( mContinuingRotation )
+        {
+        finishRotation();
+        }
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    private void actionUp2(MotionEvent event)
+      {
+      int index = event.getActionIndex();
+
+      if( index==event.findPointerIndex(mPtrID1) )
+        {
+        mPtrID1 = INVALID_POINTER_ID;
+        int pointer = event.findPointerIndex(mPtrID2);
+
+        if( pointer>=0 )
+          {
+          float x1 = event.getX(pointer);
+          float y1 = event.getY(pointer);
+
+          mX = (x1 - mScreenWidth*0.5f)/mScreenMin;
+          mY = (mScreenHeight*0.5f -y1)/mScreenMin;
+          }
+        }
+      else if( index==event.findPointerIndex(mPtrID2) )
+        {
+        mPtrID2 = INVALID_POINTER_ID;
+        }
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    void initialize()
+      {
+      mPtrID1 = INVALID_POINTER_ID;
+      mPtrID2 = INVALID_POINTER_ID;
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// PUBLIC API
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public TutorialSurfaceView(Context context, AttributeSet attrs)
+      {
+      super(context,attrs);
+
+      if(!isInEditMode())
+        {
+        mCurrRotSpeed= 0.0f;
+
+        mLastX = new float[NUM_SPEED_PROBES];
+        mLastY = new float[NUM_SPEED_PROBES];
+        mLastT = new long[NUM_SPEED_PROBES];
+        mFirstIndex =0;
+        mLastIndex  =0;
+
+        mRenderer  = new TutorialRenderer(this);
+        mPreRender = new TutorialPreRender(this);
+
+        TutorialActivity act = (TutorialActivity)context;
+        DisplayMetrics dm = new DisplayMetrics();
+        act.getWindowManager().getDefaultDisplay().getMetrics(dm);
+
+        mDensity = dm.densityDpi;
+
+        final ActivityManager activityManager= (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
+
+        try
+          {
+          final ConfigurationInfo configurationInfo = activityManager.getDeviceConfigurationInfo();
+          int esVersion = configurationInfo.reqGlEsVersion>>16;
+          setEGLContextClientVersion(esVersion);
+          setRenderer(mRenderer);
+          }
+        catch(Exception ex)
+          {
+          act.OpenGLError();
+
+          String shading = GLES30.glGetString(GLES30.GL_SHADING_LANGUAGE_VERSION);
+          String version = GLES30.glGetString(GLES30.GL_VERSION);
+          String vendor  = GLES30.glGetString(GLES30.GL_VENDOR);
+          String renderer= GLES30.glGetString(GLES30.GL_RENDERER);
+
+          FirebaseCrashlytics crashlytics = FirebaseCrashlytics.getInstance();
+          crashlytics.setCustomKey("GLSL Version"  , shading );
+          crashlytics.setCustomKey("GLversion"     , version );
+          crashlytics.setCustomKey("GL Vendor "    , vendor  );
+          crashlytics.setCustomKey("GLSLrenderer"  , renderer);
+          crashlytics.recordException(ex);
+          }
+        }
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event)
+      {
+      int action = event.getActionMasked();
+
+      switch(action)
+         {
+         case MotionEvent.ACTION_DOWN        : actionDown(event) ; break;
+         case MotionEvent.ACTION_MOVE        : actionMove(event) ; break;
+         case MotionEvent.ACTION_UP          : actionUp(event)   ; break;
+         case MotionEvent.ACTION_POINTER_DOWN: actionDown2(event); break;
+         case MotionEvent.ACTION_POINTER_UP  : actionUp2(event)  ; break;
+         }
+
+      return true;
+      }
+}
+
diff --git a/src/main/java/org/distorted/tutorials/TutorialWebView.java b/src/main/java/org/distorted/tutorials/TutorialWebView.java
new file mode 100644
index 00000000..76b5fdc3
--- /dev/null
+++ b/src/main/java/org/distorted/tutorials/TutorialWebView.java
@@ -0,0 +1,90 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// 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.tutorials;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.webkit.WebView;
+import android.webkit.WebViewClient;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+public class TutorialWebView
+{
+    private String  mUrl;
+    private Context mContext;
+    private WebView mWebView;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    @SuppressLint("SetJavaScriptEnabled")
+    public TutorialWebView(Context context, WebView webview)
+      {
+      mWebView = webview;
+      mContext = context;
+      mWebView.setBackgroundColor(0);
+      mWebView.getSettings().setJavaScriptEnabled(true);
+
+      mWebView.setWebViewClient(new WebViewClient()
+        {
+        @Override
+        public boolean shouldOverrideUrlLoading(WebView view, String url)
+          {
+          return false;
+          }
+        });
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public void load(String url)
+      {
+      mUrl = url;
+
+      String data1 = "<html><body><iframe width=\"100%\" height=\"100%\" src=\"";
+      String data2 = "\" frameborder=\"0\" allowfullscreen></iframe></body></html>";
+
+      mWebView.loadData(data1+url+data2, "text/html", "UTF-8");
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public void onPause()
+      {
+      mWebView.onPause();
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public void onResume()
+      {
+      mWebView.onResume();
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public void reload()
+      {
+      if (mUrl!=null)
+        {
+        load(mUrl);
+        }
+      }
+}
diff --git a/src/main/res/layout/tutorial.xml b/src/main/res/layout/tutorial.xml
index 6d8f2013..d6b6a914 100644
--- a/src/main/res/layout/tutorial.xml
+++ b/src/main/res/layout/tutorial.xml
@@ -21,7 +21,7 @@
         android:orientation="horizontal"
         android:background="@android:color/transparent">
 
-        <org.distorted.tutorial.TutorialSurfaceView
+        <org.distorted.tutorials.TutorialSurfaceView
            android:id="@+id/tutorialSurfaceView"
            android:layout_width="0dp"
            android:layout_height="match_parent"/>
