source:

import java.applet.Applet;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;

import javax.imageio.ImageIO;


public class PoolTest extends Applet implements Runnable {

	private static final int TEX_SIZE = 128;
	private static final int BALL_SIZE = 64;
	private static final float TABLE_WIDTH = 640 - BALL_SIZE;
	private static final float TABLE_HEIGHT = 480 - BALL_SIZE;
	
	private Font font = new Font(Font.SANS_SERIF, Font.BOLD, 18);
	
	private List<Ball> balls = new ArrayList<Ball>();
	

	private BufferedImage tenBall = new BufferedImage(TEX_SIZE*2,TEX_SIZE*2,BufferedImage.TYPE_INT_ARGB);
	private BufferedImage backBuffer = new BufferedImage(800,600, BufferedImage.TYPE_INT_ARGB);
	private BufferedImage lightMap = new BufferedImage(BALL_SIZE,BALL_SIZE, BufferedImage.TYPE_INT_ARGB);
	
	private class Ball {
		BufferedImage img;
		BufferedImage tex;
	
		float[] rot;
		float x,y;
		float x1,y1;
		
		public Ball(BufferedImage tex, int posX, int posY){
			this.tex = tex;
			rot = new float[]{0,0,0,1};
			img = new BufferedImage(BALL_SIZE,BALL_SIZE,BufferedImage.TYPE_INT_ARGB);
			x = posX;
			y = posY;
			x1 = posX;
			y1 = posX;
		}
		
		public void setVel(float dx, float dy){
			x1 = x - dx;
			y1 = y - dy;
		}
		
		
	}
	
	public PoolTest(){
		// Draw the 10 ball texture.
		Graphics2D g = (Graphics2D)tenBall.getGraphics();
		g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
		g.setColor(Color.BLUE.darker());
		g.fillRect(0,0,TEX_SIZE,TEX_SIZE);
		g.setColor(Color.WHITE);
		g.fillRect(0, 0, TEX_SIZE, TEX_SIZE/4);
		g.fillRect(0, 3*TEX_SIZE/4,TEX_SIZE,TEX_SIZE/4);
		g.translate(TEX_SIZE/2,TEX_SIZE/2);
		g.scale(0.5, 1.0);
		int diameter = TEX_SIZE/4; 
		g.fillOval(-diameter/2,-diameter/2,diameter,diameter);
		g.setColor(Color.BLACK);
		g.setFont(font);
		int w = g.getFontMetrics().stringWidth("10");
		int h = g.getFontMetrics().getAscent();
		g.drawString("10",-w/2,h/3);
		g.dispose();
		
		float lightX = -0.4f;
		float lightY = -0.4f;
		// Draw the light map.
		for(int ix = 0; ix < BALL_SIZE; ix++){
			for(int iy = 0; iy < BALL_SIZE; iy++){
				float x = (2*ix - BALL_SIZE)/(float)BALL_SIZE;
				float y = (2*iy - BALL_SIZE)/(float)BALL_SIZE;
				if(x*x + y*y < 1.0f){
					float distLight = (x-lightX)*(x-lightX) + (y-lightY)*(y-lightY);
					int alpha = 0;
					int color = 0;
					if (distLight < 0.2){
						alpha = (int)((0.2f - distLight)*255);
						color = 0xFFFFFF;
					} else if(distLight > 0.5){
						alpha = (int)((distLight - 0.5)*128);
					}
					lightMap.setRGB(ix,iy,color | (alpha >> 24));
				}
			}
		}
	
		try {
			BufferedImage white = ImageIO.read(new URL("http://www.angryoctopus.co.nz/java4k/white_ball.png").openStream());
			Ball whiteBall = new Ball(white,50,50);
			whiteBall.setVel(-1f, -0.5f);
			balls.add(whiteBall);
		} catch (MalformedURLException e) {
			e.printStackTrace();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		Ball b1 = new Ball(tenBall,100,100);
		b1.setVel(3.0f, 2.0f);
		balls.add(b1);
		for(Ball ball : balls){
			updateImage(ball);
		}
	}
	
	public void start() {
		new Thread(this).start();
	}

	public void run() {
		while(true){
			long delta = System.nanoTime();
			for(Ball ball : balls){
				updateBall(ball);
			}
			repaint();
			delta -= System.nanoTime();
			delta /= 1000000;
			if (delta < 20){
				try {
					Thread.sleep(20-delta);
				} catch (InterruptedException e) {}
			}
		}
	}
	
	public float clamp(float v, float min, float max){
		if (v < min){
			return min;
		} else if (v > max){
			return max;
		} else {
			return v;
		}
	}

	public void updateBall(Ball ball){
		float dx = ball.x - ball.x1;
		float dy = ball.y - ball.y1;
		
		float len = (float)Math.sqrt(dx*dx + dy*dy);
		if (len > 0.01f){
			rotate(ball.rot, dy/len, -dx/len,len*0.05f);
			normalize(ball.rot);
			updateImage(ball);
		}
		
		ball.x1 = ball.x;
		ball.y1 = ball.y;
		ball.x = clamp(ball.x,0,TABLE_WIDTH);
		ball.y = clamp(ball.y,0,TABLE_HEIGHT);
		ball.x += dx;
		ball.y += dy;
	}

	public void paint(Graphics g){
		Graphics2D bg = (Graphics2D)backBuffer.getGraphics();
		bg.setColor(new Color(34,85,0));
		bg.fillRect(0,0,getWidth(),getHeight());
		bg.setColor(new Color(0,0,0,64));
		bg.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
		for(Ball ball : balls){
			int x = (int)ball.x;
			int y = (int)ball.y;
			bg.fillOval(x + BALL_SIZE/4,y + BALL_SIZE/4,BALL_SIZE,BALL_SIZE);
		}
		for(Ball ball : balls){
			int x = (int)ball.x;
			int y = (int)ball.y;
			bg.translate(x,y);
			bg.drawImage(ball.img,0,0,null);
			bg.drawImage(lightMap,0,0,null);
			bg.translate(-x,-y);
		}
		bg.dispose();
		g.drawImage(backBuffer,0,0,null);
	}
	
	public void update(Graphics g){
		paint(g);
	}
	
	public void rotate(float[] q, float x, float y, float angle){
		float n = (float)Math.sqrt(x*x + y*y);
		float s = (float)Math.sin(0.5*angle)/n;
		float q2x = x*s;
		float q2y = y*s;
		float q2w = (float)Math.cos(0.5*angle);
		float dx, dy, dz, dw;
		dx = q[0] * q2w + q[3] * q2x - q[2] * q2y;
		dy = q[1] * q2w + q[3] * q2y + q[2] * q2x;
		dz = q[2] * q2w + q[0] * q2y - q[1] * q2x;
		dw = q[3] * q2w - q[0] * q2x - q[1] * q2y;
		q[0] = dx;
		q[1] = dy;
		q[2] = dz;
		q[3] = dw;
	}
	
	public void rotate(float[] q, float[] v){
		  float vx, vy, vz, vw;
		  vx = (q[3] * v[0] + q[1] * v[2] - q[2] * v[1]);
		  vy = (q[3] * v[1] + q[2] * v[0] - q[0] * v[2]);
		  vz = (q[3] * v[2] + q[0] * v[1] - q[1] * v[0]);
		  vw = (-q[0] * v[0] - q[1] * v[1] - q[2] * v[2]);
		  v[0] = vx * q[3] - vw * q[0] - vy * q[2] + vz * q[1];
		  v[1] = vy * q[3] - vw * q[1] - vz * q[0] + vx * q[2];
		  v[2] = vz * q[3] - vw * q[2] - vx * q[1] + vy * q[0];
	}
	
	
	public void normalize(float[] q){
		float len = (float)Math.sqrt(q[0]*q[0] + q[1]*q[1] + q[2]*q[2] + q[3]*q[3]);
		q[0] /= len;
		q[1] /= len;
		q[2] /= len;
		q[3] /= len;
	}
	
	public void updateImage(Ball ball){
		float[] v = new float[3];
		for(int ix = 0; ix < BALL_SIZE; ix++){
			for(int iy = 0; iy < BALL_SIZE; iy++){
				float x = (2*ix - BALL_SIZE)/(float)BALL_SIZE;
				float y = (2*iy - BALL_SIZE)/(float)BALL_SIZE;
				float z2 = (x*x + y*y); 
				if (z2 < 1.0f){
					float z = (float)Math.sqrt(1.0f - z2);
					v[0] = x;
					v[1] = y;
					v[2] = z;
					
					int alpha = (int)(z*2.0f*255);
					if (alpha > 255) alpha = 255;
					alpha = (alpha << 24);
					
					rotate(ball.rot, v);
					float theta = (float)Math.acos(v[2]);
					float phi = (float)(Math.atan2(v[1], v[0]) + Math.PI);
					int ty = (int)((theta/Math.PI)*TEX_SIZE);
					int tx = TEX_SIZE - (int)((phi/(Math.PI*2))*TEX_SIZE);
					tx &= TEX_SIZE-1;
					ty &= TEX_SIZE-1;
					ball.img.setRGB(ix,iy,(ball.tex.getRGB(tx, ty) & 0xFFFFFF) | alpha);
				} else {
					ball.img.setRGB(ix,iy,0);
				}
			}
		}
	}

	
	
}