/*
	Shaded Spheres - Written by Brandon Hawker

	This applet draws several two-dimensional, colored circles, and uses shading with
	a light source to make them appear as though they are three-dimensional spheres. Each
	sphere is drawn with different coordinates in the z-plane, so some part of each sphere
	intersects with part of another sphere.

	To deal with this issue, the Z-buffer algorithm is implemented. Two arrays are used,
	each with an element per pixel in the image. One (zPixel) initially holds the color of
	the background, while the other (zBuffer) initially holds the distance from the background
	to the viewer. Every time a pixel for an object must be drawn, its distance to the viewer
	must be checked with the corresponding pixel location in the zBuffer array. If the distance
	is less than the value in the zBuffer array, then the pixel on the new sphere is closer
	to the viewer, and the pixel should be drawn. Thus, the distance from the new sphere to the
	viewer should be placed in the ZBuffer array and the color of the closer sphere should be
	placed in the zPixel array.
*/

import java.awt.*;
import java.awt.event.*;
import java.awt.image.*;
import javax.swing.*;
import javax.swing.border.*;

public class ShadedSpheres extends JApplet implements ActionListener {

	// Window width;
	private static int WIDTH = 400;

	private int pixels[], zPixels[];
	private double lightX, lightY, lightZ, lightMag, zBuffer[];
	private JTextField lightXfield, lightYfield, lightZfield;
	private JButton redraw;
	private JLabel lightXlabel, lightYlabel, lightZlabel, invalidLabel;
	private boolean invalidCoords;

	public void init() {
		// Set default light source coordinates.
		lightX = 0;
		lightY = 0;
		lightZ = -1;

		// Calculate light source magnitude.
		lightMag = Math.sqrt((lightX * lightX) + (lightY * lightY) + (lightZ * lightZ));
		lightX /= lightMag;
		lightY /= lightMag;
		lightZ /= lightMag;

		// Create ContentPane and fill it with the text fields, labels, and a reddraw button.
		Container content = getContentPane();
		content.setBackground(Color.lightGray);
		content.setLayout(new FlowLayout());
		lightXfield = new JTextField(""+lightX,5);
		lightYfield = new JTextField(""+lightY,5);
		lightZfield = new JTextField(""+lightZ,5);
		lightXlabel = new JLabel("X:");
		lightYlabel = new JLabel("Y:");
		lightZlabel = new JLabel("Z:");
		invalidLabel = new JLabel("");
		redraw = new JButton("Redraw Spheres");

		// Add all content items.
		content.add(lightXlabel);
		content.add(lightXfield);
		content.add(lightYlabel);
		content.add(lightYfield);
		content.add(lightZlabel);
		content.add(lightZfield);
		content.add(redraw);
		content.add(invalidLabel);

		redraw.addActionListener(this);
		invalidCoords = false;
		}

	// paint() paints all of the spheres on-screen.

	public void paint(Graphics g) {
		super.paint(g);
		if (pixels == null) {
			// Create array for pixels, zPixels, and zBuffer.
			pixels = new int[WIDTH * WIDTH];
			zPixels = new int[WIDTH * WIDTH];
			zBuffer = new double[WIDTH * WIDTH];
			for (int n = 0; n < zPixels.length; n++) {												// Fill ZBuffer array and array of background pixels.
				setPixel(zPixels,n,0,Color.lightGray);
				zBuffer[n] = 400000;
			}
		}
		if (invalidCoords == true) {
			// invalidCoords is true, meaning the coordinates entered by the user are invalid.
			invalidLabel.setText("Invalid coordinates. Please enter a valid number for each coordinate.");
			invalidCoords = false;
		}
		else invalidLabel.setText("");
		// Paint all shaded spheres on-screen.
		paintSpheres(g);
		// Reset the pixel, zPixels, and zBuffer arrays.
		pixels = null;
		zPixels = null;
		zBuffer = null;
	}

	// update() clears the screen and calls paint. It is called by repaint().

	public void update(Graphics g) {
		paint(g);
	}

	// paintSpheres() determines the color of each pixel for four shaded spheres and then creates an image
	// which actually colors the pixels.

	public void paintSpheres(Graphics g) {
		// Fill in pixels of all four spheres.
		fillPixels(60,60,20,50,Color.blue);
		fillPixels(35,35,5,30,Color.red);
		fillPixels(225,120,0,80,new Color(150,250,50));
		fillPixels(300,100,-30,40,Color.white);
		// Creates an image containing pixels of all four spheres, and draws it at coordinates (50,50).
		Image spheres = createImage(new MemoryImageSource(WIDTH,WIDTH,pixels,0,WIDTH));
		g.drawImage(spheres,50,80,null);
	}

	// setPixel() sets the WIDTH*y+x pixel as Color c.

	private void setPixel(int pixels[], int x, int y, Color c) {
		pixels[WIDTH * y + x] = 0xff000000 | c.getRed() << 16 | c.getGreen() << 8 | c.getBlue();
	}

	// fillPixels() colors the pixels for a circle with a set of coordinates (cx,cy,cz), the radius of
	// the circle, and its color.
	private void fillPixels(int cx, int cy, int cz, int rad, Color color) {
		for (int y = cy - rad; y <= cy + rad; y++) {
			int dy = cy - y;
			int halfWidth = (int)Math.sqrt((rad * rad) - (dy * dy));
			int firstX = cx - halfWidth, lastX = cx + halfWidth;
			for (int x = firstX; x <= lastX; x++) {
				int dx = cx - x;
				double dz =  cz - Math.sqrt((rad * rad) - (dx * dx) - (dy * dy));
				// Calculate magnitude at point (dx,dy,dz).
				double dMag = Math.sqrt((dx * dx) + (dy * dy) + (dz * dz));
				// Calculate color intensity at point (dx,dy,dz).
				double intensity = ((lightX * (double)dx) + (lightY * (double)dy) + (lightZ * dz))/dMag;
				// If intensity is negative, set it to 0.
				if (intensity < 0) intensity = 0;
				// Make the minimum intensity .3 and the maximum intensity (.7 * intensity + .3). These
				// are arbitrary figures.
				intensity = intensity * .7 + .3;
				// Use intensity to determine shaded color for this pixel.
				Color newColor = new Color((int)(color.getRed() * intensity),(int)(color.getGreen() * intensity), (int)(color.getBlue() * intensity));
				// Compare distance from pixel to distance stored in ZBuffer.
				if (dz < zBuffer[WIDTH * y + x]) {
					// dz is less, so store the new distance in the ZBuffer array.
					zBuffer[WIDTH * y + x] = dz;
					// Set the color of the sphere in the pixels and zPixels arrays.
					setPixel(pixels,x,y,newColor);
					setPixel(zPixels,x,y,newColor);
				}
				else pixels[WIDTH * y + x] = zPixels[WIDTH * y + x];
			}
		}
	}

	// actionPerformed() handles the redraw button press.

	public void actionPerformed(ActionEvent e) {
		// If redraw button is pressed, the user wants to repaint the spheres (possibly) based on
		// new light source coordinates they have entered.
		if (e.getSource() == redraw) {
			// Check if each of the entered light source coordinates is a valid double.
			try {
				lightX = Double.parseDouble(lightXfield.getText());
				lightY = Double.parseDouble(lightYfield.getText());
				lightZ = Double.parseDouble(lightZfield.getText());
			}
			catch (Exception ex) {
				// One or more of the coordinates entered is invalid (not a double).
				// Set the light coordinates back to their default values, and set invalidCoords to true.
				lightX = 0;
				lightY = 0;
				lightZ = -1;
				invalidCoords = true;
			}
			// Stick the light coordinates back into the text fields.
			lightXfield.setText(""+lightX);
			lightYfield.setText(""+lightY);
			lightZfield.setText(""+lightZ);
			// Recalculate light source magnitude and repaint the spheres using this magnitude.
			lightMag = Math.sqrt((lightX * lightX) + (lightY * lightY) + (lightZ * lightZ));
			lightX /= lightMag;
			lightY /= lightMag;
			lightZ /= lightMag;
			repaint();
		}
	}
}
