// do not link to this file, visit www.scriptersoft.com for info

/**************************************************************************
JavaScript Graphics Library 0.0.2, Updated Source Code at Scriptersoft.com
Copyright (C) 2005-2006  Kurt L. Whicher
February,11,2006

This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.

This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
**************************************************************************/

// global variables ////////////////////////////////////////////////////////////

// math vars
var S_pi_doubled=Math.PI*2;
var S_deg_2_rad=Math.PI/180;
var S_rad_2_deg=180/Math.PI;

// debug vars
var S_start_time;

// DOM functions ///////////////////////////////////////////////////////////////

function S_check_browser() { if (document.getElementById) return true; else return false; }
function S_write_inner_html(id,text) { document.getElementById(id).innerHTML=text; }
function S_z_index(e,z) { document.getElementById(e).style.zIndex=z; }

// debug functions /////////////////////////////////////////////////////////////

function S_start_timing(){ S_start_time=new Date().getTime(); }
function S_stop_timing(t){ // target
  var s='Time interval: '+(new Date().getTime()-S_start_time)/1000+' secs';
  if(t=="alert") alert(s); else S_write_inner_html(t,s);
}

// math functions & constructors ///////////////////////////////////////////////

// 4x4 transformation matrix
function S_matrix_4x4() {
  return [1,0,0,0,
          0,1,0,0,
          0,0,1,0,
          0,0,0,1];
}

// vector constructors & functions
function S_vec_2d(x,y) { this.x=x; this.y=y; }
function S_vec_3d(x,y,z) { this.x=x; this.y=y; this.z=z; }
function S_sub_vec_2d(a,b) { return new S_vec_2d(a.x-b.x, a.y-b.y); }
function S_sub_vec_3d(a,b) { return new S_vec_3d(a.x-b.x, a.y-b.y, a.z-b.z); }
function S_length_squared_vec_3d(v) { return S_dot_vec_3d(v,v); }
function S_length_vec_3d(v) { return Math.sqrt(S_length_squared_vec_3d(v)); }
function S_normalize_vec_3d(v) {
  var l=S_length_vec_3d(v);
  return (l!=0)?new S_vec_3d(v.x/l,v.y/l,v.z/l):new S_vec_3d(0,0,0);
}

// 3d dot product
function S_dot_vec_3d(a, b) { return a.x*b.x+a.y*b.y+a.z*b.z; }

// cross product
function S_cross(a,b) {
  return new S_vec_3d( a.y*b.z-a.z*b.y, a.z*b.x-a.x*b.z, a.x*b.y-a.y*b.x);
}

// general rotation using transformation matrix, axis & angle parameters
function S_rotate(m,ax,a) {
  var i,j,ij=new Array(),v=new Array(),c=Math.cos(a),s=Math.sin(a);
  if (ax=="x") ij=[1,2,5,6,9,10,13,14];
  else if (ax=="y") ij=[2,0,6,4,10,8,14,12];
  else if (ax=="z") ij=[0,1,4,5,8,9,12,13];
  for (i=0;i<8;i++) v[i]=m[ij[i]];
  for (i=0,j=1;i<8;i+=2,j+=2) {
    m[ij[i]]=v[i]*c-v[j]*s;
    m[ij[j]]=v[i]*s+v[j]*c
  }
}

// S_model functions & constructors ////////////////////////////////////////////

function S_model(v,f) {
  // vertice & face arrays, transformation matrix, display boolean
  this.v=v; this.f=f, this.tm=S_matrix_4x4(), this.d=true;
}
// widths 1 & 2, height, "rows", "columns", color
function S_cone(w1,w2,h,r,c,clr) {
  var i,r1=0,r2=c,v=new Array(),t=new Array(),rxc=r*c;
  for(i=0;i<=r;i++)
    v=v.concat(S_vertice_ring(h*(0.5-i/r),c,w1*i/r+w2*(r-i)/r));
  for(i=0;i<r;i++,r1+=c,r2+=c)
    t=t.concat(S_triangle_ring(r1,r2,c,clr));
    for(i=1;i<(c-1);i++) {
      t.push(new S_face(0,i,i+1,clr));
      t.push(new S_face(rxc,rxc+i+1,rxc+i,clr));
    }
  return new S_model(v,t);
}
//distance & color
function S_cube(d,c) { return new S_cone(d,d,Math.cos(Math.PI/4)*d*2,1,4,c); }
function S_cylinder(w,h,r,c,clr) { return new S_cone(w,w,h,r,c,clr); }
function S_sphere(d,r,c,clr) { // distance,"rows">=2,"columns">=3,color paramaters
  var i,j,tmp,r1=0,r2=c,v=new Array(),t=new Array(),r_1xc=(r-1)*c,r_2xc=(r-2)*c;
  for(i=1;i<r;i++) {
    tmp=Math.PI*i/r;
    v=v.concat(S_vertice_ring(d*Math.cos(tmp),c,Math.sin(tmp)*d));
  }
  v.push(new S_vertice( d,0,0));
  v.push(new S_vertice(-d,0,0));
  for(i=0;i<(r-2);i++,r1+=c,r2+=c)
    t=t.concat(S_triangle_ring(r1,r2,c,clr));
  for(i=0,j=1;i<c;i++,j=++j%c) {
    t.push(new S_face(r_1xc,i,j,clr));
    t.push(new S_face(r_1xc+1,r_2xc+j,r_2xc+i,clr));
  }
  return new S_model(v,t);
}
function S_vertice(x,y,z) {
  this.x=x; this.y=y; this.z=z; this.w=1;
  this.t=new S_vec_3d(x,y,z); // transformed 3d
  this.p=new S_vec_2d(0,0); // projected 2d
}
function S_face(v0,v1,v2,c) { // 3 vertice faces
  this.v=[v0,v1,v2]; this.c=c; this.b=0; // b:brightness
  this.d=true; // display: true or false
}
// x coordinate, number of vertices, distance
function S_vertice_ring(x,nv,d) {
  var i,a,v=new Array();
  for(i=0;i<nv;i++) {
    a=S_pi_doubled*i/nv;
    v[i]=new S_vertice(x,d*Math.sin(a),d*Math.cos(a));
  }
  return v;
}
function S_triangle_ring(r1,r2,c,clr) { // rows 1 & 2, cols, color
  var i,j,tr=new Array();
  for(i=0,j=1;i<c;i++,j=++j%c) {
    tr.push(new S_face(r1+i,r2+i,r2+j,clr));
    tr.push(new S_face(r1+i,r2+j,r1+j,clr));
  }
  return tr;
}
// normal, brightness, array of 2D projection coordinates, and z depth
function S_triangle(fn,b,p,c,z) { this.fn=fn; this.b=b; this.p=p; this.z=z; this.c=c; }
function S_face_normal(a,b,c){
  return S_normalize_vec_3d(S_cross(S_sub_vec_3d(b,a), S_sub_vec_3d(b,c)));
}
function S_face_intensity(fn,lght) { var i=S_dot_vec_3d(fn,lght); return (i>0)?i:0; }

// model transformations
S_model.prototype.scale_xyz=function(s) { this.tm[0]*=s; this.tm[5]*=s; this.tm[10]*=s; }
S_model.prototype.scale_x=function(s) { this.tm[0]*=s; }
S_model.prototype.scale_y=function(s) { this.tm[5]*=s; }
S_model.prototype.scale_z=function(s) { this.tm[10]*=s; }
S_model.prototype.rotate_x=function(a) { S_rotate(this.tm,"x",a*=S_deg_2_rad); }
S_model.prototype.rotate_y=function(a) { S_rotate(this.tm,"y",a*=S_deg_2_rad); }
S_model.prototype.rotate_z=function(a) { S_rotate(this.tm,"z",a*=S_deg_2_rad); }

// model display methods
S_model.prototype.yes_show=function() { this.d=true; }
S_model.prototype.no_show=function() { this.d=false; }
S_model.prototype.face_color=function(i,c) { this.f[i].c=c; }
S_model.prototype.number_of_faces=function() { return this.f.length; }

// color functions /////////////////////////////////////////////////////////////

function S_rgb_color(r,g,b) {
  var i, c=[r,g,b];
  for(i=0; i<3; i++) {
    c[i]=Math.floor(c[i]);
    if(c[i]<0) c[i]=0; else if(c[i]>255) c[i]=255;
  }
  return c;
}
function S_rgb_color_string(c) { return "rgb("+c[0]+","+c[1]+","+c[2]+")"; }
function S_balanced_palette(n){
  var i,j,k,r,g,b,a=new Array(),nl1=n-1; // number less one
  for(i=0;i<n;i++) {
    r=(i==0)?0:Math.floor(i/nl1*256-1);
    for(j=0;j<n;j++) {
      g=(j==0)?0:Math.floor(j/nl1*256-1);
      for(k=0;k<n;k++) {
        b=(k==0)?0:Math.floor(k/nl1*256-1);
        a.push([r,g,b]);
      }
    }
  }
  return a;
}
function S_same_color(a,b) {
  for(var i=0;i<3;i++) { if(a[i]!=b[i]) return false; }
  return true;
}

// S_scene constructor and methods, except for text handling routines //////////

function S_scene(w,h,ca,ch,div_id,cmra) { // width,height,color array,char,div,camera
  var i,s="";
  this.h=h; this.w=w; this.wl1=w-1; this.hl1=h-1; // width & height less 1
  this.ch=ch;
  this.dv=new Array(); // div array
  this.text_lookup_tables(); // w/ this, sets bclu, eclu, hslu & vslu
  this.text_blocks(); // writes this.vs, this.hs & this.chs
  this.image_buffer(h,w); // set this.b
  this.e=new Array(); // e:empty string
  this.cx=w/2; this.cy=h/2; // center x, center y
  this.cmra=-cmra; // camera on z axis
  this.m=new Array(); // model array
  this.lght=new S_light();
  this.lc=S_rgb_color(255,255,255); // light color
  this.ca_txt=new Array();
  this.ca=new Array();
  this.nc=ca.length; // number of colors
  this.cs=new Array();
  for(i=0;i<=ca.length;i++){
    if(i<ca.length) {
      this.cs[i]=new Array();
      this.ca_txt[i]=S_rgb_color_string(ca[i]);
      this.ca[i]=S_rgb_color(ca[i][0],ca[i][1],ca[i][2]);
    }
    s+='<div id="'+div_id+i+'" style="position:absolute;top:0;left:0;"></div>';
    this.dv.push(div_id+i);
    this.e.push(true);
  }
  S_write_inner_html(div_id,s);
}
S_scene.prototype.set_pixel=function(x,y,c){ this.b[x][y]=c; }
S_scene.prototype.clear_buffer=function(){
  var i,j;
  for(i=0;i<this.h;i++) for(j=0;j<this.w;j++) this.b[j][i]=0;
}
function S_light() {
  this.x=0; this.y=1; this.z=0; this.w=1; // original coordinates
  this.t=new S_vec_3d(0,1,0); // transformed coordinates
  this.tm=new S_matrix_4x4();
}
S_scene.prototype.adjust_light=function() {
  var m=this.lght.tm, l=this.lght;
  l.t.x=l.x*m[0]+l.y*m[4]+ l.z*m[8]+l.w*m[12];
  l.t.y=l.x*m[1]+l.y*m[5]+ l.z*m[9]+l.w*m[13];
  l.t.z=l.x*m[2]+l.y*m[6]+ l.z*m[10]+l.w*m[14];
  l.t=S_normalize_vec_3d(l.t);
}
S_scene.prototype.light_rotate_x=function(a) { S_rotate(this.lght.tm,"x",a*=S_deg_2_rad); }
S_scene.prototype.light_rotate_y=function(a) { S_rotate(this.lght.tm,"y",a*=S_deg_2_rad); }
S_scene.prototype.light_rotate_z=function(a) { S_rotate(this.lght.tm,"z",a*=S_deg_2_rad); }
S_scene.prototype.image_buffer=function(h,w) {
  var i,bw=new Array();
  this.b=new Array();
  for(i=0;i<h;i++) bw[i]=0;
  for(i=0;i<w;i++) this.b[i]=bw.slice();
}
S_scene.prototype.models=function() {
  var i; this.m=new Array();
  for(i=0;i<arguments.length;i++) this.m.push(arguments[i]);
}
S_scene.prototype.light_color=function(c){ this.lc=c; }
S_scene.prototype.project=function() {
  var i,j,v,tm,d,m;
  for(i=0;i<this.m.length;i++) {
    m=this.m[i]; tm=this.m[i].tm;
    for(j=0;j<m.v.length;j++) {
      v=m.v[j];
      v.t.x=v.x*tm[0]+v.y*tm[4]+v.z*tm[8]+v.w*tm[12];
      v.t.y=v.x*tm[1]+v.y*tm[5]+v.z*tm[9]+v.w*tm[13];
      v.t.z=v.x*tm[2]+v.y*tm[6]+v.z*tm[10]+v.w*tm[14];
      d=(this.cmra-v.t.z/2);
      if (d<0) {
        v.p.x=(this.cmra*v.t.x/d)+this.cx;
        v.p.y=-(this.cmra*v.t.y/d)+this.cy;
      }
    }
  }
}
S_scene.prototype.faces_2_triangles=function() {
  var i,j,k,m,fn,v=new Array(),p=new Array(),z=new Array(),tr=new Array();
  for(i=0;i<this.m.length;i++){
    m=this.m[i];
    for(j=0;j<m.f.length;j++) {
      for(k=0;k<3;k++) {
        v[k]=m.v[m.f[j].v[k]];
        p[k]=v[k].p;
        z[k]=v[k].t.z;
      }
      if((p[1].x-p[0].x)*(p[2].y-p[0].y)<(p[2].x-p[0].x)*(p[1].y-p[0].y)) {
        m.f[j].d=true;
        fn=S_face_normal(v[0].t,v[1].t,v[2].t);
        m.f[j].b=S_face_intensity(fn,this.lght.t);
        tr.push(new S_triangle(fn,m.f[j].b,p.slice(),m.f[j].c,z));
      } else m.f[j].d=false;
    }
  }
  return tr;
}
S_scene.prototype.models_3d_to_buffer=function(){
  // iterators, temp, old maximum, temp color, palette color array
  var i,j,t,old_max,tc,pc=new Array(),ca=this.ca,tr;
  this.project();
  this.adjust_light();
  tr=this.faces_2_triangles();
  for(i=0;i<tr.length;i++) { // loop through triangles
    pc[i]=0;
    tc=S_triangle_color(tr[i].c,this.lc,tr[i].b);
    old_max=Number.MAX_VALUE;
    for(j=0;j<ca.length;j++) {
      t=Math.pow(ca[j][0]-tc[0],2)+Math.pow(ca[j][1]-tc[1],2)+Math.pow(ca[j][2]-tc[2],2);
      if(t<old_max) { old_max=t; pc[i]=j; }
    }
  }
  this.write_triangles(tr,pc);
}
function S_triangle_color(c,lc,b) { // c:array of colors
  var i, clr=new Array();
  for(i=0;i<3;i++) clr[i]=c[i]+(lc[i]-c[i]+1)*b;
  for(i=0;i<3;i++) if (clr[i]>lc[i]) { clr[i]=lc[i]; }
  return S_rgb_color(clr[0],clr[1],clr[2]);
}
S_scene.prototype.write_triangles=function(tr,c) {
  // iterators,projected triangle,temp,min,max,slope & vector arrays
  var i,j,k,l,p,t,xmin,xmax,m=new Array(),vx=new Array(),vy=new Array();
  for(i=0;i<tr.length;i++){
    p=tr[i].p; p.sort(function(a,b) { return a.y-b.y; } );
    for(j=0,k=1,l=0;j<3;j++) {
      if(j==1)k++; else if(j==2)l++;
      vx[j]=p[k].x-p[l].x; vy[j]=p[k].y-p[l].y;
      m[j]=(vy[j]!=0)?vx[j]/vy[j]:0;
    }
    for(j=Math.floor(p[2].y);j>=p[0].y;j--) {
      if((j>=0)&&(j<this.h)){
        xmin=xmax=Math.floor(p[0].x+m[1]*(j-p[0].y));
        if(j<p[1].y) {
          t=Math.floor(p[0].x+m[0]*(j-p[0].y));
          if(t<xmin) xmin=Math.floor(t); else if(t>xmax) xmax=Math.floor(t);
        }
        if(j>=p[1].y) {
          t=Math.floor(p[1].x+m[2]*(j-p[1].y));
          if(t<xmin) xmin=Math.floor(t); else if(t>xmax) xmax=Math.floor(t);
        }
        if(xmin<0) xmin=0;
        if(xmax>this.wl1) xmax=this.wl1;
        for(k=Math.floor(xmax);k>=xmin;k--) this.b[k][j]=c[i];
      }
    }
  }
}
S_scene.prototype.line=function(x,y,x2,y2,clr) {
  var sx=(x2-x<0)?-1:1,ax=Math.abs(x2-x)*2,sy=(y2-y<0)?-1:1,ay=Math.abs(y2-y)*2,d;
  if(ax>ay) {
    d=ay-ax/2;
    this.b[x][y]=clr;
    while(x!=x2) {
      if(d>=0) { y=y+sy; d-=ax; }
      x=x+sx; d+=ay;
      this.b[x][y]=clr;
    }
  } else {
    d=ax-ay/2;
    this.b[x][y]=clr;
    while(y!=y2) {
      if(d>=0) { x=x+sx; d-=ay; }
      y=y+sy; d+=ax;
      this.b[x][y]=clr;
    }
  }
}

// S_scene text handling methods ///////////////////////////////////////////////

S_scene.prototype.display=function(){
  // text strips, vertical/horizontal string pos, text data, number of colors to write
  var i,j,nc=this.nc,hl1=this.hl1,wl1=this.wl1,ts,vsp1,vsp2,hsp,td=new Array(),nctw=0;
  // x&y begin/end chars, x&y begin spaces, string & color separator arrays
  var xbc,ybc,xec,yec,xbs,ybs,s=new Array(),cs=this.cs,chs=this.chs;
  var bclu=this.bclu,eclu=this.eclu,vslu=this.vslu,hslu=this.hslu,vs=this.vs,hs=this.hs;
  for(i=1;i<nc;i++) cs[i].length=0;
  this.separate_colors();
  // The following code runs too slowly in IE >>>>>>>>>>>>>>>>>>>>>>>>>
  for(i=1;i<nc;i++){
    if((ts=cs[i].length/4)>=1) {
      s.length=0;
      xbs=wl1; ybs=hl1;
      s.push('<pre style="color:'+this.ca_txt[i]+'">');
      for(j=0;j<ts;j++){
        ybc=cs[i].pop(); xbc=cs[i].pop(); yec=cs[i].pop(); xec=cs[i].pop();
        if((ybc!=hl1)||(xbc!=wl1)) { 
          hsp=hslu[ybc][xbc]; vsp1=vslu[ybs][xbs]; vsp2=vslu[ybc][xbc];
      	  if(vsp1<vsp2) { s.push(" "); s.push(vs.slice(vsp1,vsp2)); s.push(hs.slice(0,hsp)); }
      	  else s.push(hs.slice(hslu[ybs][xbs],hsp)); // vsp1==vsp2
        }
        s.push(chs.slice(bclu[ybc][xbc],eclu[yec][xec]));
        ybs=yec; xbs=xec;
      }
      s.push(' </pre>');
      nctw++; td.push(s.join("")); td.push(this.dv[i]); this.e[i]=false;
    } else if(this.e[i]==false) {
             nctw++; td.push(""); td.push(this.dv[i]); this.e[i]=true;
           }
  }
  // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
  for(i=0;i<nctw;i++) document.getElementById(td.pop()).innerHTML=td.pop();
}
S_scene.prototype.separate_colors=function(){
  var i,j,b=this.b,cs=this.cs,bc=0,fc; // back color, front color
  for(i=this.hl1;i>=0;i--){
    for(j=this.wl1;j>=0;j--){
      fc=this.b[j][i];
      if(fc!=bc) {
      	if(fc!=0) { cs[fc].push(j); cs[fc].push(i); }
      	if(bc!=0) { cs[bc].push(j); cs[bc].push(i); }
      }
      bc=fc;
    }
  }
  if(fc!=0) { cs[fc].push(this.wl1); cs[fc].push(this.hl1); }
}
S_scene.prototype.text_lookup_tables=function() {
  var i,j,w=this.w,h=this.h,wl1=this.wl1,t=new Array(),chp=0,sp=0; // temp,char & string pos
  // begin & end character lookup, horizontal & vertical space lookup
  this.bclu=new Array(); this.eclu=new Array();
  this.hslu=new Array(); this.vslu=new Array();
  for(i=0;i<w;i++) t[i]=(i+1)%w;
  for(i=0;i<h;i++) {
    this.bclu[i]=new Array(); this.eclu[i]=new Array();
    this.hslu[i]=new Array(); this.vslu[i]=new Array();
    for(j=0;j<w;j++) {
      if(j==wl1) { chp+=3; sp+=1; } else chp+=1;
      this.bclu[i][j]=this.eclu[i][j]=chp;
      this.hslu[i][j]=t[j];
      this.vslu[i][j]=sp;
    }
  }
  this.bclu[h-1][wl1]=0;
  this.vslu[h-1][wl1]=0;
}
S_scene.prototype.text_blocks=function() {
  var w=this.w,h=this.h,ch=this.ch;
  var i,ls,ra=new Array(),ba=new Array(); // line string, row array, block array
  this.vs=new Array(); this.hs=new Array();
  for(i=0;i<w;i++) { ra.push(ch); this.hs.push(" "); }
  ra.push(" \n"); ls=ra.join("");
  for(i=0;i<h;i++) { ba[i]=ls.slice(); this.vs.push("\n"); }
  // character string, horizontal/vertical spaces
  this.chs=ba.join(""); this.hs=this.hs.join(""); this.vs=this.vs.join("");
}

