PrairieDraw.js User Manual

Other sources of information are the Reference Manual, the Developer Manual, and the Sylvester Docs for vectors. See the Mozilla Developer Network JavaScript documentation for JavaScript guides and references.

Quick-start

Include both sylvester.js (for vector and matrix routines) and PrairieDraw.js:

<script src="sylvester.js"></script>
<script src="PrairieDraw.js"></script>

Then in the HTML body, create a canvas and a PrairieDraw() object:

<canvas id="test" width="200" height="200"></canvas>
<script type="application/javascript">
new PrairieDraw("test", function() {
    this.setUnits(2, 2);
    this.line($V([0, 0]), $V([1, 1]));
});
</script>

The PrairieDraw(canvas, drawFcn) constructor takes the canvas ID as the first argument, and a function with drawing commands as the second argument.

Coordinate systems

There are three different coordinate systems in use by PrairieDraw:

  1. The pixel coordinates used by the canvas object, with distances measures in pixels (technically the HTML px unit), the origin in the upper left corner, the x-axis going right and the y-axis going down.
  2. The drawing coordinates used for PrairieDraw commands, with the origin in the center of the canvas, the x-axis going right, and the y-axis going up.
  3. The normalized coordinates for the viewport, ranging from 0 (left/bottom) to 1 (right/top).

To set the horizontal and vertical canvas dimensions in drawing coordinates, call this.setUnits(xSize, ySize). The canvas will be shrunk horizontally or vertically to match the requested aspect ratio, as necessary. The coordinates below are the result of calling this.setUnits(6,6).

All vectors in PrairieDraw have drawing coordinate units, while quantities like line widths are generally measured in pixel coordinates. Each PrairieDraw command argument specifies whether it is drawing coordinates or pixel coordinates.

Vectors and drawing commands

Vectors in PrairieDraw use the Sylvester library. These can be created with the notation $V([4,3]), and operated on like:

var a = $V([1, 2]);
var b = $V([3, -1]);

var c = a.e(1);     // first element, c = 1
var d = a.add(b);   // d is a + b = (4, 1)
var e = a.x(4);     // e is 4 * a = (4, 8)
var f = a.dot(b);   // f is 1
var g = a.cross(b); // g is (0, 0, 5)

Note that 2D vectors are automatically extended to 3D with zero z-component when necessary, such as for taking cross products.

Drawing commands take vectors as arguments to describe positions, such as:

new PrairieDraw("vectors", function() {
    this.setUnits(6, 6);
    var O = $V([0, 0]);
    var P = $V([2, 1]);
    var V = $V([-1, -2]);

    this.line(O, P);
    this.line(P, P.add(V));
});

Arrows

Arrows show vectors with arrow(start, end, type), where start and end are vectors and type is an optional string indicating the meaning of the arrow, which determines its color (e.g., "position", "velocity", etc.). If type is not given then the arrow is drawn in black.

Circle arrows show angles with circleArrow(center, radius, startAngle, endAngle, type). The specified radius of a circle arrow is the radius at the center of the arrow, and the actual radius increases along the arrow. This means that even if the angle is greater than a full circle, the arrow can still represent the angle accurately.

new PrairieDraw("circleArrow", function() {
    this.setUnits(6, 6);
    var O = $V([0, 0]);

    this.arrow(O, this.vector2DAtAngle(0).x(2.7), "position");
    this.arrow(O, this.vector2DAtAngle(3/4 * Math.PI).x(2.7));
    this.circleArrow(O, 2, 0, 3/4 * Math.PI, "angMom");
    this.circleArrow(O, 1, 0, 11/4 * Math.PI);
});

Drawing properties

In addition to color, there are other properties that control the style and thickness of lines, arrows, and other objects. These can be set with setProp("lineWidth", 3) and the current value can be retrieved with getProp("lineWidth"). The available properties include arrowLineWidth, arrowheadLength, arrowheadWidthRatio, arrowheadOffsetRatio, and circleArrowWrapOffsetRatio.

new PrairieDraw("props", function() {
    this.setUnits(6, 6);
    var O = $V([0, 0]);

    this.setProp("arrowLineWidthPx", 5);
    this.arrow(O, $V([2,2]));
    this.setProp("arrowLineWidthPx", 10);
    this.arrow(O, $V([2,-2]));
});

Transformations

The action of future drawing commands can be transformed by translate(pos), rotate(ang), or scale(factor), where both pos and factor are vectors.

new PrairieDraw("trans", function() {
    this.setUnits(6, 6);
    var O = $V([0, 0]);
    var P = $V([2, 2]);

    this.arrow(O, P);
    this.translate($V([1, 1]));
    this.rotate(Math.PI/2);
    this.arrow(O, P);
    this.translate($V([1, 1]));
    this.rotate(Math.PI/2);
    this.arrow(O, P);
});

Transformations are accumulated, so translate(p1); translate(p2) is equivalent to translate(p1.add(p2)), for example.

The mechanism for transformation accumulation is multiplication of a current transformation matrix T on the right by the applied transformation A, to give M' = TA. This transforms subsequent drawing positions x by M'x = TAx, so the most recently applied transformation acts on the position first. That is, the transformations are applied in the reverse order that they were specified. The alternative way to think about transformations is that they transform the drawing canvas, in which case we can think of them being applied in forward order.

Saving and restoring graphics state

To avoid having to remember and undo property changes and transformations, the graphics state (properties and transformations) can be saved and restored with save() and restore(). This uses a stack model, so many levels of save/restore can be nested.

new PrairieDraw("save", function() {
    this.setUnits(6, 6);
    var O = $V([0, 0]);
    var P = $V([2, 2]);

    this.save();
    this.setProp("arrowLineWidthPx", 5);
    this.translate($V([1, 1]));
    this.rotate(Math.PI/2);
    this.arrow(O, P);
    this.restore();

    this.arrow(O, P);
});

Text

Text can be drawn using this.text(position, anchor, textString), which provides the position in 2D to draw the text, and the anchor point relative to the text. If textString starts with TEX: then the rest of the string is interpreted as LaTeX. To support this, the generate_text.py script must be run on the JavaScript file to generate the required images.

new PrairieDraw("text", function() {
    this.setUnits(6, 6);
    var O = $V([0, 0]);
    var P = $V([2, 2]);
    this.arrow(O, P);
    this.text(O, $V([0, 1]), "TEX:$O$");
    this.text(P, $V([0, -1]), "TEX:$P$");
});

The anchor point coordinates are in the range -1 to 1 and specify the anchor point on the text bounding-box. This point is located at the given position. Some common anchor points are:


Drawing options and PrairieDraw objects

User-settable options can be created with this.addOption(name, defaultValue) and then later accessed with this.getOption(name, value) and set with this.setOption(name, value) or this.toggleOption(name). To use options, it is necessary to save the PrairieDraw object in a variable (the optionPD variable below) which can then be accessed from the button onclick handlers or other scripts.

optionPD = new PrairieDraw("options", function() {
    this.addOption("drawLabels", true);
    this.addOption("Px", 2);
    this.setUnits(6, 6);
    var O = $V([0, 0]);
    var P = $V([this.getOption("Px"), 2]);
    this.arrow(O, P);
    if (this.getOption("drawLabels")) {
        this.text(O, $V([0, 1]), "TEX:$O$");
        this.text(P, $V([0, -1]), "TEX:$P$");
    }
});

In the above drawing, the button code is:

<button onclick="optionPD.toggleOption('drawLabels');">Toggle labels</button>
<button onclick="optionPD.setOption('Px', 2);">Set <code>Px</code> to 2</button>
<button onclick="optionPD.setOption('Px', -2);">Set <code>Px</code> to -2</button>

Animation

Animations have almost exactly the same form as regular drawings, except that a PrairieDrawAnim object is used instead of a PrairieDraw object, and the drawing function takes a parameter t. The parameter t is the simulation time in seconds at which the system should be drawn.

animPD = new PrairieDrawAnim("anim", function(t) {
    this.addOption("drawLabels", true);
    this.setUnits(6, 6);
    var O = $V([0, 0]);
    var P = $V([2 * Math.cos(t), 2 * Math.sin(t)]);
    this.arrow(O, P);
    if (this.getOption("drawLabels")) {
        this.labelLine(O, P, $V([-1,0]), "TEX:$O$");
        this.labelLine(O, P, $V([1,0]), "TEX:$P$");
        this.labelLine(O, P, $V([0,1]), "TEX:$\\vec{v}$");
    }
});

The PrairieDrawAnim object should be saved in a variable like animPD above, so that we can call animPD.startAnim(), animPD.stopAnim(), or animPD.toggleAnim(). The buttons above have code:

<button onclick="animPD.toggleOption('drawLabels');">Toggle labels</button>
<button onclick="animPD.toggleAnim();">Toggle animation</button>

Sequenced animations

For animations with several different motions in a sequence we can interpolate between different states with state = this.sequence(states, transTimes, holdTimes, t). The states argument is an array of dictionaries, each describing the state variables. The arrays transTimes and holdTimes must be of the same length as states, where transTimes[i] is the transition time between state i and i+1 and holdTimes[i] is the holding time at state i.

The returned value state is an object with interpolated values the same as the state values, and state.t is the time within the current transition, state.alpha is the scaled transition proportion (between 0 and 1), and state.index is the current state hold index (if state.t == 0) or the previous index for the current transition.

seqPD = new PrairieDrawAnim("seq", function(t) {
    this.setUnits(9, 7);

    var sStart =  {th1:  0.5, th2:   -2, th3:   -Math.PI, d: 1};
    var sGround = {th1:  0.7, th2:    2, th3:    Math.PI, d: 0};
    var sShelf =  {th1: -0.2, th2: -1.1, th3: -Math.PI/2, d: 0};
    var sRest =   {th1:    1, th2:   -1, th3: -Math.PI/2, d: -1};
    var states = [sStart, sGround, sShelf, sRest, sShelf, sGround];
    var transTimes = [5, 5, 2, 2, 5, 5];
    var holdTimes = [2, 2, 2, 2, 2, 2];
    var state = this.sequence(states, transTimes, holdTimes, t);

    // draw with variables state.th1, state.th2, state.th3, etc.
});

There is also an externally controllable variant of sequence animation, accessed by state = this.controlSequence(name, states, transTimes, t). This requires a name (a string) but does not have holdTimes. Instead of holding for a given time when it reaches a new state, a controlled sequence holds indefinitely until this.stepSequence(name) is called to begin the transition to the next state. Controlled sequences require a name so that the correct sequence can be stepped.

Numerical differentiation

Numerical differentiation is provided by the data = this.numDiff(fcn, t) function. Here t is the time at which the derivative is desired, and fcn is a function that takes time t and returns an object dataNow with properties (like dataNow.O, dataNow.P) that are numbers or vectors at time t. Then data.P will access the current value of P, while data.diff.P and data.ddiff.P will be the first and second derivatives.

diffPD = new PrairieDrawAnim("diff", function(t) {
    var computePos = function(t) {
        var dataNow = {};
        dataNow.O = $V([0, 0]);
        dataNow.P = this.vector2DAtAngle(-Math.PI/2 + Math.sin(t)).x(2);
        return dataNow;
    }
    var data = this.numDiff(computePos.bind(this), t);

    // draw the rod and pivot

    this.arrow(data.O, data.P, "position");
    this.arrow(data.P, data.P.add(data.diff.P), "velocity");
    this.arrow(data.P, data.P.add(data.ddiff.P), "acceleration");
});