commit 0c52af30407546044c46ad8524ee9e2b5ea0193c
Author: Leszek Koltunski <leszek@koltunski.pl>
Date:   Tue Apr 9 21:27:18 2019 +0100

    New module: Rubik App. Initial commit.

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 00000000..796b96d1
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 00000000..c821d62c
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,28 @@
+apply plugin: 'com.android.application'
+
+android {
+    compileSdkVersion 27
+
+    defaultConfig {
+        applicationId "org.distorted.magic"
+        minSdkVersion 24
+        targetSdkVersion 27
+        versionCode 1
+        versionName "1.0"
+    }
+
+    buildTypes {
+        release {
+            minifyEnabled false
+            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+        }
+    }
+
+}
+
+dependencies {
+    implementation fileTree(dir: 'libs', include: ['*.jar'])
+
+    api project(':distorted-library')
+    implementation 'com.android.support:appcompat-v7:27.1.1'
+}
diff --git a/distorted-cube.iml b/distorted-cube.iml
new file mode 100644
index 00000000..2154dce2
--- /dev/null
+++ b/distorted-cube.iml
@@ -0,0 +1,145 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<module external.linked.project.id=":distorted-cube" external.linked.project.path="$MODULE_DIR$" external.root.project.path="$MODULE_DIR$/.." external.system.id="GRADLE" type="JAVA_MODULE" version="4">
+  <component name="FacetManager">
+    <facet type="android-gradle" name="Android-Gradle">
+      <configuration>
+        <option name="GRADLE_PROJECT_PATH" value=":distorted-cube" />
+      </configuration>
+    </facet>
+    <facet type="android" name="Android">
+      <configuration>
+        <option name="SELECTED_BUILD_VARIANT" value="debug" />
+        <option name="ASSEMBLE_TASK_NAME" value="assembleDebug" />
+        <option name="COMPILE_JAVA_TASK_NAME" value="compileDebugSources" />
+        <afterSyncTasks>
+          <task>generateDebugSources</task>
+        </afterSyncTasks>
+        <option name="ALLOW_USER_CONFIGURATION" value="false" />
+        <option name="MANIFEST_FILE_RELATIVE_PATH" value="/src/main/AndroidManifest.xml" />
+        <option name="RES_FOLDER_RELATIVE_PATH" value="/src/main/res" />
+        <option name="RES_FOLDERS_RELATIVE_PATH" value="file://$MODULE_DIR$/src/main/res;file://$MODULE_DIR$/build/generated/res/rs/debug;file://$MODULE_DIR$/build/generated/res/resValues/debug" />
+        <option name="ASSETS_FOLDER_RELATIVE_PATH" value="/src/main/assets" />
+      </configuration>
+    </facet>
+  </component>
+  <component name="NewModuleRootManager" LANGUAGE_LEVEL="JDK_1_7">
+    <output url="file://$MODULE_DIR$/build/intermediates/javac/debug/compileDebugJavaWithJavac/classes" />
+    <output-test url="file://$MODULE_DIR$/build/intermediates/javac/debugUnitTest/compileDebugUnitTestJavaWithJavac/classes" />
+    <exclude-output />
+    <content url="file://$MODULE_DIR$">
+      <sourceFolder url="file://$MODULE_DIR$/build/generated/source/apt/debug" isTestSource="false" generated="true" />
+      <sourceFolder url="file://$MODULE_DIR$/build/generated/aidl_source_output_dir/debug/compileDebugAidl/out" isTestSource="false" generated="true" />
+      <sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/debug" isTestSource="false" generated="true" />
+      <sourceFolder url="file://$MODULE_DIR$/build/generated/renderscript_source_output_dir/debug/compileDebugRenderscript/out" isTestSource="false" generated="true" />
+      <sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/debug" type="java-resource" />
+      <sourceFolder url="file://$MODULE_DIR$/build/generated/res/resValues/debug" type="java-resource" />
+      <sourceFolder url="file://$MODULE_DIR$/build/generated/source/apt/androidTest/debug" isTestSource="true" generated="true" />
+      <sourceFolder url="file://$MODULE_DIR$/build/generated/aidl_source_output_dir/debugAndroidTest/compileDebugAndroidTestAidl/out" isTestSource="true" generated="true" />
+      <sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/androidTest/debug" isTestSource="true" generated="true" />
+      <sourceFolder url="file://$MODULE_DIR$/build/generated/renderscript_source_output_dir/debugAndroidTest/compileDebugAndroidTestRenderscript/out" isTestSource="true" generated="true" />
+      <sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/androidTest/debug" type="java-test-resource" />
+      <sourceFolder url="file://$MODULE_DIR$/build/generated/res/resValues/androidTest/debug" type="java-test-resource" />
+      <sourceFolder url="file://$MODULE_DIR$/build/generated/source/apt/test/debug" isTestSource="true" generated="true" />
+      <sourceFolder url="file://$MODULE_DIR$/src/debug/res" type="java-resource" />
+      <sourceFolder url="file://$MODULE_DIR$/src/debug/resources" type="java-resource" />
+      <sourceFolder url="file://$MODULE_DIR$/src/debug/assets" type="java-resource" />
+      <sourceFolder url="file://$MODULE_DIR$/src/debug/aidl" isTestSource="false" />
+      <sourceFolder url="file://$MODULE_DIR$/src/debug/java" isTestSource="false" />
+      <sourceFolder url="file://$MODULE_DIR$/src/debug/rs" isTestSource="false" />
+      <sourceFolder url="file://$MODULE_DIR$/src/debug/shaders" isTestSource="false" />
+      <sourceFolder url="file://$MODULE_DIR$/src/androidTestDebug/res" type="java-test-resource" />
+      <sourceFolder url="file://$MODULE_DIR$/src/androidTestDebug/resources" type="java-test-resource" />
+      <sourceFolder url="file://$MODULE_DIR$/src/androidTestDebug/assets" type="java-test-resource" />
+      <sourceFolder url="file://$MODULE_DIR$/src/androidTestDebug/aidl" isTestSource="true" />
+      <sourceFolder url="file://$MODULE_DIR$/src/androidTestDebug/java" isTestSource="true" />
+      <sourceFolder url="file://$MODULE_DIR$/src/androidTestDebug/rs" isTestSource="true" />
+      <sourceFolder url="file://$MODULE_DIR$/src/androidTestDebug/shaders" isTestSource="true" />
+      <sourceFolder url="file://$MODULE_DIR$/src/testDebug/res" type="java-test-resource" />
+      <sourceFolder url="file://$MODULE_DIR$/src/testDebug/resources" type="java-test-resource" />
+      <sourceFolder url="file://$MODULE_DIR$/src/testDebug/assets" type="java-test-resource" />
+      <sourceFolder url="file://$MODULE_DIR$/src/testDebug/aidl" isTestSource="true" />
+      <sourceFolder url="file://$MODULE_DIR$/src/testDebug/java" isTestSource="true" />
+      <sourceFolder url="file://$MODULE_DIR$/src/testDebug/rs" isTestSource="true" />
+      <sourceFolder url="file://$MODULE_DIR$/src/testDebug/shaders" isTestSource="true" />
+      <sourceFolder url="file://$MODULE_DIR$/src/main/res" type="java-resource" />
+      <sourceFolder url="file://$MODULE_DIR$/src/main/resources" type="java-resource" />
+      <sourceFolder url="file://$MODULE_DIR$/src/main/assets" type="java-resource" />
+      <sourceFolder url="file://$MODULE_DIR$/src/main/aidl" isTestSource="false" />
+      <sourceFolder url="file://$MODULE_DIR$/src/main/java" isTestSource="false" />
+      <sourceFolder url="file://$MODULE_DIR$/src/main/rs" isTestSource="false" />
+      <sourceFolder url="file://$MODULE_DIR$/src/main/shaders" isTestSource="false" />
+      <sourceFolder url="file://$MODULE_DIR$/src/test/res" type="java-test-resource" />
+      <sourceFolder url="file://$MODULE_DIR$/src/test/resources" type="java-test-resource" />
+      <sourceFolder url="file://$MODULE_DIR$/src/test/assets" type="java-test-resource" />
+      <sourceFolder url="file://$MODULE_DIR$/src/test/aidl" isTestSource="true" />
+      <sourceFolder url="file://$MODULE_DIR$/src/test/java" isTestSource="true" />
+      <sourceFolder url="file://$MODULE_DIR$/src/test/rs" isTestSource="true" />
+      <sourceFolder url="file://$MODULE_DIR$/src/test/shaders" isTestSource="true" />
+      <sourceFolder url="file://$MODULE_DIR$/src/androidTest/res" type="java-test-resource" />
+      <sourceFolder url="file://$MODULE_DIR$/src/androidTest/resources" type="java-test-resource" />
+      <sourceFolder url="file://$MODULE_DIR$/src/androidTest/assets" type="java-test-resource" />
+      <sourceFolder url="file://$MODULE_DIR$/src/androidTest/aidl" isTestSource="true" />
+      <sourceFolder url="file://$MODULE_DIR$/src/androidTest/java" isTestSource="true" />
+      <sourceFolder url="file://$MODULE_DIR$/src/androidTest/rs" isTestSource="true" />
+      <sourceFolder url="file://$MODULE_DIR$/src/androidTest/shaders" isTestSource="true" />
+      <excludeFolder url="file://$MODULE_DIR$/build/generated/not_namespaced_r_class_sources" />
+      <excludeFolder url="file://$MODULE_DIR$/build/generated/source/r" />
+      <excludeFolder url="file://$MODULE_DIR$/build/intermediates/annotation_processor_list" />
+      <excludeFolder url="file://$MODULE_DIR$/build/intermediates/apk_list" />
+      <excludeFolder url="file://$MODULE_DIR$/build/intermediates/blame" />
+      <excludeFolder url="file://$MODULE_DIR$/build/intermediates/build-info" />
+      <excludeFolder url="file://$MODULE_DIR$/build/intermediates/builds" />
+      <excludeFolder url="file://$MODULE_DIR$/build/intermediates/bundle_manifest" />
+      <excludeFolder url="file://$MODULE_DIR$/build/intermediates/check_manifest_result" />
+      <excludeFolder url="file://$MODULE_DIR$/build/intermediates/compatible_screen_manifest" />
+      <excludeFolder url="file://$MODULE_DIR$/build/intermediates/incremental" />
+      <excludeFolder url="file://$MODULE_DIR$/build/intermediates/incremental-runtime-classes" />
+      <excludeFolder url="file://$MODULE_DIR$/build/intermediates/incremental-verifier" />
+      <excludeFolder url="file://$MODULE_DIR$/build/intermediates/instant-run-apk" />
+      <excludeFolder url="file://$MODULE_DIR$/build/intermediates/instant_app_manifest" />
+      <excludeFolder url="file://$MODULE_DIR$/build/intermediates/instant_run_app_info_output_file" />
+      <excludeFolder url="file://$MODULE_DIR$/build/intermediates/instant_run_main_apk_resources" />
+      <excludeFolder url="file://$MODULE_DIR$/build/intermediates/instant_run_merged_manifests" />
+      <excludeFolder url="file://$MODULE_DIR$/build/intermediates/instant_run_split_apk_resources" />
+      <excludeFolder url="file://$MODULE_DIR$/build/intermediates/javac" />
+      <excludeFolder url="file://$MODULE_DIR$/build/intermediates/jniLibs" />
+      <excludeFolder url="file://$MODULE_DIR$/build/intermediates/lint_jar" />
+      <excludeFolder url="file://$MODULE_DIR$/build/intermediates/manifest-checker" />
+      <excludeFolder url="file://$MODULE_DIR$/build/intermediates/merged_assets" />
+      <excludeFolder url="file://$MODULE_DIR$/build/intermediates/merged_manifests" />
+      <excludeFolder url="file://$MODULE_DIR$/build/intermediates/metadata_feature_manifest" />
+      <excludeFolder url="file://$MODULE_DIR$/build/intermediates/prebuild" />
+      <excludeFolder url="file://$MODULE_DIR$/build/intermediates/processed_res" />
+      <excludeFolder url="file://$MODULE_DIR$/build/intermediates/reload-dex" />
+      <excludeFolder url="file://$MODULE_DIR$/build/intermediates/res" />
+      <excludeFolder url="file://$MODULE_DIR$/build/intermediates/resources" />
+      <excludeFolder url="file://$MODULE_DIR$/build/intermediates/rs" />
+      <excludeFolder url="file://$MODULE_DIR$/build/intermediates/shader_assets" />
+      <excludeFolder url="file://$MODULE_DIR$/build/intermediates/shaders" />
+      <excludeFolder url="file://$MODULE_DIR$/build/intermediates/signing_config" />
+      <excludeFolder url="file://$MODULE_DIR$/build/intermediates/split-apk" />
+      <excludeFolder url="file://$MODULE_DIR$/build/intermediates/symbols" />
+      <excludeFolder url="file://$MODULE_DIR$/build/intermediates/transforms" />
+      <excludeFolder url="file://$MODULE_DIR$/build/intermediates/validate_signing_config" />
+      <excludeFolder url="file://$MODULE_DIR$/build/outputs" />
+      <excludeFolder url="file://$MODULE_DIR$/build/tmp" />
+    </content>
+    <orderEntry type="jdk" jdkName="Android API 27 Platform" jdkType="Android SDK" />
+    <orderEntry type="sourceFolder" forTests="false" />
+    <orderEntry type="library" name="Gradle: com.android.support:support-vector-drawable:27.1.1@aar" level="project" />
+    <orderEntry type="library" name="Gradle: android.arch.lifecycle:livedata-core:1.1.0@aar" level="project" />
+    <orderEntry type="library" name="Gradle: com.android.support:support-core-utils:27.1.1@aar" level="project" />
+    <orderEntry type="library" name="Gradle: android.arch.lifecycle:runtime:1.1.0@aar" level="project" />
+    <orderEntry type="library" name="Gradle: android.arch.lifecycle:common:1.1.0@jar" level="project" />
+    <orderEntry type="library" name="Gradle: com.android.support:support-annotations:27.1.1@jar" level="project" />
+    <orderEntry type="library" name="Gradle: android.arch.lifecycle:viewmodel:1.1.0@aar" level="project" />
+    <orderEntry type="library" name="Gradle: com.android.support:support-core-ui:27.1.1@aar" level="project" />
+    <orderEntry type="library" name="Gradle: com.android.support:support-compat:27.1.1@aar" level="project" />
+    <orderEntry type="library" name="Gradle: android.arch.core:runtime:1.1.0@aar" level="project" />
+    <orderEntry type="library" name="Gradle: com.android.support:support-fragment:27.1.1@aar" level="project" />
+    <orderEntry type="library" name="Gradle: com.android.support:animated-vector-drawable:27.1.1@aar" level="project" />
+    <orderEntry type="library" name="Gradle: com.android.support:appcompat-v7:27.1.1@aar" level="project" />
+    <orderEntry type="library" name="Gradle: android.arch.core:common:1.1.0@jar" level="project" />
+    <orderEntry type="module" module-name="distorted-library" />
+  </component>
+</module>
\ No newline at end of file
diff --git a/proguard-rules.pro b/proguard-rules.pro
new file mode 100644
index 00000000..f1b42451
--- /dev/null
+++ b/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+#   http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+#   public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..cedda6db
--- /dev/null
+++ b/src/main/AndroidManifest.xml
@@ -0,0 +1,20 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="org.distorted.magic">
+
+    <uses-feature android:glEsVersion="0x00030001" android:required="true" />
+
+    <application
+        android:allowBackup="true"
+        android:icon="@mipmap/icon"
+        android:label="@string/app_name"
+        android:supportsRtl="true"
+        android:theme="@android:style/Theme.NoTitleBar.Fullscreen">
+
+        <activity android:name=".RubikActivity">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+    </application>
+</manifest>
diff --git a/src/main/java/org/distorted/magic/RubikActivity.java b/src/main/java/org/distorted/magic/RubikActivity.java
new file mode 100644
index 00000000..3d8c05c2
--- /dev/null
+++ b/src/main/java/org/distorted/magic/RubikActivity.java
@@ -0,0 +1,133 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2019 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.magic;
+
+import android.app.Activity;
+import android.graphics.PorterDuff;
+import android.graphics.drawable.Drawable;
+import android.opengl.GLSurfaceView;
+import android.os.Bundle;
+import android.support.v4.content.ContextCompat;
+import android.view.View;
+
+import org.distorted.library.main.Distorted;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+public class RubikActivity extends Activity
+{
+            static final int DEFAULT_SIZE  = 3;
+    private static final int STARTING_SIZE = 2;
+    private static final int[] button_ids  = {R.id.rubikSize2, R.id.rubikSize3, R.id.rubikSize4};
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    @Override
+    protected void onCreate(Bundle icicle)
+      {
+      super.onCreate(icicle);
+      setContentView(R.layout.layout);
+
+      markButton(DEFAULT_SIZE);
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+    
+    @Override
+    protected void onPause() 
+      {
+      GLSurfaceView view = findViewById(R.id.rubikSurfaceView);
+      view.onPause();
+      Distorted.onPause();
+      super.onPause();
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+    
+    @Override
+    protected void onResume() 
+      {
+      super.onResume();
+      GLSurfaceView view = findViewById(R.id.rubikSurfaceView);
+      view.onResume();
+      }
+    
+///////////////////////////////////////////////////////////////////////////////////////////////////
+    
+    @Override
+    protected void onDestroy() 
+      {
+      Distorted.onDestroy();  
+      super.onDestroy();
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public void Scramble(View v)
+      {
+      RubikSurfaceView view = findViewById(R.id.rubikSurfaceView);
+      view.scrambleCube();
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public void Credits(View v)
+      {
+      android.util.Log.e("rubik", "credits...");
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public void setSize(View v)
+      {
+      int size=0, id = v.getId();
+
+      for(int b=0; b<button_ids.length; b++)
+        if( button_ids[b] == id )
+          {
+          size = b+STARTING_SIZE;
+          break;
+          }
+
+      markButton(size);
+
+      RubikSurfaceView view = findViewById(R.id.rubikSurfaceView);
+      view.setNewCubeSize(size);
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+   private void markButton(int size)
+     {
+     for(int b=0; b<button_ids.length; b++)
+       {
+       Drawable d = findViewById(button_ids[b]).getBackground();
+
+       if( size == b+STARTING_SIZE )
+         {
+         d.setColorFilter(ContextCompat.getColor(this,R.color.red), PorterDuff.Mode.MULTIPLY);
+         }
+       else
+         {
+         d.clearColorFilter();
+         }
+       }
+     }
+}
diff --git a/src/main/java/org/distorted/magic/RubikCube.java b/src/main/java/org/distorted/magic/RubikCube.java
new file mode 100644
index 00000000..248c189a
--- /dev/null
+++ b/src/main/java/org/distorted/magic/RubikCube.java
@@ -0,0 +1,400 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2019 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.magic;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+
+import org.distorted.library.effect.MatrixEffectMove;
+import org.distorted.library.effect.MatrixEffectQuaternion;
+import org.distorted.library.effect.MatrixEffectRotate;
+import org.distorted.library.effect.MatrixEffectScale;
+import org.distorted.library.effect.VertexEffectSink;
+import org.distorted.library.main.DistortedEffects;
+import org.distorted.library.main.DistortedScreen;
+import org.distorted.library.main.DistortedTexture;
+import org.distorted.library.mesh.MeshCubes;
+import org.distorted.library.message.EffectListener;
+import org.distorted.library.type.Dynamic1D;
+import org.distorted.library.type.Static1D;
+import org.distorted.library.type.Static3D;
+import org.distorted.library.type.Static4D;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+class RubikCube
+{
+    private static final int POST_ROTATION_MILLISEC = 500;
+    private static final int TEXTURE_SIZE = 100;
+
+    private static final Static3D VectX = new Static3D(1,0,0);
+    private static final Static3D VectY = new Static3D(0,1,0);
+    private static final Static3D VectZ = new Static3D(0,0,1);
+
+    private MeshCubes[][][] mCubes;
+    private DistortedEffects[][][] mEffects;
+    private Static4D[][][] mQuatScramble;
+    private Static3D[][][] mRotationAxis;
+    private Dynamic1D[][][] mRotationAngle;
+    private Static3D[][][] mCurrentPosition;
+    private Static1D mRotationAngleStatic, mRotationAngleMiddle, mRotationAngleFinal;
+    private DistortedTexture mTexture;
+    private DistortedEffects mEffectsListeningForNow;
+
+    private int mRotAxis, mRotRow;
+    private int mSize;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    RubikCube(int size, Static3D move, Static3D scale, Static4D quatC, Static4D quatA)
+      {
+      mSize = size;
+
+      mRotationAngleStatic = new Static1D(0);
+      mRotationAngleMiddle = new Static1D(0);
+      mRotationAngleFinal  = new Static1D(0);
+
+      mRotAxis= RubikSurfaceView.VECTX;
+      mTexture = new DistortedTexture(TEXTURE_SIZE,TEXTURE_SIZE);
+
+      mCubes          = new MeshCubes[mSize][mSize][mSize];
+      mEffects        = new DistortedEffects[mSize][mSize][mSize];
+      mQuatScramble   = new Static4D[mSize][mSize][mSize];
+      mRotationAxis   = new Static3D[mSize][mSize][mSize];
+      mRotationAngle  = new Dynamic1D[mSize][mSize][mSize];
+      mCurrentPosition= new Static3D[mSize][mSize][mSize];
+
+      Static3D[][][] cubeVectors = new Static3D[mSize][mSize][mSize];
+
+      Static3D center = new Static3D(TEXTURE_SIZE*0.5f, TEXTURE_SIZE*0.5f, TEXTURE_SIZE*0.5f);
+      Static4D region = new Static4D(0,0,0, TEXTURE_SIZE*0.72f);
+
+      VertexEffectSink        sinkEffect = new VertexEffectSink( new Static1D(getSinkStrength()), center, region );
+      MatrixEffectMove        moveEffect = new MatrixEffectMove(move);
+      MatrixEffectScale      scaleEffect = new MatrixEffectScale(scale);
+      MatrixEffectQuaternion quatCEffect = new MatrixEffectQuaternion(quatC, center);
+      MatrixEffectQuaternion quatAEffect = new MatrixEffectQuaternion(quatA, center);
+
+      // 3x2 bitmap = 6 squares:
+      //
+      // RED     GREEN   BLUE
+      // YELLOW  WHITE   BROWN
+
+      final float ze = 0.0f;
+      final float ot = 1.0f/3.0f;
+      final float tt = 2.0f/3.0f;
+      final float oh = 1.0f/2.0f;
+      final float of = 1.0f/40.0f;
+
+      final Static4D mapFront = new Static4D(ze,oh, ze+ot,oh+oh);
+      final Static4D mapBack  = new Static4D(tt,ze, tt+ot,ze+oh);
+      final Static4D mapLeft  = new Static4D(ot,ze, ot+ot,ze+oh);
+      final Static4D mapRight = new Static4D(ze,ze, ze+ot,ze+oh);
+      final Static4D mapTop   = new Static4D(tt,oh, tt+ot,oh+oh);
+      final Static4D mapBottom= new Static4D(ot,oh, ot+ot,oh+oh);
+
+      final Static4D mapBlack = new Static4D(ze,ze, ze+of,ze+of);
+
+      Static4D tmpFront, tmpBack, tmpLeft, tmpRight, tmpTop, tmpBottom;
+      float nc = 0.5f*(mSize-1);
+      int vertices = (int)(24.0f/mSize + 2.0f);
+
+      for(int x = 0; x< mSize; x++)
+        for(int y = 0; y< mSize; y++)
+          for(int z = 0; z< mSize; z++)
+            {
+            if( x==0 || x==mSize-1 || y==0 || y==mSize-1 || z==0 || z==mSize-1 ) // only the external walls
+              {
+              tmpLeft  = (x==       0 ? mapLeft  :mapBlack);
+              tmpRight = (x== mSize-1 ? mapRight :mapBlack);
+              tmpFront = (z== mSize-1 ? mapFront :mapBlack);
+              tmpBack  = (z==       0 ? mapBack  :mapBlack);
+              tmpTop   = (y== mSize-1 ? mapTop   :mapBlack);
+              tmpBottom= (y==       0 ? mapBottom:mapBlack);
+
+              mCubes[x][y][z]           = new MeshCubes(vertices,vertices,vertices, tmpFront, tmpBack, tmpLeft, tmpRight, tmpTop, tmpBottom);
+              cubeVectors[x][y][z]      = new Static3D( TEXTURE_SIZE*(x-nc), TEXTURE_SIZE*(y-nc), TEXTURE_SIZE*(z-nc) );
+              mQuatScramble[x][y][z]    = new Static4D(0,0,0,1);
+              mRotationAngle[x][y][z]   = new Dynamic1D();
+              mRotationAxis[x][y][z]    = new Static3D(1,0,0);
+              mCurrentPosition[x][y][z] = new Static3D(x,y,z);
+
+              mEffects[x][y][z] = new DistortedEffects();
+              mEffects[x][y][z].apply(sinkEffect);
+              mEffects[x][y][z].apply(moveEffect);
+              mEffects[x][y][z].apply(scaleEffect);
+              mEffects[x][y][z].apply(quatCEffect);
+              mEffects[x][y][z].apply(quatAEffect);
+              mEffects[x][y][z].apply( new MatrixEffectRotate( mRotationAngle[x][y][z], mRotationAxis[x][y][z], center));
+              mEffects[x][y][z].apply( new MatrixEffectQuaternion(mQuatScramble[x][y][z], center));
+              mEffects[x][y][z].apply( new MatrixEffectMove(cubeVectors[x][y][z]) );
+              }
+            }
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    void attachToScreen(DistortedScreen screen)
+      {
+      for(int x=0; x<mSize; x++)
+        for(int y=0; y<mSize; y++)
+          for(int z=0; z<mSize; z++)
+            {
+            if( x==0 || x==mSize-1 || y==0 || y==mSize-1 || z==0 || z==mSize-1 )
+              {
+              screen.attach(mTexture,mEffects[x][y][z],mCubes[x][y][z]);
+              }
+            }
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    void addNewRotation(int vector, float offset )
+      {
+      Static3D axis = VectX;
+
+      switch(vector)
+        {
+        case RubikSurfaceView.VECTX: axis = VectX; break;
+        case RubikSurfaceView.VECTY: axis = VectY; break;
+        case RubikSurfaceView.VECTZ: axis = VectZ; break;
+        }
+
+      mRotAxis = vector;
+      mRotRow  = (int)(mSize*offset);
+
+      mRotationAngleStatic.set1(0.0f);
+
+      for(int x=0; x<mSize; x++)
+        for(int y=0; y<mSize; y++)
+          for(int z=0; z<mSize; z++)
+            if( x==0 || x==mSize-1 || y==0 || y==mSize-1 || z==0 || z==mSize-1 )
+              {
+              if( belongsToRotation(x,y,z,vector,mRotRow) )
+                {
+                mRotationAxis[x][y][z].set(axis);
+                mRotationAngle[x][y][z].add(mRotationAngleStatic);
+                }
+              }
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    void continueRotation(float angleInDegrees)
+      {
+      mRotationAngleStatic.set1(angleInDegrees);
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    private int computeNearestAngle(float angle)
+      {
+      final int NEAREST = 90;
+
+      int tmp = (int)((angle+NEAREST/2)/NEAREST);
+      if( angle< -(NEAREST/2) ) tmp-=1;
+
+      return NEAREST*tmp;
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    void finishRotationCalledOnNextRender(EffectListener listener)
+      {
+      boolean first = true;
+      float startingAngle = mRotationAngleStatic.get1();
+      int nearestAngleInDegrees = computeNearestAngle(startingAngle);
+
+      mRotationAngleFinal.set1(nearestAngleInDegrees);
+      mRotationAngleMiddle.set1( nearestAngleInDegrees + (nearestAngleInDegrees-startingAngle)*0.2f );
+
+      for(int x=0; x<mSize; x++)
+        for(int y=0; y<mSize; y++)
+          for(int z=0; z<mSize; z++)
+            if( x==0 || x==mSize-1 || y==0 || y==mSize-1 || z==0 || z==mSize-1 )
+              {
+              if( belongsToRotation(x,y,z,mRotAxis,mRotRow) )
+                {
+                mRotationAngle[x][y][z].makeRunNowFor(POST_ROTATION_MILLISEC);
+                mRotationAngle[x][y][z].add(mRotationAngleMiddle);
+                mRotationAngle[x][y][z].add(mRotationAngleFinal);
+
+                if( first )
+                  {
+                  first = false;
+                  mEffectsListeningForNow = mEffects[x][y][z];
+                  mEffectsListeningForNow.registerForMessages(listener);
+                  }
+                }
+              }
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    void removeRotationCalledOnNextRender(EffectListener listener)
+      {
+      mEffectsListeningForNow.deregisterForMessages(listener);
+
+      int nearestAngleInDegrees = computeNearestAngle(mRotationAngleStatic.get1());
+      double nearestAngleInRadians = nearestAngleInDegrees*Math.PI/180;
+      float sinA =-(float)Math.sin(nearestAngleInRadians*0.5);
+      float cosA = (float)Math.cos(nearestAngleInRadians*0.5);
+
+      mRotationAngleStatic.set1(0);
+
+      float qx=0,qy=0,qz=0;
+
+      switch(mRotAxis)
+        {
+        case RubikSurfaceView.VECTX: qx=1; break;
+        case RubikSurfaceView.VECTY: qy=1; break;
+        case RubikSurfaceView.VECTZ: qz=1; break;
+        }
+
+      Static4D quat = new Static4D(qx*sinA, qy*sinA, qz*sinA, cosA);
+
+      for(int x=0; x<mSize; x++)
+        for(int y=0; y<mSize; y++)
+          for(int z=0; z<mSize; z++)
+            if( x==0 || x==mSize-1 || y==0 || y==mSize-1 || z==0 || z==mSize-1 )
+              {
+              if( belongsToRotation(x,y,z,mRotAxis,mRotRow) )
+                {
+                mRotationAngle[x][y][z].makeRunNowFor(0);
+                mRotationAngle[x][y][z].removeAll();
+                mQuatScramble[x][y][z].set(RubikSurfaceView.quatMultiply(quat,mQuatScramble[x][y][z]));
+                modifyCurrentPosition(x,y,z,quat);
+                }
+              }
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    private float getSinkStrength()
+      {
+      switch(mSize)
+        {
+        case 1 : return 1.1f;
+        case 2 : return 1.5f;
+        case 3 : return 1.8f;
+        case 4 : return 2.0f;
+        default: return 3.0f - 4.0f/mSize;
+        }
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    private boolean belongsToRotation(int x, int y, int z, int vector, int row)
+      {
+      switch(vector)
+        {
+        case RubikSurfaceView.VECTX: return mCurrentPosition[x][y][z].get1()==row;
+        case RubikSurfaceView.VECTY: return mCurrentPosition[x][y][z].get2()==row;
+        case RubikSurfaceView.VECTZ: return mCurrentPosition[x][y][z].get3()==row;
+        }
+
+      return false;
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    private void modifyCurrentPosition(int x, int y, int z, Static4D quat)
+      {
+      Static3D current = mCurrentPosition[x][y][z];
+      float diff = 0.5f*(mSize-1);
+      float cubitCenterX = current.get1() - diff;
+      float cubitCenterY = current.get2() - diff;
+      float cubitCenterZ = current.get3() - diff;
+
+      Static4D cubitCenter =  new Static4D(cubitCenterX, cubitCenterY, cubitCenterZ, 0);
+      Static4D rotatedCenter = RubikSurfaceView.rotateVectorByQuat( cubitCenter, quat);
+
+      float rotatedX = rotatedCenter.get1() + diff;
+      float rotatedY = rotatedCenter.get2() + diff;
+      float rotatedZ = rotatedCenter.get3() + diff;
+
+      int roundedX = (int)(rotatedX+0.1f);
+      int roundedY = (int)(rotatedY+0.1f);
+      int roundedZ = (int)(rotatedZ+0.1f);
+
+      mCurrentPosition[x][y][z].set1(roundedX);
+      mCurrentPosition[x][y][z].set2(roundedY);
+      mCurrentPosition[x][y][z].set3(roundedZ);
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    void createTexture()
+      {
+      Bitmap bitmap;
+
+      final int S = 128;
+      final int W = 3*S;
+      final int H = 2*S;
+      final int R = S/10;
+      final int M = S/20;
+
+      Paint paint = new Paint();
+      bitmap = Bitmap.createBitmap(W,H, Bitmap.Config.ARGB_8888);
+      Canvas canvas = new Canvas(bitmap);
+
+      paint.setAntiAlias(true);
+      paint.setTextAlign(Paint.Align.CENTER);
+      paint.setStyle(Paint.Style.FILL);
+
+      // 3x2 bitmap = 6 squares:
+      //
+      // RED     GREEN   BLUE
+      // YELLOW  WHITE   BROWN
+
+      paint.setColor(0xff000000);                                  // BLACK BACKGROUND
+      canvas.drawRect(0, 0, W, H, paint);                          //
+
+      paint.setColor(0xffff0000);                                  // RED
+      canvas.drawRoundRect(    M,   M,   S-M,   S-M, R, R, paint); //
+      paint.setColor(0xff00ff00);                                  // GREEN
+      canvas.drawRoundRect(  S+M,   M, 2*S-M,   S-M, R, R, paint); //
+      paint.setColor(0xff0000ff);                                  // BLUE
+      canvas.drawRoundRect(2*S+M,   M, 3*S-M,   S-M, R, R, paint); //
+      paint.setColor(0xffffff00);                                  // YELLOW
+      canvas.drawRoundRect(    M, S+M,   S-M, 2*S-M, R, R, paint); //
+      paint.setColor(0xffffffff);                                  // WHITE
+      canvas.drawRoundRect(  S+M, S+M, 2*S-M, 2*S-M, R, R, paint); //
+      paint.setColor(0xffb5651d);                                  // BROWN
+      canvas.drawRoundRect(2*S+M, S+M, 3*S-M, 2*S-M, R, R, paint); //
+
+      mTexture.setTexture(bitmap);
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    float getTextureSize()
+      {
+      return TEXTURE_SIZE;
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    float getSize()
+      {
+      return mSize;
+      }
+}
diff --git a/src/main/java/org/distorted/magic/RubikRenderer.java b/src/main/java/org/distorted/magic/RubikRenderer.java
new file mode 100644
index 00000000..907ff220
--- /dev/null
+++ b/src/main/java/org/distorted/magic/RubikRenderer.java
@@ -0,0 +1,225 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2019 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.magic;
+
+import android.opengl.GLSurfaceView;
+
+import org.distorted.library.effect.VertexEffectSink;
+import org.distorted.library.main.Distorted;
+import org.distorted.library.main.DistortedScreen;
+import org.distorted.library.message.EffectListener;
+import org.distorted.library.message.EffectMessage;
+import org.distorted.library.type.Static3D;
+import org.distorted.library.type.Static4D;
+
+import javax.microedition.khronos.egl.EGLConfig;
+import javax.microedition.khronos.opengles.GL10;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+class RubikRenderer implements GLSurfaceView.Renderer, EffectListener
+{
+    private static final float CUBE_SCREEN_RATIO = 0.5f;
+    private static final float CAMERA_DISTANCE   = 0.6f;  // 0.6 of the length of max(scrHeight,scrWidth)
+
+    private RubikSurfaceView mView;
+    private DistortedScreen mScreen;
+    private Static3D mMove, mScale;
+    private Static4D mQuatCurrent, mQuatAccumulated;
+    private Static4D mTempCurrent, mTempAccumulated;
+    private float mCubeSizeInScreenSpace;
+    private boolean mFinishRotation, mRemoveRotation, mFinishDragCurrent, mFinishDragAccumulated;
+    private boolean mCanRotate;
+    private RubikCube mCube;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    RubikRenderer(RubikSurfaceView v)
+      {
+      mView = v;
+
+      mScreen = new DistortedScreen();
+
+      mTempCurrent     = new Static4D(0,0,0,1);
+      mTempAccumulated = initializeQuat();
+      mQuatCurrent     = new Static4D(0,0,0,1);
+      mQuatAccumulated = initializeQuat();
+
+      mMove  = new Static3D(0,0,0);
+      mScale = new Static3D(1,1,1);
+
+      mFinishRotation        = false;
+      mRemoveRotation        = false;
+      mFinishDragCurrent     = false;
+      mFinishDragAccumulated = false;
+
+      mCanRotate = true;
+
+      mCube = new RubikCube( RubikActivity.DEFAULT_SIZE, mMove, mScale, mQuatCurrent, mQuatAccumulated);
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// various things are done here delayed, 'after the next render' as not to be done mid-render and
+// cause artifacts.
+
+    public void onDrawFrame(GL10 glUnused) 
+      {
+      mScreen.render( System.currentTimeMillis() );
+
+      if( mFinishDragCurrent )
+        {
+        mFinishDragCurrent = false;
+        mQuatCurrent.set(mTempCurrent);
+        }
+
+      if( mFinishDragAccumulated )
+        {
+        mFinishDragAccumulated = false;
+        mQuatAccumulated.set(mTempAccumulated);
+        }
+
+      if( mFinishRotation )
+        {
+        mCanRotate = false;
+        mFinishRotation=false;
+        mCube.finishRotationCalledOnNextRender(this);
+        }
+
+      if( mRemoveRotation )
+        {
+        mRemoveRotation=false;
+        mCube.removeRotationCalledOnNextRender(this);
+        mCanRotate = true;
+        }
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// EffectListener. The library sends a message to us when it's time to call 'removeRotation'
+
+   public void effectMessage(final EffectMessage em, final long effectID, final long objectID)
+     {
+     switch(em)
+        {
+        case EFFECT_FINISHED: mRemoveRotation = true; break;
+        }
+     }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+    
+    public void onSurfaceChanged(GL10 glUnused, int width, int height) 
+      {
+      float cameraDistance = CAMERA_DISTANCE*(width>height ? width:height);
+      float fovInDegrees   = computeFOV(cameraDistance,height);
+
+      mScreen.setProjection( fovInDegrees, 0.1f);
+      mView.setScreenSize(width,height);
+      mView.setCameraDist(cameraDistance);
+
+      mCubeSizeInScreenSpace = CUBE_SCREEN_RATIO*(width>height ? height:width);
+      float texSize = mCube.getTextureSize();
+      float scaleFactor = mCubeSizeInScreenSpace/(texSize*mCube.getSize());
+
+      mMove.set( (width-scaleFactor*texSize)/2 , (height-scaleFactor*texSize)/2 , -scaleFactor*texSize/2 );
+      mScale.set(scaleFactor,scaleFactor,scaleFactor);
+
+      mScreen.resize(width, height);
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+    
+    public void onSurfaceCreated(GL10 glUnused, EGLConfig config) 
+      {
+      mCube.createTexture();
+      mScreen.detachAll();
+      mCube.attachToScreen(mScreen);
+
+      VertexEffectSink.enable();
+
+      try
+        {
+        Distorted.onCreate(mView.getContext());
+        }
+      catch(Exception ex)
+        {
+        android.util.Log.e("Rubik", ex.getMessage() );
+        }
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    private float computeFOV(float cameraDistance, int screenHeight)
+      {
+      double halfFOVInRadians = Math.atan( screenHeight/(2*cameraDistance) );
+      return (float)(2*halfFOVInRadians*(180/Math.PI));
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// no this will not race with onDrawFrame
+
+    void finishRotation()
+      {
+      mFinishRotation = true;
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    float returnCubeSizeInScreenSpace()
+      {
+      return mCubeSizeInScreenSpace;
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    boolean canRotate()
+      {
+      return mCanRotate;
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    RubikCube getCube()
+      {
+      return mCube;
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Initial rotation of the cube. Something semi-random that looks good.
+
+    Static4D initializeQuat()
+      {
+      return new Static4D(-0.25189602f,0.3546389f,0.009657208f,0.90038127f);
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    void setQuatCurrent(Static4D current)
+      {
+      mTempCurrent.set(current);
+      mFinishDragCurrent = true;
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    void setQuatAccumulated(Static4D accumulated)
+      {
+      mTempAccumulated.set(accumulated);
+      mFinishDragAccumulated = true;
+      }
+}
diff --git a/src/main/java/org/distorted/magic/RubikSurfaceView.java b/src/main/java/org/distorted/magic/RubikSurfaceView.java
new file mode 100644
index 00000000..ca446443
--- /dev/null
+++ b/src/main/java/org/distorted/magic/RubikSurfaceView.java
@@ -0,0 +1,484 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2019 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.magic;
+
+import android.app.ActivityManager;
+import android.content.Context;
+import android.content.pm.ConfigurationInfo;
+import android.opengl.GLSurfaceView;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+
+import org.distorted.library.type.Static4D;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+class RubikSurfaceView extends GLSurfaceView
+{
+    private final static int NONE   =-1;
+    private final static int FRONT  = 0;  // has to be 6 consecutive ints
+    private final static int BACK   = 1;  // FRONT ... BOTTOM
+    private final static int LEFT   = 2;  //
+    private final static int RIGHT  = 3;  //
+    private final static int TOP    = 4;  //
+    private final static int BOTTOM = 5;  //
+
+    static final int VECTX = 0;  //
+    static final int VECTY = 1;  // dont change this
+    static final int VECTZ = 2;  //
+
+    private static final int[] VECT = {VECTX,VECTY,VECTZ};
+
+    private boolean mDragging, mBeginningRotation, mContinuingRotation;
+    private int mX, mY;
+    private Static4D mQuatCurrent, mQuatAccumulated;
+    private int mRotationVect;
+    private RubikRenderer mRenderer;
+    private RubikCube mCube;
+
+    private float[] mPoint, mCamera, mTouchPointCastOntoFace, mDiff, mTouchPoint; // all in screen space
+    private int mLastTouchedFace;
+    private int mScreenWidth, mScreenHeight, mScreenMin;
+    private float mCameraDistance;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public RubikSurfaceView(Context context, AttributeSet attrs)
+      {
+      super(context,attrs);
+
+      if(!isInEditMode())
+        {
+        mRotationVect = VECT[0];
+
+        mPoint = new float[3];
+        mCamera= new float[3];
+        mDiff  = new float[3];
+        mTouchPoint = new float[3];
+        mTouchPointCastOntoFace = new float[3];
+
+        mScreenWidth = mScreenHeight = mScreenMin = 0;
+
+        mRenderer = new RubikRenderer(this);
+        mCube = mRenderer.getCube();
+
+        mQuatCurrent     = new Static4D(0,0,0,1);
+        mQuatAccumulated = mRenderer.initializeQuat();
+
+        final ActivityManager activityManager     = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
+        final ConfigurationInfo configurationInfo = activityManager.getDeviceConfigurationInfo();
+        setEGLContextClientVersion( (configurationInfo.reqGlEsVersion>>16) >= 3 ? 3:2 );
+        setRenderer(mRenderer);
+        }
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public RubikRenderer getRenderer()
+      {
+      return mRenderer;
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event)
+      {
+      int action = event.getAction();
+      int x = (int)event.getX();
+      int y = (int)event.getY();
+
+      switch(action)
+         {
+         case MotionEvent.ACTION_DOWN: mX = x;
+                                       mY = y;
+                                       mLastTouchedFace = faceTouched(x,y);
+
+                                       if( mLastTouchedFace != NONE )
+                                         {
+                                         mDragging           = false;
+                                         mBeginningRotation  = mRenderer.canRotate();
+                                         mContinuingRotation = false;
+                                         }
+                                       else
+                                         {
+                                         mDragging           = true;
+                                         mBeginningRotation  = false;
+                                         mContinuingRotation = false;
+                                         }
+                                       break;
+         case MotionEvent.ACTION_MOVE: if( mDragging )
+                                         {
+                                         mQuatCurrent.set(quatFromDrag(mX-x,mY-y));
+                                         mRenderer.setQuatCurrent(mQuatCurrent);
+                                         }
+                                       if( mBeginningRotation )
+                                         {
+                                         int minimumDistToStartRotating = (mScreenMin*mScreenMin)/100;
+
+                                         if( (mX-x)*(mX-x)+(mY-y)*(mY-y) > minimumDistToStartRotating )
+                                           {
+                                           addNewRotation(x,y);
+                                           mBeginningRotation = false;
+                                           mContinuingRotation= true;
+                                           }
+                                         }
+                                       else if( mContinuingRotation )
+                                         {
+                                         continueRotation(x,y);
+                                         }
+                                       break;
+         case MotionEvent.ACTION_UP  : if( mDragging )
+                                         {
+                                         mQuatAccumulated.set(quatMultiply(mQuatCurrent, mQuatAccumulated));
+                                         mQuatCurrent.set(0f, 0f, 0f, 1f);
+                                         mRenderer.setQuatCurrent(mQuatCurrent);
+                                         mRenderer.setQuatAccumulated(mQuatAccumulated);
+                                         }
+
+                                       if( mContinuingRotation )
+                                         {
+                                         finishRotation();
+                                         }
+
+                                       break;
+         }
+
+      return true;
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    void setNewCubeSize(int newCubeSize)
+      {
+      android.util.Log.e("view", "new size="+newCubeSize);
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    void scrambleCube()
+      {
+      android.util.Log.e("view", "scrambling...");
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    void setScreenSize(int width, int height)
+      {
+      mScreenWidth = width;
+      mScreenHeight= height;
+
+      mScreenMin = width<height ? width:height;
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    void setCameraDist(float distance)
+      {
+      mCameraDistance = distance;
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    private int faceTouched(int xTouch, int yTouch)
+      {
+      float cubeHalfSize= mRenderer.returnCubeSizeInScreenSpace()*0.5f;
+
+      convertTouchPointToScreenSpace(xTouch,yTouch);
+      convertCameraPointToScreenSpace();
+
+      for(int face=FRONT; face<=BOTTOM; face++)
+        {
+        if( faceIsVisible(face,cubeHalfSize) )
+          {
+          castTouchPointOntoFace(face,cubeHalfSize, mTouchPointCastOntoFace);
+
+          float qX= (mTouchPointCastOntoFace[0]+cubeHalfSize) / (2*cubeHalfSize);
+          float qY= (mTouchPointCastOntoFace[1]+cubeHalfSize) / (2*cubeHalfSize);
+          float qZ= (mTouchPointCastOntoFace[2]+cubeHalfSize) / (2*cubeHalfSize);
+
+          if( qX<=1 && qX>=0 && qY<=1 && qY>=0 && qZ<=1 && qZ>=0 ) return face;
+          }
+        }
+
+      return NONE;
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    private void addNewRotation(int x, int y)
+      {
+      float cubeHalfSize= mRenderer.returnCubeSizeInScreenSpace()*0.5f;
+
+      convertTouchPointToScreenSpace(x,y);
+      castTouchPointOntoFace(mLastTouchedFace,cubeHalfSize,mDiff);
+
+      mDiff[0] -= mTouchPointCastOntoFace[0];
+      mDiff[1] -= mTouchPointCastOntoFace[1];
+      mDiff[2] -= mTouchPointCastOntoFace[2];
+
+      int xAxis = retFaceXaxis(mLastTouchedFace);
+      int yAxis = retFaceYaxis(mLastTouchedFace);
+      mRotationVect = (isVertical( mDiff[xAxis], mDiff[yAxis]) ? VECT[xAxis]:VECT[yAxis]);
+      float offset= (mTouchPointCastOntoFace[mRotationVect]+cubeHalfSize)/(2*cubeHalfSize);
+
+      mTouchPoint[0] = mPoint[0];
+      mTouchPoint[1] = mPoint[1];
+      mTouchPoint[2] = mPoint[2];
+
+      mCube.addNewRotation(mRotationVect,offset);
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    private boolean isVertical(float x, float y)
+      {
+      return (y>x) ? (y>=-x) : (y< -x);
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// 240 --> moving finger from the middle of the vertical screen to the right edge will rotate a
+// given face by 240/2 = 120 degrees.
+
+    private void continueRotation(int x, int y)
+      {
+      convertTouchPointToScreenSpace(x,y);
+
+      mDiff[0] = mPoint[0]-mTouchPoint[0];
+      mDiff[1] = mPoint[1]-mTouchPoint[1];
+      mDiff[2] = mPoint[2]-mTouchPoint[2];
+
+      int xAxis= retFaceXaxis(mLastTouchedFace);
+      int yAxis= retFaceYaxis(mLastTouchedFace);
+      int sign = retFaceRotationSign(mLastTouchedFace);
+      float angle = (mRotationVect==xAxis ? mDiff[yAxis] : -mDiff[xAxis]);
+
+      mCube.continueRotation(240.0f*sign*angle/mScreenMin);
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    private void finishRotation()
+      {
+      mRenderer.finishRotation();
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// return quat1*quat2
+
+    static Static4D quatMultiply( Static4D quat1, Static4D quat2 )
+      {
+      float qx = quat1.get1();
+      float qy = quat1.get2();
+      float qz = quat1.get3();
+      float qw = quat1.get4();
+
+      float rx = quat2.get1();
+      float ry = quat2.get2();
+      float rz = quat2.get3();
+      float rw = quat2.get4();
+
+      float tx = rw*qx - rz*qy + ry*qz + rx*qw;
+      float ty = rw*qy + rz*qx + ry*qw - rx*qz;
+      float tz = rw*qz + rz*qw - ry*qx + rx*qy;
+      float tw = rw*qw - rz*qz - ry*qy - rx*qx;
+
+      return new Static4D(tx,ty,tz,tw);
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// rotate 'vector' by quat^(-1)  ( i.e. return (quat^-1)*vector*quat )
+
+    static Static4D rotateVectorByInvertedQuat(Static4D vector, Static4D quat)
+      {
+      float qx = quat.get1();
+      float qy = quat.get2();
+      float qz = quat.get3();
+      float qw = quat.get4();
+
+      Static4D quatInverted= new Static4D(-qx,-qy,-qz,qw);
+      Static4D tmp = quatMultiply(quatInverted,vector);
+
+      return quatMultiply(tmp,quat);
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// rotate 'vector' by quat  ( i.e. return quat*vector*(quat^-1) )
+
+    static Static4D rotateVectorByQuat(Static4D vector, Static4D quat)
+      {
+      float qx = quat.get1();
+      float qy = quat.get2();
+      float qz = quat.get3();
+      float qw = quat.get4();
+
+      Static4D quatInverted= new Static4D(-qx,-qy,-qz,qw);
+      Static4D tmp = quatMultiply(quat,vector);
+
+      return quatMultiply(tmp,quatInverted);
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    private Static4D quatFromDrag(float dragX, float dragY)
+      {
+      float axisX = dragY;  // inverted X and Y - rotation axis is
+      float axisY = dragX;  // perpendicular to (dragX,dragY)   Why not (-dragY, dragX) ? because Y axis is also inverted!
+      float axisZ = 0;
+      float axisL = (float)Math.sqrt(axisX*axisX + axisY*axisY + axisZ*axisZ);
+
+      if( axisL>0 )
+        {
+        axisX /= axisL;
+        axisY /= axisL;
+        axisZ /= axisL;
+
+        float cosA = (float)Math.cos(axisL*Math.PI/mScreenMin);
+        float sinA = (float)Math.sqrt(1-cosA*cosA);
+
+        return new Static4D(axisX*sinA, axisY*sinA, axisZ*sinA, cosA);
+        }
+
+      return new Static4D(0f, 0f, 0f, 1f);
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    private boolean faceIsVisible(int face, float cubeHalfSize)
+      {
+      int sign = retFaceSign(face);
+      int zAxis= retFaceZaxis(face);
+
+      return sign*mCamera[zAxis] > cubeHalfSize;
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    private void convertTouchPointToScreenSpace(int x, int y)
+      {
+      float halfScrWidth  = mScreenWidth *0.5f;
+      float halfScrHeight = mScreenHeight*0.5f;
+      Static4D touchPoint = new Static4D(x-halfScrWidth, halfScrHeight-y, 0, 0);
+      Static4D rotatedTouchPoint= rotateVectorByInvertedQuat(touchPoint, mQuatAccumulated);
+
+      mPoint[0] = rotatedTouchPoint.get1();
+      mPoint[1] = rotatedTouchPoint.get2();
+      mPoint[2] = rotatedTouchPoint.get3();
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    private void convertCameraPointToScreenSpace()
+      {
+      Static4D cameraPoint = new Static4D(0, 0, mCameraDistance, 0);
+      Static4D rotatedCamera= rotateVectorByInvertedQuat(cameraPoint, mQuatAccumulated);
+
+      mCamera[0] = rotatedCamera.get1();
+      mCamera[1] = rotatedCamera.get2();
+      mCamera[2] = rotatedCamera.get3();
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// given precomputed mCamera and mPoint, respectively camera and touch point positions in ScreenSpace,
+// cast this touch point onto the surface defined by the 'face' and write the cast coords to 'output'.
+// Center of the 'face' = (0,0), third coord always +- cubeHalfSize.
+
+    private void castTouchPointOntoFace(int face, float cubeHalfSize, float[] output)
+      {
+      int sign = retFaceSign(face);
+      int zAxis= retFaceZaxis(face);
+      float diff = mPoint[zAxis]-mCamera[zAxis];
+
+      float ratio =  diff!=0.0f ? (sign*cubeHalfSize-mCamera[zAxis])/diff : 0.0f;
+
+      output[0] = (mPoint[0]-mCamera[0])*ratio + mCamera[0];
+      output[1] = (mPoint[1]-mCamera[1])*ratio + mCamera[1];
+      output[2] = (mPoint[2]-mCamera[2])*ratio + mCamera[2];
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    private int retFaceSign(int face)
+      {
+      return (face==FRONT || face==RIGHT || face==TOP) ? 1:-1;
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    private int retFaceRotationSign(int face)
+      {
+      return (face==BACK || face==RIGHT || face==TOP) ? 1:-1;
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// retFace{X,Y,Z}axis: 3 functions which return which real AXIS gets mapped to which when we look
+// directly at a given face. For example, when we look at the RIGHT face of the cube (with TOP still
+// in the top) then the 'real' X axis becomes the 'Z' axis, thus retFaceZaxis(RIGHT) = VECTX.
+
+    private int retFaceXaxis(int face)
+      {
+      switch(face)
+        {
+        case FRONT :
+        case BACK  : return VECTX;
+        case LEFT  :
+        case RIGHT : return VECTZ;
+        case TOP   :
+        case BOTTOM: return VECTX;
+        }
+
+      return -1;
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    private int retFaceYaxis(int face)
+      {
+      switch(face)
+        {
+        case FRONT :
+        case BACK  : return VECTY;
+        case LEFT  :
+        case RIGHT : return VECTY;
+        case TOP   :
+        case BOTTOM: return VECTZ;
+        }
+
+      return -1;
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    private int retFaceZaxis(int face)
+      {
+      switch(face)
+        {
+        case FRONT :
+        case BACK  : return VECTZ;
+        case LEFT  :
+        case RIGHT : return VECTX;
+        case TOP   :
+        case BOTTOM: return VECTY;
+        }
+
+      return -1;
+      }
+}
+
diff --git a/src/main/res/drawable/button2.png b/src/main/res/drawable/button2.png
new file mode 100644
index 00000000..eb781e64
Binary files /dev/null and b/src/main/res/drawable/button2.png differ
diff --git a/src/main/res/drawable/button3.png b/src/main/res/drawable/button3.png
new file mode 100644
index 00000000..dae0f164
Binary files /dev/null and b/src/main/res/drawable/button3.png differ
diff --git a/src/main/res/drawable/button4.png b/src/main/res/drawable/button4.png
new file mode 100644
index 00000000..c37a2e75
Binary files /dev/null and b/src/main/res/drawable/button4.png differ
diff --git a/src/main/res/layout/layout.xml b/src/main/res/layout/layout.xml
new file mode 100644
index 00000000..1cdfa896
--- /dev/null
+++ b/src/main/res/layout/layout.xml
@@ -0,0 +1,68 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="fill_parent"
+    android:layout_height="fill_parent"
+    android:orientation="vertical" >
+
+    <LinearLayout
+        android:id="@+id/linearLayout"
+        android:layout_width="fill_parent"
+        android:layout_height="wrap_content"
+        android:gravity="center|fill_horizontal" >
+
+         <Button
+            android:id="@+id/rubikCredits"
+            android:layout_width="wrap_content"
+            android:layout_height="64dp"
+            android:layout_weight="0.5"
+            android:onClick="Credits"
+            android:paddingLeft="5dp"
+            android:paddingRight="5dp"
+            android:text="@string/credits" />
+
+        <ImageButton
+            android:id="@+id/rubikSize2"
+            android:layout_width="64dp"
+            android:layout_height="wrap_content"
+            android:onClick="setSize"
+            android:paddingLeft="5dp"
+            android:paddingRight="5dp"
+            android:src="@drawable/button2"/>
+
+        <ImageButton
+            android:id="@+id/rubikSize3"
+            android:layout_width="64dp"
+            android:layout_height="wrap_content"
+            android:onClick="setSize"
+            android:paddingLeft="5dp"
+            android:paddingRight="5dp"
+            android:src="@drawable/button3"/>
+
+        <ImageButton
+            android:id="@+id/rubikSize4"
+            android:layout_width="64dp"
+            android:layout_height="wrap_content"
+            android:onClick="setSize"
+            android:paddingLeft="5dp"
+            android:paddingRight="5dp"
+            android:src="@drawable/button4"/>
+
+        <Button
+            android:id="@+id/rubikScramble"
+            android:layout_width="wrap_content"
+            android:layout_height="64dp"
+            android:layout_weight="0.5"
+            android:onClick="Scramble"
+            android:paddingLeft="5dp"
+            android:paddingRight="5dp"
+            android:text="@string/scramble" />
+
+    </LinearLayout>
+
+    <org.distorted.magic.RubikSurfaceView
+        android:id="@+id/rubikSurfaceView"
+        android:layout_width="fill_parent"
+        android:layout_height="0dp"
+        android:layout_weight="1" />
+
+</LinearLayout>
diff --git a/src/main/res/mipmap-hdpi/icon.png b/src/main/res/mipmap-hdpi/icon.png
new file mode 100644
index 00000000..88678a85
Binary files /dev/null and b/src/main/res/mipmap-hdpi/icon.png differ
diff --git a/src/main/res/mipmap-mdpi/icon.png b/src/main/res/mipmap-mdpi/icon.png
new file mode 100644
index 00000000..a8607c93
Binary files /dev/null and b/src/main/res/mipmap-mdpi/icon.png differ
diff --git a/src/main/res/mipmap-xhdpi/icon.png b/src/main/res/mipmap-xhdpi/icon.png
new file mode 100644
index 00000000..01f21203
Binary files /dev/null and b/src/main/res/mipmap-xhdpi/icon.png differ
diff --git a/src/main/res/mipmap-xxhdpi/icon.png b/src/main/res/mipmap-xxhdpi/icon.png
new file mode 100644
index 00000000..1614c424
Binary files /dev/null and b/src/main/res/mipmap-xxhdpi/icon.png differ
diff --git a/src/main/res/mipmap-xxxhdpi/icon.png b/src/main/res/mipmap-xxxhdpi/icon.png
new file mode 100644
index 00000000..fdaa3a34
Binary files /dev/null and b/src/main/res/mipmap-xxxhdpi/icon.png differ
diff --git a/src/main/res/values/colors.xml b/src/main/res/values/colors.xml
new file mode 100644
index 00000000..37d5b6d6
--- /dev/null
+++ b/src/main/res/values/colors.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <color name="colorPrimary">#008577</color>
+    <color name="colorPrimaryDark">#00574B</color>
+    <color name="colorAccent">#D81B60</color>
+    <color name="red">#ffff0000</color>
+</resources>
diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml
new file mode 100644
index 00000000..37d3c325
--- /dev/null
+++ b/src/main/res/values/strings.xml
@@ -0,0 +1,5 @@
+<resources>
+    <string name="app_name">Magic Cube</string>
+    <string name="scramble">Scramble</string>
+    <string name="credits">Credits</string>
+</resources>
diff --git a/src/main/res/values/styles.xml b/src/main/res/values/styles.xml
new file mode 100644
index 00000000..5885930d
--- /dev/null
+++ b/src/main/res/values/styles.xml
@@ -0,0 +1,11 @@
+<resources>
+
+    <!-- Base application theme. -->
+    <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
+        <!-- Customize your theme here. -->
+        <item name="colorPrimary">@color/colorPrimary</item>
+        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
+        <item name="colorAccent">@color/colorAccent</item>
+    </style>
+
+</resources>
