3D Page Curl Animation on Android

Advancements in computer graphics technologies have taken computers more towards the real world. We can now simulate the real world scenarios in computer like special effects in movies, animations, CAD/CAM etc. My this article is more devoted to the same. In this article I am gonna discuss how to simulate the page curl effect in 3D world using OpenGL on Android platform. In this article I am not going to discuss about the technical details of the OpenGL rendering. I would discuss only about the mathematics and logic behind this animation. The picture below depicts what we gonna discuss.


This demo is very simple page curl in which page is curled form one edge uniformly as shown in figure:(a). To withdraw the mathematical equation of the curl we assume that the page is made of infinite parallel curves as shown in figure:(b). We would calculate the equation for one curve as shown in the figure:(c). We also assume that curve is formed of line segments drawn by joining the points shown in figure:(d). We imagine a circle moving linearly along the line and as it moves points get wrap around the circle and the circle moves quantized i.e. on points and not in between them. The same thing is applied to each curve to achieve page curl animation.

The class ‘Page’ holds all those calculations which we discussed above.

package com.page.curl;

import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import java.nio.ShortBuffer;
import javax.microedition.khronos.opengles.GL10;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.opengl.GLUtils;

public class Page {
	public final int GRID = 25;
	private final float RADIUS = 0.2f;
	public float curlCirclePosition = 25;

	private FloatBuffer vertexBuffer;
	private FloatBuffer textureBuffer;
	private ShortBuffer indexBuffer;
	private int[] textures = new int[1];
	private float vertices[] = new float[(GRID+1)*(GRID+1)*3];
	private float texture[] = new float[(GRID+1)*(GRID+1)*2];
	private short indices[] = new short[GRID*GRID*6];

	public Page() {
		calculateVerticesCoords();
		calculateFacesCoords();
		calculateTextureCoords();
		ByteBuffer byteBuf = ByteBuffer.allocateDirect(texture.length * 4);
		byteBuf.order(ByteOrder.nativeOrder());
		textureBuffer = byteBuf.asFloatBuffer();
		textureBuffer.put(texture);
		textureBuffer.position(0);

	        byteBuf = ByteBuffer.allocateDirect(indices.length * 4);
		byteBuf.order(ByteOrder.nativeOrder());
		indexBuffer = byteBuf.asShortBuffer();
		indexBuffer.put(indices);
		indexBuffer.position(0);
	}

	public void calculateVerticesCoords(){
		float angle = 1.0f/((float)GRID*RADIUS);
		for(int row=0;row<=GRID;row++)
			for(int col=0;colcurlCirclePosition){
					vertices[pos]=(float) ((curlCirclePosition/GRID)+RADIUS*Math.sin(angle*(col-curlCirclePosition)));
					vertices[pos+2]=(float) (RADIUS*(1-Math.cos(angle*(col-curlCirclePosition))));
				}else{
				        vertices[pos]=(float)col/(float)GRID;
					vertices[pos+2]=0;
				}
				vertices[pos+1]=(float)row/(float)GRID;
			}
		ByteBuffer byteBuf = ByteBuffer.allocateDirect(vertices.length * 4);
		byteBuf.order(ByteOrder.nativeOrder());
		vertexBuffer = byteBuf.asFloatBuffer();
		vertexBuffer.put(vertices);
		vertexBuffer.position(0);
	}

	private void calculateFacesCoords(){
		for(int row=0;row<GRID;row++)
			for(int col=0;col<GRID;col++){
			        int pos = 6*(row*GRID+col);
				indices[pos] = (short) (row*(GRID+1)+col);
				indices[pos+1]=(short) (row*(GRID+1)+col+1);
				indices[pos+2]=(short) ((row+1)*(GRID+1)+col);
				indices[pos+3]=(short) (row*(GRID+1)+col+1);
				indices[pos+4]=(short) ((row+1)*(GRID+1)+col+1);
				indices[pos+5]=(short) ((row+1)*(GRID+1)+col);
			}
	}

	private void calculateTextureCoords(){
		for(int row=0;row<=GRID;row++)
	        	for(int col=0;col<=GRID;col++){
				int pos = 2*(row*(GRID+1)+col);
				texture[pos]=col/(float)GRID;
				texture[pos+1]=1-row/(float)GRID;
			}
	}

	public void draw(GL10 gl) {
		calculateVerticesCoords();	
		gl.glBindTexture(GL10.GL_TEXTURE_2D, textures[0]);
		gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
		gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
		gl.glFrontFace(GL10.GL_CCW);
		gl.glVertexPointer(3, GL10.GL_FLOAT, 0, vertexBuffer);
		gl.glTexCoordPointer(2, GL10.GL_FLOAT, 0, textureBuffer);
		gl.glDrawElements(GL10.GL_TRIANGLES, indices.length, GL10.GL_UNSIGNED_SHORT, indexBuffer);
		gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);
		gl.glDisableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
	}

	public void loadGLTexture(GL10 gl, Context context) {
		InputStream is = context.getResources().openRawResource(R.drawable.page);
		Bitmap bitmap = null;
		try {
			bitmap = BitmapFactory.decodeStream(is);
		} finally {
			try {
				is.close();
				is = null;
			} catch (IOException e) {
			}
		}
        	gl.glGenTextures(1, textures, 0);
		gl.glBindTexture(GL10.GL_TEXTURE_2D, textures[0]);
		gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_NEAREST);
		gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_LINEAR);
		gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_S, GL10.GL_REPEAT);
		gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_T, GL10.GL_REPEAT);
		GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, bitmap, 0);
		bitmap.recycle();
	}
}

Checkout code: https://github.com/manoj-chauhan/3dPageCurl

Advertisements

Sparkle (Simple Particle System) on Android Canvas

Computer graphics is really an interesting domain to work in. There are numerous topics to study in computer graphics but in this article I am gonna discuss about a simple particle system. Which I am gonna demonstrate on the Android canvas. Particle systems are used to simulate the fire, smoke, fog, explosions etc. This demonstration would simulate the sparkle effect on touching the screen of the Android device. As you can see in the video below:

Let’s first discuss about few basics of the simple particle systems. In a simple particle system every particle has a life span. Particles take birth, lives their life and dies. In a particle system there is an emitter of the particles which generates the particles. In our case the emitter is the point where the user touches the screen and the particles have the life span till they are visible on the screen of the device. As the particles go off the screen the particles get removed from the drawing list of the particles.

In the demo the class Particle represents a single particle. The init() method initializes the particle generation point (initX, initY) and its distance form that point. When the Particle object is created the particle is initialized with init() method, set the random color out of (0,1,2), direction out of NO_OF_DIRECTION directions, directionCosine and directionSine. The directionCosine and directionSine are just to improve the performance of the animation so that these values would not get calculated again and again on each drawing on the canvas. And move() is just to increase the distFromOrigin for the movement of the particle in animation.

package com.sparkle;

import java.util.Random;

public class Particle {
	public int distFromOrigin = 0;
	private double direction;
	private double directionCosine;
	private double directionSine;
	public int color;
	public int x;
	public int y;
	private int initX;
	private int initY;

	public Particle(int x, int y) {
		init(x, y);
		this.direction = 2*Math.PI * new Random().nextInt(NO_OF_DIRECTION)/NO_OF_DIRECTION;
		this.directionCosine = Math.cos(direction);
		this.directionSine = Math.sin(direction);
		this.color = new Random().nextInt(3);
	}

	public void init(int x, int y) {
		distFromOrigin = 0;
		this.initX = this.x = x;
		this.initY = this.y = y;
	}

	public synchronized void move(){
		distFromOrigin +=2;
		x = (int) (initX+distFromOrigin*directionCosine);
		y = (int) (initY+distFromOrigin*directionSine);
	}
	private final static int NO_OF_DIRECTION = 400; 

}

ParticleDrawingThread is a thread, which is responsible for drawing on the canvas of the view. Basically there are two lists of particle one is ‘mParticleList’ which has to be drawn on the canvas and another ‘mRecycleList’ which is used for holding all the particle which get off the screen and ready for the recycling. The ParticleDrawingThread removes the particles from the ‘mParticleList’ if the particles get off the screen add them to the ‘mRecycleList’. With this recycling we can reduce allocation and deallocation of the particle object which improves the performance.

package com.sparkle;

import java.util.ArrayList;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.drawable.BitmapDrawable;
import android.view.SurfaceHolder;

class ParticleDrawingThread extends Thread {

	private boolean mRun = true;

	private SurfaceHolder mSurfaceHolder;

	private ArrayList mParticleList =new ArrayList();
	private ArrayList mRecycleList =new ArrayList();

	private int mCanvasWidth;
	private int mCanvasHeight;
	private Paint mPaint;
	private Bitmap mImage[] =new Bitmap[3];

	public ParticleDrawingThread(SurfaceHolder mSurfaceHolder, Context mContext) {
		this.mSurfaceHolder = mSurfaceHolder;
		this.mPaint = new Paint();
		mPaint.setColor(Color.WHITE);
		mImage[0] =((BitmapDrawable)mContext.getResources().getDrawable(R.drawable.yellow_spark)).getBitmap();
		mImage[1] =((BitmapDrawable)mContext.getResources().getDrawable(R.drawable.blue_spark)).getBitmap();
		mImage[2] =((BitmapDrawable)mContext.getResources().getDrawable(R.drawable.red_spark)).getBitmap();

	}

	@Override
	public void run() {
		while (mRun) {
			Canvas c = null;
			try {
				c = mSurfaceHolder.lockCanvas(null);
				synchronized (mSurfaceHolder) {
					doDraw(c);
				}
			} finally {
				if (c != null) {
					mSurfaceHolder.unlockCanvasAndPost(c);
				}
			}
		}
	}

	private void doDraw(Canvas c) {
		c.drawRect(0, 0, mCanvasWidth, mCanvasHeight, mPaint);
		synchronized (mParticleList) {
			for (int i = 0; i < mParticleList.size(); i++) {
				Particle p = mParticleList.get(i);
				p.move();
				c.drawBitmap(mImage[p.color], p.x-10, p.y-10, mPaint);
				if (p.x < 0 || p.x > mCanvasWidth || p.y < 0 || p.y > mCanvasHeight) {
					mRecycleList.add(mParticleList.remove(i));
					i--;
				}
			}
		}
	}

	public void stopDrawing() {
		this.mRun = false;
	}

	public ArrayList getParticleList() {
		return mParticleList;
	}

	public ArrayList getRecycleList() {
		return mRecycleList;
	}

	public void setSurfaceSize(int width, int height) {
		mCanvasWidth = width;
		mCanvasHeight = height;
	}

}

In ParticalView main thing to concentrate about is particle generation. onTouchEvent() method is responsible for adding particle to the ‘mParticleList’. If the particle is available in the ‘mRecycleList’ it would get added from there otherwise create new particle object and add to the ‘mParticleList’.

package com.sparkle;

import java.util.ArrayList;

import android.content.Context;
import android.view.MotionEvent;
import android.view.SurfaceHolder;
import android.view.SurfaceView;

public class ParticalView extends SurfaceView implements SurfaceHolder.Callback {

	private ParticleDrawingThread mDrawingThread;

	private ArrayList mParticleList;
	private ArrayList mRecycleList;

	private Context mContext;

	public ParticalView(Context context) {
		super(context);
		SurfaceHolder holder = getHolder();
		holder.addCallback(this);
		this.mContext = context;

	}

	@Override
	public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
		mDrawingThread.setSurfaceSize(width, height);
	}

	@Override
	public void surfaceCreated(SurfaceHolder holder) {
		mDrawingThread = new ParticleDrawingThread(holder, mContext);
		mParticleList = mDrawingThread.getParticleList();
		mRecycleList = mDrawingThread.getRecycleList();
		mDrawingThread.start();
	}

	@Override
	public void surfaceDestroyed(SurfaceHolder holder) {
		boolean retry = true;
		mDrawingThread.stopDrawing();
		while (retry) {
			try {
				mDrawingThread.join();
				retry = false;
			} catch (InterruptedException e) {
			}
		}
	}

	@Override
	public boolean onTouchEvent(MotionEvent event) {
		Particle p;
		int recycleCount = 0;

		if(mRecycleList.size()>1)
			recycleCount = 2;
		else
			recycleCount =mRecycleList.size();

		for (int i = 0; i < recycleCount; i++) {
			p = mRecycleList.remove(0);
			p.init((int) event.getX(), (int) event.getY());
			mParticleList.add(p);
		}

		for (int i = 0; i < 2-recycleCount; i++)
			mParticleList.add(new Particle((int)event.getX(), (int)event.getY()));

		return super.onTouchEvent(event);
	}

}

Checkout code: https://github.com/manoj-chauhan/Sparkles

Hi!

It’s been a long time since I was thinking to write a blog of me. I tried it earlier with www.blogspot.com as www.techie-manoj.blogspot.com but I felt that’s not sufficient for my needs. So, now I have come up with this www.techie-manoj.com as my blog. Here I would be happy to share my thoughts and experiences with you people and your comments and suggestions are always welcome. Soon I would come with some interesting posts to share with you people.