Reference Pages

    Scroll back to top

    Animation Documentation

    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.

    Initialization

    The following import brings in the PrairieDrawCanvas component, which is the core element used to render interactive drawings and animations on the page. Make sure to import the element at the top of the page before using it. If you get an error like `XXX is not defined`, this is typically what it's refering to. To import an element, use the following syntax.

    Text successfully copied to clipboard!

        
            import PrairieDrawCanvas from "../../components/PrairieDrawCanvas.astro"
        
    

    Canvas

    Once imported, you can drop a PrairieDrawCanvas into your layout and link it to a named drawing function, making it easy to embed visual content directly alongside your text. The id changes on the function you provide it with.

    Text successfully copied to clipboard!

        
            <PrairieDrawCanvas id="<canvasId>" width="xxx" height="yyy">
                        
                        </PrairieDrawCanvas>
        
    

    Method 1: Script

    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
                            });
        
    

    Method 2: Javascript in Astro

    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.

    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).

    Vectors

    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));
            });
        
    

    Elements

    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.

    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);
            });
        
    

    Points

    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

    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

    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

    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

    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);
            });
        
    

    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.

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

    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.

    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

    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)
              });
        
    

    Buttons

    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>
        
    

    Sliders

    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:

    1. Add an option in your PrairieDraw code:

      Text successfully copied to clipboard!

          
              myAnim = new PrairieDrawAnim("my-canvas", function(t) {
                      &nbsp;&nbsp;&nbsp;this.addOption("radius", 1); // default value
                      });
          
      
    2. Create a slider tied to that option. The range defines the minimum and maximum values the slider can travel between, while the step sets the interval between possible values. For example, a step of 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">
          
      
    3. When the slider moves, PrairieDraw updates the option and the scene changes instantly.

    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))">
        
    

    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.

    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

    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>
        
    

    Sequenced animations

    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.

    Numerical differentiation

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

    Resizing

    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();
            })
        
    

    Working Examples

    Moving Vectors

    Change:

    Derivatives

    Show:

    Sliders

    Show:
    Coordinate lines:

    Radius:

    \(r = \; \) 4

    Azimuth:

    \(\theta = \; \) 45\(^\circ\)

    Elevation:

    \(z = \; \) 4

    More Sliders

    Stage: 0