# A Ripple Effect for Android

The code in this article shows you how to apply a wave effect over a bitmap. Unlike many apps in the Google Play Store, this is a genuine wave effect that implements the 2d Wave Equation:

$\frac{{\partial }^{2}u}{\partial {t}^{2}}={c}^{2}\left(\frac{{\partial }^{2}u}{\partial {x}^{2}}+\frac{{\partial }^{2}u}{\partial {y}^{2}}\right)$

2d Wave Equation

Here is a sample animation:

The code is presented below with comments.  A full physical model that models specific fluid depths, refractive indices, etc is not required. Instead, practical end-effect constants are defined. E.g. a simple refractive index ratio, a scale factor on the refraction displacement, etc. For speed and battery conservation, the waves are computed on a grid 1/32th the size of the bitmap. The choice of 32 allows quick bit-shift conversion between the wave grid and the pixel grid. 2d linear interpolation is then used to convert the calculated refraction deflections to the bitmap’s pixel grid.

The code includes a ‘Poke‘ function which simulates a finger touching the air/water boundary. This would typically be hooked up to a touch or mouse event. This simulation raises a circular area of the wavefield to the maximum amplitude. Multiple closely-spaced calls to Poke can be used to simulate a dragged finger.

Here is the code:

```package com.winwaed.ripples;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.RectF;

/**
* Ripple Data Model
* Computes ripples and applies them to a base bitmap.
*/

class RippleBox {

// Amplitude and size of a 'poke' - eg. from a finger touch
final static double POKE_AMPLITUDE = 200.0;
final static int POKE_RADIUS = 3;

// max wave amplitude
final static double MAX_AMPLITUDE = 255.0;

// Elasticity<1.0 allows for some absorption - waves slowly die away
final static double ELASTICITY = 0.99;  // 1.0 = fully elastic : Absorption/delta-t = 1.0 - ELASTICITY

// ratio of refractive indices - controls the amount of refraction
final static double REFRACTIVE_RATIO = 0.75;

final static double REFRACTIVE_RATIO_SQ = REFRACTIVE_RATIO*REFRACTIVE_RATIO;

final static double GRADIENT_SCALE = 0.005; //0.01; // vertical scale on ripples when converting for refraction
final static double DISPLACEMENT_SCALE = 500.0;  // multiplier for the final pixel displacement

// speed of wave propagation
final static double c = 0.3333;

final static int PIXEL_BITSHIFT = 5; // log2 of PIXEL_SIZE
final static int PIXEL_BITSHIFT_SQ = 10; // double - for use in squared divisions
final static int PIXEL_SIZE = 1<<PIXEL_BITSHIFT;  // Actual pixel size in the refraction model

final static double PIXEL_SIZE_RECIP = 1.0 / (double) PIXEL_SIZE;

final static double C2 = c*c;
final static double D1 = 2.0;
final static double D2 = 1.0;

// for wave model
int nWidth, nHeight;

// wave model
double [][] displ;
double [][] displ_1, displ_2;

// output image
int[] outpixels;
Bitmap imgBitmap;

int [] inpixels;  // stores the input bitmap pixels
int pxWidth,pxHeight; // display

/**
* constructor with a default bitmap
*/
public RippleBox(Context context, int width, int height) {

imgBitmap = Bitmap.createBitmap(width,height, Bitmap.Config.ARGB_8888);
pxWidth = width;
pxHeight = height;
nWidth = width >> PIXEL_BITSHIFT;
nHeight = height >> PIXEL_BITSHIFT;
displ = new double[nWidth][nHeight];
displ_1 = new double[nWidth][nHeight];
displ_2 = new double[nWidth][nHeight];

SetBaseBitmap( BitmapFactory.decodeResource(context.getResources(), R.drawable.base_image) );
outpixels = new int[pxWidth * pxHeight];

ClearData();

}

/**
* Set the entire wavefield to 0 - ie. flat
*/
public void ClearData()
{
for (int i=0; i<nWidth; i++)
for (int j=0; j<nHeight; j++)
{
displ[i][j] = 0.0;
displ_1[i][j] = 0.0;
displ_2[i][j] = 0.0;
}
}

/**
* Apply a 'poke' at pixel position xx,yy
* Simulates a finger touching the water surface
*/
public void Poke(int xx, int yy)
{
// xx and yy are screen coords: convert to ripple coords
int x = xx >> PIXEL_BITSHIFT;
int y = yy >> PIXEL_BITSHIFT;
if (x<0 || y<0 || x>=nWidth || y>=nHeight)
return;

if (x + i > 0 && x + i < nWidth-1)
if (y + j > 0 && y + j < nHeight-1) {
double this_displ = POKE_AMPLITUDE * 0.5 * (1.0 + Math.cos(Math.sqrt(i * i + j * j) * Math.PI / POKE_RADIUS));
displ[x + i][y + j] += this_displ;
if (displ[x + i][y + j] > MAX_AMPLITUDE)
displ[x + i][y + j] = MAX_AMPLITUDE;

displ_1[x + i][y + j] += this_displ;
if (displ_1[x + i][y + j] > MAX_AMPLITUDE)
displ_1[x + i][y + j] = MAX_AMPLITUDE;

}
}
}

/**
* Update the waves (and hence ripple effect) with one time increment
*/
public void update()  {
RefractRipples();
}

/**
*  Returns a bitmap of the refracted image
*/
public Bitmap getBitmap()   {

imgBitmap.setPixels(outpixels, 0, pxWidth,  0,0, pxWidth,pxHeight);

return imgBitmap;
}

/**
* Advance the waves (ripples) by one time increment
*/
{
double[][] tempd = displ_2;
displ_2 = displ_1;
displ_1 = displ;
displ = tempd;

double dd, uu;
for (int x=1; x<nWidth-1; x++)
for (int y=1; y<nHeight-1; y++)
{
uu = displ_1[x-1][y] - 2 * displ_1[x][y] + displ_1[x+1][y] +
displ_1[x][y-1] - 2 * displ_1[x][y] + displ_1[x][y+1];

dd = 2.0 * displ_1[x][y] - displ_2[x][y] + C2 * uu; //(u_xx + u_yy);

dd = Math.min(Math.max(dd,-MAX_AMPLITUDE), MAX_AMPLITUDE);

displ[x][y] = dd * ELASTICITY;
}
}

/**
* Apply refractions from the current wavefield to the bitmap image.
* This creates the visual distortion (aberration) in the image
*/
public void RefractRipples()
{
int w = pxWidth; //cv.getWidth();
int h = pxHeight; //cv.getHeight();

double norm[] = new double[3];

double prev_row_dx[] = new double[nWidth];
double prev_row_dy[] = new double[nWidth];
double this_row_dx[] = new double[nWidth];
double this_row_dy[] = new double[nWidth];
double trow[];

for (int y = 1; y < nHeight - 1; y++)
{
// calc new row of displacements
trow = prev_row_dx;
prev_row_dx = this_row_dx;
this_row_dx = trow;
trow = prev_row_dy;
prev_row_dy = this_row_dy;
this_row_dy = trow;

for (int x = 1; x < nWidth - 1; x++)
{
// find normal by averaging the four triangles that share this point
// xn[x][y] = 0.0; yn[x][y] = 0.0; zn[x][y] = 0.0;
norm[0] = 0.0;
norm[1] = 0.0;
norm[2] = 0.0;

XProductAndSum(displ[x][y], displ[x + 1][y], displ[x][y + 1], 1.0, 1.0, norm);
XProductAndSum(displ[x][y], displ[x + 1][y], displ[x][y - 1], 1.0, -1.0, norm);
XProductAndSum(displ[x][y], displ[x - 1][y], displ[x][y + 1], -1.0, 1.0, norm);
XProductAndSum(displ[x][y], displ[x - 1][y], displ[x][y - 1], -1.0, -1.0, norm);

// normalise the normal!
double m = Math.sqrt(norm[0] * norm[0] + norm[1] * norm[1] + norm[2] * norm[2]);
norm[0] /= m;
norm[1] /= m;
norm[2] /= m;

double cos1 = norm[2];
double sin1sq = norm[0] * norm[0] + norm[1] * norm[1];
double sin1 = Math.sqrt(sin1sq);

double sin3DS = -DISPLACEMENT_SCALE * sin1 * (Math.sqrt(1.0 - REFRACTIVE_RATIO_SQ * sin1sq) - cos1 * REFRACTIVE_RATIO);

// sin3 is proportional to the sideways aberration
// direction of this aberration is opposite to norm[0],norm[1]
this_row_dx[x] = norm[0] * sin3DS;
this_row_dy[x] = norm[1] * sin3DS;
}

// deal with edges
this_row_dx[0] = this_row_dx[1];
this_row_dy[0] = this_row_dy[1];
this_row_dx[nWidth - 1] = this_row_dx[nWidth - 2];
this_row_dy[nWidth - 1] = this_row_dy[nWidth - 2];

if (y == 1) {   // make data good for the top row
System.arraycopy(this_row_dx, 0, prev_row_dx, 0, nWidth);
System.arraycopy(this_row_dy, 0, prev_row_dy, 0, nWidth);
}

// Loop over these squares, filling in with interpolation
int yps = y << PIXEL_BITSHIFT;
for (int x = 0; x < nWidth - 1; x++)
{
int xps = x << PIXEL_BITSHIFT;

int dx_y1 = (int)Math.round(prev_row_dx[x]);
int dx_y2 = (int)Math.round(prev_row_dx[x+1]);
int dx_y3 = (int)Math.round(this_row_dx[x+1]);
int dx_y4 = (int)Math.round(this_row_dx[x]);
int dy_y1 = (int)Math.round(prev_row_dy[x]);
int dy_y2 = (int)Math.round(prev_row_dy[x+1]);
int dy_y3 = (int)Math.round(this_row_dy[x+1]);
int dy_y4 = (int)Math.round(this_row_dy[x]);
//int t=0;
int mt = PIXEL_SIZE;
//double t = 0.0;
int xx = xps;
for (int t = 0; t < PIXEL_SIZE; xx+=2, t+=2,mt-=2)
{
int mu = PIXEL_SIZE;
int yy = yps;
for (int u = 0; u < PIXEL_SIZE; yy+=2, u+=2,mu-=2)
{
int dx = ( mt*mu*dx_y1 + t*mu*dx_y2 + t*u*dx_y3 + mt*u*dx_y4 ) >> PIXEL_BITSHIFT_SQ;
int dy = ( mt*mu*dy_y1 + t*mu*dy_y2 + t*u*dy_y3 + mt*u*dy_y4 ) >> PIXEL_BITSHIFT_SQ;

if (dx +xx+1 < w  &&  dx + xx >= 0  &&
dy + yy+1 < h   &&  dy +yy >= 0)
{
outpixels[xx + w * yy] = inpixels[dx + xx + w * (dy + yy)];
outpixels[xx + w+ w * yy] = inpixels[dx + xx + w * (dy + yy+1)];
outpixels[xx+1 + w * yy] = inpixels[dx + xx+1 + w * (dy + yy)];
outpixels[xx+1 + w + w * yy] = inpixels[dx + xx+1 + w * (dy + yy+1)];
}
else
{
outpixels[xx + w * yy] = Color.rgb(0,0,0);
outpixels[xx + w+ w * yy] = Color.rgb(0,0,0);
outpixels[xx+1 + w * yy] = Color.rgb(0,0,0);
outpixels[xx+1 + w+ w * yy] = Color.rgb(0,0,0);
}
}
}

}
}

}

/**
* Draw the wavefield to a bitmap. Useful for debugging
*/
public void DrawToBitmap( Bitmap bmp)
{
Canvas cv = new Canvas(bmp);
int w = cv.getWidth();
int h = cv.getHeight();

for (int x=0; x<nWidth; x++)
for (int y=0; y<nHeight; y++)
{
int d = (int)(displ[x][y]);
int c;
if (d<0)
c = Color.rgb( (-d)%256,0,0);
else c = Color.rgb( 0, d%256,0);
Paint pp = new Paint();
pp.setColor(c);
pp.setStyle(Paint.Style.FILL);
cv.drawRect(x*32,y*32,x*32+32, y*32+32, pp);
}
}

/**
* Set the base bitmap - ie. extracts the pixels into an array for future use
* @param bmp
*/
public void SetBaseBitmap( Bitmap bmp)
{
int w = bmp.getWidth();
int h = bmp.getHeight();

float xScale = (float)pxWidth / (float)w;
float yScale = (float)pxHeight / (float)h;
float scale = Math.max(xScale, yScale);

float scaledWidth = scale * w;
float scaledHeight = scale * h;

float left = (pxWidth - scaledWidth) / 2;
float top = (pxHeight - scaledHeight) / 2;

RectF targetRect = new RectF(left, top, left + scaledWidth, top + scaledHeight);

Bitmap bm = Bitmap.createBitmap(pxWidth, pxHeight, bmp.getConfig() );
Canvas canvas = new Canvas(bm);

canvas.drawBitmap(bmp, null, targetRect, null);

inpixels = new int[pxWidth * pxHeight];
bm.getPixels(inpixels, 0, pxWidth, 0, 0, pxWidth, pxHeight);
}

/**
* Calculates a cross product and ADDS the result to norm
* vectors are aligned with x and y axes
* Also applies the gradient scale - converts the wave to a sensible ripple gradient
* (helper function for refraction)
*
* @param aa Central point value
* @param ax value for x direction
* @param ay value for y direction
* @param dx x displacement
* @param dy y displacement
* @param norm 3 element normal vector
*/
private void XProductAndSum( double aa, double ax, double ay, double dx, double dy, double[] norm)
{
double u3 = (ax - aa) * GRADIENT_SCALE;
double v3 = (ay - aa) * GRADIENT_SCALE;

double n1 =  /*-*/ u3*dy;
double n2 =  /*-*/ dx*v3;
double n3=  dx*dy;
if (n3 >=0.0)
{
norm[0] -= n1;
norm[1] -= n2;
norm[2] += n3;
}
else
{
norm[0] += n1;
norm[1] += n2;
norm[2] -= n3;
}
}

}
```