// Rubik's Cube 3D simulator // Karl Hörnell, March 11 1996 // Last modified October 6 import java.awt.*; import java.lang.Math; import java.lang.Character; public final class rubik extends java.applet.Applet { int i,j,k,n,o,p,q,lastX,lastY,dx,dy; int rectX[],rectY[]; Color colList[],bgcolor; final double sideVec[]={0,0,1,0,0,-1,0,-1,0,1,0,0,0,1,0,-1,0,0}; // Normal vectors final double corners[]={-1,-1,-1,1,-1,-1,1,1,-1,-1,1,-1, -1,-1,1,1,-1,1,1,1,1,-1,1,1}; // Vertex co-ordinates double topCorners[],botCorners[]; final int sides[]={4,5,6,7,3,2,1,0,0,1,5,4,1,2,6,5,2,3,7,6,0,4,7,3}; final int nextSide[]={2,3,4,5, 4,3,2,5, 1,3,0,5, 1,4,0,2, 1,5,0,3, 2,0,4,1}; final int mainBlocks[]={0,3,0,3,0,3,0,3,0,3,0,3,0,3,0,3,0,3,0,3,0,3,0,3}; final int twistDir[]={-1,1,-1,1, -1,1,-1,1, 1,1,1,1, 1,-1,1,-1, 1,1,1,1, -1,1,-1,1}; final int colDir[]={-1,-1,1,-1,1,-1}; final int circleOrder[]={0,1,2,5,8,7,6,3}; int topBlocks[],botBlocks[]; int sideCols[],sideW,sideH; int dragReg,twistSide=-1; int nearSide[],buffer[]; // Which side belongs to dragCorn double dragCorn[],dragDir[]; double eye[]={0.3651,0.1826,-0.9129}; // Initial observer co-ordinate axes (view) double eX[]={0.9309,-0.0716,0.3581}; // (sideways) double eY[]; // (vertical) double Teye[],TeX[],TeY[]; double light[],temp[]={0,0,0},temp2[]={0,0,0},newCoord[]; double sx,sy,sdxh,sdyh,sdxv,sdyv,d,t1,t2,t3,t4,t5,t6; double phi,phibase=0,Cphi,Sphi,currDragDir[]; boolean naturalState=true,twisting=false,OKtoDrag=false; Math m; Graphics offGraphics; Image offImage; public void init() { offImage=createImage(120,120); // Double buffer offGraphics=offImage.getGraphics(); rectX=new int[4]; rectY=new int[4]; newCoord=new double[16]; // Projected co-ordinates (on screen) dragDir=new double[24]; dragCorn=new double[96]; topCorners=new double[24]; // Vertex co-ordinate storage botCorners=new double[24]; // for sub-cubes during twist topBlocks=new int[24]; botBlocks=new int[24]; buffer=new int[12]; nearSide=new int[12]; light=new double[3]; Teye=new double[3]; TeX=new double[3]; TeY=new double[3]; currDragDir=new double[2]; eY=new double[3]; vecProd(eye,0,eX,0,eY,0); // Fix y axis of observer co-ordinate system normalize(eY,0); colList=new Color[120]; for (i=0;i<20;i++) { colList[i]=new Color(103+i*8,103+i*8,103+i*8); // White colList[i+20]=new Color(i*6,i*6,84+i*9); // Blue colList[i+40]=new Color(84+i*9,i*5,i*5); // Red colList[i+60]=new Color(i*6,84+i*9,i*6); // Green colList[i+80]=new Color(84+i*9,84+i*9,i*6); // Yellow colList[i+100]=new Color(84+i*9,55+i*8,i*3); // Orange } sideCols=new int[54]; for (i=0;i<54;i++) sideCols[i]=i/9; bgcolor=findBGColor(); resize(125,125); repaint(); } public Color findBGColor() // Convert hexadecimal RGB parameter to color { int hex[]; String s,h="0123456789abcdef"; Color c; hex=new int[6]; s=getParameter("bgcolor"); if ((s!=null)&&(s.length()==6)) { for (i=0;i<6;i++) for (j=0;j<16;j++) if (Character.toLowerCase(s.charAt(i))==h.charAt(j)) hex[i]=j; c=new Color(hex[0]*16+hex[1],hex[2]*16+hex[3],hex[4]*16+hex[5]); } else c=Color.lightGray; // Default return c; } // Various vector manipulation functions public double scalProd(double v1[],int ix1,double v2[],int ix2) { return v1[ix1]*v2[ix2]+v1[ix1+1]*v2[ix2+1]+v1[ix1+2]*v2[ix2+2]; } public double vNorm(double v[],int ix) { return m.sqrt(v[ix]*v[ix]+v[ix+1]*v[ix+1]+v[ix+2]*v[ix+2]); } public double cosAng(double v1[],int ix1,double v2[],int ix2) { return scalProd(v1,ix1,v2,ix2)/(vNorm(v1,ix1)*vNorm(v2,ix2)); } public void normalize(double v[], int ix) { double t=vNorm(v,ix); v[ix]=v[ix]/t; v[ix+1]=v[ix+1]/t; v[ix+2]=v[ix+2]/t; } public void scalMult(double v[], int ix,double a) { v[ix]=v[ix]*a; v[ix+1]=v[ix+1]*a; v[ix+2]=v[ix+2]*a; } public void addVec(double v1[], int ix1,double v2[],int ix2) { v2[ix2]+=v1[ix1]; v2[ix2+1]+=v1[ix1+1]; v2[ix2+2]+=v1[ix1+2]; } public void subVec(double v1[], int ix1,double v2[],int ix2) { v2[ix2]-=v1[ix1]; v2[ix2+1]-=v1[ix1+1]; v2[ix2+2]-=v1[ix1+2]; } public void copyVec(double v1[], int ix1,double v2[],int ix2) { v2[ix2]=v1[ix1]; v2[ix2+1]=v1[ix1+1]; v2[ix2+2]=v1[ix1+2]; } public void vecProd(double v1[],int ix1,double v2[],int ix2, double v3[],int ix3) { v3[ix3]=v1[ix1+1]*v2[ix2+2]-v1[ix1+2]*v2[ix2+1]; v3[ix3+1]=v1[ix1+2]*v2[ix2]-v1[ix1]*v2[ix2+2]; v3[ix3+2]=v1[ix1]*v2[ix2+1]-v1[ix1+1]*v2[ix2]; } public void cutUpCube() // Produce large and small sub-cube for twisting { boolean check; for (i=0;i<24;i++) // Copy main coordinate data { topCorners[i]=corners[i]; botCorners[i]=corners[i]; } copyVec(sideVec,3*twistSide,temp,0); // Start manipulating and build new parts copyVec(temp,0,temp2,0); // Fix new co-ordinates. Some need to be altered. scalMult(temp,0,1.3333); scalMult(temp2,0,0.6667); for (i=0;i<8;i++) { check=false; for (j=0;j<4;j++) if (i==sides[twistSide*4+j]) check=true; if (check) subVec(temp2,0,botCorners,i*3); else addVec(temp,0,topCorners,i*3); } // The sub-cubes need information about which colored fields belong to them. for (i=0;i<24;i++) // Fix the sub-cube blockings. First copy data from main { topBlocks[i]=mainBlocks[i]; // Large sub-cube data botBlocks[i]=mainBlocks[i]; // Small sub-cube data } for (i=0;i<6;i++) { if (i==twistSide) { botBlocks[i*4+1]=0; // Large sub-cube is blank on top botBlocks[i*4+3]=0; } else { k=-1; for (j=0;j<4;j++) if (nextSide[i*4+j]==twistSide) k=j; switch (k) // Twisted side adjacent to... { case 0: // Up side? { topBlocks[i*4+3]=1; botBlocks[i*4+2]=1; break; } case 1: // Right side? { topBlocks[i*4]=2; botBlocks[i*4+1]=2; break; } case 2: // Down side? { topBlocks[i*4+2]=2; botBlocks[i*4+3]=2; break; } case 3: // Left side? { topBlocks[i*4+1]=1; botBlocks[i*4]=1; break; } case -1: // None { topBlocks[i*4+1]=0; // Small sub-cube is blank on bottom topBlocks[i*4+3]=0; break; } } } } } public boolean keyDown(java.awt.Event evt, int key) { if (key==114) // Restore { twisting=false; naturalState=true; for (i=0;i<54;i++) sideCols[i]=i/9; repaint(); } else if (key==115) // Scramble { twisting=false; naturalState=true; for (i=0;i<20;i++) colorTwist((int)(m.random()*6),(int)(m.random()*3+1)); repaint(); } return false; } public boolean mouseDrag(java.awt.Event evt, int x, int y) { boolean check; double x1,x2,y1,y2,alpha,beta; if ((!twisting)&&(OKtoDrag)) { OKtoDrag=false; check=false; for (i=0;i0)&&(alpha<1)&&(beta>0)&&(beta<1)) // We're in { currDragDir[0]=dragDir[i*2]; currDragDir[1]=dragDir[i*2+1]; d=currDragDir[0]*(x-lastX)+currDragDir[1]*(y-lastY); d=d*d/((currDragDir[0]*currDragDir[0]+currDragDir[1]*currDragDir[1])* ((x-lastX)*(x-lastX)+(y-lastY)*(y-lastY))); if (d>0.6) { check=true; twistSide=nearSide[i]; i=100; } } } if (check) // We're twisting { if (naturalState) // The cube still hasn't been split up { cutUpCube(); naturalState=false; } twisting=true; phi=0.02*(currDragDir[0]*(x-lastX)+currDragDir[1]*(y-lastY))/ m.sqrt(currDragDir[0]*currDragDir[0]+currDragDir[1]*currDragDir[1]); repaint(); return false; } } OKtoDrag=false; if (!twisting) // Normal rotation { dx=lastX-x; // Vertical shift copyVec(eX,0,temp,0); scalMult(temp,0,((double)dx)*0.016); addVec(temp,0,eye,0); vecProd(eY,0,eye,0,eX,0); normalize(eX,0); normalize(eye,0); dy=y-lastY; // Horizontal shift copyVec(eY,0,temp,0); scalMult(temp,0,((double)dy)*0.016); addVec(temp,0,eye,0); vecProd(eye,0,eX,0,eY,0); normalize(eY,0); normalize(eye,0); lastX=x; lastY=y; repaint(); } else // Twist, compute twisting angle phi { phi=0.02*(currDragDir[0]*(x-lastX)+currDragDir[1]*(y-lastY))/ m.sqrt(currDragDir[0]*currDragDir[0]+currDragDir[1]*currDragDir[1]); repaint(); } return false; } public boolean mouseDown(java.awt.Event evt, int x, int y) { lastX=x; lastY=y; OKtoDrag=true; return false; } public boolean mouseUp(java.awt.Event evt, int x, int y) { int quads; double qu; if (twisting) // We have let go of the mouse when twisting { twisting=false; phibase+=phi; // Save twist angle phi=0; qu=phibase; while (qu<0) qu+=125.662; quads=((int)(qu*3.183)); if (((quads % 5)==0)||((quads % 5)==4)) // Close enough to a corner? { quads=((quads+1)/5) % 4; if (colDir[twistSide]<0) quads=(4-quads) % 4; phibase=0; naturalState=true; // Return the cube to its natural state colorTwist(twistSide,quads); // and shift the colored fields } repaint(); } return false; } public void colorTwist(int sideNum, int quads) // Shift colored fields { int i,j,k,l=0; k=quads*2; // quads = number of 90-degree multiples for (i=0;i<8;i++) { buffer[k]=sideCols[sideNum*9+circleOrder[i]]; k=(k+1) % 8; } for (i=0;i<8;i++) sideCols[sideNum*9+circleOrder[i]]=buffer[i]; k=quads*3; for (i=0;i<4;i++) { for (j=0;j<4;j++) if (nextSide[nextSide[sideNum*4+i]*4+j]==sideNum) l=j; for (j=0;j<3;j++) { switch(l) { case 0: buffer[k]=sideCols[nextSide[sideNum*4+i]*9+j]; break; case 1: buffer[k]=sideCols[nextSide[sideNum*4+i]*9+2+3*j]; break; case 2: buffer[k]=sideCols[nextSide[sideNum*4+i]*9+8-j]; break; case 3: buffer[k]=sideCols[nextSide[sideNum*4+i]*9+6-3*j]; break; default: break; } k=(k+1) % 12; } } k=0; for (i=0;i<4;i++) { for (j=0;j<4;j++) if (nextSide[nextSide[sideNum*4+i]*4+j]==sideNum) l=j; for (j=0;j<3;j++) { switch(l) { case 0: sideCols[nextSide[sideNum*4+i]*9+j]=buffer[k]; break; case 1: sideCols[nextSide[sideNum*4+i]*9+2+3*j]=buffer[k]; break; case 2: sideCols[nextSide[sideNum*4+i]*9+8-j]=buffer[k]; break; case 3: sideCols[nextSide[sideNum*4+i]*9+6-3*j]=buffer[k]; break; default: break; } k++; } } } public void paint(Graphics g) { dragReg=0; offGraphics.setColor(bgcolor); // Clear drawing buffer offGraphics.fillRect(0,0,120,120); if (naturalState) fixBlock(eye,eX,eY,corners,mainBlocks,0); // Draw cube else { copyVec(eye,0,Teye,0); // In twisted state? Compute top observer copyVec(eX,0,TeX,0); Cphi=m.cos(phi+phibase); Sphi=-m.sin(phi+phibase); switch(twistSide) // Twist around which axis? { case 0: // z Teye[0]=Cphi*eye[0]+Sphi*eye[1]; TeX[0]=Cphi*eX[0]+Sphi*eX[1]; Teye[1]=-Sphi*eye[0]+Cphi*eye[1]; TeX[1]=-Sphi*eX[0]+Cphi*eX[1]; break; case 1: // -z Teye[0]=Cphi*eye[0]-Sphi*eye[1]; TeX[0]=Cphi*eX[0]-Sphi*eX[1]; Teye[1]=Sphi*eye[0]+Cphi*eye[1]; TeX[1]=Sphi*eX[0]+Cphi*eX[1]; break; case 2: // -y Teye[0]=Cphi*eye[0]-Sphi*eye[2]; TeX[0]=Cphi*eX[0]-Sphi*eX[2]; Teye[2]=Sphi*eye[0]+Cphi*eye[2]; TeX[2]=Sphi*eX[0]+Cphi*eX[2]; break; case 3: // x Teye[1]=Cphi*eye[1]+Sphi*eye[2]; TeX[1]=Cphi*eX[1]+Sphi*eX[2]; Teye[2]=-Sphi*eye[1]+Cphi*eye[2]; TeX[2]=-Sphi*eX[1]+Cphi*eX[2]; break; case 4: // y Teye[0]=Cphi*eye[0]+Sphi*eye[2]; TeX[0]=Cphi*eX[0]+Sphi*eX[2]; Teye[2]=-Sphi*eye[0]+Cphi*eye[2]; TeX[2]=-Sphi*eX[0]+Cphi*eX[2]; break; case 5: // -x Teye[1]=Cphi*eye[1]-Sphi*eye[2]; TeX[1]=Cphi*eX[1]-Sphi*eX[2]; Teye[2]=Sphi*eye[1]+Cphi*eye[2]; TeX[2]=Sphi*eX[1]+Cphi*eX[2]; break; default: break; } vecProd(Teye,0,TeX,0,TeY,0); if (scalProd(eye,0,sideVec,twistSide*3)<0) // Top facing away? Draw it first { fixBlock(Teye,TeX,TeY,topCorners,topBlocks,2); fixBlock(eye,eX,eY,botCorners,botBlocks,1); } else { fixBlock(eye,eX,eY,botCorners,botBlocks,1); fixBlock(Teye,TeX,TeY,topCorners,topBlocks,2); } } g.drawImage(offImage,0,0,this); } public void update(Graphics g) { paint(g); } // Draw cube or sub-cube public void fixBlock(double beye[],double beX[],double beY[], double bcorners[],int bblocks[],int mode) { copyVec(beye,0,light,0); scalMult(light,0,-3); addVec(beX,0,light,0); subVec(beY,0,light,0); for (i=0;i<8;i++) // Project 3D co-ordinates into 2D screen ones { newCoord[i*2]=(60+35.1*scalProd(bcorners,i*3,beX,0)); newCoord[i*2+1]=(60-35.1*scalProd(bcorners,i*3,beY,0)); } for (i=0;i<6;i++) { if (scalProd(beye,0,sideVec,3*i)>0.001) // Face towards us? Draw it. { k=(int)(9.6*(1-cosAng(light,0,sideVec,3*i))); offGraphics.setColor(Color.black); for (j=0;j<4;j++) // Find corner co-ordinates { rectX[j]=(int)newCoord[2*sides[i*4+j]]; rectY[j]=(int)newCoord[2*sides[i*4+j]+1]; } offGraphics.fillPolygon(rectX,rectY,4); // First draw black sideW=bblocks[i*4+1]-bblocks[i*4]; sideH=bblocks[i*4+3]-bblocks[i*4+2]; if (sideW>0) { sx=newCoord[2*sides[i*4]]; sy=newCoord[2*sides[i*4]+1]; sdxh=(newCoord[2*sides[i*4+1]]-sx)/sideW; sdxv=(newCoord[2*sides[i*4+3]]-sx)/sideH; sdyh=(newCoord[2*sides[i*4+1]+1]-sy)/sideW; sdyv=(newCoord[2*sides[i*4+3]+1]-sy)/sideH; p=bblocks[i*4+2]; for (n=0;n0)) { if (sideW==3) // Determine positive drag direction if (bblocks[i*4+2]==0) { dragDir[dragReg*2]=sdxh*twistDir[i*4]; dragDir[dragReg*2+1]=sdyh*twistDir[i*4]; } else { dragDir[dragReg*2]=-sdxh*twistDir[i*4+2]; dragDir[dragReg*2+1]=-sdyh*twistDir[i*4+2]; } else if (bblocks[i*4]==0) { dragDir[dragReg*2]=-sdxv*twistDir[i*4+3]; dragDir[dragReg*2+1]=-sdyv*twistDir[i*4+3]; } else { dragDir[dragReg*2]=sdxv*twistDir[i*4+1]; dragDir[dragReg*2+1]=sdyv*twistDir[i*4+1]; } for (j=0;j<4;j++) { dragCorn[dragReg*8+j]=newCoord[2*sides[i*4+j]]; dragCorn[dragReg*8+4+j]=newCoord[2*sides[i*4+j]+1]; } nearSide[dragReg]=twistSide; dragReg++; } break; default: break; } } } } }