Source Code - JavaScript
JavaScript is not known for its excellent graphics capabilities because it simply
doesn't have any that compares with the graphics that most of us are used to having
at our programming fingertips. However, as you'll see on this page that even the
most minimal graphics capabilities can be put to good use. Through the use of
HTML CSS (Cascade Style Sheets) we'll see that limited 3D graphics and animation are possible.
Basic Theory
Walter Zorn
JavaScript Code
Download Code
Return to top of document
Basic Theory - HTML and CSS: Graphics on the Cheap
Here's a sample 3D rotating cube that you can create using nothing but
HTML and JavaScript.
In summary, the trick to creating graphics with HTML and JavaScript is to use
HTML Cascading Style Sheet features to create a canvas onto which JavaScript
draws the graphics. Several existing features of HTML/CSS and JavaScript act
as a starting point, but require some development (code) to get to the point
of being able to create the 3D rotating cube you see above.
HTML simply consists of tags which the browser can use as directions on how
to display (render) the content of the HTML files. Cascading Style Sheets (CSS)
technology was developed to provide a way to apply rendering rules to HTML
tags anywhere within a document. Then eventually an update to CSS enabled
the programmer to position HTML elements anywhere on the page with pixel
level accuracy.
CSS positioning works by placing an HTML object into an invisible bounding
box whose position can be specified by HTML/CSS code. Positioning can be
absolute (with respect to the top/left corner of the page) or relative
(with respect to other elements on the page). This includes layering
of objects on top of one another. CSS also offers the ability to clip
objects, such as when two objects overlap. Objects may also be
rendered invisible. Objects in this case refer to an HTML tag pair
and any content placed within them.
Of particular importance to our goal of generating graphics with
JavaScript is the application of CSS to the DIV tag of HTML.
HTML supports the DIV tag, which is a way of applying standard
HTML tags to a block of HTML content, but it is the CSS positioning
capabilities which has opened the door to using the DIV tag
.... more to come. It's midnight and my eyes are closing. I'll be back ...
Return to top of document
Walter Zorn
As you saw above, the use of DIV tags and CSS features to create limited graphics is pretty
straightforward. However, the discussion above simply showed that graphics are possible.
Creating a set of methods or functions that can be easily used in JavaScript can take
a fair amount of coding!
Fortunately, a good Samaritan by the name of Walter Zorn has done the hard work for
us and made a JavaScript library available free of charge which will provide a set of methods
for drawing lines and basic shapes (ellipses, rectangles, polygons) and also for filling
those shapes. The code is available at
Walter's site.
The rotating cube you saw above uses Zorn's graphics library and I'll be discussing
the code that was needed (in addition to Zorn's library) to create the 3D graphics.
This first version is simply a rotating wireframe 3D cube, but I'll have a shaded version up
shortly.
Return to top of document
JavaScript Code for 3D Rotating Cube
If you've read my other pages which discuss the code for VB and Java (applet) versions
of the rotating cube, you'll find the following code pretty easy to follow. I'll be
adding additional features and GUI elements later on, but right now the code is
particularly simple.
There are two lines of code which must be placed before the JavaScript code listed
below.
Graphics Pipeline
The graphics pipeline is just another name for the complete set of procedures
executed to rotate and display a 3D object. In this example, I have modelled
the cube as 8 points which comprise 12 triangles. The JavaScript program
starts by initializing the variables needed for the program and then repeatedly
calls the graphics pipeline function. The Pipeline function in turn calls
each of the elements of a graphics pipeline.
function Pipeline()
{
SortByDepth();
BackFaceCulling();
ApplyProjection();
DrawCube();
RotatePoints();
}
Initialization
I've chosen to use 3 arrays (Px, Py, and Pz) to contain the xyz coordinates of
the eight points that make up the cube. Three other arrays (V1, V2, and V3) contain
the points which make up the 12 triangles. Other variables include:
- Theta
Angle of rotation
- L
Dimension of a side of the cube
- XOffset/YOffset
Distance to move the cube (left/right) to put it away from the
margin of the web page
- jg
This is the object onto which all graphics will be written. The
jsGraphics class (object) is defined in Zorn's library.
The JavaScript initialization code is as follows:
Theta = 0.015; L = 50; POV = 500; Offset = 100;
iLoop = 0; Delay = 15; NumberTriangles=12;
XOffset=100; YOffset=100;
Px = new Array(-L, -L, L, L, -L, -L, L, L); //real x-coord, 8 pts
Py = new Array(-L, L, L, -L, -L, L, L, -L); //real y-coord, 8 pts
Pz = new Array(-L, -L, -L, -L, L, L, L, L); //real z-coord, 8 pts
PPx = new Array(-L, -L, L, L, -L, -L, L, L); //projected x-coord, 8 pts
PPy = new Array(-L, L, L, -L, -L, L, L, -L); //projected y-coord, 8 pts
XP = new Array(4);
YP = new Array(4);
V1 = new Array(0, 0, 4, 4, 7, 7, 3, 3, 2, 2, 3, 3); //vertex1
V2 = new Array(3, 2, 0, 1, 4, 5, 7, 6, 6, 5, 0, 4); //vertex2
V3 = new Array(2, 1, 1, 5, 5, 6, 6, 2, 5, 1, 4, 7); //vertex3
V4 = new Array(12); //Average Z of all 3 vertices
V5 = new Array(12); //DotProduct of Normal and POV
Point Rotation
Rotation of an object is performed by rotating each of its points about the
point of rotation. In this case I defined the cube as symmetrical about the
origin, so I do not have to perform a translation of the cube before
applying the equations of rotation. In this example I use Theta to rotate
about the x, y, and z axes.
function RotatePoints()
{
for (wr=0; wr < 8; wr++)
{
oldY = Py[wr]; oldZ = Pz[wr];
Py[wr] = oldY * Math.cos(Theta) - oldZ * Math.sin(Theta); //rotate about X
Pz[wr] = oldY * Math.sin(Theta) + oldZ * Math.cos(Theta); //rotate about X
oldX = Px[wr]; oldZ = Pz[wr];
Px[wr] = oldZ * Math.sin(Theta) + oldX * Math.cos(Theta); //rotate about Y
Pz[wr] = oldZ * Math.cos(Theta) - oldX * Math.sin(Theta); //rotate about Y
oldX = Px[wr]; oldY = Py[wr];
Px[wr] = oldX * Math.cos(Theta) - oldY * Math.sin(Theta); //rotate about Z
Py[wr] = oldX * Math.sin(Theta) + oldY * Math.cos(Theta); //rotate about Z
}
}
Sort the Triangles by Depth
At the end of the 3D graphics pipeline, the DrawCube subroutine draws the triangles
one at a time starting at the zero position of the Px, Py and Pz arrays. By sorting
the arrays relative to their depth in the 3D graphics scene (the average z value of
the three points in the triangle) before drawing the triangles, the triangles
farthest away are drawn first.
This approach is called the Painter's Algorithm and ensures that the nearest objects
in a 3D graphics scene will be in front of the farthest objects. It works well but
has limitations, such as not working well for intersecting triangles. There are
variations of the Painter's Algorithm which address these shortcomings but this
example uses the simple sort routine, which works fine for objects of low complexity.
function SortByDepth()
{
for (ws=0;ws<12;ws++)
{
V4[ws] = (Pz[V1[ws]]+Pz[V2[ws]]+Pz[V3[ws]]) / 3;
}
for (gs=0; gs < 11 ; gs++)
{
for (hs=0; hs < 12; hs++)
{
if (V4[gs] < V4[hs])
{
V1temp = V1[gs]; V2temp = V2[gs]; V3temp = V3[gs]; _
V4temp = V4[gs]; V5temp = V5[gs];
V1[gs]=V1[hs]; V2[gs]=V2[hs]; V3[gs]=V3[hs]; _
V4[gs]=V4[hs]; V5[gs]=V5[hs];
V1[hs]=V1temp; V2[hs]=V2temp; V3[hs]=V3temp; _
V4[hs]=V4temp; V5[hs]=V5temp;
}
}
}
}
Backface Culling
Triangles which face away from the point of view in a 3D graphics scene are not visible to the viewer.
Not drawing those triangles simplifies the load on the computer display system and provides for a faster
update of each scene. This is called backface culling and is almost always included in graphics pipelines.
The approach is to calculate a normal vector to the triangle by calculating the cross product against two
points in the triangle. The dot product is then calculated between the normal vector and the point
of view vector. A dot product less than zero signifies that the triangles faces away from the viewer
and need not be displayed. The BackFaceCulling function calculates and stores the dot product for later
use in the DrawCube function.
function BackFaceCulling()
{
for (wb=0; wb < 12 ; wb++)
{
// Cross Product
CPX1 = Px[V2[wb]] - Px[V1[wb]];
CPY1 = Py[V2[wb]] - Py[V1[wb]];
CPZ1 = Pz[V2[wb]] - Pz[V1[wb]];
CPX2 = Px[V3[wb]] - Px[V1[wb]];
CPY2 = Py[V3[wb]] - Py[V1[wb]];
CPZ2 = Pz[V3[wb]] - Pz[V1[wb]];
DPX = CPY1 * CPZ2 - CPY2 * CPZ1;
DPY = CPX2 * CPZ1 - CPX1 * CPZ2;
DPZ = CPX1 * CPY2 - CPX2 * CPY1;
// DotProduct uses POV vector 0,0,POV as x1,y1,z1
V5[wb] = 0 * DPX + 0 * DPY + POV * DPZ;
}
}
Projection
Until now all point coordinates have been world coordinates - the position of the
3D cube in space. These point coordinates must now be projected onto the
computer screen - onto a 2D surface. The function is given below. In this example
I use simple parallel projection - where the xy coordinates are used. The
A more common technique is to use perspective projection, where objects farther in
the 3D graphics scene appear smaller.
function ApplyProjection()
{
for (wp=0; wp < 8 ; wp++) {
PPx[wp] = Px[wp];
PPy[wp] = Py[wp];
}
}
Draw the Cube
Once the point coordinates are rotated, we can use the drawPolygon() function of
Zorn's library to draw the perimeter of the triangles. The following code
assigns the coordinates of each triangle's points to the arrays XP and YP, then
used the drawPolygon() function to render the cube. The clear() and paint()
functions are used, respectively, to erase prior graphics and to make visible
the results of the drawPolygon() function.
function DrawCube()
{
jg.clear();
for (wd=0; wd < 12; wd++)
{
XP[0]=PPx[V1[wd]]+XOffset;XP[1]=PPx[V2[wd]]+XOffset;_
XP[2]=PPx[V3[wd]]+XOffset;
YP[0]=PPy[V1[wd]]+YOffset;YP[1]=PPy[V2[wd]]+YOffset;_
YP[2]=PPy[V3[wd]]+YOffset;
XP[0]=Math.floor(XP[0]);
XP[1]=Math.floor(XP[1]);
XP[2]=Math.floor(XP[2]);
YP[0]=Math.floor(YP[0]);
YP[1]=Math.floor(YP[1]);
YP[2]=Math.floor(YP[2]);
XP[3]=XP[0]; YP[3]=YP[0];
if (V5[wd]>0)
{
jg.setColor("#0000ff");
jg.fillPolygon(XP,YP);
jg.setColor("#ff0000");
jg.drawPolygon(XP,YP);
}
}
jg.paint();
}
Animation
JavaScript has a very useful method called setInterval() which will simply repeat
an expression or function at fixed intervals. The animation of the 3D cube simply
requires the use of setInterval to call the Pipeline() function, which in turn
calls the RotatePoints() and DrawCube() functions. The setInterval() method is
coded to repeat every 50 milliseconds. Note that the graphics library is
relatively slow, and that more complex objects may take considerably more time
than 50 milliseconds for each cycle.
I'll be adding other graphics
pipeline functions later on - just as was done with the VB and Java (applet) versions
of this the 3D rotating cube.
setInterval('Pipeline()',50);
That's it. Using the graphics library from Zorn (about 900 lines of code), we were able
to implement a complete 3D graphics scene with barely a hundred lines of JavaScript code.
|