1 /* -*- mode: javascript; tab-width: 4; indent-tabs-mode: nil; -*- */
  2 /**
  3  *
  4  * @file Let's Make a Demo Workshop 1.x base library
  5  *
  6  */
  7 
  8 /**
  9  *
 10  * <h3>A Dirty Small Library for Graphics</h3>
 11  *
 12  * <p>This was, and basically still is, course material for our course
 13  * "TIEA311 Principles of Computer graphics" (based on "6.837 Computer
 14  * Graphics" as published in MIT OCW). Am I a rogue teacher leaking
 15  * example answers here? No. I'm quite certain that a student who
 16  * manages to translate this convoluted Javascript snippet to
 17  * grade-worthy C++ exercise answers has earned the credit, probably
 18  * the "hard way", too.
 19  * </p>
 20  *
 21  * <p>This code does dirty tricks to fit in very small storage space as
 22  * if it was part of an entry in a demoscene "4k intro"
 23  * competition. That means at most 4096 bytes for everything. Also,
 24  * this is written by a total Javascript newbie - proper practices of
 25  * any language can only be learned by lots and lots of programming
 26  * and reading codes by more experienced programmers. So far, I've
 27  * only gone through some tutorial examples, parts of specifications,
 28  * and demoscene intro codes that necessarily minimize code size with
 29  * the expense of *everything* else.
 30  * </p>
 31  *
 32  * <p>Do not try these programming practices at home (until you know what
 33  * you do and which parts of the specification are beautifully
 34  * misused).
 35  * </p>
 36  *
 37  * <p>If you're less experienced, try to keep on learning good
 38  * programming practices (in Javascript and other languages) from nice
 39  * tutorials. Listen to your teachers and coaches. They know what is
 40  * best for you. That said, some of this code may show you some
 41  * features that are less common in basic tutorials.
 42  * </p>
 43  *
 44  * <p>Browser support: Productions made using this code seem to load and
 45  * run in desktop Chrome, Edge, and Firefox, but not on IE. Mobile
 46  * browsers unknown.
 47  * </p>
 48  *
 49  * <p>Original goal: use at least spline curves, generalized cylinder,
 50  * surface of revolution, hierarchical model, perspective projection,
 51  * and simple fragment shading with directed light. (some 50% of
 52  * course content)
 53  * </p>
 54  *
 55  * <p>Outcome: yep, I got all that stuffed in an example production
 56  * (along with shaders, vector math, softsynth, soundtrack, and an
 57  * English message.. all in 4k "executable" after some serious
 58  * minification).
 59  * </p>
 60  *
 61  *
 62  **/
 63 
 64 // Variables used all over the library. Must have as globals:
 65 var gl, prg;
 66 
 67 // TODO: Should these gl and prg be included as method parameters or
 68 // object properties?
 69 
 70 // --------------------------------------------------------------------------------
 71 // "Globals needed in many routines", these are used by the main starter code:
 72 var C, Cw, Ch;          // Canvas object and previous width and height
 73 var audio;              // Audio object needed for song playback
 74 var s;                  // Temporary variable for "style" but also other things
 75 var _document=document; // automatically minified name for the "document" object
 76 
 77 // Global for camera inside scenegraph.
 78 var cameraTransformation;
 79 
 80 
 81 
 82 // Constants
 83     var PI=Math.PI;
 84 //    var PI=3.141593
 85 
 86 // Utility functions
 87 function clamp01(parameter){
 88     if (parameter < 0) return 0;
 89     if (parameter > 1) return 1;
 90     return parameter;
 91 }
 92 
 93     // Observe: Everything looks transposed compared to the theory
 94     // slides and C++ codes of the course. This is because of column
 95     // major ordering used by Javascript arrays and the WebGL
 96     // interface. The mathematical meaning is the same as on the
 97     // course, but you should remember that what looks like a row here
 98     // is actually a column if written as actual math. Pen and paper
 99     // and your own brains are very powerful tools, as I keep
100     // repeating on my lectures.
101 
102     // Bezier basis matrix
103     var bezB = [  1,  0,  0, 0,
104                  -3,  3,  0, 0,
105                   3, -6,  3, 0,
106                  -1,  3, -3, 1];
107 
108     // B-spline basis matrix
109     var bspB = [ 1./6,  4./6,  1./6, 0./6,
110                 -3./6,  0./6,  3./6, 0./6,
111                  3./6, -6./6,  3./6, 0./6,
112                 -1./6,  3./6, -3./6, 1./6 ];
113 
114     // "Re-implementing the wheel" ------------------------------
115 
116     /** Returns a 4x4 matrix of zeros */
117     function zeros(){
118         return [ 0, 0, 0, 0,
119                  0, 0, 0, 0,
120                  0, 0, 0, 0,
121                  0, 0, 0, 0 ];
122     };
123 
124     /** Returns a translation matrix of homogeneous coordinates. */
125     function translate(tx,ty,tz){
126         return [ 1, 0, 0, 0,
127                  0, 1, 0, 0,
128                  0, 0, 1, 0,
129                  tx, ty, tz, 1 ];
130     };
131 
132     /** Returns a scale matrix. */
133     function scaleXYZ(sx,sy,sz){
134         return [ sx, 0, 0, 0,
135                  0, sy, 0, 0,
136                  0, 0, sz, 0,
137                  0, 0, 0, 1 ];
138     };
139 
140     /** Returns an isotropic scale matrix. */
141     function scale(s){
142         return scaleXYZ(s,s,s);
143     };
144 
145     /** Counter-clockwise rotation around the Z-axis */
146     function rotZ(theta){
147         var s = Math.sin(theta);
148         var c = Math.cos(theta);
149         return [ c, -s, 0, 0,
150                  s,  c, 0, 0,
151                  0,  0, 1, 0,
152                  0,  0, 0, 1];
153     };
154 
155     /** Counter-clockwise rotation around the Y-axis */
156     function rotY(theta){
157         var s = Math.sin(theta);
158         var c = Math.cos(theta);
159         return [ c,  0, -s, 0,
160                  0,  1, 0, 0,
161                  s,  0, c, 0,
162                  0,  0, 0, 1];
163     };
164 
165     /** Counter-clockwise rotation around the X-axis */
166     function rotX(theta){
167         var s = Math.sin(theta);
168         var c = Math.cos(theta);
169         return [ 1,  0, 0, 0,
170                  0,  c, -s, 0,
171                  0,  s, c, 0,
172                  0,  0, 0, 1];
173     };
174 
175     /** Perspective projection, imitates gluPerspective() */
176     function perspective(fov, ar, near, far) {
177         var f = 1/Math.tan(fov/2);
178         var div = near - far;
179         return [f/ar, 0,  0,               0,
180                 0,    f,  0,               0,
181                 0,    0,  (far+near)/div, -1,
182                 0,    0,  2*far*near/div,  0 ];
183     };
184 
185     /**
186      * Perspective projection as in gluPerspective() but assumes a
187      * precomputed f==1/Math.tan(fov/2)
188      */
189     function perspectiveF(f, ar, near, far) {
190         var div = near - far;
191         return [f/ar, 0,   0,               0,
192                 0,    f,   0,               0,
193                 0,    0,   (far+near)/div, -1,
194                 0,    0,   2*far*near/div,  0 ];
195     };
196 
197     /**
198      * Perspective with hardcoded near plane "quite near". Far plane
199      * is "far away". Might have issues with Z stability. Assumes
200      * precomputed f==1/Math.tan(fov/2).
201      */
202     function perspectiveFhc(f, ar) {
203         return [f/ar, 0,   0,  0,
204                 0,    f,   0,  0,
205                 0,    0,  -1, -1,
206                 0,    0,  -1,  0 ];
207     };
208 
209 
210     /**
211      * Orthographic projection with fixed width
212      */
213     function orthographic(top, ar, near, far) {
214         var bottom=-top,right=top*ar, left=-top*ar;
215         return [2/(right-left), 0,                0,             0,
216                 0,              2/(top-bottom),   0,             0,
217                 0,              0,                -2/(far-near), 0,
218                 -(right+left)/(right-left),
219                                 -(top+bottom)/(top-bottom),
220                                                   -(far+near)/(far-near),
221                                                                  1 ];
222     };
223 
224     // "Matrices with inverses" ------------------------------
225     function translate_wi(tx,ty,tz){
226         var res=translate(tx,ty,tz);
227         res.n=translate(-tx,-ty,-tz);
228         return res;
229     };
230     /** Returns a scale matrix. */
231     function scaleXYZ_wi(sx,sy,sz){
232         var res=scaleXYZ(sx,sy,sz);
233         res.n=scaleXYZ(1/sx,1/sy,1/sz);
234         return res;
235     };
236     /** Returns an isotropic scale matrix. */
237     function scale_wi(s){
238         return scaleXYZ_wi(s,s,s);
239     };
240     function rotZ_wi(theta){
241         var res=rotZ(theta);
242         res.n=rotZ(-theta);
243         return res;
244     }
245     function rotY_wi(theta){
246         var res=rotY(theta);
247         res.n=rotY(-theta);
248         return res;
249     }
250     function rotX_wi(theta){
251         var res=rotX(theta);
252         res.n=rotX(-theta);
253         return res;
254     }
255 
256 
257 
258 
259 
260     /** 4x4 Matrix multiplication */
261     function matmul(a,b){
262         var i,j,k,m = zeros();
263         for (i=0;i<4;i++){
264             for (j=0;j<4;j++){
265                 for(k=0;k<4;k++){
266                     m[j*4+i] += a[k*4+i]*b[j*4+k];
267                 }
268             }
269         }
270         return m;
271     }
272 
273     /** 4x4 Matrix times 4x1 vector multiplication */
274     function matvec(a,b){
275         var i,j,res = [0,0,0,0];
276         for (i=0;i<4;i++){
277             for(j=0;j<4;j++){
278                 res[i] += a[j*4+i]*b[j];
279             }
280         }
281         return res;
282     }
283 
284     /**
285      * 4x4 matrix times 4xN matrix multiplication. Does a bit of extra
286      * work but allows the same routine to multiply both matrices and
287      * vectors. The end result seems to be 15 bytes shorter when using
288      * this instead of separate matmul and matvec routines.
289      */
290     function matmul4(a,b){
291         var i,j,k,m=[];
292         for (i=0;i<b.length;i++){
293             m[i]=0;
294         }
295         for (i=0;i<4;i++){
296             for (j=0;j<(b.length/4);j++){
297                 for(k=0;k<4;k++){
298                     m[j*4+i] += a[k*4+i]*b[j*4+k];
299                 }
300             }
301         }
302         return m;
303     }
304 
305     /**
306      * For 4x4 matrices, multiplies both a*b and b^{-1} * a^{-1}. 
307      */
308     function matmul_wi(a,b){
309         var res = matmul4(a,b);
310         res.n = matmul4(b.n,a.n);
311         return res;
312     }
313 
314     /**
315      * Transpose the upper 3x3 part and zero the rest. For building
316      * the normal matrix from the inverse of model matrix.
317      */
318     function transposed3x3(a){
319         return [a[0],a[4],a[8], 0,
320                 a[1],a[5],a[9], 0,
321                 a[2],a[6],a[10],0,
322                 0,   0,   0,   1];
323     }
324 
325     /** Cross product for homogeneous directions. "[(axb)^t,0]^t" */
326     function cross(a,b){
327         return [a[1]*b[2]-a[2]*b[1],
328                 a[2]*b[0]-a[0]*b[2],
329                 a[0]*b[1]-a[1]*b[0],
330                 0];
331     }
332 
333     /** Normalize x,y,z disregarding and untouching w */
334     function nmld(v){
335         var length3d=Math.sqrt(v[0]*v[0]+v[1]*v[1]+v[2]*v[2]);
336         return [v[0]/length3d,v[1]/length3d,v[2]/length3d,v[3]];
337     }
338 
339     /** Homogenize by x/w,y/w,z/w  */
340     function homogenized(v){
341         if (v[3]==0) return v;
342         return [v[0]/v[3],v[1]/v[3],v[2]/v[3],1];
343     }
344 
345 
346 // "Prelude to re-inventions" ------------------------------
347 
348 // Tentative material object..
349 // colors==[a,d,s,q] could be a 4x4 matrix?
350 // Yep, quite short code when colors are in a matrix.
351 // TODO: I think need to learn the object model of javascript, to know
352 // if this is proper.
353 function Material(colors){
354     var myc=colors.slice();
355     this.c = function(gl){
356         gl.uniformMatrix4fv(
357             gl.getUniformLocation(prg,"i"), false, myc);
358     }
359 }
360 
361 // Tentative camera object.. just put inverse of current transf in the
362 // global cameraTransformation var.
363 function Camera(){
364     this.c = function(nodetrans){
365         //alert("Positioning camera" + nodetrans);
366         cameraTransformation = nodetrans.n;
367         cameraTransformation.n = nodetrans;
368 
369     }
370 }
371 
372 
373 
374 // Traversal without inverse matrices and thus normal matrices. Use
375 // this if you know you don't need normals or camera as scenegraph node.
376 function traverse(node,ms){
377     ms=node.f.reduce(matmul4,ms);
378     gl.uniformMatrix4fv(
379         gl.getUniformLocation(prg,"mv"), false, ms);
380     node.o.map(function(o){o.c(gl);}); // map < forEach :)
381     node.c.map(function(c){traverse(c,ms);});
382 }
383 
384 /**
385  * Traverse a scene graph, and output to the WebGL pipeline.
386  *
387  * Algorithm:
388  *
389  *  This is a very basic recursive tree traversal.
390  *
391  * Size optimizations:
392  *
393  *   - The 'functional' operations provided by Javascript allow a very
394  *     compact way of writing this.
395  *
396  *   - Call map() without using its output. It is wasteful, but it is
397  *     4 characters shorter to write than forEach().
398  *
399  *   - Always send two uniform matrices, regardless of whether
400  *     anything is drawn using them. Wasted effort when not drawing.
401  **/
402 function traverse_wi(node,ms){
403     ms=node.f.reduce(matmul_wi,ms);
404     gl.uniformMatrix4fv(
405         gl.getUniformLocation(prg,"mv"), false, ms);
406     gl.uniformMatrix4fv(
407         gl.getUniformLocation(prg,"nm"), false, transposed3x3(ms.n));
408     node.o.map(function(o){o.c(gl);}); // map < forEach :)
409     node.c.map(function(c){traverse_wi(c,ms);});
410 }
411 
412 /** Dry traversal to find camera (TODO: lights?) 
413  *
414  * This traverses the scene graph but doesn't draw anything. Look for
415  * property 'r' (for cameRa or dRy or Rehearsal or whatever
416  * mnemonic..) and apply the functions found in the property.
417  */
418 function findcam_wi(node,ms){
419     ms = node.f.reduce(matmul_wi,ms);
420     if (node.r){
421         node.r.map(function(o){o.c(ms);});
422     }
423     node.c.map(function(c){findcam_wi(c,ms);});
424 }
425 
426 
427 
428 
429 
430 
431     // "Re-inventing the cylinder" ------------------------------
432 
433     /**
434      * Create an evaluator that can return a local frame for circle in
435      * the xy plane. As in the MIT course assignment, but transposed.
436      */
437     function funCircleBasic(radius,n){
438         var r = radius;
439         this.n = n; // Fidelity hint for the surface evaluator
440         this.c = function(t){
441             var s = Math.sin(t * 2 * PI);
442             var c = Math.cos(t * 2 * PI);
443             return [-c,  -s,   0,  0,  // normal
444                      0,   0,   1,  0,  // binormal
445                     -s,   c,   0,  0,  // tangent
446                      c*r, s*r, 0,  1   // position
447                    ];
448         };
449     }
450 
451     /**
452      * Create an evaluator that can return a local frame for a part or
453      * whole circle on the xy plane. The starting point of the arc is
454      * at the highest y point "top", and direction is counterclockwise
455      * in right-handed coordinates, z towards viewer. Assumes
456      * 0<arclen<=1. Arclen is optional.
457      */
458     function funCircle(radius,swpfidel,arclen){
459         var r = radius;
460         var a = arclen?arclen:1;
461         // Fidelity hint for the sweep surface evaluator
462         this.n = swpfidel?swpfidel:10;
463         this.c = function(t){
464             var s = Math.sin(t * 2 * PI * a);
465             var c = Math.cos(t * 2 * PI * a);
466             return [    s,  -c,  0,  0,  // normal
467                         0,   0,  1,  0,  // binormal
468                        -c,  -s,  0,  0,  // tangent
469                      -s*r, c*r,  0,  1   // position
470                    ];
471         };
472     }
473 
474 
475     // Line from zero to length, along y axis
476     // Some bugs here? Haven't actually used this at all...
477     function funLine(length,n){
478         var l = length;
479         this.n = n;
480         this.c = function(t){
481             return [ 1,   0,   0, 0,   // "normal"/orientation
482                      0,   0,   1, 0,   // binormal
483                      0,   1,   0, 0,   // tangent
484                      0,   t*l, 0, 1    // position
485                    ];
486         };
487     }
488 
489 
490     /**
491      * Simple uniform B-spline evaluator.
492      *
493      * Hmm.. position and tangent could be evaluated for any t... But
494      * how to maintain correct normal and bi-normal for surface
495      * creation? Initial idea: pre-compute at some intervals using the
496      * cross product trick from our lecture notes, and then evaluate a
497      * normalized interpolant upon call to eval(). NOTE: Only need to
498      * store binormal (?), because normal can be computed via cross
499      * product. The binormal rotations could be corrected while
500      * precomputing. (not yet done).
501      *
502      * Note that analytic tangent doesn't exist for all possible
503      * inputs - we don't handle curves with vanishing derivatives,
504      * so keep this in mind when defining control points.
505      *
506      * TODO: Maybe could be sloppy and not even interpolate, if result
507      * is OK visually? Hmm... we do have storage, so why not just
508      * precompute like a *lot* of values when creating the spline
509      * object, and then return the closest one in compute(t)? Nasty, but
510      * without redundance...
511      *
512      * Back-of-the-envelope: 100 control pts * 100 intermediates * 16
513      * * 8 byte float is 1280000 bytes... well.. that's a megabyte for
514      * one spline..  admittedly, it sounds like a lot..
515      *
516      * TODO (cheating a bit, though): Use only xy-curves with no
517      * change of curvature, and delete all the code about flipping
518      * gradients.  Would be so much smaller and leaner! Well.. if we
519      * don't cheat that much, then at least the production should USE
520      * the feature and have some twisty curve(s) to show it
521      * off.. Closure will omit unused functions, so we can have many
522      * versions here in the library. This is the bloaty, most general
523      * version. Depending on what you need, you can "cheat" more by
524      * using the restricted versions below.
525      */
526 
527     function funBSpline(pts) {
528         // Let us declare all vars here to shorten the code.
529         var i;
530         var g,Tt,dTt,v,T,N;
531         var t,ifirst,npts = this.n = pts.length/4;
532         var nsteps = (npts-3)*300; // internal points (actually +1 because we go to t=1.0)
533         var b = [];           // the internal storage
534         var B = [0,0,1,0];    // "arbitrary binormal at beginning"
535         //this.n=npts;
536 
537         for (i=0;i<nsteps+1;i++){
538             t = i/nsteps; // scale t to [0,1] within curve
539             if (i<nsteps) {
540                 ifirst = t*(npts-3) | 0; // 1st point is... (funny "|0" makes a floor())
541                 t = t*(npts-3) - ifirst; // reset t to [0,1] within segm.
542             } else {
543                 ifirst = npts-4;
544                 t = 1;
545             }
546 
547             g = pts.slice(ifirst*4,ifirst*4+16); // pick cps to G.
548             Tt = [1, t, t*t, t*t*t];
549             dTt = [0, 1, 2*t, 3*t*t];
550             v = matmul4(matmul4(g, bspB),Tt);
551             T = nmld(matmul4(matmul4(g, bspB),dTt));
552             N = nmld(cross(B,T));
553             B = nmld(cross(T,N));
554             b.push([].concat(N, // "normal"/orientation
555                              B, // binormal
556                              T, // tangent
557                              v)); // pos.
558         }
559 
560         // as of now, we don't care to interpolate:
561         this.c = function(t){
562             return b[0 | t*(nsteps)];
563         }
564     }
565 
566     /**
567      * Simple uniform B-spline with transformed control points.. This
568      * was "new" in Instanssi 2018 - allows scaled and/or skewed
569      * geometries but needs to be done during instantiation.
570      */
571     function funBSplineTransformed(pts,tfm) {
572         // Let us declare all vars here to shorten the code.
573         var i;
574         var g,Tt,dTt,v,T,N;
575         var t,ifirst,npts = this.n = pts.length/4;
576         var nsteps = (npts-3)*300; // internal points (actually +1 because we go to t=1.0)
577         var b = [];           // the internal storage
578         var B = [0,0,1,0];    // "arbitrary binormal at beginning"
579         //this.n=npts;
580         pts = matmul4(tfm,pts); // transform!
581 
582 
583         for (i=0;i<nsteps+1;i++){
584             t = i/nsteps; // scale t to [0,1] within curve
585             if (i<nsteps) {
586                 ifirst = t*(npts-3) | 0; // 1st point is... (funny "|0" makes a floor())
587                 t = t*(npts-3) - ifirst; // reset t to [0,1] within segm.
588             } else {
589                 ifirst = npts-4;
590                 t = 1;
591             }
592 
593             g = pts.slice(ifirst*4,ifirst*4+16); // pick cps to G.
594             Tt = [1, t, t*t, t*t*t];
595             dTt = [0, 1, 2*t, 3*t*t];
596             v = matmul4(matmul4(g, bspB),Tt);
597             T = nmld(matmul4(matmul4(g, bspB),dTt));
598             N = nmld(cross(B,T));
599             B = nmld(cross(T,N));
600             b.push([].concat(N, // "normal"/orientation
601                              B, // binormal
602                              T, // tangent
603                              v)); // pos.
604         }
605 
606         // as of now, we don't care to interpolate:
607         this.c = function(t){
608             return b[0 | t*(nsteps)];
609         }
610     }
611 
612 
613 
614     /**
615      * XY-plane curves with no inflection points (B==(0,0,1)).
616      *
617      * Some 58 bytes smaller than the more general version.
618      *
619      */
620     function funBSplineXYnoInf(ipts) {
621         // Let us declare all vars here to shorten the code.
622         var npts = this.n = ipts.length/4;
623         var pts=ipts;
624 
625         this.c = function(t){
626             var ifirst;
627             // t arrives as [0,1] within curve
628             if (t<1) {
629                 ifirst = t*(npts-3) | 0;
630                 t = t*(npts-3) - ifirst;
631             } else {
632                 ifirst = npts-4;
633                 //t = 1;
634             }
635 
636             var g = pts.slice(ifirst*4,ifirst*4+16), // pick cps to G.
637                 B = [0,0,1,0],    // Constant binormal
638                 Tt = [1, t, t*t, t*t*t],
639                 dTt = [0, 1, 2*t, 3*t*t],
640                 T = nmld(matmul4(matmul4(g, bspB),dTt));
641             return [].concat(nmld(cross(B,T)), // "normal"/orientation
642                              B, // binormal
643                              T, // tangent
644                              matmul4(matmul4(g, bspB),Tt)); // pos.
645         }
646     }
647 
648 
649     /**
650      * Simple rational Bezier evaluator.
651      *
652      */
653 
654     function funBezierCurve(pts) {
655         // Let us declare all vars here to shorten the code.
656         var i;
657         var g,Tt,dTt,v,T,N;
658         var t,ifirst,npts = pts.length/4;
659         this.n = pts.length*4; // Fidelity hint for surface eval
660         var nsteps = ((npts-1)/3)*10; // internal points (actually +1 because we go to t=1.0)
661         var b = [];           // the internal storage
662         var B = [0,0,1,0];    // "arbitrary binormal at beginning"
663         //this.n=npts;
664 
665         for (i=0;i<nsteps+1;i++){
666             t = i/nsteps; // scale t to [0,1] within curve
667             if (i<nsteps) {
668                 ifirst = (t*((npts-1)/3) | 0)*3; // 1st point is... (funny "|0" makes a floor())
669                 t = t*((npts-1)/3) - (ifirst/3); // reset t to [0,1] within segm.
670             } else {
671                 ifirst = npts-4;
672                 t = 1;
673             }
674 
675             g = pts.slice(ifirst*4,ifirst*4+16); // pick cps to G.
676             Tt = [1, t, t*t, t*t*t];
677             dTt = [0, 1, 2*t, 3*t*t];
678             //v = matmul4(matmul4(g, bezB),Tt);
679             v = homogenized(matmul4(matmul4(g, bezB),Tt));
680             T = nmld(matmul4(matmul4(g, bezB),dTt));
681             N = nmld(cross(B,T));
682             B = nmld(cross(T,N));
683             b.push([].concat(N, // "normal"/orientation
684                              B, // binormal
685                              T, // tangent
686                              v)); // pos.
687             //console.log("ja tuota " + npts + " " + ifirst + " " + t);
688         }
689 
690         // as of now, we don't care to interpolate:
691         this.c = function(t){
692             return b[0 | t*(nsteps)];
693         }
694     }
695 
696 
697 
698     /**
699      * Convert packed 2d-vectors to (x,y,0,1) homogenous 3d
700      * coordinates. (Abandoned idea - didn't gain anything)
701      */
702     function xyToHomog(ptsxy){
703         var res=[];
704         for(var i=0;i<ptsxy.length;){
705             res.push(ptsxy[i++],ptsxy[i++],0,1);
706         }
707         return res;
708     }
709 
710     /**
711      * Push values of the matrix column icol to array, optionally
712      * multiply by mul!=0
713      */
714     function pushCol4(array,mat,icol,mul){
715         mul=mul?mul:1;
716         for(var i=0;i<4;i++){
717             array.push(mul*mat[4*icol+i]);
718         }
719     }
720 
721     /** Helper function. More bloaty than the few inlined calls: */
722     function createAndFillArrayBuffer32(data){
723         var buf=gl.createBuffer();
724         gl.bindBuffer(gl.ARRAY_BUFFER, buf);
725         gl.bufferData(gl.ARRAY_BUFFER,
726                       new Float32Array(data),
727                       gl.STATIC_DRAW);
728         return buf;
729     }
730 
731     /**
732      * Prepare a drawable WebGL buffer that represents a box.
733      *
734      * It's a bit funny that even though the box is a very simple
735      * shape, the data and code required for making one is actually
736      * quite bloaty when compared to generalized cylinders. Hmm,
737      * technically it could be possible to make a box with a 4-point
738      * circle, but then there should be some clever way to make
739      * non-smooth normals. Think about this..
740      *
741      */
742     function Box(size){
743         // GL buffer objects
744         var vertexColorBuf = gl.createBuffer(),
745             vertexBuf = gl.createBuffer(),
746             vertexNormalBuf = gl.createBuffer(),
747             faceBuf = gl.createBuffer();
748 
749         var vertices=[];
750         var normals=[];
751         var vind=[];
752 
753         vertices = [
754             // Front
755             -1, -1,  1, 1,
756              1, -1,  1, 1,
757              1,  1,  1, 1,
758             -1,  1,  1, 1,
759             // Back
760             -1, -1, -1, 1,
761             -1,  1, -1, 1,
762              1,  1, -1, 1,
763              1, -1, -1, 1,
764             // Top
765             -1,  1, -1, 1,
766             -1,  1,  1, 1,
767              1,  1,  1, 1,
768              1,  1, -1, 1,
769             // Bottom
770             -1, -1, -1, 1,
771              1, -1, -1, 1,
772              1, -1,  1, 1,
773             -1, -1,  1, 1,
774             // Right
775              1, -1, -1, 1,
776              1,  1, -1, 1,
777              1,  1,  1, 1,
778              1, -1,  1, 1,
779             // Left
780             -1, -1, -1, 1,
781             -1, -1,  1, 1,
782             -1,  1,  1, 1,
783             -1,  1, -1, 1,
784         ];
785 
786         normals = [
787             // Front
788             0, 0, 1, 0,
789             0, 0, 1, 0,
790             0, 0, 1, 0,
791             0, 0, 1, 0,
792             // Back
793             0, 0,-1, 0,
794             0, 0,-1, 0,
795             0, 0,-1, 0,
796             0, 0,-1, 0,
797             // Top
798             0, 1, 0, 0,
799             0, 1, 0, 0,
800             0, 1, 0, 0,
801             0, 1, 0, 0,
802             // Bottom
803             0,-1, 0, 0,
804             0,-1, 0, 0,
805             0,-1, 0, 0,
806             0,-1, 0, 0,
807             // Right face
808             1, 0, 0, 0,
809             1, 0, 0, 0,
810             1, 0, 0, 0,
811             1, 0, 0, 0,
812             // Left face
813             -1, 0, 0, 0,
814             -1, 0, 0, 0,
815             -1, 0, 0, 0,
816             -1, 0, 0, 0,
817         ]
818 
819         /*
820         // Colors.. nah..
821         gcolors = [
822             // Front
823              1,  0,  0, 1,
824              0,  1,  0, 1,
825              0,  0,  1, 1,
826              0,  1,  0, 1,
827             // Back
828             -1, -1, -1, 1,
829             -1,  1, -1, 1,
830              1,  1, -1, 1,
831              1, -1, -1, 1,
832             // Top
833             -1,  1, -1, 1,
834             -1,  1,  1, 1,
835              1,  1,  1, 1,
836              1,  1, -1, 1,
837             // Bottom
838             -1, -1, -1, 1,
839              1, -1, -1, 1,
840              1, -1,  1, 1,
841             -1, -1,  1, 1,
842             // Right
843              1, -1, -1, 1,
844              1,  1, -1, 1,
845              1,  1,  1, 1,
846              1, -1,  1, 1,
847             // Left
848             -1, -1, -1, 1,
849             -1, -1,  1, 1,
850             -1,  1,  1, 1,
851             -1,  1, -1, 1,
852         ];
853         */
854 
855         vind = [
856             0,  1,  2,      0,  2,  3,    // front
857             4,  5,  6,      4,  6,  7,    // back
858             8,  9,  10,     8,  10, 11,   // top
859             12, 13, 14,     12, 14, 15,   // bottom
860             16, 17, 18,     16, 18, 19,   // right
861             20, 21, 22,     20, 22, 23,   // left
862         ];
863 
864         // Fill in buffers (non-animated shapes)
865 
866         //hmm?
867         gl.bindBuffer(gl.ARRAY_BUFFER, vertexColorBuf);
868         gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(normals), gl.STATIC_DRAW);
869 
870         gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuf);
871         gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
872         gl.bindBuffer(gl.ARRAY_BUFFER, vertexNormalBuf);
873         gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(normals), gl.STATIC_DRAW);
874         gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, faceBuf);
875         gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(vind), gl.STATIC_DRAW);
876 
877         // "Compute", i.e., Bind and draw to pipeline
878         this.c = function(gl){
879             var i;
880             // This quite unnecessary, actually (TODO:)
881             gl.bindBuffer(gl.ARRAY_BUFFER, vertexColorBuf);
882             gl.enableVertexAttribArray(i=gl.getAttribLocation(prg,"g"));
883             gl.vertexAttribPointer(i, 4, gl.FLOAT, false, 0, 0);
884 
885             gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuf);
886             gl.enableVertexAttribArray(i=gl.getAttribLocation(prg,"v"));
887             gl.vertexAttribPointer(i, 4, gl.FLOAT, false, 0, 0);
888 
889             gl.bindBuffer(gl.ARRAY_BUFFER, vertexNormalBuf);
890             gl.enableVertexAttribArray(i=gl.getAttribLocation(prg,"N"));
891             gl.vertexAttribPointer(i, 4, gl.FLOAT, false, 0, 0);
892 
893             gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, faceBuf);
894             gl.drawElements(gl.TRIANGLES, 6*2*3, gl.UNSIGNED_SHORT,0);
895         };
896 
897 
898     }
899 
900 
901     /**
902      * A square [-1,1]^2 on xy-plane, z==0. This may not be very useful..
903      */
904     function Square(size){
905         // GL buffer objects
906         var vertexColorBuf = gl.createBuffer(),
907             vertexBuf = gl.createBuffer(),
908             vertexNormalBuf = gl.createBuffer(),
909             faceBuf = gl.createBuffer();
910 
911         var vertices=[];
912         var normals=[];
913         var vind=[];
914 
915         vertices = [
916             // Front
917             -1, -1,  0, 1,
918              1, -1,  0, 1,
919              1,  1,  0, 1,
920             -1,  1,  0, 1,
921         ];
922 
923         normals = [
924             // Front
925             0, 0, 1, 0,
926             0, 0, 1, 0,
927             0, 0, 1, 0,
928             0, 0, 1, 0,
929         ]
930 
931         vind = [
932             0,  1,  2,      0,  2,  3,
933         ];
934 
935         // Fill in buffers (non-animated shapes)
936 
937         //hmm?
938         gl.bindBuffer(gl.ARRAY_BUFFER, vertexColorBuf);
939         gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(normals), gl.STATIC_DRAW);
940 
941         gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuf);
942         gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
943         gl.bindBuffer(gl.ARRAY_BUFFER, vertexNormalBuf);
944         gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(normals), gl.STATIC_DRAW);
945         gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, faceBuf);
946         gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(vind), gl.STATIC_DRAW);
947 
948         // "Compute", i.e., Bind and draw to pipeline
949         this.c = function(gl){
950             var i;
951             // This quite unnecessary, actually (TODO:)
952             gl.bindBuffer(gl.ARRAY_BUFFER, vertexColorBuf);
953             gl.enableVertexAttribArray(i=gl.getAttribLocation(prg,"g"));
954             gl.vertexAttribPointer(i, 4, gl.FLOAT, false, 0, 0);
955 
956             gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuf);
957             gl.enableVertexAttribArray(i=gl.getAttribLocation(prg,"v"));
958             gl.vertexAttribPointer(i, 4, gl.FLOAT, false, 0, 0);
959 
960             gl.bindBuffer(gl.ARRAY_BUFFER, vertexNormalBuf);
961             gl.enableVertexAttribArray(i=gl.getAttribLocation(prg,"N"));
962             gl.vertexAttribPointer(i, 4, gl.FLOAT, false, 0, 0);
963 
964             gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, faceBuf);
965             gl.drawElements(gl.TRIANGLES, 1*2*3, gl.UNSIGNED_SHORT,0);
966         };
967     }
968 
969 
970 
971 
972 
973     /**
974      * Prepare a drawable WebGL buffer of a generalized cylinder
975      * (swept profile + caps)
976      */
977     function GenCyl(prof, profilesz, swp) {
978         var j,i,t,fp,lf,fs,ts;
979         var vertices = [];
980         var normals = [];
981         var vind = [];
982         var colors = [];
983         //var numfaces = (profilesz-1)*(sweepsz-1)*2+2*profilesz;
984         var sweepsz = (swp.n*1.5)|0; // heuristic for sweep size.
985 
986         //var numfaces = 2*(sweepsz)*(profilesz-1)+2;
987         var numfaces = 2*(sweepsz-1)*(profilesz-1);
988 
989 
990         // GL buffer objects, to be filled as the last stage of init
991         var vertexColorBuf = gl.createBuffer(),
992             vertexNormalBuf = gl.createBuffer(),
993             vertexBuf = gl.createBuffer(),
994             faceBuf = gl.createBuffer();
995 
996 
997         // location, normal, and color of each vertex
998         for (j=0; j<sweepsz; j++){
999             ts = j/(sweepsz-1);
1000             fs = swp.c(ts);
1001             for (i=0;i<profilesz;i++){
1002                 t = i/(profilesz-1);
1003                 fp = prof.c(t);
1004                 lf = matmul4(fs,fp);
1005                 //vertices.push(lf[12],lf[13],lf[14],1);
1006                 //normals.push(lf[0],lf[1],lf[2],0);
1007                 pushCol4(vertices,lf,3);
1008                 pushCol4(normals,lf,0,-1); // invert!
1009                 colors.push((2*j+i+0)%3?0:1,
1010                             (2*j+i+1)%3?0:1,
1011                             (2*j+i+2)%3?0:1,
1012                             1);
1013             }
1014         }
1015 
1016         // triangles as indices
1017         for (j=0; j<sweepsz-1; j++){
1018             for (i=0; i<profilesz-1; i++){
1019                 vind.push(j*profilesz+i,   j*profilesz+i+1,     (j+1)*profilesz+i);
1020                 vind.push(j*profilesz+i+1, (j+1)*profilesz+i+1, (j+1)*profilesz+i);
1021             }
1022         }
1023 
1024 
1025         // Add end caps (quite naive, assume convex profile curve
1026         // containing origin and curving to the left on xy-plane)
1027         // TODO: Separate, optional, function for making caps if needed.
1028 
1029 /*
1030         // start cap (normal opposite of sweep tangent)
1031         fs = swp.c(0); // center point
1032         pushCol4(vertices,fs,3);
1033         pushCol4(normals,fs,2,-1);
1034         colors.push(1,0,0,1);
1035         for (i=0;i<profilesz;i++){
1036             t = i/(profilesz-1);
1037             fp = prof.c(t);
1038             lf = matmul4(fs,fp);
1039             colors.push(0,i%2,(i+1)%2,1);
1040             pushCol4(vertices,lf,3);
1041             pushCol4(normals,fs,2,-1);
1042         }
1043 
1044         // end cap (normal same as sweep tangent)
1045         fs = swp.c(1); // center point
1046         pushCol4(vertices,fs,3);
1047         pushCol4(normals,fs,2);
1048         colors.push(1,0,0,1);
1049         for (i=0;i<profilesz;i++){
1050             t = i/(profilesz-1);
1051             fp = prof.c(t);
1052             lf = matmul4(fs,fp);
1053             colors.push(0,i%2,(i+1)%2,1);
1054             pushCol4(vertices,lf,3);
1055             pushCol4(normals,fs,2);
1056 
1057             // Add faces here to avoid having another loop
1058             vind.push(sweepsz*profilesz,
1059                       1+sweepsz*profilesz+((i+1)%profilesz),
1060                       1+sweepsz*profilesz+i,
1061                       1+sweepsz*profilesz+profilesz,
1062                       1+sweepsz*profilesz+i+profilesz+1,
1063                       1+sweepsz*profilesz+((i+1)%profilesz)+profilesz+1
1064                      );
1065 
1066         }
1067 */
1068 
1069         // Fill in buffers (non-animated shapes)
1070 
1071         gl.bindBuffer(gl.ARRAY_BUFFER, vertexColorBuf);
1072         gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW);
1073         gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuf);
1074         gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
1075         gl.bindBuffer(gl.ARRAY_BUFFER, vertexNormalBuf);
1076         gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(normals), gl.STATIC_DRAW);
1077         gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, faceBuf);
1078         gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(vind), gl.STATIC_DRAW);
1079 
1080         // "Compute", i.e., Bind and draw to pipeline
1081         this.c = function(gl){
1082             gl.bindBuffer(gl.ARRAY_BUFFER, vertexColorBuf);
1083             gl.enableVertexAttribArray(i=gl.getAttribLocation(prg,"g"));
1084             gl.vertexAttribPointer(i, 4, gl.FLOAT, false, 0, 0);
1085 
1086             gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuf);
1087             gl.enableVertexAttribArray(i=gl.getAttribLocation(prg,"v"));
1088             gl.vertexAttribPointer(i, 4, gl.FLOAT, false, 0, 0);
1089 
1090             gl.bindBuffer(gl.ARRAY_BUFFER, vertexNormalBuf);
1091             gl.enableVertexAttribArray(i=gl.getAttribLocation(prg,"N"));
1092             gl.vertexAttribPointer(i, 4, gl.FLOAT, false, 0, 0);
1093 
1094             gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, faceBuf);
1095             gl.drawElements(gl.TRIANGLES, numfaces*3, gl.UNSIGNED_SHORT,0);
1096         };
1097     };
1098 
1099 
1100 
1101 
1102