Scroll back to top
Include both sylvester.js (for vector and matrix routines) and PrairieDraw.js:
Text successfully copied to clipboard!
<script src="sylvester.js"></script>;
<script src="PrairieDraw.js"></script>
Then in the HTML body, create a canvas and a
PrairieDraw() object:
Text successfully copied to clipboard!
<canvas id="test" width="300" height="300"></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.
There are three different coordinate systems in use by PrairieDraw:
px unit), the origin in the upper left corner, the x-axis going right and the y-axis going down.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 in PrairieDraw use the Sylvester library. These can be created with the notation $V([4,3]), and operated on like:
Text successfully copied to clipboard!
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:
Text successfully copied to clipboard!
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 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.
Text successfully copied to clipboard!
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);
});
Add a point with point(position), where position should be a vector.
Text successfully copied to clipboard!
new PrairieDraw("point", function () {
this.setUnits(6, 6);
var O = $V([0, 0]);
for(let i=0; i<5; i++){
this.point(O.add($V([i-2, 2-i])));
}
});
Text can be drawn using this.text(position, anchor, textString, boxed, angle, height, width), which provides the position in 2D to draw the text, and the anchor point relative to the text. IftextString starts with TEX: then the rest of the string is interpreted as LaTeX. boxed is a boolean refering to whether a box should be drawn around it, angle is the angle of rotation of the text in radians, and height and width represent the dimensions of the text.
Note: Only the most restrictive dimension of the two will be applied. In the example below, even though
width is set at 500, height being 30 means the text is 30 pixels tall, and the width is adjusted accordingly.
Text successfully copied to clipboard!
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", false, Math.PI/2, 30, 500);
});
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:
Text successfully copied to clipboard!
new PrairieDraw("anchors", function() {
this.setUnits(6, 6);
var xnn = $V([-1, -1]);
var xn0 = $V([-1, 0]);
var xnp = $V([-1, 1]);
var x0n = $V([ 0, -1]);
var x0p = $V([ 0, 1]);
var xpn = $V([ 1, -1]);
var xp0 = $V([ 1, 0]);
var xpp = $V([ 1, 1]);
this.text(xnn, xpp, "TEX:(-1,-1)");
this.point(xn0);
this.text(xn0, xp0, "TEX:(-1, 0)");
this.point(xnp);
this.text(xnp, xpn, "TEX:(-1, 1)");
this.point(x0n);
this.text(x0n, x0p, "TEX:( 0,-1)");
this.point(x0p);
this.text(x0p, x0n, "TEX:( 0, 1)");
this.point(xpn);
this.text(xpn, xnp, "TEX:( 1,-1)");
this.point(xp0);
this.text(xp0, xn0, "TEX:( 1, 0)");
this.point(xpp);
this.text(xpp, xnn, "TEX:( 1, 1)");
});
/* ]]> */
Lines can be drawn with line(start, end). start and end should be vectors. Use setProp("shapeStrokePattern", type); to change the type of line. For example, setProp("shapeStrokePattern", "dashed"); will make subsequent lines dashed, and use setProp("shapeStrokePattern", "solid"); to revert them to solid lines.
Text successfully copied to clipboard!
new PrairieDraw("line", function () {
this.setUnits(6, 6);
var O = $V([0, 0]);
const points = {
0: [-1.5, -2],
1: [1.5, -2],
...
12: [-1.5, -2],
};
const keys = Object.keys(points);
for (let i = 0; i < keys.length-1; i++) {
if(i%2){
this.setProp("shapeStrokePattern", "dashed");
this.line(O.add($V(points[i])), O.add($V(points[i+1])))
}else{
this.setProp("shapeStrokePattern", "solid");
this.line(O.add($V(points[i])), O.add($V(points[i+1])))
}}
});
Circles can be drawn with circle(center, radius, filled?). center should be a vector, radius a float, and filled? is a boolean representing whether the inside of the circle should be opaque or transparent. In the example below, points are added at the center of each circle, but note how the circle with filled?=true obfuscates the point beneath it.
Text successfully copied to clipboard!
new PrairieDraw("circle", function () {
this.setUnits(6, 6);
var O = $V([0, 0]);
this.point(O.add($V([-1.25, 0])));
this.circle(O.add($V([-1.25, 0])), 1, false);
this.point(O.add($V([-1.25, 0])));
this.circle(O.add($V([1.25, 0])), 1, true);
});
Rectangles can be drawn with rectangle(width, height, center, angle, filled?). width and height should be floats, center a vector, angle a float, and filled? a boolean. angle represents the angle (in radians, counter-clockwise is positive) at which the rectangle should be rotated, while filled? represents whether the inside of the rectangle should be opaque or transparent. More information on the filled option can be found in the circles section.
Text successfully copied to clipboard!
new PrairieDraw("rectangle", function () {
this.setUnits(12, 6);
var O = $V([0, 0]);
this.rectangle(3, 4, O.add($V([-2.5, 0])), Math.PI/4, false);
this.rectangle(4, 2, O.add($V([2.5, 0])), 0, true);
});
Images can be drawn with drawImage(src, position, anchor, width). src is a string representing the path to the image file, position and anchor are vectors, and width a float.
Note: While using relative paths is technically possible, it is not recommended. In practice, it has been observed that depending on the environment (dev vs. production), the same relative path can be resolved to different places. Thus it is recommended to use absolute paths.
Text successfully copied to clipboard!
new PrairieDraw("image", function () {
this.setUnits(12, 6);
var O = $V([0, 0]);
this.drawImage('/about/documentation/alma.jpg', O, $V([0, 0]), 12)
});
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.
Text successfully copied to clipboard!
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]));
});
The action of future drawing commands can be transformed by
translate(pos), rotate(ang), or
scale(factor), where both pos and
factor are vectors.
Text successfully copied to clipboard!
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.
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.
Text successfully copied to clipboard!
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);
});
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.
Text successfully copied to clipboard!
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:
Text successfully copied to clipboard!
<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>
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.
Text successfully copied to clipboard!
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:
Text successfully copied to clipboard!
<button onclick="animPD.toggleOption('drawLabels');">Toggle labels</button>
<button onclick="animPD.toggleAnim();">Toggle animation</button>
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.
Text successfully copied to clipboard!
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 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.
Text successfully copied to clipboard!
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");
});