MISCELLANEOUS TECHNICAL ARTICLES BY Dr A R COLLINS

Canvas 3D Graphics

3D Graphics for HTML5 canvas

Cango3D is a graphics library for the HTML5 canvas element which simplifies the drawing and animation of 3D shapes on the 2D canvas. Cango3D uses the fact that straight lines and Bézier curves maintain their shape under 3D transformation. Restricting object outline definitions to these types means that smooth curves can be drawn in 3D with very few points needing to be transformed and projected onto the canvas.

Here are various examples of objects, drawn and animated by the Cango3D graphics library.

Cango3D

Cango3D provides methods for creating, rendering and animating 3D outline paths, filled shapes and stroke text on a 2D canvas. Complex 3D objects can be constructed by maneuvering shapes, acting as panels, forming the 3D object. 3D objects created by Cango3D can be saved to JSON format files for re-use.

The current version of Cango3D is 8v00, and the source code is available at Cango3D-8v00.js.

Features of the Cango3D

  • Conceptual simplicity - Cango3D draws three object types: Path3D, Shape3D and Text3D. All these inherit methods from the generic Obj3D object. They can be maneuvered to form multi-faceted 3D shapes by 'hard' transforms; translate, rotate or scale which modify the object definition. Obj3D can be grouped as children of Group3D objects to form more complex objects. Group3Ds can themselves be added as children of other Group3Ds. Group3D transforms are applied to the group's Obj3D children and recursively. The effect is that children inherit motion that has been applied to any Group3D ancestors higher up the family tree.

  • Curved or Straight outlines - Path3D and Shape3D are defined by its multi-segment outline path. Path segments can be straight lines or Bézier curves with 3D coordinates. Path3D are rendered as a simple outline, Shape3D are always filled with a color and Text3D are formed from the Hershey stroke font with each character formed as multi-segment path that can be moved about in 3D.

  • Movement using matrix transforms - All Obj3D and Group3D have a transform property which is an object with translate, revolve, rotate and scale methods to apply temporary transforms to the object coordinates as they are rendered. The transforms are reset after rendering, new transforms can then be applied at every frame of an animation without changing the underlying coordinates of the Obj3D.

  • World Coordinates - Cango3D uses the Right Handed Cartesian coordinate system. The canvas sits in the XY plane, the viewpoint is at some distance along the positive Z axis (out of the screen) X values increase to the RIGHT and Y axis values increase UP the screen. The field of view is user defined. The world coordinates are also used defined, mapping of 3D world coordinates to 2D canvas pixels is handled by the Cango3D.

  • Shading - When a Shape3D are rendered their fill color is shaded according to the current position direction of the user defined light source.

  • Drag and Drop - Any Obj3D or Group3D can be simply enabled for drag-n-drop, just specify the callback functions, all the event handling support code is built-in.

Using Cango3D

Firstly, download the Cango3D JavaScript source file: Cango3D-8v00.js or the minified version Cango3D-8v00-min.js. This can be placed in the same directory as the web page HTML file. Add the following line to the web page header:

  <script type="text/javascript" src="Cango3D-8v00.js"></script>

This file exposes the global objects: Cango3D, Path3D, Shape3D, Text3D and Group3D along with the utility methods svgToCgo3D, calcNormal, calcIncAngle and the 3D shape generator, shapeDefs3D.

Within the body of the web page, there must be a canvas element, it must have a unique id.

Typical HTML code is:

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

An instance of a Cango3D graphics context is created as follows:

 var g = new Cango3D(canvasID);

The returned object referenced by g, has the Cango3D methods such as g.setWorldCoords3D, g.clearCanvas, g.setFOV and so on.

Creating an object with Cango3D

A 3D object it is made by first creating its component panels by calling the object constructor either Path3D, Shape3D or Text3D. Each constructor requires a definition parameter, an outline path for the Path3D and Shape3D and a String for the Text3D. Various optional properties controlling the appearance of the object can be passed to the constructor.

Cango3D provides the global object shapeDefs3D to simplify defining common shapes. The shapeDefs3D object has methods to generate circle, ellipse, square, triangle, cross and ex. These methods take basic dimensions as a parameter and return an array of data in Cgo3D format defining the shape.

Yet another way to make objects is to load pre-defined objects that have been saved in Cango3D JSON format. These can be read from a file and converted back to a Group3D and all its children.

Example

Here is a very simple example, it creates a Shape3D representing a plate using a shapeDefs3D.circle as its outline path and a Path3D representing a curved stick using a Cgo3D data array holding a Quadratic Bézier curve definition. These are then added to a Group3D to move them as one entity, the Group3D's transform.rotate method is called to rotate the group. This rotation is re-applied and the Group3D re-rendered every 50 msec.

JavaScript source code for this example is shown below:

function drawDemo(cvsID)
{
  var g = new Cango3D(cvsID),
      stick = new Path3D(["M",0,0,0, "Q", 0,50,0, -15, 100, 0], {strokeColor:"sienna", lineWidth:3}),
      plate = new Shape3D(shapeDefs3D.circle(50), {fillColor:"yellow", backColor:"yellow"}),
      plateNstick = new Group3D(stick, plate),
      angle = 0;

  function turnPlate()
  {
    angle += 20;
    if (angle > 360)
      angle -= 360;

    plateNstick.transform.translate(0, -100);
    plateNstick.transform.rotate(0, 1, 0, angle);  // apply matrix to Group3D
    g.render(plateNstick);
  }

  plate.rotate(1, 0, 0, -75);  // flip to near horizontal
  plate.translate(0, 100, 0);  // move up to top of stick

  g.setPropertyDefault("backgroundColor", "aliceblue");
  g.setWorldCoords3D(-75, -120, 150);
  g.setLightSource(0, 500, 200);

  setInterval(turnPlate, 50)        // keep doing this forever
}

Drag and Drop

Drag and Drop capability can be enabled on all Obj3D and Group3D objects by defining the callback functions for mousedown, mousemove and mouseup events. References to these three callback functions are passed as parameters to the object's enableDrag method.

Once an object is enabled for drag-n-drop, the canvas 'mousedown' event handler will check if the mouse event occurred within the outline of the object. If it did, then the object's 'grab' event handler is called, this assigns the 'drag' and 'drop' handlers for subsequent 'mousemove' and 'mouseup' events and then calls the 'grab' callback function.

These callback functions are executed in the scope of a Drag3D object, so its properties are all available to the event handler. Drag-n-drop handlers often need to know the drawing origin and the cursor offset from the drawing origin when grabbed. Drag3D properties supply these values accessed as 'this.dwgOrg' and 'this.grabOfs'.

For every drag callback ie. every mousemove event, the current world coordinates of the cursor are always passed to the callback functions as an object with x,y,z properties. The cursor location is a point on the canvas which is always in the XY plane, therefore the z property of the cursor location will always be 0.

Turning 3D cube with drag-n-drop

Here is an example of drag-n-drop using a 3D cube object. The cube is constructed folding the individual panels forming its net. It can be rotated by clicking on it and dragging.

Group3D and Obj3D Drag precedence

Group3D children can all be drag-n-drop enabled in a single call to the group's enableDrag method. When dragged the whole group of objects will move. If a child has been drag enabled independently of the group then when dragged it will move by itself but when another child of the group is dragged it will move along with all the children of the Group3D.

As an example of group and object dragging, the canvas below shows three cubes that have been created and made the children of a Group3D. When either of the two multi-colored cubes are dragged, they will drag the group containing of all three cubes. The green cube has a different drag callback, it will just drag the green cube around. Click on any of the cubes and drag to see the difference.

The source code for this drag and drop example is shown below.

function groupDragDemo(scrnID)
{
  var g = new Cango3D(scrnID);
  var width = 20;
  var colors = ["red", "green", "blue", "yellow", "silver", "brown"];
  var cube1, cube2, cube3, grp;
  var grpPos = {x:30, y:-20, z:-20};
  var objPos = {x:15, y:-10, z:-10};

  function dragGroup(mousePos)
  {
    grpPos = {x: mousePos.x - this.grabOfs.x,
              y: mousePos.y - this.grabOfs.y,
              z: mousePos.z - this.grabOfs.z};

    cube1.transform.rotate(1, 1, 0, 30);
    cube1.transform.translate(objPos.x, objPos.y, objPos.z);
    cube2.transform.rotate(1, 1, 0, 30);
    cube2.transform.translate(-25, -10, -20);
    cube3.transform.rotate(1, 1, 0, -20);
    cube3.transform.translate(-5, 15, 0);

    grp.transform.translate(grpPos.x, grpPos.y, grpPos.z);
    g.render(grp);
  }

  function dragObj(mousePos)
  {
    objPos = {x: mousePos.x - this.grabOfs.x,
              y: mousePos.y - this.grabOfs.y,
              z: mousePos.z - this.grabOfs.z};

    cube1.transform.rotate(1, 1, 0, 30);
    cube1.transform.translate(objPos.x, objPos.y, objPos.z);
    cube2.transform.rotate(1, 1, 0, 30);
    cube2.transform.translate(-25, -10, -20);
    cube3.transform.rotate(1, 1, 0, -20);
    cube3.transform.translate(-5, 15, 0);

    grp.transform.translate(grpPos.x, grpPos.y, grpPos.z);
    g.render(grp);
  }

  g.setWorldCoords3D(-50, -50, 110);   // this sets viewpoint x,y at 0,0
  g.setFOV(40);
  g.setPropertyDefault("backgroundColor", "lightyellow");

  cube1 = buildCube(g, width, ["green", "pink", "green", "green", "green", "green"]);
  cube1.transform.rotate(1, 1, 0, 30);
  cube1.transform.translate(objPos.x, objPos.y, objPos.z);

  cube2 = buildCube(g, width, colors);
  cube2.transform.rotate(1, 1, 0, 30);
  cube2.transform.translate(-25, -10, -20);

  cube3 = buildCube(g, width, colors);
  cube3.transform.rotate(1, 1, 0, -20);
  cube3.transform.translate(-5, 15, 0);

  // create a group to parent the cubes for manoeuvring
  grp = new Group3D(cube1, cube2, cube3);
  // shift group down and back into screen from the drawing origin
  grp.transform.translate(grpPos.x, grpPos.y, grpPos.z);

  // enabled GREEN cube to be dragged independently
  cube1.enableDrag(null, dragObj, null);
  // enable grp to be dragged by RED cubes
  grp.enableDrag(null, dragGroup, null);

  g.render(grp);
}

Saving to Cango3D JSON format

To save the cube just created to a file for later use, Cango3D provides the Obj3DtoJSON method:

var g = new Cango3D(cvsID),  // create a graphics context
    width = 100,
    colors = ["red", "green", "blue", "yellow", "silver", "sienna"],
    cube, jsonStr,
...
cube = buildCube(g, width, colors);
jsonStr = g.Obj3DtoJSON(cube, "Colored Cube");
// jsonStr can now be saved to the local file system (if accessible) as a '.json' file.

The JSON format data returned from this call is shown below (line breaks have been added for clarity).

{"type":"Component","name":"Colored Cube","ComponentData":
{"type":"GROUP","children":[
{"type":"SHAPE","fillColor":"rgba(255, 255, 0, 1)","backHidden":true,"hardOfsTfm":{"matrix":[[1,0,0,0],[0,1,0,0],[0,0,1,0],[0,0,0,1]]},"lineWidth":1,"strokeCap":"butt","lorg":7,"pathData":["M",50,-50,-50,"L",-50,-50,-50,"L",-50,50,-50,"L",50,50,-50,"Z"],"centroid":[0,0,-50],"normal":[0,0,-51]},
{"type":"SHAPE","fillColor":"rgba(0, 0, 255, 1)","backHidden":true,"hardOfsTfm":{"matrix":[[1,0,0,0],[0,1,0,0],[0,0,1,0],[0,0,0,1]]},"lineWidth":1,"strokeCap":"butt","lorg":7,"pathData":["M",50,50,50,"L",50,-50,50,"L",50,-50,-50,"L",50,50,-50,"Z"],"centroid":[50,0,0],"normal":[51,0,0]},
{"type":"SHAPE","fillColor":"rgba(192, 192, 192, 1)","backHidden":true,"hardOfsTfm":{"matrix":[[1,0,0,0],[0,1,0,0],[0,0,1,0],[0,0,0,1]]},"lineWidth":1,"strokeCap":"butt","lorg":7,"pathData":["M",50,-50,-50,"L",50,-50,50,"L",-50,-50,50,"L",-50,-50,-50,"Z"],"centroid":[0,-50,0],"normal":[0,-51,0]},
{"type":"SHAPE","fillColor":"rgba(0, 128, 0, 1)","backHidden":true,"hardOfsTfm":{"matrix":[[1,0,0,0],[0,1,0,0],[0,0,1,0],[0,0,0,1]]},"lineWidth":1,"strokeCap":"butt","lorg":7,"pathData":["M",50,50,50,"L",50,50,-50,"L",-50,50,-50,"L",-50,50,50,"Z"],"centroid":[0,50,0],"normal":[0,51,0]},
{"type":"SHAPE","fillColor":"rgba(160, 82, 45, 1)","backHidden":true,"hardOfsTfm":{"matrix":[[1,0,0,0],[0,1,0,0],[0,0,1,0],[0,0,0,1]]},"lineWidth":1,"strokeCap":"butt","lorg":7,"pathData":["M",-50,-50,50,"L",-50,50,50,"L",-50,50,-50,"L",-50,-50,-50,"Z"],"centroid":[-50,0,0],"normal":[-51,0,0]},
{"type":"SHAPE","fillColor":"rgba(255, 0, 0, 1)","backHidden":true,"hardOfsTfm":{"matrix":[[1,0,0,0],[0,1,0,0],[0,0,1,0],[0,0,0,1]]},"lineWidth":1,"strokeCap":"butt","lorg":7,"pathData":["M",-50,-50,50,"L",50,-50,50,"L",50,50,50,"L",-50,50,50,"Z"],"centroid":[0,0,50],"normal":[0,0,51]}
]}}

Create a 3D object by profile rotation

A powerful Cango3D method is objectOfRevolution3D, which does most of the work of making the component Obj3Ds forming an object that has symmetry about central axis, such as a glass, or a column or circular waste paper basket. This method just requires a profile of the shape in Cgo3D format. The object is formed by rotating the profile in a number of steps about the Y axis, the number of steps is passed as a parameter. The segments of the profile are joined to adjacent rotated profile copies. The segments can be joined by circular arcs or by straight lines. More detail is given in the Cango3D User Guide, but here is an example to demonstrate its use.

Champagne Glass

A 3D model of a champagne glass created by a created by a single call of cgo.objectOfRevolution3D. The sliders allow the model to be rotated about the X, Y and Z axes.

Animated sculpture in 3D with draggable base

Here is an example of many of the features of Cango3D working together. The hexagonal display stand base is made by 'objectOfRevolution3D' with 6 segments joined by straight lines. Drag-n-drop has been enabled on the base to swivel the whole display so see it from any angle. The turntable on the display stand has also been made from 'objectOfRevolution3D' but with 36 segments joined by arcs. It is rotated using the 'PLAY', 'PAUSE', 'STOP' and 'STEP' controls. The sculpture is made in the style of Markus Raetz work's. It is made from a single Path3D which has been saved in JSON format.

The sculpture is animated using the Timeline utility controlled by the Play, Pause, Stop, Step control buttons.

The dark green base has been enabled for drag-n-drop so that the display stand can be tilted by clicking and dragging with the mouse. This allows a good view of the ingenuity of the Markus Raetz style sculpture.