Scroll back to top
Animations are a powerful way to make ideas more concrete. They can reveal how systems evolve over time, and connect abstract concepts to a clear visual sense of the idea. The goal is not just to explain, but to invite exploration and make ideas feel alive. This page serves as a guide covers the a range of visual elements one can draw and animate with, along with instructions on how to set them up.
Potential solutions to common problems when creating pages are on the troubleshooting page. If a problem is still unsolvable after consulting the troubleshooting page, please submit an official issue on the github. For those seeking a more interactive 3D environment, consider using Plotly. Visit the Plotly Open Source Graphing Library for Python guide to learn more.
Text successfully copied to clipboard!
import PrairieDrawCanvas from "../../components/PrairieDrawCanvas.astro"
Text successfully copied to clipboard!
<PrairieDrawCanvas id="<canvasId>" width="xxx" height="yyy">
</PrairieDrawCanvas>
On this website, JavaScript files are used to create and control animations. These files are stored in the public directory and are linked to indiviudal pages using script tags. To populate a PrairieDraw canvas with items and drawings, you'll need to create a .js file in the public folder and reference it in your corresponding .astro file. This JavaScript file should contain the function calls that define and animate your PrairieDraw elements. Use this method if you plan on having a lot of functions for one page.
Text successfully copied to clipboard!
public
|--> <course_abbreviation>
|--> <page_1>
|--> <script_name>.js
src
|--> components
|--> layouts
|--> pages
|--> <course_abbreviation>
|--> <page_1>.astro
Wrapper: This block is required and should be the first thing in your .js script. It ensures that your JavaScript only runs after the page has fully loaded. All your PrairieDraw functions must go inside this block:
Text successfully copied to clipboard!
$(document).ready(function(){
})
Script Tag: To link to the JavaScript file. (Place this at the very bottom of your .astro
file)
Text successfully copied to clipboard!
<script src="/about/testing/canvases.js" is:inline></script>
To create a custom animation or drawing for a specific canvas, you declare a function using the PrairieDrawAnim constructor.
This function defines how your drawing evolves over time and is directly tie to the canvas via its id.
Text successfully copied to clipboard!
<variableName> = new PrairieDrawAnim("<canvasId>", function() {
// Logic goes here
});
Once your define a canvas within the template, you can embed the corresponding Javascript code for interacting with that canvas directly within the same component using a script tag. Use this method for a short and quick method to access the animations.
Text successfully copied to clipboard!
<script type="application/javascript" is:inline>
<variableName> = new PrairieDraw("canvasid>", function () {
});
</script>
Creating a canvas and assigning a variable to it allows to redraw the canvas for resizing later.
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)
.
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!
vectors = 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!
circleArrow = 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!
points = 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!
Text = 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!
anchors = 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.point(xnn);
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!
line = 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!
circle = 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!
rectangle = 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);
});
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!
properties = 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!
transformation = 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.
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!
images = new PrairieDraw("image", function () {
this.setUnits(12, 6);
var O = $V([0, 0]);
this.drawImage('/about/documentation/alma.jpg', O, $V([0, 0]), 12)
});
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>
Like buttons, sliders work by changing options in your PrairieDraw code -- the difference is that sliders let you adjust those option values continuously in real time. To use them:
Text successfully copied to clipboard!
myAnim = new PrairieDrawAnim("my-canvas", function(t) {
this.addOption("radius", 1); // default value
});
0.5
means the slider moves in half-unit increments.
Text successfully copied to clipboard!
<input type="range" min="0" max="5" value="1" step="0.5"
class="data-input:my-canvas:radius">
Make sure your canvasId
matches in the
PrairieDrawCanvas
, the constructor, and the slider classes.
Radius:
Text successfully copied to clipboard!
myDraw = new PrairieDraw("circleCanvas", function () {
this.addOption("radius", 2);
this.setUnits(6, 6);
const r = Number(this.getOption("radius"));
const O = $V([0, 0]);
const P = $V([r, 0]);
this.circle(O, r);
this.arrow(O, P, "position");
});
//Button code
<input type="range" min="1" max="5" value="2" step="1"
oninput="myDraw.setOption('radius', Number(this.value))">
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!
save = 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);
});
Animations work in a similar manner compared with regular drawings. The main difference is that you use PrairieDrawAnim
object instead of a PrairieDraw
object.
In addition, the drawing function receives a parameter t
which represents the simulation time (in seconds) at which the scene 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>
If you want animations that goes through several motions in order you can use state = this.sequence(states, transTimes, holdTimes, t)
.
The states
is a list of "snapshots" of your animations. Each snapshot is a dictionary that says what all the variables should be at that point.
transTimes
is a list of how long it takes to move from one state to the next.
holdTimes
is a list of how long to stay in each stay before moving on.
transTimes
and holdTimes
should have the same number of items as states
.
For example, transTimes[i]
is how long it takes to get from state i
to state i+1
, and holdTimes[i]
is how long you stay in state i
before starting that move.
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.
The function data = this.numDiff(fcn, t)
is used to calculate derivatives at a specific time.
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");
});
When resizing the browser window, PrairieDraw canvases may not display correct. To resolve this, simply add the following code block to the bottom of your script. You may choose to redraw multiple canvases at the same time.
Text successfully copied to clipboard!
$(window).on("resize", function() {
(variableName1).redraw();
(variableName2).redraw();
})
Change:
Show:
Show: | |
Coordinate lines: |
Radius:
Azimuth:
Elevation: