MISCELLANEOUS TECHNICAL ARTICLES BY Dr A R COLLINS

Cango Animation User Guide

Cango animation extension

The Cango Animation extension module provides the Cango graphics library with additional methods to simplify animated drawing on the canvas element. These additional Cango methods are:

animation
deleteAnimation
deleteAllAnimations
playAnimation
pauseAnimation
stepAnimation
stopAnimation
redrawAnimation
setAnimationPropertyDefault

Also included in the module are additional global objects to assist animation:

Tweener
PathTweener
BoneArray
WideBoneArray

The source code can be downloaded from CangoAnimation-11v07.js or the minified version at CangoAnimation-11v07-min.js.

New in Version 11

The BoneArray and WideBoneArrayobjects have been added. This objects are designed to assist animation of segmented objects where a segment inherits any rotation and translation applied to preceding segments.

New in Version 10

The setAnimationPropertyDefault has been added. The functionality of the method was previously handled by the generic Cango setPropertyDefault method.

Getting Started

The extension requires Cango canvas graphics library Version 26 or later. Save this file along with the CangoAnimation file, then add the following lines to the web page header:

<script src="[directory path/]Cango-26v14-min.js"></script>
<script src="[directory path/]CangoAnimation-11v07-min.js"></script>

Make sure that the CangoAnimation file is loaded after the Cango file, as it adds methods to an existing Cango core. Application code can then create Cango instances that will have the additional methods available.

Animation architecture

Animations may be simply created by calling the Cango animation method. The calling syntax is as follow:

const animId = cgo.animation(drawFn, pathFn, options);

The 'drawFn' is the function that actually does the rendering of the scene onto the canvas at each frame. The 'pathFn' is a function called at every frame just before the 'drawFn' to calculate any new positions, rotations etc. and apply the corresponding transforms to the objects to be rendered. The 'options' parameter is an object which can hold any user defined properties that may be required by the drawFn or pathFn.

All animations on a canvas are controlled by a master timeline with all animated objects transformed and rendered at each frame even if they are drawn on different layers. The 'pathFn' functions of all animated objects are passed the elapsed time along the same timeline at each frame ensuring the motion of different objects are synchronized. The default animation frame rate is set by the window.requestAnimationFrame (RAF) utility, the time between frames will not be constant but will generally be around 17 msec (~60 frames/second). This may be overridden by the Cango.setAnimationPropertyDefault setting the frameRate property to a fixed value.

Note: the 'pathFn' will be called at the RAF rate regardless of the requested framerate. This ensures that animations whose next frame state depends upon the previous frame state will appear as smooth as possible.

The animation control methods cgo.playAnimation, cgo.pauseAnimation, cgo.stepAnimation, cgo.stopAnimation may be called on any Cango context on any layer, they all refer to the same methods. The sequence of operations followed by the 'playAnimation' and 'stepAnimation' methods are as follows:

  1. Call the animation's 'pathFn', passing the elapsed time along the timeline for this frame and the 'options' object.
  2. Clear the canvas on which each animation's calling Cango context is defined.
    Note: This automatic clearCanvas call is the default behavior, user code may override this and substitute a manual canvas clearing function within the 'drawFn' by creating a "manualClear" property in the 'options' object and setting it 'true'.
  3. Call the animation's 'drawFn' to render the Obj2D or Group onto the canvas.
  4. Swap the animation's nextState and currState objects.
  5. Save the time at which the frame was drawn in the 'currState.time' property of each animation.

Cango methods

animation

Syntax:

const animId = cgo.animation(drawFn, pathFn, options);

Description:

The animation method creates an AnimObj object with properties that may be useful to the drawFn and pathFn functions which are called in the scope of this AnimObj object.

The constructor calls the "pathFn" passing time=0 so that it can to set up the initial state prior to the animation being started by a call to "playAnimation". The 'drawFn' is then called to render the scene in this initial state. Once the animation has been started, the pathFn is called at each frame along the timeline. The user defined pathFn should calculate the new movement transforms and apply them to objects. The 'drawFn' is then called to render the frame. The frame rate is set by the Cango 'frameRate' property which may be by the 'setPropertyDefault' method. If 'frameRate' is not explicitly set then the 'requestAnimationFrame' utility will produce a frame rate of approximately 60 frames per sec.

Parameters:

drawFn: Function - A function to be called after every 'pathFn' call to actually render the animated objects to the canvas. When called, drawFn is passed one parameter, the 'options' object.

pathFn: Function - A function to be called to update the object properties to the values to be used when drawing the next frame. When called pathFn is passed two parameters 'time' and the 'options' object. The 'time' argument represents the elapsed time in msec along the timeline, the same format as 'this.currState.time' so that the elapsed time since the last frame was drawn will be given by:

  var dt = time - this.currState.time;  // in milliseconds

options: Object - An object storing any variables or constants that may be required by the drawFn or pathFn.

deleteAnimation

Syntax:

cgo.deleteAnimation(animId);

Description:

All animations are stopped and the animation whose ID is 'animId' is deleted. This ID value was returned when the animation was created by a call to 'animation' method. The animation's AnimObj is removed from the current array of animations. The frame currently drawn on the canvas will remain frozen. This method may be called on any Cango context on any layer.

Parameters:

animId: String - An ID string returned when the animation was created.

deleteAllAnimations

Syntax:

cgo.deleteAllAnimations();

Description:

Deletes all the animations from the background layer and any added canvas layers. All the animations are stopped and animation definitions removed from the current array of animations, All the animated objects will remain frozen where they were last drawn. This method may be called on any Cango context on any layer and will clear all animations on all layers.

Parameters:

none.

playAnimation

Syntax:

cgo.playAnimation([startTime[, stopTime]]);

Description:

Starts all animations that have been defined for Cango contexts on this canvas or stack of canvases (if any canvas layers have been created). At each frame the elapsed time along the timeline is passed to the pathFn of each animated object so they can apply the movement transforms. By default, the timeline is traversed from time 0, but if a 'startTime' is passed then this will be the time initially passed to the pathFn. The timeline is traversed by drawing a frame and then making a call to 'requestAnimationFrame' to draw the next and so on, the resulting frame rate will be about 60 frames/sec. If a 'stopTime' was passed to the playAnimation method then the animation will stop as soon as the elapsed time since starting equals or exceeds this stopTime.

Parameters:

startTime: Number - Optional time in milliseconds along the timeline where the animation playing will commence.

stopTime: Number - Optional time in milliseconds along the timeline where the animation will cease and the mode will be set to 'stopped'.

pauseAnimation

Syntax:

cgo.pauseAnimation();

Description:

Stops any current animation and saves the elapsed time since the start of the animation. When 'playAnimation' or 'stepAnimation' are called the animation will resume from the current time offset from the start. This differs from the 'stopAnimation' call which forces the animation to return to the start when 'playAnimation' or 'stepAnimation' is called. All Cango instances on any layer on this background canvas use the same timeline so any can make this call and it will have the same effect.

Parameters:

none.

stepAnimation

Syntax:

cgo.stepAnimation();

Description:

If the animation is currently playing this call does nothing. If paused or stopped the animation will request a frame to be drawn on the background canvas and all overlay layers with the elapsed time of the animation advanced by the current value of the Cango.stepTime property. One frame will be drawn and the animation put into the 'paused' state. The value of 'stepTime' may be set by setAnimationPropertyDefault, the default value is 50 msec.

Parameters:

none.

stopAnimation

Syntax:

cgo.stopAnimation();

Description:

Stops any current animation and reset the elapsed time into the animation back to 0. When 'playAnimation' or 'stepAnimation' is subsequently called the animation will resume from the beginning of the animation timeline. This differs from the 'pauseAnimation' call which restart the animation from the current elapsed time along the animation timeline. All Cango instances on any layer on this background canvas use the same timeline so any can make this call and it will have the same effect.

Parameters:

none

redrawAnimation

Syntax:

cgo.redrawAnimation();

Description:

The last frame of all the animations on the background canvas timeline are redrawn at the state saved in the 'currState' object. If the animation is currently playing, then the animation is paused and the last frame to be drawn is redrawn and the animation resumes playing. This method is particularly useful when used as the drawFn of the Zoom utility. If CangoZoomPan utility is used on the animation the 'redrawAnimation' method is used to after each change to the world coordinate scaling and offsets.

Parameters:

none.

setAnimationPropertyDefault

Syntax:

cgo.setAnimationPropertyDefault(property, value);

Description:

This method is provided to allow the default value of the animation stepTime and frameRate to be changed.

Parameters:

property: String - The name of the property whose default value is to be changed, either "stepTime" or "frameRate". The property names may be in upper, lower or mixed case.

value: Number - The value to be used as the new default value for the Cango property.
Valid stepTime values are between 15 msec and 500 msec.
Valid frameRate values are between 0.5 and 30 (frames per second).

Note: There is only one master timeline for each background canvas regardless of the number of layers or the number of instances of Cango on the canvas stack, so setting a stepTime interval or a frameRate affects the behavior of all animations made by all Cango instances sharing this Cango's background canvas.

AnimObj object

To implement this animation model, the Cango animation method creates a object of type AnimObj which encapsulates references to the Cango context that created the animation and other properties that may be useful to the 'drawFn' and 'pathFn'. These functions are called in the scope of the AnimObj object ie. within these functions 'this' will refer to the AnimObj object so all its properties are readily available.

The 'pathFn' may be of a type that needs to refer to the values used to drawn the frame currently on the screen and the time it was drawn, to calculate values for the next frame. To assist in such situations, Cango animation provides the currState and nextState objects. All the newly calculated values that the pathFn will need to create the transforms for the next frame should be stored in the object this.nextState. After playAnimation or stepAnimation renders the object to the canvas it swaps this.currState and this.nextState objects so that the 'as rendered' properties are always available in this.currState. The pathFn should treat this.currState properties as 'read-only' using the values to make calculations, write these to this.nextState then apply the transforms to the object.

AnimObj properties

PropertyTypeDescription
gc Cango objectThe graphics context that will render the object onto the canvas at each frame along the timeline. The pathFn can make use of this graphics context to call Cango methods such as setPropertyDefault etc.
nextState ObjectA JavaScript object provided to hold all the user defined properties that will be changed at for each frame. The 'pathFn' updates the nextState property values prior to 'obj' being rendered. Once the frame is rendered 'nextState' is swapped with the 'currState' object so the 'as rendered' values of the property values are available to the pathFn as a reference when next called. The pathFn will then overwrite the nextState values with new values for the next frame and so on.
currState ObjectA JavaScript object to hold a copy of the 'nextState'. This object should be considered read-only as it will be swapped with the 'nextState' object after each frame is rendered. The currState object will always have the property 'time' which holds the time (in msec) at which the frame on the canvas was rendered. All other properties will be user defined 'nextState' properties.
options ObjectAn object that is available to the 'drawFn' and 'pathFn' to hold values useful in setup, drawing or generating the 'nextState'. If the 'pathFn' calls a Tweener.getVal method the 'options' object can be used to hold the array of keyframe values to be interpolated.
If the animation 'options' object a property manualClear is set 'true' then automatic clearing of the canvas layer will be disabled. If 'manualClear' if 'false' or undefined the canvas will be cleared before the next frame is drawn.

Animation example

Here is the source code for an animation which rotates a Text object through 360 degrees then back again at the same time scaling the object to twice its size and back again. The animation starts after a 1 sec delay then repeats the movement and the delay period indefinitely.

Figure 1. Text object with animated translate and scale.

function orbitDemo(cvsID)
{
  const txt = new Text("Hullo", {
        fillColor:"blue",
        fontSize:8,
        lorg:5}),
      orbitData = {radius:60, va:1.5, ang:0};

  function drawTxt(opts)
  {
    this.gc.render(txt);
  }

  function orbit(time, opts)
  {
    const dt = time-this.currState.time; // time since last frame

    opts.ang += opts.va*dt/1000;     // constant angular velocity
    if (opts.ang > 2*Math.PI)        // wraparound for angle
    {
      opts.ang -= 2*Math.PI;
    }
    txt.scale(2.5+ Math.cos(opts.ang));
    txt.translate(opts.radius*Math.cos(opts.ang),
                  opts.radius*Math.sin(opts.ang));
  }

  const g = new Cango(cvsID);
  g.setWorldCoordsRHC(-100, -100, 200);

  g.animation(drawTxt, orbit, orbitData);
  g.playAnimation();
}

Animation example using 'currState' and 'nextState'

In the animation of a bouncing ball shown below in Fig 2, 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.

Figure 2. Example of animation next frame state calculation based on current frame state.


Here is the source code for the animation shown above.

var bounceGC;           // Cango graphics context

function bouncingBallDemo(cvsID)
{
  const dia = 45,
        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, // =9.8m/s/s =0.0098mm/ms/ms =0.0098*speed units/ms/ms
        startX = 120,
        startY = 250;

  function drawBall(opts)
  {
    ballGrp.translate(this.nextState.x, this.nextState.y);
    this.gc.render(ballGrp);
  }

  function bouncingPath(time, opts)    // time = time since start of animation
  {
    // 'this' refers to the Animation object (this.gc, this.nextState etc)
    // this.currState is for reference, what is on the screen (don't write to it)
    // after the frame is drawn in nextState, nextState and currState are swapped
    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
      const vel = 2*speed;    // x units/ms
      const 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);

      return;     // this is the state to get drawn at start
    }
    // calculate the new position and velocity
    const timeInt = time - this.currState.time;   // time since last draw
    // v = u + at
    let yVel = this.currState.vy + gravity * timeInt;    // accelerating due to gravity
    let xVel = this.currState.vx;                    // constant
    let x = this.currState.x + xVel*timeInt;
    let 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)
      const s = this.currState.y - (opts.bottomWall + opts.radius);     // pre bounce
      const 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;
  }

  const shadows = sphereShading(dia, 255, 0, 0);
  const ball = new Shape(PathSVG.circle(dia), {fillColor:shadows.base} );
  const shade = new Shape(PathSVG.circle(dia), {fillColor:shadows.shadow});
  const ballHilite = new Shape(PathSVG.circle(dia), {fillColor:shadows.hilite});
  const ballGrp = new Group(ball, shade, ballHilite);

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

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

Tweener interpolation utility

When writing a Cango animation 'pathFn' functions, the aim is to generate values of position, scale, rotation etc or some style property that can be set for an object being animated. New values need to be generated for each frame so the 'pathFn' is called prior to each frame. If the property is specified as an array of key values and key times then the next frame values can be obtained by interpolating between these keyframe values. The Tweener object is provided to simplify this task. A Tweener holds the basic parameters of a timeline and has just one method: getVal which will do the interpolation calculations based on key frame values and the time along the timeline.

Tweener

Syntax:

var twnr = new Tweener(delay, dur, loopStr);

Description:

This utility is provided to simplify interpolation between elements in an array of key values. Creating a Tweener object sets up a timeline of length 'dur' milliseconds which optional 'delay' to the start of the animation and whether the animation repeats specified by the 'loop' parameter.

Parameters:

delay:Number - A time in milliseconds that must be exceeded before interpolating key values begins. When a Cango animation is started by a call to the 'playAnimation' method times starting at 0 msec are passed to path functions.

dur:Number - The duration of the animation starting after 'delay' milliseconds and lasting 'dur' milliseconds. Interpolated values will be returned for time between delay msec and delay+dur msec.

loopStr:String - The 'loopStr' parameter can take two values that will cause animation looping: 'loop' and 'loopAll'. After the initial delay (if delay is non-zero) 'loop' will cause the animated sequence that is 'dur' msec long to be repeated without repeating the delay. 'loopAll' will cause the delay and the animation to repeat, so the repeat interval will be delay+dur msec long. Either value will repeat its sequence indefinitely or until 'stopAnimation' or 'pauseAnimation' stops more calls being made to animation path functions. If the delay = 0 there is no difference in behavior between 'loop' and 'loopAll'.

Tweener method

getVal

Syntax:

var val = twnr.getVal(time, keyValues[, keyTimes]);

Description:

The getVal method is designed to be used in an animation pathFn which will be called immediately prior to each animation frame being rendered. It returns a value for some property at time 'time' along a timeline of duration twnr.dur by interpolating between elements in an array of key values. The keyframe values are specified in the keyValues array.

Optionally an array of times corresponding to the key values may be passed to 'getVal' in the keyTimes array. If no 'keyTimes' array is passed to the 'getVal' method then the key values are assumed to be equally spaced over the 'dur' time, the first key value represents the property at the beginning of the Tweener timeline 'dur' period and the last key value is the value at the end of 'dur' period.

If the Tweener has "loop" or "loopAll" set the the time value passed to the getVal method will also loop back of the Tweener duration or the Tweener delay+duration respectively.

Parameters:

time: Number - Elapsed time along the Tweener timeline, measured in milliseconds when getVal is called. The value starts at 0 and continues incrementing until Cango.stopAnimation is called, which will reset the elapsed time to 0. The 'getVal' method will not start interpolating until the time exceeds the Tweener 'delay' time. It then commences interpolating the value of the properties based on the time elapsed since starting as a percentage of the 'duration' parameter. The animation will last for 'dur' milliseconds. It will continue to return the last keyValue for all times exceeding delay+dur unless the timeline has looping enabled.

keyValues: Number or Array of Numbers - A single number will represent the static value for the entire animation. If 'keyValues' is an array then its elements represent the key values in the animation. If the array of values has 2 or more elements, the first will be the initial value returned after time 'delay' milliseconds. The last value will be the value at the finish of the animation after time interval 'delay+dur'.

keyTimes: Array of Numbers - An array holding the keyframe times corresponding to the keyValues. The keyTimes array must have the same number of elements as its matching values array. Time values are specified as a percentage of the Tweener.dur property so the values are limited to the range 0 to 100. If keyTimes is undefined then the key values are assumed to be equally spaced over the 'dur' time.

Returns: Number.

Tweener example

Here is the source code for an animation which changes the fillColor a circle from red to green then blue then back to red. The animation lasts 3 seconds and repeats the cycle indefinitely.

Figure 3. Shape object with animated "fillColor" property.

function animColor(cvsID)
{
  const disc = new Shape(PathSVG.circle(50)),
        colData = { r:[255, 0,   0, 255],
                    g:[0, 200,   0,   0],
                    b:[0,   0, 255,   0] },
        colTwnr = new Tweener(0, 3000, "loop");

  function drawDisc(opts)
  {
    this.gc.render(disc);
  }

  function changeColor(time, opts)
  {
    const rVal = Math.round(colTwnr.getVal(time, opts.r)),
          gVal = Math.round(colTwnr.getVal(time, opts.g)),
          bVal = Math.round(colTwnr.getVal(time, opts.b));

    disc.setStyleProperty("fillColor", "rgb("+rVal+","+gVal+","+bVal+")");
  }

  const g = new Cango(cvsID);
  g.setWorldCoordsRHC(-50, -50, 100);
  g.animation(drawDisc, changeColor, colData);

  g.playAnimation();
}

Animation along a Path

CangoAnimation provides the PathTweener object to simplify the animation of objects following a path. New values of the objects position along the path must be generated for each frame. The objects speed may vary so that equal intervals in time won't necessarily correspond to equal distances along the path. A PathTweener holds the basic parameters of a timeline and has just one method:
getPos which will do the interpolation calculations based on the path object, key position values and the time along the timeline.

PathTweener

Syntax:

var ttnr = new PathTweener(pth, delay, dur, loopStr);

Description:

This utility is provided to simplify calculation of the coordinates of positions along a path defined by the 'pth' PathSVG object. Creating a PathTweener object sets up a timeline of length 'dur' milliseconds which optional 'delay' to the start of the animation and whether the animation repeats specified by the 'loop' parameter.

Parameters:

pth: PathSVG - An PathSVG object defining the path to be follow.

delay: Number - A time in milliseconds that must be exceeded before interpolating key values begins. When a Cango animation is started by a call to the 'playAnimation' method times starting at 0 msec are passed to path functions.

dur: Number - The duration of the animation starting after 'delay' milliseconds and lasting 'dur' milliseconds. Interpolated values will be returned for time between delay msec and delay+dur msec.

loopStr: String - The 'loopStr' parameter can take two values that will cause animation looping: 'loop' and 'loopAll'. After the initial delay (if delay is non-zero) 'loop' will cause the animated sequence that is 'dur' msec long to be repeated without repeating the delay. 'loopAll' will cause the delay and the animation to repeat, so the repeat interval will be delay+dur msec long. Either value will repeat its sequence indefinitely or until 'stopAnimation' or 'pauseAnimation' stops more calls being made to animation path functions. If the delay = 0 there is no difference in behavior between 'loop' and 'loopAll'.

PathTweener method

getPos

Syntax:

var pos = twnr.getPos(time, keyDists[, keyTimes]);

Description:

The getPos method is designed to be used in an animation pathFn which will be called for each animation frame. It returns an object containing the properties "x", "y" and "gradient" representing the objects position and the path's slope at that percentage distance along the path. The 'keyDists' argument provides an array of distances along the path expressed as percentages of the total path length. The 'keyTimes' array holds the times at which the object should be at the corresponding distance.

Parameters:

time: Number - Elapsed time along the Tweener timeline, measured in milliseconds when getPos is called. The value starts at 0 and continues incrementing until Cango.stopAnimation is called, which will reset the elapsed time to 0. The 'getPos' method will not start interpolating until the time exceeds the Tweener 'delay' time. It then commences interpolating the value of the properties based on the time elapsed since starting as a percentage of the animation's 'dur' property. getPos will continue to return the position at the last 'keyDists' value for all times exceeding delay+dur unless the timeline has looping enabled.

keyDists: Array (or Number) - An array of percentage values representing key distances along the path.
Note: A single number is allowed for keyDists which will represent the static position defined as a percentage of the total path length.

keyTimes: Array - An array of percentages of the animation's 'dur' property. Each element represents the time at when the corresponding 'keyDists' value is to be reached. If keyTimes is undefined then the key values are assumed to be equally spaced over the 'dur' time.

Returns:

An object with properties {x: , y: , gradient: }, 'x' and 'y' in world coordinates and 'gradient' in radians measured from the X axis.

PathTweener getPos example

Here is the source code for a animated rocket following a path representing the flight path. The animation lasts 5 seconds and can be repeated by clicking "PLAY".

Figure 4. Example of animating movement along an arbitrary pth using the PathTweener object.

function rocketDemo(cvsID)
{
  const rawTrack = "m 32.6,235 c -3.2,-96 -2.4,-112 13.5,-137.5 15.9,-25.7 64.9,-40.8 81.9,-6.1 \
                                  17,35.6 -9,72.6 -28.6,75.6 -19.9,8 -58,1 -49.3,-37 \
                                  7.5,-33 31.5,-42.4 58.9,-34.7 26,7.7 44,31.7 56,50.7 \
                                  14,21 28,93 28,93";
  const trkData = segment(rawTrack).translate(-32.6,-235).scale(1,-1);

  const rocket = makeRocket();
  const flame = makeFlame();

  const pathConfig = {rocketGrp: rocket,
                      distances: [0, 3, 8, 30, 100],  // % of path length
                      delay: 0,
                      duration: 5000,
                      loop:'noloop' };
  const flameConfig = {flameGrp: flame,
                      sizes: [0, 0.8, 0.8, 1.2, 1.2, 0],
                      sizeTimes: [0, 1, 46, 50, 92, 93]};   // % of animation duration

  const ttwr = new PathTweener(trkData, pathConfig.delay, pathConfig.duration, pathConfig.loop);
  const vtwnr = new Tweener(pathConfig.delay, pathConfig.duration, pathConfig.loop)

  function makeRocket(scale=1)
  {
    const fin1 = "M -3.476 5.543 L -7.641 8.482 C -8.142 8.835 -8.773 8.95 -9.367 8.799 \
                  L -14.989 7.403 C -15.136 7.386 -15.236 7.245 -15.204 7.1 \
                  C -15.193 7.056 -15.171 7.015 -15.139 6.982 L -11.065 2.52 Z";
    const path3 = new Shape(fin1, {fillColor:"teal"});

    const fin2 = "M -3.417 -5.608 L -7.507 -8.604 C -8.005 -8.962 -8.633 -9.086 -9.229 -8.941 \
                  L -14.882 -7.62 C -15.068 -7.576 -15.138 -7.348 -15.008 -7.208 \
                  L -10.98 -2.683 Z";
    const path4 = new Shape(fin2, {fillColor:"teal"});

    const hull = "M 12.63 0.115 C 8.52 -4.974 -4.529 -7.835 -10.747 -3.763 \
                  C -10.93 -3.653 -11.048 -3.46 -11.062 -3.247 \
                  L -11.079 3.048 C -11.079 3.264 -10.972 3.466 -10.793 3.586 \
                  C -4.656 7.765 8.415 5.114 12.63 0.115 Z";
    const path5 = new Shape(hull, {fillColor:"grey", lineWidth:2});

    const nose = "M 12.63 0.115 C 10.667 -1.98 8.174 -3.505 5.414 -4.297 \
                  C 5.513 -5.922 5.355 4.385 5.355 4.385 \
                  C 8.12 3.642 10.633 2.167 12.63 0.115 Z";
    const path6 = new Shape(nose, {fillColor:"teal", lineWidth:2});

    const path7 = new Shape(PathSVG.circle(5), {fillColor:"black", lineWidth:2});

    return new Group(path3, path4, path5, path6, path7);
  }

  function makeFlame(scale=1)
  {
    const outFlame  =  "M 0.118 -0.137 C 0.12 -1.972 -1.366 -3.463 -3.201 -3.466 \
                        C -7.983 -3.499 -11.114 -0.156 -17.582 -0.194 \
                        C -11.114 -0.156 -7.996 3.174 -3.212 3.182 \
                        C -1.376 3.185 0.114 1.699 0.118 -0.137 Z";
    const path1 = new Shape(outFlame, {fillColor:"orange"});

    const inFlame =  "M 0.118 -0.137 C 0.111 -1.362 -0.876 -2.357 -2.101 -2.374 \
                      C -5.298 -2.389 -7.411 -0.154 -11.715 -0.179 \
                      C -7.387 -0.151 -5.317 2.067 -2.12 2.082 \
                      C -0.894 2.075 0.101 1.089 0.118 -0.137 Z";
    const path2 = new Shape(inFlame, {fillColor:"yellow"});

    return new Group(path1, path2);
  }

  function rocketPathFn(time, opts)
  {
    const pos = ttwr.getPos(time, pathConfig.distances);
    const size = vtwnr.getVal(time, flameConfig.sizes, flameConfig.sizeTimes);

    const x = pos.x;
    const y = pos.y;
    const angle = 180*pos.gradient/Math.PI;

    flame.scale(size);
    flame.translate(-11, 0);
    flame.rotate(angle);
    flame.translate(x, y);
    rocket.rotate(angle);
    rocket.translate(x, y);
  }

  function drawRocket(opts)
  {
    this.gc.render(rocket);
    this.gc.render(flame);
  }

  gc = new Cango(cvsID);
  gc.gridboxPadding(7, 10, 0, 0);
  gc.setWorldCoordsRHC(0, 3, 180); // square pixels

  const gL1 = new Cango(gc.createLayer());
  gL1.dupCtx(gc);

  // draw the rocket path as a dashed line
  gc.drawPath(trkData, {strokeColor:'green', dashed:[11,5]});
  gL1.animation(drawRocket, rocketPathFn, pathConfig);
}

BoneArray

The BoneArray is designed to assist in animating articulated arms or multi-segmented objects. A BoneArray object maintains the angle of rotation and coordinates of the origin and end points of an array of Bone objects. The coordinates of the origin are always updated to match those of the end point of the preceding bone. Each Bone may be rotated or have its length changed, all succeeding bones in the array will inherit this movement.

In the diagram below, four Bones have been added to a BoneArray. A red dot has been drawn at the end node of each Bone, click and drag these dots to see how a rotation applied to a Bone is inherited by all the following Bones.

Figure 5. Green squares are Bone origin nodes. Red dots are Bone end nodes. Click and drag the red dots to see BoneArray movement inheritance.

BoneArray constructor

Syntax:

const bnary = new BoneArray();

Description:

Creates a BoneArray object, an (initially empty) array that will hold Bone objects which are added one at a time by the BoneArray.addBone method.

Parameters:

none

BoneArray methods

addBone

Syntax:

const b = bnary.addBone(len);

Description:

Adds a Bone element to the BoneArray.

Parameters:

len: Number - A value specifying the length of the bone.

Returns:

Bone - A reference to the Bone object just added to the BoneArray.

addLobe

Syntax:

bnary.addLobe(node, path);

Description:

The addLobe method adds an object to the BoneArray.lobes array. The lobe object has a 'link' property that is either an origin or an end node of one of the Bones in the BoneArray. The Lobe also has a 'path' property which is a PathSVG object defining the outline a the lobe. When BoneArray.updateBones method is called, the coordinates of the 'path' segments will be updated to match the movement of the node to which the lobe has been linked. This updated Lobe.path can then be used as the descriptor for a Cango Path or Shape that can then be drawn as an attachment to the object being animated by the movement of the Bone.

Parameters:

node: Object - The 'origin' or 'end' node of a Bone element of the BoneArray.

path: PathSVG object - A PathSVG defining the lobe's outline.

linkObj

Syntax:

bnary.linkObj(node, obj);

Description:

The addObj method links a Cango object; a Path, Shape, Group etc. to a node of the BoneArray. The 'node' argument is either an origin or end of one of the Bones in the BoneArray. When BoneArray.updateBones method is called, the obj.rotate and obj.translate methods are applied so that the object's drawing origin position matches that of the node and rotation angle matches the angle of the node's bone.

Parameters:

node: Object - The 'origin' or 'end' node of a Bone element of the WideBoneArray.

obj: Cango object - A Path, Shape, Text, Img, or Group object.

dup

Syntax:

const bnary2 = bnary1.dup();

Description:

Creates a new BoneArray instance which is a deep copy of the calling BoneArray object.

Parameters:

none

Returns:

A new BoneArray object.

updateBones

Syntax:

bnary.updateBones();

Description:

Propagates all the accumulated rotation and length values set for the various elements of the BoneArray, through the succeeding elements of the array. The angle of rotation of each bone is added to rotation inherited from previous bones. The updated angle of rotation is saved in the Bone.angle property. The accumulated angles and lengths are used to update the (x,y) coordinates of each Bone.origin and Bone.end to reflect their new positions. If any lobes or linkedObjs have been added, the coordinates of objects are also updated the reflect the movement of their link nodes.

Parameters:

none

Returns:

A new BoneArray with the updated. The calling BoneArray is unchanged.

BoneArray properties

PropertyTypeDescription
endNodes ArrayAn array of objects which have 'x' and 'y' properties holding the coordinates of the nodes along the array from the 'origin' node of the first bone then the 'end' node of all the bone to the 'end' node of the last.
lobes ArrayAn array of objects, which have 'link' and 'path' properties. The 'link' is a reference to the origin or end node of a Bone element. The 'path' is a PathSVG object whose coordinates will be recalculated to track the position of the 'link' and the angle of the link's bone.
linkedObjs ArrayAn array of objects, which have 'link' and 'obj' properties. The 'link' is a reference to a node of a Bone element. The 'obj' is a Cango object which will be rotated and translated to match the position of the 'link' node and the angle of the link's bone.
span NumberThe total length of all the Bone elements in the array.

Animation Example using BoneArray

CangoAnimation extension can take advantage of the way a BoneArray enables transforms applied to an element to be inherited by succeeding elements in the array. This is demonstrated in Fig 6 which shows a drawing of an excavator, its arm sections are positioned by a BoneArray.

Figure 6. Example of animation demonstrating the BoneArray object controlling the inherited movement of elements of the array.


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

...

function dist(x0, y0, x1, y1)
{
  const dx = x1 - x0;
  const dy = y1 - y0;
  return Math.sqrt(dx*dx + dy*dy);
}
const len1 = dist(cx1, cy1, cx2, cy2);
const len2 = dist(cx2, cy2, cx3, cy3);
const len3 = 0;

const armBones = new BoneArray()
armBones.addBone(len1);
armBones.addBone(len2);
armBones.addBone(len3); // Bone with 0 length, still moves and can rotate

armBones.linkObj(armBones[0].origin, seg0Grp);
armBones.linkObj(armBones[1].origin, seg1Grp);
armBones.linkObj(armBones[2].origin, seg2Grp);

function drawArm(opts)
{
  this.gc.render(armGrp);
}

function armPathFn(time, opts)
{
  const seg0Rot = armTwnr.getVal(time, opts.s1),
        seg1Rot = armTwnr.getVal(time, opts.s2),
        seg2Rot = armTwnr.getVal(time, opts.s3);

  armBones[0].setRotation(seg0Rot);
  armBones[1].setRotation(seg1Rot);
  armBones[2].setRotation(seg2Rot);
  
  armBones.updateBones();   // calculate all the new positions
}

armCtx = new Cango(cvsID);
armCtx.setWorldCoordsSVG(-250, -450, 1000);

armCtx.animation(drawArm, armPathFn, animData);
      

WideBoneArray

Thw WideBonesArray is a sub-class of the BoneArray which adds a width to each bone. In addition to the 'origin' and 'end' nodes, the WideBoneArray adds a 'pNode' and 'sNode' that are positioned on either side of the end node with the distance between them defining the WideBoneArray width at that point. Their coordinates are updated when the WideBoneArray.updateBones method is called. The updated Node coordinates are available in the WideBoneArray.pNodes and WideBoneArray.sNodes arrays. These arrays are suitable for curve fitting to recreate the shape of the articulated body being defined by the WideBodyArray.

WideBoneArray constructor

Syntax:

const bnary = new WideBoneArray(width);

Description:

Creates a WideBoneArray object, an (initially empty) array that will hold Bone objects which are added one at a time using the WideBoneArray.addBone method. The 'width' value will be the default width for addBone calls. Also creates P and S nodes on either side of the WideBoneArray origin (0,0) which will become the origin of the first bone.

Parameters:

width Number: - The width at the head the WideBoneArray and the default width for all subsequent calls to the WideBoneArray.addBone method.

WideBoneArray methods

addBone

Syntax:

const b = bnary.addBone(len[, width]);

Description:

Adds a Bone element to a WideBoneArray. In addition to the 'origin' and 'end' nodes this method creates a 'pNode' and an 'sNode' node that are always at a distance of 'width/2' on either side of the Bone 'end' node. When the WideBonesArray.updateBones method is called, the coordinates of the pNode and sNode are rotated and translated along with the end node to reflect and rotation or length changes made to the bone directly and inherited from preceding bones.

Parameters:

len: Number - A value specifying the length of the bone.

width: Number - An optional value specifying the width of the bone. If width is undefined, then the default width (0) is used.

Returns:

Bone - A reference to the Bone object just added to the WideBoneArray.

addLobe

Syntax:

bnary.addLobe(node, path);

Description:

The addLobe method adds a Lobe object to the BoneArray.lobes array. The lobe object has a 'link' property that is either an origin or and end of one of the Bones in the WideBoneArray. The Lobe also has a 'path' property which is a PathSVG object defining the outline a the lobe. When WideBoneArray.updateBones method is called, the coordinates of the Lobe.path segments will be updated to match the movement of the node to which the lobe has been linked.

Parameters:

link: Object - A reference to a node of a Bone element of the WideBoneArray.

path: PathSVG object - A PathSVG defining the lobe's outline.

linkObj

Syntax:

bnary.linkObj(node, obj);

Description:

The addObj method links a Cango object (a Path, Shape, Group etc.) to a node of the WideBoneArray. The 'node' argument is either an origin, end, pNode or sNode of one of the Bones in the WideBoneArray. When WideBoneArray.updateBones method is called, the obj.rotate and obj.translate methods are applied so that the object's drawing origin position matches that of the node and rotation angle matches the angle of the node's bone.

Parameters:

link: Object - A reference to a node of a Bone element of the WideBoneArray.

obj: Cango object - A Path, Shape, Text, Img, or Group object.

dup

Syntax:

const bnary2 = bnary1.dup();

Description:

Creates a new WideBoneArray instance which is a deep copy of the calling WideBoneArray object.

Parameters:

none

Returns:

A new WideBoneArray object.

updateBones

Syntax:

bnary.updateBones();

Description:

When the WideBoneArray.updateBones method is called, the Bone elements inherit the net rotation of all the bones that precede them in the array and the value is saved in the Bone.angle property. The (x,y) coordinates of each bone's origin, end, pNode and sNode are all updated to reflect their new positions. If lobes or linkedObjs have been added, the objects are also updated the reflect the movement of their link nodes.

Parameters:

none

Returns:

A new WideBoneArray with the updated. The calling WideBoneArray is unchanged.

WideBoneArray properties

PropertyTypeDescription
endNodes ArrayAn array whose elements are objects with x and y properties holding the updated coordinates of the nodes along the array from the origin node of the first bone then the end node of all the bone to the end node of the last.
pNodes ArrayAn array of objects with x and y properties holding the updated coordinates of the P nodes along the array, from the P node adjacent to the origin of the first bone to P node adjacent to the end node of the last bone.
sNodes ArrayAn array of objects with x and y properties holding the updated coordinates of the S nodes along the array, from the S node adjacent to the origin of the first bone to S node adjacent to the end node of the last bone.
lobes ArrayAn array of objects, which have 'link' and 'path' properties. The 'link' is a reference to a node of a Bone element. The 'path' is a PathSVG object all of whose coordinates will be recalculated to track the movement of the 'link' node.
linkedObjs ArrayAn array of objects, which have 'link' and 'obj' properties. The 'link' is a reference to a node of a Bone element. The 'obj' is a Cango drawing object that will have transforms applied to track the movement of the 'link' node.
span NumberThe total length of all the Bone elements in the array.

Animation Example using WideBoneArray

CangoAnimation extension can take advantage of the way a WideBoneArray as demonstrated in Fig 7 which shows a drawing of a fish swimming, the fish outline is determined by a WideBoneArray, the individual Bone elements each have their own width as these its arm sections are moved so the outline shape changes. The source code of the example is available by viewing the page source code.

Figure 7. Example of animation demonstrating how a WideBoneArray object can control movement of a complex 2D shape.

Bone

This is a private object used as the elements of the BoneArray or WideBoneArray objects. The Bone object doesn't have a public constructor, they created by the BoneArray.addBone or WideBoneArray.addBone method.

Bone methods

setRotation

Syntax:

bn.setRotation(deg);

Description:

This method sets the angle of rotation to be applied to the Bone over-riding any previous call to setRotation. When the BoneArray.updateBones methods is called, this angle is added to any inherited rotation. This total angle value will be inherited by the succeeding Bone in the array.

Parameters:

deg: Number - The angle of rotation measured in degrees CCW if the World Coordinates are Left Handed Cartesian (like SVG) or Clockwise if Right Handed Cartesian.

getRotation

Syntax:

const deg = bn.getRotation();

Description:

This method returns the value set by the last call to the Bone.setRotation method without any inherited rotations added. This may be useful when adding incremental rotations to the Bone.

Parameters:

none

Returns:

Number - The rotation angle measured in degrees.

setLength

Syntax:

bn.setLength(len);

Description:

This method sets the length of the Bone, the distance between the origin and end nodes, over-riding the value set by the BoneArray.addBone method or any previous call to setLength.

Parameters:

deg: Number - The new length of the Bone measured in current world coordinates.

getLength

Syntax:

const len = bn.getLength();

Description:

This method returns the value set by the last call to the Bone.setLength method or if no call such has been made, the initial length set by the BoneArray.addBone length parameter is returned. This may be useful when making incremental changes to the Bone length.

Parameters:

none

Returns:

Number - The length in current world coordinates.

Bone properties

PropertyTypeDescription
origin ObjectAn object with 'x' and 'y' properties holding the coordinates of the Bone origin. The coordinates are updated by a call to BoneArray.updateBones method to reflect the accumulated movement of the Bone. The coordinates are always the same as the 'end' object of the preceding Bone.
end ObjectAn object with 'x' and 'y' properties holding coordinates of the Bone end point. The coordinates are updated by a call to BoneArray.updateBones method to reflect the accumulated movement of the Bone.
angle NumberThe angle measured in degrees representing the inherited rotation of all preceding Bones plus the rotation angle of this bone set by 'setRotation' method.