import java.awt.*;
import java.applet.*;
import java.awt.image.*;
import java.awt.event.*;

public class SinusMap extends Applet implements Runnable, KeyListener
{
  Thread runner = null;

  //Frames per sec messeasueres.
  long startTime = 0;
  double ftp = 0;
  int nrOfFrames = 0;

  //Next som boring dimensions variabels, mainly used for copying parts of the image
  //to other parts.
  int width = 400;
  int height = 200;
  int nrOfPixels = width*height;
  int halfWidth = width/2;
  int halfWidthMinusOne = halfWidth-1;
  int halfHeight = height/2;
  int halfHeightMinusOne = halfHeight-1;
  double widthIncrease = 1F/(width);
  double heightIncrease = 1F/(height);

  double m = 0; //the sinus stretch factor.
  float n = 0;  //the zoom factor.
  int k = 0;    //k is used together with n, to time the zoom in and out routine.
  SineTexture st = new SineTexture(m, m, 2);

  //Controles.
  boolean zoom = false;
  boolean colorcycle = true;
  int speed = 10;
  boolean blend = false;
  boolean mix = false;
  boolean rotate = false;
  boolean stop = false;
  int angel = 1;
  Font font = new Font("arial", Font.PLAIN, 30);

  WritableImage wi; //The image witch we manipulate all the time.

  public void init()
  {
    startTime= System.currentTimeMillis();
    new SinCosTabel();
	addKeyListener(this);
    setSize(width, height);
	wi = new WritableImage(width,height);
	calculateN();
  }
  public void start()
  {
	runner = new Thread(this);
	runner.start();
  }
  public void stop()
  {
    runner.interrupt();
    runner = null;
  }
  void calculateN()
  {
    widthIncrease = 1F/(width/(n+1));
    heightIncrease = 1F/(height/(n+1));
  }
  public void update(Graphics g)
  {
    if(zoom)
    {
      if((k/50)%2 == 0)
        n += 0.1F;
      else
        n -= 0.1F;
      k++;
      //if we have changed the dimensions of the texture we have to recalculate the space between
      //pixels at the x and y axel.
      calculateN();
    }
    if(!stop)
      st.change(m, m);
    if(colorcycle)
      st.changeColor();

    double heightMove = 0;
    double widthMove = 0;
    int color, newY, newX, oldY=0, oldX=0;
    //calculate pixels for upper right quadrant of the sinus texture.
    for(int y = 0; y < halfHeight; y++)
    { widthMove = 0;
      for(int x = 0; x < halfWidth; x++)
      {
        if(rotate)
        { oldX = x; oldY = y;
          newX = (int)(x * SinCosTabel.cos[angel] - y * SinCosTabel.sin[angel]);
          newY = (int)(x * SinCosTabel.sin[angel] + y * SinCosTabel.cos[angel]);
          x = newX; y = newY;
        }
        if(x >= 0 && x < halfWidth && y >= 0 && y > 8))+1);
            int newRed = 255 - Math.abs((0xffffff00 | (color >> 16))+1);
            int oldBlue = 255-Math.abs((0xffffff00 | oldColor)+1);
            int oldGreen = 255-Math.abs((0xffffff00 | (oldColor >> 8))+1);
            int oldRed = 255 - Math.abs((0xffffff00 | (oldColor >> 16))+1);

            color = 0xff000000 | ((newRed+oldRed)>>1/*div with 2*/) << 16
                               | ((newGreen+oldGreen)>>1) << 8
                               | ((newBlue+oldBlue)>>1) << 0;
	      }
	      if(blend)
	        color = (int)((color+wi.pixels[dY+deformedX])/2);


          //We only calculate the pixels to the upper right quadrant of the picture.
          //Then we simply copy the quadrant to the others, by miorring it. =>
          wi.pixels[dY+deformedX] = color;
          wi.pixels[dY+dX] = color;
          wi.pixels[dY2+deformedX] = color;
          wi.pixels[dY2+dX] = color;

          widthMove += widthIncrease;
        }
        if(rotate)
        { x = oldX; y = oldY; }
      }
      heightMove += heightIncrease;
    }
    ((Graphics2D)g).drawImage(wi.getNextFrame(), 0, 0, this);

    if(!stop)
      m += 0.2;
    if(rotate)
    { angel++;
      if(angel == 90)
        angel = 0;
    }

    //Doing frames pr sec calculations.
    nrOfFrames++;
    if(nrOfFrames == 10)
    {
      startTime = System.currentTimeMillis() - startTime;
      if(startTime != 0)
        ftp = (double)(5000/startTime);
      else
        ftp = 1000;
      startTime = System.currentTimeMillis();
      nrOfFrames = 0;
    }

    ((Graphics2D)g).setColor(Color.white);
    ((Graphics2D)g).drawString("FPS: "+ftp, 0, 20);

  }
  public void run()
  {
    try
    { while(!runner.interrupted())
      {
        repaint();
        runner.sleep(speed);
      }
    }catch(InterruptedException e) {}
  }

  public void keyPressed(java.awt.event.KeyEvent evt)
  {
    char code = evt.getKeyChar();
    int intCode = evt.getKeyCode();
    switch(intCode)
    { case KeyEvent.VK_UP: n += 0.1; calculateN(); break;
      case KeyEvent.VK_DOWN: n -= 0.1; calculateN(); break;
      case KeyEvent.VK_LEFT: if(m > 0.4) m -= 0.2; break;
      case KeyEvent.VK_RIGHT: m += 0.2; break;
    }

    switch(code)
    {
      case '1': st.type = st.RING; break;
      case '2': st.type = st.SQUARE; break;
      case '3': st.type = st.WAVE; break;
      case '4': st.type = st.SPIKEWAVE; break;
      case '5': st.type = st.ELIPSE; break;
      case '6': st.type = st.PARABEL; break;
      case '7': st.type = st.LINES; break;
      case '8': st.type = st.CELLS; break;
      case '9': st.type = st.CULLS; break;
      case '0': st.type = st.EXP; break;
      case 'z': zoom = !zoom; break;
      case 'c': colorcycle = !colorcycle; break;
      case 'q': st.modFunction = 2; break;
      case 'w': st.modFunction = 3; break;
      case 'e': st.modFunction = 4; break;
      case 't': st.modFunction = 5; break;
      case '-': speed += 2; break;
      case '+': if(speed > 2) speed -= 2; break;
      case 'b': blend = !blend; break;
      case 'm': mix = !mix; break;
      case 'r': rotate = !rotate; break;
      case 's': stop = !stop; break;
      default: break;
    }
  }
  public void keyTyped(java.awt.event.KeyEvent evt) {}
  public void keyReleased(java.awt.event.KeyEvent evt) {}
}

class SineTexture
{
  double multiplier, scale;
  int modFunction;
  int n = 0;
  int m = 0;

  static final int RING = 0;
  static final int SQUARE = 1;
  static final int WAVE = 2;
  static final int SPIKEWAVE = 3;
  static final int ELIPSE = 4;
  static final int PARABEL = 5;
  static final int EXP = 6;
  static final int LINES = 7;
  static final int CELLS = 8;
  static final int CULLS = 9;

  static int type = SQUARE;

  SineTexture (double multiplier, double scale, int modFunction)
  {
    this.multiplier = multiplier;
    this.scale = scale;
    this.modFunction = modFunction;
  }
  public void change(double multiplier, double scale)
  { this.multiplier = Math.exp(multiplier);
    this.scale = scale;
  }
  public void changeColor()
  {
    if((m/255)%2 == 0)
      n++;
    else n--;
    m++;
  }
  public int getTexel (double i, double j)
  {
    double f = 0;
    switch(type)
    {  case SQUARE: f = scale * (Math.tan(j) + Math.sin(i)); break;
       case RING: f = scale * (Math.cos(j) + Math.cos(i)); break;
       case WAVE: f = scale * (Math.cos(j) + Math.sin(i)); break;
       case SPIKEWAVE: f = scale * (Math.acos(j) + Math.sin(i)); break;
       case ELIPSE: f = scale * (Math.acos(j) + Math.cos(i)); break;
       case PARABEL: f = scale * i*i + scale*j + i; break;
       case LINES: f = scale * i  + j; break;
       case CELLS: f = scale *i*j + j; break;
       case CULLS: f = Math.sqrt(scale * (Math.cos(j) + Math.sin(i))scale *scale *j * j*i); break;
       case EXP: f = Math.sqrt(scale *scale *j * i ); break;
    }
    int revN = Math.abs(n-255);
    switch((int)(f % modFunction))
    { case 0: return 0xff000000 |  revN << 16 | 0 << 8 | 0<< 0;
      case 1: return 0xff000000 |  0 << 16 | n << 8 | revN << 0;
      case 2: return 0xff000000 | n << 16 | revN << 8 | 0 << 0;
      case 3: return 0xff000000 | 0 << 16 | revN << 8 | n << 0;
      default: return 0xff000000 | n << 16 | revN << 8 | revN << 0;
    }
  }
}
class SinCosTabel
{
  public static double sin[] = new double[361];
  public static double cos[] = new double[361];
  SinCosTabel()
  {
    for(int i = 0; i < 361; i++)
    {
      sin[i] = Math.sin(Math.toRadians(i));
      cos[i] = Math.cos(Math.toRadians(i));
    }
  }
}

class WritableImage implements ImageProducer
{
  ImageConsumer consumer;
  Image image = null;
  int width,height;
  ColorModel cm;
  int[] pixels;
  private int hints,sfd;

  WritableImage(int width, int height)
  {
    this.width=width;
    this.height=height;
    this.cm = new DirectColorModel(32,0xFF0000,0xFF00,0xFF);
    this.pixels = new int[width*height];
    hints = ImageConsumer.TOPDOWNLEFTRIGHT
          | ImageConsumer.COMPLETESCANLINES
          | ImageConsumer.SINGLEPASS
          | ImageConsumer.SINGLEFRAME;
    sfd = ImageConsumer.SINGLEFRAMEDONE;
    image = Toolkit.getDefaultToolkit().createImage(this);
  }
  public Image getNextFrame()
  {
    update();
    return image;
  }
  public synchronized void addConsumer(ImageConsumer consumer)
  {
    this.consumer = consumer;
  }
  public final void startProduction(ImageConsumer imageconsumer)
  {
    if(consumer != imageconsumer)
    {
      consumer = imageconsumer;
      consumer.setDimensions(width,height);
      consumer.setProperties(null);
      consumer.setColorModel(cm);
      consumer.setHints(hints);
    }
    consumer.setPixels(0, 0, width, height, cm, pixels, 0, width);
    consumer.imageComplete(sfd);
  }
  public void update()
  {
    if(consumer != null)
      startProduction(consumer);
  }
  public final boolean isConsumer(ImageConsumer imageconsumer)
  {
    return consumer==imageconsumer;
  }
  public final void requestTopDownLeftRightResend(ImageConsumer imageconsumer) { }
  public final void removeConsumer(ImageConsumer imageconsumer) { }
}