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.
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.
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:
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 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); }); |
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])); }); |
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.
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 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:
|
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>
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>
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 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"); }); |