MISCELLANEOUS TECHNICAL ARTICLES BY Dr A R COLLINS

Canvas Graphics Library

Cango graphics library for HTML5 canvas

Cango (CANvas Graphics Object) is a JavaScript library designed to assist drawing on the HTML5 canvas element. The core Cango module provides a set of objects: Path, Shape, Img, Text and ClipMask that are easily styled and transformed. It also provides methods to set up a world coordinate system and render these objects to the canvas. Several extension modules are available to give enhanced capabilities for Animation, Text manipulation and so on. The Cango User Guide provides a detailed reference for the objects and methods of the core module.

The current version of Cango is 25, it is open source and the source code is available at Cango-25v00.js and the minified version at Cango-25v00-min.js.

New in version Version 25:

  • The renderInSeries property has been added to the Group object, if set true, the child objects of the Group will be rendered in the order of insertion, even if some are only available asynchronously such as Images still loading.

New in version Version 24:

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. These are the building blocks of all the drawing done by the Cango library.
  • Groups and inherited transforms - Once constructed objects can be grouped as children of Group objects. Groups can have more Groups as children, creating a family tree of objects. Group methods enable transforms to be applied to all members of the Group. Child objects inherit the transforms applied to parent Groups.
  • Pointer events - Both Groups and individual objects can be enabled for mouse click and drag-n-drop events. 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.

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 with parameters 'drawFn' to actually draw the frame and 'pathFn' to apply the transforms to the animated objects for each frame. Tweener and PathTweener objects are provided to interpolate parameters between key frames. 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.

PathSVG Utilities extension module

If the PathSVGUtils extension module provides the PathSVG object, which behaves somewhat like the Path2D canvas utility which takes a string in SVG path data format and makes it ready to be drawn on a canvas. PathSVG takes either a string or array of SVG path data and breaks it into an array of segments. These segment data are then available for further user manipulation. Methods are provided that can transform, join, reverse path direction and so on. PathSVG methods return a new PathSVG so chaining of methods is supported.

PathSVG also has static methods which generate predefined shape outlines; circle, arc, ellipse, square, rectangle, triangle, cross and ex.

Cango Axes extension module

If the CangoAxes extension module is loaded then Cango is augmented with methods drawAxes and drawBoxAxes for drawing various style axes with auto or manual tick spacing. 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. The drawHTMLtext method allows text to be styled with inline CSS and rendered to the canvas as an Img object. The drawMathText method allows text in LaTeX syntax to be formatted as a mathematical expression and then rendered to the canvas as an Img object. The drawVectorText method provides an alternate to the standard canvas text using the Hershey vector font to draw characters as Path objects. drawSciNotationText draws numbers in scientific notation ie. the mantissa is drawn with a user defined font size and the exponent is drawn in a smaller font as a superscript. drawTextOnPath draws a text string with the character positions following a path defined by an PathSVG array.

Cango Zoom and Pan extension module

If the CangoZoomPan-1v02.js extension module is loaded the initZoomPan function is provided which creates a small overlay canvas holding the zoom and pan controls these are configured to zoom or pan the objects drawn by specified Cango graphics contexts.

Getting Started

To use the Cango, download the minified version Cango-25v00-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-25v00-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);

Note: Cango can also draw on an off-screen canvas, just pass a reference to the canvas object rather than an ID.

Setup the world coordinate system

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.

cgo.gridboxPadding(12, 6, 6, 5); // (left, bottom, right, top)
cgo.fillGridbox("lightgreen");

Figure 2. Defining the gridbox

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 (left hand cartesian) 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 of SVG path definition commands, an PathSVG object or a Path2D object. Path objects are rendered as outlines, Shape objects are always filled with color and a ClipMask restricts subsequent drawing to the interior of its outline.

Img objects take an image file URL, an ImageSegment object, an HTML Image object or another canvas as their descriptor argument. If a URL is passed then the image is loaded into an HTML Image object. The image is asynchronously loaded (if not pre-loaded) and then rendered to the canvas. An ImageSegment displays a cropped version of an Image object.

Text objects are specified by a string. The options properties then define the font-family, font size and weight etc.

Simple drawing example

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 3 shows a graph of a sine wave of amplitude 50 over the range 0 to 2π.

Figure 3. Graph plotting 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 several stationary objects but for animations the render method should clear the canvas before rendering each frame, so the 'clear' argument should be true.

Animation Example with inherited transforms

CangoAnimation extension adds the animation method which defines how animated objects are drawn and transformed between frames. The animations feature movement inheritance. This is demonstrated in Fig 4 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 4. 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 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).translate(cx1, cy1);
      seg2Grp.rotate(seg2Rot).translate(cx2-cx1, cy2-cy1);
      seg3Grp.rotate(seg3Rot).translate(cx3-cx2, cy3-cy2);
    }
    
    armCtx = new Cango(cvsID);
    armCtx.setWorldCoordsSVG(0, -100, 1000);
    
    armCtx.animation(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(PathSVG.circle(6), {fillColor:'red'});
  c1.enableDrag(null, dragC1, null);

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

  const c3 = new Shape(PathSVG.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, Astronomical Clock, 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.