commit e458a4baf4e8bd0419adf91623d10368d4342500
Author: Leszek Koltunski <leszek@distoretedandroid.org>
Date:   Wed Jun 15 13:40:50 2016 +0100

    1. new package 'message'
    2. Rename the 'Grid' classes to match

diff --git a/src/main/java/org/distorted/library/DistortedBitmap.java b/src/main/java/org/distorted/library/DistortedBitmap.java
index c533909..37a64fe 100644
--- a/src/main/java/org/distorted/library/DistortedBitmap.java
+++ b/src/main/java/org/distorted/library/DistortedBitmap.java
@@ -80,7 +80,7 @@ public class DistortedBitmap extends DistortedObject
      mSizeX= width;
      mSizeY= height;
      mSizeZ= 1;     
-     mGrid = new GridBitmap(xsize,ysize);
+     mGrid = new DistortedBitmapGrid(xsize,ysize);
      initializeData(gridSize);
      }
 
diff --git a/src/main/java/org/distorted/library/DistortedBitmapGrid.java b/src/main/java/org/distorted/library/DistortedBitmapGrid.java
new file mode 100644
index 0000000..d27b3c5
--- /dev/null
+++ b/src/main/java/org/distorted/library/DistortedBitmapGrid.java
@@ -0,0 +1,116 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2016 Leszek Koltunski                                                               //
+//                                                                                               //
+// This file is part of Distorted.                                                               //
+//                                                                                               //
+// Distorted 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.                                                           //
+//                                                                                               //
+// Distorted 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 Distorted.  If not, see <http://www.gnu.org/licenses/>.                            //
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+package org.distorted.library;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+class DistortedBitmapGrid extends DistortedObjectGrid
+  {
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  DistortedBitmapGrid(int xLength, int yLength)
+    {
+    dataLength = 2*xLength*(yLength-1)+2*(yLength-2); // (yLength-1) strips, 2*xLength triangles in each, plus 2 degenerate triangles per each of (yLength-2) joins 
+    final short[] indexData = new short[dataLength];
+    
+    int offset=0;    
+    for(int y=0; y<yLength-1; y++) 
+      {
+      if (y>0) indexData[offset++] = (short) (y*xLength); // Degenerate begin: repeat first vertex
+
+      for (int x = 0; x < xLength; x++) 
+        {
+        indexData[offset++] = (short) (( y   *xLength)+x);
+        indexData[offset++] = (short) (((y+1)*xLength)+x);
+        }
+
+      if (y<yLength-2) indexData[offset++] = (short) (((y+1)*xLength) + (xLength-1)); // Degenerate end: repeat last vertex
+      }
+
+    // for(int g=0; g<dataLength; g++) Log.e(TAG_BACKGROUND, "index["+g+"]="+indexData[g]);
+
+    float[] bufferData= new float[COLOR_DATA_SIZE*dataLength];
+
+    offset=0;
+    for(int i=0; i<dataLength; i++)
+      {
+      bufferData[offset++] = 1.0f; // r
+      bufferData[offset++] = 1.0f; // g
+      bufferData[offset++] = 1.0f; // b
+      bufferData[offset++] = 1.0f; // a
+      }
+    mGridColors = ByteBuffer.allocateDirect(COLOR_DATA_SIZE*dataLength*BYTES_PER_FLOAT).order(ByteOrder.nativeOrder()).asFloatBuffer();                                                        
+    mGridColors.put(bufferData).position(0); 
+
+    bufferData = new float[NORMAL_DATA_SIZE*dataLength];
+
+    offset=0;
+    for(int i=0; i<dataLength; i++)
+      {
+      bufferData[offset++] = 0.0f; // x
+      bufferData[offset++] = 0.0f; // y
+      bufferData[offset++] = 1.0f; // z
+      }
+    mGridNormals = ByteBuffer.allocateDirect(NORMAL_DATA_SIZE*dataLength*BYTES_PER_FLOAT).order(ByteOrder.nativeOrder()).asFloatBuffer();                                                        
+    mGridNormals.put(bufferData).position(0); 
+
+    float tmpx,tmpy,tmpz;
+    bufferData = new float[TEX_DATA_SIZE*dataLength];
+
+    offset=0;
+    for(int i=0; i<dataLength; i++)
+      {
+      tmpx = ((float)(indexData[offset/2]%xLength))/(xLength-1);
+      tmpy = ((float)(indexData[offset/2]/xLength))/(yLength-1);
+
+      bufferData[offset++] = tmpx; // s=x
+      bufferData[offset++] = tmpy; // t=y
+      }
+    mGridTexture = ByteBuffer.allocateDirect(TEX_DATA_SIZE*dataLength*BYTES_PER_FLOAT).order(ByteOrder.nativeOrder()).asFloatBuffer();                                                        
+    mGridTexture.put(bufferData).position(0); 
+
+    //for(int g=0; g<dataLength; g++) Log.e(TAG_BACKGROUND, "tex["+g+"]=("+bufferData[2*g]+","+bufferData[2*g+1]+")");
+    //Log.e(TAG, "regWidth="+(2*mRegW)+" regHeight="+(2*mRegH)+" xLength="+xLength+" yLength="+yLength);
+
+    offset=0;
+    bufferData= new float[POSITION_DATA_SIZE*dataLength];
+
+    for(int i=0; i<dataLength; i++)
+      {
+      tmpx = ((float)(indexData[offset/3]%xLength))/(xLength-1);
+      tmpy = ((float)(indexData[offset/3]/xLength))/(yLength-1);
+      tmpz = 0;
+
+      bufferData[offset++] = (tmpx-0.5f); // x
+      bufferData[offset++] = (0.5f-tmpy); // y
+      bufferData[offset++] =        tmpz; // z
+      }
+    mGridPositions = ByteBuffer.allocateDirect(POSITION_DATA_SIZE*dataLength*BYTES_PER_FLOAT).order(ByteOrder.nativeOrder()).asFloatBuffer();                                                        
+    mGridPositions.put(bufferData).position(0); 
+
+    //for(int g=0; g<dataLength; g++) android.util.Log.e("BACKGROUND", "pos["+g+"]=("+bufferData[3*g]+","+bufferData[3*g+1]+")");
+    }
+  };
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
diff --git a/src/main/java/org/distorted/library/DistortedCubes.java b/src/main/java/org/distorted/library/DistortedCubes.java
index 900ddaa..3b5a42b 100644
--- a/src/main/java/org/distorted/library/DistortedCubes.java
+++ b/src/main/java/org/distorted/library/DistortedCubes.java
@@ -96,7 +96,7 @@ public class DistortedCubes extends DistortedObject
    mSizeX= gridSize*Cs;
    mSizeY= gridSize*Rs;
    mSizeZ= frontOnly ? 0 : gridSize;
-   mGrid = new GridCubes(cols,desc, frontOnly);
+   mGrid = new DistortedCubesGrid(cols,desc, frontOnly);
    initializeData(gridSize);
    }
 
diff --git a/src/main/java/org/distorted/library/DistortedCubesGrid.java b/src/main/java/org/distorted/library/DistortedCubesGrid.java
new file mode 100644
index 0000000..f768bbc
--- /dev/null
+++ b/src/main/java/org/distorted/library/DistortedCubesGrid.java
@@ -0,0 +1,758 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2016 Leszek Koltunski                                                               //
+//                                                                                               //
+// This file is part of Distorted.                                                               //
+//                                                                                               //
+// Distorted 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.                                                           //
+//                                                                                               //
+// Distorted 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 Distorted.  If not, see <http://www.gnu.org/licenses/>.                            //
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+package org.distorted.library;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.ArrayList;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+class DistortedCubesGrid extends DistortedObjectGrid
+   {
+   private static final float R = 0.2f;
+   private static final float FRONTZ = 0.5f;
+   private static final float BACKZ  =-0.5f;
+   
+   private static final int NORTH = 0;
+   private static final int WEST  = 1;
+   private static final int EAST  = 2;
+   private static final int SOUTH = 3;
+   
+   private static final boolean BACK  = true;
+   private static final boolean FRONT = false;
+   private static final boolean UPPER = false;
+   private static final boolean LOWER = true;
+   
+   private static final float[] mNormalX = new float[4];
+   private static final float[] mNormalY = new float[4];
+   
+   private class Edge
+     {
+     final int side; 
+     final int row;
+     final int col;
+     
+     public Edge(int s, int r, int c)
+       {
+       side= s; 
+       row = r;
+       col = c;
+       }
+     };
+   
+   private int frontVert, sideVert;
+   private int mCols, mRows;
+   private short[][] mCubes;
+   private ArrayList<Edge> mEdges = new ArrayList<Edge>();
+   
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+   private int computeDataLength(boolean frontOnly)
+      {
+      int frontWalls=0, frontSegments=0, sideWalls=0, sideBends=0;
+      
+      for(int i=0; i<mRows; i++)
+         for(int j=0; j<mCols; j++)
+            {
+            if( mCubes[i][j]%2 == 1 )  // land
+              {
+              frontWalls++;
+              if( j==mCols-1 || mCubes[i][j+1]%2 == 0 ) frontSegments++;
+              }
+              
+            if( (i==0 && mCubes[i][j]!=2) || (i!=0 && mCubes[i][j] != mCubes[i-1][j  ]) ) sideWalls++; // up
+            if( (j==0 && mCubes[i][j]!=2) || (j!=0 && mCubes[i][j] != mCubes[i  ][j-1]) ) sideWalls++; // left
+            if( i==mRows-1 && mCubes[i][j]!=2                                           ) sideWalls++; // bottom
+            if( j==mCols-1 && mCubes[i][j]!=2                                           ) sideWalls++; // right
+            }
+
+      int edges= mEdges.size();
+      
+      for(int i=0; i<edges; i++) 
+        {
+        Edge curr = mEdges.get(i);
+        Edge next = getNextEdge(curr);
+        int startX = curr.col;
+        int startY = curr.row;
+        int startS = curr.side;
+        
+        do
+          {
+          if( next.side != curr.side ) sideBends++; 
+          curr  = next; 
+          next = getNextEdge(curr);
+          }
+        while( curr.col!=startX || curr.row!=startY || curr.side!=startS );
+        }
+      
+      frontVert = 2*( frontWalls + 2*frontSegments - 1);
+      sideVert  = 2*( sideWalls + sideBends + edges -1);
+      
+      int dataL = frontOnly ? frontVert : (frontVert+1) + (1+sideVert+1) + (1+frontVert);
+      
+      android.util.Log.e("CUBES","frontVert="+frontVert+" sideVert="+sideVert);
+      android.util.Log.e("CUBES", "frontW="+frontWalls+" fSegments="+frontSegments+" sWalls="+sideWalls+" sSegments="+edges+" sideBends="+sideBends+" dataLen="+dataL );
+      
+      return dataL<0 ? 0:dataL;
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/*
+   private static String debug(short[] val)
+     {
+     String ret="";
+     
+     for(int i=0; i<val.length; i++) ret+=(" "+val[i]); 
+     
+     return ret;
+     }
+*/
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/*
+   private static String debug(float[] val, int stop)
+     {
+     String ret="";
+
+     for(int i=0; i<val.length; i++) 
+        {
+        if( i%stop==0 ) ret+="\n";
+        ret+=(" "+val[i]);
+        }
+
+     return ret;
+     }
+*/  
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/*
+   private static String debug(Edge e)
+     {
+     String d = "";
+     
+     switch(e.side)
+       {
+       case NORTH: d+="NORTH "; break;
+       case SOUTH: d+="SOUTH "; break;
+       case WEST : d+="WEST  "; break;
+       case EAST : d+="EAST  "; break;
+       }
+     
+     d+=("("+e.row+","+e.col+")");
+     
+     return d;
+     }   
+*/ 
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+   private void buildGrid(int cols, String desc, boolean frontOnly)
+     {
+     mRows     =0;
+     mCols     =0;
+     dataLength=0;
+     
+     if( cols>0 )
+       {
+       int reallen = desc.length();
+       int len = reallen;
+
+       if( (reallen/cols)*cols != reallen )
+         {
+         len = ((reallen/cols)+1)*cols; 
+         for(int i=reallen; i<len; i++) desc += "0";
+         }
+    
+       if( desc.indexOf("1")>=0 )
+         {
+         mCols = cols;
+         mRows = len/cols;
+
+         mCubes = new short[mRows][mCols];
+       
+         for(int j=0; j<mCols; j++) 
+           for(int i=0; i<mRows; i++)
+             mCubes[i][j] = (short)(desc.charAt(i*mCols+j) == '1' ? 1:0); 
+       
+         markRegions();
+         dataLength = computeDataLength(frontOnly);
+         }
+       }
+     }
+ 
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Mark all the 'regions' of our grid  - i.e. separate pieces of 'land' (connected blocks that will 
+// be rendered) and 'water' (connected holes in between) with integers. Each connected block of land
+// gets a unique odd integer, each connected block of water a unique even integer.
+//
+// Water on the edges of the grid is also considered connected to itself!   
+//   
+// This function also creates a list of 'Edges'. Each Edge is a data structure from which later on we
+// will start building the side walls of each connected black of land (and sides of holes of water 
+// inside)   
+   
+   private void markRegions()
+     {
+     int i, j, numWater=1, numLand=0;
+     
+     for(i=0; i<mRows;i++) if( mCubes[      i][      0]==0 ) markRegion((short)2,      i,       0);
+     for(i=0; i<mRows;i++) if( mCubes[      i][mCols-1]==0 ) markRegion((short)2,      i, mCols-1);
+     for(i=0; i<mCols;i++) if( mCubes[0      ][      i]==0 ) markRegion((short)2,      0,       i);
+     for(i=0; i<mCols;i++) if( mCubes[mRows-1][      i]==0 ) markRegion((short)2,mRows-1,       i);
+           
+     for(i=0; i<mRows; i++)
+        for(j=0; j<mCols; j++)
+           {
+           if( mCubes[i][j] == 0 ) { numWater++; markRegion( (short)(2*numWater ),i,j); mEdges.add(new Edge(NORTH,i,j)); }
+           if( mCubes[i][j] == 1 ) { numLand ++; markRegion( (short)(2*numLand+1),i,j); mEdges.add(new Edge(NORTH,i,j)); }
+           }
+     
+     // now we potentially need to kick out some Edges - precisely the edges with water inside -
+     // which are surrounded by more than one type of land. Otherwise the following does not work:
+     //
+     // 0 1 0
+     // 1 0 1
+     // 0 1 0
+     //
+     // The 'water inside' edges that did not get kicked out by this procedure need to be transformed
+     // with Edge(NORTH,row,col) -> Edge(SOUTH,row-1,col) so that later on normals work correctly
+     // (Edge always needs to point out from land to water for that)
+     
+     int numEdges= mEdges.size();
+     short initLand;
+     int initCol, initRow;
+     boolean kicked;
+     Edge e;
+     
+     for(i=0; i<numEdges; i++) 
+       {
+       e = mEdges.get(i);
+       initRow= e.row;
+       initCol= e.col;
+         
+       //android.util.Log.e("CUBES", "checking edge "+debug(e));
+             
+       if( mCubes[initRow][initCol]%2==0 )
+         {
+         kicked = false; 
+         initLand = mCubes[initRow-1][initCol];
+         
+         do
+           {
+           e = getNextEdge(e); 
+           //android.util.Log.e("CUBES", " next edge "+debug(e));   
+       
+           switch(e.side)
+             {
+             case NORTH: if( initLand!=mCubes[e.row-1][e.col  ] ) kicked=true; break;
+             case SOUTH: if( initLand!=mCubes[e.row+1][e.col  ] ) kicked=true; break;
+             case WEST:  if( initLand!=mCubes[e.row  ][e.col-1] ) kicked=true; break;
+             case EAST:  if( initLand!=mCubes[e.row  ][e.col+1] ) kicked=true; break;
+             }
+           
+           if( kicked )
+             {
+             //android.util.Log.e("CUBES", "kicking out edge!");
+             mEdges.remove(i);
+             i--;
+             numEdges--; 
+             }
+           }
+         while( kicked==false && (e.col!=initCol || e.row!=initRow || e.side!=NORTH) );
+         
+         if( kicked==false )
+           {
+           mEdges.set(i, new Edge(SOUTH,e.row-1,e.col)); 
+           }
+         }
+       }
+     }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// when calling, make sure that newVal != val
+   
+   private void markRegion(short newVal, int row, int col)
+     {
+     short val = mCubes[row][col];
+     mCubes[row][col] = newVal;
+     
+     if( row>0       && mCubes[row-1][col  ]==val ) markRegion(newVal, row-1, col  );
+     if( row<mRows-1 && mCubes[row+1][col  ]==val ) markRegion(newVal, row+1, col  );
+     if( col>0       && mCubes[row  ][col-1]==val ) markRegion(newVal, row  , col-1);
+     if( col<mCols-1 && mCubes[row  ][col+1]==val ) markRegion(newVal, row  , col+1);
+     }
+   
+///////////////////////////////////////////////////////////////////////////////////////////////////
+   
+   private void createNormals(int row, int col)
+     {
+     int td,lr; 
+      
+	  int nw = (col>0       && row>0      ) ? (mCubes[row-1][col-1]%2) : 0;
+	  int w  = (col>0                     ) ? (mCubes[row  ][col-1]%2) : 0;
+	  int n  = (               row>0      ) ? (mCubes[row-1][col  ]%2) : 0;
+	  int c  =                                (mCubes[row  ][col  ]%2);
+	  int sw = (col>0       && row<mRows-1) ? (mCubes[row+1][col-1]%2) : 0;
+     int s  = (               row<mRows-1) ? (mCubes[row+1][col  ]%2) : 0;
+     int ne = (col<mCols-1 && row>0      ) ? (mCubes[row-1][col+1]%2) : 0;
+     int e  = (col<mCols-1               ) ? (mCubes[row  ][col+1]%2) : 0;
+     int se = (col<mCols-1 && row<mRows-1) ? (mCubes[row+1][col+1]%2) : 0;
+     
+     td = nw+n-w-c;
+     lr = c+n-w-nw;
+     if( td<0 ) td=-1;
+     if( td>0 ) td= 1;
+     if( lr<0 ) lr=-1;
+     if( lr>0 ) lr= 1;
+     mNormalX[0] = lr*R;
+     mNormalY[0] = td*R;
+     
+     td = w+c-sw-s;
+     lr = c+s-w-sw;
+     if( td<0 ) td=-1;
+     if( td>0 ) td= 1;
+     if( lr<0 ) lr=-1;
+     if( lr>0 ) lr= 1;
+     mNormalX[1] = lr*R;
+     mNormalY[1] = td*R;
+     
+     td = n+ne-c-e;
+     lr = e+ne-c-n;
+     if( td<0 ) td=-1;
+     if( td>0 ) td= 1;
+     if( lr<0 ) lr=-1;
+     if( lr>0 ) lr= 1;
+     mNormalX[2] = lr*R;
+     mNormalY[2] = td*R;
+     
+     td = c+e-s-se;
+     lr = e+se-c-s;
+     if( td<0 ) td=-1;
+     if( td>0 ) td= 1;
+     if( lr<0 ) lr=-1;
+     if( lr>0 ) lr= 1;
+     mNormalX[3] = lr*R;
+     mNormalY[3] = td*R;
+     /*
+	  android.util.Log.d("CUBES", "row="+row+" col="+col);
+	  android.util.Log.d("CUBES", mNormalX[0]+" "+mNormalY[0]);
+	  android.util.Log.d("CUBES", mNormalX[1]+" "+mNormalY[1]);
+	  android.util.Log.d("CUBES", mNormalX[2]+" "+mNormalY[2]);
+	  android.util.Log.d("CUBES", mNormalX[3]+" "+mNormalY[3]);
+     */
+     }
+   
+///////////////////////////////////////////////////////////////////////////////////////////////////
+   
+   private int buildFrontBackGrid(boolean front, int vertex, float[] position, float[] normal, float[] texture)
+     {
+     short last, current;
+     boolean seenland=false;
+     float centerX, centerY;
+    
+     for(int i=0; i<mRows; i++)
+       {
+       last =0;
+         
+       for(int j=0; j<mCols; j++)
+         {
+         current = mCubes[i][j];
+            
+         if( current%2 == 1 )
+           {
+           centerX = j-(mCols-1.0f)/2.0f;
+           centerY = (mRows-1.0f)/2.0f-i;
+      
+           createNormals(i,j);
+          
+           if( last != current )
+             {
+             if( seenland ) vertex = repeatLast(vertex,position,normal,texture);    
+
+             if( front ) // NW corner
+               {
+               position[3*vertex+0] = (centerX-0.5f)/mCols;
+               position[3*vertex+1] = (centerY+0.5f)/mRows;
+               position[3*vertex+2] = FRONTZ;
+               normal[3*vertex+0]   = mNormalX[0];
+               normal[3*vertex+1]   = mNormalY[0];
+               normal[3*vertex+2]   = 1.0f;
+               texture[2*vertex+0]  = (float)j/mCols;
+               texture[2*vertex+1]  = (float)i/mRows;     
+               vertex++;
+               }
+             else  // SW corner
+               { 
+               position[3*vertex+0] = (centerX-0.5f)/mCols;
+               position[3*vertex+1] = (centerY-0.5f)/mRows; 
+               position[3*vertex+2] = BACKZ; 
+               normal[3*vertex+0]   = mNormalX[1];
+               normal[3*vertex+1]   = mNormalY[1];
+               normal[3*vertex+2]   =-1.0f;
+               texture[2*vertex+0]  = (float)j/mCols;
+               texture[2*vertex+1]  = (float)(i+1)/mRows;
+               vertex++;
+               
+               if( !seenland ) vertex = repeatLast(vertex,position,normal,texture);   //  if drawing the back, repeat the very first vertex
+               }
+             
+             if( seenland ) vertex = repeatLast(vertex,position,normal,texture);    
+
+             if( front ) // SW corner
+               {
+               position[3*vertex+0] = (centerX-0.5f)/mCols;
+               position[3*vertex+1] = (centerY-0.5f)/mRows; 
+               position[3*vertex+2] = FRONTZ; 
+               normal[3*vertex+0]   = mNormalX[1];
+               normal[3*vertex+1]   = mNormalY[1];
+               normal[3*vertex+2]   = 1.0f;
+               texture[2*vertex+0]  = (float)j/mCols;
+               texture[2*vertex+1]  = (float)(i+1)/mRows;
+               vertex++; 
+               }
+             else  // NW corner
+               {
+               position[3*vertex+0] = (centerX-0.5f)/mCols;
+               position[3*vertex+1] = (centerY+0.5f)/mRows;
+               position[3*vertex+2] = BACKZ;
+               normal[3*vertex+0]   = mNormalX[0];
+               normal[3*vertex+1]   = mNormalY[0];
+               normal[3*vertex+2]   =-1.0f;
+               texture[2*vertex+0]  = (float)j/mCols;
+               texture[2*vertex+1]  = (float)i/mRows;     
+               vertex++; 
+               }
+             }
+              
+           if( front )  // NE corner
+             {
+             position[3*vertex+0] = (centerX+0.5f)/mCols;
+             position[3*vertex+1] = (centerY+0.5f)/mRows;
+             position[3*vertex+2] = FRONTZ; 
+             normal[3*vertex+0]   = mNormalX[2];
+             normal[3*vertex+1]   = mNormalY[2];
+             normal[3*vertex+2]   = 1.0f;
+             texture[2*vertex+0]  = (float)(j+1)/mCols;
+             texture[2*vertex+1]  = (float)i/mRows;
+             vertex++;
+             }
+           else // SE corner
+             {
+             position[3*vertex+0] = (centerX+0.5f)/mCols;
+             position[3*vertex+1] = (centerY-0.5f)/mRows;
+             position[3*vertex+2] = BACKZ; 
+             normal[3*vertex+0]   = mNormalX[3];
+             normal[3*vertex+1]   = mNormalY[3];
+             normal[3*vertex+2]   =-1.0f;
+             texture[2*vertex+0]  = (float)(j+1)/mCols;
+             texture[2*vertex+1]  = (float)(i+1)/mRows;
+             vertex++; 
+             }
+           
+           if( front )  // SE corner
+             {
+             position[3*vertex+0] = (centerX+0.5f)/mCols;
+             position[3*vertex+1] = (centerY-0.5f)/mRows;
+             position[3*vertex+2] = FRONTZ; 
+             normal[3*vertex+0]   = mNormalX[3];
+             normal[3*vertex+1]   = mNormalY[3];
+             normal[3*vertex+2]   = 1.0f;
+             texture[2*vertex+0]  = (float)(j+1)/mCols;
+             texture[2*vertex+1]  = (float)(i+1)/mRows;
+             vertex++;
+             }
+           else // NE corner
+             {
+             position[3*vertex+0] = (centerX+0.5f)/mCols;
+             position[3*vertex+1] = (centerY+0.5f)/mRows;
+             position[3*vertex+2] = BACKZ; 
+             normal[3*vertex+0]   = mNormalX[2];
+             normal[3*vertex+1]   = mNormalY[2];
+             normal[3*vertex+2]   =-1.0f;
+             texture[2*vertex+0]  = (float)(j+1)/mCols;
+             texture[2*vertex+1]  = (float)i/mRows;
+             vertex++; 
+             }
+           
+           seenland = true;
+           }
+            
+         last = current;
+         }
+       }
+     
+     return vertex;
+     }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+   private int repeatLast(int vertex, float[] position, float[] normal, float[] texture)
+     {
+     if( vertex>0 )
+       {
+       position[3*vertex+0] = position[3*vertex-3]; 
+       position[3*vertex+1] = position[3*vertex-2];
+       position[3*vertex+2] = position[3*vertex-1];
+
+       normal[3*vertex+0]   = normal[3*vertex-3]; 
+       normal[3*vertex+1]   = normal[3*vertex-2];
+       normal[3*vertex+2]   = normal[3*vertex-1];
+
+       texture[2*vertex+0]  = texture[2*vertex-2];
+       texture[2*vertex+1]  = texture[2*vertex-1];
+         
+       vertex++;     
+       }
+     
+     return vertex;
+     }
+   
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+   private int buildSideGrid(int vertex, float[] position, float[] normal, float[] texture)
+     {
+     int edges= mEdges.size();
+     
+     for(int i=0; i<edges; i++) 
+       {
+       vertex = buildIthSide(mEdges.get(i), vertex, position, normal, texture);  
+       } 
+      
+     return vertex;
+     }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+   private int buildIthSide(Edge curr, int vertex, float[] position, float[] normal, float[] texture)
+     {
+     Edge prev; 
+     
+     if( curr.side==NORTH ) // water outside
+       {
+       prev = new Edge(WEST,curr.row,curr.col);
+       }
+     else                   // land outside; we need to move forward one link because we are going in opposite direction and we need to start from a bend.
+       {
+       prev = curr;
+       curr = new Edge(EAST,curr.row+1,curr.col-1);
+       }
+     
+     int col = curr.col;
+     int row = curr.row;
+     int side= curr.side;  
+     Edge next = getNextEdge(curr);
+     
+     addVertex(curr,BACK,LOWER,prev.side,vertex,position,normal,texture);
+     vertex++;
+     
+     do
+       {
+       if( prev.side!=curr.side )
+         {
+         addVertex(curr,BACK,LOWER,prev.side,vertex,position,normal,texture);
+         vertex++;
+         addVertex(curr,BACK,UPPER,prev.side,vertex,position,normal,texture);
+         vertex++;
+         }
+       
+       addVertex(curr,FRONT,LOWER,next.side,vertex,position,normal,texture);
+       vertex++;
+       addVertex(curr,FRONT,UPPER,next.side,vertex,position,normal,texture);
+       vertex++;
+       
+       prev = curr;
+       curr = next; 
+       next = getNextEdge(curr);
+       }
+     while( curr.col!=col || curr.row!=row || curr.side!=side );
+     
+     vertex = repeatLast(vertex,position,normal,texture);
+     
+     return vertex;
+     }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+   private Edge getNextEdge(Edge curr)
+     {
+     int col = curr.col;
+     int row = curr.row;
+      
+     //android.util.Log.e("CUBES", "row="+row+" col="+col+" mRows="+mRows+" mCols="+mCols);
+                       
+     switch(curr.side) 
+       {
+       case NORTH: if( col==mCols-1 ) 
+                     return new Edge(EAST,row,col);
+                   if( row>0 && mCubes[row-1][col+1]==mCubes[row][col] )
+                     return new Edge(WEST,row-1,col+1);
+                   if( mCubes[row][col+1]==mCubes[row][col] )
+                     return new Edge(NORTH,row,col+1);
+                   else  
+                     return new Edge(EAST,row,col);
+                   
+       case SOUTH: if( col==0 ) 
+                     return new Edge(WEST,row,col);
+                   if( (row<mRows-1) && mCubes[row+1][col-1]==mCubes[row][col] )
+                     return new Edge(EAST,row+1,col-1); 
+                   if( mCubes[row][col-1]==mCubes[row][col] )
+                     return new Edge(SOUTH,row,col-1);
+                   else
+                     return new Edge(WEST,row,col); 
+                     
+       case EAST : if( row==mRows-1 ) 
+                     return new Edge(SOUTH,row,col);
+                   if( (col<mCols-1) && mCubes[row+1][col+1]==mCubes[row][col] )
+                     return new Edge(NORTH,row+1,col+1);
+                   if( mCubes[row+1][col]==mCubes[row][col] )
+                     return new Edge(EAST,row+1,col);
+                   else 
+                     return new Edge(SOUTH,row,col);
+                   
+       case WEST : if( row==0 )
+                     return new Edge(NORTH,row,col);
+                   if( col>0 && mCubes[row-1][col-1]==mCubes[row][col] )
+                     return new Edge(SOUTH,row-1,col-1);
+                   if( mCubes[row-1][col]==mCubes[row][col] )
+                     return new Edge(WEST,row-1,col);
+                   else
+                     return new Edge(NORTH,row,col);     
+       }
+     
+     return null;
+     }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+   
+   private void addVertex(Edge curr, boolean back, boolean lower,int side, int vertex, float[] position, float[] normal, float[] texture)
+     {
+     float centerX = curr.col-(mCols-1.0f)/2.0f;
+     float centerY = (mRows-1.0f)/2.0f-curr.row;
+  
+     switch(curr.side)
+       {
+       case NORTH: position[3*vertex+0] = (back ? (centerX-0.5f) : (centerX+0.5f))/mCols; 
+                   position[3*vertex+1] = (centerY+0.5f)/mRows;
+                   position[3*vertex+2] = lower ? BACKZ : FRONTZ;
+
+                   normal[3*vertex+0]   = side==NORTH ? 0.0f : (side==WEST?-R:R);
+                   normal[3*vertex+1]   = 1.0f;
+                   normal[3*vertex+2]   = lower ? -R:R;
+
+                   texture[2*vertex+0]  = (float)(back ? (curr.col  ):(curr.col+1))/mCols;
+                   texture[2*vertex+1]  = (float)(lower? (curr.row-1):(curr.row  ))/mRows;  
+                   break;
+       case SOUTH: position[3*vertex+0] = (back ? (centerX+0.5f) : (centerX-0.5f))/mCols;
+                   position[3*vertex+1] = (centerY-0.5f)/mRows;
+                   position[3*vertex+2] = lower ? BACKZ : FRONTZ;  
+            
+                   normal[3*vertex+0]   = side==SOUTH ? 0.0f: (side==EAST?-R:R);
+                   normal[3*vertex+1]   =-1.0f;
+                   normal[3*vertex+2]   = lower ? -R:R;
+
+                   texture[2*vertex+0]  = (float)(back ? (curr.col+1):(curr.col  ))/mCols;
+                   texture[2*vertex+1]  = (float)(lower? (curr.row+2):(curr.row+1))/mRows;
+                   break;
+       case WEST : position[3*vertex+0] = (centerX-0.5f)/mCols;
+                   position[3*vertex+1] = (back ? (centerY-0.5f):(centerY+0.5f))/mRows;
+                   position[3*vertex+2] = lower ? BACKZ : FRONTZ;
+
+                   normal[3*vertex+0]   =-1.0f;
+                   normal[3*vertex+1]   = side==WEST ? 0.0f : (side==NORTH?-R:R);
+                   normal[3*vertex+2]   = lower ? -R:R;
+ 
+                   texture[2*vertex+0]  = (float)(lower ? (curr.col-1):(curr.col  ))/mCols;
+                   texture[2*vertex+1]  = (float)(back  ? (curr.row+1):(curr.row  ))/mRows;
+                   break;
+       case EAST : position[3*vertex+0] = (centerX+0.5f)/mCols;
+                   position[3*vertex+1] = (back ? (centerY+0.5f):(centerY-0.5f))/mRows;
+                   position[3*vertex+2] = lower ? BACKZ : FRONTZ;
+
+                   normal[3*vertex+0]   = 1.0f;
+                   normal[3*vertex+1]   = side==EAST ? 0.0f : (side==SOUTH?-R:R);
+                   normal[3*vertex+2]   = lower ? -R:R; 
+
+                   texture[2*vertex+0]  = (float)(lower ? (curr.col+2):(curr.col+1))/mCols;
+                   texture[2*vertex+1]  = (float)(back  ? (curr.row  ):(curr.row+1))/mRows;
+                   break;
+       }
+     
+     if(texture[2*vertex+0]>1.0f) texture[2*vertex+0] =2.0f-texture[2*vertex+0];
+     if(texture[2*vertex+0]<0.0f) texture[2*vertex+0] =    -texture[2*vertex+0];
+     if(texture[2*vertex+1]>1.0f) texture[2*vertex+1] =2.0f-texture[2*vertex+1];
+     if(texture[2*vertex+1]<0.0f) texture[2*vertex+1] =    -texture[2*vertex+1];
+     }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// PUBLIC API
+///////////////////////////////////////////////////////////////////////////////////////////////////
+   
+/**
+ * Creates the underlying grid of vertices, normals, texture coords and colors.
+ *    
+ * @param rows See {@link DistortedCubes#DistortedCubes(String)} 
+ * @param desc See {@link DistortedCubes#DistortedCubes(String)}
+ */
+   public DistortedCubesGrid(int cols, String desc, boolean frontOnly)
+      {
+      buildGrid(cols,desc,frontOnly);
+       
+      int numVertices=0;
+      float[] colorData   = new float[COLOR_DATA_SIZE   *dataLength];
+      float[] positionData= new float[POSITION_DATA_SIZE*dataLength];
+      float[] normalData  = new float[NORMAL_DATA_SIZE  *dataLength];
+      float[] textureData = new float[TEX_DATA_SIZE     *dataLength];
+      
+      for(int i=0; i<dataLength; i++)
+        {
+        colorData[COLOR_DATA_SIZE*i+0] = 1.0f; // r
+        colorData[COLOR_DATA_SIZE*i+1] = 1.0f; // g
+        colorData[COLOR_DATA_SIZE*i+2] = 1.0f; // b
+        colorData[COLOR_DATA_SIZE*i+3] = 1.0f; // a
+        }
+
+      numVertices = buildFrontBackGrid(true, numVertices,positionData,normalData,textureData);
+      
+      if( !frontOnly )
+        {
+        numVertices = repeatLast(numVertices,positionData,normalData,textureData);
+        numVertices = buildSideGrid (numVertices,positionData,normalData,textureData);
+        numVertices = buildFrontBackGrid (false,numVertices,positionData,normalData,textureData);
+        }
+      
+      /*
+      android.util.Log.e("CUBES","dataLen="+dataLength+" vertex="+numVertices);
+      android.util.Log.d("CUBES", "position: "+debug(positionData,3) );
+      android.util.Log.d("CUBES", "normal: "  +debug(  normalData,3) );
+      android.util.Log.d("CUBES", "texture: " +debug( textureData,2) );
+      */
+      mGridColors = ByteBuffer.allocateDirect(COLOR_DATA_SIZE*dataLength*BYTES_PER_FLOAT).order(ByteOrder.nativeOrder()).asFloatBuffer();                                                        
+      mGridColors.put(colorData).position(0); 
+
+      mGridPositions = ByteBuffer.allocateDirect(POSITION_DATA_SIZE*dataLength*BYTES_PER_FLOAT).order(ByteOrder.nativeOrder()).asFloatBuffer();                                                        
+      mGridPositions.put(positionData).position(0); 
+      
+      mGridNormals = ByteBuffer.allocateDirect(NORMAL_DATA_SIZE*dataLength*BYTES_PER_FLOAT).order(ByteOrder.nativeOrder()).asFloatBuffer();                                                        
+      mGridNormals.put(normalData).position(0); 
+
+      mGridTexture = ByteBuffer.allocateDirect(TEX_DATA_SIZE*dataLength*BYTES_PER_FLOAT).order(ByteOrder.nativeOrder()).asFloatBuffer();                                                        
+      mGridTexture.put(textureData).position(0); 
+      }
+   }
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
diff --git a/src/main/java/org/distorted/library/DistortedObject.java b/src/main/java/org/distorted/library/DistortedObject.java
index 344bae4..1aa92cf 100644
--- a/src/main/java/org/distorted/library/DistortedObject.java
+++ b/src/main/java/org/distorted/library/DistortedObject.java
@@ -23,6 +23,7 @@ import android.graphics.Bitmap;
 import android.opengl.GLES20;
 import android.opengl.GLUtils;
 
+import org.distorted.library.message.EffectListener;
 import org.distorted.library.type.Float1D;
 import org.distorted.library.type.Float2D;
 import org.distorted.library.type.Float3D;
@@ -52,7 +53,7 @@ public abstract class DistortedObject
 
     protected boolean matrixCloned, vertexCloned, fragmentCloned;
  
-    protected GridObject mGrid = null;
+    protected DistortedObjectGrid mGrid = null;
     protected long mID;
     protected int mSizeX, mSizeY, mSizeZ, mSize; // in screen space
 
diff --git a/src/main/java/org/distorted/library/DistortedObjectGrid.java b/src/main/java/org/distorted/library/DistortedObjectGrid.java
new file mode 100644
index 0000000..d547a82
--- /dev/null
+++ b/src/main/java/org/distorted/library/DistortedObjectGrid.java
@@ -0,0 +1,53 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2016 Leszek Koltunski                                                               //
+//                                                                                               //
+// This file is part of Distorted.                                                               //
+//                                                                                               //
+// Distorted 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.                                                           //
+//                                                                                               //
+// Distorted 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 Distorted.  If not, see <http://www.gnu.org/licenses/>.                            //
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+package org.distorted.library;
+
+import java.nio.FloatBuffer;
+
+import android.opengl.GLES20;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+abstract class DistortedObjectGrid
+   {
+   protected static final int BYTES_PER_FLOAT   = 4; //
+   protected static final int POSITION_DATA_SIZE= 3; // Size of the position data in elements
+   protected static final int COLOR_DATA_SIZE   = 4; // Size of the color data in elements 
+   protected static final int NORMAL_DATA_SIZE  = 3; // Size of the normal data in elements.
+   protected static final int TEX_DATA_SIZE     = 2; // Size of the texture coordinate data in elements. 
+
+   protected int dataLength;                       
+      
+   protected FloatBuffer mGridPositions,mGridColors,mGridNormals,mGridTexture;
+ 
+///////////////////////////////////////////////////////////////////////////////////////////////////
+   
+   void draw()
+     { 
+     GLES20.glVertexAttribPointer(Distorted.mPositionH    , POSITION_DATA_SIZE, GLES20.GL_FLOAT, false, 0, mGridPositions);          
+     GLES20.glVertexAttribPointer(Distorted.mColorH       , COLOR_DATA_SIZE   , GLES20.GL_FLOAT, false, 0, mGridColors   );   
+     GLES20.glVertexAttribPointer(Distorted.mNormalH      , NORMAL_DATA_SIZE  , GLES20.GL_FLOAT, false, 0, mGridNormals  );
+     GLES20.glVertexAttribPointer(Distorted.mTextureCoordH, TEX_DATA_SIZE     , GLES20.GL_FLOAT, false, 0, mGridTexture  );  
+
+     GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, dataLength); 
+     }
+   }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
diff --git a/src/main/java/org/distorted/library/EffectListener.java b/src/main/java/org/distorted/library/EffectListener.java
deleted file mode 100644
index d7374b0..0000000
--- a/src/main/java/org/distorted/library/EffectListener.java
+++ /dev/null
@@ -1,50 +0,0 @@
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Copyright 2016 Leszek Koltunski                                                               //
-//                                                                                               //
-// This file is part of Distorted.                                                               //
-//                                                                                               //
-// Distorted 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.                                                           //
-//                                                                                               //
-// Distorted 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 Distorted.  If not, see <http://www.gnu.org/licenses/>.                            //
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-package org.distorted.library;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * This interface lets users of the Distorted library get notified when something happens to one of the effects.
- * To receive the notifications, we first have to register with a call to {@link DistortedBitmap#addEventListener(EffectListener)}.
- * List of all possible events that can happen is defined in {@link EffectMessage}
- */
-
-public interface EffectListener 
-  {
-/**
- * Gets called when event of type 'em' happens to effect 'effectID'. 
- * 
- * @param eventType  Type of event that happened.
- * @param effectID   ID of the effect the event happened to. This ID must have been previously returned by one
- *                   of the DistortedBitmap.{deform,distort,move,...} functions.
- * @param effectName Name of the effect as defined in EffectNames, e.g. if effectType==EffectNames.MOVE.ordinal(),
- *                   then the event happened to a MOVE effect.
- * @param bitmapID   the ID of the DistortedBitmap object, as returned by {@link DistortedBitmap#getID()}, this event
- *                   happened to. If the object has been created using a copy constructor from another instance of
- *                   DistortedBitmap, the ID here will be the one of the original object.
- * @param message    Any message string associated with it. 'Failed' event types have one.
- * @see EffectMessage
- * @see EffectNames
- */
-   
-  void effectMessage(final EffectMessage eventType, final long effectID, final int effectName, final long bitmapID, final String message);
-  }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
diff --git a/src/main/java/org/distorted/library/EffectMessage.java b/src/main/java/org/distorted/library/EffectMessage.java
deleted file mode 100644
index 3ce89e3..0000000
--- a/src/main/java/org/distorted/library/EffectMessage.java
+++ /dev/null
@@ -1,63 +0,0 @@
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Copyright 2016 Leszek Koltunski                                                               //
-//                                                                                               //
-// This file is part of Distorted.                                                               //
-//                                                                                               //
-// Distorted 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.                                                           //
-//                                                                                               //
-// Distorted 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 Distorted.  If not, see <http://www.gnu.org/licenses/>.                            //
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-package org.distorted.library;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-import org.distorted.library.type.Interpolator;
-
-/**
-* Defines all possible events a class implementing the {@link EffectListener} interface can receive.
-*/
-
-public enum EffectMessage 
-  {
-/**
- * The effect has been removed. This can happen if:
- * <ul>
- * <li> someone explicitly removed the effect with a call to {@link DistortedBitmap#abortEffect(long)} (or one of the other 'abort' methods)
- * <li> the interpolation of the effect has finished and the end result is equal to the effect's unity.
- * </ul>    
- */
-  EFFECT_REMOVED,
-  
-/**
- * Interpolation of the effect has finished. 
- * <p>
- * If you set up an interpolated effect and set its Interpolator to do 3.5 interpolations of 1000 ms each
- * with calls to {@link Interpolator#setCount(float)} and {@link Interpolator#setDuration(long)},
- * then you are going to get this message exactly once after 3.5*1000 = 3500 milliseconds when the interpolation 
- * finishes. You will never get this message if you set the effect to go on indefinitely with a call to 
- * {@link Interpolator#setCount(float)}.
- * <p>  
- * If then the end effect is equal to the effect's unity, then immediately after this message you
- * will also get a EFFECT_REMOVED message.
- */
-  EFFECT_FINISHED,
-
-  /**
- * The effect has failed to properly execute.
- * <p>
- * Currently only OTHER effects (saving to PNG file and to a MP4 movie) can fail.
- */
-  EFFECT_FAILED
-  }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
diff --git a/src/main/java/org/distorted/library/EffectMessageSender.java b/src/main/java/org/distorted/library/EffectMessageSender.java
index 690b4c1..8965188 100644
--- a/src/main/java/org/distorted/library/EffectMessageSender.java
+++ b/src/main/java/org/distorted/library/EffectMessageSender.java
@@ -19,6 +19,9 @@
 
 package org.distorted.library;
 
+import org.distorted.library.message.EffectListener;
+import org.distorted.library.message.EffectMessage;
+
 import java.util.Vector;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
diff --git a/src/main/java/org/distorted/library/EffectQueue.java b/src/main/java/org/distorted/library/EffectQueue.java
index 757ab27..25c996e 100644
--- a/src/main/java/org/distorted/library/EffectQueue.java
+++ b/src/main/java/org/distorted/library/EffectQueue.java
@@ -19,6 +19,8 @@
 
 package org.distorted.library;
 
+import org.distorted.library.message.EffectListener;
+import org.distorted.library.message.EffectMessage;
 import org.distorted.library.type.Interpolator;
 import org.distorted.library.type.Interpolator2D;
 
@@ -247,7 +249,7 @@ abstract class EffectQueue
    
     for(int i=0; i<mNumListeners; i++) 
       EffectMessageSender.newMessage( mListeners.elementAt(i),
-                                      EffectMessage.EFFECT_REMOVED, 
+                                      EffectMessage.EFFECT_REMOVED,
                                       (removedID<<EffectTypes.LENGTH)+EffectNames.getType(removedType).type,
                                       removedType,
                                       mBitmapID,
diff --git a/src/main/java/org/distorted/library/EffectQueueFragment.java b/src/main/java/org/distorted/library/EffectQueueFragment.java
index 5fb94f4..db2130e 100644
--- a/src/main/java/org/distorted/library/EffectQueueFragment.java
+++ b/src/main/java/org/distorted/library/EffectQueueFragment.java
@@ -21,6 +21,7 @@ package org.distorted.library;
 
 import android.opengl.GLES20;
 
+import org.distorted.library.message.EffectMessage;
 import org.distorted.library.type.Float2D;
 import org.distorted.library.type.Float3D;
 import org.distorted.library.type.Float4D;
@@ -78,7 +79,7 @@ class EffectQueueFragment extends EffectQueue
         {
         for(int j=0; j<mNumListeners; j++)   
           EffectMessageSender.newMessage( mListeners.elementAt(j),
-                                          EffectMessage.EFFECT_FINISHED, 
+                                          EffectMessage.EFFECT_FINISHED,
                                           (mID[i]<<EffectTypes.LENGTH)+EffectTypes.FRAGMENT.type,
                                           mType[i], 
                                           mBitmapID,
diff --git a/src/main/java/org/distorted/library/EffectQueueMatrix.java b/src/main/java/org/distorted/library/EffectQueueMatrix.java
index 6d303c7..e923342 100644
--- a/src/main/java/org/distorted/library/EffectQueueMatrix.java
+++ b/src/main/java/org/distorted/library/EffectQueueMatrix.java
@@ -22,6 +22,7 @@ package org.distorted.library;
 import android.opengl.GLES20;
 import android.opengl.Matrix;
 
+import org.distorted.library.message.EffectMessage;
 import org.distorted.library.type.Float3D;
 import org.distorted.library.type.Interpolator;
 import org.distorted.library.type.Interpolator1D;
@@ -109,7 +110,7 @@ class EffectQueueMatrix extends EffectQueue
         {   
         for(int j=0; j<mNumListeners; j++)   
           EffectMessageSender.newMessage( mListeners.elementAt(j),
-                                          EffectMessage.EFFECT_FINISHED, 
+                                          EffectMessage.EFFECT_FINISHED,
                                          (mID[i]<<EffectTypes.LENGTH)+EffectTypes.MATRIX.type,
                                           mType[i], 
                                           mBitmapID,
diff --git a/src/main/java/org/distorted/library/EffectQueueOther.java b/src/main/java/org/distorted/library/EffectQueueOther.java
index 97bba61..4a49cbb 100644
--- a/src/main/java/org/distorted/library/EffectQueueOther.java
+++ b/src/main/java/org/distorted/library/EffectQueueOther.java
@@ -24,6 +24,8 @@ package org.distorted.library;
 import android.graphics.Bitmap;
 import android.opengl.GLES20;
 
+import org.distorted.library.message.EffectMessage;
+
 import java.io.BufferedOutputStream;
 import java.io.FileOutputStream;
 import java.io.IOException;
diff --git a/src/main/java/org/distorted/library/EffectQueueVertex.java b/src/main/java/org/distorted/library/EffectQueueVertex.java
index 0a28285..09456b4 100644
--- a/src/main/java/org/distorted/library/EffectQueueVertex.java
+++ b/src/main/java/org/distorted/library/EffectQueueVertex.java
@@ -21,6 +21,7 @@ package org.distorted.library;
 
 import android.opengl.GLES20;
 
+import org.distorted.library.message.EffectMessage;
 import org.distorted.library.type.Float2D;
 import org.distorted.library.type.Float4D;
 import org.distorted.library.type.Interpolator;
@@ -76,7 +77,7 @@ class EffectQueueVertex extends EffectQueue
         {
         for(int j=0; j<mNumListeners; j++)   
           EffectMessageSender.newMessage( mListeners.elementAt(j),
-                                          EffectMessage.EFFECT_FINISHED, 
+                                          EffectMessage.EFFECT_FINISHED,
                                          (mID[i]<<EffectTypes.LENGTH)+EffectTypes.VERTEX.type,
                                           mType[i], 
                                           mBitmapID,
diff --git a/src/main/java/org/distorted/library/GridBitmap.java b/src/main/java/org/distorted/library/GridBitmap.java
deleted file mode 100644
index 3c14332..0000000
--- a/src/main/java/org/distorted/library/GridBitmap.java
+++ /dev/null
@@ -1,116 +0,0 @@
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Copyright 2016 Leszek Koltunski                                                               //
-//                                                                                               //
-// This file is part of Distorted.                                                               //
-//                                                                                               //
-// Distorted 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.                                                           //
-//                                                                                               //
-// Distorted 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 Distorted.  If not, see <http://www.gnu.org/licenses/>.                            //
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-package org.distorted.library;
-
-import java.nio.ByteBuffer;
-import java.nio.ByteOrder;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-class GridBitmap extends GridObject
-  {
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  GridBitmap(int xLength, int yLength)
-    {
-    dataLength = 2*xLength*(yLength-1)+2*(yLength-2); // (yLength-1) strips, 2*xLength triangles in each, plus 2 degenerate triangles per each of (yLength-2) joins 
-    final short[] indexData = new short[dataLength];
-    
-    int offset=0;    
-    for(int y=0; y<yLength-1; y++) 
-      {
-      if (y>0) indexData[offset++] = (short) (y*xLength); // Degenerate begin: repeat first vertex
-
-      for (int x = 0; x < xLength; x++) 
-        {
-        indexData[offset++] = (short) (( y   *xLength)+x);
-        indexData[offset++] = (short) (((y+1)*xLength)+x);
-        }
-
-      if (y<yLength-2) indexData[offset++] = (short) (((y+1)*xLength) + (xLength-1)); // Degenerate end: repeat last vertex
-      }
-
-    // for(int g=0; g<dataLength; g++) Log.e(TAG_BACKGROUND, "index["+g+"]="+indexData[g]);
-
-    float[] bufferData= new float[COLOR_DATA_SIZE*dataLength];
-
-    offset=0;
-    for(int i=0; i<dataLength; i++)
-      {
-      bufferData[offset++] = 1.0f; // r
-      bufferData[offset++] = 1.0f; // g
-      bufferData[offset++] = 1.0f; // b
-      bufferData[offset++] = 1.0f; // a
-      }
-    mGridColors = ByteBuffer.allocateDirect(COLOR_DATA_SIZE*dataLength*BYTES_PER_FLOAT).order(ByteOrder.nativeOrder()).asFloatBuffer();                                                        
-    mGridColors.put(bufferData).position(0); 
-
-    bufferData = new float[NORMAL_DATA_SIZE*dataLength];
-
-    offset=0;
-    for(int i=0; i<dataLength; i++)
-      {
-      bufferData[offset++] = 0.0f; // x
-      bufferData[offset++] = 0.0f; // y
-      bufferData[offset++] = 1.0f; // z
-      }
-    mGridNormals = ByteBuffer.allocateDirect(NORMAL_DATA_SIZE*dataLength*BYTES_PER_FLOAT).order(ByteOrder.nativeOrder()).asFloatBuffer();                                                        
-    mGridNormals.put(bufferData).position(0); 
-
-    float tmpx,tmpy,tmpz;
-    bufferData = new float[TEX_DATA_SIZE*dataLength];
-
-    offset=0;
-    for(int i=0; i<dataLength; i++)
-      {
-      tmpx = ((float)(indexData[offset/2]%xLength))/(xLength-1);
-      tmpy = ((float)(indexData[offset/2]/xLength))/(yLength-1);
-
-      bufferData[offset++] = tmpx; // s=x
-      bufferData[offset++] = tmpy; // t=y
-      }
-    mGridTexture = ByteBuffer.allocateDirect(TEX_DATA_SIZE*dataLength*BYTES_PER_FLOAT).order(ByteOrder.nativeOrder()).asFloatBuffer();                                                        
-    mGridTexture.put(bufferData).position(0); 
-
-    //for(int g=0; g<dataLength; g++) Log.e(TAG_BACKGROUND, "tex["+g+"]=("+bufferData[2*g]+","+bufferData[2*g+1]+")");
-    //Log.e(TAG, "regWidth="+(2*mRegW)+" regHeight="+(2*mRegH)+" xLength="+xLength+" yLength="+yLength);
-
-    offset=0;
-    bufferData= new float[POSITION_DATA_SIZE*dataLength];
-
-    for(int i=0; i<dataLength; i++)
-      {
-      tmpx = ((float)(indexData[offset/3]%xLength))/(xLength-1);
-      tmpy = ((float)(indexData[offset/3]/xLength))/(yLength-1);
-      tmpz = 0;
-
-      bufferData[offset++] = (tmpx-0.5f); // x
-      bufferData[offset++] = (0.5f-tmpy); // y
-      bufferData[offset++] =        tmpz; // z
-      }
-    mGridPositions = ByteBuffer.allocateDirect(POSITION_DATA_SIZE*dataLength*BYTES_PER_FLOAT).order(ByteOrder.nativeOrder()).asFloatBuffer();                                                        
-    mGridPositions.put(bufferData).position(0); 
-
-    //for(int g=0; g<dataLength; g++) android.util.Log.e("BACKGROUND", "pos["+g+"]=("+bufferData[3*g]+","+bufferData[3*g+1]+")");
-    }
-  };
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
diff --git a/src/main/java/org/distorted/library/GridCubes.java b/src/main/java/org/distorted/library/GridCubes.java
deleted file mode 100644
index 9b02bee..0000000
--- a/src/main/java/org/distorted/library/GridCubes.java
+++ /dev/null
@@ -1,758 +0,0 @@
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Copyright 2016 Leszek Koltunski                                                               //
-//                                                                                               //
-// This file is part of Distorted.                                                               //
-//                                                                                               //
-// Distorted 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.                                                           //
-//                                                                                               //
-// Distorted 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 Distorted.  If not, see <http://www.gnu.org/licenses/>.                            //
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-package org.distorted.library;
-
-import java.nio.ByteBuffer;
-import java.nio.ByteOrder;
-import java.util.ArrayList;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-class GridCubes extends GridObject
-   {
-   private static final float R = 0.2f;
-   private static final float FRONTZ = 0.5f;
-   private static final float BACKZ  =-0.5f;
-   
-   private static final int NORTH = 0;
-   private static final int WEST  = 1;
-   private static final int EAST  = 2;
-   private static final int SOUTH = 3;
-   
-   private static final boolean BACK  = true;
-   private static final boolean FRONT = false;
-   private static final boolean UPPER = false;
-   private static final boolean LOWER = true;
-   
-   private static final float[] mNormalX = new float[4];
-   private static final float[] mNormalY = new float[4];
-   
-   private class Edge
-     {
-     final int side; 
-     final int row;
-     final int col;
-     
-     public Edge(int s, int r, int c)
-       {
-       side= s; 
-       row = r;
-       col = c;
-       }
-     };
-   
-   private int frontVert, sideVert;
-   private int mCols, mRows;
-   private short[][] mCubes;
-   private ArrayList<Edge> mEdges = new ArrayList<Edge>();
-   
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-   private int computeDataLength(boolean frontOnly)
-      {
-      int frontWalls=0, frontSegments=0, sideWalls=0, sideBends=0;
-      
-      for(int i=0; i<mRows; i++)
-         for(int j=0; j<mCols; j++)
-            {
-            if( mCubes[i][j]%2 == 1 )  // land
-              {
-              frontWalls++;
-              if( j==mCols-1 || mCubes[i][j+1]%2 == 0 ) frontSegments++;
-              }
-              
-            if( (i==0 && mCubes[i][j]!=2) || (i!=0 && mCubes[i][j] != mCubes[i-1][j  ]) ) sideWalls++; // up
-            if( (j==0 && mCubes[i][j]!=2) || (j!=0 && mCubes[i][j] != mCubes[i  ][j-1]) ) sideWalls++; // left
-            if( i==mRows-1 && mCubes[i][j]!=2                                           ) sideWalls++; // bottom
-            if( j==mCols-1 && mCubes[i][j]!=2                                           ) sideWalls++; // right
-            }
-
-      int edges= mEdges.size();
-      
-      for(int i=0; i<edges; i++) 
-        {
-        Edge curr = mEdges.get(i);
-        Edge next = getNextEdge(curr);
-        int startX = curr.col;
-        int startY = curr.row;
-        int startS = curr.side;
-        
-        do
-          {
-          if( next.side != curr.side ) sideBends++; 
-          curr  = next; 
-          next = getNextEdge(curr);
-          }
-        while( curr.col!=startX || curr.row!=startY || curr.side!=startS );
-        }
-      
-      frontVert = 2*( frontWalls + 2*frontSegments - 1);
-      sideVert  = 2*( sideWalls + sideBends + edges -1);
-      
-      int dataL = frontOnly ? frontVert : (frontVert+1) + (1+sideVert+1) + (1+frontVert);
-      
-      android.util.Log.e("CUBES","frontVert="+frontVert+" sideVert="+sideVert);
-      android.util.Log.e("CUBES", "frontW="+frontWalls+" fSegments="+frontSegments+" sWalls="+sideWalls+" sSegments="+edges+" sideBends="+sideBends+" dataLen="+dataL );
-      
-      return dataL<0 ? 0:dataL;
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/*
-   private static String debug(short[] val)
-     {
-     String ret="";
-     
-     for(int i=0; i<val.length; i++) ret+=(" "+val[i]); 
-     
-     return ret;
-     }
-*/
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/*
-   private static String debug(float[] val, int stop)
-     {
-     String ret="";
-
-     for(int i=0; i<val.length; i++) 
-        {
-        if( i%stop==0 ) ret+="\n";
-        ret+=(" "+val[i]);
-        }
-
-     return ret;
-     }
-*/  
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/*
-   private static String debug(Edge e)
-     {
-     String d = "";
-     
-     switch(e.side)
-       {
-       case NORTH: d+="NORTH "; break;
-       case SOUTH: d+="SOUTH "; break;
-       case WEST : d+="WEST  "; break;
-       case EAST : d+="EAST  "; break;
-       }
-     
-     d+=("("+e.row+","+e.col+")");
-     
-     return d;
-     }   
-*/ 
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-   private void buildGrid(int cols, String desc, boolean frontOnly)
-     {
-     mRows     =0;
-     mCols     =0;
-     dataLength=0;
-     
-     if( cols>0 )
-       {
-       int reallen = desc.length();
-       int len = reallen;
-
-       if( (reallen/cols)*cols != reallen )
-         {
-         len = ((reallen/cols)+1)*cols; 
-         for(int i=reallen; i<len; i++) desc += "0";
-         }
-    
-       if( desc.indexOf("1")>=0 )
-         {
-         mCols = cols;
-         mRows = len/cols;
-
-         mCubes = new short[mRows][mCols];
-       
-         for(int j=0; j<mCols; j++) 
-           for(int i=0; i<mRows; i++)
-             mCubes[i][j] = (short)(desc.charAt(i*mCols+j) == '1' ? 1:0); 
-       
-         markRegions();
-         dataLength = computeDataLength(frontOnly);
-         }
-       }
-     }
- 
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Mark all the 'regions' of our grid  - i.e. separate pieces of 'land' (connected blocks that will 
-// be rendered) and 'water' (connected holes in between) with integers. Each connected block of land
-// gets a unique odd integer, each connected block of water a unique even integer.
-//
-// Water on the edges of the grid is also considered connected to itself!   
-//   
-// This function also creates a list of 'Edges'. Each Edge is a data structure from which later on we
-// will start building the side walls of each connected black of land (and sides of holes of water 
-// inside)   
-   
-   private void markRegions()
-     {
-     int i, j, numWater=1, numLand=0;
-     
-     for(i=0; i<mRows;i++) if( mCubes[      i][      0]==0 ) markRegion((short)2,      i,       0);
-     for(i=0; i<mRows;i++) if( mCubes[      i][mCols-1]==0 ) markRegion((short)2,      i, mCols-1);
-     for(i=0; i<mCols;i++) if( mCubes[0      ][      i]==0 ) markRegion((short)2,      0,       i);
-     for(i=0; i<mCols;i++) if( mCubes[mRows-1][      i]==0 ) markRegion((short)2,mRows-1,       i);
-           
-     for(i=0; i<mRows; i++)
-        for(j=0; j<mCols; j++)
-           {
-           if( mCubes[i][j] == 0 ) { numWater++; markRegion( (short)(2*numWater ),i,j); mEdges.add(new Edge(NORTH,i,j)); }
-           if( mCubes[i][j] == 1 ) { numLand ++; markRegion( (short)(2*numLand+1),i,j); mEdges.add(new Edge(NORTH,i,j)); }
-           }
-     
-     // now we potentially need to kick out some Edges - precisely the edges with water inside -
-     // which are surrounded by more than one type of land. Otherwise the following does not work:
-     //
-     // 0 1 0
-     // 1 0 1
-     // 0 1 0
-     //
-     // The 'water inside' edges that did not get kicked out by this procedure need to be transformed
-     // with Edge(NORTH,row,col) -> Edge(SOUTH,row-1,col) so that later on normals work correctly
-     // (Edge always needs to point out from land to water for that)
-     
-     int numEdges= mEdges.size();
-     short initLand;
-     int initCol, initRow;
-     boolean kicked;
-     Edge e;
-     
-     for(i=0; i<numEdges; i++) 
-       {
-       e = mEdges.get(i);
-       initRow= e.row;
-       initCol= e.col;
-         
-       //android.util.Log.e("CUBES", "checking edge "+debug(e));
-             
-       if( mCubes[initRow][initCol]%2==0 )
-         {
-         kicked = false; 
-         initLand = mCubes[initRow-1][initCol];
-         
-         do
-           {
-           e = getNextEdge(e); 
-           //android.util.Log.e("CUBES", " next edge "+debug(e));   
-       
-           switch(e.side)
-             {
-             case NORTH: if( initLand!=mCubes[e.row-1][e.col  ] ) kicked=true; break;
-             case SOUTH: if( initLand!=mCubes[e.row+1][e.col  ] ) kicked=true; break;
-             case WEST:  if( initLand!=mCubes[e.row  ][e.col-1] ) kicked=true; break;
-             case EAST:  if( initLand!=mCubes[e.row  ][e.col+1] ) kicked=true; break;
-             }
-           
-           if( kicked )
-             {
-             //android.util.Log.e("CUBES", "kicking out edge!");
-             mEdges.remove(i);
-             i--;
-             numEdges--; 
-             }
-           }
-         while( kicked==false && (e.col!=initCol || e.row!=initRow || e.side!=NORTH) );
-         
-         if( kicked==false )
-           {
-           mEdges.set(i, new Edge(SOUTH,e.row-1,e.col)); 
-           }
-         }
-       }
-     }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// when calling, make sure that newVal != val
-   
-   private void markRegion(short newVal, int row, int col)
-     {
-     short val = mCubes[row][col];
-     mCubes[row][col] = newVal;
-     
-     if( row>0       && mCubes[row-1][col  ]==val ) markRegion(newVal, row-1, col  );
-     if( row<mRows-1 && mCubes[row+1][col  ]==val ) markRegion(newVal, row+1, col  );
-     if( col>0       && mCubes[row  ][col-1]==val ) markRegion(newVal, row  , col-1);
-     if( col<mCols-1 && mCubes[row  ][col+1]==val ) markRegion(newVal, row  , col+1);
-     }
-   
-///////////////////////////////////////////////////////////////////////////////////////////////////
-   
-   private void createNormals(int row, int col)
-     {
-     int td,lr; 
-      
-	  int nw = (col>0       && row>0      ) ? (mCubes[row-1][col-1]%2) : 0;
-	  int w  = (col>0                     ) ? (mCubes[row  ][col-1]%2) : 0;
-	  int n  = (               row>0      ) ? (mCubes[row-1][col  ]%2) : 0;
-	  int c  =                                (mCubes[row  ][col  ]%2);
-	  int sw = (col>0       && row<mRows-1) ? (mCubes[row+1][col-1]%2) : 0;
-     int s  = (               row<mRows-1) ? (mCubes[row+1][col  ]%2) : 0;
-     int ne = (col<mCols-1 && row>0      ) ? (mCubes[row-1][col+1]%2) : 0;
-     int e  = (col<mCols-1               ) ? (mCubes[row  ][col+1]%2) : 0;
-     int se = (col<mCols-1 && row<mRows-1) ? (mCubes[row+1][col+1]%2) : 0;
-     
-     td = nw+n-w-c;
-     lr = c+n-w-nw;
-     if( td<0 ) td=-1;
-     if( td>0 ) td= 1;
-     if( lr<0 ) lr=-1;
-     if( lr>0 ) lr= 1;
-     mNormalX[0] = lr*R;
-     mNormalY[0] = td*R;
-     
-     td = w+c-sw-s;
-     lr = c+s-w-sw;
-     if( td<0 ) td=-1;
-     if( td>0 ) td= 1;
-     if( lr<0 ) lr=-1;
-     if( lr>0 ) lr= 1;
-     mNormalX[1] = lr*R;
-     mNormalY[1] = td*R;
-     
-     td = n+ne-c-e;
-     lr = e+ne-c-n;
-     if( td<0 ) td=-1;
-     if( td>0 ) td= 1;
-     if( lr<0 ) lr=-1;
-     if( lr>0 ) lr= 1;
-     mNormalX[2] = lr*R;
-     mNormalY[2] = td*R;
-     
-     td = c+e-s-se;
-     lr = e+se-c-s;
-     if( td<0 ) td=-1;
-     if( td>0 ) td= 1;
-     if( lr<0 ) lr=-1;
-     if( lr>0 ) lr= 1;
-     mNormalX[3] = lr*R;
-     mNormalY[3] = td*R;
-     /*
-	  android.util.Log.d("CUBES", "row="+row+" col="+col);
-	  android.util.Log.d("CUBES", mNormalX[0]+" "+mNormalY[0]);
-	  android.util.Log.d("CUBES", mNormalX[1]+" "+mNormalY[1]);
-	  android.util.Log.d("CUBES", mNormalX[2]+" "+mNormalY[2]);
-	  android.util.Log.d("CUBES", mNormalX[3]+" "+mNormalY[3]);
-     */
-     }
-   
-///////////////////////////////////////////////////////////////////////////////////////////////////
-   
-   private int buildFrontBackGrid(boolean front, int vertex, float[] position, float[] normal, float[] texture)
-     {
-     short last, current;
-     boolean seenland=false;
-     float centerX, centerY;
-    
-     for(int i=0; i<mRows; i++)
-       {
-       last =0;
-         
-       for(int j=0; j<mCols; j++)
-         {
-         current = mCubes[i][j];
-            
-         if( current%2 == 1 )
-           {
-           centerX = j-(mCols-1.0f)/2.0f;
-           centerY = (mRows-1.0f)/2.0f-i;
-      
-           createNormals(i,j);
-          
-           if( last != current )
-             {
-             if( seenland ) vertex = repeatLast(vertex,position,normal,texture);    
-
-             if( front ) // NW corner
-               {
-               position[3*vertex+0] = (centerX-0.5f)/mCols;
-               position[3*vertex+1] = (centerY+0.5f)/mRows;
-               position[3*vertex+2] = FRONTZ;
-               normal[3*vertex+0]   = mNormalX[0];
-               normal[3*vertex+1]   = mNormalY[0];
-               normal[3*vertex+2]   = 1.0f;
-               texture[2*vertex+0]  = (float)j/mCols;
-               texture[2*vertex+1]  = (float)i/mRows;     
-               vertex++;
-               }
-             else  // SW corner
-               { 
-               position[3*vertex+0] = (centerX-0.5f)/mCols;
-               position[3*vertex+1] = (centerY-0.5f)/mRows; 
-               position[3*vertex+2] = BACKZ; 
-               normal[3*vertex+0]   = mNormalX[1];
-               normal[3*vertex+1]   = mNormalY[1];
-               normal[3*vertex+2]   =-1.0f;
-               texture[2*vertex+0]  = (float)j/mCols;
-               texture[2*vertex+1]  = (float)(i+1)/mRows;
-               vertex++;
-               
-               if( !seenland ) vertex = repeatLast(vertex,position,normal,texture);   //  if drawing the back, repeat the very first vertex
-               }
-             
-             if( seenland ) vertex = repeatLast(vertex,position,normal,texture);    
-
-             if( front ) // SW corner
-               {
-               position[3*vertex+0] = (centerX-0.5f)/mCols;
-               position[3*vertex+1] = (centerY-0.5f)/mRows; 
-               position[3*vertex+2] = FRONTZ; 
-               normal[3*vertex+0]   = mNormalX[1];
-               normal[3*vertex+1]   = mNormalY[1];
-               normal[3*vertex+2]   = 1.0f;
-               texture[2*vertex+0]  = (float)j/mCols;
-               texture[2*vertex+1]  = (float)(i+1)/mRows;
-               vertex++; 
-               }
-             else  // NW corner
-               {
-               position[3*vertex+0] = (centerX-0.5f)/mCols;
-               position[3*vertex+1] = (centerY+0.5f)/mRows;
-               position[3*vertex+2] = BACKZ;
-               normal[3*vertex+0]   = mNormalX[0];
-               normal[3*vertex+1]   = mNormalY[0];
-               normal[3*vertex+2]   =-1.0f;
-               texture[2*vertex+0]  = (float)j/mCols;
-               texture[2*vertex+1]  = (float)i/mRows;     
-               vertex++; 
-               }
-             }
-              
-           if( front )  // NE corner
-             {
-             position[3*vertex+0] = (centerX+0.5f)/mCols;
-             position[3*vertex+1] = (centerY+0.5f)/mRows;
-             position[3*vertex+2] = FRONTZ; 
-             normal[3*vertex+0]   = mNormalX[2];
-             normal[3*vertex+1]   = mNormalY[2];
-             normal[3*vertex+2]   = 1.0f;
-             texture[2*vertex+0]  = (float)(j+1)/mCols;
-             texture[2*vertex+1]  = (float)i/mRows;
-             vertex++;
-             }
-           else // SE corner
-             {
-             position[3*vertex+0] = (centerX+0.5f)/mCols;
-             position[3*vertex+1] = (centerY-0.5f)/mRows;
-             position[3*vertex+2] = BACKZ; 
-             normal[3*vertex+0]   = mNormalX[3];
-             normal[3*vertex+1]   = mNormalY[3];
-             normal[3*vertex+2]   =-1.0f;
-             texture[2*vertex+0]  = (float)(j+1)/mCols;
-             texture[2*vertex+1]  = (float)(i+1)/mRows;
-             vertex++; 
-             }
-           
-           if( front )  // SE corner
-             {
-             position[3*vertex+0] = (centerX+0.5f)/mCols;
-             position[3*vertex+1] = (centerY-0.5f)/mRows;
-             position[3*vertex+2] = FRONTZ; 
-             normal[3*vertex+0]   = mNormalX[3];
-             normal[3*vertex+1]   = mNormalY[3];
-             normal[3*vertex+2]   = 1.0f;
-             texture[2*vertex+0]  = (float)(j+1)/mCols;
-             texture[2*vertex+1]  = (float)(i+1)/mRows;
-             vertex++;
-             }
-           else // NE corner
-             {
-             position[3*vertex+0] = (centerX+0.5f)/mCols;
-             position[3*vertex+1] = (centerY+0.5f)/mRows;
-             position[3*vertex+2] = BACKZ; 
-             normal[3*vertex+0]   = mNormalX[2];
-             normal[3*vertex+1]   = mNormalY[2];
-             normal[3*vertex+2]   =-1.0f;
-             texture[2*vertex+0]  = (float)(j+1)/mCols;
-             texture[2*vertex+1]  = (float)i/mRows;
-             vertex++; 
-             }
-           
-           seenland = true;
-           }
-            
-         last = current;
-         }
-       }
-     
-     return vertex;
-     }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-   private int repeatLast(int vertex, float[] position, float[] normal, float[] texture)
-     {
-     if( vertex>0 )
-       {
-       position[3*vertex+0] = position[3*vertex-3]; 
-       position[3*vertex+1] = position[3*vertex-2];
-       position[3*vertex+2] = position[3*vertex-1];
-
-       normal[3*vertex+0]   = normal[3*vertex-3]; 
-       normal[3*vertex+1]   = normal[3*vertex-2];
-       normal[3*vertex+2]   = normal[3*vertex-1];
-
-       texture[2*vertex+0]  = texture[2*vertex-2];
-       texture[2*vertex+1]  = texture[2*vertex-1];
-         
-       vertex++;     
-       }
-     
-     return vertex;
-     }
-   
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-   private int buildSideGrid(int vertex, float[] position, float[] normal, float[] texture)
-     {
-     int edges= mEdges.size();
-     
-     for(int i=0; i<edges; i++) 
-       {
-       vertex = buildIthSide(mEdges.get(i), vertex, position, normal, texture);  
-       } 
-      
-     return vertex;
-     }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-   private int buildIthSide(Edge curr, int vertex, float[] position, float[] normal, float[] texture)
-     {
-     Edge prev; 
-     
-     if( curr.side==NORTH ) // water outside
-       {
-       prev = new Edge(WEST,curr.row,curr.col);
-       }
-     else                   // land outside; we need to move forward one link because we are going in opposite direction and we need to start from a bend.
-       {
-       prev = curr;
-       curr = new Edge(EAST,curr.row+1,curr.col-1);
-       }
-     
-     int col = curr.col;
-     int row = curr.row;
-     int side= curr.side;  
-     Edge next = getNextEdge(curr);
-     
-     addVertex(curr,BACK,LOWER,prev.side,vertex,position,normal,texture);
-     vertex++;
-     
-     do
-       {
-       if( prev.side!=curr.side )
-         {
-         addVertex(curr,BACK,LOWER,prev.side,vertex,position,normal,texture);
-         vertex++;
-         addVertex(curr,BACK,UPPER,prev.side,vertex,position,normal,texture);
-         vertex++;
-         }
-       
-       addVertex(curr,FRONT,LOWER,next.side,vertex,position,normal,texture);
-       vertex++;
-       addVertex(curr,FRONT,UPPER,next.side,vertex,position,normal,texture);
-       vertex++;
-       
-       prev = curr;
-       curr = next; 
-       next = getNextEdge(curr);
-       }
-     while( curr.col!=col || curr.row!=row || curr.side!=side );
-     
-     vertex = repeatLast(vertex,position,normal,texture);
-     
-     return vertex;
-     }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-   private Edge getNextEdge(Edge curr)
-     {
-     int col = curr.col;
-     int row = curr.row;
-      
-     //android.util.Log.e("CUBES", "row="+row+" col="+col+" mRows="+mRows+" mCols="+mCols);
-                       
-     switch(curr.side) 
-       {
-       case NORTH: if( col==mCols-1 ) 
-                     return new Edge(EAST,row,col);
-                   if( row>0 && mCubes[row-1][col+1]==mCubes[row][col] )
-                     return new Edge(WEST,row-1,col+1);
-                   if( mCubes[row][col+1]==mCubes[row][col] )
-                     return new Edge(NORTH,row,col+1);
-                   else  
-                     return new Edge(EAST,row,col);
-                   
-       case SOUTH: if( col==0 ) 
-                     return new Edge(WEST,row,col);
-                   if( (row<mRows-1) && mCubes[row+1][col-1]==mCubes[row][col] )
-                     return new Edge(EAST,row+1,col-1); 
-                   if( mCubes[row][col-1]==mCubes[row][col] )
-                     return new Edge(SOUTH,row,col-1);
-                   else
-                     return new Edge(WEST,row,col); 
-                     
-       case EAST : if( row==mRows-1 ) 
-                     return new Edge(SOUTH,row,col);
-                   if( (col<mCols-1) && mCubes[row+1][col+1]==mCubes[row][col] )
-                     return new Edge(NORTH,row+1,col+1);
-                   if( mCubes[row+1][col]==mCubes[row][col] )
-                     return new Edge(EAST,row+1,col);
-                   else 
-                     return new Edge(SOUTH,row,col);
-                   
-       case WEST : if( row==0 )
-                     return new Edge(NORTH,row,col);
-                   if( col>0 && mCubes[row-1][col-1]==mCubes[row][col] )
-                     return new Edge(SOUTH,row-1,col-1);
-                   if( mCubes[row-1][col]==mCubes[row][col] )
-                     return new Edge(WEST,row-1,col);
-                   else
-                     return new Edge(NORTH,row,col);     
-       }
-     
-     return null;
-     }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-   
-   private void addVertex(Edge curr, boolean back, boolean lower,int side, int vertex, float[] position, float[] normal, float[] texture)
-     {
-     float centerX = curr.col-(mCols-1.0f)/2.0f;
-     float centerY = (mRows-1.0f)/2.0f-curr.row;
-  
-     switch(curr.side)
-       {
-       case NORTH: position[3*vertex+0] = (back ? (centerX-0.5f) : (centerX+0.5f))/mCols; 
-                   position[3*vertex+1] = (centerY+0.5f)/mRows;
-                   position[3*vertex+2] = lower ? BACKZ : FRONTZ;
-
-                   normal[3*vertex+0]   = side==NORTH ? 0.0f : (side==WEST?-R:R);
-                   normal[3*vertex+1]   = 1.0f;
-                   normal[3*vertex+2]   = lower ? -R:R;
-
-                   texture[2*vertex+0]  = (float)(back ? (curr.col  ):(curr.col+1))/mCols;
-                   texture[2*vertex+1]  = (float)(lower? (curr.row-1):(curr.row  ))/mRows;  
-                   break;
-       case SOUTH: position[3*vertex+0] = (back ? (centerX+0.5f) : (centerX-0.5f))/mCols;
-                   position[3*vertex+1] = (centerY-0.5f)/mRows;
-                   position[3*vertex+2] = lower ? BACKZ : FRONTZ;  
-            
-                   normal[3*vertex+0]   = side==SOUTH ? 0.0f: (side==EAST?-R:R);
-                   normal[3*vertex+1]   =-1.0f;
-                   normal[3*vertex+2]   = lower ? -R:R;
-
-                   texture[2*vertex+0]  = (float)(back ? (curr.col+1):(curr.col  ))/mCols;
-                   texture[2*vertex+1]  = (float)(lower? (curr.row+2):(curr.row+1))/mRows;
-                   break;
-       case WEST : position[3*vertex+0] = (centerX-0.5f)/mCols;
-                   position[3*vertex+1] = (back ? (centerY-0.5f):(centerY+0.5f))/mRows;
-                   position[3*vertex+2] = lower ? BACKZ : FRONTZ;
-
-                   normal[3*vertex+0]   =-1.0f;
-                   normal[3*vertex+1]   = side==WEST ? 0.0f : (side==NORTH?-R:R);
-                   normal[3*vertex+2]   = lower ? -R:R;
- 
-                   texture[2*vertex+0]  = (float)(lower ? (curr.col-1):(curr.col  ))/mCols;
-                   texture[2*vertex+1]  = (float)(back  ? (curr.row+1):(curr.row  ))/mRows;
-                   break;
-       case EAST : position[3*vertex+0] = (centerX+0.5f)/mCols;
-                   position[3*vertex+1] = (back ? (centerY+0.5f):(centerY-0.5f))/mRows;
-                   position[3*vertex+2] = lower ? BACKZ : FRONTZ;
-
-                   normal[3*vertex+0]   = 1.0f;
-                   normal[3*vertex+1]   = side==EAST ? 0.0f : (side==SOUTH?-R:R);
-                   normal[3*vertex+2]   = lower ? -R:R; 
-
-                   texture[2*vertex+0]  = (float)(lower ? (curr.col+2):(curr.col+1))/mCols;
-                   texture[2*vertex+1]  = (float)(back  ? (curr.row  ):(curr.row+1))/mRows;
-                   break;
-       }
-     
-     if(texture[2*vertex+0]>1.0f) texture[2*vertex+0] =2.0f-texture[2*vertex+0];
-     if(texture[2*vertex+0]<0.0f) texture[2*vertex+0] =    -texture[2*vertex+0];
-     if(texture[2*vertex+1]>1.0f) texture[2*vertex+1] =2.0f-texture[2*vertex+1];
-     if(texture[2*vertex+1]<0.0f) texture[2*vertex+1] =    -texture[2*vertex+1];
-     }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// PUBLIC API
-///////////////////////////////////////////////////////////////////////////////////////////////////
-   
-/**
- * Creates the underlying grid of vertices, normals, texture coords and colors.
- *    
- * @param rows See {@link DistortedCubes#DistortedCubes(String)} 
- * @param desc See {@link DistortedCubes#DistortedCubes(String)}
- */
-   public GridCubes(int cols, String desc, boolean frontOnly) 
-      {
-      buildGrid(cols,desc,frontOnly);
-       
-      int numVertices=0;
-      float[] colorData   = new float[COLOR_DATA_SIZE   *dataLength];
-      float[] positionData= new float[POSITION_DATA_SIZE*dataLength];
-      float[] normalData  = new float[NORMAL_DATA_SIZE  *dataLength];
-      float[] textureData = new float[TEX_DATA_SIZE     *dataLength];
-      
-      for(int i=0; i<dataLength; i++)
-        {
-        colorData[COLOR_DATA_SIZE*i+0] = 1.0f; // r
-        colorData[COLOR_DATA_SIZE*i+1] = 1.0f; // g
-        colorData[COLOR_DATA_SIZE*i+2] = 1.0f; // b
-        colorData[COLOR_DATA_SIZE*i+3] = 1.0f; // a
-        }
-
-      numVertices = buildFrontBackGrid(true, numVertices,positionData,normalData,textureData);
-      
-      if( !frontOnly )
-        {
-        numVertices = repeatLast(numVertices,positionData,normalData,textureData);
-        numVertices = buildSideGrid (numVertices,positionData,normalData,textureData);
-        numVertices = buildFrontBackGrid (false,numVertices,positionData,normalData,textureData);
-        }
-      
-      /*
-      android.util.Log.e("CUBES","dataLen="+dataLength+" vertex="+numVertices);
-      android.util.Log.d("CUBES", "position: "+debug(positionData,3) );
-      android.util.Log.d("CUBES", "normal: "  +debug(  normalData,3) );
-      android.util.Log.d("CUBES", "texture: " +debug( textureData,2) );
-      */
-      mGridColors = ByteBuffer.allocateDirect(COLOR_DATA_SIZE*dataLength*BYTES_PER_FLOAT).order(ByteOrder.nativeOrder()).asFloatBuffer();                                                        
-      mGridColors.put(colorData).position(0); 
-
-      mGridPositions = ByteBuffer.allocateDirect(POSITION_DATA_SIZE*dataLength*BYTES_PER_FLOAT).order(ByteOrder.nativeOrder()).asFloatBuffer();                                                        
-      mGridPositions.put(positionData).position(0); 
-      
-      mGridNormals = ByteBuffer.allocateDirect(NORMAL_DATA_SIZE*dataLength*BYTES_PER_FLOAT).order(ByteOrder.nativeOrder()).asFloatBuffer();                                                        
-      mGridNormals.put(normalData).position(0); 
-
-      mGridTexture = ByteBuffer.allocateDirect(TEX_DATA_SIZE*dataLength*BYTES_PER_FLOAT).order(ByteOrder.nativeOrder()).asFloatBuffer();                                                        
-      mGridTexture.put(textureData).position(0); 
-      }
-   }
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
diff --git a/src/main/java/org/distorted/library/GridObject.java b/src/main/java/org/distorted/library/GridObject.java
deleted file mode 100644
index 7135adc..0000000
--- a/src/main/java/org/distorted/library/GridObject.java
+++ /dev/null
@@ -1,53 +0,0 @@
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Copyright 2016 Leszek Koltunski                                                               //
-//                                                                                               //
-// This file is part of Distorted.                                                               //
-//                                                                                               //
-// Distorted 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.                                                           //
-//                                                                                               //
-// Distorted 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 Distorted.  If not, see <http://www.gnu.org/licenses/>.                            //
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-package org.distorted.library;
-
-import java.nio.FloatBuffer;
-
-import android.opengl.GLES20;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-abstract class GridObject 
-   {
-   protected static final int BYTES_PER_FLOAT   = 4; //
-   protected static final int POSITION_DATA_SIZE= 3; // Size of the position data in elements
-   protected static final int COLOR_DATA_SIZE   = 4; // Size of the color data in elements 
-   protected static final int NORMAL_DATA_SIZE  = 3; // Size of the normal data in elements.
-   protected static final int TEX_DATA_SIZE     = 2; // Size of the texture coordinate data in elements. 
-
-   protected int dataLength;                       
-      
-   protected FloatBuffer mGridPositions,mGridColors,mGridNormals,mGridTexture;
- 
-///////////////////////////////////////////////////////////////////////////////////////////////////
-   
-   void draw()
-     { 
-     GLES20.glVertexAttribPointer(Distorted.mPositionH    , POSITION_DATA_SIZE, GLES20.GL_FLOAT, false, 0, mGridPositions);          
-     GLES20.glVertexAttribPointer(Distorted.mColorH       , COLOR_DATA_SIZE   , GLES20.GL_FLOAT, false, 0, mGridColors   );   
-     GLES20.glVertexAttribPointer(Distorted.mNormalH      , NORMAL_DATA_SIZE  , GLES20.GL_FLOAT, false, 0, mGridNormals  );
-     GLES20.glVertexAttribPointer(Distorted.mTextureCoordH, TEX_DATA_SIZE     , GLES20.GL_FLOAT, false, 0, mGridTexture  );  
-
-     GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, dataLength); 
-     }
-   }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
diff --git a/src/main/java/org/distorted/library/message/EffectListener.java b/src/main/java/org/distorted/library/message/EffectListener.java
new file mode 100644
index 0000000..864e43a
--- /dev/null
+++ b/src/main/java/org/distorted/library/message/EffectListener.java
@@ -0,0 +1,54 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2016 Leszek Koltunski                                                               //
+//                                                                                               //
+// This file is part of Distorted.                                                               //
+//                                                                                               //
+// Distorted 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.                                                           //
+//                                                                                               //
+// Distorted 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 Distorted.  If not, see <http://www.gnu.org/licenses/>.                            //
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+package org.distorted.library.message;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+import org.distorted.library.DistortedBitmap;
+import org.distorted.library.EffectNames;
+
+/**
+ * This interface lets users of the Distorted library get notified when something happens to one of the effects.
+ * To receive the notifications, we first have to register with a call to {@link DistortedBitmap#addEventListener(EffectListener)}.
+ * List of all possible events that can happen is defined in {@link EffectMessage}
+ */
+
+public interface EffectListener 
+  {
+/**
+ * Gets called when event of type 'em' happens to effect 'effectID'. 
+ * 
+ * @param eventType  Type of event that happened.
+ * @param effectID   ID of the effect the event happened to. This ID must have been previously returned by one
+ *                   of the DistortedBitmap.{deform,distort,move,...} functions.
+ * @param effectName Name of the effect as defined in EffectNames, e.g. if effectType==EffectNames.MOVE.ordinal(),
+ *                   then the event happened to a MOVE effect.
+ * @param bitmapID   the ID of the DistortedBitmap object, as returned by {@link DistortedBitmap#getID()}, this event
+ *                   happened to. If the object has been created using a copy constructor from another instance of
+ *                   DistortedBitmap, the ID here will be the one of the original object.
+ * @param message    Any message string associated with it. 'Failed' event types have one.
+ * @see EffectMessage
+ * @see EffectNames
+ */
+   
+  void effectMessage(final EffectMessage eventType, final long effectID, final int effectName, final long bitmapID, final String message);
+  }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
diff --git a/src/main/java/org/distorted/library/message/EffectMessage.java b/src/main/java/org/distorted/library/message/EffectMessage.java
new file mode 100644
index 0000000..a23698f
--- /dev/null
+++ b/src/main/java/org/distorted/library/message/EffectMessage.java
@@ -0,0 +1,64 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2016 Leszek Koltunski                                                               //
+//                                                                                               //
+// This file is part of Distorted.                                                               //
+//                                                                                               //
+// Distorted 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.                                                           //
+//                                                                                               //
+// Distorted 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 Distorted.  If not, see <http://www.gnu.org/licenses/>.                            //
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+package org.distorted.library.message;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+import org.distorted.library.DistortedBitmap;
+import org.distorted.library.type.Interpolator;
+
+/**
+* Defines all possible events a class implementing the {@link EffectListener} interface can receive.
+*/
+
+public enum EffectMessage 
+  {
+/**
+ * The effect has been removed. This can happen if:
+ * <ul>
+ * <li> someone explicitly removed the effect with a call to {@link DistortedBitmap#abortEffect(long)} (or one of the other 'abort' methods)
+ * <li> the interpolation of the effect has finished and the end result is equal to the effect's unity.
+ * </ul>    
+ */
+  EFFECT_REMOVED,
+  
+/**
+ * Interpolation of the effect has finished. 
+ * <p>
+ * If you set up an interpolated effect and set its Interpolator to do 3.5 interpolations of 1000 ms each
+ * with calls to {@link Interpolator#setCount(float)} and {@link Interpolator#setDuration(long)},
+ * then you are going to get this message exactly once after 3.5*1000 = 3500 milliseconds when the interpolation 
+ * finishes. You will never get this message if you set the effect to go on indefinitely with a call to 
+ * {@link Interpolator#setCount(float)}.
+ * <p>  
+ * If then the end effect is equal to the effect's unity, then immediately after this message you
+ * will also get a EFFECT_REMOVED message.
+ */
+  EFFECT_FINISHED,
+
+  /**
+ * The effect has failed to properly execute.
+ * <p>
+ * Currently only OTHER effects (saving to PNG file and to a MP4 movie) can fail.
+ */
+  EFFECT_FAILED
+  }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
