MISCELLANEOUS TECHNICAL ARTICLES BY Dr A R COLLINS

Cango Canvas Graphics Library

User friendly canvas graphics

Cango (CANvas Graphics Object) is a JavaScript graphics library designed to assist drawing on the HTML5 canvas element. In addition to basic drawing capability it uses the matrix transforms to make animation easy. The current version of Cango is 18, and the source code is available at Cango-19v07.js and the minified version at Cango-19v07-min.js. The Cango User Guide provides a detailed reference for the objects and methods.

Cango Version 19 add support for the new drawing method: drawTextOnPath, which allows text to follow an arbitrary path. This method along with drawHTMLText, drawVectorText and drawSciNotationText have been collected in a new extension module CangoTextUtils-1v03.js. The other Text utility methods were previously available in the CangoAxes extension module. The new version of the axes module without these Text methods is CangoAxes-8v00

Cango basic drawing capability

Figure 1. Examples of drawing using the Cango graphics library.

Features of Cango

  • User defined World Coordinates - Cango supports independent X and Y scaling. Data may to be plotted in its original units removing the need to scale all values to canvas pixels. Each Cango instance has its own world coordinates. Multiple graphics contexts may exist together on a single canvas.
  • Right Handed Cartesian (RHC) and SVG style (Left Handed Cartesian) coordinate systems support - Cango supports both RHC coordinates which have Y values increasing UP the canvas, and SVG coordinates which have Y values increase DOWN the canvas. Since each Cango context instance has its own world coordinates, the different coordinate systems may co-exist on a canvas.
  • Canvas Layers - The functionality of a CanvasStack is built into Cango. Transparent canvas overlays can be created to assist in drawing cursors, data overlays and animation without re-drawing any static objects on the background canvas or other layers.
  • Objects - Cango supports draw objects of type Path, Shape, Img, Text and ClipMask. Shape, Path and ClipMask outlines are specified using the SVG 'path' syntax. The commands and their coordinates can be specified as a string or an array. Img objects can be specified either by a pre-loaded HTML Image object or the URL of an image file, Text objects just expect a string.
  • Predefined shape outlines - Cango provides functions circle, arc, ellipse, ellipticalArc, square, rectangle, triangle, cross and ex. These functions take object dimensions as arguments and return an array in SVG path style data specifying the object's outline.
  • Groups and inherited transforms - Once constructed objects can be grouped as children of Group objects which enables transforms to be applied to the group as a whole. Children inherit the transforms applied to parent Groups. Groups can have more Groups as children, creating a family tree of objects.
  • Drag and Drop - Both Groups and individual objects can be enabled for drag-n-drop. All the event handling support code is built-in, applications just specify the callback functions.
  • Shadows and Borders - Drop shadow effects applied and Shape, Text and Img objects, they can also have borders of user defined width and color.

  • Zoom and Pan - Cango provides a Zoom and Pan utility which creates an overlay canvas holding the zoom and pan controls these configured to zoom or pan any of the objects in the canvas stack.

Cango Animation extension module

  • If the CangoAnimation extension module is loaded then Cango.animation method becomes available. Animations are created by calling the Cango 'animation' method passing reference to three functions, 'initFn', to initialize the animation, 'drawFn' to actually draw the scene and 'pathFn' to apply the transforms to the animated objects for each frame and optionally an 'options' object holding user defined properties. The 'options' object is passed as a parameter to the three functions when they are called. All animations, regardless of which layer they are on, are controlled by playAnimation, pauseAnimation, stopAnimation and stepAnimation methods on a single master timeline ensuring animations are synchronized.

Cango Axes extension module

  • If the CangoAxes extension module is loaded then Cango is augmented with methods for drawing various styles of axes with auto or manual tick spacing with the drawAxes and drawBoxAxes methods. drawArrow and drawArrowArc methods are also included. CangoAxes also holds some utility functions for number formatting and a JavaScript version of the sprintf utility.

Cango Text Utilities extension module

  • If the CangoTextUtils extension module is loaded then Cango is augmented with methods for drawing various styles of Text. drawHTMLtext method which creates DOM text objects rendered over the canvas that can be positioned in Cango world coordinates. This has the additional advantage of allowing text processing utilities such as MathJax or Katex to be integrated with canvas drawing. The drawVectorText method provides an alternate to the standard canvas text using the Hershey vector font to draw characters as Path objects. drawSciNotationText draw numbers in scientific notation ie. the mantissa at a user defined font size and the exponent in a smaller font draw as a superscript. drawTextOnPath draws a text string with the character positions following a user defined path.

Getting Started

To use the Cango, download the minified version Cango-19v07-min.js. Save the file in a directory accessible to JavaScript code running in the web page, then add the following line to the web page header:

<script src="[directory path/]Cango-19v07-min.js"></script>

Within the body of the web page insert a canvas element. The only attribute required by Cango is a unique id string.

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

Cango drawing starts by creating a new Cango context, the only parameter of the constructor is the ID of the canvas.

var cgo = new Cango(canvasID);

Cango uses a gridbox to provide a reference for the world coordinate origin and the X and Y scale factors. By default the gridbox covers the full canvas. The gridbox dimensions can be defined by setting a padding width from each edge of the canvas. Padding the gridbox is particularly useful when plotting graphs on the canvas element since the padding area gives room for scales and annotation of the graph axes. The world coordinate system is set up by calling either the setWorldCoordsRHC() method to set up a Right Hand Cartesian system, or the setWorldCoordsSVG() method to set up an SVG style system. Each method specifies the X,Y world coordinate value of the gridbox origin (lower left for RHC and upper left for SVG systems) and the width and height of the gridbox in world coordinates. If the height parameter is omitted then Y axis scaling is the same as the X axis scaling.

Cango can draw paths, shapes, images and text. The simplest way to do this is using any of the Cango methods:

cgo.drawPath(pathDef, options);
cgo.drawShape(outlineDef, options);
cgo.drawImg(imgDef, options);
cgo.drawText(strg, options);

Each of these methods creates an instance of the corresponding Path, Shape, Img or Text object and renders it to the canvas. The 'options' object properties are used to set where the object will be drawn, its color, line width, borders, drop shadows etc. and to alter the object relative to its drawing origin, to scale it or rotate or even skew it.

Path, Shape and clipMask objects can have their outline path defined by either a string or an array holding SVG path definition commands. Path objects are rendered as outlines, Shape objects are always filled with color and clipMasks restrict subsequent drawing to the interior of their shape.

Img objects take an image file URL, an HTML Image object or another canvas as their definition parameter. If a URL is passed then the image is loaded into an HTML Image object. The 'options' properties can specify size, rotation, borders etc. The image is asynchronously loaded (if not pre-loaded) and then rendered to the canvas.

Text objects are specified by a string. The options properties then define the font-family, font size and weight etc. Text is always drawn with its aspect ratio preserved, regardless of whether the coordinate system has different X and Y world coordinate scaling.

Graph plotting

A line graph can be very simply plotted using the Path object since the Cango allows the path to be specified as an array of x,y number pairs. This represents a concession to the strict SVG syntax which always requires an initial 'M' command before an array of coordinates are recognized as line segments. The world coordinates should first be set to suit the range of x values and y values expected in the data. As an example Fig 2 shows a graph of a sine wave of amplitude 50 over the range 0 to 2π.

Figure 2. Graph plot using 'drawPath'.

function plotSine(cvsID)
{
  const g = new Cango(cvsID);
  g.gridboxPadding(10);
  g.setWorldCoordsRHC(0, -50, 2*Math.PI, 100);
  g.drawAxes(0, 6.5, -50, 50, {
    xOrigin:0, yOrigin:0,
    fontSize:10,
    strokeColor:'gray'});

  const data = [];
  for (let i=0; i<=2*Math.PI; i+=0.03) {
    data.push(i, 50*Math.sin(i));
  }
  g.drawPath(data, {strokeColor:'red'});
}

Object reuse

Once a canvas drawing gets more complex with shapes needing modification or if we are going to animate them, the single use draw methods are not well suited. Cango allows Path, Shape, Img, Text and ClipMask objects to be are created and then have transforms applied or their properties changed before they are rendered to the canvas with the Cango render method. Objects are created independent of any Cango instance that may be used to render them.

Any object can be created by calling its constructor.

var pathobj = new Path(pathDef, options);
var shapeobj = new Shape(outlineDef, options);
var imgobj = new Img(imgDef, options);
var txtobj = new Text(strg, options);
var mask = new ClipMask(outlineDef, options);

Dynamic transforms

All objects (and Groups) have a transform matrix property and methods that apply translation, rotation, scale or skew transforms to their transform matrix. The transform matrix is built up by successive calls to these methods. The resulting matrix is applied when the objects are rendered. These transforms do not affect the object definition, the transform matrix is reset to the identity matrix after the object is rendered. The transforms applied to a Group are inherited by its children. Transforms on an object are applied in order of insertion and before transforms inherited from a parent Group. These transform methods are:

obj.translate(xOfs, yOfs);
obj.scale(xScl, yScl);
obj.rotate(degs);
obj.skew(degH, degV);

Rendering to the canvas

Once an object is created it can be drawn onto the canvas by the 'render' method of a Cango instance.

cgo.render(obj, clear);

The render method takes a single object or Group as its first parameter, the optional 'clear' parameter is evaluated as a Boolean, if true the canvas is cleared prior to rendering the object or group. If undefined or false the object is rendered onto the canvas leaving any existing drawing intact. So 'clear' is usually omitted when drawing a static mix of objects but for animations the render method usually clears the canvas before rendering each frame, so 'clear' is set true.

Animation Example with inherited transforms

Cango animation features movement inheritance which enables transforms applied to a Group to be inherited by its children and which can then add their own movement transforms the net movement then inherited by their children and so on. This is demonstrated in Fig 3 which shows a drawing of an excavator, its arm has several jointed segments. Each segment inherits the movement applied to the previous segments and then has its own movements added.

Figure 3. Example of animation demonstrating child Groups inheriting the movement of parent Groups.


Here is the code snippet that runs the animation shown above.

    ...

    // make groups to enable inherited movement
    const seg1Grp = new Group(seg1);
    const armGrp = new Group(cabin, seg1Grp, tread, hiLites, axle1);

    const seg2Grp = new Group(seg2, axle2);
    seg1Grp.addObj(seg2Grp);

    const seg3Grp = new Group(seg3, axle3);
    seg2Grp.addObj(seg3Grp);

    // now set up animation
    const animData = {s1: [0, 40, 30, 0, -15, 0],
                      s2: [0, -90, -5, 20, 30, 0],
                      s3: [-60, 0, 15, 70, 90, -60]};

    const armTwnr = new Tweener(0, 5000, 'loop');
    
    function initArm(opts)
    {
      seg1Grp.translate(cx1, cy1);
      seg2Grp.translate(cx2-cx1, cy2-cy1);
      seg3Grp.translate(cx3-cx2, cy3-cy2);
    }
    
    function drawArm(opts)
    {
      this.gc.render(armGrp);
    }
    
    function armPathFn(time, opts)
    {
      const seg1Rot = armTwnr.getVal(time, opts.s1),
            seg2Rot = armTwnr.getVal(time, opts.s2),
            seg3Rot = armTwnr.getVal(time, opts.s3);
    
      seg1Grp.rotate(seg1Rot);
      seg1Grp.translate(cx1, cy1);
      seg2Grp.rotate(seg2Rot);
      seg2Grp.translate(cx2-cx1, cy2-cy1);
      seg3Grp.rotate(seg3Rot);
      seg3Grp.translate(cx3-cx2, cy3-cy2);
    }
    
    armCtx = new Cango(cvsID);
    armCtx.setWorldCoordsSVG(0, -100, 1000);
    
    armCtx.animation(initArm, drawArm, armPathFn, animData);
    

Drag and Drop

Drag-n-drop capability can be enabled on any object or Group by the obj.enableDrag method which takes as a parameters the callback functions to be called when mousedown, mousemove and mouseup events occur. The callbacks are passed the current cursor coordinates when called. They are executed in the scope of a Drag2D object which, for convenience, has various properties such as grabCsrPos, dwgOrg, dwgOrgOfs, grabOfs and so on, to assist in simple coding of event handlers. Enabling drag-n-drop on a Group recursively enables the drag-n-drop on all the group's children.

Here is a simple example, which shows two Bezier curves with draggable control points.

Figure 5. Example of drag-n-drop, drag a red circle to edit the curve.

The code for the curve editor in Fig. 5 is shown below.

function editCurve(cvsID)
{
  const x1 = 40,  y1 = 20,
        x2 = 120, y2 = 100,
        x3 = 180, y3 = 60;
  let cx1 = 90,  cy1 = 120,
      cx2 = 130, cy2 = 20,
      cx3 = 150, cy3 = 120;

  function dragC1(mousePos)    // called in scope of dragNdrop obj
  {
    cx1 = mousePos.x - this.grabOfs.x;
    cy1 = mousePos.y - this.grabOfs.y;
    drawCurve();
  }

  function dragC2(mousePos)
  {
    cx2 = mousePos.x - this.grabOfs.x;
    cy2 = mousePos.y - this.grabOfs.y;
    drawCurve();
  }

  function dragC3(mousePos)
  {
    cx3 = mousePos.x - this.grabOfs.x;
    cy3 = mousePos.y - this.grabOfs.y;
    drawCurve();
  }

  function drawCurve()
  {
    // curves change shape so it must be re-constructed each time
    const qbez = new Path(['M', x1, y1, 'Q', cx1, cy1, x2, y2], {
      strokeColor:'blue'});
    const cbez = new Path(['M', x2, y2, 'C', cx2, cy2, cx3, cy3, x3, y3], {
      strokeColor:'green'});
    // show lines to control point
    const L1 = new Path(['M', x1, y1, 'L', cx1, cy1, x2, y2], {
      strokeColor:"rgba(0, 0, 0, 0.4)",
      dashed:[4]});  
    const L2 = new Path(['M', x2, y2, 'L', cx2, cy2], {
      strokeColor:"rgba(0, 0, 0, 0.4)",
      dashed:[4]});
    const L3 = new Path(['M', x3, y3, 'L', cx3, cy3], {
      strokeColor:"rgba(0, 0, 0, 0.4)",
      dashed:[4]});
    // draw draggable control points
    c1.translate(cx1, cy1);
    c2.translate(cx2, cy2);
    c3.translate(cx3, cy3);
    const grp = new Group(qbez, cbez, L1, L2, L3, c1, c2, c3);
    g.render(grp, true);
  }

  const g = new Cango(cvsID);
  g.clearCanvas("lightyellow");
  g.deleteAllLayers();
  g.setWorldCoordsRHC(0, 0, 200);

  // draggable control points
  const c1 = new Shape(circle(6), {fillColor:'red'});
  c1.enableDrag(null, dragC1, null);

  const c2 = new Shape(circle(6), {fillColor:'red'});
  c2.enableDrag(null, dragC2, null);

  const c3 = new Shape(circle(6), {fillColor:'red'});
  c3.enableDrag(null, dragC3, null);

  drawCurve();
}

Further details

The Cango User Guide the provides detailed reference for all the Cango methods and utilities. Examining the source code of the Filter Design, Armstrong Pattern, Screw Thread Drawing, Sonar Ray Tracing, Spectrum Analyser, Zoom FFT, Gear Drawing, Flintlock and Wheellock pages will give numerous examples of Cango in use.