MISCELLANEOUS TECHNICAL ARTICLES BY Dr A R COLLINS

Cango Canvas Graphics Library

User friendly canvas graphics

Cango (Canvas Graphics Object) is a JavaScript graphics context for object oriented drawing on the HTML5 canvas element. In addition to basic drawing capability it uses the canvas matrix transforms to make animation easy. The Cango User Guide provides detailed reference using Cango. The current version of Cango is 12v05, and the source code is available at Cango-12v05.js and the minified version at Cango-12v05-min.js.

Cango provides the graphics engine for the Filter Design, Armstrong Pattern Guns, Screw Thread Drawing, Spectrum Analyser and Zoom FFT pages. Its animation capabilities are demonstrated in the Gear Drawing, Flintlock and Wheellock pages.

The Cango library continues to be refined and enhanced, so users should not assume backward compatibility when a new version is released. Version 12 is a major re-write of Version 11 maintaining the API but using browser support for svgMatrix to replace the JavaScript matrix transform code used in earlier versions.

Cango basic drawing capability

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

Animation example demonstrating inherited movement

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 Figure 1 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 adds its own movement.

Figure 1. Example of animation demonstrating child Group2Ds inheriting the movement of parent Group2Ds.

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.
  • Both Cartesian and SVG coordinate systems support - Cango supports both RHC (Right Handed Cartesian) coordinates which have Y values increasing UP the canvas, and SVG (Scalar Vector Graphics) 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 and 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 can be simple predefined shapes such as circles, triangles and squares etc. or any complex shapes specified in the SVG path syntax. Images can be specified either by a pre-loaded HTML Image object or the URL of an image file.
  • Compact SVG path notation - Cango uses the compact SVG path syntax to define all Path, Shape and ClipMask outlines. The commands and their coordinates can be specified in a string or in an array.
  • Predefined shapes - Cango provides a shapeDefs object which has methods circle, ellipse, square, rectangle, triangle, cross and ex. These functions take object dimensions as arguments and return a Cgo2D data array 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.
  • Animation - Animations can be created by designating three functions, an initialization function to set the initial state of the objects in the scene. A draw function that actually renders the scene to the canvas and a path function which applies the various transforms such as translate, rotate etc. to the objects for each frame.

    All animations, regardless of which layer they are on are controlled by play, pause, stop and step methods on a single master timeline ensuring animations are synchronized.

  • 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 colored borders.

  • 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 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. drawArrow and drawArrowArc methods are also included. This module also adds the drawHTMLtext method which creates DOM text objects that can be positioned in Cango world coordinates. Allowing text processing utilities such as MathJax to be integrated with canvas drawing. The drawVectorText method provides an alternate to the standard canvas text where characters are converted to Path objects. CangoAxes also holds some utility functions for number formatting and a JavaScript version of the sprintf utility.

Getting Started

To use the Cango, download the minified version Cango-12v05-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 type="text/javascript" src="[directory path]/Cango-12v05-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(cvsID);

Cango defines a gridbox to provide a reference point for the world coordinate origin and width and height as a reference for the X and Y scale factors. By default the gridbox covers the full canvas. 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 also the width of the gridbox in world coordinates. Optionally the height of the gridbox can also be specified to set up different X and Y scaling. If the height parameter is omitted then Y axis scaling is the same as the X axis scaling.

Cango provides control of the gridbox dimensions by defining 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 around the gridbox gives room for scales and annotation of the graph axes.

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 the world coordinates where the object will be drawn, its color, line width, borders, drop shadows etc.

Path and Shape objects can have their outline path defined by either a string, holding SVG path definition commands or a array holding Cgo2D path definition commands. The path definition commands syntax is same as the SVG 'path' data syntax. Cgo2D uses the same syntax but its coordinates are interpreted as Right Hand Cartesian rather than the left handed SVG convention. Path objects are rendered as outlines and Shape objects are always filled with color. Many standard shapes have been predefined, they are accessed by the global 'shapeDefs' object which has methods 'circle', 'ellipse', 'square' etc.

Img objects take an image file URL or an HTML Image object 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, scaling, borders etc to be applied when the loading is complete and it is 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 he Cgo2D syntax allows the path to be specified as an array of x,y number pairs. The world coordinates are first set to suit the range of x values and y values expected in the data. As an example let's plot a sine wave of amplitude 50 over the range 0 to 2π.

Figure 1. Graph plot using 'drawPath'.

function plotSine(cvsID)
{
  var g = new Cango(cvsID),
      i, data = [];

  g.gridboxPadding(10);
  g.setWorldCoordsRHC(0, -50, 2*Math.PI, 100);
  // draw axes
  g.drawAxes(0, 6.5, -50, 50, {
    xOrigin:0, yOrigin:0,
    fontSize:10,
    strokeColor:'gray'});

  for (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 and simple shapes need modification or if we are going reuse objects, perhaps to animate them, the single use draw methods are not well suited. Cango allows Path, Shape, Img or Text objects to be are created and then have permanent or temporary transforms applied or their properties changed and then be 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 Path(imgDef, options);
var txtobj = new Text(strg, options);
var mask = new ClipMask(outlineDef);

Construction (hard) transforms

Once an object is instantiated it can be further modified by scaling, rotating or moving the object relative to its drawing origin. All objects have the following methods to perform these manipulations.

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

Transforms applied by these methods are 'hard' changes, permanently changing the definition of the object. Hard transforms applied to Groups are immediately applied to Group children recursively.

Dynamic (soft) transforms

All objects (and Groups) have a transform property which is a JavaScript object holding a transform matrix and a set of methods that apply transforms to the matrix which represent translation, rotation etc. The transform matrix is built up by successive calls to these obj.transform 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 or Group is rendered. The transforms applied to a Group are inherited by its children. These soft transform methods are:

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

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 '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 architecture

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. An 'options' object holding user defined properties may also be passed. The 'options' object is passed as a parameter to the three functions when they are called. The animation method creates an AnimObj which holds as properties references to these functions, the Cango context and the user defined options. This object is added to the array of animations to be run on the canvas or canvas stack if additional layers have been created. The 'initFn', 'drawFn' and 'pathFn' are called in the scope of this AnimObj.

The animation's 'initFn' is called as soon as the animation is created to setup initial conditions and apply initial transforms to the animated objects. This is followed by the drawFn to paint the objects in their starting positions. When 'playAnimation' is called the 'requestAnimationFrame' utility is used to step along the timeline calling the pathFn to calculate new state properties and apply them to the objects followed by the drawFn to render the objects to the canvas for each frame.

Timeline

All animations on a canvas are controlled by a master timeline. The Cango Timeline is created by the first Cango instantiated on the canvas. All subsequent Cango instances made on the background canvas or on any layer created over the background use the same timeline. The animation timeline methods are:
cgo.playAnimation, cgo.pauseAnimation, cgo.stepAnimation, cgo.stopAnimation.

These methods may be called on any Cango context from the background canvas or any layer, they all refer to the same methods.

Tweener utility

To simplify the generation of the state values (animated object's position, rotation etc.) at each frame along the timeline, Cango provides a Tweener object to interpolate between key frame values. A Tweener object holds the start time for the movement, the total duration and a 'loop' parameter which determines how many times the motion should be repeated. The Tweener object has just one method: getVal which interpolates between key frame values for any property. Key frame times are expressed as a percentage of the Timeline's duration. The Tweener's getVal method can be called multiple time for a single frame if there are several state properties to be interpolated, each with its own array of key frame values.

Animation 'currState' and 'nextState' properties

For those pathFns that need to refer to the previous frame's state values to calculate values for the next frame, The AnimObj provides the currState and nextState objects as properties so they are in the scope for the initFn, drawFn and pathFn. All the newly calculated values that the pathFn creates for each frame can be saved in this.nextState object. After the 'drawFn' renders each frame to the canvas the 'currState' and 'nextState' objects are swapped so that the 'as rendered' properties are available to the pathFn next time it is called in 'this.currState'. The currState object should be treated as read-only, the pathFn reads it, make calculations and writes the new values to be used into 'nextState' then applies the transforms to the objects to be rendered.

Animation Examples

Animation example using Tweener interpolation

Shown below is a beach ball is drawn from three objects, a white circle, a red shape drawn in Inkscape using Bezier curve, and a semi-transparent circular overlay providing the 3D shading effect. The ball's timeline is set to loop continuously. The animation can be run, stopped, paused or stepped.


Here is the code snippet that sets up the rolling beachball animation shown above.

var ballGC;           // Cango graphics context

function beachBallDemo(cvsID)
{
  var circle = "M 534.29,274.5 A 165.7,165.7 0 1 1 202.9,274.5 165.7,165.7 0 1 1 534.3,274.5 z",
      stripes = "M 279.1,134.7 C 260,193.8 318.1,303.8 349.5,343.8 \
      338.3,349.3 330.3,357.9 331.1,372.0 \
      282.5,369.2 232.8,353.1 207.3,313.1 220.9,367.5 258.7,410.6 312,430.4 \
      331.1,419.5 336.5,412.9 349.8,395.6 361.7,401.3 377.6,400.1 388.6,394.2 \
      404.8,411.9 414.6,420.0 433.6,427 482.7,405.2 518.4,364.5 530.7,309.3 \
      508.9,343.1 462.8,363.7 404.7,369.4 404.1,356.7 396.1,348.5 384.8,343.2 \
      426.2,276 460.9,182 451.8,130.9 393.8,98.6 330.4,103 279.1,134.7 z",
      xRef = 368.57,  // this is the reference point expressed in Inkscape coords
      yRef = 274.5,
      dia = 2*165.7,
      ballData, ball,
      panelsData, panels,
      shade, shading, shadows,
      beachball, shadedBall,
      twnr,
      ballConfig = {x:[1100, 150, 1100],
                    y:[300, 200, 300],
                    scl:[0.6, 1, 0.6],
                    rot:[0, 360, 0],
                    delay:0,
                    duration:3500,
                    loop:'loop' };

  twnr = new Tweener(ballConfig.delay, ballConfig.duration, ballConfig.loop);

  function initBall(opts)
  {
    beachball.transform.translate(1100, 300);
    beachball.transform.scale(0.6);
    shading.transform.translate(1100, 300);
    shading.transform.scale(0.6);
  }

  function ballPathFn(time, opts)
  {
    var sclVal = twnr.getVal(time, opts.scl),
        xVal = twnr.getVal(time, opts.x),
        yVal = twnr.getVal(time, opts.y),
        rotVal = twnr.getVal(time, opts.rot);

    beachball.transform.scale(sclVal);
    beachball.transform.rotate(rotVal);
    beachball.transform.translate(xVal, yVal);
    shading.transform.scale(sclVal);
    // no rotation for the shadow
    shading.transform.translate(xVal, yVal);
  }

  function drawBall()
  {
    this.gc.render(shadedBall);
  }

  ballGC = new Cango(cvsID);
  ballGC.setWorldCoordsRHC(0, 0, 1200); // square pixels

  ballData = svgToCgo2D(circle, -xRef, -yRef);
  ball = new Shape(ballData, {fillColor:"ghostwhite"});

  panelsData = svgToCgo2D(stripes, -xRef, -yRef);
  panels = new Shape(panelsData, {fillColor:"red"});

  shadows = sphereShading(dia),
  shade = new Shape(shapeDefs.circle(dia), {fillColor:shadows.shadow});
  ballHilite = new Shape(shapeDefs.circle(dia), {fillColor:shadows.hilite});

  beachball = new Group(ball, panels);
  shading = new Group(shade, ballHilite);
  shadedBall = new Group(beachball, shading);   // draw shading after ball

  ballGC.animation(initBall, drawBall, ballPathFn, ballConfig);
}

Animation example using currState/nextState

In the animation of a bouncing ball shown below, the ball's position and velocity at each frame depends on its position and velocity at the last frame and the time since the last frame was drawn. Newton's laws of motion will dictate that the ball should continue to move according to its velocity vector, added to this is the acceleration due to gravity and nay collisions with the boundary walls. Saving the state vector in 'nextState' object means that it will be available as 'currState' when the pathFn is called for the next frame. The time a frame is drawn is always maintained in currState.time and the time at which the pathFn is called is passed as a parameter to the pathFn.


Here is the source code for the animation shown above.

var bounceGC;           // Cango graphics context

function bouncingBallDemo(cvsID)
{
  var shadows,
      ball, shade, ballHilite,
      ballGrp,
      dia = 45,
      startX = 120,
      startY = 250;

  function initBall(opts)
  {
    // create any properties you want in nextState, currState will be a clone
    // don't write to currState its just there for reference to what is on the screen
    this.nextState.x = startX;
    this.nextState.y = startY;
    this.nextState.vx = 0;
    this.nextState.vy = 0;

    ballGrp.transform.translate(this.nextState.x, this.nextState.y);
  }

  function drawBall(opts)
  {
    this.gc.render(ballGrp);
  }

  function bouncingPath(time, opts)    // time passed is the time since start of animation
  {
    // 'this' refers to the Animation object (this.gc, this.nextState etc)
    // this.currState is available for reference to what is on the screen (don't write to it)
    // after the frame is drawn in nextState, nextState and currState are swapped
    var reflect = -1,     // bounce off wall else disappear
        coeff = 0.82,     // percentage bounce height
        friction = 0.985, // rolling friction loss/msec
        speed = 1.5,      // units are like worldCoords x axis units/mm
        gravity = -0.0098 * speed, // gravity =9.8m/s/s =0.0098mm/ms/ms =0.0098*speed units/ms/ms
        xVel = 0,
        yVel = 0,
        x, y,
        vel, startAngle, timeInt, s, u;

    if (time == 0)   // generate random launch angle for each reset
    {
      // restart at 0.4 m/sec initial velocity and at a random angle
      // velocity in 2m/s = 2 mm/ms = 2mm/ms * units/mm = 2*speed
      vel = 2*speed;    // x units/ms
      startAngle = 30+120*Math.random();
      this.nextState.x = startX;           // put ball back at the start point
      this.nextState.y = startY;
      this.nextState.vx = vel * Math.cos(startAngle * Math.PI / 180);
      this.nextState.vy = vel * Math.sin(startAngle * Math.PI / 180);

      ballGrp.transform.translate(this.nextState.x, this.nextState.y);
      return;     // this is the state to get drawn at start
    }
    // calculate the new position and velocity
    timeInt = time - this.currState.time;   // time since last draw
    // v = u + at
    yVel = this.currState.vy + gravity * timeInt;    // accelerating due to gravity
    xVel = this.currState.vx;                    // constant
    x = this.currState.x + xVel*timeInt;
    y = this.currState.y + yVel*timeInt + 0.5*gravity * timeInt * timeInt;
      // now check for hitting the walls
    if (x > opts.rightWall - opts.radius)
    {
      x = opts.rightWall - opts.radius;
      xVel *= reflect*coeff;    // lossy reflection next step
    }
    if (x < opts.leftWall + opts.radius)
    {
      x = opts.leftWall + opts.radius;
      xVel *= reflect*coeff;    // lossy reflection next step
    }
    if (y > opts.topWall - opts.radius)
    {
      y = opts.topWall - opts.radius;
      yVel *= reflect*coeff;    // lossy reflection next step
    }
    if (y < opts.bottomWall + opts.radius)  // this is always true after yVel become small
    {
      y = opts.bottomWall + opts.radius;
      // calc velocity at the floor   (v^2 = u^2 + 2*g*s)
      s = this.currState.y - (opts.bottomWall + opts.radius);     // pre bounce
      u = this.currState.vy;
      yVel = -Math.sqrt(u*u - 2*gravity*s);
      yVel *= reflect*coeff;  // lossy reflection next step
      // after bouncing phase this is rolling friction
      xVel *= friction;
    }
    this.nextState.x = x;
    this.nextState.y = y;
    this.nextState.vx = xVel;
    this.nextState.vy = yVel;

    ballGrp.transform.translate(this.nextState.x, this.nextState.y); // ball hasn't got this yet
  }

  shadows = sphereShading(dia, 255, 0, 0);

  ball = new Shape(shapeDefs.circle(dia), {fillColor:shadows.base} );
  shade = new Shape(shapeDefs.circle(dia), {fillColor:shadows.shadow});
  ballHilite = new Shape(shapeDefs.circle(dia), {fillColor:shadows.hilite});
  ballGrp = new Group(ball, shade, ballHilite);

  bounceGC = new Cango(cvsID);
  bounceGC.setWorldCoordsRHC();      // use raw pixels

  bounceGC.animation(initBall, drawBall, bouncingPath, {radius:dia/2,
                                                        leftWall:0,
                                                        rightWall:bounceGC.rawWidth,
                                                        topWall:bounceGC.rawHeight,
                                                        bottomWall:0});
}

Drag and Drop

Drag-n-drop capability can be enabled and disabled on any object or Group by obj.enableDrag and obj.disableDrag methods. The 'enableDrag' method takes as a parameters the callback functions that are to be run 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 4. Example of drag-n-drop, drag a red circle to edit the curve.

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

function editCurve(cvsID)
{
  var x1 = 40, y1 = 20,
      cx1 = 90, cy1 = 120,
      x2 = 120, y2 = 100,
      cx2 = 130, cy2 = 20,
      cx3 = 150, cy3 = 120,
      x3 = 180, y3 = 60,
      c1, c2, c3,
      g = new Cango(cvsID);

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

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

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

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

  g.clearCanvas("lightyellow");
  g.deleteAllLayers();
  g.setWorldCoordsRHC(0, 0, 200);

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

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

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

  drawCurve();
}

Drag and Drop using Group2Ds

If a Group has drag-n-drop enabled all children will be tested for grab and drag events and all descendent objects from this Group will be dragged as a single unit. If a child Group (or object) has drag-n-drop enabled on it then the child's drag-n-drop handler will take precedence over the parent group handle.

Figure 5 shows a simple example of drag-n-drop precedence. Three objects are all children of a Group which has drag-n-drop enabled to move the whole group around. The green objects don't have drag-n-drop enabled on them individually but the orange triangle does. Click and drag a green object will therefore move the whole group, but click and drag on the triangle will move it independently.

Figure 5. Example of drag-n-drop Group precedence.

The code for Group drag example in Fig. 5 is shown below.

function groupDrag(cvsID)
{
  var groupAll = new Group(),
      allX = 0, allY = 0,
      triX = 0, triY = 0,
      box = new Shape(shapeDefs.rectangle(40, 90), {fillColor: "green",border:true}),
      sqr = new Shape(shapeDefs.square(70), {fillColor: "green",border:true}),
      tri = new Shape(shapeDefs.triangle(70), {fillColor:"orange",border:true}),
      g = new Cango(cvsID);

  var drawIt = function()
  {
    groupAll.transform.translate(allX, allY);
    tri.transform.translate(triX, triY);

    g.render(groupAll, true);  // true = clear canvas
  };

  var dragRoot = function(mousePos)
  {
    // grabOfs is the distance the mouse is from the drawing origin of the target
    allX = mousePos.x - this.grabOfs.x;
    allY = mousePos.y - this.grabOfs.y;

    drawIt();
  };

  var dragObj = function(mousePos)
  {
    triX = mousePos.x - this.grabOfs.x;
    triY = mousePos.y - this.grabOfs.y;

    drawIt();
  };

  g.clearCanvas();
  g.setWorldCoordsRHC(-100, -100, 400);
  // arrange objects
  sqr.translate(70, 70);
  tri.translate(0, -50);       // drag-n-drop doesn't know about hard transforms !!!! FIX THIS
  groupAll.addObj(box, sqr, tri);

  tri.enableDrag(null, dragObj, null);
  groupAll.enableDrag(null, dragRoot, null);

  drawIt();
}

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, Spectrum Analyser, Zoom FFT, Gear Drawing, Flintlock and Wheellock pages will give numerous examples of Cango in use.