Canvas 3D Graphics


3D Graphics for HTML5 canvas

Here's an example of a 3D shape, a regular icosahedron, drawn on a canvas element using the cvsGraphCtx3D graphics context and its companion library cvs3DLib. The drawing context object provides the programming interface and the JavaScript library has all the basic 3D objects and methods.

CvsGraphCtx3D a canvas 3D graphics context

The CvsGraphCtx3D canvas graphics context provides a user friendly set of methods for drawing 3D graphics. 3D objects are constructed by creating panels of any shape, moving them into position and grouping them to form the 3D object.

The main features of the CvsGraphCtx3D drawing context are:

  • Curved or Straight edged panels - The basic building block of 3D objects is the Path3D object which is a multi-segment path, its segments can be straight lines or Bezier curves. Path3Ds can be translated and rotated in 3D space to form more complex shapes.
  • Simple 3D animation - When several Path3D objects are grouped to form a Group3D object, they can be moved as a single entity with 3 dimensions of rotation (about an arbitrary point) and 3 dimensions of translation, making animation very simple.
  • World Coordinates - All 3D objects can be created and rendered in a user defined, world coordinate units. All the mapping of world coordinates to canvas pixels is handled by the cvsGraphCtx3D.
  • Filled or Wireframe Rendering - When rendered to the canvas, the Path3Ds that are closed paths are filled with their own color. Open Path3Ds are be drawn as a wireframe model. When Group3Ds are rendered as wireframe, all component Path3Ds are drawn as wireframe even if they are closed.
  • Shading - When 3D objects are rendered with filled panels, each panel's color is shaded according to the position of the user defined light source.
  • Perspective projection - When rendered onto the 2D canvas the correct perspective is applied for a viewpoint at a user defined distance.
  • Text - Drawing text strings in 3D is supported. A vector based font allows text to be created as a Path3D object with all the capabilities of 3D movement and perspective projection.

Cross-browser support

The canvas element has native support in all the major browsers, Firefox, Safari, Opera etc. Internet Explorer lacks native support for the canvas element, but an excellent canvas emulator excanvas translates canvas methods into IE's VML graphics. CvsGraphCtx3D is programmed using only the commands common to the excanvas emulator and HTML5 canvas methods. A patched version of excanvas, excanvas-modified.js written by Tommy Maintz, is recommended.

Using CvsGraphCtx3D.

Firstly, download the five JavaScript files required for CvsGraphCtx3D:
cvsGraphCtx3D2v04.js,
cvs3DLib-07.js,
rgbaColor.js.
canvastext.js.
excanvas-modified.js,
These should be placed in the same directory as the web page html file. Then add the following lines to the web page HTML file header:

<!--[if IE]><script type="text/javascript" src="excanvas-modified.js"></script><![endif]-->
  <script type="text/javascript" src="cvsGraphCtx3D2v04.js"></script>
  <script type="text/javascript" src="cvs3DLib-07.js"></script>
  <script type="text/javascript" src="rgbaColor.js"></script>
  <script type="text/javascript" src="canvastext.js"></script>

Within the body of the web page, the canvas is inserted as a normal HTML element. The only canvas attribute required is a unique id. The width and height attributes may specify the size, or the CSS styling is used to set the canvas dimensions.

Typical HTML code is:

 <canvas id="canvasID" width="500" height="300"></canvas>

Each graphics context is an instance of the CvsGraphCtx3D object. It is created as follows:

 var g = new CvsGraphCtx3D("canvasID");

The returned object referenced by g, has the CvsGraphCtx3D drawing methods such as setWorldCoords3D, rect3D, polygon3D and so on.

Creating a 3D object

A 3D object it is made by first creating its component panels with the CvsGraphCtx3D methods such as; polyLine3D, svgPath3D, etc. which create objects of type Path3D, a reference to which is returned by these methods.

Multiple Path3Ds can be positioned to form a complex 3D shape using the Path3D methods rotate and translate.

Path3D objects can be rendered to the canvas without the overhead of creating a group by using the renderPath3D method. This is useful for providing a picture of components during development of a more complex shape. renderPath3D does the perspective projection of the object and draws it onto the 2D canvas.

These Path3Ds can then be grouped into a Group3D, with the CvsGraphCtx3D methods: groupPaths, groupClosedPaths.

Group3D objects have their own rotate and translate methods that can be very useful in constructing 3D shapes.

When the Group3D object is complete, it can be animated by movement in 6 dimensions using the methods: translateGroup3D and rotateGroup3D. These movements don't change the relative positions of the component Path3D objects that make up the group, so as the group is translated and rotated in 3D space the box will move as a single unit. The method renderGroup3D will project the group onto the 2D canvas with perspective as would be seen from the current viewpoint, and with shading produced by the current light source location. The object can also be rendered just as a wireframe with the method wireframeGroup3D.

Example

JavaScript graphics drawing code using the CvsGraphCtx3D will look something like this:

function drawDemo(cvsID) // canvas ID is passed in as a string
{
  var g = new CvsGraphCtx3D(cvsID);  // create a graphics context
  g.clearCanvas("#eeeea0");          // fill canvas to lightYellow
  g.setWorldCoords3D(-200, -200, 800);  // x axis 800 units long

  // create a blue circle centered on 0,0 with radius 100
  var circle = g.arc3D(0, 0, 100, 0, 360, true, "cyan");
  // write text centered on the origin 100 units high
  var txt = g.text3D("Ab", 0, 0, 5, 100, "white");

  var grp = g.groupPaths(circle, txt);  // group 2 paths
  g.renderGroup3D(grp);      // draw group

  g.rotateGroup3D(grp, 20, -30, 0);
  g.translateGroup3D(grp, 300, 200, 100);
  g.renderGroup3D(grp);      // draw after moving
}

Building a cube - face by face

Six square Path3D objects can be individually rotated and translated to form a cube. Passing these six Path3Ds to the groupPaths method returns a reference to a Group3D object representing the cube.

The source code for constructing the 3D cube is listed below:

function drawCube(cvsID)
{
  var g = new CvsGraphCtx3D(cvsID);  // create a graphics context

  g.clearCanvas("lightyellow");
  g.setWorldCoords3D(-150, -50, 300);
  g.setLightSource(0, 100, -200);

  var faces = [6];
  // left side
  faces[0] = g.rect3D(0, 0, 100, 100, "sienna");
  faces[0].rotate(0, 90, 0);        // flip right edge out of screen
  faces[0].translate(0, 0, 0);      // sienna face now left side
//      g.renderPath3D(faces[0]);
  // top
  faces[1] = g.rect3D(0, 0, 100, 100, "yellow");
  faces[1].rotate(90, 0, 0);        // flip top into of screen
  faces[1].translate(0, 100, -100); // move up and forward, yellow face now top
  // bottom
  faces[2] = g.rect3D(0, 0, 100, 100, "blue");
  faces[2].rotate(-90, 0, 0);       // flip top edge out of screen
  faces[2].translate(0, 0, 0);      // blue face now bottom
  // right side
  faces[3] = g.rect3D(0, 0, 100, 100, "red");
  faces[3].rotate(0, -90, 0);       // flip right edge into of screen
  faces[3].translate(100, 0, -100); // move right and out, red face now right side
  // back
  faces[4] = g.rect3D(0, 0, 100, 100, "gray");
  faces[4].rotate(180, 0, 0);       // flip backward down into screen
  faces[4].translate(0, 100, 0);    // move up so gray face now back
  // front
  faces[5] = g.rect3D(0, 0, 100, 100, "green");
  faces[5].translate(0, 0, -100);   // move out, green face now front

  // Make a group3D
  var s = g.groupClosedPaths(faces);
  s.translate(0, 0, 100);          // move back behind xy plane (canvas)

  g.rotateGroup3D(s, -40, 30, 0);
  g.translateGroup3D(s, 0, 0, 100);
  g.renderGroup3D(s);
}

Building a cube - folding net method

Group3Ds can also be progressively assembled adding one Path3D at a time to an already existing group using the Group3D method addPath3D. This is used in conjunction with the Group3D methods translate and rotate which, like the Path3D equivalents, move the group relative to its current position, they are not suitable for animation but should be used to construct complex 3D shapes. Using these methods the group is moved, then another path added then this bigger group is moved, and so on.

The 'net' of a polyhedron is useful determining the correct edges to join.

Note that the six panels of the cube are all copies of the original square Path3D. The code can thus be simplified by creating the initial Path3D and making 5 copies using the path.dup method.

Here is an alternate way to create the same cube as above, albeit with sides all the same color.

function drawCube2(cvsID)
{
  var g = new CvsGraphCtx3D(cvsID);  // create a graphics context

  g.clearCanvas("lightyellow");
  g.setWorldCoords3D(-150, -50, 300);
  g.setLightSource(0, 100, -200);

  var faces = [6];

  faces[0] = g.rect3D(0, 0, 100, 100, "yellow");  // top
  for (var i=1; i<6; i++)
    faces[i] = faces[0].dup();

  // start group with face[0] as the top
  var s = g.groupClosedPaths(faces[0]);

  s.rotate(90, 0, 0);         // flip it over to the x-z plane
  s.translate(0, 100, 0);     // move it up ready for the first side
  s.addBackcullPath3D(faces[1]); // add first side
  s.rotate(90, 0, 0);         // flip over to x-z plane
  s.translate(0, 100, 0);     // move it up ready for the bottom
  s.addBackcullPath3D(faces[2]);
  s.translate(0, -100, 0);    // move back down ready for other sides
  s.rotate(-90, 0, 0);        // rotate to so bottom faces down

  for (i=3; i<6; i++)
  {
    s.translate(-100, 0, 0);    // move so rotate edge is on y axis
    s.rotate(0, 90, 0);
    s.addBackcullPath3D(faces[i]);
  }

  var txt = g.text3D("Front", 50, 50, 5, 20, "white");
  txt.translate(0, 0, -1);       // make sure its in front of cube face
  s.addPath3D(txt);

  // construction complete, move around as a single unit
  g.resetTransformGroup3D(s);
  g.rotateGroup3D(s, -40, 30, 0);
  g.translateGroup3D(s, 0, 0, 100);
  g.renderGroup3D(s);
}

The results are shown below:

Create a 3D shape by rotation

A powerful cvsGraphCtx3D method is groupByRotation, which does most of the work of making the component Path3Ds forming an object that has symmetry about central axis, such as a glass, or a column or circular waste paper basket. This method requires a profile of the shape in SVG 'path' format. Rather than construct such a string, just grab a copy of Inkscape draw the profile, save the file, load it into an editor and copy the 'path' data element.

Zeppelin

Here is a 3D model of a Zeppelin airship, the body is created by a single call of ctx.groupByRotation and the fins are made from four identical Path3Ds. The sliders allow the model to be rotated about the x, y and z axes.

The source code for the Zeppelin is listed below:

function makeAirship(gc)
{
  var zeppelin = "M 192.0,95.673858 \
  C 199.47258,95.673858 211.55516,111.48419 216.74535,129.6622 \
  C 220.58528,143.11108 232.07509,187.60978 230.3964,290.96754 \
  C 229.86533,323.57764 232.55821,389.2994 216.18816,480.96796 \
  C 207.34749,530.47371 194.7365,554.23792 192.0,554.23792";
  var fin = "M 219.53128,461.18786 \
  C 225.28034,461.48046 230.86973,470.00339 231.55857,477.72044 \
  C 231.75698,479.94326 232.21513,527.55943 230.20521,528.88596 \
  C 229.01656,529.67045 203.65148,531.95048 203.65148,531.95048";

  var profilePath = gc.svgPath3D(zeppelin, 0, 0, 1)
  var airShip = gc.groupByRotation(profilePath, 2, 360, 12, "steelBlue");

  var fin0 = gc.svgPath3D(fin, -191.9, 95.7, 1, "red");
  fin0.rotate(0, 270, 0);
  airShip.addPath3D(fin0);
  var fin1 = gc.svgPath3D(fin, -191.9, 95.7, 1, "blue");
  fin1.rotate(0, 180, 0);
  airShip.addPath3D(fin1);
  var fin2 = gc.svgPath3D(fin, -191.9, 95.7, 1, "green");
  fin2.rotate(0, 90, 0);
  airShip.addPath3D(fin2);
  var fin3 = gc.svgPath3D(fin, -191.9, 95.7, 1, "yellow");
  airShip.addPath3D(fin3);

  //rotate the airship to the horizontal
  airShip.translate(0, 250, 0);
  airShip.rotate(0, 0, 90);

  // now push the whole shape back behind the projection plane
  airShip.translate(0, 0, 50);

  return airShip;
}